145 Commits

Author SHA1 Message Date
shawn 214f3c13d7 feat: Footswitch Modes — Stomp/Preset/Snapshot/Combo with scribble strips
Implements multi-mode footswitch system for the Helix Stadium-inspired pedal UI:

- Footswitch mode context provider with 4 modes: stomp, preset, snapshot, combo
- ModeSelector in status bar (collapsed cycling mode for mobile)
- Stomp mode: footswitches toggle individual blocks on/off, scribble strips show block name + status
- Preset mode: footswitches show Bank Up/Down + preset selection, scribble strips show preset name + number
- Snapshot mode: footswitches recall 8 snapshots, scribble strips show snapshot name
- Combo mode: first 4 stomp + bank nav + preset select + global bypass
- ScribbleStrip LCD component with LED indicator and double-tap support
- Double-tap gesture hook for secondary actions (tuner, snapshot save, etc.)
- FootswitchBar container with 8 capacitive-touch footswitch buttons and LED rings
- usePedalState hook (WebSocket + API polling, ported from frontend-react/App.jsx)
- Dark pedal theme CSS (Helix-inspired) with responsive mobile layout
- Full App.tsx rewrite with status bar, footswitch bar, tab bar, and all screens
2026-06-12 19:22:50 -04:00
shawn b6dff3c0f3 feat: Snapshots — 8 per preset save/recall
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
2026-06-12 19:20:56 -04:00
shawn a1f35a56e3 feat: new preset, looper recorder, IR display improvements
- 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
2026-06-12 17:24:14 -04:00
shawn 2a30592ab3 fix: all dummy data replaced with real API data
- 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
2026-06-12 17:16:03 -04:00
shawn 7498ad55fc fix: real VU meter levels from pipeline instead of random animation
- 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
2026-06-12 17:09:49 -04:00
shawn 32690c293e feat: comprehensive React SPA replacing Jinja2 UI
- 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
2026-06-12 15:30:03 -04:00
shawn 35d2dc0893 R4: Save Tone3000 downloads with readable filenames instead of internal IDs 2026-06-12 14:24:09 -04:00
shawn 763e152d86 R3: Add Focusrite 2i2 1st gen profile + models/irs escapeHtml fix 2026-06-12 14:20:04 -04:00
shawn 737b123994 R2: Add escapeHtml to app.js for models/irs pages 2026-06-12 14:17:24 -04:00
shawn 77a757cee6 R1: Dashboard 500 fix, Phase 2/7 uncommitted changes
- Fix dashboard 500 when deps disconnected — _gather_state() now returns
  full defaults instead of bare {'connected': False}
- Phase 2: FX param schemas aligned with DSP implementations
- Phase 7: Pipeline routing, preset manager improvements
- Factory presets: cleanup and normalization

