CLAP: HEXAWE voices, bit-identical to the AU #8

Merged
peterswimm merged 1 commit from feat/clap-lgpt-voice into main 2026-10-16 00:38:43 +00:00
Owner

Summary

The CLAP now plays the 212 HEXAWE voices with output bit-identical to the AU.

The voice

core/lgpt_voice.{h,cpp} is a C++17 port of LGPTVoiceConfig and LGPTVoice. It adds no dependencies, and render never allocates.

  • Arithmetic: Swift's wrapping and truncating operations are done in unsigned types, and the filter difference is computed in 64 bits.
  • No fused multiply-adds: the file is built with -ffp-contract=off. Without it, Clang on arm64 and GCC in gnu++17 mode fuse multiply-adds and the output drifts.

Choosing a voice

  • The voice parameter (101, lgpt.voice): 0 is Init (the wavetable engine), and n is the n-th voice. The catalog is sorted the same way as the AU's, so n is the AU's factory preset number.
  • Host display: the host sees the voice names through value_to_text.
  • Saved as an index: like kBanks, voices may only ever be appended to the catalog.

Loading and tempo

  • Hand-off: the main thread builds a voice and puts it in an atomic mailbox. process() takes it at the next block or note.
  • Freeing: the replaced voice goes back through a lock-free queue and is freed on the main thread. The audio thread never allocates, locks or frees.
  • Tempo: the transport's tempo drives the voice's tables. Without one, the voice uses its song's own tempo.
  • Finding the voices works like the banks: O1NK_VOICES, then the bundle's Resources/hexawe, then o1nk-voices/ beside the .so, then the share directories.
  • Bundling: CMake copies the sidecars, samples and credits, but not the .device.json files.

UDG

udg_parity.py now checks that the CLAP's non-OPSL parameters are exactly EXTENSION_PARAMS. Before this, a new extension parameter went unchecked.

Testing

clap/tests/lgpt.sh runs each check below alongside a deliberately broken control, which must fail:

Check Result Control
C++ voice vs Swift voice, every sidecar at the root note and root+7, 2 s in uneven blocks 424/424 identical perturbed k-rate: 216/424
Voice catalog vs the AU's 212 voices, same order and names one inserted voice: catalog differs
Plugin through a stub host: selection, hand-off, transport tempo, state restore, 60 voice switches 922 checks, 0 failed no voices: fails
C++ voice vs LGPT itself 418/418 bit-exact, plus the same 6 renders with no reference as the Swift voice perturbed k-rate: breaks the match

Other checks:

  • clap/tests/parity.sh: passes, with 3 new voice scenarios. Its two new controls each fail only the voice scenarios.
  • Parity scripts: udg_parity.py (2 extensions) and opsl_parity.py pass.
  • AU suite: tests/lgpt/run.sh passes.
  • Sanitizers: the plugin test and the renderer pass under ASan and UBSan.
  • Build: the macOS CMake build has no warnings, and the bundle contains 212 sidecars and 63 samples.

Not covered

  • Linux: the Docker build for the handhelds hasn't been run.
  • Handheld screen: it can't pick a voice, because UDG has no control for one. Voices can be chosen from the host's parameter list.
