Wire 4CM routing: JackAudioClient + boot orchestration + set_routing_mode()
- Add JackAudioClient class to src/system/audio.py — real-time JACK client owning ports and a process callback that streams audio through the DSP pipeline. Handles mono (1ch I/O) and stereo 4CM (2ch I/O). - Wire PedalApp.boot(): read audio.mode from config, set pipeline routing_mode (mono/4cm), create and start JackAudioClient with matching channel count. - Add PedalApp.set_routing_mode(mode) — hot-switch between mono and stereo_4cm at runtime: updates pipeline.routing_mode, restarts JACK client with new channel count via set_channel_count(), re-connects JACK ports. - Wire shutdown: stop JackAudioClient before MIDI/footswitch. - Export JackAudioClient from src.system.__init__. - 53 tests pass (22 integration + 31 audio).
This commit is contained in:
@@ -22,7 +22,7 @@ from typing import Optional
|
||||
import yaml
|
||||
|
||||
# ── Subsystem imports ─────────────────────────────────────────────────────────
|
||||
from src.system.audio import AudioConfig, AudioSystem, _jack_is_running
|
||||
from src.system.audio import AudioConfig, AudioSystem, JackAudioClient, _jack_is_running
|
||||
from src.dsp.pipeline import AudioPipeline
|
||||
from src.dsp.nam_host import NAMHost
|
||||
from src.dsp.ir_loader import IRLoader
|
||||
@@ -77,6 +77,7 @@ class PedalApp:
|
||||
# ── Subsystem references (set during boot) ─────────────────────
|
||||
self.audio_config: AudioConfig | None = None
|
||||
self.audio_system: AudioSystem | None = None
|
||||
self.jack_audio: JackAudioClient | None = None
|
||||
self.nam_host: NAMHost | None = None
|
||||
self.ir_loader: IRLoader | None = None
|
||||
self.pipeline: AudioPipeline | None = None
|
||||
@@ -110,9 +111,11 @@ class PedalApp:
|
||||
try:
|
||||
# ── 1. Audio config + system ──────────────────────────
|
||||
acfg = self._config["audio"]
|
||||
audio_mode = acfg.get("mode", "mono")
|
||||
self.audio_config = AudioConfig(
|
||||
hat_type=acfg.get("hat_type", "audioinjector"),
|
||||
profile=acfg.get("profile", "standard"),
|
||||
mode=audio_mode,
|
||||
input_device=acfg.get("input_device", "hw:0,0"),
|
||||
output_device=acfg.get("output_device", "hw:0,0"),
|
||||
jack_enabled=acfg.get("jack_enabled", True),
|
||||
@@ -130,6 +133,32 @@ class PedalApp:
|
||||
self.ir_loader = IRLoader()
|
||||
self.pipeline = AudioPipeline(nam_host=self.nam_host, ir_loader=self.ir_loader)
|
||||
|
||||
# Set pipeline routing from config mode
|
||||
if audio_mode == "stereo_4cm":
|
||||
self.pipeline.routing_mode = "4cm"
|
||||
logger.info("Pipeline routing set to 4CM mode")
|
||||
else:
|
||||
self.pipeline.routing_mode = "mono"
|
||||
|
||||
# ── 2b. JACK audio client (capture → pipeline → playback) ──
|
||||
if self.audio_config.jack_enabled:
|
||||
is_stereo = audio_mode == "stereo_4cm"
|
||||
channels = 2 if is_stereo else 1
|
||||
self.jack_audio = JackAudioClient(
|
||||
pipeline=self.pipeline,
|
||||
client_name="pi-multifx",
|
||||
input_channels=channels,
|
||||
output_channels=channels,
|
||||
)
|
||||
self.jack_audio.start()
|
||||
if not self.jack_audio._active:
|
||||
logger.warning("JACK audio client failed to start — running without real-time audio I/O")
|
||||
else:
|
||||
logger.info(
|
||||
"JACK audio client started: %s mode, %d ch I/O",
|
||||
audio_mode, channels,
|
||||
)
|
||||
|
||||
# ── 3. Preset manager ─────────────────────────────────
|
||||
pcfg = self._config["presets"]
|
||||
self.presets = PresetManager(
|
||||
@@ -390,6 +419,43 @@ class PedalApp:
|
||||
self.leds.set_pixel(0, (0, 64, 255)) # Blue = active
|
||||
logger.info("Active: '%s' (bank=%d, pg=%d)", preset.name, preset.bank, preset.program)
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# Audio routing mode switching
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
def set_routing_mode(self, mode: str) -> None:
|
||||
"""Hot-switch between mono and stereo_4cm routing at runtime.
|
||||
|
||||
Updates the pipeline routing mode, then restarts the JACK
|
||||
audio client with the appropriate channel count.
|
||||
|
||||
Args:
|
||||
mode: ``"mono"`` or ``"stereo_4cm"``.
|
||||
|
||||
Raises:
|
||||
ValueError: If *mode* is not recognised.
|
||||
"""
|
||||
if mode not in ("mono", "stereo_4cm"):
|
||||
raise ValueError(f"set_routing_mode: unknown mode {mode!r}")
|
||||
|
||||
if self.audio_config is not None:
|
||||
self.audio_config.mode = mode
|
||||
|
||||
if self.pipeline is not None:
|
||||
self.pipeline.routing_mode = "4cm" if mode == "stereo_4cm" else "mono"
|
||||
logger.info("Pipeline routing mode switched to %s", self.pipeline.routing_mode)
|
||||
|
||||
# Restart JACK client with new channel count
|
||||
if self.jack_audio is not None:
|
||||
channels = 2 if mode == "stereo_4cm" else 1
|
||||
self.jack_audio.set_channel_count(channels, channels)
|
||||
|
||||
# Re-connect JACK ports for the new routing
|
||||
if self.audio_system is not None and self.audio_config is not None:
|
||||
self.audio_system.connect_fx_ports()
|
||||
|
||||
logger.info("Routing mode switched to %s", mode)
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# Display sync
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
@@ -500,7 +566,15 @@ class PedalApp:
|
||||
logger.info("═══════ Shutting down ═══════")
|
||||
shutdown_start = time.monotonic()
|
||||
|
||||
# 1. Stop accepting input (MIDI + footswitch)
|
||||
# 1. Stop JACK audio client (before JACK server)
|
||||
if self.jack_audio:
|
||||
try:
|
||||
self.jack_audio.stop()
|
||||
logger.debug("JACK audio client stopped")
|
||||
except Exception as e:
|
||||
logger.warning("JACK audio client stop error: %s", e)
|
||||
|
||||
# 2. Stop accepting input (MIDI + footswitch)
|
||||
if self.midi:
|
||||
try:
|
||||
self.midi.stop()
|
||||
|
||||
Reference in New Issue
Block a user