Files
reasampler/src/vst/sample_map.cpp
T
daniel 1e1d6bddbb 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.
2026-07-27 04:16:48 -04:00

539 lines
24 KiB
C++

// sample_map — pure implementation. See sample_map.h. NO VST3 / REAPER / SWELL /
// vendor includes; standard library + the pure bank_book / wav_trim / sampler_core.
#include "sample_map.h"
#include <algorithm> // std::min
#include <cstring> // std::memcpy
#include <utility> // std::move
namespace reasampler {
namespace {
// Translate a bank_model Sample's S2 intrinsics into the core's SampleLoop. The bank
// stores loop points as an optional LoopPoints (both-or-neither); the core wants a
// SampleLoop with an explicit hasLoop. Absent -> no loop.
SampleLoop loopFromSample(const Sample& s) {
SampleLoop out;
if (s.loop) {
out.hasLoop = true;
out.start = s.loop->start;
out.end = s.loop->end;
}
return out;
}
// A distilled SelectedSample from a bank_model Sample. rootNote defaults to middle C
// (60) when the bank left the intrinsic empty — Tier 0 still plays, just centered on
// C rather than a captured pitch (surfaced: an un-rooted sample plays unity at C4).
SelectedSample distill(const Sample& s) {
SelectedSample out;
out.relativePath = s.relativePath;
out.rootNote = s.rootNote ? *s.rootNote : 60;
out.loop = loopFromSample(s);
return out;
}
} // namespace
std::optional<SelectedSample> selectSample(const std::string& banksJson,
const std::string& sampleId) {
// POLICY REVERSAL (S10): an empty selection is SILENCE, not the first sample. Short-
// circuit before parsing — no stored id resolves to nothing to play by design.
if (sampleId.empty()) return std::nullopt;
if (banksJson.empty()) return std::nullopt;
std::optional<BankBook> book = BankBook::deserialize(banksJson);
if (!book) return std::nullopt; // malformed -> nothing to play (never throw)
// Search every bank (pool first, then named — banks() is ordinal order) for the
// stored id. A sample lives in exactly one bank, so first hit wins.
for (const Bank& b : book->banks()) {
if (const Sample* s = b.index.query(sampleId)) {
return distill(*s);
}
}
// A stale stored id (no longer resolves) is SILENCE, not a substituted first sample:
// the editor reflects the missing pick with its empty state rather than masking it.
return std::nullopt;
}
std::vector<SampleChoice> listSamples(const std::string& banksJson) {
std::vector<SampleChoice> out;
if (banksJson.empty()) return out;
std::optional<BankBook> book = BankBook::deserialize(banksJson);
if (!book) return out;
for (const Bank& b : book->banks()) {
for (const Sample& s : b.index.all()) {
out.push_back(SampleChoice{s.id, s.displayName, s.rootNote, s.key, b.id});
}
}
return out;
}
std::vector<BankChoice> listBanks(const std::string& banksJson) {
std::vector<BankChoice> out;
if (banksJson.empty()) return out;
std::optional<BankBook> book = BankBook::deserialize(banksJson);
if (!book) return out;
for (const Bank& b : book->banks()) {
out.push_back(BankChoice{b.id, b.displayName});
}
return out;
}
std::vector<AudioSample> downmixToMono(const std::vector<AudioSample>& interleaved,
int channelCount) {
std::vector<AudioSample> out;
if (channelCount <= 0 || interleaved.empty()) return out;
const std::size_t stride = static_cast<std::size_t>(channelCount);
const std::size_t frames = interleaved.size() / stride;
out.resize(frames);
const double inv = 1.0 / static_cast<double>(channelCount);
for (std::size_t f = 0; f < frames; ++f) {
double acc = 0.0;
const std::size_t base = f * stride;
for (std::size_t c = 0; c < stride; ++c) {
acc += static_cast<double>(interleaved[base + c]);
}
out[f] = static_cast<AudioSample>(acc * inv);
}
return out;
}
std::vector<AudioSample> extractChannel(const std::vector<AudioSample>& interleaved,
int channelCount, int which) {
std::vector<AudioSample> out;
if (channelCount <= 0 || interleaved.empty()) return out;
const std::size_t stride = static_cast<std::size_t>(channelCount);
// Clamp the requested channel into the source's range: a channel past the last one reads
// the last channel (a mono source asked for channel 1 yields channel 0 — dual-mono).
std::size_t ch = which < 0 ? 0 : static_cast<std::size_t>(which);
if (ch >= stride) ch = stride - 1;
const std::size_t frames = interleaved.size() / stride;
out.resize(frames);
for (std::size_t f = 0; f < frames; ++f) out[f] = interleaved[f * stride + ch];
return out;
}
DecodedZonePcm decodeChannels(const std::vector<AudioSample>& interleaved,
int sourceChannels, ChannelMode mode, int sampleRate) {
DecodedZonePcm out;
out.sampleRate = sampleRate > 0 ? sampleRate : 44100;
if (mode == ChannelMode::Mono) {
// MONO mode: the existing downmix policy (average all source channels), one channel out.
out.monoFrames = downmixToMono(interleaved, sourceChannels);
return out; // framesR stays empty
}
// STEREO mode: channel 0 = source channel 0; channel 1 = source channel 1, or channel 0
// duplicated when the source is mono (dual-mono, centered). extractChannel clamps the
// out-of-range channel request to the last channel, so a mono source yields L == R.
out.monoFrames = extractChannel(interleaved, sourceChannels, 0);
out.framesR = extractChannel(interleaved, sourceChannels, 1);
return out;
}
Keymap buildTier0Keymap(std::vector<AudioSample> frames, int sampleRate,
int rootNote, const SampleLoop& loop,
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
// mono — SampleData::channelCount() enforces the same rule, so a bad pair never half-plays).
if (!framesR.empty() && framesR.size() == data.frames.size()) {
data.framesR = std::move(framesR);
}
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));
}
// --- Performance map ---------------------------------------------------------
ResolvedPerformance resolvePerformance(const std::string& banksJson,
const PerformanceMap& map) {
ResolvedPerformance out;
if (map.zones.empty()) return out; // empty map -> empty (shell -> Tier 0)
if (banksJson.empty()) return out; // no bank -> nothing resolves
std::optional<BankBook> book = BankBook::deserialize(banksJson);
if (!book) return out; // malformed -> nothing (never throw)
for (const PerformanceZone& z : map.zones) {
// Look the id up across every bank (pool + named) — a sample lives in exactly
// one bank, so first hit wins.
const Sample* found = nullptr;
for (const Bank& b : book->banks()) {
if (const Sample* s = b.index.query(z.sampleId)) {
found = s;
break;
}
}
if (!found) {
// STALE-ID POLICY: drop the zone cleanly, report the id (editor can prune).
out.droppedSampleIds.push_back(z.sampleId);
continue;
}
ResolvedZone rz;
rz.relativePath = found->relativePath;
rz.lowNote = z.lowNote;
rz.highNote = z.highNote;
// Effective root: override beats bank intrinsic beats middle-C default.
rz.rootNote = z.rootOverride ? *z.rootOverride
: (found->rootNote ? *found->rootNote : 60);
// Effective loop / start (S11): the instrument's per-zone override wins over the
// bank's S2 intrinsic; absent -> the intrinsic (loop) / frame 0 (start). The bank is
// 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;
}
Keymap buildZonedKeymap(const std::vector<ResolvedZone>& zones,
const std::vector<DecodedZonePcm>& decoded) {
Keymap km;
const std::size_t n = std::min(zones.size(), decoded.size());
for (std::size_t i = 0; i < n; ++i) {
// An unreadable/empty WAV drops just this zone (not the whole map).
if (decoded[i].monoFrames.empty()) continue;
SampleData data;
data.frames = decoded[i].monoFrames;
// Carry the second channel only when it length-matches channel 0 (channelCount()
// enforces the same rule; a mismatched pair falls back to mono rather than half-play).
if (!decoded[i].framesR.empty() &&
decoded[i].framesR.size() == data.frames.size()) {
data.framesR = decoded[i].framesR;
}
data.sampleRate = decoded[i].sampleRate > 0 ? decoded[i].sampleRate : 44100;
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;
zone.lowNote = zones[i].lowNote;
zone.highNote = zones[i].highNote;
zone.rootNote = zones[i].rootNote;
zone.sampleIndex = sampleIndex;
km.zones.push_back(zone);
}
return km; // empty zones in -> empty Keymap (silence)
}
// --- Performance-map instance state (setState/getState) -----------------------
namespace {
void putU32le(std::vector<std::uint8_t>& out, std::uint32_t v) {
out.push_back(static_cast<std::uint8_t>(v & 0xFF));
out.push_back(static_cast<std::uint8_t>((v >> 8) & 0xFF));
out.push_back(static_cast<std::uint8_t>((v >> 16) & 0xFF));
out.push_back(static_cast<std::uint8_t>((v >> 24) & 0xFF));
}
// 64-bit little-endian, for the S11 loop start/end + start frame (int64 on the wire as
// two's-complement u64, mirroring the u32 signed-int idiom above).
void putU64le(std::vector<std::uint8_t>& out, std::uint64_t v) {
for (int b = 0; b < 8; ++b) out.push_back(static_cast<std::uint8_t>((v >> (b * 8)) & 0xFF));
}
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.
struct ByteReader {
const std::vector<std::uint8_t>& bytes;
std::size_t pos = 0;
bool ok = true;
explicit ByteReader(const std::vector<std::uint8_t>& b) : bytes(b) {}
std::uint32_t u32() {
if (!ok || pos + 4 > bytes.size()) { ok = false; return 0; }
const std::uint32_t v = static_cast<std::uint32_t>(bytes[pos]) |
(static_cast<std::uint32_t>(bytes[pos + 1]) << 8) |
(static_cast<std::uint32_t>(bytes[pos + 2]) << 16) |
(static_cast<std::uint32_t>(bytes[pos + 3]) << 24);
pos += 4;
return v;
}
std::uint8_t u8() {
if (!ok || pos + 1 > bytes.size()) { ok = false; return 0; }
return bytes[pos++];
}
std::string str(std::uint32_t len) {
if (!ok || pos + len > bytes.size()) { ok = false; return {}; }
std::string s(reinterpret_cast<const char*>(bytes.data() + pos), len);
pos += len;
return s;
}
// Signed ints go on the wire as u32 two's-complement (fixed 32-bit width).
int i32() { return static_cast<int>(static_cast<std::int32_t>(u32())); }
std::uint64_t u64() {
if (!ok || pos + 8 > bytes.size()) { ok = false; return 0; }
std::uint64_t v = 0;
for (int b = 0; b < 8; ++b)
v |= static_cast<std::uint64_t>(bytes[pos + static_cast<std::size_t>(b)]) << (b * 8);
pos += 8;
return v;
}
// Signed 64-bit frame indices go on the wire as u64 two's-complement (fixed width).
std::int64_t i64() { return static_cast<std::int64_t>(u64()); }
// Non-consuming peek of the next u32 (for the zones-payload format-marker probe). Yields
// 0 and latches nothing when fewer than 4 bytes remain — the caller treats a short blob
// as "no marker" and falls through to the (also-guarded) v1 count read.
std::uint32_t peekU32() const {
if (!ok || pos + 4 > bytes.size()) return 0;
return static_cast<std::uint32_t>(bytes[pos]) |
(static_cast<std::uint32_t>(bytes[pos + 1]) << 8) |
(static_cast<std::uint32_t>(bytes[pos + 2]) << 16) |
(static_cast<std::uint32_t>(bytes[pos + 3]) << 24);
}
};
// Append the zones payload — the shared body of the v2 performance blob and the v3 component
// blob, so both write zones identically. Always emits PAYLOAD v2 (the S11 self-describing
// marker + version + EXTENDED records): the marker precedes the zone count so any reader can
// detect the record shape independently of the envelope version (see sample_map.h). The S11
// loop/start overrides therefore round-trip through EITHER envelope with no envelope bump.
void putZonesPayload(std::vector<std::uint8_t>& out, const PerformanceMap& map) {
putU32le(out, kZonesFormatMarker);
putU32le(out, kZonesPayloadVersion);
putU32le(out, static_cast<std::uint32_t>(map.zones.size()));
for (const PerformanceZone& z : map.zones) {
putU32le(out, static_cast<std::uint32_t>(z.sampleId.size()));
out.insert(out.end(), z.sampleId.begin(), z.sampleId.end());
putU32le(out, static_cast<std::uint32_t>(static_cast<std::int32_t>(z.lowNote)));
putU32le(out, static_cast<std::uint32_t>(static_cast<std::int32_t>(z.highNote)));
out.push_back(z.rootOverride ? 1 : 0);
if (z.rootOverride) {
putU32le(out,
static_cast<std::uint32_t>(static_cast<std::int32_t>(*z.rootOverride)));
}
// S11 extension: loop override (hasLoop flag + start/end), then start point.
out.push_back(z.loopOverride ? 1 : 0);
if (z.loopOverride) {
out.push_back(z.loopOverride->hasLoop ? 1 : 0);
putU64le(out, asU64(z.loopOverride->start));
putU64le(out, asU64(z.loopOverride->end));
}
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));
}
}
// Read a zones payload from `r` into `map`. Shared by the performance parse and the component
// parse. Detects the S11 format marker: present -> PAYLOAD v2 (extended records with the
// 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) {
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);
z.lowNote = r.i32();
z.highNote = r.i32();
const std::uint8_t hasOverride = r.u8();
if (hasOverride) z.rootOverride = r.i32();
if (extended) {
const std::uint8_t hasLoop = r.u8();
if (hasLoop) {
SampleLoop lp;
lp.hasLoop = (r.u8() != 0);
lp.start = r.i64();
lp.end = r.i64();
z.loopOverride = lp;
}
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));
}
}
} // namespace
std::vector<std::uint8_t> serializePerformance(const PerformanceMap& map) {
std::vector<std::uint8_t> out;
putU32le(out, kPerformanceStateVersion);
putZonesPayload(out, map);
return out;
}
PerformanceMap deserializePerformance(const std::vector<std::uint8_t>& bytes) {
PerformanceMap map;
ByteReader r(bytes);
const std::uint32_t version = r.u32();
if (!r.ok) return map; // no version tag -> empty
// BACK-COMPAT: a v1 blob is the S4 single-selection format (version 1 + id bytes,
// no length prefix). Lift it to one full-keyboard zone playing that id.
if (version == kSelectionStateVersion) {
const std::string id = deserializeSelection(bytes);
if (!id.empty()) {
PerformanceZone z;
z.sampleId = id;
z.lowNote = 0;
z.highNote = 127;
map.zones.push_back(std::move(z));
}
return map;
}
if (version != kPerformanceStateVersion) return map; // unknown -> empty
readZonesPayload(r, map);
return map;
}
// --- Combined component state (v3, S10) --------------------------------------
std::vector<std::uint8_t> serializeComponentState(const ComponentState& state) {
std::vector<std::uint8_t> out;
putU32le(out, kComponentStateVersion);
// v4 envelope addition: the channel mode (0 = mono, 1 = stereo) precedes the v3 body.
out.push_back(state.channelMode == ChannelMode::Stereo ? 1 : 0);
// Length-prefixed selection id (it precedes the zones payload, so it MUST be framed —
// unlike the v1 selection blob where the id ran to end-of-stream).
putU32le(out, static_cast<std::uint32_t>(state.selectionId.size()));
out.insert(out.end(), state.selectionId.begin(), state.selectionId.end());
putZonesPayload(out, state.map);
return out;
}
ComponentState deserializeComponentState(const std::vector<std::uint8_t>& bytes) {
ComponentState out;
ByteReader r(bytes);
const std::uint32_t version = r.u32();
if (!r.ok) return out; // no version tag -> empty (the S10 silent empty state)
// BACK-COMPAT: an older blob predates the v3 {selection, zones} split.
// * v1 (S4 single-selection: version 1 + id-to-end): restore {id, one full-keyboard
// zone} so the old pick survives as BOTH the selection and a one-zone map.
// * v2 (S5 zones-only): restore {"", zones} — that instance had zones but no separate
// single-capture selection.
if (version == kSelectionStateVersion) {
out.selectionId = deserializeSelection(bytes);
if (!out.selectionId.empty()) {
PerformanceZone z;
z.sampleId = out.selectionId;
z.lowNote = 0;
z.highNote = 127;
out.map.zones.push_back(std::move(z));
}
return out;
}
if (version == kPerformanceStateVersion) {
readZonesPayload(r, out.map); // 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).
if (version == kSelectionZonesV3Version) {
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);
return out; // channelMode stays Mono (pre-S7)
}
if (version != kComponentStateVersion) return out; // unknown -> empty
// v4: the channel-mode byte precedes the v3 body. A non-{0,1} byte is treated as mono
// (conservative default) rather than rejected — a corrupt mode never silences the instance.
const std::uint8_t modeByte = r.u8();
if (!r.ok) return out; // truncated before the mode byte -> empty (mono default holds)
out.channelMode = (modeByte == 1) ? ChannelMode::Stereo : ChannelMode::Mono;
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);
return out;
}
std::vector<std::uint8_t> serializeSelection(const std::string& sampleId) {
std::vector<std::uint8_t> out;
out.resize(4 + sampleId.size());
const std::uint32_t v = kSelectionStateVersion;
out[0] = static_cast<std::uint8_t>(v & 0xFF);
out[1] = static_cast<std::uint8_t>((v >> 8) & 0xFF);
out[2] = static_cast<std::uint8_t>((v >> 16) & 0xFF);
out[3] = static_cast<std::uint8_t>((v >> 24) & 0xFF);
std::memcpy(out.data() + 4, sampleId.data(), sampleId.size());
return out;
}
std::string deserializeSelection(const std::vector<std::uint8_t>& bytes) {
if (bytes.size() < 4) return {}; // no version tag -> no selection
const std::uint32_t v = static_cast<std::uint32_t>(bytes[0]) |
(static_cast<std::uint32_t>(bytes[1]) << 8) |
(static_cast<std::uint32_t>(bytes[2]) << 16) |
(static_cast<std::uint32_t>(bytes[3]) << 24);
if (v != kSelectionStateVersion) return {}; // unknown version -> ignore
return std::string(reinterpret_cast<const char*>(bytes.data() + 4),
bytes.size() - 4);
}
} // namespace reasampler