Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<release>` 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 `<release>` 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

Expand All @@ -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)
Expand All @@ -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.

Expand All @@ -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=<scratch>` (JUCE ignores `XDG_CONFIG_HOME`), on an empty workspace via `hyprctl dispatch 'hl.dsp.exec_cmd("<script>", { workspace = "11 silent" })'`; never start a second instance on the real config, and never under `timeout`.
- **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=<scratch>` (JUCE ignores `XDG_CONFIG_HOME`), on an empty workspace via `hyprctl dispatch 'hl.dsp.exec_cmd("<script>", { workspace = "11 silent" })'`; never start a second instance on the real config, and never under `timeout`. To see it without a visible window: `hyprctl output create headless <NAME>`, set its mode with `hyprctl eval 'hl.monitor({...})'`, launch on the workspace shown there, `grim -o <NAME>`; `SIGNALPATCH_UI_SCRIPT=<file>` (UiScript.h) plays taps/holds/keys and writes `shot` PNGs from the framebuffer. `hl.dsp.send_shortcut` delivers keys to the window without moving focus. `controller/tools/fakec3.c` plays the C3 on an ALSA port.
- **Windows**: `PlatformWindow.cpp` (no JUCE headers there) turns off press-and-hold/touch feedback on the HWND; `JUCE_USE_WINRT_MIDI` (option `SIGNALPATCH_WINDOWS_BLE_MIDI`) makes BLE MIDI visible and routes all MIDI through WinRT. Neither can be compiled locally; CI is the check.
- **Touch mode** (`setTouchMode`, persisted in settings, auto-detected in `Main.cpp` from a small physical screen at a high content scale): `touchHit()` multiplies every hit radius, taps arm `pendingSource` for tap-to-connect, a held press in `tick()` synthesises a right click, and `touchButtonBounds` draws the zoom/fit buttons. Menu/FileBrowser/TextPrompt metrics are members with `setTouchMode`.
- Tooling: never `pkill -f SignalPatch` from a tool shell (it matches the shell) — use `pkill -x`; after renaming targets delete stale binaries in `build/*_artefacts/`.

Expand Down
5 changes: 4 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

option(SIGNALPATCH_ENABLE_ASIO "Enable JUCE's ASIO backend on Windows" OFF)
option(SIGNALPATCH_WINDOWS_BLE_MIDI "Windows: use the WinRT MIDI API, which also sees Bluetooth LE MIDI devices (the C3 controller)" ON)
option(SIGNALPATCH_ALLOW_JUCE_FETCH
"Fetch the pinned JUCE release when no matching package is installed"
ON)
Expand Down Expand Up @@ -194,6 +195,7 @@ target_sources(signalpatch_tests PRIVATE
src/audio/PatchBundle.cpp
src/audio/MidiMap.cpp
src/audio/ControllerFeedback.cpp
src/audio/ChainEdit.cpp
src/audio/Tone3000.cpp
src/audio/Graph.h
src/audio/Processors.cpp
Expand Down Expand Up @@ -284,7 +286,8 @@ if(SIGNALPATCH_BUILD_V2 AND TARGET glfw AND TARGET OpenGL::GL)
target_compile_definitions(signalpatch PRIVATE JUCE_ALSA=1 JUCE_JACK=1)
elseif(WIN32)
target_compile_definitions(signalpatch PRIVATE JUCE_WASAPI=1 JUCE_DIRECTSOUND=1
JUCE_ASIO=$<BOOL:${SIGNALPATCH_ENABLE_ASIO}>)
JUCE_ASIO=$<BOOL:${SIGNALPATCH_ENABLE_ASIO}>
JUCE_USE_WINRT_MIDI=$<BOOL:${SIGNALPATCH_WINDOWS_BLE_MIDI}>)
# A windowed app: no console window behind the rack, main() kept as the entry point.
if(MSVC)
target_link_options(signalpatch PRIVATE /SUBSYSTEM:WINDOWS /ENTRY:mainCRTStartup)
Expand Down
1 change: 1 addition & 0 deletions controller/c3-oled/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.pio/
Loading
Loading