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:
@@ -678,6 +678,149 @@ static void testComponentStateGarbage() {
|
||||
CHECK(deserializeComponentState(t).map.zones.empty());
|
||||
}
|
||||
|
||||
// --- S7: extractChannel / decodeChannels (cross-mode channel policy) ----------
|
||||
|
||||
static void testExtractChannelStereo() {
|
||||
// Interleaved stereo [L0,R0,L1,R1,...]; extract channel 0 -> L's, channel 1 -> R's.
|
||||
const std::vector<AudioSample> in{0.1f, 0.9f, 0.2f, 0.8f, 0.3f, 0.7f};
|
||||
const std::vector<AudioSample> l = extractChannel(in, 2, 0);
|
||||
const std::vector<AudioSample> r = extractChannel(in, 2, 1);
|
||||
CHECK(l.size() == 3 && approx(l[0], 0.1) && approx(l[1], 0.2) && approx(l[2], 0.3));
|
||||
CHECK(r.size() == 3 && approx(r[0], 0.9) && approx(r[1], 0.8) && approx(r[2], 0.7));
|
||||
}
|
||||
|
||||
static void testExtractChannelClampsToLast() {
|
||||
// A mono source asked for channel 1 yields channel 0 (clamp to last) — the dual-mono block.
|
||||
const std::vector<AudioSample> mono{0.1f, 0.2f, 0.3f};
|
||||
const std::vector<AudioSample> ch1 = extractChannel(mono, 1, 1);
|
||||
CHECK(ch1.size() == 3 && approx(ch1[0], 0.1) && approx(ch1[2], 0.3)); // == channel 0
|
||||
CHECK(extractChannel({}, 2, 0).empty()); // empty in
|
||||
CHECK(extractChannel({0.1f}, 0, 0).empty()); // zero stride
|
||||
}
|
||||
|
||||
static void testDecodeChannelsMonoModeDownmixes() {
|
||||
// MONO mode: a stereo source averages to one channel (the existing policy), framesR empty.
|
||||
const std::vector<AudioSample> stereo{1.0f, 0.0f, 0.4f, 0.6f}; // frames (1,0) and (0.4,0.6)
|
||||
const DecodedZonePcm d = decodeChannels(stereo, 2, ChannelMode::Mono, 48000);
|
||||
CHECK(d.monoFrames.size() == 2 && approx(d.monoFrames[0], 0.5) && approx(d.monoFrames[1], 0.5));
|
||||
CHECK(d.framesR.empty()); // mono mode -> single channel
|
||||
CHECK(d.sampleRate == 48000);
|
||||
}
|
||||
|
||||
static void testDecodeChannelsStereoModeStereoSource() {
|
||||
// STEREO mode + stereo source: channels taken as-is (L/R), both present + distinct.
|
||||
const std::vector<AudioSample> stereo{0.1f, 0.9f, 0.2f, 0.8f};
|
||||
const DecodedZonePcm d = decodeChannels(stereo, 2, ChannelMode::Stereo, 44100);
|
||||
CHECK(d.monoFrames.size() == 2 && approx(d.monoFrames[0], 0.1) && approx(d.monoFrames[1], 0.2));
|
||||
CHECK(d.framesR.size() == 2 && approx(d.framesR[0], 0.9) && approx(d.framesR[1], 0.8));
|
||||
}
|
||||
|
||||
static void testDecodeChannelsStereoModeMonoSourceDualMono() {
|
||||
// STEREO mode + mono source: dual-mono — framesR duplicates channel 0 (centered, not silent).
|
||||
const std::vector<AudioSample> mono{0.3f, 0.6f, 0.9f};
|
||||
const DecodedZonePcm d = decodeChannels(mono, 1, ChannelMode::Stereo, 44100);
|
||||
CHECK(d.monoFrames.size() == 3);
|
||||
CHECK(d.framesR.size() == 3);
|
||||
for (std::size_t i = 0; i < 3; ++i) CHECK(approx(d.monoFrames[i], d.framesR[i])); // R == L
|
||||
}
|
||||
|
||||
// --- S7: buildTier0Keymap stereo threading ------------------------------------
|
||||
|
||||
static void testBuildKeymapStereoCarriesSecondChannel() {
|
||||
const Keymap km = buildTier0Keymap({0.1f, 0.2f}, 48000, 60, SampleLoop{}, {0.9f, 0.8f});
|
||||
CHECK(km.samples.size() == 1);
|
||||
CHECK(km.samples.size() == 1 && km.samples[0].channelCount() == 2);
|
||||
CHECK(km.samples.size() == 1 && km.samples[0].framesR.size() == 2 &&
|
||||
approx(km.samples[0].framesR[0], 0.9) && approx(km.samples[0].framesR[1], 0.8));
|
||||
}
|
||||
|
||||
static void testBuildKeymapMonoWhenNoSecondChannel() {
|
||||
// No framesR passed -> mono SampleData (byte-identical to the pre-S7 build).
|
||||
const Keymap km = buildTier0Keymap({0.1f, 0.2f}, 48000, 60, SampleLoop{});
|
||||
CHECK(km.samples.size() == 1 && km.samples[0].channelCount() == 1);
|
||||
CHECK(km.samples.size() == 1 && km.samples[0].framesR.empty());
|
||||
}
|
||||
|
||||
static void testBuildKeymapDropsMismatchedSecondChannel() {
|
||||
// A framesR whose length mismatches frames is dropped -> mono (a bad pair never half-plays).
|
||||
const Keymap km = buildTier0Keymap({0.1f, 0.2f, 0.3f}, 48000, 60, SampleLoop{}, {0.9f});
|
||||
CHECK(km.samples.size() == 1 && km.samples[0].channelCount() == 1);
|
||||
}
|
||||
|
||||
static void testBuildZonedKeymapCarriesSecondChannel() {
|
||||
// The zoned build threads each zone's framesR when it length-matches channel 0.
|
||||
std::vector<ResolvedZone> zones;
|
||||
ResolvedZone z; z.lowNote = 0; z.highNote = 127; z.rootNote = 60; zones.push_back(z);
|
||||
std::vector<DecodedZonePcm> decoded;
|
||||
DecodedZonePcm d; d.monoFrames = {0.1f, 0.2f}; d.sampleRate = 44100; d.framesR = {0.9f, 0.8f};
|
||||
decoded.push_back(d);
|
||||
const Keymap km = buildZonedKeymap(zones, decoded);
|
||||
CHECK(km.samples.size() == 1 && km.samples[0].channelCount() == 2);
|
||||
CHECK(km.samples.size() == 1 && km.samples[0].framesR.size() == 2 &&
|
||||
approx(km.samples[0].framesR[1], 0.8));
|
||||
}
|
||||
|
||||
// --- S7: component state v4 (channel mode) ------------------------------------
|
||||
|
||||
static void testComponentStateV4RoundTripStereo() {
|
||||
ComponentState s;
|
||||
s.selectionId = "pick";
|
||||
s.channelMode = ChannelMode::Stereo;
|
||||
s.map.zones.push_back(zone("z0", 0, 127, /*override=*/std::nullopt));
|
||||
const ComponentState back = deserializeComponentState(serializeComponentState(s));
|
||||
CHECK(back.selectionId == "pick");
|
||||
CHECK(back.channelMode == ChannelMode::Stereo); // mode round-trips
|
||||
CHECK(back.map.zones.size() == 1 && back.map.zones[0].sampleId == "z0");
|
||||
}
|
||||
|
||||
static void testComponentStateV4RoundTripMono() {
|
||||
ComponentState s;
|
||||
s.selectionId = "pick";
|
||||
s.channelMode = ChannelMode::Mono;
|
||||
const ComponentState back = deserializeComponentState(serializeComponentState(s));
|
||||
CHECK(back.selectionId == "pick");
|
||||
CHECK(back.channelMode == ChannelMode::Mono);
|
||||
}
|
||||
|
||||
static void testComponentStateV4DefaultIsMono() {
|
||||
// A default-constructed state serializes with mono and restores mono (preserves behavior).
|
||||
const ComponentState back = deserializeComponentState(serializeComponentState(ComponentState{}));
|
||||
CHECK(back.channelMode == ChannelMode::Mono);
|
||||
CHECK(back.selectionId.empty() && back.map.zones.empty());
|
||||
}
|
||||
|
||||
static void testComponentStateV3LiftsToMono() {
|
||||
// A pre-S7 v3 blob (selection + zones, no mode byte) lifts to channelMode = mono, with the
|
||||
// selection and zones intact. Build a v3 blob by hand: tag 3, id length + id, zones payload.
|
||||
std::vector<std::uint8_t> v3;
|
||||
v3.push_back(3); v3.push_back(0); v3.push_back(0); v3.push_back(0); // version 3
|
||||
const std::string id = "legacy";
|
||||
v3.push_back(static_cast<std::uint8_t>(id.size())); v3.push_back(0); v3.push_back(0); v3.push_back(0);
|
||||
v3.insert(v3.end(), id.begin(), id.end());
|
||||
v3.push_back(0); v3.push_back(0); v3.push_back(0); v3.push_back(0); // zone count 0
|
||||
const ComponentState back = deserializeComponentState(v3);
|
||||
CHECK(back.selectionId == "legacy");
|
||||
CHECK(back.channelMode == ChannelMode::Mono); // pre-S7 default
|
||||
CHECK(back.map.zones.empty());
|
||||
}
|
||||
|
||||
static void testComponentStateV1V2LiftToMono() {
|
||||
// The older lifts (v1 single-selection, v2 zones-only) also default to mono under v4 read.
|
||||
const ComponentState v1 = deserializeComponentState(serializeSelection("old"));
|
||||
CHECK(v1.channelMode == ChannelMode::Mono && v1.selectionId == "old");
|
||||
PerformanceMap m; m.zones.push_back(zone("s", 12, 24));
|
||||
const ComponentState v2 = deserializeComponentState(serializePerformance(m));
|
||||
CHECK(v2.channelMode == ChannelMode::Mono && v2.map.zones.size() == 1);
|
||||
}
|
||||
|
||||
static void testComponentStateV4TruncatedModeByte() {
|
||||
// A v4 blob truncated right after the version tag (no mode byte) -> empty, mono default holds.
|
||||
std::vector<std::uint8_t> t{4, 0, 0, 0}; // version 4, nothing after
|
||||
const ComponentState back = deserializeComponentState(t);
|
||||
CHECK(back.channelMode == ChannelMode::Mono);
|
||||
CHECK(back.selectionId.empty() && back.map.zones.empty());
|
||||
}
|
||||
|
||||
int main() {
|
||||
testSelectByIdHit();
|
||||
testSelectEmptyIdIsSilence();
|
||||
@@ -726,6 +869,21 @@ int main() {
|
||||
testComponentStateV1BackCompat();
|
||||
testComponentStateV2BackCompat();
|
||||
testComponentStateGarbage();
|
||||
testExtractChannelStereo();
|
||||
testExtractChannelClampsToLast();
|
||||
testDecodeChannelsMonoModeDownmixes();
|
||||
testDecodeChannelsStereoModeStereoSource();
|
||||
testDecodeChannelsStereoModeMonoSourceDualMono();
|
||||
testBuildKeymapStereoCarriesSecondChannel();
|
||||
testBuildKeymapMonoWhenNoSecondChannel();
|
||||
testBuildKeymapDropsMismatchedSecondChannel();
|
||||
testBuildZonedKeymapCarriesSecondChannel();
|
||||
testComponentStateV4RoundTripStereo();
|
||||
testComponentStateV4RoundTripMono();
|
||||
testComponentStateV4DefaultIsMono();
|
||||
testComponentStateV3LiftsToMono();
|
||||
testComponentStateV1V2LiftToMono();
|
||||
testComponentStateV4TruncatedModeByte();
|
||||
|
||||
if (g_fail == 0) std::printf("sample_map: all tests passed\n");
|
||||
return g_fail != 0;
|
||||
|
||||
Reference in New Issue
Block a user