Skip to content

CI release build, NEON performance, Force fixes, hardware-accurate sound, 30 presets - #1

Merged
Devko merged 5 commits into
ccr-06c1bcc3-vpdjx6from
ci-perf-presets
Oct 5, 2026
Merged

Devko merged 5 commits into
ccr-06c1bcc3-vpdjx6from
ci-perf-presets

Conversation

@Devko

@Devko Devko commented Oct 5, 2026 •

Copy link
Copy Markdown
Owner

What's in it

Release build in CI (.github/workflows/build.yml, as PolyForce's): the device .so is built in arm32v7/gcc:11-bullseye (GCC 11, glibc 2.31) under QEMU, profile-guided, with the test suite run against the objects it is linked from; the sanitizer suite on x86; the zip checked with the plugin catalog's catalog_check.py and kept as an artifact. A vX.Y.Z tag publishes a GitHub release (a prerelease while 0.x). The release .so needs glibc 2.27.

Performance on the Force (make bench-device, % of the 2902 µs block, PGO):

Case before after
idle 0.38% 0.17%
Init, one held note 3.64% 2.79%
heavy patch, Duo (now with the new feedback loop) 3.70% 3.55%

NEON ladder (four stages in vector lanes, the loop solved as an affine chain with one division), no divisions in the oscillators, four-lane cutoff coefficients, no libm in the control step, a cheaper idle path that keeps the control grid (block sizes still never change the sound).

Fixes: buttons and the preset stepper on the Force (each tap is a press; detents measured from the plugin's own value — PolyForce's device-run rules), Multi trigger on key releases, poly aftertouch, CC 121, flush-to-zero scoped to the engine, per-instance random seeds, /tmp/subforce.trace diagnostics, test host taps and turns like the Force.

The hardware's behaviour (from its manuals): the mixer's own feedback loop (was post-VCA), resonance self-oscillating past 70%, asymmetric Multidrive, linear attack, Loop through the release, white → pink → dark noise (pink default). Factory presets converted to keep their character (median change 1 dB per third octave).

Presets: 30 new (57 in 7 categories, new Sequence and Pad), many for melodic techno; all level-matched to −18 LUFS; a melodic techno section in the user guide.

Checked

  • make test and make test-arm: 468 passed (x86 ASan/UBSan, and ARM under qemu)
  • CI green on every commit; catalog check OK: subforce 0.0.1 (0 warnings)
  • Installed and benched on the Force

🤖 Generated with Claude Code

Devko and others added 4 commits October 5, 2026 12:16
…g-checked

The plugin catalog lists only plugins that need glibc 2.32 or less (they then load on MPC OS
2.x too); Ubuntu 24.04's cross toolchain made the .so need 2.38 (the C23 __isoc23_sscanf). The
workflow, as PolyForce's, builds the device .so in arm32v7/gcc:11-bullseye (GCC 11, glibc
2.31) under QEMU, profile-guided, runs the suite against the objects it is linked from,
packages, and checks the zip with the catalog's own catalog_check.py; the sanitizer suite runs
on x86. A vX.Y.Z tag publishes a GitHub release (a prerelease for 0.x: the catalog's beta
channel).

Makefile: ARM_PREFIX (empty for a native ARM build) and ARM_RUN (qemu-arm; empty natively)
replace ARM_TOOL and the hard-coded qemu-arm; local defaults are unchanged. The PGO trainer is
linked dynamically (static against glibc 2.31, std::thread fails before glibc 2.34).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
… and idle

Measured on the Force (Cortex-A17 at 1.8 GHz), profile-guided: Init on one note 3.64% -> 2.67%
of the block (p99 4.09% -> 3.00%), the heavy Duo patch 3.70% -> 2.87%, idle 0.38% -> 0.17%.

The voice was queueing divisions behind VFP's one unpipelined divider (18 cycles each, one
every 14): 10 per ladder tick, 14 in the audio loop's body, one in every oscillator tick.

- dsp/simd.h: four floats at a time, NEON on the device and GCC's generic vectors elsewhere (the
  x86 tests run the same arithmetic): reciprocal estimates refined by Newton-Raphson steps, and
  four-lane exp2Fast / tanFast / tanhXdX with the scalar versions' polynomials.
- The ladder: the four stages' nonlinear gains and 1 / (1 + f t) in vector lanes; the loop
  solved as y_k = al_k + be_k y3 by a short scalar chain and closed with one division; the
  stages' inputs, outputs and integrators in one vector step each. 162 -> 103 ns a tick. (An
  all-NEON prefix scan was slower: on this core a q-register op issues every 2 cycles, and the
  serial part is quicker in VFP.)
- The oscillators' advance() takes the stretch's length instead of dividing for it, its rare
  divisions are reciprocals, and it is inlined; the shape's tri/pulse checks are flags.
- renderRun in two passes: the envelopes and the cutoff coefficients (four at a time) for the
  run first, then the audio; a still wave knob's shape is worked out once per run.
- control(): no powf / log2f (log2Fast, the cutoff note cached), multiplications for the
  divisions, exp glide by exp2Fast; Multidrive's in and out gains ramp separately.
- Silent: only time moves on, on the same control grid (block sizes still never change the
  sound); the values snap to their targets when a note wakes the voice.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…mode, seeds

On the Force every value MPC sets is the value it last read back plus its step (PolyForce's
first device run found both of these there; SubForce had the same code):
- Buttons fired only on a rising edge and waited for a release. A tap toggles the read-back,
  and a button always reads back 0, so no release ever comes: INIT, SAVE, RANDOM, PREV/NEXT
  and the browser's arrows worked once per instance. Each 1 is now a press.