## Summary The CLAP now plays the 212 HEXAWE voices with output bit-identical to the AU. ### The voice `core/lgpt_voice.{h,cpp}` is a C++17 port of `LGPTVoiceConfig` and `LGPTVoice`. It adds no dependencies, and render never allocates. - **Arithmetic:** Swift's wrapping and truncating operations are done in unsigned types, and the filter difference is computed in 64 bits. - **No fused multiply-adds:** the file is built with `-ffp-contract=off`. Without it, Clang on arm64 and GCC in gnu++17 mode fuse multiply-adds and the output drifts. ### Choosing a voice - **The `voice` parameter** (101, `lgpt.voice`): 0 is Init (the wavetable engine), and n is the n-th voice. The catalog is sorted the same way as the AU's, so n is the AU's factory preset number. - **Host display:** the host sees the voice names through `value_to_text`. - **Saved as an index:** like `kBanks`, voices may only ever be appended to the catalog. ### Loading and tempo - **Hand-off:** the main thread builds a voice and puts it in an atomic mailbox. `process()` takes it at the next block or note. - **Freeing:** the replaced voice goes back through a lock-free queue and is freed on the main thread. The audio thread never allocates, locks or frees. - **Tempo:** the transport's tempo drives the voice's tables. Without one, the voice uses its song's own tempo. - **Finding the voices** works like the banks: `O1NK_VOICES`, then the bundle's `Resources/hexawe`, then `o1nk-voices/` beside the `.so`, then the share directories. - **Bundling:** CMake copies the sidecars, samples and credits, but not the `.device.json` files. ### UDG `udg_parity.py` now checks that the CLAP's non-OPSL parameters are exactly `EXTENSION_PARAMS`. Before this, a new extension parameter went unchecked. ## Testing `clap/tests/lgpt.sh` runs each check below alongside a deliberately broken control, which must fail: | Check | Result | Control | |---|---|---| | C++ voice vs Swift voice, every sidecar at the root note and root+7, 2 s in uneven blocks | 424/424 identical | perturbed k-rate: 216/424 | | Voice catalog vs the AU's | 212 voices, same order and names | one inserted voice: catalog differs | | Plugin through a stub host: selection, hand-off, transport tempo, state restore, 60 voice switches | 922 checks, 0 failed | no voices: fails | | C++ voice vs LGPT itself | 418/418 bit-exact, plus the same 6 renders with no reference as the Swift voice | perturbed k-rate: breaks the match | Other checks: - **`clap/tests/parity.sh`:** passes, with 3 new voice scenarios. Its two new controls each fail only the voice scenarios. - **Parity scripts:** `udg_parity.py` (2 extensions) and `opsl_parity.py` pass. - **AU suite:** `tests/lgpt/run.sh` passes. - **Sanitizers:** the plugin test and the renderer pass under ASan and UBSan. - **Build:** the macOS CMake build has no warnings, and the bundle contains 212 sidecars and 63 samples. ## Not covered - **Linux:** the Docker build for the handhelds hasn't been run. - **Handheld screen:** it can't pick a voice, because UDG has no control for one. Voices can be chosen from the host's parameter list.
- core/lgpt_voice.{h,cpp}: C++17 port of LGPTVoiceConfig and LGPTVoice
  (no new dependencies; render does not allocate). Swift's wrapping and
  truncating arithmetic is done in unsigned types, the filter difference
  in 64 bits, and the file builds with -ffp-contract=off so nothing fuses.
- A "voice" parameter (101, lgpt.voice): 0 is Init (the wavetable
  engine), n the n-th voice in the same sorted catalog the AU uses, so n
  matches the AU's factory preset number. It is saved as an index, so
  voices may only be appended, like kBanks.
- The main thread builds a voice and hands it over in an atomic mailbox;
  process() takes it at a block or note, and the replaced voice returns
  through a lock-free queue to be freed on the main thread. The
  transport's tempo drives the tables; without one, the song's own.
- Voices are found like banks (O1NK_VOICES, the bundle's Resources/hexawe,
  o1nk-voices/ beside the .so, share dirs); CMake copies the sidecars,
  samples and credits, not the .device.json files.
- udg_parity.py now checks that the CLAP's non-OPSL parameters are
  exactly EXTENSION_PARAMS; before, a new one went unchecked.

clap/tests/lgpt.sh: C++ vs Swift voice on every sidecar (424/424
identical), catalog identical to the AU's, the plugin through a stub host
(selection, hand-off, tempo, state), and C++ vs LGPT itself (418/418
bit-exact); each with a control that fails. parity.sh gains voice
scenarios and two voice-only controls.

Co-Authored-By: Claude <noreply@anthropic.com>
peterswimm deleted branch feat/clap-lgpt-voice 2026-10-16 00:38:43 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
peterswimm/01nk!8
No description provided.