Skip to content

Commit 0759320

Browse files
authored
Merge branch 'main' into feat/midi-preset-sysex
2 parents 50daf7f + 78c9b26 commit 0759320

4 files changed

Lines changed: 61 additions & 14 deletions

File tree

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,15 @@ uses [Semantic Versioning](https://semver.org/) (`vMAJOR.MINOR.PATCH`).
1313
Program Change is an alias for recall. The slots are the same ones the Footswitch-2 gesture
1414
uses; the commands reuse the existing capture/apply glue. Patch values are 7-bit to stay
1515
under libDaisy's 128-byte inbound SysEx buffer.
16+
- **Fix: intermittent crackle in Synth mode under load.** The crackle was a per-block CPU
17+
spike, not sustained load (so the watchdog, which only sheds after ~150 ms, never caught
18+
it): a moving filter envelope changed the cutoff every sample, forcing a per-sample
19+
`SetFreq` (Svf `sinf`+`powf` / Moog polynomial) on every voice — and a chord put all 6
20+
voices on that path at once, tipping a block past its deadline. The voice filter now
21+
recomputes its coefficients at **control rate** (every 8 samples, ~6 kHz — inaudible for
22+
sweeps) while keeping the existing "skip when unchanged" fast path for static patches.
23+
The audio **block size also goes 48 → 64** (~1.3 ms @ 48 kHz) for more headroom against
24+
transient spikes.
1625

1726
## [v0.4.0] - 2026-06-25
1827
- **Presets** (`io/presets.h`): three per mode, stored in QSPI. Hold Footswitch 2 to enter

CONTRIBUTING.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,32 @@ shared between them lives in globals in `main.cpp`:
7676
If you introduce new shared state, document which context owns it and prefer a single
7777
writer.
7878

79+
## Stress-testing CPU load
80+
81+
The audio callback has a hard deadline (one block, `kBlockSize`/48 kHz). A `CpuLoadMeter`
82+
(`g_cpu`) tracks average/peak callback load and reports it over SysEx (cmd `0x02`); a
83+
watchdog (`params::watchdog`) sheds the global FX and halves Synth polyphony after sustained
84+
overload. When you touch the DSP or the voice count, verify the **worst case** still has
85+
headroom — and watch the **peak**, not just the average, since a single over-deadline block
86+
crackles even when the average looks fine.
87+
88+
The heaviest configuration the engine can produce:
89+
90+
- **Mode = Synth**, **FX = Reverb** (`ReverbSc` is the costly one), **master filter on** with
91+
high resonance.
92+
- Synth params (CC 40+): **voices = max (6)**, **unison = max (4)**, **engine = analog**
93+
(4 PolyBLEP osc + sub per voice), **filter = Moog** (4-pole), **drive up**.
94+
- **LFO→cutoff** depth up and **chaos speed** (CC 18) maxed so modulation churns every block.
95+
- **MIDI-flood**: hold all 6 voices *and* retrigger fast with a short attack/decay, so every
96+
voice's filter envelope stays in motion — that is what exercises the filter-coefficient
97+
path on all voices at once (see the control-rate `SetFreq` in `dsp/voice.h`).
98+
99+
This pins 6 voices × 5 oscillators + 6 Moog filters + `ReverbSc` + master filter + limiter
100+
simultaneously. A "pass" is: no audible crackle in the ~150 ms before the watchdog trips, and
101+
the watchdog trips and then recovers cleanly (LED returns to heartbeat, full polyphony) once
102+
the flood stops. Granular at 12 grains / max density + reverb is a lighter, separate path
103+
worth a second check.
104+
79105
## Build scripts
80106

81107
Each script exists as a `.sh`/`.ps1` pair (`scripts/build.{sh,ps1}`, etc.). They are thin

src/config/params.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ constexpr char kFwVersion[] = "0.4.0";
3737
// Audio engine
3838
// ----------------------------------------------------------------------------
3939
namespace audio {
40-
constexpr int kBlockSize = 48; // samples/channel per callback
40+
constexpr int kBlockSize = 64; // samples/channel per callback (~1.3 ms @ 48 kHz)
4141
// Sample rate is set via SaiHandle config in main.cpp (48 kHz).
4242
} // namespace audio
4343

src/dsp/voice.h

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -223,25 +223,35 @@ class Voice {
223223
// Pre-filter saturation -> grit (and dirties the filter for fat/acid tones).
224224
float drv = Saturate(sig, drive) * 0.6f;
225225
// Filter coefficients only depend on (fc, res, filter type). SetFreq is costly
226-
// (Svf: sinf+powf; Moog: a polynomial), so skip it on samples where none changed
227-
// -- a big saving for static-filter patches (cutoff held, no filter envelope).
226+
// (Svf: sinf+powf; Moog: a polynomial). Two savings stack:
227+
// 1. skip it when nothing changed -- free for static-filter patches.
228+
// 2. update at CONTROL RATE (every kCoefInterval samples), not per sample --
229+
// a moving filter envelope changes fc every sample, which used to force a
230+
// per-sample SetFreq on every voice; on a chord that spiked one block past
231+
// the deadline and crackled. ~6 kHz coef updates are inaudible for sweeps.
232+
// A filter-TYPE switch forces an immediate recompute so the new filter isn't stale.
228233
const int fltSel = (filterType < 0.5f) ? 0 : 1;
229-
const bool coefDirty = (fc != lastFc_) || (res != lastRes_) || (fltSel != lastFltSel_);
230-
lastFc_ = fc;
231-
lastRes_ = res;
234+
const bool typeChanged = (fltSel != lastFltSel_);
232235
lastFltSel_ = fltSel;
233-
if (fltSel == 0) { // clean 2-pole Svf
234-
if (coefDirty) {
235-
flt_.SetFreq(fc);
236-
flt_.SetRes(res * 0.85f);
236+
if (typeChanged) coefCountdown_ = 0;
237+
if (--coefCountdown_ <= 0) {
238+
coefCountdown_ = kCoefInterval;
239+
if (fc != lastFc_ || res != lastRes_ || typeChanged) {
240+
lastFc_ = fc;
241+
lastRes_ = res;
242+
if (fltSel == 0) {
243+
flt_.SetFreq(fc);
244+
flt_.SetRes(res * 0.85f);
245+
} else {
246+
mflt_.SetFreq(fc);
247+
mflt_.SetRes(res * 0.95f); // fat 4-pole MoogLadder
248+
}
237249
}
250+
}
251+
if (fltSel == 0) { // clean 2-pole Svf
238252
flt_.Process(drv);
239253
return flt_.Low() * env * vel_;
240254
}
241-
if (coefDirty) {
242-
mflt_.SetFreq(fc);
243-
mflt_.SetRes(res * 0.95f); // fat 4-pole MoogLadder
244-
}
245255
return mflt_.Process(drv) * env * vel_;
246256
}
247257

@@ -269,8 +279,10 @@ class Voice {
269279
float wtPhase_ = 0.f, fmPhase_ = 0.f; // wavetable carrier + FM modulator phases
270280
bool gate_ = false;
271281
// Cached per-block work (sentinels force a recompute on the first sample):
282+
static constexpr int kCoefInterval = 8; // recompute filter coefs every N samples
272283
float lastFc_ = -1.f, lastRes_ = -1.f; // filter coefficients (see Process)
273284
int lastFltSel_ = -1; // 0 = Svf, 1 = Moog
285+
int coefCountdown_ = 0; // samples until the next coef recompute
274286
float uniMul_[kUni] = {1.f, 1.f, 1.f, 1.f}; // unison detune frequency multipliers
275287
float uniGain_ = 1.f; // 1 / unison count
276288
int lastU_ = -1; // unison count the multipliers were built for

0 commit comments

Comments
 (0)