fix: move NAM block size sync before JACK client restart

The profile change handler was calling nam_host.set_block_size() AFTER
jack_client.start(), creating a race where the JACK callback fired with
the new buffer size while the old NAM subprocess was still running at
the old block_size. Pipe byte counts desynced => NAM returned passthrough.

+ also: auto_connect disable during restart, connect_fx_ports reorder,
  Tone3000 gear/sort/pagination params, NAM engine switch API endpoints
This commit is contained in:
2026-06-17 17:09:38 -04:00
parent 1b2747abb1
commit 6008776e3c
12 changed files with 646 additions and 190 deletions
+31 -3
View File
@@ -25,7 +25,7 @@ import yaml
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_engine_host import FastNAMHost
from src.dsp.nam_router import NAMEngineRouter
from src.dsp.ir_loader import IRLoader
from src.presets.manager import PresetManager
from src.presets.types import Channel, MIDIMapping, Preset
@@ -69,6 +69,7 @@ class PedalApp:
def __init__(self, config_path: Path = DEFAULT_CONFIG_PATH) -> None:
self._config = load_config(config_path)
self._config_path = config_path
# ── Runtime state ──────────────────────────────────────────────
self._running = False
@@ -79,7 +80,7 @@ class PedalApp:
self.audio_config: AudioConfig | None = None
self.audio_system: AudioSystem | None = None
self.jack_audio: JackAudioClient | None = None
self.nam_host: FastNAMHost | None = None
self.nam_host: NAMEngineRouter | None = None
self.ir_loader: IRLoader | None = None
self.pipeline: AudioPipeline | None = None
self.presets: PresetManager | None = None
@@ -124,6 +125,8 @@ class PedalApp:
output_device=acfg.get("output_device", "hw:0,0"),
jack_enabled=acfg.get("jack_enabled", True),
auto_connect=acfg.get("auto_connect", True),
period=acfg.get("period"),
rate=acfg.get("rate"),
)
self.audio_system = AudioSystem(self.audio_config)
self.audio_system.setup_i2s(reboot_hint=True)
@@ -134,7 +137,7 @@ class PedalApp:
# ── 2. DSP pipeline (NAM + IR + FX chain) ────────────
block_size = self.audio_config.latency_profile["period"]
self.nam_host = FastNAMHost(block_size=block_size)
self.nam_host = NAMEngineRouter(block_size=block_size)
self.ir_loader = IRLoader()
self.pipeline = AudioPipeline(nam_host=self.nam_host, ir_loader=self.ir_loader)
self.nam_host.warm_up()
@@ -251,6 +254,8 @@ class PedalApp:
ir_loader=self.ir_loader,
audio_system=self.audio_system,
jack_audio=self.jack_audio,
config=self._config,
config_path=self._config_path,
),
host="0.0.0.0",
port=80,
@@ -681,6 +686,29 @@ def main() -> int:
Returns exit code 0 on clean shutdown, 1 on boot failure.
"""
# Lock process memory early to prevent page faults in RT callback
try:
import ctypes
libc = ctypes.CDLL('libc.so.6')
# MCL_CURRENT | MCL_FUTURE = 1 | 2 = 3
if libc.mlockall(3) == 0:
logger.info("mlockall() OK — process memory locked")
else:
logger.warning("mlockall() failed — check LimitMEMLOCK in systemd unit")
except Exception as exc:
logger.warning("mlockall() not available: %s", exc)
# Set CPU governor to performance for stable RT audio
try:
for c in range(4): # RPi 4B has 4 cores
gov_path = f"/sys/devices/system/cpu/cpu{c}/cpufreq/scaling_governor"
if os.path.exists(gov_path):
with open(gov_path, "w") as f:
f.write("performance")
logger.info("CPU%d governor set to performance", c)
except Exception as exc:
logger.warning("Could not set CPU governor (non-root?): %s", exc)
import argparse
parser = argparse.ArgumentParser(