Phase S voices: user-set polyphony, mono stack (retrig|legato), isolated preview card, idle-drain retirement; unity bypass preview-only (v7)
This commit is contained in:
@@ -1105,6 +1105,121 @@ static void testComponentStateV6TruncatedVelocity() {
|
||||
CHECK(back.selectionId.empty() && back.map.zones.empty());
|
||||
}
|
||||
|
||||
// --- v7 component state: the Phase S voice-system fields (count / mode / trigger) -------------
|
||||
|
||||
static void testComponentStateVoiceSystemRoundTrip() {
|
||||
// Non-default values on all three fields prove the bytes are read back, not defaulted; the
|
||||
// envelope neighbours (mode, marker, velocity, selection, zones) ride alongside intact.
|
||||
ComponentState s;
|
||||
s.selectionId = "pick";
|
||||
s.channelMode = ChannelMode::Stereo;
|
||||
s.lastConsumedAssignGeneration = 42;
|
||||
s.previewVelocity = 99;
|
||||
s.voiceCount = 5;
|
||||
s.voiceMode = VoiceMode::Mono;
|
||||
s.monoTrigger = MonoTrigger::Legato;
|
||||
s.map.zones.push_back(zone("z0", 0, 127, /*override=*/std::nullopt));
|
||||
const ComponentState back = deserializeComponentState(serializeComponentState(s), 44100.0);
|
||||
CHECK(back.voiceCount == 5);
|
||||
CHECK(back.voiceMode == VoiceMode::Mono);
|
||||
CHECK(back.monoTrigger == MonoTrigger::Legato);
|
||||
CHECK(back.channelMode == ChannelMode::Stereo);
|
||||
CHECK(back.lastConsumedAssignGeneration == 42);
|
||||
CHECK(back.previewVelocity == 99);
|
||||
CHECK(back.selectionId == "pick");
|
||||
CHECK(back.map.zones.size() == 1 && back.map.zones[0].sampleId == "z0");
|
||||
}
|
||||
|
||||
static void testComponentStateVoiceDefaultsRoundTrip() {
|
||||
// A default-constructed state carries {16, Poly, Retrigger} — the pre-Phase-S behavior —
|
||||
// and round-trips it. Locks the constants the engine + editor share.
|
||||
const ComponentState back =
|
||||
deserializeComponentState(serializeComponentState(ComponentState{}), 44100.0);
|
||||
CHECK(back.voiceCount == kDefaultVoiceCount);
|
||||
CHECK(kDefaultVoiceCount == 16 && kMinVoiceCount == 1 && kMaxVoiceCount == 32);
|
||||
CHECK(back.voiceMode == VoiceMode::Poly);
|
||||
CHECK(back.monoTrigger == MonoTrigger::Retrigger);
|
||||
}
|
||||
|
||||
static void testComponentStateVoiceCountExtremesRoundTrip() {
|
||||
// Both range edges survive the single-byte field exactly.
|
||||
for (int vc : {kMinVoiceCount, kMaxVoiceCount}) {
|
||||
ComponentState s;
|
||||
s.voiceCount = vc;
|
||||
const ComponentState back = deserializeComponentState(serializeComponentState(s), 44100.0);
|
||||
CHECK(back.voiceCount == vc);
|
||||
}
|
||||
}
|
||||
|
||||
static void testComponentStateVoiceCountWriterClamps() {
|
||||
// The WRITER never emits an out-of-range byte: above-max clamps to max; a nonsensical
|
||||
// below-min value (a programming error upstream) falls back to the default.
|
||||
ComponentState hi;
|
||||
hi.voiceCount = 99;
|
||||
CHECK(deserializeComponentState(serializeComponentState(hi), 44100.0).voiceCount ==
|
||||
kMaxVoiceCount);
|
||||
ComponentState lo;
|
||||
lo.voiceCount = 0;
|
||||
CHECK(deserializeComponentState(serializeComponentState(lo), 44100.0).voiceCount ==
|
||||
kDefaultVoiceCount);
|
||||
}
|
||||
|
||||
static void testComponentStateV6LiftsVoiceDefaults() {
|
||||
// A GENUINE v6 blob (version tag 6: mode, marker, velocity, id, zones — NO voice bytes)
|
||||
// lifts to the Phase S voice defaults {16, Poly, Retrigger}, its other fields intact.
|
||||
// Hand-built (serializeComponentState now emits v7, so it cannot make a v6 blob). This
|
||||
// proves an already-saved pre-Phase-S instance restores playing exactly as it did.
|
||||
std::vector<std::uint8_t> v6;
|
||||
v6.push_back(6); v6.push_back(0); v6.push_back(0); v6.push_back(0); // version 6
|
||||
v6.push_back(1); // channel mode = stereo
|
||||
for (int i = 0; i < 8; ++i) v6.push_back(0); // marker = 0
|
||||
v6.push_back(111); // preview velocity
|
||||
const std::string id = "saved";
|
||||
v6.push_back(static_cast<std::uint8_t>(id.size())); v6.push_back(0); v6.push_back(0); v6.push_back(0);
|
||||
v6.insert(v6.end(), id.begin(), id.end());
|
||||
v6.push_back(0); v6.push_back(0); v6.push_back(0); v6.push_back(0); // zone count 0
|
||||
const ComponentState back = deserializeComponentState(v6, 44100.0);
|
||||
CHECK(back.voiceCount == kDefaultVoiceCount);
|
||||
CHECK(back.voiceMode == VoiceMode::Poly);
|
||||
CHECK(back.monoTrigger == MonoTrigger::Retrigger);
|
||||
CHECK(back.previewVelocity == 111); // the v6 byte still honored
|
||||
CHECK(back.channelMode == ChannelMode::Stereo);
|
||||
CHECK(back.selectionId == "saved");
|
||||
CHECK(back.map.zones.empty());
|
||||
}
|
||||
|
||||
static void testComponentStateV7CorruptVoiceBytesFallBack() {
|
||||
// Out-of-range voice bytes in a v7 blob fall back to each field's DEFAULT (the
|
||||
// previewVelocity corrupt-byte precedent) — a corrupt blob never silences or distorts the
|
||||
// instance to an edge the user never chose. Build v7 by serializing, then vandalize the
|
||||
// three voice bytes in place (offsets: 4 version + 1 mode + 8 marker + 1 velocity = 14).
|
||||
ComponentState s;
|
||||
s.voiceCount = 7;
|
||||
s.voiceMode = VoiceMode::Mono;
|
||||
s.monoTrigger = MonoTrigger::Legato;
|
||||
std::vector<std::uint8_t> bytes = serializeComponentState(s);
|
||||
bytes[14] = 0; // voice count 0: below kMinVoiceCount
|
||||
bytes[15] = 7; // voice mode: not a legal {0,1} value
|
||||
bytes[16] = 9; // mono trigger: not a legal {0,1} value
|
||||
const ComponentState back = deserializeComponentState(bytes, 44100.0);
|
||||
CHECK(back.voiceCount == kDefaultVoiceCount);
|
||||
CHECK(back.voiceMode == VoiceMode::Poly); // non-1 mode byte -> Poly default
|
||||
CHECK(back.monoTrigger == MonoTrigger::Retrigger);
|
||||
}
|
||||
|
||||
static void testComponentStateV7TruncatedVoiceBytes() {
|
||||
// A v7 blob cut INSIDE the three voice bytes -> empty, defaults holding (bounded read).
|
||||
std::vector<std::uint8_t> t{7, 0, 0, 0, 1}; // version 7, mode byte
|
||||
for (int i = 0; i < 8; ++i) t.push_back(0); // full marker
|
||||
t.push_back(64); // velocity byte
|
||||
t.push_back(16); // voice count only —
|
||||
const ComponentState back = deserializeComponentState(t, 44100.0); // mode/trigger cut
|
||||
CHECK(back.voiceCount == kDefaultVoiceCount);
|
||||
CHECK(back.voiceMode == VoiceMode::Poly);
|
||||
CHECK(back.monoTrigger == MonoTrigger::Retrigger);
|
||||
CHECK(back.selectionId.empty() && back.map.zones.empty());
|
||||
}
|
||||
|
||||
// --- MERGE COMPOSITION (S9 v5 marker envelope x S15/S16 v3 play-param payload) ----------------
|
||||
//
|
||||
// The merge of ps-w9-t1-sync (envelope v5, adds the consumed-assignment marker) and
|
||||
@@ -1908,6 +2023,13 @@ int main() {
|
||||
testComponentStateV5LiftsVelocityToMid();
|
||||
testComponentStateV4LiftsVelocityToMid();
|
||||
testComponentStateV6TruncatedVelocity();
|
||||
testComponentStateVoiceSystemRoundTrip();
|
||||
testComponentStateVoiceDefaultsRoundTrip();
|
||||
testComponentStateVoiceCountExtremesRoundTrip();
|
||||
testComponentStateVoiceCountWriterClamps();
|
||||
testComponentStateV6LiftsVoiceDefaults();
|
||||
testComponentStateV7CorruptVoiceBytesFallBack();
|
||||
testComponentStateV7TruncatedVoiceBytes();
|
||||
testV5EnvelopeWithMarkerAndPlayParamsRoundTrip();
|
||||
testV4BlobWithPlayParamsLiftsMarkerZeroKeepsPlay();
|
||||
testReconcileKeepsOnlySelectedFullRangeZone();
|
||||
|
||||
Reference in New Issue
Block a user