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:
@@ -562,6 +562,118 @@ static void testPolyphonyMixesAdditively() {
|
||||
CHECK(approx(out[0], 2.0, 1e-4)); // both voices sum
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 7. Stereo channel dimension (S7).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// A distinct-per-channel stereo DC sample: L = `l`, R = `r` everywhere. A stereo render
|
||||
// must keep them distinct; a mono render (channel 0 only) sees L.
|
||||
static SampleData stereoDcSample(std::size_t frames, float l, float r, int rootNote = 60) {
|
||||
SampleData s;
|
||||
s.frames.assign(frames, l);
|
||||
s.framesR.assign(frames, r);
|
||||
s.rootNote = rootNote;
|
||||
return s;
|
||||
}
|
||||
|
||||
static void testChannelCount() {
|
||||
// Mono: framesR empty -> 1 channel. Stereo: matching-length framesR -> 2.
|
||||
CHECK(dcSample(10, 60).channelCount() == 1);
|
||||
CHECK(stereoDcSample(10, 1.0f, -1.0f).channelCount() == 2);
|
||||
// A mismatched framesR length is treated as mono (a bad pair never half-plays).
|
||||
SampleData bad = dcSample(10, 60);
|
||||
bad.framesR.assign(5, 0.5f); // wrong length
|
||||
CHECK(bad.channelCount() == 1);
|
||||
}
|
||||
|
||||
static void testStereoRenderKeepsChannelsDistinct() {
|
||||
// A stereo sample (L=1.0, R=-1.0) rendered stereo must emit L and R distinctly, each
|
||||
// scaled by velocity (full here). If the engine copied L to both channels the R check fails.
|
||||
Keymap km = Keymap::singleSampleChromatic(stereoDcSample(100, 1.0f, -1.0f, 60));
|
||||
VoiceEngine eng(1, km, flatAdsr());
|
||||
eng.noteOn(60, 127);
|
||||
|
||||
std::vector<AudioSample> left(8, 0.f), right(8, 0.f);
|
||||
eng.render(left.data(), right.data(), 8);
|
||||
for (std::size_t i = 0; i < 8; ++i) {
|
||||
CHECK(approx(left[i], 1.0, 1e-4)); // channel 0
|
||||
CHECK(approx(right[i], -1.0, 1e-4)); // channel 1 — distinct, NOT a copy of L
|
||||
}
|
||||
}
|
||||
|
||||
static void testMonoSamplePlaysDualMonoInStereo() {
|
||||
// A MONO sample rendered through the stereo path plays dual-mono: both channels equal
|
||||
// (centered), not silent on the right. The cross-mode "mono source in stereo mode" case.
|
||||
Keymap km = Keymap::singleSampleChromatic(dcSample(100, 60)); // mono, DC 1.0
|
||||
VoiceEngine eng(1, km, flatAdsr());
|
||||
eng.noteOn(60, 127);
|
||||
std::vector<AudioSample> left(8, 0.f), right(8, 0.f);
|
||||
eng.render(left.data(), right.data(), 8);
|
||||
for (std::size_t i = 0; i < 8; ++i) {
|
||||
CHECK(approx(left[i], 1.0, 1e-4));
|
||||
CHECK(approx(right[i], 1.0, 1e-4)); // R == L (dual-mono), not 0
|
||||
}
|
||||
}
|
||||
|
||||
static void testMonoRenderUnchangedByStereoData() {
|
||||
// Regression: the mono render path (renderFrame) reads channel 0 ONLY and is byte-identical
|
||||
// whether or not a second channel is present. A stereo sample rendered mono == its L channel.
|
||||
Keymap kmS = Keymap::singleSampleChromatic(stereoDcSample(100, 0.75f, -0.25f, 60));
|
||||
VoiceEngine engS(1, kmS, flatAdsr());
|
||||
engS.noteOn(60, 127);
|
||||
std::vector<AudioSample> mono;
|
||||
engS.render(mono, 8); // the mono overload
|
||||
for (std::size_t i = 0; i < 8; ++i) CHECK(approx(mono[i], 0.75, 1e-4)); // == L, ignores R
|
||||
}
|
||||
|
||||
static void testStereoRenderAdvancesLikeMonoRepitch() {
|
||||
// The stereo path must advance the read head by the SAME per-frame ratio as the mono path,
|
||||
// so repitch is identical. Play a stereo sine (both channels the same signal) an octave up
|
||||
// and confirm the observed period halves — the mono repitch assertion, on the stereo path.
|
||||
const std::size_t frames = 8000;
|
||||
const double cycles = 20.0;
|
||||
const double nativePeriod = static_cast<double>(frames) / cycles; // 400
|
||||
SampleData s;
|
||||
s.frames.resize(frames);
|
||||
s.framesR.resize(frames);
|
||||
for (std::size_t i = 0; i < frames; ++i) {
|
||||
const float v = static_cast<float>(std::sin(2.0 * kPi * cycles *
|
||||
static_cast<double>(i) / static_cast<double>(frames)));
|
||||
s.frames[i] = v;
|
||||
s.framesR[i] = v;
|
||||
}
|
||||
s.rootNote = 60;
|
||||
Keymap km = Keymap::singleSampleChromatic(std::move(s));
|
||||
VoiceEngine eng(4, km, flatAdsr());
|
||||
eng.noteOn(72, 127); // +1 octave
|
||||
std::vector<AudioSample> left(frames / 2, 0.f), right(frames / 2, 0.f);
|
||||
eng.render(left.data(), right.data(), frames / 2);
|
||||
CHECK(approx(observedPeriodFrames(left), nativePeriod / 2.0, 2.0));
|
||||
CHECK(approx(observedPeriodFrames(right), nativePeriod / 2.0, 2.0)); // R repitches identically
|
||||
}
|
||||
|
||||
static void testStereoRenderSumsVoicesPerChannel() {
|
||||
// Two voices on a stereo sample sum PER CHANNEL (additive polyphony holds in stereo).
|
||||
Keymap km = Keymap::singleSampleChromatic(stereoDcSample(100, 0.5f, -0.5f, 60));
|
||||
VoiceEngine eng(4, km, flatAdsr());
|
||||
eng.noteOn(60, 127);
|
||||
eng.noteOn(60, 127); // second voice, same note
|
||||
std::vector<AudioSample> left(1, 0.f), right(1, 0.f);
|
||||
eng.render(left.data(), right.data(), 1);
|
||||
CHECK(approx(left[0], 1.0, 1e-4)); // 0.5 + 0.5
|
||||
CHECK(approx(right[0], -1.0, 1e-4)); // -0.5 + -0.5
|
||||
}
|
||||
|
||||
static void testStereoRenderNullBufferIsNoOp() {
|
||||
Keymap km = Keymap::singleSampleChromatic(stereoDcSample(100, 1.0f, -1.0f, 60));
|
||||
VoiceEngine eng(1, km, flatAdsr());
|
||||
eng.noteOn(60, 127);
|
||||
std::vector<AudioSample> buf(4, 0.f);
|
||||
eng.render(nullptr, buf.data(), 4); // null left -> no-op, no crash
|
||||
eng.render(buf.data(), nullptr, 4); // null right -> no-op
|
||||
for (float v : buf) CHECK(approx(v, 0.0, 1e-9)); // untouched
|
||||
}
|
||||
|
||||
int main() {
|
||||
testChromaticSingleRoot();
|
||||
testZonedRangesBoundaries();
|
||||
@@ -582,6 +694,13 @@ int main() {
|
||||
testAbsentLoopGoesSilent();
|
||||
testVelocityToVolume();
|
||||
testPolyphonyMixesAdditively();
|
||||
testChannelCount();
|
||||
testStereoRenderKeepsChannelsDistinct();
|
||||
testMonoSamplePlaysDualMonoInStereo();
|
||||
testMonoRenderUnchangedByStereoData();
|
||||
testStereoRenderAdvancesLikeMonoRepitch();
|
||||
testStereoRenderSumsVoicesPerChannel();
|
||||
testStereoRenderNullBufferIsNoOp();
|
||||
|
||||
if (g_fail == 0) {
|
||||
std::printf("all sampler_core tests passed\n");
|
||||
|
||||
Reference in New Issue
Block a user