Ξ-W2-T1: the resample bake chain — instrument renders, extension banks, one click re-points and resets
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
// Standalone tests for reasampler::instrument::bake::bake_plan — no VST3, no REAPER, no
|
||||
// framework. Same fast assert loop as the sibling pure tests.
|
||||
//
|
||||
// Covers: the default program's window (a note length plus a release tail, so the bake is
|
||||
// not truncated at note-off); the frame window and both event frames against hand-computed
|
||||
// values; a capture opening BEFORE note-on; the refusals — a collapsed window, a
|
||||
// non-positive rate, and a window that rounds to nothing; and root/velocity clamping.
|
||||
|
||||
#include "../src/core/instrument/bake/bake_plan.h"
|
||||
|
||||
#include <cstdio>
|
||||
|
||||
using namespace reasampler::instrument::bake;
|
||||
using namespace reasampler::instrument::note;
|
||||
|
||||
static int g_fail = 0;
|
||||
#define CHECK(cond) do { if(!(cond)) { \
|
||||
std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)
|
||||
|
||||
static Tempo at(double bpm) {
|
||||
const std::optional<Tempo> t = Tempo::fromBpm(bpm);
|
||||
if (!t) { std::printf("FAIL: fixture tempo %f rejected\n", bpm); ++g_fail; }
|
||||
return t.value_or(Tempo::fromBpm(120.0).value());
|
||||
}
|
||||
|
||||
int main() {
|
||||
// --- The default program opens the window past note-off -----------------------
|
||||
{
|
||||
const NoteProgram p = defaultBakeProgram();
|
||||
const ResolvedNote r = resolveNote(p, at(120.0));
|
||||
// A quarter note at 120 BPM is 0.5 s; the tail is kDefaultReleaseTailMs past it.
|
||||
CHECK(r.noteOffSeconds > 0.499 && r.noteOffSeconds < 0.501);
|
||||
CHECK(r.captureStartSeconds == 0.0);
|
||||
// The whole point of the default: the window must extend past the release, or
|
||||
// every bake would be cut at note-off.
|
||||
CHECK(r.captureEndSeconds > r.noteOffSeconds);
|
||||
CHECK(!r.windowCollapsed);
|
||||
}
|
||||
|
||||
// --- The frame window and both event frames ----------------------------------
|
||||
{
|
||||
NoteProgram p; // 1/4 straight, velocity 100
|
||||
p.end = EndOffset(offsetFromMs(250.0));
|
||||
const ResolvedNote r = resolveNote(p, at(120.0)); // note-off 0.5 s, end 0.75 s
|
||||
const auto plan = planBake(r, 48000, 60);
|
||||
CHECK(plan.has_value());
|
||||
CHECK(plan->totalFrames == 36000); // 0.75 s * 48 kHz
|
||||
CHECK(plan->noteOnFrame == 0);
|
||||
CHECK(plan->noteOffFrame == 24000); // 0.5 s * 48 kHz
|
||||
CHECK(plan->sampleRate == 48000);
|
||||
CHECK(plan->note == 60);
|
||||
CHECK(plan->velocity == 100);
|
||||
}
|
||||
|
||||
// --- A capture that opens BEFORE note-on -------------------------------------
|
||||
{
|
||||
NoteProgram p;
|
||||
p.start = StartOffset(offsetFromMs(-100.0)); // negative = earlier
|
||||
p.end = EndOffset(offsetFromMs(100.0));
|
||||
const ResolvedNote r = resolveNote(p, at(120.0));
|
||||
const auto plan = planBake(r, 44100, 60);
|
||||
CHECK(plan.has_value());
|
||||
// Window is [-0.1, 0.6] s = 0.7 s; note-on sits 0.1 s in, note-off 0.5 s after it.
|
||||
CHECK(plan->totalFrames == 30870);
|
||||
CHECK(plan->noteOnFrame == 4410);
|
||||
CHECK(plan->noteOffFrame == 26460);
|
||||
CHECK(plan->noteOffFrame - plan->noteOnFrame == 22050); // the note's own length
|
||||
}
|
||||
|
||||
// --- Refusals -----------------------------------------------------------------
|
||||
{
|
||||
NoteProgram p;
|
||||
// An end offset more negative than the note length inverts the window.
|
||||
p.end = EndOffset(offsetFromMs(-10000.0));
|
||||
const ResolvedNote r = resolveNote(p, at(120.0));
|
||||
CHECK(r.windowCollapsed);
|
||||
CHECK(!planBake(r, 48000, 60).has_value());
|
||||
}
|
||||
{
|
||||
const ResolvedNote r = resolveNote(defaultBakeProgram(), at(120.0));
|
||||
CHECK(!planBake(r, 0, 60).has_value());
|
||||
CHECK(!planBake(r, -48000, 60).has_value());
|
||||
}
|
||||
{
|
||||
// A legal but sub-frame window rounds to nothing and is refused rather than
|
||||
// rendered as a degenerate buffer.
|
||||
NoteProgram p;
|
||||
p.end = EndOffset(offsetFromMs(-500.0)); // exactly cancels the 0.5 s note
|
||||
const ResolvedNote r = resolveNote(p, at(120.0));
|
||||
CHECK(!r.windowCollapsed);
|
||||
CHECK(r.captureLengthSeconds() == 0.0);
|
||||
CHECK(!planBake(r, 48000, 60).has_value());
|
||||
}
|
||||
|
||||
// --- Domain clamps -------------------------------------------------------------
|
||||
{
|
||||
const ResolvedNote r = resolveNote(defaultBakeProgram(), at(120.0));
|
||||
CHECK(planBake(r, 48000, -5)->note == 0);
|
||||
CHECK(planBake(r, 48000, 900)->note == 127);
|
||||
CHECK(planBake(r, 48000, 60)->note == 60);
|
||||
}
|
||||
|
||||
if (g_fail == 0) std::printf("bake_plan: all tests passed\n");
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
// Standalone tests for reasampler::instrument::bake::bake_render — no VST3, no REAPER, no
|
||||
// framework. Same fast assert loop as the sibling pure tests.
|
||||
//
|
||||
// Covers: Gate termination WITH a sustain loop active (the render must end at the window,
|
||||
// and the tail must be silent because the gate actually released — not merely because the
|
||||
// buffer ran out); Trigger termination on its own play span; channel-count preservation
|
||||
// with no stereo fold; byte-identical repeats; and the refusals (unplayable sample, empty
|
||||
// window).
|
||||
|
||||
#include "../src/core/instrument/bake/bake_render.h"
|
||||
|
||||
#include "../src/core/instrument/engine/live_params.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
|
||||
using namespace reasampler;
|
||||
using namespace reasampler::instrument::bake;
|
||||
|
||||
static int g_fail = 0;
|
||||
#define CHECK(cond) do { if(!(cond)) { \
|
||||
std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int kRate = 48000;
|
||||
|
||||
// Distinct per-channel DC so a downmix or a channel duplication is visible in the output
|
||||
// rather than hidden behind two identical channels.
|
||||
SampleData makeSample(bool stereo, std::size_t frames = 1000) {
|
||||
SampleData s;
|
||||
s.frames.assign(frames, 0.5f);
|
||||
if (stereo) s.framesR.assign(frames, -0.25f);
|
||||
s.sampleRate = kRate;
|
||||
s.rootNote = 60;
|
||||
return s;
|
||||
}
|
||||
|
||||
// Peak magnitude of channel 0 over [from, to) output frames.
|
||||
double peakAt(const BakeAudio& audio, std::int64_t from, std::int64_t to) {
|
||||
double peak = 0.0;
|
||||
for (std::int64_t f = from; f < to && f < audio.frameCount(); ++f) {
|
||||
const double v = std::fabs(
|
||||
static_cast<double>(audio.interleaved[static_cast<std::size_t>(
|
||||
f * audio.channelCount)]));
|
||||
if (v > peak) peak = v;
|
||||
}
|
||||
return peak;
|
||||
}
|
||||
|
||||
BakePlan planOf(std::int64_t total, std::int64_t noteOn, std::int64_t noteOff) {
|
||||
BakePlan p;
|
||||
p.totalFrames = total;
|
||||
p.noteOnFrame = noteOn;
|
||||
p.noteOffFrame = noteOff;
|
||||
p.note = 60;
|
||||
p.velocity = 100;
|
||||
p.sampleRate = kRate;
|
||||
return p;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
// --- Gate, sustain loop active: the render terminates and the gate really released --
|
||||
{
|
||||
SampleData s = makeSample(/*stereo=*/false, /*frames=*/200);
|
||||
// A 100-frame loop over a 200-frame sample: held past the sample end it would cycle
|
||||
// forever, which is exactly the runaway the window has to bound.
|
||||
s.loop = SampleLoop{true, 0, 100};
|
||||
s.play.playMode = PlayMode::Gate;
|
||||
s.play.adsr.releaseFrames = 480; // 10 ms — short enough to finish inside the tail
|
||||
|
||||
const BakePlan plan = planOf(/*total=*/9600, /*noteOn=*/0, /*noteOff=*/4800);
|
||||
const BakeAudio audio = renderBake(s, plan);
|
||||
|
||||
CHECK(audio.frameCount() == 9600); // bounded, not a runaway
|
||||
CHECK(audio.channelCount == 1);
|
||||
// Sounding right up to the release…
|
||||
CHECK(peakAt(audio, 4700, 4800) > 0.4);
|
||||
// …and silent well after it, which only holds if the note-off was honoured: the
|
||||
// loop would otherwise still be cycling at full level here.
|
||||
CHECK(peakAt(audio, 6000, 9600) < 1e-6);
|
||||
}
|
||||
|
||||
// --- Trigger: note-off is ignored, the play span ends the sound -------------------
|
||||
{
|
||||
SampleData s = makeSample(/*stereo=*/false, /*frames=*/1000);
|
||||
s.play.playMode = PlayMode::Trigger;
|
||||
s.play.trigger.lengthFraction = 0.5; // 500 source frames at unity ratio
|
||||
|
||||
const BakePlan plan = planOf(/*total=*/2000, /*noteOn=*/0, /*noteOff=*/100);
|
||||
const BakeAudio audio = renderBake(s, plan);
|
||||
|
||||
CHECK(audio.frameCount() == 2000);
|
||||
// Still sounding past the note-off Trigger ignores…
|
||||
CHECK(peakAt(audio, 200, 400) > 0.4);
|
||||
// …and finished at its own span end, well before the window closes.
|
||||
CHECK(peakAt(audio, 700, 2000) < 1e-6);
|
||||
}
|
||||
|
||||
// --- Channel count preserved; no stereo fold --------------------------------------
|
||||
{
|
||||
SampleData s = makeSample(/*stereo=*/true, /*frames=*/1000);
|
||||
s.play.playMode = PlayMode::Trigger;
|
||||
|
||||
const BakePlan plan = planOf(/*total=*/500, /*noteOn=*/0, /*noteOff=*/500);
|
||||
const BakeAudio audio = renderBake(s, plan);
|
||||
|
||||
CHECK(audio.channelCount == 2);
|
||||
CHECK(audio.frameCount() == 500);
|
||||
CHECK(audio.interleaved.size() == 1000u);
|
||||
// The two channels carry the source's two distinct signals: a downmix would make
|
||||
// them equal, a duplication would make R equal L.
|
||||
CHECK(audio.interleaved[200] > 0.4f);
|
||||
CHECK(audio.interleaved[201] < -0.2f);
|
||||
CHECK(audio.interleaved[201] > -0.3f);
|
||||
}
|
||||
|
||||
// --- Bit-identical repeats ---------------------------------------------------------
|
||||
{
|
||||
SampleData s = makeSample(/*stereo=*/true, /*frames=*/1000);
|
||||
s.loop = SampleLoop{true, 0, 333};
|
||||
s.play.adsr.attackFrames = 97; // a shape whose per-frame state must replay exactly
|
||||
s.play.adsr.releaseFrames = 211;
|
||||
|
||||
const BakePlan plan = planOf(/*total=*/4096, /*noteOn=*/13, /*noteOff=*/2731);
|
||||
const BakeAudio a = renderBake(s, plan);
|
||||
const BakeAudio b = renderBake(s, plan);
|
||||
|
||||
CHECK(a.interleaved.size() == b.interleaved.size());
|
||||
CHECK(!a.interleaved.empty());
|
||||
bool identical = a.interleaved.size() == b.interleaved.size();
|
||||
for (std::size_t i = 0; identical && i < a.interleaved.size(); ++i)
|
||||
identical = (a.interleaved[i] == b.interleaved[i]);
|
||||
CHECK(identical);
|
||||
// The window opened before the note: those frames must be untouched silence.
|
||||
CHECK(peakAt(a, 0, 13) == 0.0);
|
||||
CHECK(peakAt(a, 200, 400) > 0.0);
|
||||
}
|
||||
|
||||
// --- The bake is off the audio thread's block, structurally ------------------------
|
||||
// The only thing process() and a bake could share is the live-parameter block. This
|
||||
// pins that they do not: the caller's SampleData is untouched (renderBake took a
|
||||
// copy), and the render ignores what is published in the block — the bake prints the
|
||||
// dialed parameter set, not whatever the audio thread is currently observing.
|
||||
{
|
||||
instrument::engine::LiveParams block;
|
||||
SampleData s = makeSample(/*stereo=*/false, /*frames=*/1000);
|
||||
s.play.playMode = PlayMode::Trigger;
|
||||
s.play.trigAhd.attackFrames = 0; // dialed: instant attack
|
||||
|
||||
// Publish a MUCH slower attack into the block. A render that observed it would be
|
||||
// near-silent at the point the dialed shape is already at full level.
|
||||
s.live = █
|
||||
{
|
||||
PlayParams slow = s.play;
|
||||
slow.trigAhd.attackFrames = 900;
|
||||
block.publish(instrument::engine::foldLive(slow));
|
||||
}
|
||||
|
||||
const BakePlan plan = planOf(/*total=*/1000, /*noteOn=*/0, /*noteOff=*/1000);
|
||||
const BakeAudio audio = renderBake(s, plan);
|
||||
|
||||
CHECK(s.live == &block); // the caller's own snapshot was not detached
|
||||
// At frame 100 the dialed instant attack is at full level; the published 900-frame
|
||||
// attack would be barely a ninth of the way up.
|
||||
CHECK(peakAt(audio, 90, 110) > 0.4);
|
||||
|
||||
// And it matches a render from a block-free copy exactly.
|
||||
SampleData detached = s;
|
||||
detached.live = nullptr;
|
||||
const BakeAudio reference = renderBake(detached, plan);
|
||||
bool identical = audio.interleaved.size() == reference.interleaved.size();
|
||||
for (std::size_t i = 0; identical && i < audio.interleaved.size(); ++i)
|
||||
identical = (audio.interleaved[i] == reference.interleaved[i]);
|
||||
CHECK(identical);
|
||||
}
|
||||
|
||||
// --- Refusals -----------------------------------------------------------------------
|
||||
{
|
||||
SampleData empty; // nothing decoded
|
||||
empty.sampleRate = kRate;
|
||||
CHECK(renderBake(empty, planOf(1000, 0, 500)).empty());
|
||||
|
||||
SampleData s = makeSample(false);
|
||||
CHECK(renderBake(s, planOf(0, 0, 0)).empty());
|
||||
}
|
||||
|
||||
if (g_fail == 0) std::printf("bake_render: all tests passed\n");
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
// Standalone tests for reasampler::instrument::bake::bake_reset — no VST3, no REAPER, no
|
||||
// framework. Same fast assert loop as the sibling pure tests.
|
||||
//
|
||||
// Covers the ratified reset scope PER PARAMETER, in both directions: every control whose
|
||||
// effect the render printed comes back at its default, and every mapping fact comes back
|
||||
// untouched. Asserted field by field rather than by struct equality on purpose — a
|
||||
// whole-struct compare would pass while silently resetting a survivor, or vice versa.
|
||||
|
||||
#include "../src/core/instrument/bake/bake_reset.h"
|
||||
|
||||
#include <cstdio>
|
||||
|
||||
using namespace reasampler;
|
||||
using namespace reasampler::instrument::bake;
|
||||
using reasampler::instrument::map::InstrumentParams;
|
||||
using reasampler::instrument::map::PlaySeconds;
|
||||
|
||||
static int g_fail = 0;
|
||||
#define CHECK(cond) do { if(!(cond)) { \
|
||||
std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)
|
||||
|
||||
namespace {
|
||||
|
||||
// Every field moved off its default, so a reset that misses one is visible.
|
||||
InstrumentParams dialed() {
|
||||
InstrumentParams p;
|
||||
p.rootOverride = 43;
|
||||
p.loopOverride = SampleLoop{true, 111, 222};
|
||||
p.startPoint = 4321;
|
||||
p.loopCrossfadeFrames = 512;
|
||||
p.keyTrack = 0.5;
|
||||
p.velocityCurve = VelocityCurve::linear();
|
||||
|
||||
p.play.playMode = PlayMode::Trigger;
|
||||
p.play.adsr.attackSeconds = 0.4;
|
||||
p.play.adsr.holdSeconds = 0.3;
|
||||
p.play.adsr.decaySeconds = 0.2;
|
||||
p.play.adsr.sustainLevel = 0.1;
|
||||
p.play.adsr.releaseSeconds = 0.9;
|
||||
p.play.adsr.attackCurve = 2.5;
|
||||
p.play.adsr.decayCurve = 0.4;
|
||||
p.play.adsr.releaseCurve = 3.0;
|
||||
p.play.trigger.lengthFraction = 0.25;
|
||||
p.play.trigAhd.attackSeconds = 0.11;
|
||||
p.play.trigAhd.decaySeconds = 0.22;
|
||||
p.play.trigAhd.holdFraction = 0.33;
|
||||
p.play.trigAhd.attackCurve = 1.7;
|
||||
p.play.pitchEngine = PitchEngine::Varispeed;
|
||||
p.play.pitchEnv.enabled = true;
|
||||
p.play.pitchEnv.peakSemitones = -7.0;
|
||||
p.play.pitchEnv.shape.attackSeconds = 0.05;
|
||||
p.play.pitchVelocityCurve = VelocityCurve::linear();
|
||||
p.play.filter.enabled = true;
|
||||
p.play.filter.modAmount = -0.8;
|
||||
p.play.filter.velAmount = 0.6;
|
||||
p.play.filter.keyTrack = 1.5;
|
||||
p.play.filter.env.attackSeconds = 0.7;
|
||||
p.play.filter.trigEnv.decaySeconds = 0.8;
|
||||
p.play.filter.velocityCurve = VelocityCurve::linear();
|
||||
p.play.ampSpline.mode = EnvMode::Spline;
|
||||
p.play.ampSpline.contour = VelocityCurve::linear();
|
||||
p.play.pitchSpline.mode = EnvMode::Spline;
|
||||
p.play.filterSpline.mode = EnvMode::Spline;
|
||||
return p;
|
||||
}
|
||||
|
||||
bool sameCurve(const VelocityCurve& a, const VelocityCurve& b) {
|
||||
if (a.domain() != b.domain() || a.size() != b.size()) return false;
|
||||
for (std::size_t i = 0; i < a.size(); ++i) {
|
||||
if (a.points()[i].velocity != b.points()[i].velocity) return false;
|
||||
if (a.points()[i].value != b.points()[i].value) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
const InstrumentParams before = dialed();
|
||||
const BakeReset reset = resetAfterBake(before);
|
||||
const InstrumentParams& after = reset.params;
|
||||
const InstrumentParams fresh; // the defaults every reset control must land on
|
||||
const PlaySeconds freshPlay;
|
||||
|
||||
// --- SURVIVE: mapping facts, absent from the printed audio ----------------------
|
||||
CHECK(after.rootOverride.has_value());
|
||||
CHECK(after.rootOverride == before.rootOverride);
|
||||
CHECK(after.keyTrack == before.keyTrack);
|
||||
CHECK(after.keyTrack == 0.5); // and it is the dialed value, not the default 1.0
|
||||
CHECK(fresh.keyTrack != before.keyTrack); // the fixture really did move it
|
||||
|
||||
// --- RESET: loop points, start point, crossfade ---------------------------------
|
||||
CHECK(!after.loopOverride.has_value());
|
||||
CHECK(!after.startPoint.has_value());
|
||||
CHECK(after.loopCrossfadeFrames == 0);
|
||||
|
||||
// --- RESET: the velocity transfer curves ----------------------------------------
|
||||
CHECK(sameCurve(after.velocityCurve, VelocityCurve::flat()));
|
||||
CHECK(!sameCurve(after.velocityCurve, before.velocityCurve));
|
||||
CHECK(sameCurve(after.play.pitchVelocityCurve, VelocityCurve::zero()));
|
||||
CHECK(sameCurve(after.play.filter.velocityCurve, VelocityCurve::zero()));
|
||||
|
||||
// --- RESET: the amp envelope, staged, every stage and every curve exponent ------
|
||||
CHECK(after.play.playMode == freshPlay.playMode);
|
||||
CHECK(after.play.adsr.attackSeconds == freshPlay.adsr.attackSeconds);
|
||||
CHECK(after.play.adsr.holdSeconds == freshPlay.adsr.holdSeconds);
|
||||
CHECK(after.play.adsr.decaySeconds == freshPlay.adsr.decaySeconds);
|
||||
CHECK(after.play.adsr.sustainLevel == freshPlay.adsr.sustainLevel);
|
||||
CHECK(after.play.adsr.releaseSeconds == freshPlay.adsr.releaseSeconds);
|
||||
CHECK(after.play.adsr.attackCurve == freshPlay.adsr.attackCurve);
|
||||
CHECK(after.play.adsr.decayCurve == freshPlay.adsr.decayCurve);
|
||||
CHECK(after.play.adsr.releaseCurve == freshPlay.adsr.releaseCurve);
|
||||
CHECK(after.play.trigger.lengthFraction == freshPlay.trigger.lengthFraction);
|
||||
CHECK(after.play.trigAhd.attackSeconds == freshPlay.trigAhd.attackSeconds);
|
||||
CHECK(after.play.trigAhd.decaySeconds == freshPlay.trigAhd.decaySeconds);
|
||||
CHECK(after.play.trigAhd.holdFraction == freshPlay.trigAhd.holdFraction);
|
||||
CHECK(after.play.trigAhd.attackCurve == freshPlay.trigAhd.attackCurve);
|
||||
|
||||
// --- RESET: pitch engine + pitch envelope ---------------------------------------
|
||||
CHECK(after.play.pitchEngine == freshPlay.pitchEngine);
|
||||
CHECK(after.play.pitchEngine == PitchEngine::Preserve); // the product default
|
||||
CHECK(!after.play.pitchEnv.enabled);
|
||||
CHECK(after.play.pitchEnv.peakSemitones == 0.0);
|
||||
CHECK(after.play.pitchEnv.shape.attackSeconds == freshPlay.pitchEnv.shape.attackSeconds);
|
||||
|
||||
// --- RESET: the filter, including its velocity/key-tracking mod -----------------
|
||||
CHECK(!after.play.filter.enabled);
|
||||
CHECK(after.play.filter.modAmount == 0.0);
|
||||
CHECK(after.play.filter.velAmount == 0.0);
|
||||
CHECK(after.play.filter.keyTrack == 0.0);
|
||||
CHECK(after.play.filter.env.attackSeconds == freshPlay.filter.env.attackSeconds);
|
||||
CHECK(after.play.filter.trigEnv.decaySeconds == freshPlay.filter.trigEnv.decaySeconds);
|
||||
|
||||
// --- RESET: the three spline contours AND their mode flags ----------------------
|
||||
// The flag selects which shape ran, so the shape it selected is in the audio; with
|
||||
// both contours reset it also has nothing left to preserve.
|
||||
CHECK(after.play.ampSpline.mode == EnvMode::Staged);
|
||||
CHECK(after.play.pitchSpline.mode == EnvMode::Staged);
|
||||
CHECK(after.play.filterSpline.mode == EnvMode::Staged);
|
||||
CHECK(sameCurve(after.play.ampSpline.contour, VelocityCurve::rampDown()));
|
||||
CHECK(!sameCurve(after.play.ampSpline.contour, before.play.ampSpline.contour));
|
||||
|
||||
// --- RESET: master gain ----------------------------------------------------------
|
||||
CHECK(reset.masterGainLinear == 1.0);
|
||||
|
||||
// --- An absent root override stays absent (nothing is invented) ------------------
|
||||
{
|
||||
InstrumentParams noRoot = dialed();
|
||||
noRoot.rootOverride.reset();
|
||||
CHECK(!resetAfterBake(noRoot).params.rootOverride.has_value());
|
||||
}
|
||||
|
||||
if (g_fail == 0) std::printf("bake_reset: all tests passed\n");
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
// Standalone tests for reasampler::wire::bake_wire — no VST3, no REAPER, no framework.
|
||||
// Same fast assert loop as the sibling wire tests.
|
||||
//
|
||||
// Covers: request + outcome round-trips including bytes that would break a delimiter-based
|
||||
// format; the refusals every house wire record shares (wrong tag, truncation, trailing
|
||||
// garbage, a swapped record kind); an unrecognized status integer degrading to Failed
|
||||
// rather than to Ok; and the action lookup name's leading underscore.
|
||||
|
||||
#include "../src/core/wire/bake_wire.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
|
||||
using namespace reasampler::wire;
|
||||
|
||||
static int g_fail = 0;
|
||||
#define CHECK(cond) do { if(!(cond)) { \
|
||||
std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)
|
||||
|
||||
int main() {
|
||||
// --- Request round-trip, with hostile field content -----------------------------
|
||||
{
|
||||
BakeRequest req;
|
||||
req.instanceGuid = "0123abcd";
|
||||
req.stagedFilePath = "C:/Temp/re:sampler 9000/bake 12:34.wav"; // colons + spaces
|
||||
req.sourceSampleId = "cap-1";
|
||||
req.sourceRelativePath = "reasampler_bank/kick.wav";
|
||||
req.sourceDisplayName = "Kick r2";
|
||||
req.ownUsageKey = "rsusage_0123abcd";
|
||||
req.rootNote = 36;
|
||||
req.generation = 1893456000;
|
||||
|
||||
const std::string encoded = encodeBakeRequest(req);
|
||||
const auto decoded = decodeBakeRequest(encoded);
|
||||
CHECK(decoded.has_value());
|
||||
CHECK(*decoded == req);
|
||||
|
||||
// Empty strings and a zero generation survive too (a first, un-named source).
|
||||
BakeRequest bare;
|
||||
CHECK(decodeBakeRequest(encodeBakeRequest(bare)) == bare);
|
||||
}
|
||||
|
||||
// --- Outcome round-trip -----------------------------------------------------------
|
||||
{
|
||||
BakeOutcome out;
|
||||
out.status = BakeStatus::Ok;
|
||||
out.sampleId = "bake-1893456000-kick_1893456000.wav";
|
||||
out.relativePath = "reasampler_bank/kick_1893456000.wav";
|
||||
out.displayName = "Kick r3";
|
||||
out.rootNote = 36;
|
||||
out.channelCount = 2;
|
||||
out.replaced = true;
|
||||
out.message = "replaced the bank entry";
|
||||
out.generation = 1893456000;
|
||||
|
||||
const auto decoded = decodeBakeOutcome(encodeBakeOutcome(out));
|
||||
CHECK(decoded.has_value());
|
||||
CHECK(*decoded == out);
|
||||
CHECK(decoded->replaced);
|
||||
|
||||
out.replaced = false;
|
||||
CHECK(decodeBakeOutcome(encodeBakeOutcome(out))->replaced == false);
|
||||
}
|
||||
|
||||
// --- Malformed input is refused, never half-parsed ---------------------------------
|
||||
{
|
||||
BakeRequest req;
|
||||
req.instanceGuid = "abc";
|
||||
req.rootNote = 60;
|
||||
const std::string good = encodeBakeRequest(req);
|
||||
|
||||
CHECK(!decodeBakeRequest("").has_value());
|
||||
CHECK(!decodeBakeRequest("rsbakereq0" + good.substr(10)).has_value()); // wrong tag
|
||||
CHECK(!decodeBakeRequest(good.substr(0, good.size() - 3)).has_value()); // truncated
|
||||
CHECK(!decodeBakeRequest(good + "junk").has_value()); // trailing
|
||||
// The two records share a key; each must refuse the other's bytes outright.
|
||||
CHECK(!decodeBakeOutcome(good).has_value());
|
||||
CHECK(!decodeBakeRequest(encodeBakeOutcome(BakeOutcome{})).has_value());
|
||||
}
|
||||
|
||||
// --- A status integer this build does not know reads as a FAILURE ------------------
|
||||
{
|
||||
// Hand-built with a future status value; every other field is well-formed, so only
|
||||
// the vocabulary gap is under test.
|
||||
BakeOutcome out;
|
||||
out.status = BakeStatus::Ok;
|
||||
out.generation = 7;
|
||||
std::string wire = encodeBakeOutcome(out);
|
||||
// The status field is the first after the tag: "<len>':'<digits>".
|
||||
const std::string okField = "1:0";
|
||||
const std::size_t at = wire.find(okField);
|
||||
CHECK(at != std::string::npos);
|
||||
wire.replace(at, okField.size(), "2:99");
|
||||
|
||||
const auto decoded = decodeBakeOutcome(wire);
|
||||
CHECK(decoded.has_value());
|
||||
CHECK(decoded->status == BakeStatus::Failed); // never Ok
|
||||
CHECK(decoded->generation == 7);
|
||||
}
|
||||
|
||||
// --- The lookup name carries the underscore the registration string does not -------
|
||||
{
|
||||
const std::string lookup = bakeActionLookupName();
|
||||
CHECK(!lookup.empty());
|
||||
CHECK(lookup[0] == '_');
|
||||
CHECK(lookup.find(kBakeActionSuffix) != std::string::npos);
|
||||
CHECK(lookup.find(kBakeActionSuffix) > 0u);
|
||||
}
|
||||
|
||||
if (g_fail == 0) std::printf("bake_wire: all tests passed\n");
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// Standalone tests for reasampler::model::resample_name — no REAPER, no framework.
|
||||
//
|
||||
// Covers: the first iteration, the chain incrementing rather than stacking, the empty
|
||||
// name, and the tails that are NOT one of ours and must be left alone.
|
||||
|
||||
#include "../src/core/model/resample_name.h"
|
||||
|
||||
#include <cstdio>
|
||||
|
||||
using reasampler::model::nextIterationName;
|
||||
|
||||
static int g_fail = 0;
|
||||
#define CHECK(cond) do { if(!(cond)) { \
|
||||
std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)
|
||||
|
||||
int main() {
|
||||
CHECK(nextIterationName("Kick") == "Kick r2");
|
||||
CHECK(nextIterationName("Kick r2") == "Kick r3");
|
||||
CHECK(nextIterationName("Kick r9") == "Kick r10");
|
||||
CHECK(nextIterationName("Kick r10") == "Kick r11");
|
||||
|
||||
// The chain composes: three bakes read as r2, r3, r4 — not "Kick r2 r2 r2".
|
||||
CHECK(nextIterationName(nextIterationName(nextIterationName("Kick"))) == "Kick r4");
|
||||
|
||||
CHECK(nextIterationName("") == "resample r2");
|
||||
CHECK(nextIterationName(" ") == " r2"); // a name of spaces is still a name
|
||||
|
||||
// Tails that are not ours: appended to, never rewritten.
|
||||
CHECK(nextIterationName("Kick r") == "Kick r r2");
|
||||
CHECK(nextIterationName("Kick r0") == "Kick r0 r2");
|
||||
CHECK(nextIterationName("Kick rx") == "Kick rx r2");
|
||||
CHECK(nextIterationName("Kickr2") == "Kickr2 r2"); // no space before the r
|
||||
CHECK(nextIterationName("Kick R2") == "Kick R2 r2"); // capital R is not the marker
|
||||
CHECK(nextIterationName("r2") == "r2 r2"); // no stem to attach the tail to
|
||||
CHECK(nextIterationName("2") == "2 r2");
|
||||
CHECK(nextIterationName("Take 3") == "Take 3 r2"); // digits without the " r"
|
||||
|
||||
// A digit run past the counting bound is left alone rather than wrapping into a low
|
||||
// number that would collide with an existing entry's name.
|
||||
CHECK(nextIterationName("Kick r99999999999999999999") ==
|
||||
"Kick r99999999999999999999 r2");
|
||||
|
||||
if (g_fail == 0) std::printf("resample_name: all tests passed\n");
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
@@ -59,12 +59,16 @@ static void testToolbarRunIsOrderedRightToLeftWithoutOverlap() {
|
||||
CHECK(r.chanMono.right() == r.chanStereo.x);
|
||||
CHECK(r.velCell.right() <= r.chanMono.x);
|
||||
CHECK(r.preview.right() <= r.velCell.x);
|
||||
CHECK(r.title.right() <= r.preview.x);
|
||||
CHECK(r.bake.right() <= r.preview.x);
|
||||
CHECK(r.bake.width == kBakeButtonWidth);
|
||||
CHECK(r.bake.y == r.preview.y); // shares the run's button baseline
|
||||
CHECK(r.bake.height == r.preview.height);
|
||||
CHECK(r.title.right() <= r.bake.x); // the title yields to the bake, not the preview
|
||||
CHECK(r.title.x == band.x + kPad);
|
||||
CHECK(r.title.width > 0);
|
||||
|
||||
// Every toolbar rect sits inside the toolbar row.
|
||||
const Rect items[] = {r.title, r.preview, r.velCell, r.chanMono,
|
||||
const Rect items[] = {r.title, r.bake, r.preview, r.velCell, r.chanMono,
|
||||
r.chanStereo, r.navBrowse};
|
||||
for (const Rect& it : items) {
|
||||
CHECK(it.y >= r.toolbar.y && it.bottom() <= r.toolbar.bottom());
|
||||
@@ -78,7 +82,7 @@ static void testChromePartsNeverOverlapAtAnyWidth() {
|
||||
// stay inside its own row, clear of every control.
|
||||
CHECK(!overlaps(r.toolbar, r.rootStrip));
|
||||
CHECK(r.rootStrip.y >= r.controls.y && r.rootStrip.bottom() <= r.controls.bottom());
|
||||
const Rect items[] = {r.preview, r.velCell, r.chanMono, r.chanStereo,
|
||||
const Rect items[] = {r.bake, r.preview, r.velCell, r.chanMono, r.chanStereo,
|
||||
r.navBrowse};
|
||||
for (const Rect& it : items) {
|
||||
CHECK(!overlaps(it, r.rootStrip));
|
||||
|
||||
@@ -449,7 +449,44 @@ static void testTiedUniverseIsStrictSubsetOfProtectedUniverse() {
|
||||
CHECK(tiedUsageExists(state, "bank/self.wav", "rsusage_ME") == Answer::No);
|
||||
}
|
||||
|
||||
// The resample's own branch off the three answers. Indeterminate must land where Yes does:
|
||||
// AddDistinct disturbs no existing holder, so it is the non-destructive side.
|
||||
static void testResampleLandingTakesReplaceOnlyOnADefiniteNo() {
|
||||
CHECK(resampleLanding(Answer::No) == Landing::Replace);
|
||||
CHECK(resampleLanding(Answer::Yes) == Landing::AddDistinct);
|
||||
CHECK(resampleLanding(Answer::Indeterminate) == Landing::AddDistinct);
|
||||
|
||||
// Composed with the query itself, over the three states a real bake meets.
|
||||
OriginLedger ledger;
|
||||
ledger.record(originOf("bank/src.wav", OriginKind::Capture, "S-src"));
|
||||
|
||||
// Sole holder, excluding itself -> nothing is tied -> replace.
|
||||
const UsageFoldResult sole = foldLive(
|
||||
{usage("rsusage_ME", "{T1}", {UsageHold{"S-src", "bank/src.wav"}})}, {"{T1}"});
|
||||
const TrackingState soleState{LedgerStatus::Loaded, ledger, sole};
|
||||
CHECK(resampleLanding(tiedUsageExists(soleState, "bank/src.wav", "rsusage_ME")) ==
|
||||
Landing::Replace);
|
||||
|
||||
// A second live instance holds it -> add distinct, so that holder is undisturbed.
|
||||
const UsageFoldResult shared = foldLive(
|
||||
{usage("rsusage_ME", "{T1}", {UsageHold{"S-src", "bank/src.wav"}}),
|
||||
usage("rsusage_OTHER", "{T2}", {UsageHold{"S-src", "bank/src.wav"}})},
|
||||
{"{T1}", "{T2}"});
|
||||
const TrackingState sharedState{LedgerStatus::Loaded, ledger, shared};
|
||||
CHECK(resampleLanding(tiedUsageExists(sharedState, "bank/src.wav", "rsusage_ME")) ==
|
||||
Landing::AddDistinct);
|
||||
|
||||
// A degraded ledger cannot answer -> add distinct rather than take over an entry
|
||||
// whose holders are unknown.
|
||||
const TrackingState degradedState{LedgerStatus::Unreadable, ledger, sole};
|
||||
CHECK(tiedUsageExists(degradedState, "bank/src.wav", "rsusage_ME") ==
|
||||
Answer::Indeterminate);
|
||||
CHECK(resampleLanding(tiedUsageExists(degradedState, "bank/src.wav", "rsusage_ME")) ==
|
||||
Landing::AddDistinct);
|
||||
}
|
||||
|
||||
int main() {
|
||||
testResampleLandingTakesReplaceOnlyOnADefiniteNo();
|
||||
testPruneProtectedSet();
|
||||
testLiveHoldProtectsDeReferencedCapture();
|
||||
testUnreadableUsageBlocksPruneAndNamesIt();
|
||||
|
||||
Reference in New Issue
Block a user