S15/S16: Gate(AHDSR)/Trigger play modes + Varispeed/Preserve pitch engines + AD pitch envelope
Per-zone play params on SampleData; hand-rolled pure pitch_shift OLA for Preserve (WDL drags windows.h); zone-payload v3 tail; RT-safe pre-warmed shifters + Preserve voice cap.
This commit is contained in:
+51
-2
@@ -135,7 +135,7 @@ DecodedZonePcm decodeChannels(const std::vector<AudioSample>& interleaved,
|
||||
|
||||
Keymap buildTier0Keymap(std::vector<AudioSample> frames, int sampleRate,
|
||||
int rootNote, const SampleLoop& loop,
|
||||
std::vector<AudioSample> framesR) {
|
||||
std::vector<AudioSample> framesR, const ZonePlayParams& play) {
|
||||
SampleData data;
|
||||
data.frames = std::move(frames);
|
||||
// A second channel only counts when it length-matches channel 0 (else the sample stays
|
||||
@@ -146,6 +146,7 @@ Keymap buildTier0Keymap(std::vector<AudioSample> frames, int sampleRate,
|
||||
data.sampleRate = sampleRate > 0 ? sampleRate : 44100;
|
||||
data.rootNote = rootNote;
|
||||
data.loop = loop;
|
||||
data.play = play; // S15/S16 single-capture play params (product defaults unless overridden)
|
||||
return Keymap::singleSampleChromatic(std::move(data));
|
||||
}
|
||||
|
||||
@@ -186,6 +187,9 @@ ResolvedPerformance resolvePerformance(const std::string& banksJson,
|
||||
// never mutated — this only shapes what the core plays for THIS instance (D-B).
|
||||
rz.loop = z.loopOverride ? *z.loopOverride : loopFromSample(*found);
|
||||
rz.startFrame = z.startPoint ? *z.startPoint : 0;
|
||||
// S15/S16 per-zone play params carry through unchanged (they are instrument state, not
|
||||
// resolved against the bank) so the keymap build can stamp them onto the SampleData.
|
||||
rz.play = z.play;
|
||||
out.zones.push_back(std::move(rz));
|
||||
}
|
||||
return out;
|
||||
@@ -210,6 +214,7 @@ Keymap buildZonedKeymap(const std::vector<ResolvedZone>& zones,
|
||||
data.rootNote = zones[i].rootNote;
|
||||
data.loop = zones[i].loop;
|
||||
data.startFrame = zones[i].startFrame; // S11 effective start (override, else 0)
|
||||
data.play = zones[i].play; // S15/S16 per-zone play mode + engine + envelopes
|
||||
const std::size_t sampleIndex = km.samples.size();
|
||||
km.samples.push_back(std::move(data));
|
||||
KeyZone zone;
|
||||
@@ -241,6 +246,19 @@ void putU64le(std::vector<std::uint8_t>& out, std::uint64_t v) {
|
||||
|
||||
std::uint64_t asU64(std::int64_t v) { return static_cast<std::uint64_t>(v); }
|
||||
|
||||
// IEEE-754 double <-> u64 bit-cast for the wire (memcpy is the only defined type-pun in C++).
|
||||
// Used for the S15/S16 trigger.lengthFraction + pitchEnv.peakSemitones fields.
|
||||
std::uint64_t doubleToBits(double d) {
|
||||
std::uint64_t bits;
|
||||
std::memcpy(&bits, &d, sizeof(bits));
|
||||
return bits;
|
||||
}
|
||||
double bitsToDouble(std::uint64_t bits) {
|
||||
double d;
|
||||
std::memcpy(&d, &bits, sizeof(d));
|
||||
return d;
|
||||
}
|
||||
|
||||
// A bounded little-endian reader over a byte blob. Every read is length-checked; once a
|
||||
// read runs past the end the reader latches `ok=false` and yields zeros, so a truncated
|
||||
// blob degrades to a partial/empty parse rather than reading out of bounds.
|
||||
@@ -324,6 +342,20 @@ void putZonesPayload(std::vector<std::uint8_t>& out, const PerformanceMap& map)
|
||||
}
|
||||
out.push_back(z.startPoint ? 1 : 0);
|
||||
if (z.startPoint) putU64le(out, asU64(*z.startPoint));
|
||||
|
||||
// S15/S16 extension (PAYLOAD v3): the per-zone play params, always present (every zone
|
||||
// has a play mode + engine — no flag gate). Order matches the header's v3 record spec.
|
||||
const ZonePlayParams& pp = z.play;
|
||||
out.push_back(pp.playMode == PlayMode::Trigger ? 1 : 0);
|
||||
putU64le(out, asU64(pp.adsr.holdFrames));
|
||||
putU64le(out, doubleToBits(pp.trigger.lengthFraction));
|
||||
putU64le(out, asU64(pp.trigger.fadeInFrames));
|
||||
putU64le(out, asU64(pp.trigger.fadeOutFrames));
|
||||
out.push_back(pp.pitchEngine == PitchEngine::Preserve ? 1 : 0);
|
||||
out.push_back(pp.pitchEnv.enabled ? 1 : 0);
|
||||
putU64le(out, asU64(pp.pitchEnv.attackFrames));
|
||||
putU64le(out, asU64(pp.pitchEnv.decayFrames));
|
||||
putU64le(out, doubleToBits(pp.pitchEnv.peakSemitones));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -333,14 +365,18 @@ void putZonesPayload(std::vector<std::uint8_t>& out, const PerformanceMap& map)
|
||||
// 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) {
|
||||
bool extended = false;
|
||||
bool extended = false; // v2+: the S11 loop/start tail is present
|
||||
bool hasPlay = false; // v3+: the S15/S16 play-params tail is present
|
||||
if (r.peekU32() == kZonesFormatMarker) {
|
||||
r.u32(); // consume the marker
|
||||
const std::uint32_t pv = r.u32(); // payload version
|
||||
extended = (pv >= 2); // v2+ carries the loop/start tail
|
||||
hasPlay = (pv >= 3); // v3+ carries the S15/S16 play-params tail
|
||||
}
|
||||
const std::uint32_t count = r.u32();
|
||||
for (std::uint32_t i = 0; i < count && r.ok; ++i) {
|
||||
// z.play defaults to the PRODUCT defaults (Gate + Preserve). A v1/v2 payload (no play
|
||||
// tail) therefore lifts every zone to those defaults — the deliberate S16-F1 change.
|
||||
PerformanceZone z;
|
||||
const std::uint32_t idLen = r.u32();
|
||||
z.sampleId = r.str(idLen);
|
||||
@@ -360,6 +396,19 @@ void readZonesPayload(ByteReader& r, PerformanceMap& map) {
|
||||
const std::uint8_t hasStart = r.u8();
|
||||
if (hasStart) z.startPoint = r.i64();
|
||||
}
|
||||
if (hasPlay) {
|
||||
// S15/S16 play params, always present in a v3 record (read in the emit order).
|
||||
z.play.playMode = (r.u8() != 0) ? PlayMode::Trigger : PlayMode::Gate;
|
||||
z.play.adsr.holdFrames = r.i64();
|
||||
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.attackFrames = r.i64();
|
||||
z.play.pitchEnv.decayFrames = r.i64();
|
||||
z.play.pitchEnv.peakSemitones = bitsToDouble(r.u64());
|
||||
}
|
||||
if (!r.ok) break; // truncated mid-zone -> keep what parsed cleanly, drop the rest
|
||||
map.zones.push_back(std::move(z));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user