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("