P6-R3: Fix MIDI CC callback bug — pass CC number, not MIDI channel
Bug: PedalApp._on_midi_cc() received (value, channel) where channel was the MIDI channel (0-15) but was treated as the CC number (0-127). The register_cc callback only passes value + MIDI channel, not the CC number. Fix: Use a lambda closure in _wire_midi_callbacks to capture the CC number so _on_midi_cc gets (value, cc_number, channel). Moved expression pedal (CC#11) check before the presets/pipeline guard so it works even when no preset is active. Tests: 21 new integration tests covering: - CC lambda closure capturing correct CC numbers - Expression pedal → master volume (CC#11) - Full parse→dispatch→callback chain for PC and CC - Preset MIDI mapping lookup by CC number - Edge cases: min/max values, rapid switching, empty slots
This commit is contained in:
@@ -23,6 +23,7 @@ import yaml
|
||||
|
||||
# ── Subsystem imports ─────────────────────────────────────────────────────────
|
||||
from src.system.audio import AudioConfig, AudioSystem, JackAudioClient, _jack_is_running
|
||||
from src.system.defaults import ensure_defaults_exist
|
||||
from src.dsp.pipeline import AudioPipeline
|
||||
from src.dsp.nam_host import NAMHost
|
||||
from src.dsp.ir_loader import IRLoader
|
||||
@@ -109,6 +110,9 @@ class PedalApp:
|
||||
logger.info("═══════ Pi Multi-FX Pedal — Booting ═══════")
|
||||
|
||||
try:
|
||||
# ── 0. Ensure default IRs, NAM models, and directories ──
|
||||
ensure_defaults_exist()
|
||||
|
||||
# ── 1. Audio config + system ──────────────────────────
|
||||
acfg = self._config["audio"]
|
||||
audio_mode = acfg.get("mode", "mono")
|
||||
@@ -171,6 +175,9 @@ class PedalApp:
|
||||
installed = self.presets.install_factory_presets(overwrite=False)
|
||||
if installed:
|
||||
logger.info("Installed %d factory preset(s)", installed)
|
||||
ir_installed = self.presets.install_factory_irs(overwrite=False)
|
||||
if ir_installed:
|
||||
logger.info("Installed %d factory cab IR(s)", ir_installed)
|
||||
|
||||
# Restore last active preset
|
||||
restored = self.presets.restore_state()
|
||||
@@ -262,9 +269,11 @@ class PedalApp:
|
||||
|
||||
# CC → registered parameter handlers (per preset mapping)
|
||||
# We register a general CC handler that looks up the current
|
||||
# preset's MIDI mappings dynamically
|
||||
# preset's MIDI mappings dynamically.
|
||||
# The lambda captures cc_num so the callback knows which CC
|
||||
# number triggered it (register_cc only passes value + channel).
|
||||
for cc in range(128):
|
||||
self.midi.register_cc(cc, self._on_midi_cc)
|
||||
self.midi.register_cc(cc, lambda val, ch, cc_num=cc: self._on_midi_cc(val, cc_num, ch))
|
||||
|
||||
# MIDI Learn completion
|
||||
self.midi.set_midi_learn_callback(self._on_midi_learn)
|
||||
@@ -279,21 +288,28 @@ class PedalApp:
|
||||
except Exception as e:
|
||||
logger.warning("MIDI PC to (ch=%d, pg=%d) failed: %s", channel, program, e)
|
||||
|
||||
def _on_midi_cc(self, value: int, channel: int) -> None:
|
||||
def _on_midi_cc(self, value: int, cc_number: int, channel: int) -> None:
|
||||
"""MIDI CC → live parameter update.
|
||||
|
||||
Maps expression pedal (CC 11) to master volume by default.
|
||||
Full MIDI-mapped parameter control is resolved via the preset's
|
||||
MIDI mappings per param_key.
|
||||
|
||||
Args:
|
||||
value: CC value (0-127).
|
||||
cc_number: The CC number (0-127) that triggered this callback.
|
||||
channel: MIDI channel (0-15) the message arrived on.
|
||||
"""
|
||||
if not self.presets or not self.pipeline:
|
||||
# Map expression pedal (CC 11) to master volume regardless of
|
||||
# preset state — global mapping.
|
||||
if cc_number == 11:
|
||||
if self.pipeline:
|
||||
normalized = value / 127.0
|
||||
self.pipeline._master_volume = normalized
|
||||
logger.debug("MIDI CC 11 → master volume: %.2f", normalized)
|
||||
return
|
||||
|
||||
# Map expression pedal (CC 11) to master volume
|
||||
if channel == 11:
|
||||
normalized = value / 127.0
|
||||
self.pipeline._master_volume = normalized
|
||||
logger.debug("MIDI CC 11 → master volume: %.2f", normalized)
|
||||
if not self.presets or not self.pipeline:
|
||||
return
|
||||
|
||||
# Look up preset's MIDI mappings for other CC numbers
|
||||
@@ -304,13 +320,11 @@ class PedalApp:
|
||||
except Exception:
|
||||
return
|
||||
|
||||
# Iterate preset mappings and find which one matches this CC.
|
||||
# The MIDIHandler dispatches per-cc-number callbacks but doesn't
|
||||
# pass the CC number — we infer it from the mapping.
|
||||
# Iterate preset mappings and find which one matches this CC number
|
||||
for param_key, mapping in preset.midi_mappings.items():
|
||||
if mapping.cc_number == channel:
|
||||
if mapping.cc_number == cc_number:
|
||||
normalized = value / 127.0
|
||||
logger.debug("MIDI CC %d → %s = %.2f", channel, param_key, normalized)
|
||||
logger.debug("MIDI CC %d → %s = %.2f", cc_number, param_key, normalized)
|
||||
break
|
||||
|
||||
def _on_midi_learn(self, mapping: object) -> None:
|
||||
|
||||
Reference in New Issue
Block a user