From 82f687323cf9ad14d101328ce25c7dbe62038bc0 Mon Sep 17 00:00:00 2001 From: Shawn Date: Sat, 13 Jun 2026 10:18:07 -0400 Subject: [PATCH] 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 --- src/midi/handler.py | 2 +- src/web/server.py | 72 ++++++++++++++++++++++++++------------------- 2 files changed, 43 insertions(+), 31 deletions(-) diff --git a/src/midi/handler.py b/src/midi/handler.py index 6900c65..a6e9117 100644 --- a/src/midi/handler.py +++ b/src/midi/handler.py @@ -21,7 +21,7 @@ from collections import deque from dataclasses import dataclass, field from typing import Any, Callable, Optional -from presets.types import MIDIMapping +from src.presets.types import MIDIMapping logger = logging.getLogger(__name__) diff --git a/src/web/server.py b/src/web/server.py index 0ad4eea..269bc29 100644 --- a/src/web/server.py +++ b/src/web/server.py @@ -66,7 +66,7 @@ else: # ── 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: """Normalize a channel string; fall back to 'guitar' for invalid values.""" @@ -109,11 +109,11 @@ class ChannelState: 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 - runtime state. In ``stereo`` mode the two channels are linked — - the ``guitar`` channel dict is the single source of truth for both. + In ``dual-mono`` mode each channel (guitar, bass, keys, vocals, + backing_tracks) has independent runtime state. In ``stereo`` mode all + channels share the ``guitar`` channel state as the single source of truth. """ def __init__(self) -> None: @@ -121,6 +121,9 @@ class ChannelManager: self._channels: dict[str, ChannelState] = { "guitar": ChannelState(), "bass": ChannelState(), + "keys": ChannelState(), + "vocals": ChannelState(), + "backing_tracks": ChannelState(), } # ── Mode management ──────────────────────────────────────────────── @@ -159,12 +162,12 @@ class ChannelManager: """Return the active channel name. 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"``. """ if self.mode == "stereo": return "guitar" - if param in ("guitar", "bass"): + if param in VALID_CHANNELS: return param return "guitar" @@ -176,11 +179,13 @@ class ChannelManager: class WebServerDeps: """Dependencies injected into the web server from the host pedal app. - Supports dual-channel operation (guitar + bass). Each channel has its - own preset manager, audio pipeline, NAM host, and IR loader so the two - signal paths are fully independent. + Supports multi-channel operation (guitar, bass, keys, vocals, + backing_tracks). Each channel can have its own preset manager, audio + 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. """ @@ -204,15 +209,10 @@ class WebServerDeps: def get_deps(self, channel: str) -> tuple: """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. """ - 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) @@ -374,13 +374,13 @@ class WebServer: pm, _pl, _nam, _ir = self._channel_deps(channel) if not pm: raise HTTPException(status_code=503, detail="Preset manager not available") - banks = pm.list_banks() + banks = pm.list_banks(channel=Channel(channel)) result = [] for bank in banks: presets_in_bank = [] for prog in range(4): try: - p = pm.load(bank.number, prog) + p = pm.load(bank.number, prog, channel=Channel(channel)) presets_in_bank.append({ "name": p.name, "bank": p.bank, @@ -415,7 +415,7 @@ class WebServer: if not pm: raise HTTPException(status_code=503) try: - p = pm.load(bank, program) + p = pm.load(bank, program, channel=Channel(channel)) return { "name": p.name, "bank": p.bank, @@ -1679,29 +1679,38 @@ class WebServer: return self._gather_channel_state(ch) 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:: { - \"channels\": { - \"guitar\": { … }, - \"bass\": { … }, + "channels": { + "guitar": { … }, + "bass": { … }, + "keys": { … }, + "vocals": { … }, + "backing_tracks": { … }, }, - \"active_channel\": \"guitar\", - \"channel_mode\": \"dual-mono\", - \"wifi\": { … }, - \"bluetooth\": { … }, - \"cpu_percent\": …, + "active_channel": "guitar", + "channel_mode": "dual-mono", + "wifi": { … }, + "bluetooth": { … }, + "cpu_percent": …, } """ guitar = self._gather_channel_state("guitar") 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 { "channels": { "guitar": guitar, "bass": bass, + "keys": keys, + "vocals": vocals, + "backing_tracks": backing, }, "active_channel": "guitar", "channel_mode": self._channel_mgr.mode, @@ -1710,6 +1719,9 @@ class WebServer: "cpu_percent": max( guitar.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, }