// 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 // std::min #include // std::memcpy #include // 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 selectSample(const std::string& banksJson, const std::string& sampleId) { if (banksJson.empty()) return std::nullopt; std::optional 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. if (!sampleId.empty()) { for (const Bank& b : book->banks()) { if (const Sample* s = b.index.query(sampleId)) { return distill(*s); } } } // No stored id, or the id no longer resolves (the sample was deleted/moved out): // fall back to the FIRST sample in ordinal order so a fresh instance plays. for (const Bank& b : book->banks()) { if (!b.index.all().empty()) { return distill(b.index.all().front()); } } return std::nullopt; // bank has zero samples anywhere } std::vector listSamples(const std::string& banksJson) { std::vector out; if (banksJson.empty()) return out; std::optional 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}); } } return out; } std::vector downmixToMono(const std::vector& interleaved, int channelCount) { std::vector out; if (channelCount <= 0 || interleaved.empty()) return out; const std::size_t stride = static_cast(channelCount); const std::size_t frames = interleaved.size() / stride; out.resize(frames); const double inv = 1.0 / static_cast(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(interleaved[base + c]); } out[f] = static_cast(acc * inv); } return out; } Keymap buildTier0Keymap(std::vector monoFrames, int sampleRate, int rootNote, const SampleLoop& loop) { SampleData data; data.frames = std::move(monoFrames); data.sampleRate = sampleRate > 0 ? sampleRate : 44100; data.rootNote = rootNote; data.loop = loop; 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 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); rz.loop = loopFromSample(*found); out.zones.push_back(std::move(rz)); } return out; } Keymap buildZonedKeymap(const std::vector& zones, const std::vector& 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; data.sampleRate = decoded[i].sampleRate > 0 ? decoded[i].sampleRate : 44100; data.rootNote = zones[i].rootNote; data.loop = zones[i].loop; 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& out, std::uint32_t v) { out.push_back(static_cast(v & 0xFF)); out.push_back(static_cast((v >> 8) & 0xFF)); out.push_back(static_cast((v >> 16) & 0xFF)); out.push_back(static_cast((v >> 24) & 0xFF)); } // 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& bytes; std::size_t pos = 0; bool ok = true; explicit ByteReader(const std::vector& b) : bytes(b) {} std::uint32_t u32() { if (!ok || pos + 4 > bytes.size()) { ok = false; return 0; } const std::uint32_t v = static_cast(bytes[pos]) | (static_cast(bytes[pos + 1]) << 8) | (static_cast(bytes[pos + 2]) << 16) | (static_cast(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(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(static_cast(u32())); } }; } // namespace std::vector serializePerformance(const PerformanceMap& map) { std::vector out; putU32le(out, kPerformanceStateVersion); putU32le(out, static_cast(map.zones.size())); for (const PerformanceZone& z : map.zones) { putU32le(out, static_cast(z.sampleId.size())); out.insert(out.end(), z.sampleId.begin(), z.sampleId.end()); putU32le(out, static_cast(static_cast(z.lowNote))); putU32le(out, static_cast(static_cast(z.highNote))); out.push_back(z.rootOverride ? 1 : 0); if (z.rootOverride) { putU32le(out, static_cast(static_cast(*z.rootOverride))); } } return out; } PerformanceMap deserializePerformance(const std::vector& 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 const std::uint32_t count = r.u32(); for (std::uint32_t i = 0; i < count && r.ok; ++i) { 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 (!r.ok) break; // truncated mid-zone -> keep what parsed cleanly, drop the rest map.zones.push_back(std::move(z)); } return map; } std::vector serializeSelection(const std::string& sampleId) { std::vector out; out.resize(4 + sampleId.size()); const std::uint32_t v = kSelectionStateVersion; out[0] = static_cast(v & 0xFF); out[1] = static_cast((v >> 8) & 0xFF); out[2] = static_cast((v >> 16) & 0xFF); out[3] = static_cast((v >> 24) & 0xFF); std::memcpy(out.data() + 4, sampleId.data(), sampleId.size()); return out; } std::string deserializeSelection(const std::vector& bytes) { if (bytes.size() < 4) return {}; // no version tag -> no selection const std::uint32_t v = static_cast(bytes[0]) | (static_cast(bytes[1]) << 8) | (static_cast(bytes[2]) << 16) | (static_cast(bytes[3]) << 24); if (v != kSelectionStateVersion) return {}; // unknown version -> ignore return std::string(reinterpret_cast(bytes.data() + 4), bytes.size() - 4); } } // namespace reasampler