Bake window: derived note lengths carry exact durations, not ladder rungs — a long take is no longer cut at 384 beats
Hold keeps its picker. Also: one home for the %-fold, duration-ordered Hold travel, and a corrupt tail degrades to absent rather than fabricating one.
This commit is contained in:
@@ -35,11 +35,13 @@ decision about what the render made obsolete.
|
||||
source exhaustion, since the read head frees the voice whether or not the gate is down.
|
||||
`bakeWindowNeedsHold` is the predicate, and it reads the ENGINE's loop fold rather than the
|
||||
loop fields, so the control that collects Hold cannot appear for a loop the voice refuses.
|
||||
- **Trailing silence is free; truncation is not.** Every derivation rounds outward — the Gate
|
||||
note rounds UP to a programmable length that outlasts the source, the Varispeed bound takes
|
||||
the deepest reachable offset, and every path is padded by the voice's terminal declick ramp
|
||||
(`kDeclickFrames`, unconditionally — not branched on the pitch engine that has the ramp
|
||||
today). Judge any change to this module against that asymmetry.
|
||||
- **Trailing silence is free; truncation is not.** Every derivation errs outward — the
|
||||
Varispeed bound takes the deepest reachable offset the voice can play, and every path is
|
||||
padded by the voice's terminal declick ramp (`kDeclickFrames`, unconditionally — not branched
|
||||
on the pitch engine that has the ramp today). Judge any change to this module against that
|
||||
asymmetry. What it does NOT mean is quantizing: a derived length is an exact duration and a
|
||||
finite ladder cannot express one (`note/CLAUDE.md`) — rounding up to a rung truncated any
|
||||
source past the top rung, which is the failure this asymmetry exists to prevent.
|
||||
- **The reset's survive list is written out; everything else defaults.** `resetAfterBake`
|
||||
starts from a default-constructed parameter set and copies back only the mapping facts.
|
||||
A parameter added later therefore resets by default — the safe direction, since
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
#include "core/instrument/engine/loop/loop_span.h" // resolveLoop (the one sustain-loop fold)
|
||||
#include "core/instrument/engine/voice.h" // kDeclickFrames (the terminal ramp length)
|
||||
#include "core/instrument/map/trigger_seam.h" // the one Trigger span formula + %-fold
|
||||
#include "core/instrument/map/trigger_seam.h" // triggerPlayLength (the one span formula)
|
||||
|
||||
namespace reasampler::instrument::bake {
|
||||
|
||||
@@ -71,9 +71,8 @@ bool bakeWindowNeedsHold(const SampleData& dialed) {
|
||||
}
|
||||
|
||||
NoteProgram defaultBakeProgram(const SampleData& dialed, int renderSampleRate,
|
||||
note::Tempo tempo, note::Division hold,
|
||||
note::Velocity velocity) {
|
||||
NoteProgram p; // 1/4 straight, capture opening at note-on
|
||||
note::Division hold, note::Velocity velocity) {
|
||||
NoteProgram p; // a quarter note, capture opening at note-on
|
||||
p.velocity = velocity;
|
||||
if (renderSampleRate <= 0) return p;
|
||||
const double rate = static_cast<double>(renderSampleRate);
|
||||
@@ -86,25 +85,24 @@ NoteProgram defaultBakeProgram(const SampleData& dialed, int renderSampleRate,
|
||||
double endOffsetSeconds = 0.0;
|
||||
if (dialed.play.playMode == PlayMode::Trigger) {
|
||||
// Trigger ignores note-off entirely: the sound ends when the read head reaches the
|
||||
// play span's end, which has nothing to do with the note's length — so the end
|
||||
// offset is whatever is left after the note, positive or negative.
|
||||
const std::int64_t span = map::triggerPlayLength(
|
||||
map::effectiveLengthFraction(dialed.play), frameCount, start);
|
||||
endOffsetSeconds = static_cast<double>(span) / rate * stretch -
|
||||
tempo.beatsToSeconds(note::divisionBeats(p.length));
|
||||
// play span's end. The note is that span, so the window closes on the sound rather
|
||||
// than on a length the voice never consulted.
|
||||
const std::int64_t span =
|
||||
map::triggerPlayLength(effectiveLengthFraction(dialed.play), frameCount, start);
|
||||
p.length = note::lengthOfSeconds(static_cast<double>(span) / rate * stretch);
|
||||
} else if (bakeWindowNeedsHold(dialed)) {
|
||||
// The loop cycles for as long as the note is held, so the hold IS the length, and the
|
||||
// release is the one stage that runs after note-off.
|
||||
p.length = hold;
|
||||
p.length = note::lengthOfDivision(hold);
|
||||
endOffsetSeconds = releaseSeconds;
|
||||
} else {
|
||||
// Gate with no loop: the read head runs off the source and frees the voice whether or
|
||||
// not the gate is still down, so the maximal sound is the whole post-start span held.
|
||||
// Rounding the note UP to a programmable length that outlasts it costs nothing — the
|
||||
// voice is already gone by then — while a shorter note releases mid-attack.
|
||||
// Exact, not a ladder rung: a source longer than the ladder's top rung would otherwise
|
||||
// take that rung and release mid-sound, and rounding up to one costs trailing silence
|
||||
// on every other source.
|
||||
const std::int64_t postStart = (std::max)(std::int64_t{0}, frameCount - start);
|
||||
const double exhaustSeconds = static_cast<double>(postStart) / rate * stretch;
|
||||
p.length = note::shortestDivisionAtLeast(tempo.secondsToBeats(exhaustSeconds));
|
||||
p.length = note::lengthOfSeconds(static_cast<double>(postStart) / rate * stretch);
|
||||
endOffsetSeconds = releaseSeconds;
|
||||
}
|
||||
// The voice rings its last output out over kDeclickFrames instead of hard-cutting it, and
|
||||
|
||||
@@ -31,21 +31,25 @@ bool bakeWindowNeedsHold(const SampleData& dialed);
|
||||
// The bake's programmed note, DERIVED from the dialed sound at `renderSampleRate` (the rate
|
||||
// the bake renders at, which is what the engine's frame counts are consumed against):
|
||||
//
|
||||
// Trigger — note length is nominal (note-off is ignored); the end offset carries the
|
||||
// whole play span, stretched by the deepest downward Varispeed offset.
|
||||
// Trigger — the note IS the play span (note-off is ignored anyway), stretched by the
|
||||
// deepest downward Varispeed offset.
|
||||
// Gate, loop — `hold` is the note length; the end offset is the release.
|
||||
// Gate, no loop— the read head runs off the source and frees the voice whatever the gate is
|
||||
// doing, so the note is rounded UP to the shortest programmable length that
|
||||
// outlasts the source. Holding longer than that sounds identical, which is
|
||||
// what makes the overshoot trailing silence rather than a different sound.
|
||||
// doing, so the note is the whole post-start span, stretched the same way.
|
||||
//
|
||||
// Both derived lengths are EXACT durations, not ladder rungs: a source longer than the
|
||||
// ladder's top rung has no rung that covers it, and quantizing up to one overshoots every
|
||||
// other source (see note/CLAUDE.md). `hold` alone stays musical — it is a picker.
|
||||
//
|
||||
// Every case is padded by the voice's terminal declick ramp (kDeclickFrames): trailing
|
||||
// silence is free, and closing the window on the frame the ramp starts is a hard cut.
|
||||
// `hold` is read only in the Gate-with-loop case; `velocity` is the velocity the note fires
|
||||
// at, and it feeds the Varispeed stretch as well as the render.
|
||||
//
|
||||
// Takes no tempo: nothing derived here is beat-denominated. The one field that is — `hold` —
|
||||
// meets the tempo in resolveNote, with the rest of the program's beat-denominated fields.
|
||||
note::NoteProgram defaultBakeProgram(const SampleData& dialed, int renderSampleRate,
|
||||
note::Tempo tempo, note::Division hold,
|
||||
note::Velocity velocity);
|
||||
note::Division hold, note::Velocity velocity);
|
||||
|
||||
// The render window in frames. TWO domains meet here: `totalFrames` is the captured FILE's
|
||||
// length, everything else counts RENDER frames from whichever comes first, note-on or the
|
||||
|
||||
Reference in New Issue
Block a user