Status: ✅ done · 🔜 next · ⬜ planned · 💤 deferred
Full review (2026-10-07), released as 0.0.2
Six reviews in parallel (the plugin glue and threads; state, presets, files and the engine; Drive, Filter, EQ, Comp; Chorus, Phaser, Pulse and the rack; Grain and the looper; Delay and Reverb). No crash, memory-safety, audio-thread or NaN bug. Every finding below was checked against the code and has a check that fails without its fix.
| Item | State |
|---|---|
| Looper: a jump inside a running crossfade dropped the head fading out (REC Last just after a bar line, a Repeat change, a Capture Next on the line where a same-length loop wraps): one jump at a time now, kGuard 1024 | ✅ |
Looper: Blend steps the live input in or out (now a 3 ms glide); LOOP KEPT with Hold off; allocate() leaked a half-made store |
✅ |
| Grain: held Mosaic entered mid-slice read its source a phase too old (a click off the grid) | ✅ |
| CHAIN's ON / OFF while a scene is edited wasn't locked in it (and was undone at the edit's end); REC's Hold was undone by the edit's end | ✅ |
| A load (preset, project, FX tile) could show the engine old knobs with half the new locks for a block or two: the scenes are held for the batch | ✅ |
| A module a scene switches in woke from a rest with its old state (a compressor's gain reduction, a synced phaser's lag): cleared on waking | ✅ |
Smaller: OTT's tail (its crossovers ring), EQ cuts off with some room (fast-exp2 values), paramNorm of an overflowing value, a category tile after a rescan, .last without fsync, the preset scan without fs::relative |
✅ |
| Not changed, below the bar: the reverb's size glide parks 6e-6 short and its modulation depth isn't slewed on hard jumps (both inaudible; fixing them moves the reverb's fingerprints); Drive's rest offset on a one-chunk bias jump; the looper's Pre / Post switch steps (rarely moved); Phaser and Pulse floor the tempo at 20 BPM (only a host tempo below that would show it) | – |
| Item | State |
|---|---|
CHANGELOG.md; the release workflow takes the tag's ## X.Y.Z section as the notes (the catalog shows them) and fails before publishing without one |
✅ |
make test-arm-pgo runs: the target sat above PGO_ON's definition, so make always took its plain branch, in CI too (the release job's "suite against the shipped objects" never ran). The Delay's Tape hash, which pins one compilation's bits, now skips against profile-guided objects and stays in the plain builds |
✅ |
Release candidate from CI (glibc 2.31, GCC 11): catalog_check.py --catalog OK, 0 warnings; needs glibc 2.29 and GCC 11's libstdc++ (GLIBCXX_3.4.29) |
✅ |
Vendored release.py: SubForce's local patch 6 (INSTALL.md gave user-data files a folder's slash) |
✅ |
| README: requirements, installation, releases, status, credits; page images (docs/img, rendered offline from the skin) | ✅ |
| CI's first real run of the suite against GCC 11's profile-guided objects: the Reverb's fingerprints (pinned to GCC 13's plain build) moved; they skip there now, as the Tape hash. 13413 checks pass | ✅ |
| v0.0.1 published (2026-10-06): a regular release, notes from the changelog, the zip catalog-checked again as published | ✅ |
The catalog entry's PR to sd88me/mpc-vst-plugins (catalog/plugins/effectforce.json, checked with its registry check): merged (2026-10-06) |
✅ |
The release zip installed and played on the Force, then tested.json |
🔜 |
A scene that moves by itself, the Octatrack way of a filter sweep across 8 bars (Design, User guide, the decisions): locks with a start, played to the lock over a LENGTH (Once, Loop, Ping-pong) from the bar line after the fader brings the scene in.
| Item | State |
|---|---|
| Store, clocks, the engine's morph, state lines, PERFORM's LENGTH / PLAY, the scenes' line | ✅ test/scenes_test.cpp (moves); 13420 checks on x86 (ASan/UBSan), under qemu-arm and on the Force's own CPU |
| 11 effects of the FX library move; the Perform presets built from them follow | ✅ test/fx_test.cpp: every move played whole, through the engine before the output guard, no clicks |
| Bench and PGO cases | ✅ on the Force: 14 locks moving every chunk cost 0.9 points over the same scene held still (Performance) |
| Review | ✅ a lock and its start published as one change; the move epoch release / acquire; the scenes' line no longer reads UI-thread strings from display(); a test that couldn't fail (behind the output guard) |
| Fixed on the way: a fresh engine given only the fader (before anything played) kept the scenes at the old position | ✅ a check |
| On the device | ✅ installed (2026-10-06); 🔜 by ear: LP Sweep on the bar line, Build Roll, Tape Stop, back at A and again |
An Octatrack-style performance mixer in the rack (Design, User guide): eight scenes of parameter locks, A and B at the ends of a crossfader parameter for the Force's own crossfader, modules switched in as sends whose tails ring out, and a looper that grabs the bar just played and plays it on the grid (rolls, speed, tape stop). Built on the Shimmer / Pulse / Grain branch, merged with it as it moves.
| Item | State |
|---|---|
| Looper (dsp/looper.*) | ✅ 44 module checks on x86 and ARM (make test-module M=looper) |
| Scenes, the engine's morph, sends, EDIT A / B, state (plugin/scenes.*, engine, surface, state, rack) | ✅ 80 checks (test/scenes_test.cpp), the whole suite green on x86 and ARM |
| PERFORM page, 10 Perform presets, bench and PGO cases | ✅ layout checked and previewed; presets level-matched |
| The Octatrack Performance Mixer's workflow: REC on the grid (Capture Next / Last, a late press forgiven, replacing a playing loop without a gap), 8-bar loops, Swap / Layer blend, the looper's line; an FX library of 64 named effects in 4 banks picked into scene B; the LOOPER page; 10 Perform mixers built from the library | ✅ 61 looper checks, test/fx_test.cpp |
| On the device | 🔜 learn the Force's crossfader to Crossfader on an insert (on the master too: an open probe item), make bench-device with the two performance cases, play the Perform presets |
Shimmer, Pulse and Grain were integrated on shimmer-grain-pulse and handed over unfinished; the
cloud session's branch carries it on.
| Item | State |
|---|---|
| Grain on ARM | ✅ its 299 module checks passed under qemu-arm as they had on x86 |
| Preset levels | ✅ make preset-levels: the 37 older presets came back exactly as on main, the 15 new ones set |
| Full suites | ✅ x86 (ASan/UBSan) and ARM (qemu), 12913 checks after the fixes below; CI green |
| Review: Grain | ✅ memory and real-time safety held (no stale or unwritten frame read in 50M+ instrumented reads). Fixed: held Stutter silent when Hold came near a grid line; short pitched-up Stutter never repeating; the grid missing loops shorter than a slice (and loops wrapping on a block edge) and firing twice on a mid-block wrap; held slices off the grid; a second release lifting the samples before it; a stalled slice cut dead; a backwards read's margin. 16 new checks |
| Review: Pulse, Shimmer | ✅ Pulse clean. Shimmer: the shifter's first splice after a restart read silence (+19 dropped out), the path faded in while the shifter was still silent (a dip), the tail length ignored the shifter's delay; a frozen shimmer drains (now documented). 9 new checks |
| Review: integration | ✅ an order saved by 0.0.1 (eight slots) fell back to the default (now kept), Choppy Pads' gate on 1/8 steps (now 1/16, the patterns' unit), option lists checked against the enums, stale docs |
| Performance | ✅ everything on: avg -10%, p99 -27% in ARM instructions; the p99 had been the shimmer's splice search (Performance) |
Still to do, in order:
- ✅ Merge the branch into
main. - On the device: ✅
make bench-devicewith all ten modules (2026-10-06: everything on at its heaviest p99 14.9%, within the 15% budget with nothing to spare; with the performance layer on top, 16.6-16.9, a warning: Performance); ✅ installed. 🔜 Play Shimmer, Pulse and Grain; Grain's Sync with MPC looping (the slice grid's locate tolerance is a host block: whether MPC's song position ever jitters more than that is untested).
- ✅ Device bench (2026-10-05): everything on at its heaviest with 8 mod slots, p99 11.3% of the block (Performance).
- ✅ On the device (2026-10-05, 0.0.1): installed and played; everything worked and the pages open fast (MPC loads it next to SubForce without a complaint in its log).
- 🔜 Still to try on the device: save and reload a project, two instances, Delay Glide's Fade.
- 🔜 Probe items still open (Probe): pads, returns and master as insert places; automation playback.
- ⬜ v0.1, the first release: parameter list frozen (append-only from then on), catalog-style package from CI.
What MPC does with a VST2 insert effect, measured on the Force: Probe.
Design record: Design.
- ✅ Eight modules in any order: Drive (2x oversampled, six shapers), Filter, EQ, Comp / OTT, Chorus (three modes), Phaser / Flanger (synced), Delay (stereo, ping-pong, mono; tape character), Reverb (four modes, 8-line FDN, freeze)
- ✅ Then (2026-10-05, after the first device run): Pulse (tremolo, auto-pan, a 16-step gate), Grain (cloud, stretch, mosaic, stutter, arp on MPC's beat; hold, feedback), the Reverb's shimmer; ten modules, 52 presets in 10 categories, 54 modulation targets
- ✅ The rack: order, 10 ms on / off fades, a 3 ms dip for a new order, no CPU for modules that are off, bit-exact pass-through with everything off
- ✅ Modulation: 4 macros, 2 LFOs (free or beat-locked), an envelope follower, an 8-slot matrix over 48 targets
- ✅ Ten pages in five tabs, each with its own Q-Link set; the CHAIN page (tiles, MOVE, ON / OFF)
- ✅ Presets: 37 factory presets in 8 categories, level-matched on reference material
(
make preset-levels); user presets, favorites, the browser - ✅ What the probe found: tails survive Stop, a long suspend clears them, zero latency, cheap getters
- ✅ The output guard: never NaN, never past +18 dBFS
- ✅ Tests: every module against theory, the rack, modulation, the plugin through a fake MPC, the presets' levels; x86 under ASan/UBSan and ARM under qemu
| Feature | Why not (now) |
|---|---|
| Two instances of a module in one rack | The fixed parameter list (automation and saved projects stay valid) has one of each; two EffectForce inserts do it |
| Sidechain input | MPC's VST2 host gives an insert 2 inputs |
| Lookahead limiting, linear-phase EQ | MPC can't compensate latency (acceptIOChanges: no) |
| Drawn curves (EQ, compressor) | MPC skins can't draw dynamic graphics |
| A pitch shifter module | The shimmer's shifter (dsp/pitch.h) is built for feedback paths; a module of its own would want formants and lower latency |
- 2026-10-05 — Rack model: each module once with its own parameters, reordered (not generic slots). Automation, Q-Links and saved projects always mean the same thing.
- 2026-10-05 — Modules for v0.1: Drive, Filter, EQ, Comp / OTT, Chorus, Phaser / Flanger, Delay, Reverb; 4 macros, 2 LFOs, an envelope follower, an 8-slot matrix.
- 2026-10-05 — Suspend rule: under 250 ms keeps every buffer (Stop), longer clears them (ON button).
- 2026-10-05 — Delay time changes: both behaviours, as Delay Glide: Tape (repeats bend in pitch) or Fade (a crossfade, no bend).
- 2026-10-05 — Level-matching by category: Synth and Pads on chords, Bass on a bass line, Drums on drums, the rest on the full mix (Texture and Rhythm on chords too).
- 2026-10-05 — Shimmer, Pulse, Grain before v0.1: the CPU budget allowed them.
- 2026-10-05 — Frozen shimmer drains rather than holding: what climbs out of the band leaves, the loop stays bounded. Shimmer 0 freezes for good.
- 2026-10-05 — Grain's grid follows locates and loops by the song position's distance from where it should be (more than a host block: a jump), not only by a new slice number.
- 2026-10-05 — An order saved with fewer modules keeps its slots; the new modules (off in it) fill the rest in the default order.
- 2026-10-05 — Performance layer: in EffectForce itself (one insert on the master has every effect, the scenes and the looper), not a plugin of its own. Scenes lock parameters the Octatrack way (each scene only what it locks); a module a scene switches is a send, so tails ring out; the looper is a fixed stage (Pre or Post), not a chain slot; its buffers only where it is armed; new parameters appended after all others.