Fixes dashboard crash on disconnected state (dogfood issue #1)
2026-06-12 14:02:13 -04:00
shawn 1301a8a128 docs: update test plan with Phase 2/7 results, README test count -> 574, close Gitea issues #1 #2 2026-06-12 13:36:39 -04:00
shawn 8e84b0217d Phase 7: Stability test + fix MIDI test signature
- tests/test_stability.py — 1-hour continuous DSP pipeline stability test
  covering all 46 FX types with CPU/memory/NaN monitoring
- tests/test_integration.py — fixed test_midi_cc_maps_expression to use
  correct _on_midi_cc(value, cc_number, channel) signature matching
  the Phase 6 MIDI fix
2026-06-12 13:29:55 -04:00
shawn f8d7db731e P6-R3: Fix MIDI CC callback bug — pass CC number, not MIDI channel
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
2026-06-12 13:05:21 -04:00
shawn 9a8d42e1b6 P2: Fix FX param schemas to match DSP implementations
- EQ: Renamed low_gain/mid_gain/high_gain to bass/mid/treble + added
  bass_freq/mid_freq/treble_freq/q params matching _apply_eq()
- Chorus: Added 'delay' param matching _apply_chorus()
- Flanger: Added 'delay' param matching _apply_flanger()
- Phaser: Added 'stages' and 'mix' params matching _apply_phaser()
- Tremolo: Added 'shape' param matching _apply_tremolo()
- Delay: Added 'tap_tempo' param matching _apply_delay()
- Reverb: Added 'predelay' param matching _apply_reverb()
- Added missing nam_amp + ir_cab param schemas

Verified: 49/49 FX DSP blocks pass, 227 tests pass
2026-06-12 12:57:40 -04:00
shawn 3e55ea8a9d Fix multi-word query URL encoding for Tone3000 search
Replace spaces with PostgREST wildcards in ilike filter values so
multi-word queries like 'vox ac30' produce valid URLs.

Root cause: PostgREST filter parser doesn't handle spaces URL-encoded
as '+' in query parameter values, returning 400 Bad Request.

Fix: query.replace(' ', '*') converts spaces to PostgREST wildcards
before inserting into ilike.* pattern. 'vox ac30' becomes
ilike.*vox*ac30* -> ILIKE '%vox%ac30%'.

Changes:
- src/system/tonedownload.py: fix search_models() and search_irs()
- tests/test_tonedownload.py: update test_multi_word_query assertion
2026-06-12 12:43:55 -04:00
shawn e890a4aad3 Wire React UI dist mount at /ui/
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
2026-06-12 03:08:07 -04:00
shawn da3097c2dc docs(presets): add factory-presets.md reference
Document all 3 banks (12 presets) with chain, parameters, and best-use notes.
2026-06-12 03:06:48 -04:00
shawn 9a6cbdf9ac feat(presets): seed default overdrive/delay/reverb/chorus presets
Add Bank 2 'FX Bench' with 4 dedicated factory presets:
- Overdrive Bench: noise_gate + overdrive + volume
- Delay Bench: noise_gate + eq + delay + volume
- Reverb Bench: noise_gate + eq + reverb + volume
- Chorus Bench: noise_gate + eq + chorus + volume

Each preset showcases a single core effect with well-tuned default
parameters derived from the pipeline DSP schemas.

All 46 existing tests pass.
2026-06-12 03:05:56 -04:00
shawn ced339bd18 I2S HAT: add PCB net definitions, net assignments, trace segments, and GND copper pours
- 17 net definitions (GND, +3.3V/+5V, BCK/LRCK/DIN/DOUT, audio signals)
- Net assignments to 35+ pads across all components (PCM1808, PCM5102A, TL072, AMS1117)
- 20 trace segments for I2S bus and audio signal routing
- 4 GND copper pour zones (F.Cu + B.Cu)

Uses string-based S-expression manipulation with proper parenthesis tracking.
2026-06-10 00:47:25 -04:00
shawn 8080ef8940 I2S HAT: complete PCB with all footprints placed, Gerbers exported
- Generated complete PCB file via pcbnew API (KiCad 9.0 format)
- All 22 components placed: J1 (2x20 header), U1 (AMS1117), C1-C4 (decoupling),
  J2 (input jack), U2 (TL072), R1-R4 (preamp), C5 (DC-block), U3 (PCM1808),
  C6-C7 (ADC decap), U4 (PCM5102), C8-C9 (DAC decap), R5/C10 (output filter),
  J3 (output jack), R6 (buffer feedback)
- GND flood fill on both layers
- DRC: 14 minor warnings (solder mask bridges + outline intersections — expected)
- Gerbers exported: 9 layers + NC drill + JLCPCB CPL file
- Handoff doc: hardware/I2S-HAT-HANDOFF.md
2026-06-09 19:01:32 -04:00
shawn 07e98aab57 Fix test_web: mock _gather_wifi_state/_gather_bt_state to avoid bluetoothctl 30s hang on non-Pi dev machines
The test_state_connected test timed out because _gather_bt_state calls real
bluetoothctl which hangs for 30s when no BT adapter is present. Since these
gathers depend on hardware (nmcli, bluetoothctl) that only exists on the Pi,
mock them in the test fixture so the test suite runs cleanly on any machine.

43/43 tests pass in 2.93s.
2026-06-09 08:08:11 -04:00
shawn a31342b478 feat(tonedownload): Tone3000 NAM + IR browser, real anon key, tests
- Tonedownload.py: Tone3000 API client with search/download/cache/enrich
- Server.py: 4 endpoints (search models, search IRs, install model, install IR)
- Models.html + irs.html: Browse Tone3000 tab with search, trending, offline
- Models.js + irs.js: Tone3000 search UI with install tracking via localStorage
- Style.css: Card layout for search results with thumbnail, meta, install btn
- Tests: 47 tests covering data classes, cache, API, search, download, enrichment
- Anon key: Updated from placeholder to real Supabase anon key (verified: API works)
2026-06-09 01:39:12 -04:00
shawn b4237f2f1d Add Network + Bluetooth config in Web UI
- src/system/network.py: WiFi scan/connect/hotspot via nmcli + iwlist fallback
- src/system/bluetooth.py: BT scan/pair/connect/MIDI via bluetoothctl
- REST endpoints: /api/wifi/* and /api/bluetooth/* (16 endpoints)
- Web UI: tabbed settings with Network + Bluetooth panels
- network.js: WiFi scan, connect modal, saved nets, hotspot
- bluetooth.js: BT scan, pair/unpair, connect, MIDI service
- style.css: tabs, network list, BT device list, signal indicators
- scripts/setup-bt-midi.sh: BT MIDI systemd bridge service
- tests/test_network.py: 22 tests (nmcli/iwlist, fallback, hotspot)
- tests/test_bt.py: 24 tests (status, scan, pair, MIDI, edge cases)
- _gather_state includes wifi + bluetooth sub-objects
- All 93 tests pass
2026-06-09 01:14:41 -04:00
shawn c73b1ffa6c feat: Bluetooth MIDI setup script (discoverable, pairable, systemd service) 2026-06-09 01:02:23 -04:00
shawn ac16d211de feat: WiFi access point setup script + README hotspot instructions 2026-06-09 00:56:58 -04:00
shawn a1f4088229 docs: full install instructions for Focusrite test + effect list 2026-06-09 00:36:25 -04:00
shawn 9481130f13 test: Focusrite 2i2 test plan, mono + 4CM config files 2026-06-09 00:31:53 -04:00
shawn dbb3b9d84f docs: module catalog, 3D model, touchscreen concept, and PCB preview 2026-06-09 00:28:16 -04:00
shawn c8d7541065 feat: full FX palette expansion — 32 new effects
Adds all 27 new DSP implementations plus tests and parameter schemas:

Pitch & Frequency (5): octaver, pitch_shifter, harmonizer, whammy, detune
Modulation (7): ring_modulator, auto_wah, envelope_filter, rotary_speaker,
  uni_vibe, auto_pan, stereo_widener
Drive & Saturation (3): bitcrusher, wavefolder, rectifier
Dynamics (4): expander, de_esser, transient_shaper, sidechain_compressor
Filters & EQ (6): parametric_eq, high_pass_filter, low_pass_filter,
  band_pass_filter, notch_filter, formant_filter
Time-Based (6): ping_pong_delay, multi_tap_delay, reverse_delay, tape_echo,
  shimmer_reverb, looper
Ambience (1): early_reflections

Each effect has: DSP function (5-30 lines numpy), case dispatch entry,
parameter schema in _FX_PARAM_SCHEMAS, and tests for critical effects.
CPU-heavy effects (pitch_shifter, shimmer_reverb) tagged as BETA.
All 79 tests pass.
2026-06-08 18:04:02 -04:00
shawn a5e4f57fcf Add tests for 4CM routing endpoints (GET/POST /api/routing)
- TestAPIRouting: 7 tests covering get/set routing mode and breakpoint
- mock_pipeline now has a real set_routing side_effect that updates properties
- Covers connected/disconnected states, mode toggle, breakpoint change, both
2026-06-08 13:07:43 -04:00
shawn 44cf978873 Wire 4CM routing: JackAudioClient + boot orchestration + set_routing_mode()
- 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).
2026-06-08 12:59:47 -04:00
shawn 8ff584cea9 4CM split routing: stereo pipeline + breakpoint + Web UI
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
2026-06-08 10:57:47 -04:00
shawn c2071a9724 feat(audio): Focusrite 2i2 stereo JACK config for 4CM
Adds:
- AudioConfig.mode field: 'mono' (default) or 'stereo_4cm'
- capture_channels / playback_channels properties for dynamic JACK -i/-o counts
- FOCUSRITE_PROFILES dict with focusrite_2i2_3gen entry
- AudioSystem.channel_mapping_help() static method
- stereo_4cm port auto-connect wiring (4 cable method)
- Systemd service content uses dynamic channel counts
- Default config YAML includes 'mode: mono'
- 11 new tests (31 total pass)
- Focusrite 4CM wiring docs in docs/config-audio.md
2026-06-08 10:40:38 -04:00
shawn 80011274f2 Add 4CM routing fields to Preset data model
- Add routing_mode (mono/4cm) and routing_breakpoint (default 7) to Preset
- Serialize/deserialize in _preset_to_dict / _preset_from_dict
- Backward compatible — old presets default to mono/7
- Add get_4cm_config() / set_4cm_config() helpers on PresetManager
- set_4cm_config validates mode and saves back to disk
- All 48 related tests pass (46 preset + 2 integration activation)
2026-06-08 10:39:38 -04:00
shawn 1c5b728e8b Build: Web UI + mDNS (pedal.local)
FastAPI web server with REST + WebSocket, 5 Jinja2 HTML pages,
mobile-friendly dark theme CSS, page-specific JS controllers,
and Avahi mDNS service advertisement for pedal.local.

Files:
  src/web/server.py          — FastAPI app with 17 REST endpoints + /ws WebSocket
  src/web/__init__.py         — Package init with WebServer, WebServerDeps exports
  src/web/templates/*.html    — Dashboard, Presets, Models, IRs, Settings (base)
  src/web/static/style.css    — Dark mobile-first CSS (80-82% max-width)
  src/web/static/websocket.js — Auto-reconnecting WebSocket manager
  src/web/static/app.js       — Dashboard live updates + REST helpers
  src/web/static/presets.js   — Bank/preset CRUD modal UI
  src/web/static/models.js    — NAM model list/load/upload
  src/web/static/irs.js       — IR file list/load/upload
  etc/avahi/pi-multifx-pedal.service — Avahi service advertisement
  scripts/setup-mdns.sh       — One-shot mDNS setup (avahi, hostname, restart)
  main.py                     — Wire WebServer into boot/shutdown
  requirements.txt            — Add fastapi, uvicorn, jinja2, python-multipart
  tests/test_web.py           — 36 tests (pages, REST, WebSocket, errors)

All 36 tests pass.
2026-06-08 00:57:26 -04:00
shawn b7f5488efd Keep Avahi for pedal.local mDNS, add web UI task 2026-06-08 00:38:37 -04:00
shawn 9e6f23cdb0 Fix minimize script: keep bluetooth+wifi, update baseline doc 2026-06-08 00:23:01 -04:00
shawn 39bac0b0cd Build: DietPi OS minimization
- dietpi-postinstall.sh: first-boot setup from DietPi minimal with only
  needed packages (Python, JACK, cmake, GPIO libs, I2C tools)
- minimize.sh: removes unused packages, systemd services, and kernel
  modules with --dry-run and --aggressive modes; saves before/after profiles
- build-minimal-image.sh: produces flashable SD card image from scratch
  (download DietPi + provision) or from a running SD card device
- baseline-footprint.md: documents before/after RAM, disk, boot time
  targets and verification checklist

Closes t_38b86d31 — DietPi OS minimization
2026-06-08 00:12:40 -04:00
shawn 1c611646be NAM model integration: rewrite nam_host.py with full inference pipeline
- NAMHost: process(), warm_up(), avg_inference_ms, model cache, crossfade
- ModelSwitchMode enum (INSTANT/CROSSFADE/PAUSE) with pipeline wiring
- list_available_models(), available_models(), process_with_model()
- Fixed pipeline.py type typo (NAMIHost -> NAMHost)
- Crossfade support wired through pipeline NAM_AMP route
- 25/25 NAM tests + 41/41 integration tests pass
- download_models.sh generates 10 verified Linear .nam models
2026-06-07 23:53:33 -04:00
shawn 0ae2ca6e8e feat: PCM1808+PCM5102 I2S HAT design files
Custom I2S HAT for Pi Multi-FX Pedal — full hardware package:

- KiCad project: schematic (kicad_sch), PCB layout (kicad_pcb), project file
- Custom DT overlay: pcm1808-pcm5102-overlay.dts (no upstream overlay exists for PCM1808)
- docs/hardware-bom.md: BOM (9.25 HAT + 1.75 with enclosure), wiring diagram, PCB design notes
- docs/config-audio.md: DT overlay build, ALSA config, JACK integration, troubleshooting
- scripts/install_hat.sh: One-command install + test suite (6 tests)
- hardware/gerber/: JLCPCB fab instructions and assembly notes

Circuit: Guitar input → TL072 preamp (gain ~20dB) → PCM1808 ADC → RPi I2S
  → PCM5102 DAC → RC filter → output jack
I2S: BCLK/GPIO18, LRCLK/GPIO19, DIN/GPIO20, DOUT/GPIO21
Stacking header passes all 40 GPIO pins for footswitches/LEDs/display
2026-06-07 23:52:13 -04:00
shawn 0e77adb4c3 Build IR convolution engine
- Full FFT overlap-add IR convolution in IRLoader (process(), set_mix(), toggle)
- Lazy FFT computation — IR FFT padded to correct block+ir size on first process()
- Wet/dry mix control, enabled/disabled toggle with tail clearing
- Fixed pipeline._apply_ir_cab() to delegate to IRLoader.process() instead of
  poking internals (old code had array-size mismatch bug: IR FFT at ir_len vs
  block FFT at conv_size)
- 46 tests: loading, convolution correctness, overlap-add state, mix, toggle,
  directory listing, performance budget (all <5ms even at 8192 taps), edge cases
- scripts/download_irs.sh: free IR pack downloader (God's Cab, Seacow)
2026-06-07 23:46:02 -04:00
shawn c38a7b0fd8 Add main entry point + systemd services + integration tests
New files:
  main.py                   - PedalApp: boots all subsystems in order,
                              wires MIDI/footswitch callbacks, graceful
                              teardown reverses boot order
  src/system/config.py      - YAML config loader with deep-merge
                              (separated to avoid hardware deps)
  src/system/services.py    - systemd unit generator for pedal.service
                              + multi-fx-pedal.target
  scripts/install_service.sh - copies project, creates venv, installs
                              + enables service units
  tests/test_integration.py - 41 tests: boot, routing, display sync,
                              teardown, systemd content, CLI, edge cases

Modified:
  tests/conftest.py         - add project root to sys.path
2026-06-07 23:39:50 -04:00
shawn d9682f3bea Build all FX blocks: gate, comp, boost/od/dist/fuzz, eq, mod (chorus/flanger/phaser/tremolo/vibrato), delay, reverb (Schroeder), volume
- 15 FX blocks implemented with per-block state isolation
- All blocks <500us per 256-sample block (reverb closest at 465us on x86)
- 57 unit tests all passing (per-effect, chain, bypass, state isolation)
- Benchmark script at scripts/benchmark_fx.py
- Vectorised delay reads via read_block_varying()
- scipy.lfilter for EQ (3-band RBJ) and reverb damping
- Fixed _DelayLine.write_block wraparound crash for large blocks
- Comb/Allpass buffers sized to BLOCK_SIZE * 2 minimum
2026-06-07 23:32:28 -04:00
shawn ce06a4360d Build: hardware UI layer — footswitch debounce, RGB LEDs, OLED display
- footswitch.py: GPIO debounce engine (20ms window), long-press detection
  (500ms), threaded poll loop with virtual pin support for testing
- leds.py: WS2812B/APA102 RGB LED controller with bypass indicator
  (red/green), preset navigation scan animation, tap tempo blip,
  configurable brightness (0.0-1.0)
- display.py: SSD1306 128x64 OLED renderer with preset mode (bank +
  preset name + FX chain), tuner mode (note + cents bar), FX edit
  (parameter bar), settings mode, boot splash
- __init__.py: Public API re-exports for all 3 modules
- tests/test_ui.py: 37 tests — debounce engine, GPIO simulation,
  short/long press boundary, LED pixel control, bypass colors,
  display mode rendering (preset/tuner/fx_edit/settings)
- scripts/ui_test.py: standalone test tool (python scripts/ui_test.py
  --all or --interactive)
2026-06-07 23:26:44 -04:00
shawn ed29748a62 Initial scaffold: Pi Multi-FX Pedal with NAM A2, IR cab, multi-FX, MIDI, stomp UI 2026-06-07 23:22:43 -04:00