- Add USB audio IRQ affinity pinning to core 3 in main.py
- Add enable_xrun_tracking() to AudioSystem for kernel-level diagnostics
- Wrap Python process with chrt -f 80 in systemd service template
- Add LimitSIGPENDING=128 for signal queue depth
- Create scripts/rt-tune.sh — comprehensive RT tuning startup script
(IRQ affinity, CPU governor, C-states, ALSA limits, xrun_debug)
- Create docs/rt-performance-tuning.md — reference doc with all
tuning knobs, measurement tools, and systematic procedure
Targets: <12ms RT latency (8ms ideal), zero xruns, CPU <40% at 512/48k
- Add first-order DC blocker (R=0.999) after NAM processing to kill subsonic offset
- Add 80Hz Butterworth HPF after NAM to catch residual 60/120Hz hum
- Recompute HPF coefficients on sample rate change in set_audio_profile()
- Warm-before-kill: spawn new C++ subprocess before stopping old one (no gap)
- Add background reader thread for non-blocking stdout consumption
- Reuse last known output frame if engine is slow (keeps stream aligned)
- Pass sample_rate to NAMEngineProcess and FastNAMHost constructors
- Forward sample_rate in server.py profile change and main.py init
- Read actual architecture from .nam files instead of hardcoding 'LSTM'
- Add threading.Lock to FastNAMHost for safe engine ref swaps
Default GC threshold (700 allocs) triggers ~every 1.4s in the
real-time audio callback due to ~500 numpy allocs/sec. Each GC
pause (10-50ms) exceeds the 5.3ms callback window at 256/48k,
producing audible pops at ~1Hz intervals.
Reference counting handles 99% of object cleanup — GC only handles
cyclic garbage. Disabling it is safe for a process that exits cleanly
(os reclaims everything).
Root causes of 'changing bit rate / sample rate kills NAM':
1. Zombie jackd from stale second Python process — when stop_jack()
kills jackd, the other process's auto-restart respawns jackd with
OLD config between stop and start. start_jack() sees a running
jackd and returns True immediately without verifying it's actually
accepting connections.
2. Zombie Python process from incomplete service restart — two
main.py instances running, each with its own AudioSystem and
JackAudioClient. Profile change kills one JACK but the other
respawns it. jack_client.start() then fails with server_error
because the stale SHM conflicts.
3. start_jack() returned True for a dead jackd — process exists but
JackServer::Open failed with -1 (bad period/rate for device).
No SHM socket was created, but the process check passed.
Fixes:
- _ensure_singleton(): kill any other main.py process at boot
- start_jack(): verify jack_default_0_0 SHM socket exists before
returning True; kill zombie process if socket is missing
- Profile handler: clean SHM right before jack_client.start()
- JackAudioClient.start(): retry once with SHM cleanup on
server_error (0x21)
- _ensure_auth_pin() generates random 6-digit PIN on first boot
- PIN stored in config.yaml under web.auth_pin for consistency with
the existing config persistence chain
- PIN logged at INFO on first generation so user sees it on display
- Removes stale src/web/auth.py (superseded by config.yaml approach)
When JACK is killed forcefully (killall -9), the /dev/shm/jack_sem.*
shared memory segments and /dev/shm/jack_db-0/ directory are left
behind. The next start_jack() call finds these stale segments and
fails silently — no audio output, JACK process may appear briefly
then crash.
Fix:
- AudioSystem.stop_jack(): after killing jackd, also rm -rf
/dev/shm/jack* to clean up stale SHM.
- main.py boot sequence: clean SHM before first start_jack(), so
the pedal recovers from crashes that happened before this fix.
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
- FastNAMHost now initialized with block_size from audio config's
latency profile (e.g. 512 for stable, 256 for standard)
- Prevents sample-alignment mismatch between JACK period and
NAM engine internal buffer size at boot
- GET /api/audio/profiles — list available profiles with params
- GET /api/audio/profile — current profile + jack_running + xrun_count
- POST /api/audio/profile {profile: 'standard'|'low'} — switch profile,
restart JACK server + JACK audio client atomically
- Adds audio_system + jack_audio refs to WebServerDeps
- Rollback on JACK restart failure
restore_state() loads the last state from disk but doesn't call
activate() on the pipeline, so the NAM model was never loaded
on boot and no audio passed through. Added select() call with
the restored bank/program values so the pipeline loads the
preset chain including the NAM model.
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
- 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).