S7: stereo channel mode — core channel dimension, v4 mode state, VST3 bus negotiation, editor toggle

Per-instance mono|stereo (default mono, byte-identical). Stereo grows SampleData a 2nd
channel + a per-channel VoiceEngine render; decodeChannels applies the cross-mode policy;
setBusArrangements pins the mode's arrangement and restartComponent(kIoChanged) re-negotiates.
This commit is contained in:
2026-07-26 21:26:21 -04:00
parent ee82b50fb7
commit 4a3551b3b9
11 changed files with 727 additions and 85 deletions
+22 -19
View File
@@ -285,25 +285,28 @@ unchanged (regression).
**Depends on:** S3 (extends the core), S4 (extends the process/bus shell). Independent of
S8/S9.
- [ ] Core channel dimension (pure, S3 extension): `SampleData` carries N-channel
(1 or 2) decoded PCM; `Voice::renderFrame` and `VoiceEngine::render` produce a
per-channel frame; stereo linear interpolation + loop read per channel. Mono stays the
degenerate case (single channel) — no behavior change for existing mono play. Tests:
stereo render asserted against a known 2-channel signal; mono render unchanged.
- [ ] Channel-mode toggle as per-instance state: `mono | stereo` in the instrument's own
component state (setState/getState, alongside the selected sample); default preserves
current behavior (mono). Cross-mode policy: **mono source + stereo mode → dual-mono**
(same signal both channels, centered); **stereo source + mono mode → downmix** (the
existing decode-side policy). The toggle lives in the instrument, never written to the
bank (a performance choice, not a file fact — D-B).
- [ ] Shell: decode fills 1- or 2-channel `SampleData` per the source's channel count
(the S2/bank channel-count intrinsic already exists); the process path renders the
active mode's channel count into the output bus.
- [ ] VST3 bus negotiation: implement `setBusArrangements` so the output bus reports
mono or stereo per the instance's channel mode, and REAPER's routing follows
automatically (no manual channel wiring). **Must-verify before build:** the
`setBusArrangements` / `getBusArrangement` contract and REAPER's mono/stereo instrument
bus expectations against the vendored Steinberg SDK + `reaper_vst3_interfaces.h`.
- [x] Core channel dimension (pure, S3 extension): `SampleData` carries 1- or 2-channel
decoded PCM (`frames` + optional length-matched `framesR`; `channelCount()`);
`Voice::renderFrameStereo` + a `VoiceEngine::render(left,right,n)` overload produce a
per-channel frame sharing one read head + one envelope tick; stereo linear interpolation +
loop read per channel. Mono stays the degenerate case (`renderFrame` reads channel 0 only,
byte-identical). Tests: stereo render asserted against a known 2-channel signal; dual-mono;
per-channel repitch + additive mix; mono render unchanged (regression) — sampler_core_tests.
- [x] Channel-mode toggle as per-instance state: `ChannelMode {Mono,Stereo}` in the
instrument's own component state (v4 = v3 + a channel-mode byte; setState/getState);
default mono. Cross-mode policy in `decodeChannels`: **mono source + stereo mode →
dual-mono**; **stereo source + mono mode → downmix** (existing decode-side policy). The
toggle lives in the instrument, never written to the bank (D-B). v1/v2/v3 blobs lift to v4
with mono default; round-trip + lift tests — sample_map_tests.
- [x] Shell: `decodeRelative` fills 1- or 2-channel `DecodedZonePcm` per the active mode
(source channel count from the WAV layout); the process path renders the host's negotiated
output channel count (stereo into ch0/ch1, mono into ch0) — RT discipline unchanged.
- [x] VST3 bus negotiation: `setBusArrangements` accepts only the mode's arrangement
(kMono/kStereo), else rejects (kResultFalse) but keeps a valid mode arrangement so
`getBusArrangement` (base default) reports it; a runtime mode change repoints the output bus
+ calls `restartComponent(kIoChanged)` so REAPER re-negotiates. **Verified** against the
vendored Steinberg SDK (`ivstaudioprocessor.h` contract, `vstsinglecomponenteffect.cpp`
base impl, `ivsteditcontroller.h` kIoChanged); see handoff notes.
## S8 — ingest through the bank (one gesture: capture/import into bank + assign to instance)
**Goal:** Loading a sample into the sampler is **one gesture** — capture/import-into-bank