From 46995c4c53a7876c94b3393c2b32ea3120139bd4 Mon Sep 17 00:00:00 2001 From: yaniv schonfeld Date: Wed, 7 Oct 2026 17:50:18 +0300 Subject: [PATCH 1/2] Small-screen patching, Windows touch and BLE MIDI, C3 handheld controller Building a patch on the ROG Ally under Windows: - module picker with large tiles, A / + ADD / menus; modules go after the selected one or at the end of the chain with cables moved (ChainEdit, one undo step each, stereo pairs kept); a + between pedals on the Board - move earlier/later, remove keeping the chain connected - tap a knob for a large value sheet; touch dead zone on knob drags - touch header with finger-sized buttons, MUTE, UNDO/REDO on screen; long press now works on plates and pedals - Windows: WinRT MIDI (Bluetooth LE devices), press-and-hold off for the window, maximised first start on small screens C3 handheld controller (ESP32-C3 SuperMini with 0.42" OLED, BLE MIDI): - firmware in controller/c3-oled (built, 72 host tests, not run on a board) - protocol: labels and values, mute flag, model hello, settings SysEx - CONTROLLER page: assign controls, app-wide actions with pot pickup, settings editor; mappings follow a channel change - printable case in controller/c3-oled/enclosure (checked, not printed) Also SIGNALPATCH_UI_SCRIPT for scripted UI checks and controller/tools/fakec3.c. Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 12 +- CMakeLists.txt | 5 +- controller/c3-oled/.gitignore | 1 + controller/c3-oled/README.md | 211 + controller/c3-oled/enclosure/.gitignore | 3 + controller/c3-oled/enclosure/DECISIONS.md | 75 + controller/c3-oled/enclosure/README.md | 128 + .../c3-oled/enclosure/SKILL-FEEDBACK.md | 45 + controller/c3-oled/enclosure/SPEC.md | 137 + .../c3-oled/enclosure/gen/blender_check.py | 149 + .../c3-oled/enclosure/gen/blender_render.py | 352 + controller/c3-oled/enclosure/gen/build.py | 279 + controller/c3-oled/enclosure/gen/ekit.py | 594 + controller/c3-oled/enclosure/gen/parts_lib.py | 428 + controller/c3-oled/enclosure/gen/spec.py | 410 + .../c3-oled/enclosure/gen/spec_noboard.py | 140 + .../c3-oled/enclosure/images/01-assembled.png | Bin 0 -> 1834783 bytes .../c3-oled/enclosure/images/02-top.png | Bin 0 -> 1496153 bytes .../c3-oled/enclosure/images/03-far-wall.png | Bin 0 -> 846230 bytes .../c3-oled/enclosure/images/04-lid-off.png | Bin 0 -> 1813311 bytes .../c3-oled/enclosure/images/05-exploded.png | Bin 0 -> 1793299 bytes .../enclosure/images/06-section-c3.png | Bin 0 -> 1270803 bytes .../enclosure/images/07-section-pots.png | Bin 0 -> 1021886 bytes .../enclosure/images/08-section-jack.png | Bin 0 -> 1279916 bytes .../enclosure/images/09-print-plates.png | Bin 0 -> 1500569 bytes .../c3-oled/enclosure/signalpatch-c3.scad | 5 + .../c3-oled/enclosure/stl/lid-assembled.stl | 62750 ++++++++++++++++ controller/c3-oled/enclosure/stl/lid.scad | 1 + controller/c3-oled/enclosure/stl/lid.stl | 62750 ++++++++++++++++ .../c3-oled/enclosure/stl/shell-assembled.stl | 41498 ++++++++++ controller/c3-oled/enclosure/stl/shell.scad | 1 + controller/c3-oled/enclosure/stl/shell.stl | 41498 ++++++++++ controller/c3-oled/include/Pins.h | 51 + controller/c3-oled/platformio.ini | 38 + controller/c3-oled/src/BleMidiPort.cpp | 188 + controller/c3-oled/src/BleMidiPort.h | 64 + controller/c3-oled/src/Display.cpp | 143 + controller/c3-oled/src/Display.h | 46 + controller/c3-oled/src/Inputs.cpp | 68 + controller/c3-oled/src/Inputs.h | 22 + controller/c3-oled/src/Mpu6050.cpp | 76 + controller/c3-oled/src/Mpu6050.h | 27 + controller/c3-oled/src/SettingsStore.cpp | 37 + controller/c3-oled/src/SettingsStore.h | 12 + controller/c3-oled/src/core/BleMidi.cpp | 316 + controller/c3-oled/src/core/BleMidi.h | 109 + controller/c3-oled/src/core/Controller.cpp | 618 + controller/c3-oled/src/core/Controller.h | 167 + controller/c3-oled/src/core/InputMath.cpp | 182 + controller/c3-oled/src/core/InputMath.h | 133 + controller/c3-oled/src/core/PacketRing.h | 73 + controller/c3-oled/src/core/Protocol.cpp | 189 + controller/c3-oled/src/core/Protocol.h | 113 + controller/c3-oled/src/core/Screen.cpp | 273 + controller/c3-oled/src/core/Screen.h | 116 + controller/c3-oled/src/core/Settings.cpp | 91 + controller/c3-oled/src/core/Settings.h | 93 + controller/c3-oled/src/core/Tilt.cpp | 50 + controller/c3-oled/src/core/Tilt.h | 39 + controller/c3-oled/src/main.cpp | 241 + .../c3-oled/test/test_ble_midi/test_main.cpp | 463 + .../test/test_controller/test_main.cpp | 799 + .../c3-oled/test/test_inputs/test_main.cpp | 332 + .../c3-oled/test/test_protocol/test_main.cpp | 306 + controller/tools/fakec3.c | 92 + docs/ARCHITECTURE.md | 16 + docs/BUILDING.md | 21 +- docs/CHANGELOG.md | 47 + docs/CONTROLLER.md | 134 +- docs/HANDHELD.md | 3 + docs/ROADMAP.md | 6 +- docs/TESTING.md | 20 +- src/audio/ChainEdit.cpp | 412 + src/audio/ChainEdit.h | 84 + src/audio/ControllerFeedback.cpp | 368 +- src/audio/ControllerFeedback.h | 80 +- src/audio/Graph.cpp | 2 + src/audio/MidiMap.cpp | 2 + src/audio/MidiMap.h | 10 +- src/audio/PatchEngine.cpp | 285 +- src/audio/PatchEngine.h | 87 +- src/v2/ControllerPage.cpp | 477 + src/v2/ControllerPage.h | 104 + src/v2/Main.cpp | 28 +- src/v2/ModulePicker.cpp | 520 + src/v2/ModulePicker.h | 88 + src/v2/Palette.h | 93 +- src/v2/PlatformWindow.cpp | 45 + src/v2/PlatformWindow.h | 13 + src/v2/RackView.cpp | 1350 +- src/v2/RackView.h | 94 +- src/v2/UiScript.cpp | 163 + src/v2/UiScript.h | 50 + src/v2/ValueSheet.cpp | 323 + src/v2/ValueSheet.h | 70 + tests/EngineTests.cpp | 476 +- 96 files changed, 222171 insertions(+), 339 deletions(-) create mode 100644 controller/c3-oled/.gitignore create mode 100644 controller/c3-oled/README.md create mode 100644 controller/c3-oled/enclosure/.gitignore create mode 100644 controller/c3-oled/enclosure/DECISIONS.md create mode 100644 controller/c3-oled/enclosure/README.md create mode 100644 controller/c3-oled/enclosure/SKILL-FEEDBACK.md create mode 100644 controller/c3-oled/enclosure/SPEC.md create mode 100644 controller/c3-oled/enclosure/gen/blender_check.py create mode 100644 controller/c3-oled/enclosure/gen/blender_render.py create mode 100644 controller/c3-oled/enclosure/gen/build.py create mode 100644 controller/c3-oled/enclosure/gen/ekit.py create mode 100644 controller/c3-oled/enclosure/gen/parts_lib.py create mode 100644 controller/c3-oled/enclosure/gen/spec.py create mode 100644 controller/c3-oled/enclosure/gen/spec_noboard.py create mode 100644 controller/c3-oled/enclosure/images/01-assembled.png create mode 100644 controller/c3-oled/enclosure/images/02-top.png create mode 100644 controller/c3-oled/enclosure/images/03-far-wall.png create mode 100644 controller/c3-oled/enclosure/images/04-lid-off.png create mode 100644 controller/c3-oled/enclosure/images/05-exploded.png create mode 100644 controller/c3-oled/enclosure/images/06-section-c3.png create mode 100644 controller/c3-oled/enclosure/images/07-section-pots.png create mode 100644 controller/c3-oled/enclosure/images/08-section-jack.png create mode 100644 controller/c3-oled/enclosure/images/09-print-plates.png create mode 100644 controller/c3-oled/enclosure/signalpatch-c3.scad create mode 100644 controller/c3-oled/enclosure/stl/lid-assembled.stl create mode 100644 controller/c3-oled/enclosure/stl/lid.scad create mode 100644 controller/c3-oled/enclosure/stl/lid.stl create mode 100644 controller/c3-oled/enclosure/stl/shell-assembled.stl create mode 100644 controller/c3-oled/enclosure/stl/shell.scad create mode 100644 controller/c3-oled/enclosure/stl/shell.stl create mode 100644 controller/c3-oled/include/Pins.h create mode 100644 controller/c3-oled/platformio.ini create mode 100644 controller/c3-oled/src/BleMidiPort.cpp create mode 100644 controller/c3-oled/src/BleMidiPort.h create mode 100644 controller/c3-oled/src/Display.cpp create mode 100644 controller/c3-oled/src/Display.h create mode 100644 controller/c3-oled/src/Inputs.cpp create mode 100644 controller/c3-oled/src/Inputs.h create mode 100644 controller/c3-oled/src/Mpu6050.cpp create mode 100644 controller/c3-oled/src/Mpu6050.h create mode 100644 controller/c3-oled/src/SettingsStore.cpp create mode 100644 controller/c3-oled/src/SettingsStore.h create mode 100644 controller/c3-oled/src/core/BleMidi.cpp create mode 100644 controller/c3-oled/src/core/BleMidi.h create mode 100644 controller/c3-oled/src/core/Controller.cpp create mode 100644 controller/c3-oled/src/core/Controller.h create mode 100644 controller/c3-oled/src/core/InputMath.cpp create mode 100644 controller/c3-oled/src/core/InputMath.h create mode 100644 controller/c3-oled/src/core/PacketRing.h create mode 100644 controller/c3-oled/src/core/Protocol.cpp create mode 100644 controller/c3-oled/src/core/Protocol.h create mode 100644 controller/c3-oled/src/core/Screen.cpp create mode 100644 controller/c3-oled/src/core/Screen.h create mode 100644 controller/c3-oled/src/core/Settings.cpp create mode 100644 controller/c3-oled/src/core/Settings.h create mode 100644 controller/c3-oled/src/core/Tilt.cpp create mode 100644 controller/c3-oled/src/core/Tilt.h create mode 100644 controller/c3-oled/src/main.cpp create mode 100644 controller/c3-oled/test/test_ble_midi/test_main.cpp create mode 100644 controller/c3-oled/test/test_controller/test_main.cpp create mode 100644 controller/c3-oled/test/test_inputs/test_main.cpp create mode 100644 controller/c3-oled/test/test_protocol/test_main.cpp create mode 100644 controller/tools/fakec3.c create mode 100644 src/audio/ChainEdit.cpp create mode 100644 src/audio/ChainEdit.h create mode 100644 src/v2/ControllerPage.cpp create mode 100644 src/v2/ControllerPage.h create mode 100644 src/v2/ModulePicker.cpp create mode 100644 src/v2/ModulePicker.h create mode 100644 src/v2/PlatformWindow.cpp create mode 100644 src/v2/PlatformWindow.h create mode 100644 src/v2/UiScript.cpp create mode 100644 src/v2/UiScript.h create mode 100644 src/v2/ValueSheet.cpp create mode 100644 src/v2/ValueSheet.h diff --git a/CLAUDE.md b/CLAUDE.md index e8964f6..5c379a3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,7 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co SignalPatch is a C++20 real-time guitar rig / modular rack for Linux and Windows (AGPL-3.0, github.com/willbearfruits/signalpatch, site at willbearfruits.github.io/signalpatch). A headless JUCE 8 audio engine (`src/audio/`) runs under a GLFW + OpenGL 3.3 + NanoVG app (`src/v2/`) with two views of one patch: the **rack** (modules and cables) and the **Board** (the same patch as pedals, groups and five rig slots). The graph derives its ports from the audio device's enabled channels — no hard-coded input count anywhere. -Doc map: `docs/CHANGELOG.md` (keep "Unreleased" current; add a `` to `packaging/linux/*.metainfo.xml` per version) · `docs/ROADMAP.md` · `docs/ARCHITECTURE.md` · `docs/REALTIME_SAFETY.md` (rules for callback code) · `docs/TESTING.md` (what is verified and what is not; update when that changes) · `docs/BUILDING.md` · `docs/CONTROLLER.md` (foot-controller MIDI/SysEx protocol) · `docs/HANDHELD.md`. **Writing style for every public text (README, docs, site, release notes): short, plain, factual. No slogans, no bold-phrase bullet openers stacked with em dashes, no "for real" / "honest" / "proven" claims; say what it does and what is untested.** +Doc map: `docs/CHANGELOG.md` (keep "Unreleased" current; add a `` to `packaging/linux/*.metainfo.xml` per version) · `docs/ROADMAP.md` · `docs/ARCHITECTURE.md` · `docs/REALTIME_SAFETY.md` (rules for callback code) · `docs/TESTING.md` (what is verified and what is not; update when that changes) · `docs/BUILDING.md` · `docs/CONTROLLER.md` (controller MIDI/SysEx protocol, settings SysEx) · `docs/HANDHELD.md` · `controller/c3-oled/README.md` (ESP32-C3 BLE MIDI controller firmware, PlatformIO). **Writing style for every public text (README, docs, site, release notes): short, plain, factual. No slogans, no bold-phrase bullet openers stacked with em dashes, no "for real" / "honest" / "proven" claims; say what it does and what is untested.** ## Build, run, test @@ -30,7 +30,7 @@ Hand-rolled harness in `tests/EngineTests.cpp`: each case is a free function thr - Global `new`/`delete` are replaced by an **allocation trap**; `renderPlanBlocks(..., armTrap=true)` fails if the render path allocates. Never allocate inside an armed window (including building `std::string` messages) — the trap counts the harness too. - `buildKitchenSinkDocument()` wires one of every node kind (MIDI notes are dispatched every fifth block); extend it and the kinds list in "all node kinds render finite output" when adding a kind. `SIGNALPATCH_SOAK_BLOCKS=1687500` turns the trap test into a 30-minutes-of-audio soak. -- `signalpatch_tests` lists its sources explicitly in CMakeLists (Graph, PatchHistory, PatchBundle, MidiMap, ControllerFeedback, Tone3000, Processors) and links only headless JUCE modules — a new `src/audio/*.cpp` must be added there; the app target globs. +- `signalpatch_tests` lists its sources explicitly in CMakeLists (Graph, PatchHistory, PatchBundle, MidiMap, ControllerFeedback, ChainEdit, Tone3000, Processors) and links only headless JUCE modules — a new `src/audio/*.cpp` must be added there; the app target globs. - Nodes rendered on their own (not through a plan) see every input as unconnected (`isInputConnected`), and knob smoothers need a few blocks to arrive before a test measures. ## Engine architecture (src/audio) @@ -51,7 +51,9 @@ Hand-rolled harness in `tests/EngineTests.cpp`: each case is a free function thr **Undo.** `PatchHistory` records entries *after* the change; continuous edits coalesce until `closeGesture()`; `beginCompoundGesture` groups several edits (multi-drag/delete, a slot glide via `recordGlide`) into one step. Removing a node snapshots groups and MIDI mappings so undo restores them. `loadPatch`/`newPatch`/device relayout clear the stack. -**Control.** `MidiMap` (CC/note/program → knob/stomp/command/slot/group; `relative`, calibrated `low`/`high`) is saved as `"midi"`. A velocity-0 note-on is a release everywhere (`isNoteOn()`, never `isNoteOn(true)`). Keys and program changes fire commands per press; CCs use a rising-edge gate. `ControllerFeedback` plus a controller's SysEx hello drive LED/label feedback. The Clock node emits 0.8-high pulses and a 1.0-high pulse on start/reset; `ClockFollower` (drum machine, sequencer, looper) treats pulses as "external" for max(4 s, 2.5 intervals) and realigns on restart pulses. +**Chain edits.** `ChainEdit` (insert after/before/into a cable/at the end of the chain, remove-and-bridge, move along the chain) works through a `chain::Editor` (the engine's `chainEditor()`, or a document + history in the tests); each op is one compound undo step and undoes itself (`undoStep`) when a cable fails to compile. L/R pairs come from port names (" L"/" R"), the hardware output's first two channels are a pair. + +**Control.** `MidiMap` (CC/note/program → knob/stomp/command/slot/group/view; `relative`, calibrated `low`/`high`) is saved as `"midi"`. `Target::view` is an app action in `command` ("knob", "next-knob", "module-knob-n", "next-slot", "prev-slot", "mute") applied by the UI through `onMidiUiTarget` for every message (the UI decides "press"). App-wide mappings (`setGlobalMappings`, kept by the UI in `controller.global` in its settings) apply only when no patch mapping matches. A velocity-0 note-on is a release everywhere (`isNoteOn()`, never `isNoteOn(true)`). Keys and program changes fire commands per press; CCs use a rising-edge gate. `ControllerFeedback` plus a controller's SysEx hello drive LED/label/value feedback per controller; a model 0x02 (C3) hello also gets a settings request, and its 0x11 dump updates `getControllers()`. The Clock node emits 0.8-high pulses and a 1.0-high pulse on start/reset; `ClockFollower` (drum machine, sequencer, looper) treats pulses as "external" for max(4 s, 2.5 intervals) and realigns on restart pulses. **Stereo** is L/R port pairs over mono buffers (Pan, Stereo Merge/Delay/Chorus/Reverb, Cabinet R); the rack cables the R pair in the same drag. @@ -64,8 +66,10 @@ Hand-rolled harness in `tests/EngineTests.cpp`: each case is a free function thr - **Patch swaps**: opening a patch, New, or a slot with different modules calls `patchReplaced()`, which drops drags, overlays, learn state, glides and selections and bumps `patchEpoch`. Callbacks that captured a `NodeId` must check the epoch and the node's kind before touching it (see `applyExtraKey`). Undo/redo go through `undoNow`/`redoNow`, which end drags and glides first. - `ToneBrowser` network jobs run on a thread pool with a **copy** of the client and write only into shared result objects; the message thread adopts results and rotated tokens after the `callAsync` hop. - **Adding a node kind**: `NodeKind` enum (Graph.h) → the three name/key registries in Graph.cpp → DSP class + factory in Processors.cpp → `moduleCatalogue()` in Palette.h (colour is derived from its family) → transport buttons, if any, in the `switch` in `RackView::rebuildLayouts()` (Board pedals pick them up) → the exhaustive switches in `src/ui/Theme.cpp` (the optional JUCE target still compiles them) → tests (kinds list, kitchen sink). +- **Small-screen editing**: `ModulePicker` (modal tile grid; `openPicker` + a `chain::Splice`), `ValueSheet` (tap on a knob in touch mode; `openValueSheetIfTapped`), `ControllerPage` (docked right like the inspector; only one of the two is open, `rightDock()`). The touch header is taller: use `hudHeight()` / `hudLayout()`, never 34. Drags have a slop (`dragSlop()`) so a held finger can still become a long press. Keep new overlays reachable by keys, since the gamepad drives modals through `key()`. - **Inspector** (`Inspector.*`): a non-modal panel docked right, following `selectedNode`; it edits through `engine.setParameter` / `setParameterShape` / `setModulationDepth` and asks for text through RackView's prompt. Knobs with a cabled mod socket (`modulatedKnobs`, rebuilt with the layouts) get `drawKnobModulation` over the cached plate. -- **Session** (`saveSession` / `restoreSession`, keys `session.*` in `~/.config/SignalPatch/SignalPatch.settings`): view mode, camera as the patch point at the view centre (re-placed for each window size during the first 1.5 s, because a tiling desktop resizes a mapping window), slot, file, unsaved mark, mute when "start muted" is off. Saved every few seconds when changed and on quit. **Testing the app beside a running session**: launch a copy of the binary under another name with `HOME=` (JUCE ignores `XDG_CONFIG_HOME`), on an empty workspace via `hyprctl dispatch 'hl.dsp.exec_cmd("