Merge ps-w8-t2-waveform: S11 waveform view + draggable loop points

This commit is contained in:
2026-07-26 22:06:42 -04:00
12 changed files with 1092 additions and 38 deletions
+75 -7
View File
@@ -181,7 +181,11 @@ ResolvedPerformance resolvePerformance(const std::string& banksJson,
// 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);
// 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;
out.zones.push_back(std::move(rz));
}
return out;
@@ -205,6 +209,7 @@ Keymap buildZonedKeymap(const std::vector<ResolvedZone>& zones,
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)
const std::size_t sampleIndex = km.samples.size();
km.samples.push_back(std::move(data));
KeyZone zone;
@@ -228,6 +233,14 @@ void putU32le(std::vector<std::uint8_t>& out, std::uint32_t v) {
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); }
// 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.
@@ -259,11 +272,38 @@ struct ByteReader {
}
// 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 (zone count + per-zone records) — the shared body of the v2
// performance blob and the v3 component blob, so both write zones identically.
// 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()));
@@ -275,14 +315,30 @@ void putZonesPayload(std::vector<std::uint8_t>& out, const PerformanceMap& map)
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));
}
}
// Read a zones payload (zone count + per-zone records) from `r` into `map`. Shared by the
// v2 performance parse and the v3 component parse. A truncated mid-zone read keeps the zones
// that parsed cleanly and drops the rest; the reader position is left after the last byte
// read successfully.
// 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;
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
}
const std::uint32_t count = r.u32();
for (std::uint32_t i = 0; i < count && r.ok; ++i) {
PerformanceZone z;
@@ -292,6 +348,18 @@ void readZonesPayload(ByteReader& r, PerformanceMap& map) {
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 (!r.ok) break; // truncated mid-zone -> keep what parsed cleanly, drop the rest
map.zones.push_back(std::move(z));
}