Merge ps-w8-t2-waveform: S11 waveform view + draggable loop points
This commit is contained in:
+75
-7
@@ -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));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user