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:
@@ -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
@@ -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)
|
||||
@@ -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
@@ -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)
|
||||
@@ -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 ───────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user