145 Commits

Author SHA1 Message Date
shawn 3f7257aa23 RT performance tuning: IRQ affinity, chrt, xrun tracking, reference doc
CI / test (push) Has been cancelled
- 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
2026-06-20 15:58:59 -04:00
shawn 04931fd738 fix: NAM engine stability and hum — post-NAM DC blocker + HPF, warm-before-kill subprocess swap, non-blocking pipe I/O, sample rate sync, arch detection
- 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
2026-06-20 15:58:59 -04:00
shawn acdff5eb9b feat: signal-based test suite with jack_rec capture — 9/9 passing
CI / test (push) Has been cancelled
2026-06-19 06:32:10 +00:00
shawn 95bb62dc21 feat: comprehensive test plan (24 sections, ~200 tests) + automated API test runner (121 tests, all passing)
CI / test (push) Has been cancelled
2026-06-18 23:42:16 +00:00
shawn c026bc72b7 fix: preserve block_id across save/load and API save_preset/save_snapshot endpoints
CI / test (push) Has been cancelled
2026-06-18 23:22:51 +00:00
shawn 47bb535b08 fix: multiple bug fixes found during comprehensive test plan sweep
CI / test (push) Has been cancelled
- Fix #5: Preset state lost on service restart — call restore_state()
  in PresetManager.__init__() so last active preset is tracked after boot
- Fix #14: Block bypass/toggle WS sync — add WebSocket connection
  in SPA with block_toggled handler that calls loadState()
- Fix #15: IR file naming — add display_name property to IRFile
  dataclass that strips common IR prefixes/suffixes
- Fix: block_id regeneration on every preset load — pass block_id
  from stored JSON data to FXBlock constructor in _preset_from_dict
  (both preset chain and snapshot chain)
2026-06-18 19:14:01 -04:00
shawn d152ecd1fc docs: expand test plan with channel cycling, upload, tuner tests, system info, ethernet details
CI / test (push) Has been cancelled
2026-06-18 18:59:22 -04:00
shawn 507a486a42 docs: expand audio section with dedicated sample rate, buffer, and combined tests
CI / test (push) Has been cancelled
2026-06-18 18:56:22 -04:00
shawn 6f24a940ba docs: comprehensive manual test plan for mobile QA
CI / test (push) Has been cancelled
2026-06-18 18:37:04 -04:00
shawn bb9c835f98 fix: _jack_is_operational() now creates throwaway JACK client instead of jack_lsp; standard profile period=512
CI / test (push) Has been cancelled
_jack_is_operational(): jack_lsp is unreliable on JACK 1.9.22 (returns 0
exit even when dead, and socket path varies). Instead, try creating a
real JACK.Client('_healthcheck') which is the definitive test — exactly
what jack_client.start() does. If the client connects, JACK is truly
operational.

Standard profile: period changed from 256 to 512. The NAM engine takes
2-5ms per inference at 256/48k (5.33ms window = 93% CPU). Any jitter
causes xruns (audible pops at ~1Hz). At 512/48k (10.67ms window), the
same inference takes 40-50% CPU with 5ms headroom — far more stable.
2026-06-18 18:32:55 -04:00
shawn 0cb044fb5e chore: periodic gc.collect() on HTTP thread after disabling GC for RT
CI / test (push) Has been cancelled
2026-06-18 18:28:18 -04:00
shawn d7889c6f23 fix: _process_4cm handles 1D mono input gracefully
CI / test (push) Has been cancelled
When routing mode is '4cm' but JACK provides mono audio (1D array),
the _process_4cm method crashes with IndexError on audio_in[0,:].
Treat mono input as guitar channel with silent return channel.

This happens when JACK is configured with 1 capture port (mono mode)
but routing_mode is set to '4cm' — e.g. after a settings sweep that
toggles routing_mode without changing JACK port config.
2026-06-18 18:25:12 -04:00
shawn 4039ee963d fix: restore missing import argparse
CI / test (push) Has been cancelled
2026-06-18 18:24:00 -04:00
shawn 6495facb02 fix: disable Python garbage collector to prevent 1s periodic audio pops
CI / test (push) Has been cancelled
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).
2026-06-18 18:23:01 -04:00
shawn 017a74a414 fix: kill alsa_in in stop_jack; verify jackd matches config device
CI / test (push) Has been cancelled
Two-part fix for the rogue jackd race:

1. stop_jack() now kills alsa_in (from bt-a2dp bridge). When JACK
   dies, alsa_in auto-spawns a rogue jackd -T -ndefault -d alsa
   that satisfies _jack_is_operational() but is the wrong server.

2. _jack_is_operational() now paired with _jack_matches_config()
   which verifies the running jackd's /proc/cmdline contains the
   expected ALSA device string (e.g. hw:USB,0). A rogue jackd
   without this won't pass the readiness check.

Also added _jack_matches_config() helper that scans /proc/*/cmdline.
2026-06-18 18:10:40 -04:00
shawn f1c6f64dc8 fix: add 1.5s stabilization check in start_jack to catch flaky JACK startup
CI / test (push) Has been cancelled
JACK can briefly become operational (jack_lsp returns ports) during
startup then crash when the ALSA device fails. The initial readiness
check catches this window and returns True prematurely. Add a 0.5s
stabilization window after first success — if JACK drops, continue
the wait loop instead of returning.

Without this, profile changes succeed in the API but leave JACK in a
zombie state: process exists with SHM infrastructure but can't serve
clients.
2026-06-18 18:08:24 -04:00
shawn 32990f9744 fix: _jack_is_operational() must check jack_lsp stdout, not exit code
CI / test (push) Has been cancelled
jack_lsp on JACK 1.9.22 always exits 0 even when JACK is dead —
it prints error to stderr but returns success. Check for non-empty
stdout (port names) instead, which only appears when JACK is truly
operational.
2026-06-18 18:04:06 -04:00
shawn 9a8c59d19c fix: replace broken jack_default_0_0 socket check with jack_lsp for JACK 1.9.22 compatibility
CI / test (push) Has been cancelled
start_jack() was checking for /dev/shm/jack_default_0_0 which never
exists on JACK 1.9.22 (used on RPi/DietPi). The check made start_jack()
always timeout and return False. Boot sequence ignored the return value
(luck), but the profile change handler checked it via 'if not ok: rollback',
causing all buffer/rate changes to fail silently every other time.

Replace with _jack_is_operational() using jack_lsp which tests actual
JACK IPC and works correctly on all JACK versions.
2026-06-18 17:59:30 -04:00
shawn 4b694cc5f8 fix: replace DC-blocking HPF with 60Hz biquad notch filter (hum fix)
CI / test (push) Has been cancelled
2026-06-18 17:54:23 -04:00
shawn 4ad50e3588 fix: narrow SHM cleanup in profile handler to client semaphore only; add DC-blocking HPF to pipeline input (60Hz hum fix)
CI / test (push) Has been cancelled
2026-06-18 17:46:41 -04:00
shawn 92c6bb9587 fix: stop bt-a2dp-jack during profile changes to prevent JACK race
CI / test (push) Has been cancelled
The bt-a2dp-jack systemd unit runs an independent JACK instance for
Bluetooth audio. When stop_jack() kills jackd, bt-a2dp-jack restarts
it within 5 seconds with default parameters. By the time start_jack()
runs in the profile handler, _jack_is_running() already returns True
for the bt-a2dp JACK, causing start_jack() to return immediately
without configuring our period/rate. The pedal's JACK client then
fails with server_error (0x21) because the running JACK doesn't match.

Fix:
- stop_jack() temporarily stops bt-a2dp-jack before killing jackd
- Profile handler restarts bt-a2dp-jack after JACK is configured
- Rollback path also restarts bt-a2dp-jack
2026-06-18 17:29:52 -04:00
shawn 61886858f6 fix: rollback must restore AudioConfig period/rate + LATENCY_PROFILES
CI / test (push) Has been cancelled
The profile change rollback only restored audio_sys.config.profile to
old_key, but my earlier fix had started syncing period/rate to AudioConfig
BEFORE start_jack(). This meant the rollback's start_jack() still used
the NEW period/rate (via latency_profile) because self.period was never
reverted — the same failing config was retried and failed again.

Fix: capture old_period, old_rate, and old LATENCY_PROFILES['custom']
entry before applying the new profile, then restore all of them in the
rollback path.
2026-06-18 17:26:52 -04:00
shawn f3cdf5e13b fix: multiple issues preventing NAM from surviving rate/period changes
CI / test (push) Has been cancelled
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)
2026-06-18 17:21:04 -04:00
shawn c0c2538c80 fix: verify JACK is operational before returning from start_jack()
CI / test (push) Has been cancelled
start_jack() only checked _jack_is_running() (pgrep -x jackd) which
returns True even when jackd exists but failed to initialize the ALSA
device — the process starts but hangs without creating SHM segments,
making jack_lsp/jack_wait fail. The profile handler then continues as
if JACK is running, jack_client.start() fails with server_error, the
pi-multifx ports never get created, and NAM CPU drops to 0%.

Fix: also verify /dev/shm/jack_default_0_0 (the JACK control socket)
exists before declaring JACK started. This socket is only created after
the ALSA device is successfully initialized, so a zombie jackd with a
bad period/rate combination won't pass the check.
2026-06-18 17:17:33 -04:00
shawn ce82207d03 fix: clean stale JACK SHM before client creation in profile handler
CI / test (push) Has been cancelled
jack.Client('pi-multifx') fails with server_error (0x21) when stale
jack_sem.0_default_pi-multifx segments remain in /dev/shm from a
previous client instance. This commonly happens after a profile change
(buffer+rate) because:

1. stop_jack() kills jackd and cleans SHM
2. start_jack() starts new JACK server
3. Old process's JackAudioClient.__del__ (garbage collection) recreates
   the pi-multifx semaphore in /dev/shm
4. jack_client.start() → jack.Client('pi-multifx') finds stale SHM
   and fails with server_error → no pi-multifx ports → NAM never
   called → 0% nam_cpu → 'nam is dying'

Fix:
- Clean /dev/shm/jack* in profile handler right before jack_client.start()
- JackAudioClient.start() also retries once with SHM cleanup on
  server_error, so other callers are also protected.
2026-06-18 17:14:35 -04:00
shawn de5723f138 fix: update preset NAM block path when loading a model globally
CI / test (push) Has been cancelled
/api/models/load now also updates the current preset's NAM Amp
block(s) nam_model_path, so the UI block reflects the loaded model.
2026-06-17 23:21:14 -04:00
shawn aa0bcf35fe fix: remove auth middleware — single-user pedal doesn't need it
CI / test (push) Has been cancelled
Auth was blocking all POST/PUT operations with 401.
Disabled per user request. PIN generation kept (harmless).
Frontend reverted to no-auth API helpers.
2026-06-17 23:06:46 -04:00
shawn 95029c6c8c fix: wire auth PIN to frontend, clean up dead constants
CI / test (push) Has been cancelled
- Frontend now reads auth_pin from /api/state and includes X-Pedal-Auth on all non-GET requests
- Fixed test imports after removing module-level BLOCK_SIZE/SAMPLE_RATE constants
- Removed rogue test_issue_2_3.py from tracking
2026-06-17 22:59:01 -04:00
shawn a3ec86b814 chore: remove rogue patch files from tracking
CI / test (push) Has been cancelled
2026-06-17 22:51:30 -04:00
shawn ad5b369a10 fix: wire auth PIN to frontend — 401 on NAM select and all POST/PUT calls
CI / test (push) Has been cancelled
Root cause: auth middleware (1603bfe) blocked all non-GET /api/ endpoints
requiring X-Pedal-Auth header, but the PIN was never served to the frontend.

Fix:
- Added auth_pin to /api/state response (safe GET endpoint, no auth required)
- Frontend stores pin from state on initial load
- apiPost/apiPut/apiDelete/apiUpload now include X-Pedal-Auth header
2026-06-17 22:51:23 -04:00
shawn 2558306e78 fix: code review batch 2 — multi-channel, thread safety, DSP reset, MIDI, hotspot, NAM, docs/CI, code quality
CI / test (push) Has been cancelled
2026-06-17 22:38:30 -04:00
shawn c65e4816c4 fix: add sample_rate and block_size properties to AudioPipeline
_gather_channel_state() was accessing pl.sample_rate but AudioPipeline
only had private _sample_rate. Added public properties so the /api/state
endpoint doesn't crash with AttributeError.
2026-06-17 21:36:33 -04:00
shawn 1603bfeea0 fix: add first-boot auth PIN generation and persistence
- _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)
2026-06-17 21:32:51 -04:00
shawn 42e357c4a1 fix: API cleanup — dedup endpoints, rename channel_mode, fix brightness scaling, device switch rollback
- **4.1:** Consolidated duplicate PUT/POST /api/volume and POST /api/bypass/toggle
  into single handlers via stacked FastAPI decorators
- **4.2:** Renamed 'channel_mode' → 'audio_routing_mode' in settings KEY_MAP
  and runtime apply, decoupling it from instrument routing
- **4.4:** Changed brightness API from ambiguous 'brightness' (1-10/0-1 heuristic)
  to explicit 'brightness_percent' (0-100, unambiguous)
- **4.5:** Added config snapshot/rollback on JACK device switch failure.
  Restores original devices + persists restored config. Sets needs_reboot
  flag if even the rollback restart fails. Surfaces needs_reboot in state.
2026-06-17 21:31:19 -04:00
shawn 3c8d5e4c1c fix: move blocking subprocess calls off the async event loop
Wrap long-running synchronous calls in run_in_executor to prevent
the web UI from freezing during:

- wifi_scan (~20s scan)
- hotspot_enable / hotspot_disable (~60s each)
- bluetooth_scan (~12s scan)
- start_jack (~15s JACK restart) in set_audio_profile and save_settings

Each blocking call now runs in a thread executor, keeping the FastAPI
event loop responsive for other requests.

Added: functools.partial import, asyncio.get_event_loop() in two handlers.
2026-06-17 21:19:14 -04:00
shawn 4a6e7900ce fix: NAM CPU display now responds to bypass
The CPU label in the UI was showing psutil.cpu_percent(interval=None)
which returns total system CPU — barely moves and doesn't change when
the NAM model is bypassed.

Fix:
- Added _calc_nam_cpu() that computes NAM engine CPU from the engine's
  own per-block inference timing (avg_inference_ms) vs the JACK callback
  interval. When the NAM block is bypassed in the chain, returns 0.
- Added 'nam_cpu' field to /api/state response
- Status bar shows NAM CPU % next to VU meters (drops to 0 on bypass)
- Settings CPU row shows NAM CPU with system CPU in dim text beside it
- Changed psutil.cpu_percent(interval=None) → interval=0.2 for a real
  system CPU reading
2026-06-17 17:53:36 -04:00
shawn fab8564d09 feat: add VU meter to status bar
Shows input (green) and output (amber) level bars in the status bar,
updated every 2s from /api/state. Scales factor 300x to make small
signals visible, capped at 100%.
2026-06-17 17:48:44 -04:00
shawn 1619b2e90e fix: clean stale JACK SHM segments on stop_jack() and at boot
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.
2026-06-17 17:39:01 -04:00
shawn ebdf98076a fix: skip JACK client deactivate() if server is already dead
jack.Client.deactivate() is a synchronous protocol call that hangs
indefinitely on a broken server socket. In the audio profile change
handler, stop_jack() kills the jackd process first, then tries
jack_client.stop() which calls deactivate() on the now-dead socket.

Fix: check if jackd is still running via pidof before calling
deactivate(). If the server is already gone, skip directly to
close() which just closes the local file descriptor (safe and fast).

This was causing the POST /api/audio/profile endpoint to hang for
30+ seconds on every buffer change.
2026-06-17 17:28:47 -04:00
shawn 8aaa9abadb fix: also sync AudioConfig.period/rate before JACK restart
Two-part fix for NAM audio dying after buffer change:

1. NAM block size sync moved BEFORE jack_client.start() so the NAM
   subprocess is reloaded before the process callback fires.

2. AudioConfig.period/rate must be updated BEFORE start_jack() reads
   latency_profile. Otherwise the stale boot-time override period
   stomps on the LATENCY_PROFILES entry set by the POST handler,
   causing JACK to start at the wrong period while NAM is at the
   new period — permanent JACK/NAM block size mismatch.

Both bugs together: user changes buffer, JACK restarts with wrong
period due to stale override, NAM subprocess gets mismatched pipe
bytes, returns DI (direct input) for every subsequent callback.
2026-06-17 17:17:36 -04:00
shawn 6008776e3c 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
2026-06-17 17:09:38 -04:00
shawn 1b2747abb1 fix: audio params timeout - increase API timeout for JACK restart
The setAudioParams call restarts JACK which takes up to 10 seconds.
The default API timeout was 6s, causing the request to abort and
leaving JACK in a broken state.

- Changed setAudioParams to use API._fetch directly with timeout:15000
- Also calls loadAudioParams() after success to refresh dropdown values
2026-06-16 20:02:57 -04:00
shawn 177d412c84 tone3000: IR downloads, model detail, arch filter, metadata
- NAM/IR tab toggle in Downloads overlay
- Architecture filter (All/Nano/Feather/Standard) - filters via
  Supabase architecture_version column
- Model detail overlay: tap a search result to see full description,
  gear info, author, pack, architecture, thumbnail before downloading
- IR search + install with .meta.json metadata saving
- Backend: search_models/get_trending_models accept arch param
- Backend: tone_gear field added to model search response
2026-06-16 19:59:38 -04:00
shawn 5cdc751312 tone3000 metadata capture: save .meta.json alongside downloaded models
- UI now sends full Tone3000 metadata on install (id, author,
  architecture, pack_name, description, thumbnail, etc.)
- Search results show size, architecture, and pack name
- Install endpoint saves a .meta.json file next to each .nam with:
  author, architecture, description, pack, thumbnail, download date
- Enables offline browsing of model provenance
2026-06-16 19:49:23 -04:00
shawn a37e381c4e backing tracks: add source selector + bluealsa in device list
- Audio devices endpoint now includes Bluetooth Audio (bluealsa) and
  other software PCM devices from aplay -L
- Added GET/POST /api/backing-source endpoints with JACK port routing
- Backing Track Source dropdown in Audio section (None, Bluetooth
  Audio, or any JACK capture port found)
- Selecting a source routes it into fx_in:input_2 via jack_connect
- Also supports bt_audio:capture_1/2 from existing JACK bridge
2026-06-16 19:20:30 -04:00
shawn 334e035cde ch output: per-channel output routing dropdowns
- Added GET/POST /api/channel-output endpoints for per-channel
  output routing (playback_1 / playback_2 / both)
- Added output dropdowns to CH 1 and CH 2 cards in Audio Interface
- Added setChOutput() and loadChOutput() JS functions
- Updated connect_fx_ports to read per-channel output config
2026-06-16 19:18:30 -04:00
shawn e324d9007b fix: loadAudioDevices now correctly accesses devs.devices array
Also simplified device auto-selection to match by trailing device
number (e.g. 'hw:USB,0' → finds option ending in ',0').
2026-06-16 19:15:51 -04:00
shawn 427c77cbe7 fix: loadAudioDevices accesses .devices from response object
API.listAudioDevices() returns {devices: [...]} not the array
directly. The response also uses card numbers hw:X,Y format.
2026-06-16 19:14:51 -04:00
shawn 93610e4d98 audio interface: separate input/output device selectors
- Added Output Device dropdown alongside Input Device
- loadAudioDevices() now populates both dropdowns from the API and
  selects current values
- Separated setAudioInputDevice / setAudioOutputDevice functions
  (each calls API with only its respective field)
- Added input_device / output_device to GET /api/audio/profile response
- Removed the old combined setAudioDevice function
2026-06-16 19:13:25 -04:00
shawn 1530aa266f audio: replace profiles with direct sample rate + buffer size controls
- Removed audio profile grid and preset cards
- Added two dropdowns: Sample Rate (44.1/48/96/192 kHz) and Buffer
  Size (64-2048 frames)
- New loadAudioParams() reads current rate+period from API
- New setAudioParams() POSTs {period, rate} directly on change
- Cleaned up unused CSS (.audio-profile-grid, .ap-card, .ap-key, etc.)
- Removed unused audioProfiles variable
2026-06-16 19:11:21 -04:00