- The preset stepper measured a turn's detents from MPC's previous value, which the last
  detent had moved by one item (under kQuant): a Q-Link or wheel turn moved one preset and
  stalled. Each detent is now measured from the plugin's own value.
The test host taps and turns the way the Force does (the old surface code fails 10 checks),
and its clock can run a turn's events a few ms apart (sft::Turn).

Engine: Multi trigger restarted the envelopes when a key was released back to one still held
(a trill attacked twice, a Duo pair's other key re-attacked on every lift); it retriggers on
keys struck now.

Plugin:
- Poly aftertouch (what the pads send) on a sounding key counts as pressure; CC 121 resets
  bend, wheel, pressure and pedal.
- Flush-to-zero covers the engine only: the song position, automation and display callbacks
  run in MPC's own FP mode.
- Every instance seeds its own random numbers (noise, drift, S&H, RANDOM): two layered
  instances played the same noise. SF_FIXED_SEED for the tests and demos.
- Diagnostics: with /tmp/subforce.trace present every setParameter is logged to
  /tmp/subforce.log (PolyForce's plugin/trace.*).
- sfbench checks dlsym; plugin-package warns when the .so needs glibc over the catalog's 2.32.

Tests: S&H must change between cycles and hold within one, Smooth must move and glide
monotonically (both passed for a stuck source). Docs: the Force's input rules, diagnostics,
libstdc++, the trigger and pedal behaviour, the new MIDI messages.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…ise; 30 more presets

Where the Sub 37 / Subsequent 37 manuals say how the panel behaves, SubForce now does the same:
- Feedback: the mixer's own output back into its feedback channel (the manual's block diagram:
  a mixer-only loop before the filter), through that channel's overload (a cubic clipper),
  AC coupled at 150 Hz and band-limited at 7 kHz. Tuned on renders: +1 dB and more drive at half
  the knob, unity loop gain at 85%, grit, then the chaos of an overdriven loop (+6 dB, upper
  harmonics +9 dB); no subharmonic motorboating. It was the post-VCA output (the Minimoog trick).
- Resonance: self-oscillation past 70% of the knob ("settings above 7"), 4.6 at full as before.
- Multidrive's second stage is asymmetric: tube-like even harmonics in the middle of its range
  (a triangle's 2nd harmonic at -20 dB at 50%), subtle low, toward symmetric hard clipping at
  full.
- Envelopes: the attack is linear (the Sub 37's default), and Loop runs delay, attack, hold,
  decay and release ("will loop continuously"); with sustain at 0 it is the D-A-H-D cycle it was.
- Noise colour: white -> pink (Kellet's economy filter at 88.2 kHz, the Sub 37's noise, the new
  default) -> dark, the same loudness at every colour (from the filters' responses), a 30 Hz
  high-pass against rumble.
Noise and the feedback loop only run while their level is up (the heavy bench patch pays for
them, Init doesn't).

Presets: the factory presets' resonance is converted to keep their ladder feedback; Growl,
Screamer and Noise Sweep retuned for the new loop and noise (median change of the 27 renders:
1.0 dB per third octave). 30 new ones from Sub 37 practice and Stephan Bodzin's documented
use of it (rolling 16th basslines, fifth basslines, legato smears, resonant sequences, acid,
big leads, Duo pads, risers), two new categories (Sequence, Pad) with demo phrases; all 57
level-matched to -18 LUFS.

Tests: the resonance edge (65% silent, 76% sings), bass loss under it, Multidrive's even
harmonics, the feedback's level and grit, the noise colours within 0.3 dB, the loop, the linear
attack. Docs: the user guide (and a melodic techno section), performance, architecture, roadmap.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@Devko Devko changed the title CI release build, NEON performance, Force fixes, Sub 37 sound, 30 presets CI release build, NEON performance, Force fixes, hardware-accurate sound, 30 presets Oct 5, 2026
…views

For the first public preview (0.0.1, the plugin catalog's beta channel):
- CHANGELOG.md; the release workflow takes the tag's "## X.Y.Z" section as the release notes
  (the catalog shows them) and fails before publishing if there is none.
- docs/img: the pages rendered offline from the skin (OSC, FILTER, AMP, MOD, KEYS); FILTER heads
  the README, the user guide shows them all.
- README: what has been checked on a Force (MPC OS 3.9), MPC OS 3.x as the requirement (release
  builds use glibc up to 2.27 but need GCC 11's libstdc++; 2.x untested), installing from
  Releases, where presets live, uninstalling, the changelog, the status.
- Docs reviewed against the code: the 0.x version rule (param_compat stays 0 while the list may
  change; append-only from v0.1), how CI releases from tags, binary compatibility, the test and
  demo-phrase tables, the status line, the browser (FAVORITES, RECENT, RND, the internal drive),
  MIDI channels, figures (+6 dB feedback; the subharmonic check now in the suite: -101 dB).
- The product names of the original stay out of the docs and comments, as the project's decision
  says; "the original" is defined once and used throughout.
- The feedback loop's coefficient is worked out once, in the constructor.
- Vendored release.py, a marked local patch: INSTALL.md no longer puts a slash after user-data
  files. The manifest's about/requires text updated.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@Devko
Devko merged commit 36478b4 into ccr-06c1bcc3-vpdjx6 Oct 5, 2026
4 checks passed
@Devko
Devko deleted the ci-perf-presets branch October 5, 2026 12:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant