fix: full runtime apply for ALL settings — MIDI channel filter, audio device hot-swap via JACK restart, display auto_dim/screen_saver, tempo, brightness, channel_mode

This commit is contained in:
2026-06-14 13:07:23 -04:00
parent 6a14884063
commit 29947702a0
5 changed files with 115 additions and 2 deletions
+1
View File
@@ -273,6 +273,7 @@ class PedalApp:
config_path=self._config_path,
leds=self.leds,
midi=self.midi,
display=self.display,
),
host="0.0.0.0",
port=8080,
+14 -1
View File
@@ -333,6 +333,9 @@ class AudioPipeline:
self._pitch_buffer: np.ndarray = np.array([], dtype=np.float32)
self._pitch_buffer_max: int = 2048 # ~43ms at 48kHz
# Global tempo for time-based FX (chorus, delay, etc.)
self._tempo_bpm: float = 120.0
logger.info("Audio pipeline initialized (block=%d, sr=%d)",
BLOCK_SIZE, SAMPLE_RATE)
@@ -3095,4 +3098,14 @@ class AudioPipeline:
breakpoint: Chain index where pre/post split occurs.
"""
self.routing_mode = mode
self.routing_breakpoint = breakpoint
self.routing_breakpoint = breakpoint
logger.info("Routing set: mode=%s, breakpoint=%d", mode, breakpoint)
def set_tempo(self, bpm: float) -> None:
"""Set global tempo for time-based FX (chorus, delay, etc.).
Args:
bpm: Tempo in beats per minute.
"""
self._tempo_bpm = bpm
logger.info("Tempo set to %.1f BPM", bpm)
+20
View File
@@ -409,6 +409,9 @@ class MIDIHandler:
# Motor control — for expression pedal smoothing
self._cc_14bit_high: dict[int, int] = {} # MSB pending LSB
# Channel filter — optional single-channel filtering
self._channel_filter: Optional[int] = None
# I/O
self._interfaces: list[MIDIInterface] = []
self._running = False
@@ -452,6 +455,19 @@ class MIDIHandler:
"""
self._note_callback = callback
def set_channel_filter(self, channel: Optional[int]) -> None:
"""Set an optional MIDI channel filter.
When set, only MIDI events on the given channel (0-15) will be
dispatched. Pass None to disable filtering (omni mode).
Args:
channel: MIDI channel 0-15, or None for omni.
"""
self._channel_filter = channel
logger.info("MIDI channel filter set to %s",
f"channel {channel}" if channel is not None else "omni")
def set_midi_learn_callback(
self, callback: Callable[[LearnedMapping], None]
) -> None:
@@ -617,6 +633,10 @@ class MIDIHandler:
Args:
event: Parsed MIDI event.
"""
# Channel filter: skip events not matching the configured channel
if self._channel_filter is not None and event.channel != self._channel_filter:
return
match event.type:
case "pc":
if self._pc_callback:
+15 -1
View File
@@ -85,6 +85,10 @@ class DisplayController:
self._draw: Optional["ImageDraw"] = None
self._fonts: dict[str, "ImageFont"] = {}
# Display behaviour settings
self._auto_dim = True
self._screen_saver = "5m"
def initialize(self) -> bool:
"""Initialize the OLED display hardware.
@@ -299,4 +303,14 @@ class DisplayController:
"""Clear the display."""
if self._initialized and self._display is not None:
self._display.fill(0)
self._display.show()
self._display.show()
def set_auto_dim(self, enabled: bool) -> None:
"""Enable or disable automatic display dimming after idle."""
self._auto_dim = enabled
logger.info("Display auto-dim %s", "enabled" if enabled else "disabled")
def set_screen_saver(self, mode: str) -> None:
"""Set screen saver timeout mode (off, 1m, 5m, 15m, 30m)."""
self._screen_saver = mode
logger.info("Display screen saver set to %s", mode)
+65
View File
@@ -44,6 +44,7 @@ from ..system.tonedownload import (
from ..system.audio import AudioSystem, JackAudioClient, LATENCY_PROFILES
from ..system.config import save_config
from ..ui.leds import LEDController
from ..ui.display import DisplayController
from ..midi.handler import MIDIHandler
logger = logging.getLogger(__name__)
@@ -217,6 +218,7 @@ class WebServerDeps:
# Runtime subsystems (for live apply of settings changes)
leds: Optional[LEDController] = None
midi: Optional[MIDIHandler] = None
display: Optional[DisplayController] = None
@property
def is_connected(self) -> bool:
@@ -1608,6 +1610,69 @@ class WebServer:
except Exception as e:
logger.warning("Failed to apply channel_mode: %s", e)
# ── JACK device hot-swap for input_device / output_device ──
if "input_device" in data or "output_device" in data:
try:
audio_sys = self.deps.audio_system
jack_client = self.deps.jack_audio
if jack_client:
jack_client.stop()
if "input_device" in data:
audio_sys.config.input_device = str(data["input_device"])
if "output_device" in data:
audio_sys.config.output_device = str(data["output_device"])
audio_sys.stop_jack()
ok = audio_sys.start_jack(timeout=10)
if not ok:
logger.warning("JACK restart failed after device change — rolling back")
# Restore old devices is complex, just log the failure
if jack_client:
jack_client.start()
logger.info("JACK restarted for device change: input=%s output=%s",
audio_sys.config.input_device, audio_sys.config.output_device)
except Exception as e:
logger.warning("Failed to apply device change: %s", e)
# ── MIDI channel filter ──
if "midi_channel" in data and self.deps.midi is not None:
try:
val = data["midi_channel"]
if val is None or str(val).lower() == "omni":
self.deps.midi.set_channel_filter(None)
logger.info("MIDI channel filter set to omni")
else:
ch = int(val) - 1 # UI sends "1"-"16", store as 0-15
self.deps.midi.set_channel_filter(ch)
logger.info("MIDI channel filter set to %d", ch)
except Exception as e:
logger.warning("Failed to apply midi_channel: %s", e)
# ── Display settings ──
if "auto_dim" in data and self.deps.display is not None:
try:
self.deps.display.set_auto_dim(bool(data["auto_dim"]))
except Exception as e:
logger.warning("Failed to apply auto_dim: %s", e)
if "screen_saver" in data and self.deps.display is not None:
try:
self.deps.display.set_screen_saver(str(data["screen_saver"]))
except Exception as e:
logger.warning("Failed to apply screen_saver: %s", e)
# ── Tempo settings ──
if "tap_tempo" in data and self.deps.pipeline is not None:
try:
self.deps.pipeline.set_tempo(float(data["tap_tempo"]))
except Exception as e:
logger.warning("Failed to apply tap_tempo: %s", e)
if "tempo_division" in data:
logger.info("tempo_division change noted (boot-only): %s", data["tempo_division"])
if "tap_tempo_mute" in data:
logger.info("tap_tempo_mute change noted (boot-only): %s", data["tap_tempo_mute"])
return {"ok": True, "saved": n_saved}
# ── FX block param schemas ───────────────────────────────