Bake window derives itself: %-knob fold, declick pad, Gate held to exhaustion, preview velocity; Hold is the one knob a loop needs
This commit is contained in:
@@ -2,9 +2,9 @@ reasampler_pure_library(bridge_marshal SOURCES bridge_marshal.cpp)
|
||||
reasampler_test(bridge_marshal LINK bridge_marshal)
|
||||
|
||||
reasampler_pure_library(trigger_seam SOURCES trigger_seam.cpp)
|
||||
# Links only trigger_seam — not even editor_geometry — the plainest data-boundary proof
|
||||
# available.
|
||||
reasampler_test(trigger_seam LINK trigger_seam)
|
||||
# The %-length fold lives beside the span formula, so the header reads play_params' value
|
||||
# layer; the test links that set and nothing else — still no engine, no editor geometry.
|
||||
reasampler_test(trigger_seam LINK trigger_seam velocity_curve peaks filter curve_law)
|
||||
|
||||
reasampler_pure_library(bank_sync
|
||||
SOURCES bank_sync.cpp
|
||||
@@ -21,7 +21,7 @@ reasampler_test(bank_sync LINK bank_sync)
|
||||
# the format already keeps on independent version axes (see component_state_io.h).
|
||||
reasampler_pure_library(component_state_io
|
||||
SOURCES component_state_io.cpp params_payload.cpp
|
||||
LINK PUBLIC velocity_curve master_gain curve_law)
|
||||
LINK PUBLIC velocity_curve master_gain curve_law musical_division)
|
||||
# Links only component_state_io, deliberately no sampler_core/pitch_shift: the structural
|
||||
# proof the codec is engine-free, which is what keeps engine object code out of the extension.
|
||||
reasampler_test(component_state_io LINK component_state_io)
|
||||
@@ -39,7 +39,8 @@ target_link_libraries(play_seconds INTERFACE velocity_curve peaks curve_law)
|
||||
# The mapping's product is plain SampleData, so the voice engine is not a dependency.
|
||||
reasampler_pure_library(sample_map
|
||||
SOURCES sample_map.cpp
|
||||
LINK PUBLIC bank_book wav_codec play_seconds velocity_curve peaks curve_law)
|
||||
LINK PUBLIC bank_book wav_codec play_seconds velocity_curve peaks curve_law
|
||||
musical_division)
|
||||
# Links only sample_map + component_state_io: the same plain-data-boundary proof, spanning
|
||||
# both halves of the mapping/codec split where the frozen-format assertions live.
|
||||
reasampler_test(sample_map LINK sample_map component_state_io)
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
// own links are velocity_curve + master_gain (wire value validation), never the engine.
|
||||
//
|
||||
// EVERY wire format below is FROZEN; the full version ladders (envelope v1..v11, params
|
||||
// payload v1..v13) must be preserved exactly. This header is the ONE home for both ladders
|
||||
// payload v1..v14) must be preserved exactly. This header is the ONE home for both ladders
|
||||
// and every version constant; the payload half is IMPLEMENTED in params_payload.
|
||||
|
||||
#include <cstdint>
|
||||
@@ -120,6 +120,15 @@ namespace reasampler::instrument::map {
|
||||
// A v12-or-older blob is a strict prefix and lifts to {Staged, the y = 1 - x default contour}
|
||||
// on all three EGs with no hard point anywhere, so it plays exactly as it did.
|
||||
//
|
||||
// v14 (CURRENT WRITE FORMAT) is v13 PLUS the resample bake's Hold division, appended after the
|
||||
// hard-flag tails: 4-byte LE quarterExponent (two's-complement int32) + 1 byte modifier (0
|
||||
// Straight / 1 Dotted / 2 Triplet). Decoded through makeDivision, which clamps both fields —
|
||||
// never memcpy'd into the type (core/instrument/note/CLAUDE.md owns why). A v13-or-older blob
|
||||
// is a strict prefix and lifts to one bar, and Hold reaches no audio path, so a pre-v14
|
||||
// instance plays and bakes identically except where its window was underived to begin with.
|
||||
// A blob truncated INSIDE this tail costs the Hold alone rather than resetting the record —
|
||||
// the same revive discipline the v13 hard-flag tails follow, and for the same reason.
|
||||
//
|
||||
// The two int64 slots the v5 play tail spends on the RETIRED Trigger fade pair are frozen in
|
||||
// shape and still read: a pre-v10 blob's fade-in/fade-out become the Trigger AHD that replaced
|
||||
// them (attack <- fade-in, decay <- fade-out, hold <- the whole remainder), converted to
|
||||
@@ -151,7 +160,7 @@ inline constexpr std::uint32_t kPerformanceStateVersion = 2;
|
||||
// The params-payload format version and its detection marker. The marker is a high sentinel
|
||||
// no legitimate v1 zone count (bounded by 128 MIDI zones, always tiny) could ever equal, so
|
||||
// a reader detects record shape independent of the envelope version.
|
||||
inline constexpr std::uint32_t kParamsPayloadVersion = 13; // v12 + the dual Staged/Spline state
|
||||
inline constexpr std::uint32_t kParamsPayloadVersion = 14; // v13 + the bake Hold division
|
||||
inline constexpr std::uint32_t kParamsFormatMarker = 0xFFFFFF00u;
|
||||
|
||||
// The first SINGLE-RECORD payload version. Everything below it is a retired zone list and
|
||||
@@ -179,6 +188,10 @@ inline constexpr std::uint32_t kParamsVelocityVersion = 12;
|
||||
// kParamsPayloadVersion.
|
||||
inline constexpr std::uint32_t kParamsSplineVersion = 13;
|
||||
|
||||
// v13 + the bake Hold division; the appended tail branches on THIS, never on
|
||||
// kParamsPayloadVersion.
|
||||
inline constexpr std::uint32_t kParamsBakeHoldVersion = 14;
|
||||
|
||||
// (No nominal-rate constant.) The legacy v3 payload's wall-clock frame counts convert to
|
||||
// seconds at the v3 read boundary using the PROJECT sample rate threaded in as a parameter
|
||||
// (frames / projectRate = seconds) — the same rate the build already receives, so the
|
||||
|
||||
@@ -193,6 +193,24 @@ void readHardFlags(ByteReader& r, VelocityCurve& curve) {
|
||||
for (std::size_t i = 0; i < flags.size(); ++i) curve.setHard(i, flags[i] != 0);
|
||||
}
|
||||
|
||||
// Read the v14 bake Hold. Same revive discipline as readHardFlags directly above, and for the
|
||||
// same reason: this tail reaches no audio path, so a blob truncated inside it must cost the
|
||||
// Hold alone and not reset the whole record that parsed cleanly ahead of it. It sits LAST, so
|
||||
// a truncation stranding the hard flags strands this too — reviving in only one of the two
|
||||
// would still wipe the record.
|
||||
void readBakeHold(ByteReader& r, InstrumentParams& p) {
|
||||
const bool enteredOk = r.ok;
|
||||
const std::int32_t exponent = r.i32();
|
||||
const std::uint8_t modifier = r.u8();
|
||||
if (!r.ok) {
|
||||
if (enteredOk) r.ok = true;
|
||||
return;
|
||||
}
|
||||
// makeDivision clamps BOTH fields, so a corrupt pair becomes the nearest legal rung
|
||||
// rather than an unrepresentable one — never a memcpy into the type.
|
||||
p.bakeHold = note::makeDivision(exponent, static_cast<note::DivisionModifier>(modifier));
|
||||
}
|
||||
|
||||
// Read the v9 filter tail into `p`. A blob that stops short leaves the off/neutral default,
|
||||
// which is what makes a v8 blob play bit-identically under the new codec. The curve reads as
|
||||
// bipolar at EVERY version — a pre-v12 blob's y values are already valid bipolar ones, so its
|
||||
@@ -418,6 +436,11 @@ void putParamsPayload(std::vector<std::uint8_t>& out, const InstrumentParams& p)
|
||||
putHardFlags(out, p.velocityCurve);
|
||||
putHardFlags(out, f.velocityCurve);
|
||||
putHardFlags(out, pp.pitchVelocityCurve);
|
||||
// v14: the bake's Hold division, as its {quarterExponent, modifier} pair — never its
|
||||
// picker index, which the ladder gaining a rung would silently re-map.
|
||||
putLE(out, static_cast<std::uint32_t>(
|
||||
static_cast<std::int32_t>(p.bakeHold.quarterExponent())));
|
||||
out.push_back(static_cast<std::uint8_t>(p.bakeHold.modifier()));
|
||||
}
|
||||
|
||||
// Read whichever payload shape follows: the single-record shape (v8 onward, growing by
|
||||
@@ -468,6 +491,7 @@ PayloadRead readParamsPayload(ByteReader& r, double projectRate) {
|
||||
readHardFlags(r, p.play.filter.velocityCurve);
|
||||
readHardFlags(r, p.play.pitchVelocityCurve);
|
||||
}
|
||||
if (pv >= kParamsBakeHoldVersion) readBakeHold(r, p);
|
||||
// A truncated record leaves whatever parsed plus construction defaults for the rest —
|
||||
// the same degrade-don't-throw contract the zone ladder always had.
|
||||
if (!r.ok) return PayloadRead{};
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include "core/model/bank_book.h" // BankBook::deserialize (shared bank JSON parse)
|
||||
#include "core/instrument/engine/play_params.h" // SampleData, SampleLoop, PlayParams
|
||||
#include "core/instrument/map/play_seconds.h" // PlaySeconds (the stored seconds value layer)
|
||||
#include "core/instrument/note/musical_division.h" // Division (the bake hold's value domain)
|
||||
#include "core/capture/wav_codec.h" // parseWavLayout, extractFloatFrames (shared WAV parse)
|
||||
|
||||
namespace reasampler::instrument::map {
|
||||
@@ -188,6 +189,12 @@ struct InstrumentParams {
|
||||
// tier-0 AHDSR seconds (0.003 attack / 0.060 release), hold 0, no fades, Preserve pitch
|
||||
// engine, pitch env off. An older blob lacking this tail lifts to exactly these.
|
||||
PlaySeconds play;
|
||||
|
||||
// How long the resample bake holds the gate. Read ONLY when the bake window cannot be
|
||||
// derived — a Gate voice over an active sustain loop, which sounds indefinitely
|
||||
// (bake_plan.h's bakeWindowNeedsHold is the predicate). Default one bar; a blob predating
|
||||
// the field lifts to it, and no other bake changes.
|
||||
note::Division bakeHold = note::makeDivision(2, note::DivisionModifier::Straight);
|
||||
};
|
||||
|
||||
// The loaded capture resolved for decode + build: project-relative WAV path (file seam)
|
||||
|
||||
@@ -1,19 +1,32 @@
|
||||
// trigger_seam — the shared Trigger play-span formula: how the stored %-length becomes the
|
||||
// source-frame span the voice plays and the overlay draws over. One home so the engine's
|
||||
// note-on resolve and the editor's overlay pack cannot disagree about where a Trigger note
|
||||
// ends.
|
||||
// source-frame span the voice plays, the overlay draws over, and the bake's window holds.
|
||||
// One home so those three cannot disagree about where a Trigger note ends.
|
||||
//
|
||||
// playLengthFrames = round(lengthFraction * (frameCount - startFrame))
|
||||
// playLengthFrames = round(effectiveLengthFraction * (frameCount - startFrame))
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "core/instrument/engine/play_params.h" // splineActive (the %-knob's inert rule)
|
||||
|
||||
namespace reasampler::instrument::map {
|
||||
|
||||
// The %-length the voice ACTUALLY plays. A drawn contour is a pure time function over the
|
||||
// full sample length, so any active spline EG folds the fraction to 1.0 and the stored knob
|
||||
// goes inert (splineActive, play_params.h) — but the stored value survives, so a pre-spline
|
||||
// setting is still there to be read. Every consumer of the span must fold it here or it
|
||||
// silently plays/draws/bakes a fraction of the take.
|
||||
//
|
||||
// Templated over the two parameter representations for the same reason splineActive is.
|
||||
template <class Play>
|
||||
double effectiveLengthFraction(const Play& p) {
|
||||
return splineActive(p) ? 1.0 : p.trigger.lengthFraction;
|
||||
}
|
||||
|
||||
// postStart = max(0, frameCount - startFrame); playLength = round(lengthFraction * postStart).
|
||||
// `startFrame` is the effective start point (0 when absent). Returns 0 when postStart == 0
|
||||
// or lengthFraction <= 0.
|
||||
// `startFrame` is the effective start point (0 when absent), `lengthFraction` the EFFECTIVE
|
||||
// one above. Returns 0 when postStart == 0 or lengthFraction <= 0.
|
||||
std::int64_t triggerPlayLength(double lengthFraction,
|
||||
std::int64_t frameCount,
|
||||
std::int64_t startFrame);
|
||||
|
||||
Reference in New Issue
Block a user