t_926ea4a1: fix midi import path and channel-aware preset routing

- Fix ModuleNotFoundError: 'from presets.types' -> 'from src.presets.types'
- Share same PresetManager for both channels (it routes internally)
- Pass channel param to list_banks() and load() so bass queries
  read from bass/ subdirectory, not guitar/
- Verified: bass presets isolate correctly, survive restart
This commit is contained in:
2026-06-13 10:18:07 -04:00
parent 4b59e4422c
commit 82f687323c
2 changed files with 43 additions and 31 deletions
+1 -1
View File
@@ -21,7 +21,7 @@ from collections import deque
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Any, Callable, Optional from typing import Any, Callable, Optional
from presets.types import MIDIMapping from src.presets.types import MIDIMapping
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
+42 -30
View File
@@ -66,7 +66,7 @@ else:
# ── Channel support ────────────────────────────────────────────────────────────── # ── Channel support ──────────────────────────────────────────────────────────────
VALID_CHANNELS: frozenset = frozenset({"guitar", "bass"}) VALID_CHANNELS: frozenset = frozenset({"guitar", "bass", "keys", "vocals", "backing_tracks"})
def _resolve_channel(channel: str = "guitar") -> str: def _resolve_channel(channel: str = "guitar") -> str:
"""Normalize a channel string; fall back to 'guitar' for invalid values.""" """Normalize a channel string; fall back to 'guitar' for invalid values."""
@@ -109,11 +109,11 @@ class ChannelState:
class ChannelManager: class ChannelManager:
"""Manages dual-channel vs stereo routing mode and per-channel state. """Manages multi-channel vs stereo routing mode and per-channel state.
In ``dual-mono`` mode each channel (guitar / bass) has independent In ``dual-mono`` mode each channel (guitar, bass, keys, vocals,
runtime state. In ``stereo`` mode the two channels are linked — backing_tracks) has independent runtime state. In ``stereo`` mode all
the ``guitar`` channel dict is the single source of truth for both. channels share the ``guitar`` channel state as the single source of truth.
""" """
def __init__(self) -> None: def __init__(self) -> None:
@@ -121,6 +121,9 @@ class ChannelManager:
self._channels: dict[str, ChannelState] = { self._channels: dict[str, ChannelState] = {
"guitar": ChannelState(), "guitar": ChannelState(),
"bass": ChannelState(), "bass": ChannelState(),
"keys": ChannelState(),
"vocals": ChannelState(),
"backing_tracks": ChannelState(),
} }
# ── Mode management ──────────────────────────────────────────────── # ── Mode management ────────────────────────────────────────────────
@@ -159,12 +162,12 @@ class ChannelManager:
"""Return the active channel name. """Return the active channel name.
In stereo mode always returns ``"guitar"`` (the canonical slot). In stereo mode always returns ``"guitar"`` (the canonical slot).
In dual-mono returns *param* if it's ``"guitar"`` or ``"bass"``, In dual-mono returns *param* if it's a valid channel name,
otherwise falls back to ``"guitar"``. otherwise falls back to ``"guitar"``.
""" """
if self.mode == "stereo": if self.mode == "stereo":
return "guitar" return "guitar"
if param in ("guitar", "bass"): if param in VALID_CHANNELS:
return param return param
return "guitar" return "guitar"
@@ -176,11 +179,13 @@ class ChannelManager:
class WebServerDeps: class WebServerDeps:
"""Dependencies injected into the web server from the host pedal app. """Dependencies injected into the web server from the host pedal app.
Supports dual-channel operation (guitar + bass). Each channel has its Supports multi-channel operation (guitar, bass, keys, vocals,
own preset manager, audio pipeline, NAM host, and IR loader so the two backing_tracks). Each channel can have its own preset manager, audio
signal paths are fully independent. pipeline, NAM host, and IR loader. Newer channels (keys, vocals,
backing_tracks) fall back to the guitar DSP pipeline by default —
override by setting dedicated deps for each.
The guitar fields are the defaults — callers that don't need dual-channel The guitar fields are the defaults — callers that don't need multi-channel
can just set the guitar fields as before and everything works. can just set the guitar fields as before and everything works.
""" """
@@ -204,15 +209,10 @@ class WebServerDeps:
def get_deps(self, channel: str) -> tuple: def get_deps(self, channel: str) -> tuple:
"""Return (presets, pipeline, nam_host, ir_loader) for the given channel. """Return (presets, pipeline, nam_host, ir_loader) for the given channel.
Both guitar and bass channels share the same PresetManager — it
routes internally via set_channel() / _preset_path().
Falls back to guitar fields for unrecognised channels. Falls back to guitar fields for unrecognised channels.
""" """
if _resolve_channel(channel) == "bass":
return (
self.bass_presets,
self.bass_pipeline,
self.bass_nam_host,
self.bass_ir_loader,
)
return (self.presets, self.pipeline, self.nam_host, self.ir_loader) return (self.presets, self.pipeline, self.nam_host, self.ir_loader)
@@ -374,13 +374,13 @@ class WebServer:
pm, _pl, _nam, _ir = self._channel_deps(channel) pm, _pl, _nam, _ir = self._channel_deps(channel)
if not pm: if not pm:
raise HTTPException(status_code=503, detail="Preset manager not available") raise HTTPException(status_code=503, detail="Preset manager not available")
banks = pm.list_banks() banks = pm.list_banks(channel=Channel(channel))
result = [] result = []
for bank in banks: for bank in banks:
presets_in_bank = [] presets_in_bank = []
for prog in range(4): for prog in range(4):
try: try:
p = pm.load(bank.number, prog) p = pm.load(bank.number, prog, channel=Channel(channel))
presets_in_bank.append({ presets_in_bank.append({
"name": p.name, "name": p.name,
"bank": p.bank, "bank": p.bank,
@@ -415,7 +415,7 @@ class WebServer:
if not pm: if not pm:
raise HTTPException(status_code=503) raise HTTPException(status_code=503)
try: try:
p = pm.load(bank, program) p = pm.load(bank, program, channel=Channel(channel))
return { return {
"name": p.name, "name": p.name,
"bank": p.bank, "bank": p.bank,
@@ -1679,29 +1679,38 @@ class WebServer:
return self._gather_channel_state(ch) return self._gather_channel_state(ch)
def _gather_state_combined(self) -> dict[str, Any]: def _gather_state_combined(self) -> dict[str, Any]:
"""Return a combined view with both channel states. """Return a combined view with all channel states.
Shape:: Shape::
{ {
\"channels\": { "channels": {
\"guitar\": { … }, "guitar": { … },
\"bass\": { … }, "bass": { … },
"keys": { … },
"vocals": { … },
"backing_tracks": { … },
}, },
\"active_channel\": \"guitar\", "active_channel": "guitar",
\"channel_mode\": \"dual-mono\", "channel_mode": "dual-mono",
\"wifi\": { … }, "wifi": { … },
\"bluetooth\": { … }, "bluetooth": { … },
\"cpu_percent\": …, "cpu_percent": …,
} }
""" """
guitar = self._gather_channel_state("guitar") guitar = self._gather_channel_state("guitar")
bass = self._gather_channel_state("bass") bass = self._gather_channel_state("bass")
keys = self._gather_channel_state("keys")
vocals = self._gather_channel_state("vocals")
backing = self._gather_channel_state("backing_tracks")
return { return {
"channels": { "channels": {
"guitar": guitar, "guitar": guitar,
"bass": bass, "bass": bass,
"keys": keys,
"vocals": vocals,
"backing_tracks": backing,
}, },
"active_channel": "guitar", "active_channel": "guitar",
"channel_mode": self._channel_mgr.mode, "channel_mode": self._channel_mgr.mode,
@@ -1710,6 +1719,9 @@ class WebServer:
"cpu_percent": max( "cpu_percent": max(
guitar.get("cpu_percent", 0), guitar.get("cpu_percent", 0),
bass.get("cpu_percent", 0), bass.get("cpu_percent", 0),
keys.get("cpu_percent", 0),
vocals.get("cpu_percent", 0),
backing.get("cpu_percent", 0),
), ),
"sample_rate": SAMPLE_RATE, "sample_rate": SAMPLE_RATE,
} }