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)
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.
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.
- 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
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
_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.
- _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)
- **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
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%.
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.
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.
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
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
- 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
- Moved .settings-nav out of .overlay-body so it stays pinned below
the header when scrolling
- Buttons bumped to 12px font, 8px 16px padding, 38px min-height
- Added gap, padding, background and border-bottom to nav strip
Adds a scrollable row of section tabs at the top of the settings
overlay: Network · Audio · Interface · Bluetooth · System.
Each tab scrolls smoothly to its section and highlights as active.
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.
- Changed both tab bar ⚙ and status bar ⚙ buttons to open the
inline Settings overlay (openOverlay('Settings')) with data loads
instead of navigating to /settings (old Bootstrap page)
- The overlay was already fully built — HTML, CSS, JS loaders were all
present, just never wired up
Both the tab bar ⚙ Settings button and the status bar ⚙ button
in the v2 UI now redirect to /settings instead of opening an
inline overlay. The full settings page has the left sidebar nav
with category views.
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.
The React tab bar's Settings button previously showed a minimal inline
component. Now it navigates to /settings which has the full settings
page with left sidebar navigation and category views.
Replaced horizontal tab bar with a left sidebar navigation
that feels like separate pages:
- Left nav with icons + labels (Connection, Audio, 4CM Routing,
Network, Bluetooth, About)
- Active category highlighted in accent color
- Each category has its own URL via hash (#connection, #audio, etc.)
- Back/forward browser navigation via popstate handler
- Stacked layout on mobile (sidebar becomes scrollable row)
- FXBlock was missing block_id field (and __post_init__ auto-generator)
causing AttributeError on /settings and /api/presets endpoints.
- Rebuilt Helix Vite UI from ui/ source (index-jhtO2NOz.js,
index-D3aC6klh.css). Replaces older dist files.
Commit faa7bdb was on a local branch that got orphaned during
git reset --hard origin/main. Cherry-picked back its UI changes:
- New Vite build: index-DI0gBBIR.css + index-LMuhdreF.js
- Updated SVG icons for effects (amplifier, analyzer, dial, etc.)
- prefers-reduced-motion in index.html
- Updated bank/presets icons
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.
- 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
- 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)