- 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
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
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.
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.
Auth was blocking all POST/PUT operations with 401.
Disabled per user request. PIN generation kept (harmless).
Frontend reverted to no-auth API helpers.
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
- **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.
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.
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
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.
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
- 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
- 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
- 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
- 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
Changed DEFAULT_PORT to 80 so the UI is accessible at
http://pedal.local/ directly (no port needed).
Killed orphaned old port-80 instance that was running stale code.
All static asset references now include ?v={{ version }} to force
browser to reload on deploy instead of serving cached versions.
Increment TEMPLATE_VERSION in server.py on each deploy.
Two bugs:
1. **Deadlock in set_audio_profile() (root cause of timeout)**
jack_client.stop() was called BEFORE killing the JACK server.
jack.Client.deactivate() is a synchronous call that blocks until
the JACK RT callback thread acknowledges. When ALSA reconfigures
(period/rate change), the RT thread can stall and deactivate()
hangs permanently. Reversed order: kill JACK server first (via
killall jackd) to force-kill the RT thread, then clean up the
Python client. Same fix applied to device hot-swap path.
2. **Pipeline crash after period change**
_CombFilter.buf and _AllpassFilter.buf were fixed-size arrays
at BLOCK_SIZE (256). If JACK restarts with a different period
(e.g. 512), self.buf[:] = block throws a shape mismatch in the
RT callback -> audio dies permanently. Made internal buffers
dynamically resize to match input block size.
- Add FastNAMHost.set_block_size() — reloads current model with
new block_size, keeping the engine in sync with JACK's period
- Wire into POST /api/audio/profile so NAM engine restarts with
correct frame count when switching between standard/low/stable
- Prevents sample-alignment drift when JACK period changes but
NAM engine stays at old block_size (was causing passthrough)
- 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
Reordered UI_DIST_DIR candidates so pi-multifx-pedal-ui/dist (the
correct Helix-style pedal UI) is checked first. The old priority
put src/web/ui-dist first, which allowed wrong-build deploys to
mask the real UI.
Also add UI_DIST_DIR env var to systemd service for explicit path.
Adds snapshot system to Helix Stadium:
Backend:
- Snapshot data model (8 slots per preset, captures block bypass
+ params + master volume)
- Snapshot API endpoints: list, save, recall, rename, delete
- Integrated into preset serialization/deserialization
- Tracks current_snapshot in state / WebSocket broadcasts
Frontend:
- Camera icon in header bar with current snapshot number
- SnapshotPanel as slide-up overlay
- 8 snapshot slots in a horizontal row
- Tap to recall filled snapshot, tap empty slot to save
- Press-and-hold (600ms) to save current state
- Inline name editing on tap
- Discard Edits toggle
- Green flash indicator on save
- Delete button for active snapshot
- New Preset button: finds first empty slot, saves with default name
- Looper/Recorder tab: Record/Stop via arecord backend, list captures, play back
- Rig: shows loaded IR name more clearly + quick-link buttons to browse IRs/Models
- Backend: capture/start, capture/stop, captures API endpoints with file serve
- Status bar: CPU load + sample rate from /api/state, TUNER badge shown immediately
- Rig: optimistic toggle updates (bypass/tuner respond instantly), prominent tuner card
- FX Chain: loads real block params from /api/block-params/{fx_type}, Add Block picker with all FX types
- Record: replaced fake multitrack with real Signal Monitor (live I/O VU, clip detect, level history)
- Backend: added _gather_system_stats() returning cpu_percent and sample_rate
- Added input_level and output_level to /api/state (scaled 0-100)
- React RigScreen now reads real levels from state instead of random walk
- When no audio flows, VU meters show flat (0) instead of fake animation
- New dark UI with 7 tabs: Rig, FX Chain, Models, IRs, Presets, Settings, Record
- All screens wired to live API endpoints
- Real-time state via WebSocket + polling fallback
- Models screen: list/load/unload/upload NAM files + Tone3000 search/install
- IRs screen: list/load/unload/upload IR files + Tone3000 search/install
- Presets screen: browse banks, activate, save, delete
- Settings screen: WiFi scan/connect/hotspot, Bluetooth/MIDI, 4CM routing
- Rig screen: live state, volume, bypass, tuner, signal chain
- FX Chain screen: block params, per-block bypass
- Serves at / instead of /ui
- Built with Vite, ~71KB gzipped
Add UI_DIST_DIR auto-detection and /ui static mount in the
FastAPI server so the built React frontend is served alongside
the main dashboard.
- UI_DIST_DIR env var override
- Auto-detect common project layouts (frontend-react/dist,
pi-multifx-pedal-ui/dist)
- Mount with html=True for SPA client-side routing
Pipeline:
- process() dispatches to _process_mono() or _process_4cm() based on routing_mode
- _process_4cm() splits chain at routing_breakpoint: pre blocks on ch0, post on ch1
- _process_single_block() extracted for reuse in both mono and 4cm paths
- routing_mode/routing_breakpoint load from preset via load_preset()
- set_routing() for runtime configuration
- Properties: routing_mode (mono|4cm), routing_breakpoint with validation
Web server:
- GET /api/routing — current routing mode and breakpoint
- POST /api/routing — set routing mode/breakpoint, persist to current preset, WS broadcast
- _gather_state() includes routing_mode and routing_breakpoint
Web UI:
- Settings page: 4CM toggle + breakpoint slider with routing description
- Dashboard: routing badge (4CM/Mono) with breakpoint info
- app.js: WebSocket handler, updateRoutingUI(), toggle4cm(), set4cmBreakpoint()
- style.css: .badge, .badge-4cm, .badge-mono, .routing-status, .routing-desc
Tests: mock pipeline fixture updated with routing_mode/routing_breakpoint