Ξ-W2-T1 remediation: print master gain into the bake, derive the window from the dialed sound, reset play mode to Trigger
This commit is contained in:
@@ -5,6 +5,8 @@
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
#include "core/instrument/map/trigger_seam.h" // triggerPlayLength (the one span formula)
|
||||
|
||||
namespace reasampler::instrument::bake {
|
||||
|
||||
using note::NoteProgram;
|
||||
@@ -12,17 +14,62 @@ using note::ResolvedNote;
|
||||
|
||||
namespace {
|
||||
|
||||
// Seconds -> frames by round-half-away-from-zero, the one conversion every field here
|
||||
// uses, so the window and its two event frames cannot round against each other.
|
||||
std::int64_t toFrames(double seconds, int rate) {
|
||||
return static_cast<std::int64_t>(std::llround(seconds * static_cast<double>(rate)));
|
||||
// Seconds -> frames by round-half-away-from-zero, the one conversion every field here uses,
|
||||
// so the window and its event frames cannot round against each other. Reports failure
|
||||
// rather than clamping: the double->int64 narrowing below is undefined once the product
|
||||
// leaves int64's range, which a legal offset magnitude reaches long before that.
|
||||
bool toFrames(double seconds, int rate, std::int64_t& out) {
|
||||
const double frames = seconds * static_cast<double>(rate);
|
||||
const auto ceiling = static_cast<double>(kMaxBakeFrames);
|
||||
if (!(frames >= -ceiling && frames <= ceiling)) return false; // also catches NaN
|
||||
out = static_cast<std::int64_t>(std::llround(frames));
|
||||
return true;
|
||||
}
|
||||
|
||||
// The deepest DOWNWARD pitch offset the dialed voice can reach, in semitones (<= 0). Only
|
||||
// Varispeed needs it: there the read head advances at the pitch ratio, so a downward offset
|
||||
// stretches how long Trigger's source span takes to play. Preserve decouples the two, and a
|
||||
// Gate release is ticked per output frame, so neither is affected.
|
||||
double downwardSemitones(const PlayParams& play, int velocity) {
|
||||
if (play.pitchEngine != PitchEngine::Varispeed) return 0.0;
|
||||
double down = (std::min)(0.0, kVelocityPitchRangeSemitones *
|
||||
play.pitchVelocityCurve.eval(velocity));
|
||||
if (play.pitchEnv.enabled) {
|
||||
// A drawn contour is bipolar, so it reaches -|peak| whichever way the depth points;
|
||||
// the staged AHD only ever travels between 0 and the peak.
|
||||
down += play.pitchSpline.mode == EnvMode::Spline
|
||||
? -std::fabs(play.pitchEnv.peakSemitones)
|
||||
: (std::min)(0.0, play.pitchEnv.peakSemitones);
|
||||
}
|
||||
return down;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
NoteProgram defaultBakeProgram() {
|
||||
NoteProgram p;
|
||||
p.end = note::EndOffset(note::offsetFromMs(kDefaultReleaseTailMs));
|
||||
NoteProgram defaultBakeProgram(const SampleData& dialed, int renderSampleRate,
|
||||
note::Tempo tempo) {
|
||||
NoteProgram p; // 1/4 straight, velocity 100, capture opening at note-on
|
||||
if (renderSampleRate <= 0) return p;
|
||||
const double rate = static_cast<double>(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(
|
||||
dialed.play.trigger.lengthFraction,
|
||||
static_cast<std::int64_t>(dialed.frames.size()), dialed.startFrame);
|
||||
const double stretch = std::pow(
|
||||
2.0, -downwardSemitones(dialed.play, p.velocity.value()) / 12.0);
|
||||
endOffsetSeconds = static_cast<double>(span) / rate * stretch -
|
||||
tempo.beatsToSeconds(note::divisionBeats(p.length));
|
||||
} else {
|
||||
// Gate: the release is the one stage that runs after note-off, so it is exactly
|
||||
// what the window has to hold past it.
|
||||
endOffsetSeconds = static_cast<double>(dialed.play.adsr.releaseFrames) / rate;
|
||||
}
|
||||
p.end = note::EndOffset(note::offsetFromMs(endOffsetSeconds * 1000.0));
|
||||
return p;
|
||||
}
|
||||
|
||||
@@ -31,20 +78,27 @@ std::optional<BakePlan> planBake(const ResolvedNote& resolved, int sampleRate,
|
||||
if (resolved.windowCollapsed) return std::nullopt;
|
||||
if (sampleRate <= 0) return std::nullopt;
|
||||
|
||||
// The render starts at whichever comes first, note-on or the capture opening. A POSITIVE
|
||||
// start offset is legal and means the capture opens after the note — so the head is
|
||||
// rendered and discarded, never folded away by sliding note-on later inside the window.
|
||||
const double renderStartSeconds = (std::min)(resolved.captureStartSeconds, 0.0);
|
||||
|
||||
BakePlan plan;
|
||||
plan.sampleRate = sampleRate;
|
||||
plan.totalFrames = toFrames(resolved.captureLengthSeconds(), sampleRate);
|
||||
if (!toFrames(resolved.captureLengthSeconds(), sampleRate, plan.totalFrames))
|
||||
return std::nullopt;
|
||||
if (plan.totalFrames <= 0) return std::nullopt;
|
||||
if (!toFrames(resolved.captureStartSeconds - renderStartSeconds, sampleRate,
|
||||
plan.leadInFrames))
|
||||
return std::nullopt;
|
||||
if (!toFrames(-renderStartSeconds, sampleRate, plan.noteOnFrame)) return std::nullopt;
|
||||
if (!toFrames(resolved.noteOffSeconds - renderStartSeconds, sampleRate,
|
||||
plan.noteOffFrame))
|
||||
return std::nullopt;
|
||||
// Each field cleared the ceiling alone; the render holds their sum.
|
||||
if (plan.renderFrames() > kMaxBakeFrames) return std::nullopt;
|
||||
|
||||
// Note-on sits at -captureStart into the window: a negative start offset (the capture
|
||||
// opens early) pushes it later, a positive one has already been clamped away by
|
||||
// resolveNote's own window.
|
||||
plan.noteOnFrame = std::clamp(toFrames(-resolved.captureStartSeconds, sampleRate),
|
||||
std::int64_t{0}, plan.totalFrames);
|
||||
plan.noteOffFrame =
|
||||
std::clamp(toFrames(resolved.noteOffSeconds - resolved.captureStartSeconds,
|
||||
sampleRate),
|
||||
plan.noteOnFrame, plan.totalFrames);
|
||||
plan.noteOffFrame = (std::max)(plan.noteOffFrame, plan.noteOnFrame);
|
||||
plan.note = std::clamp(rootNote, 0, 127);
|
||||
plan.velocity = std::clamp(static_cast<int>(resolved.velocity), 1, 127);
|
||||
return plan;
|
||||
|
||||
Reference in New Issue
Block a user