Skip to content

Commit 55156ec

Browse files
Joakim Langkildeclaude
andcommitted
feat: control echo (device -> editor) for live 2-way sync
Transmit a CC when a hardware control changes -- mode/FX/VAR toggles (16/17/93), the 12 shift-layer knobs (20-31), and bypass (91) -- so Propagator mirrors the physical surface. Change-detected; sent from the main loop, not the audio ISR. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 018f2c9 commit 55156ec

3 files changed

Lines changed: 46 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this pr
55
uses [Semantic Versioning](https://semver.org/) (`vMAJOR.MINOR.PATCH`).
66

77
## [Unreleased]
8+
- **Control echo (device -> editor)**: the firmware now transmits a CC when a hardware
9+
control changes -- mode/FX/VAR toggles (CC 16/17/93), the 12 shift-layer knobs (CC 20-31),
10+
and bypass (CC 91) -- so Propagator mirrors the physical surface. Change-detected, sent
11+
from the main loop (not the audio ISR). First half of live 2-way sync.
812
- **Master output stage**: a switchable **LP/BP/HP filter** (cutoff + resonance) and a
913
**master volume**, applied after the global FX and before the limiter (`fx/master.h`).
1014
Filter type 0 = off (volume only). CC 7 (vol), 88 (type), 89 (cutoff), 90 (res).

docs/MIDI_PROTOCOL.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ means depends on the active mode. All numbers are defined in
4141
| 93 | **VAR (Toggle 2)** per-mode variant (thirds: 0 / 1 / 2) |
4242
| 119 | Reboot to **DFU** bootloader when value ≥ 64 (remote flashing) |
4343

44+
**Device → editor echo (live 2-way sync):** the firmware also *transmits* these CCs when the
45+
matching hardware control changes — mode/FX/VAR toggles (16/17/93), the 12 shift-layer knobs
46+
(20–31), and bypass (91) — so the editor mirrors the physical surface. Change-detected and
47+
sent from the main loop. The editor applies incoming CC without echoing it back (no loop).
48+
4449
**Synth-panel CCs** (`CC = 40 + SP_* index`, see
4550
[`config/synth_params.h`](../src/config/synth_params.h)):
4651

src/main.cpp

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,42 @@ void AudioCallback(AudioHandle::InputBuffer in, AudioHandle::OutputBuffer out,
170170
}
171171
}
172172

173+
// Send a CC out (channel 1) to mirror our state to the editor.
174+
static inline void TxCc(MidiUsbHandler& m, int cc, float v01) {
175+
int v = static_cast<int>(v01 * 127.0f + 0.5f);
176+
uint8_t msg[3] = {0xB0, static_cast<uint8_t>(cc),
177+
static_cast<uint8_t>(v < 0 ? 0 : (v > 127 ? 127 : v))};
178+
m.SendMessage(msg, 3);
179+
}
180+
181+
// Device -> editor mirror: when a hardware control (mode/FX/VAR toggle, the 12
182+
// shift-layer knobs, or bypass) changes, transmit the matching CC so Propagator
183+
// follows the physical surface. Change-detected; runs in the main loop (not the ISR).
184+
static void EchoControls(MidiUsbHandler& m, Hothouse& hw) {
185+
using namespace params::midi;
186+
static int lMode = -1, lFx = -1, lVar = -1, lByp = -1;
187+
static float lMK[ShiftKnobs::kKnobs] = {-1, -1, -1, -1, -1, -1};
188+
static float lFK[ShiftKnobs::kKnobs] = {-1, -1, -1, -1, -1, -1};
189+
190+
int mode = g_active;
191+
int fx = static_cast<int>(g_fx.GetMode());
192+
int var = (g_varSel >= 0) ? g_varSel : TogglePos(hw, Hothouse::TOGGLESWITCH_2);
193+
int byp = g_bypass ? 1 : 0;
194+
if (mode != lMode) { lMode = mode; TxCc(m, kCcModeSelect, mode / 2.0f); }
195+
if (fx != lFx) { lFx = fx; TxCc(m, kCcFxSelect, fx / 2.0f); }
196+
if (var != lVar) { lVar = var; TxCc(m, kCcVar, var / 2.0f); }
197+
if (byp != lByp) { lByp = byp; TxCc(m, kCcFootsw1, byp ? 1.0f : 0.0f); }
198+
199+
const float* mk = g_shift.Values(ShiftKnobs::MODE);
200+
const float* fk = g_shift.Values(ShiftKnobs::FX);
201+
for (int i = 0; i < ShiftKnobs::kKnobs; ++i) {
202+
float dm = mk[i] - lMK[i]; if (dm < 0) dm = -dm;
203+
float df = fk[i] - lFK[i]; if (df < 0) df = -df;
204+
if (dm > 0.008f) { lMK[i] = mk[i]; TxCc(m, kCcModeKnobBase + i, mk[i]); }
205+
if (df > 0.008f) { lFK[i] = fk[i]; TxCc(m, kCcFxKnobBase + i, fk[i]); }
206+
}
207+
}
208+
173209
int main() {
174210
hw.Init();
175211

@@ -221,6 +257,7 @@ int main() {
221257

222258
while (true) {
223259
if (PumpMidi(midi, g_modes[g_active], g_shift, g_modeSel, g_fxSel, g_clock, g_delaySync, g_cpu, g_mod, g_master, g_bypass, g_varSel)) g_last_midi_ms = System::GetNow();
260+
EchoControls(midi, hw); // mirror physical control changes back to the editor
224261
g_clock.Update(System::GetNow()); // drop back to internal tempo if clock stops
225262
// Onboard LED: ~1 Hz heartbeat = app is alive; solid while MIDI arrives; a fast
226263
// ~5 Hz blink warns that the CPU watchdog tripped (change mode/FX to recover).

0 commit comments

Comments
 (0)