s12: thread project rate through v3 legacy lift; remove 44100 literals
kLegacyV3NominalRate removed. readZonesPayload, deserializePerformance, deserializeComponentState now take a projectRate for v3 frames→seconds. Rate fields default 0 (invalid). Four 44100 fallbacks replaced with assert+safe-return. 96k v3-lift test added.
This commit is contained in:
@@ -176,7 +176,11 @@ tresult PLUGIN_API ReaSamplerProcessor::setState(IBStream* state) {
|
||||
// blob restores {"", zones}; a v1 S4 single-selection blob restores {id, one-zone map} so
|
||||
// the old pick survives as both; an empty/unknown blob restores {"", no zones} — the S10
|
||||
// silent empty state (no first-sample fallback in reloadFromBank).
|
||||
const ComponentState cs = deserializeComponentState(bytes);
|
||||
// Pass sampleRate_ as the project rate for legacy v3 blob conversion (frames -> seconds at
|
||||
// the v3 read boundary). sampleRate_ is set by setupProcessing; REAPER calls setupProcessing
|
||||
// before setState on project load, so sampleRate_ is the real host rate here. A v3 blob on a
|
||||
// pre-setup call would assert inside readZonesPayload (a programming error, not a field case).
|
||||
const ComponentState cs = deserializeComponentState(bytes, sampleRate_);
|
||||
setSelectedSampleId(cs.selectionId);
|
||||
setPerformanceMap(cs.map);
|
||||
// S8: restore the last-consumed assignment generation so a re-open does not re-apply a
|
||||
|
||||
@@ -251,8 +251,9 @@ private:
|
||||
std::int64_t lastSeenBankGeneration_ = -1;
|
||||
|
||||
// Latched from setupProcessing so setActive/reload can size against it. Read
|
||||
// off-thread only.
|
||||
double sampleRate_ = 44100.0;
|
||||
// off-thread only. 0.0 is explicitly invalid — setupProcessing sets the real host rate
|
||||
// before any audio, and reloadFromBank guards on it before use.
|
||||
double sampleRate_ = 0.0;
|
||||
Steinberg::int32 maxBlockSize_ = 4096;
|
||||
|
||||
// --- S6 embedded TCP/MCP UI ---------------------------------------------
|
||||
|
||||
+40
-19
@@ -4,6 +4,7 @@
|
||||
#include "sample_map.h"
|
||||
|
||||
#include <algorithm> // std::min
|
||||
#include <cassert> // assert
|
||||
#include <cstring> // std::memcpy
|
||||
#include <utility> // std::move
|
||||
|
||||
@@ -118,8 +119,10 @@ std::vector<AudioSample> extractChannel(const std::vector<AudioSample>& interlea
|
||||
|
||||
DecodedZonePcm decodeChannels(const std::vector<AudioSample>& interleaved,
|
||||
int sourceChannels, ChannelMode mode, int sampleRate) {
|
||||
assert(sampleRate > 0 && "decodeChannels: sampleRate must be > 0 (programming error)");
|
||||
DecodedZonePcm out;
|
||||
out.sampleRate = sampleRate > 0 ? sampleRate : 44100;
|
||||
if (sampleRate <= 0) return out; // safe early-return; caller supplied an invalid rate
|
||||
out.sampleRate = sampleRate;
|
||||
if (mode == ChannelMode::Mono) {
|
||||
// MONO mode: the existing downmix policy (average all source channels), one channel out.
|
||||
out.monoFrames = downmixToMono(interleaved, sourceChannels);
|
||||
@@ -137,7 +140,8 @@ ZonePlayParams resolvePlay(const ZonePlaySeconds& stored, int sampleRate) {
|
||||
// seconds -> frames at the LIVE rate (round-to-nearest). Wall-clock quantities (AHDSR A/H/D/R,
|
||||
// pitch env A/D) resolve here; source-timeline quantities (trigger %-length + fades) carry
|
||||
// through untouched — they are already source frames / fractions. Non-time fields pass as-is.
|
||||
const double sr = sampleRate > 0 ? static_cast<double>(sampleRate) : 44100.0;
|
||||
assert(sampleRate > 0 && "resolvePlay: sampleRate must be > 0 (programming error)");
|
||||
const double sr = sampleRate > 0 ? static_cast<double>(sampleRate) : 1.0; // 1.0 avoids div-by-zero; assert fires first
|
||||
const auto secToFrames = [sr](double sec) {
|
||||
double f = sec * sr;
|
||||
if (f < 0.0) f = 0.0;
|
||||
@@ -162,6 +166,7 @@ ZonePlayParams resolvePlay(const ZonePlaySeconds& stored, int sampleRate) {
|
||||
Keymap buildTier0Keymap(std::vector<AudioSample> frames, int sampleRate,
|
||||
int rootNote, const SampleLoop& loop,
|
||||
std::vector<AudioSample> framesR, const ZonePlaySeconds& play) {
|
||||
assert(sampleRate > 0 && "buildTier0Keymap: sampleRate must be > 0 (programming error)");
|
||||
SampleData data;
|
||||
data.frames = std::move(frames);
|
||||
// A second channel only counts when it length-matches channel 0 (else the sample stays
|
||||
@@ -169,7 +174,8 @@ Keymap buildTier0Keymap(std::vector<AudioSample> frames, int sampleRate,
|
||||
if (!framesR.empty() && framesR.size() == data.frames.size()) {
|
||||
data.framesR = std::move(framesR);
|
||||
}
|
||||
data.sampleRate = sampleRate > 0 ? sampleRate : 44100;
|
||||
if (sampleRate <= 0) return Keymap{}; // safe early-return; assert fires first
|
||||
data.sampleRate = sampleRate;
|
||||
data.rootNote = rootNote;
|
||||
data.loop = loop;
|
||||
// Resolve the stored wall-clock SECONDS to the engine's frame domain at the WAV's actual rate.
|
||||
@@ -238,7 +244,10 @@ Keymap buildZonedKeymap(const std::vector<ResolvedZone>& zones,
|
||||
decoded[i].framesR.size() == data.frames.size()) {
|
||||
data.framesR = decoded[i].framesR;
|
||||
}
|
||||
data.sampleRate = decoded[i].sampleRate > 0 ? decoded[i].sampleRate : 44100;
|
||||
assert(decoded[i].sampleRate > 0 &&
|
||||
"buildZonedKeymap: DecodedZonePcm::sampleRate must be > 0 (programming error)");
|
||||
if (decoded[i].sampleRate <= 0) continue; // safe skip; assert fires first
|
||||
data.sampleRate = decoded[i].sampleRate;
|
||||
data.rootNote = zones[i].rootNote;
|
||||
data.loop = zones[i].loop;
|
||||
data.startFrame = zones[i].startFrame; // S11 effective start (override, else 0)
|
||||
@@ -400,7 +409,10 @@ void putZonesPayload(std::vector<std::uint8_t>& out, const PerformanceMap& map)
|
||||
// loop/start tail); absent (a plain small zone count) -> PAYLOAD v1 (pre-S11 records, no tail —
|
||||
// clean back-compat lift, the overrides simply default absent). A truncated mid-zone read
|
||||
// keeps the zones that parsed cleanly and drops the rest.
|
||||
void readZonesPayload(ByteReader& r, PerformanceMap& map) {
|
||||
// `projectRate` is the live host/project sample rate used to convert LEGACY v3 wall-clock frame
|
||||
// counts (holdFrames, pitchEnv A/D) to the seconds domain at the read boundary: seconds = frames /
|
||||
// projectRate. Must be > 0 (callers guard). v5 and later blobs carry seconds directly; no rate needed.
|
||||
void readZonesPayload(ByteReader& r, PerformanceMap& map, double projectRate) {
|
||||
bool extended = false; // v2+: the S11 loop/start tail is present
|
||||
std::uint32_t pv = 0; // payload version (0 = v1, no marker)
|
||||
if (r.peekU32() == kZonesFormatMarker) {
|
||||
@@ -435,18 +447,20 @@ void readZonesPayload(ByteReader& r, PerformanceMap& map) {
|
||||
}
|
||||
if (legacyV3Play) {
|
||||
// LEGACY v3 play tail (Daniel's beta projects). Wall-clock fields (hold, pitchEnv A/D)
|
||||
// were written as 44.1k-nominal frames -> divide by kLegacyV3NominalRate to reach the
|
||||
// seconds domain. Trigger %-length + fades are source-timeline, read as-is. A/D/S/R are
|
||||
// ABSENT in v3 -> leave the seconds defaults on z.play.adsr (0.003 / 0 / 1.0 / 0.060).
|
||||
// were written as frames -> divide by the project sample rate (threaded in as `projectRate`)
|
||||
// to reach the seconds domain. Trigger %-length + fades are source-timeline, read as-is.
|
||||
// A/D/S/R are ABSENT in v3 -> leave the seconds defaults on z.play.adsr.
|
||||
assert(projectRate > 0.0 && "readZonesPayload: projectRate must be > 0 for v3 lift");
|
||||
const double liftRate = projectRate > 0.0 ? projectRate : 1.0; // 1.0 avoids div-by-zero; assert fires first
|
||||
z.play.playMode = (r.u8() != 0) ? PlayMode::Trigger : PlayMode::Gate;
|
||||
z.play.adsr.holdSeconds = static_cast<double>(r.i64()) / kLegacyV3NominalRate;
|
||||
z.play.adsr.holdSeconds = static_cast<double>(r.i64()) / liftRate;
|
||||
z.play.trigger.lengthFraction = bitsToDouble(r.u64());
|
||||
z.play.trigger.fadeInFrames = r.i64();
|
||||
z.play.trigger.fadeOutFrames = r.i64();
|
||||
z.play.pitchEngine = (r.u8() != 0) ? PitchEngine::Preserve : PitchEngine::Varispeed;
|
||||
z.play.pitchEnv.enabled = (r.u8() != 0);
|
||||
z.play.pitchEnv.attackSeconds = static_cast<double>(r.i64()) / kLegacyV3NominalRate;
|
||||
z.play.pitchEnv.decaySeconds = static_cast<double>(r.i64()) / kLegacyV3NominalRate;
|
||||
z.play.pitchEnv.attackSeconds = static_cast<double>(r.i64()) / liftRate;
|
||||
z.play.pitchEnv.decaySeconds = static_cast<double>(r.i64()) / liftRate;
|
||||
z.play.pitchEnv.peakSemitones = bitsToDouble(r.u64());
|
||||
} else if (secondsPlay) {
|
||||
// Current v5 play tail: wall-clock times in SECONDS (doubles); trigger fades in source
|
||||
@@ -482,7 +496,11 @@ std::vector<std::uint8_t> serializePerformance(const PerformanceMap& map) {
|
||||
return out;
|
||||
}
|
||||
|
||||
PerformanceMap deserializePerformance(const std::vector<std::uint8_t>& bytes) {
|
||||
PerformanceMap deserializePerformance(const std::vector<std::uint8_t>& bytes,
|
||||
double projectRate) {
|
||||
// projectRate is only consumed by readZonesPayload when a LEGACY v3 payload is present.
|
||||
// For v5 and later blobs it is unused. The assert inside readZonesPayload fires if a v3
|
||||
// blob is encountered with an invalid rate — the calller guarantees a real rate before use.
|
||||
PerformanceMap map;
|
||||
ByteReader r(bytes);
|
||||
const std::uint32_t version = r.u32();
|
||||
@@ -503,7 +521,7 @@ PerformanceMap deserializePerformance(const std::vector<std::uint8_t>& bytes) {
|
||||
}
|
||||
if (version != kPerformanceStateVersion) return map; // unknown -> empty
|
||||
|
||||
readZonesPayload(r, map);
|
||||
readZonesPayload(r, map, projectRate);
|
||||
return map;
|
||||
}
|
||||
|
||||
@@ -526,7 +544,10 @@ std::vector<std::uint8_t> serializeComponentState(const ComponentState& state) {
|
||||
return out;
|
||||
}
|
||||
|
||||
ComponentState deserializeComponentState(const std::vector<std::uint8_t>& bytes) {
|
||||
ComponentState deserializeComponentState(const std::vector<std::uint8_t>& bytes,
|
||||
double projectRate) {
|
||||
// projectRate is only consumed by readZonesPayload when a LEGACY v3 payload is present.
|
||||
// For v5 and later blobs it is unused. See readZonesPayload for the guard.
|
||||
ComponentState out;
|
||||
ByteReader r(bytes);
|
||||
const std::uint32_t version = r.u32();
|
||||
@@ -549,8 +570,8 @@ ComponentState deserializeComponentState(const std::vector<std::uint8_t>& bytes)
|
||||
return out;
|
||||
}
|
||||
if (version == kPerformanceStateVersion) {
|
||||
readZonesPayload(r, out.map); // v2 body starts right after the version tag
|
||||
return out; // channelMode stays Mono (pre-S7)
|
||||
readZonesPayload(r, out.map, projectRate); // v2 body starts right after the version tag
|
||||
return out; // channelMode stays Mono (pre-S7)
|
||||
}
|
||||
// BACK-COMPAT: a v3 blob (pre-S7 {selection, zones}, no channel mode) restores as MONO —
|
||||
// the id length + id + zones body starts right after the version tag (no mode byte).
|
||||
@@ -558,7 +579,7 @@ ComponentState deserializeComponentState(const std::vector<std::uint8_t>& bytes)
|
||||
const std::uint32_t idLen = r.u32();
|
||||
out.selectionId = r.str(idLen);
|
||||
if (!r.ok) { out.selectionId.clear(); return out; } // truncated id -> empty
|
||||
readZonesPayload(r, out.map);
|
||||
readZonesPayload(r, out.map, projectRate);
|
||||
return out; // channelMode stays Mono, marker stays 0 (pre-S7/S8/S9)
|
||||
}
|
||||
// BACK-COMPAT: a v4 blob (pre-S8/S9 reader {mode, selection, zones}, no consumed marker):
|
||||
@@ -571,7 +592,7 @@ ComponentState deserializeComponentState(const std::vector<std::uint8_t>& bytes)
|
||||
const std::uint32_t idLen = r.u32();
|
||||
out.selectionId = r.str(idLen);
|
||||
if (!r.ok) { out.selectionId.clear(); return out; } // truncated id -> empty
|
||||
readZonesPayload(r, out.map);
|
||||
readZonesPayload(r, out.map, projectRate);
|
||||
return out; // marker stays 0 (pre-S8/S9 reader)
|
||||
}
|
||||
if (version != kComponentStateVersion) return out; // unknown -> empty
|
||||
@@ -587,7 +608,7 @@ ComponentState deserializeComponentState(const std::vector<std::uint8_t>& bytes)
|
||||
const std::uint32_t idLen = r.u32();
|
||||
out.selectionId = r.str(idLen);
|
||||
if (!r.ok) { out.selectionId.clear(); return out; } // truncated id -> empty
|
||||
readZonesPayload(r, out.map);
|
||||
readZonesPayload(r, out.map, projectRate);
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
+18
-12
@@ -260,7 +260,8 @@ ResolvedPerformance resolvePerformance(const std::string& banksJson,
|
||||
// map). Empty zones in -> empty Keymap (silence).
|
||||
struct DecodedZonePcm {
|
||||
std::vector<AudioSample> monoFrames; // channel 0 (mono, or L of a stereo decode)
|
||||
int sampleRate = 44100;
|
||||
int sampleRate = 0; // 0 is explicitly invalid; every consumer must
|
||||
// receive the WAV's real rate before use.
|
||||
std::vector<AudioSample> framesR; // channel 1 (R); EMPTY for a mono decode
|
||||
};
|
||||
Keymap buildZonedKeymap(const std::vector<ResolvedZone>& zones,
|
||||
@@ -323,10 +324,10 @@ DecodedZonePcm decodeChannels(const std::vector<AudioSample>& interleaved,
|
||||
// no fades + disabled pitch env) — the deliberate S16-F1 behavior change for already-saved
|
||||
// instruments. A truncated mid-v3-tail record keeps the zones that parsed and drops the rest.
|
||||
// LEGACY-READ CONVERSION (S12): the v3 wall-clock frame counts (hold, pitchEnv A/D) were ALWAYS
|
||||
// written by the S15/S16 editor as 44.1k-nominal frames (that build's slider domain was fixed at
|
||||
// 44100), so they convert to the seconds domain by dividing by that authoring-time nominal rate
|
||||
// (kLegacyV3NominalRate). Source-timeline fields (trigger %-length + fades) stay frames. A/D/S/R
|
||||
// are absent in v3 -> lifted to the tier-0 seconds defaults (0.003 / 0 / 1.0 / 0.060), no rate.
|
||||
// written by the S15/S16 editor as nominal frames at a baked-in rate. They convert to the seconds
|
||||
// domain by dividing by the PROJECT sample rate threaded into the v3 lift path at read time (passed
|
||||
// as a parameter — no constant). Source-timeline fields (trigger %-length + fades) stay frames.
|
||||
// A/D/S/R are absent in v3 -> lifted to the tier-0 seconds defaults (0.003 / 0 / 1.0 / 0.060).
|
||||
// * PAYLOAD v5 (S12 remediation — CURRENT WRITE FORMAT): the same marker + payload version (== 5),
|
||||
// THEN the v2 body PLUS, appended to each zone record after the S11 startPoint tail, the full
|
||||
// per-zone play params with WALL-CLOCK TIMES STORED AS SECONDS (rate-free, IEEE-754 doubles):
|
||||
@@ -364,18 +365,20 @@ inline constexpr std::uint32_t kPerformanceStateVersion = 2;
|
||||
inline constexpr std::uint32_t kZonesPayloadVersion = 5; // S12: full per-zone play params, SECONDS
|
||||
inline constexpr std::uint32_t kZonesFormatMarker = 0xFFFFFF00u;
|
||||
|
||||
// The authoring-time nominal rate the LEGACY v3 zone payload's wall-clock frame counts (hold,
|
||||
// pitchEnv A/D) were always written at (the S15/S16 editor's slider domain was fixed at 44100 Hz).
|
||||
// Used ONLY at the v3 read boundary to convert those legacy frames to the seconds domain — it is a
|
||||
// property of the frozen v3 wire format, not a live program rate. No other site may reference it.
|
||||
inline constexpr double kLegacyV3NominalRate = 44100.0;
|
||||
// (No kLegacyV3NominalRate constant.) The legacy v3 zone payload's wall-clock frame counts are
|
||||
// converted to seconds at the v3 read boundary using the PROJECT sample rate threaded in as a
|
||||
// parameter — frames ÷ projectRate = seconds. The project rate is the same rate keymap build
|
||||
// already receives, so the seconds domain is consistent across both paths. No constant is baked in.
|
||||
|
||||
// The performance map serialized to bytes for IBStream (getState).
|
||||
std::vector<std::uint8_t> serializePerformance(const PerformanceMap& map);
|
||||
|
||||
// The performance map parsed back from IBStream bytes (setState). A v2 blob parses
|
||||
// directly; a v1 blob lifts to a single full-keyboard zone; anything else -> empty map.
|
||||
PerformanceMap deserializePerformance(const std::vector<std::uint8_t>& bytes);
|
||||
// `projectRate` is the live host/project sample rate (must be > 0) used to convert the
|
||||
// legacy v3 wall-clock frame counts to the seconds domain at the read boundary.
|
||||
PerformanceMap deserializePerformance(const std::vector<std::uint8_t>& bytes,
|
||||
double projectRate);
|
||||
|
||||
// --- Combined component state (VST3 setState/getState, v3 — S10) -------------
|
||||
//
|
||||
@@ -432,7 +435,10 @@ std::vector<std::uint8_t> serializeComponentState(const ComponentState& state);
|
||||
// The full instance state parsed back from IBStream bytes (setState). Tolerant of
|
||||
// truncation/wrong-version (bounded reads, never throws); older blobs lift per the table
|
||||
// above so already-saved instances restore cleanly.
|
||||
ComponentState deserializeComponentState(const std::vector<std::uint8_t>& bytes);
|
||||
// `projectRate` is the live host/project sample rate (must be > 0) used to convert the
|
||||
// legacy v3 wall-clock frame counts to the seconds domain at the read boundary.
|
||||
ComponentState deserializeComponentState(const std::vector<std::uint8_t>& bytes,
|
||||
double projectRate);
|
||||
|
||||
// --- Instance state (VST3 setState/getState) --------------------------------
|
||||
//
|
||||
|
||||
@@ -145,8 +145,10 @@ struct SampleLoop {
|
||||
struct SampleData {
|
||||
std::vector<AudioSample> frames; // channel 0 PCM (mono, or L of a stereo sample)
|
||||
std::vector<AudioSample> framesR; // channel 1 PCM (R); EMPTY for a mono sample
|
||||
int sampleRate = 44100; // frames per second (for reference; ratio is
|
||||
// note-relative, so rate cancels for repitch)
|
||||
int sampleRate = 0; // frames per second (for reference; ratio is
|
||||
// note-relative, so rate cancels for repitch).
|
||||
// 0 is explicitly invalid — every consumer must
|
||||
// receive a real rate before use.
|
||||
int rootNote = 60; // MIDI note recorded at (plays at unity here)
|
||||
SampleLoop loop; // sustain loop, if any
|
||||
// Initial read position (frame offset) a voice starts playback at — frame 0 by
|
||||
|
||||
Reference in New Issue
Block a user