Phase S FB1 (r11): Sample-view recomposition — knob deck + elastic full-width hero + curve popup w/ right-click delete + post-mixer master gain (ComponentState v8)
This commit is contained in:
@@ -32,6 +32,7 @@
|
||||
|
||||
#include "../src/bank_book.h"
|
||||
#include "../src/bank_model.h"
|
||||
#include "../src/vst/master_gain.h" // masterGainMaxLinear (the v8 master-gain wire cap)
|
||||
|
||||
using namespace reasampler;
|
||||
|
||||
@@ -1220,6 +1221,105 @@ static void testComponentStateV7TruncatedVoiceBytes() {
|
||||
CHECK(back.selectionId.empty() && back.map.zones.empty());
|
||||
}
|
||||
|
||||
// --- v8 component state: the FB1 post-mixer master gain (linear double) -----------------------
|
||||
|
||||
static void testComponentStateMasterGainRoundTrip() {
|
||||
// A non-default gain proves the bytes are read back, not defaulted; the envelope
|
||||
// neighbours (voice bytes, velocity, selection, zones) ride alongside intact.
|
||||
ComponentState s;
|
||||
s.selectionId = "pick";
|
||||
s.previewVelocity = 99;
|
||||
s.voiceCount = 5;
|
||||
s.masterGainLinear = 0.25; // -12.04 dB
|
||||
s.map.zones.push_back(zone("z0", 0, 127, /*override=*/std::nullopt));
|
||||
const ComponentState back = deserializeComponentState(serializeComponentState(s), 44100.0);
|
||||
CHECK(std::fabs(back.masterGainLinear - 0.25) < 1e-12); // an exact double round-trip
|
||||
CHECK(back.voiceCount == 5);
|
||||
CHECK(back.previewVelocity == 99);
|
||||
CHECK(back.selectionId == "pick");
|
||||
CHECK(back.map.zones.size() == 1 && back.map.zones[0].sampleId == "z0");
|
||||
}
|
||||
|
||||
static void testComponentStateMasterGainDefaultAndZeroRoundTrip() {
|
||||
// Default unity round-trips (pre-FB1 output); the -inf bottom (TRUE zero) round-trips
|
||||
// exactly — a user who pulled the gain to silence gets silence back after a save/load.
|
||||
const ComponentState defBack =
|
||||
deserializeComponentState(serializeComponentState(ComponentState{}), 44100.0);
|
||||
CHECK(defBack.masterGainLinear == 1.0);
|
||||
ComponentState zero;
|
||||
zero.masterGainLinear = 0.0;
|
||||
const ComponentState zeroBack =
|
||||
deserializeComponentState(serializeComponentState(zero), 44100.0);
|
||||
CHECK(zeroBack.masterGainLinear == 0.0);
|
||||
}
|
||||
|
||||
static void testComponentStateMasterGainWriterClamps() {
|
||||
// The WRITER never emits an out-of-range value: above the +24 dB cap clamps to the cap;
|
||||
// a negative/non-finite value (a programming error upstream) falls back to unity.
|
||||
ComponentState hi;
|
||||
hi.masterGainLinear = 1000.0;
|
||||
CHECK(std::fabs(deserializeComponentState(serializeComponentState(hi), 44100.0)
|
||||
.masterGainLinear -
|
||||
vst::masterGainMaxLinear()) < 1e-9);
|
||||
ComponentState lo;
|
||||
lo.masterGainLinear = -5.0;
|
||||
CHECK(deserializeComponentState(serializeComponentState(lo), 44100.0).masterGainLinear ==
|
||||
1.0);
|
||||
}
|
||||
|
||||
static void testComponentStateV7LiftsUnityMasterGain() {
|
||||
// A GENUINE v7 blob (version tag 7: mode, marker, velocity, voice bytes, id, zones — NO
|
||||
// master-gain double) lifts to unity, its other fields intact. Hand-built
|
||||
// (serializeComponentState now emits v8, so it cannot make a v7 blob). Proves an
|
||||
// already-saved pre-FB1 instance restores playing at exactly its old output level.
|
||||
std::vector<std::uint8_t> v7;
|
||||
v7.push_back(7); v7.push_back(0); v7.push_back(0); v7.push_back(0); // version 7
|
||||
v7.push_back(1); // channel mode = stereo
|
||||
for (int i = 0; i < 8; ++i) v7.push_back(0); // marker = 0
|
||||
v7.push_back(111); // preview velocity
|
||||
v7.push_back(5); // voice count
|
||||
v7.push_back(1); // voice mode = mono
|
||||
v7.push_back(1); // trigger = legato
|
||||
const std::string id = "saved";
|
||||
v7.push_back(static_cast<std::uint8_t>(id.size())); v7.push_back(0); v7.push_back(0); v7.push_back(0);
|
||||
v7.insert(v7.end(), id.begin(), id.end());
|
||||
v7.push_back(0); v7.push_back(0); v7.push_back(0); v7.push_back(0); // zone count 0
|
||||
const ComponentState back = deserializeComponentState(v7, 44100.0);
|
||||
CHECK(back.masterGainLinear == 1.0);
|
||||
CHECK(back.voiceCount == 5);
|
||||
CHECK(back.voiceMode == VoiceMode::Mono);
|
||||
CHECK(back.monoTrigger == MonoTrigger::Legato);
|
||||
CHECK(back.previewVelocity == 111);
|
||||
CHECK(back.channelMode == ChannelMode::Stereo);
|
||||
CHECK(back.selectionId == "saved");
|
||||
CHECK(back.map.zones.empty());
|
||||
}
|
||||
|
||||
static void testComponentStateV8CorruptMasterGainFallsBack() {
|
||||
// A corrupt gain double (NaN) in a v8 blob falls back to unity (the previewVelocity
|
||||
// corrupt-byte precedent) — never silences or blasts the instance. Build v8 by
|
||||
// serializing, then vandalize the 8 gain bytes in place (offsets: 4 version + 1 mode +
|
||||
// 8 marker + 1 velocity + 3 voice bytes = 17..24).
|
||||
ComponentState s;
|
||||
s.masterGainLinear = 0.5;
|
||||
std::vector<std::uint8_t> bytes = serializeComponentState(s);
|
||||
for (int i = 0; i < 8; ++i) bytes[17 + i] = 0xFF; // 0xFFFF... = a negative NaN pattern
|
||||
const ComponentState back = deserializeComponentState(bytes, 44100.0);
|
||||
CHECK(back.masterGainLinear == 1.0);
|
||||
}
|
||||
|
||||
static void testComponentStateV8TruncatedMasterGain() {
|
||||
// A v8 blob cut INSIDE the gain double -> empty, defaults holding (bounded read).
|
||||
std::vector<std::uint8_t> t{8, 0, 0, 0, 0}; // version 8, 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); t.push_back(0); t.push_back(0); // the three voice bytes
|
||||
t.push_back(0); t.push_back(0); t.push_back(0); // gain cut mid-double
|
||||
const ComponentState back = deserializeComponentState(t, 44100.0);
|
||||
CHECK(back.masterGainLinear == 1.0);
|
||||
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
|
||||
@@ -2030,6 +2130,12 @@ int main() {
|
||||
testComponentStateV6LiftsVoiceDefaults();
|
||||
testComponentStateV7CorruptVoiceBytesFallBack();
|
||||
testComponentStateV7TruncatedVoiceBytes();
|
||||
testComponentStateMasterGainRoundTrip();
|
||||
testComponentStateMasterGainDefaultAndZeroRoundTrip();
|
||||
testComponentStateMasterGainWriterClamps();
|
||||
testComponentStateV7LiftsUnityMasterGain();
|
||||
testComponentStateV8CorruptMasterGainFallsBack();
|
||||
testComponentStateV8TruncatedMasterGain();
|
||||
testV5EnvelopeWithMarkerAndPlayParamsRoundTrip();
|
||||
testV4BlobWithPlayParamsLiftsMarkerZeroKeepsPlay();
|
||||
testReconcileKeepsOnlySelectedFullRangeZone();
|
||||
|
||||
Reference in New Issue
Block a user