Make spline EG drags audible live: held and new notes follow a dragged knot
Contours ride their own seqlock block into a double-buffered engine copy, and the swap step is smoothed. Each drag move publishes contours without a model write; release still commits and reloads.
This commit is contained in:
@@ -0,0 +1,438 @@
|
||||
// Standalone tests for LIVE CONTOUR DELIVERY — a drawn EG edited while notes sound — no VST3,
|
||||
// no REAPER, no framework. Asserts that each of the three contours (amp, pitch, filter) moves
|
||||
// the note already sounding and the next one struck, that the swap is absorbed rather than
|
||||
// stepped, that what start() latched (a Staged EG) stays latched, that an unchanged publish is
|
||||
// byte-identical, that two engines sharing one block both follow (the drain slot), and that the
|
||||
// whole audio-side path allocates nothing. This executable replaces the global allocator to
|
||||
// prove the last one, which is why these tests are not in live_delivery_tests.
|
||||
|
||||
#include "../src/core/instrument/engine/live_contours.h"
|
||||
#include "../src/core/instrument/engine/voice_engine.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <new>
|
||||
#include <vector>
|
||||
|
||||
using namespace reasampler;
|
||||
using instrument::engine::ContourPoints;
|
||||
using instrument::engine::CurveDomain;
|
||||
using instrument::engine::LiveContourBlock;
|
||||
using instrument::engine::contourPoints;
|
||||
using instrument::engine::foldContours;
|
||||
|
||||
// --- Allocation counter -----------------------------------------------------------------
|
||||
// Counts every global allocation while armed. Replacing the global operator new is the only way
|
||||
// to observe allocations made inside sampler_core without instrumenting it.
|
||||
|
||||
static std::atomic<long> g_allocations{0};
|
||||
static std::atomic<bool> g_countAllocations{false};
|
||||
|
||||
static void* countedAlloc(std::size_t n) {
|
||||
if (g_countAllocations.load(std::memory_order_relaxed)) {
|
||||
g_allocations.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
if (void* p = std::malloc(n ? n : 1)) return p;
|
||||
throw std::bad_alloc();
|
||||
}
|
||||
|
||||
void* operator new(std::size_t n) { return countedAlloc(n); }
|
||||
void* operator new[](std::size_t n) { return countedAlloc(n); }
|
||||
void operator delete(void* p) noexcept { std::free(p); }
|
||||
void operator delete[](void* p) noexcept { std::free(p); }
|
||||
void operator delete(void* p, std::size_t) noexcept { std::free(p); }
|
||||
void operator delete[](void* p, std::size_t) noexcept { std::free(p); }
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
|
||||
static int g_fail = 0;
|
||||
#define CHECK(cond) do { if(!(cond)) { \
|
||||
std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)
|
||||
|
||||
constexpr double kPi = 3.14159265358979323846;
|
||||
constexpr int kRate = 48000;
|
||||
constexpr std::size_t kBlock = 256;
|
||||
constexpr int kRoot = 60;
|
||||
|
||||
// Every frame 1.0, Trigger (a drawn EG is a Trigger fact — resolvePlay's job on the real path),
|
||||
// played at the root with a flat velocity curve: an output frame IS the amplitude.
|
||||
static SampleData dcSample(std::size_t frames) {
|
||||
SampleData s;
|
||||
s.frames.assign(frames, 1.0f);
|
||||
s.sampleRate = kRate;
|
||||
s.rootNote = kRoot;
|
||||
s.play.playMode = PlayMode::Trigger;
|
||||
return s;
|
||||
}
|
||||
|
||||
static VelocityCurve flatAt(double y, CurveDomain domain) {
|
||||
return VelocityCurve::fromPoints({{0.0, y}, {127.0, y}}, domain);
|
||||
}
|
||||
|
||||
// Three points rather than two, so a publish that changes the point COUNT is what lands.
|
||||
static VelocityCurve flatAt3(double y, CurveDomain domain) {
|
||||
return VelocityCurve::fromPoints({{0.0, y}, {63.0, y}, {127.0, y}}, domain);
|
||||
}
|
||||
|
||||
static double maxAbsDelta(const std::vector<AudioSample>& v, std::size_t from, std::size_t to) {
|
||||
double worst = 0.0;
|
||||
for (std::size_t i = from + 1; i < to && i < v.size(); ++i) {
|
||||
worst = (std::max)(worst, std::fabs(static_cast<double>(v[i]) - v[i - 1]));
|
||||
}
|
||||
return worst;
|
||||
}
|
||||
|
||||
static double peakOf(const std::vector<AudioSample>& v, std::size_t from, std::size_t to) {
|
||||
double peak = 0.0;
|
||||
for (std::size_t i = from; i < to && i < v.size(); ++i) {
|
||||
peak = (std::max)(peak, std::fabs(static_cast<double>(v[i])));
|
||||
}
|
||||
return peak;
|
||||
}
|
||||
|
||||
// One held note through a one-voice engine observing `block`, with `edited`'s contours
|
||||
// published at the top of block `swapBlock` (-1: never). The initial publish is `sample.play`'s
|
||||
// own contours, the state a reload leaves.
|
||||
static std::vector<AudioSample> renderHeld(SampleData& sample, LiveContourBlock& block,
|
||||
const PlayParams* edited, int swapBlock, int blocks) {
|
||||
sample.liveContours = █
|
||||
block.publish(foldContours(sample.play));
|
||||
VoiceEngine engine(1, sample);
|
||||
engine.noteOn(kRoot, 127);
|
||||
std::vector<AudioSample> out;
|
||||
for (int b = 0; b < blocks; ++b) {
|
||||
if (edited && b == swapBlock) block.publish(foldContours(*edited));
|
||||
engine.render(out, kBlock);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// --- The fold ------------------------------------------------------------------------------
|
||||
|
||||
static void testTheFoldCarriesEveryPointAndItsHardFlag() {
|
||||
std::vector<VelocityPoint> pts = {{0.0, 0.1}, {40.0, 0.9}, {90.0, 0.2}, {127.0, 0.7}};
|
||||
VelocityCurve c = VelocityCurve::fromPoints(pts, CurveDomain::Unipolar);
|
||||
c.setHard(2, true);
|
||||
const ContourPoints folded = contourPoints(c);
|
||||
CHECK(folded.count == 4);
|
||||
for (std::size_t i = 0; i < 4; ++i) {
|
||||
CHECK(folded.points[i].velocity == c.points()[i].velocity);
|
||||
CHECK(folded.points[i].value == c.points()[i].value);
|
||||
CHECK(folded.points[i].hard == c.points()[i].hard);
|
||||
}
|
||||
CHECK(folded.points[2].hard);
|
||||
|
||||
PlayParams p;
|
||||
p.ampSpline.contour = flatAt(0.3, CurveDomain::Unipolar);
|
||||
p.pitchSpline.contour = c;
|
||||
p.filterSpline.contour = flatAt3(0.6, CurveDomain::Unipolar);
|
||||
const auto all = foldContours(p);
|
||||
CHECK(all.amp.count == 2 && all.amp.points[1].value == 0.3);
|
||||
CHECK(all.pitch.count == 4 && all.pitch.points[1].value == c.points()[1].value);
|
||||
CHECK(all.filter.count == 3 && all.filter.points[2].value == 0.6);
|
||||
}
|
||||
|
||||
// --- Amp -----------------------------------------------------------------------------------
|
||||
|
||||
static void testAHeldNoteFollowsAnAmpContourEditWithoutAStep() {
|
||||
SampleData s = dcSample(200000);
|
||||
s.play.ampSpline.mode = EnvMode::Spline;
|
||||
s.play.ampSpline.contour = flatAt(1.0, CurveDomain::Unipolar);
|
||||
PlayParams edited = s.play;
|
||||
edited.ampSpline.contour = flatAt3(0.25, CurveDomain::Unipolar);
|
||||
|
||||
SampleData base = s;
|
||||
LiveContourBlock blockA, blockB;
|
||||
const std::vector<AudioSample> moved = renderHeld(s, blockA, &edited, 4, 12);
|
||||
const std::vector<AudioSample> still = renderHeld(base, blockB, nullptr, -1, 12);
|
||||
const std::size_t swapAt = 4 * kBlock;
|
||||
|
||||
CHECK(moved[swapAt - 1] == 1.0f);
|
||||
// Absorbed: the first frame read through the new contour reproduces the old level exactly...
|
||||
CHECK(moved[swapAt] == 1.0f);
|
||||
// ...and glides from there. An unabsorbed swap would step by 0.75 in one frame.
|
||||
CHECK(maxAbsDelta(moved, swapAt - 1, swapAt + 64) < 0.05);
|
||||
CHECK(moved[swapAt + 32] < 0.9f);
|
||||
// The held note lands on the edited contour, where the unedited run stays on the old one.
|
||||
CHECK(std::fabs(moved.back() - 0.25) < 1e-6);
|
||||
CHECK(still.back() == 1.0f);
|
||||
}
|
||||
|
||||
// The processor marshals note-ons BEFORE the block renders, so a publish and a note-on landing
|
||||
// in one block reach the engine through startVoice first. The voice already sounding must be
|
||||
// rebound there too: left on the buffer the read just displaced, it would never see this edit.
|
||||
static void testANoteOnInTheSwapBlockRebindsTheSoundingVoiceAndSnapsTheNewOne() {
|
||||
SampleData s = dcSample(200000);
|
||||
s.play.ampSpline.mode = EnvMode::Spline;
|
||||
s.play.ampSpline.contour = flatAt(1.0, CurveDomain::Unipolar);
|
||||
LiveContourBlock block;
|
||||
s.liveContours = █
|
||||
block.publish(foldContours(s.play));
|
||||
|
||||
VoiceEngine engine(2, s);
|
||||
engine.noteOn(kRoot, 127);
|
||||
std::vector<AudioSample> out;
|
||||
for (int b = 0; b < 4; ++b) engine.render(out, kBlock);
|
||||
|
||||
PlayParams edited = s.play;
|
||||
edited.ampSpline.contour = flatAt3(0.25, CurveDomain::Unipolar);
|
||||
block.publish(foldContours(edited));
|
||||
engine.noteOn(kRoot, 127); // the second voice, struck in the swap block
|
||||
for (int b = 0; b < 8; ++b) engine.render(out, kBlock);
|
||||
|
||||
const std::size_t swapAt = 4 * kBlock;
|
||||
// The sounding voice's absorbed 1.0 plus the fresh voice's snapped 0.25 — the fresh one
|
||||
// takes the edit outright even though the snapshot still carries the old contour.
|
||||
CHECK(std::fabs(out[swapAt] - 1.25) < 1e-6);
|
||||
// Both land on the edit: 0.25 + 0.25. A sounding voice that missed it would leave 1.25.
|
||||
CHECK(std::fabs(out.back() - 0.5) < 1e-6);
|
||||
}
|
||||
|
||||
static void testANoteStruckAfterAnEditPlaysItFromItsFirstFrame() {
|
||||
SampleData s = dcSample(200000);
|
||||
s.play.ampSpline.mode = EnvMode::Spline;
|
||||
s.play.ampSpline.contour = flatAt(1.0, CurveDomain::Unipolar);
|
||||
LiveContourBlock block;
|
||||
s.liveContours = █
|
||||
block.publish(foldContours(s.play));
|
||||
VoiceEngine engine(1, s);
|
||||
|
||||
PlayParams edited = s.play;
|
||||
edited.ampSpline.contour = flatAt3(0.25, CurveDomain::Unipolar);
|
||||
block.publish(foldContours(edited));
|
||||
engine.noteOn(kRoot, 127);
|
||||
std::vector<AudioSample> out;
|
||||
engine.render(out, kBlock);
|
||||
CHECK(out[0] == 0.25f);
|
||||
|
||||
// The snapshot really does still say 1.0: with no block the same engine plays it.
|
||||
SampleData bare = s;
|
||||
bare.liveContours = nullptr;
|
||||
VoiceEngine bareEngine(1, bare);
|
||||
bareEngine.noteOn(kRoot, 127);
|
||||
std::vector<AudioSample> bareOut;
|
||||
bareEngine.render(bareOut, kBlock);
|
||||
CHECK(bareOut[0] == 1.0f);
|
||||
}
|
||||
|
||||
// --- Pitch ---------------------------------------------------------------------------------
|
||||
|
||||
// A ramp source under Varispeed: an output frame is the read head, so the frame-to-frame step
|
||||
// IS the pitch ratio.
|
||||
static void testAHeldNoteFollowsAPitchContourEdit() {
|
||||
SampleData s;
|
||||
s.frames.resize(1u << 15);
|
||||
for (std::size_t i = 0; i < s.frames.size(); ++i) s.frames[i] = static_cast<float>(i);
|
||||
s.sampleRate = kRate;
|
||||
s.rootNote = kRoot;
|
||||
s.play.playMode = PlayMode::Trigger;
|
||||
s.play.pitchEnv.enabled = true;
|
||||
s.play.pitchEnv.peakSemitones = 12.0;
|
||||
s.play.pitchSpline.mode = EnvMode::Spline;
|
||||
s.play.pitchSpline.contour = flatAt(0.0, CurveDomain::Bipolar);
|
||||
PlayParams edited = s.play;
|
||||
edited.pitchSpline.contour = flatAt3(1.0, CurveDomain::Bipolar); // +1 x 12 st = an octave
|
||||
|
||||
SampleData base = s;
|
||||
LiveContourBlock blockA, blockB;
|
||||
const std::vector<AudioSample> moved = renderHeld(s, blockA, &edited, 4, 12);
|
||||
const std::vector<AudioSample> still = renderHeld(base, blockB, nullptr, -1, 12);
|
||||
const std::size_t swapAt = 4 * kBlock;
|
||||
const std::size_t n = moved.size();
|
||||
|
||||
CHECK(std::fabs((moved[swapAt - 1] - moved[swapAt - 2]) - 1.0) < 1e-3);
|
||||
CHECK(std::fabs((moved[n - 1] - moved[n - 2]) - 2.0) < 1e-3);
|
||||
CHECK(std::fabs((still[n - 1] - still[n - 2]) - 1.0) < 1e-3);
|
||||
// Absorbed here too: the first frame read through the new contour still advances at the old
|
||||
// ratio, where an unabsorbed swap would already step by 2.
|
||||
CHECK(std::fabs((moved[swapAt + 1] - moved[swapAt]) - 1.0) < 1e-3);
|
||||
}
|
||||
|
||||
// --- Filter --------------------------------------------------------------------------------
|
||||
|
||||
static void testAHeldNoteFollowsAFilterContourEdit() {
|
||||
SampleData s;
|
||||
s.frames.resize(100000);
|
||||
for (std::size_t i = 0; i < s.frames.size(); ++i) {
|
||||
s.frames[i] = static_cast<float>(std::sin(2.0 * kPi * static_cast<double>(i) / 8.0));
|
||||
}
|
||||
s.sampleRate = kRate;
|
||||
s.rootNote = kRoot;
|
||||
s.play.playMode = PlayMode::Trigger;
|
||||
s.play.filter.enabled = true;
|
||||
s.play.filter.settings.cutoffNorm = 0.1f;
|
||||
s.play.filter.settings.morphNorm = 1.0f; // low-pass
|
||||
s.play.filter.modAmount = 0.8;
|
||||
s.play.filterSpline.mode = EnvMode::Spline;
|
||||
s.play.filterSpline.contour = flatAt(0.0, CurveDomain::Unipolar);
|
||||
PlayParams edited = s.play;
|
||||
edited.filterSpline.contour = flatAt3(1.0, CurveDomain::Unipolar); // corner 0.1 -> 0.9
|
||||
|
||||
SampleData base = s;
|
||||
LiveContourBlock blockA, blockB;
|
||||
const std::vector<AudioSample> moved = renderHeld(s, blockA, &edited, 4, 16);
|
||||
const std::vector<AudioSample> still = renderHeld(base, blockB, nullptr, -1, 16);
|
||||
const std::size_t tail = moved.size() - 1024;
|
||||
|
||||
const double openedPeak = peakOf(moved, tail, moved.size());
|
||||
const double closedPeak = peakOf(still, tail, still.size());
|
||||
// A 6 kHz tone through the corner the edit opened, against the unedited note's, which stays
|
||||
// filtered away (measured ~0.18 vs ~0.0002 — the margins below are loose on purpose).
|
||||
CHECK(openedPeak > 0.1);
|
||||
CHECK(openedPeak > 10.0 * closedPeak);
|
||||
}
|
||||
|
||||
// --- What stays latched, and what must not move --------------------------------------------
|
||||
|
||||
// Staged|Spline is a note-on fact: an EG that start() left Staged ignores any contour publish.
|
||||
static void testAStagedEnvelopeIgnoresContourPublishes() {
|
||||
SampleData s = dcSample(200000);
|
||||
// A staged shape that is not flat from frame 0, so a leak would show.
|
||||
s.play.trigAhd.decayFrames = 20000;
|
||||
s.play.trigAhd.holdFraction = 0.0;
|
||||
PlayParams hostile = s.play;
|
||||
hostile.ampSpline.contour = flatAt3(0.0, CurveDomain::Unipolar);
|
||||
hostile.pitchSpline.contour = flatAt3(1.0, CurveDomain::Bipolar);
|
||||
hostile.filterSpline.contour = flatAt3(1.0, CurveDomain::Unipolar);
|
||||
|
||||
SampleData base = s;
|
||||
LiveContourBlock blockA, blockB;
|
||||
const std::vector<AudioSample> published = renderHeld(s, blockA, &hostile, 3, 10);
|
||||
const std::vector<AudioSample> none = renderHeld(base, blockB, nullptr, -1, 10);
|
||||
CHECK(published == none);
|
||||
CHECK(none[3 * kBlock + 100] < 1.0f); // the staged decay is really running
|
||||
}
|
||||
|
||||
// Republishing the contour a voice is already reading rebinds it mid-read; the read must not
|
||||
// move by a bit. A curved, multi-segment contour with a hard point so the re-located segment
|
||||
// and its tangents are genuinely exercised.
|
||||
static void testRepublishingAnUnchangedContourIsByteIdentical() {
|
||||
SampleData s = dcSample(20000);
|
||||
s.play.ampSpline.mode = EnvMode::Spline;
|
||||
VelocityCurve c = VelocityCurve::fromPoints(
|
||||
{{0.0, 0.0}, {20.0, 1.0}, {60.0, 0.3}, {100.0, 0.9}, {127.0, 0.0}},
|
||||
CurveDomain::Unipolar);
|
||||
c.setHard(2, true);
|
||||
s.play.ampSpline.contour = c;
|
||||
const PlayParams same = s.play;
|
||||
|
||||
SampleData base = s;
|
||||
LiveContourBlock blockA, blockB;
|
||||
const std::vector<AudioSample> republished = renderHeld(s, blockA, &same, 20, 60);
|
||||
const std::vector<AudioSample> none = renderHeld(base, blockB, nullptr, -1, 60);
|
||||
CHECK(republished == none);
|
||||
CHECK(peakOf(none, 0, none.size()) > 0.9); // the contour really is being read
|
||||
}
|
||||
|
||||
// The drain slot: a displaced snapshot and the live one both observe the instance's ONE block,
|
||||
// so a note ringing out of either follows the edit.
|
||||
static void testTwoEnginesSharingOneBlockBothFollow() {
|
||||
SampleData a = dcSample(200000);
|
||||
a.play.ampSpline.mode = EnvMode::Spline;
|
||||
a.play.ampSpline.contour = flatAt(1.0, CurveDomain::Unipolar);
|
||||
SampleData b = a;
|
||||
LiveContourBlock block;
|
||||
a.liveContours = █
|
||||
b.liveContours = █
|
||||
block.publish(foldContours(a.play));
|
||||
|
||||
VoiceEngine live(1, a), drain(1, b);
|
||||
live.noteOn(kRoot, 127);
|
||||
drain.noteOn(kRoot, 127);
|
||||
std::vector<AudioSample> outLive, outDrain;
|
||||
for (int i = 0; i < 2; ++i) { live.render(outLive, kBlock); drain.render(outDrain, kBlock); }
|
||||
|
||||
PlayParams edited = a.play;
|
||||
edited.ampSpline.contour = flatAt3(0.25, CurveDomain::Unipolar);
|
||||
block.publish(foldContours(edited));
|
||||
for (int i = 0; i < 8; ++i) { live.render(outLive, kBlock); drain.render(outDrain, kBlock); }
|
||||
CHECK(std::fabs(outLive.back() - 0.25) < 1e-6);
|
||||
CHECK(std::fabs(outDrain.back() - 0.25) < 1e-6);
|
||||
}
|
||||
|
||||
// --- Allocation ----------------------------------------------------------------------------
|
||||
|
||||
// The counter itself must be live, or the zero below would prove nothing.
|
||||
static void testTheAllocationCounterSeesAnAllocation() {
|
||||
g_allocations.store(0);
|
||||
g_countAllocations.store(true);
|
||||
std::vector<float>* v = new std::vector<float>(16);
|
||||
g_countAllocations.store(false);
|
||||
delete v;
|
||||
CHECK(g_allocations.load() >= 2); // the vector object and its buffer
|
||||
}
|
||||
|
||||
// Every audio-side step of a contour swap — the block-boundary read, the rebind of every
|
||||
// sounding voice on all three EGs, a note struck in the swap block, and the renders around them
|
||||
// — runs with the counter armed. Only the UI thread's publish is outside it (it allocates
|
||||
// nothing either, but it is not the audio side).
|
||||
static void testTheAudioSideOfAContourSwapAllocatesNothing() {
|
||||
SampleData s = dcSample(200000);
|
||||
s.play.ampSpline.mode = EnvMode::Spline;
|
||||
s.play.ampSpline.contour = flatAt(1.0, CurveDomain::Unipolar);
|
||||
s.play.pitchEnv.enabled = true;
|
||||
s.play.pitchEnv.peakSemitones = 0.0; // bound, so rebound, but transposing nothing
|
||||
s.play.pitchSpline.mode = EnvMode::Spline;
|
||||
s.play.pitchSpline.contour = flatAt(0.0, CurveDomain::Bipolar);
|
||||
s.play.filter.enabled = true;
|
||||
s.play.filter.settings.cutoffNorm = 1.0f;
|
||||
s.play.filter.modAmount = 0.0;
|
||||
s.play.filterSpline.mode = EnvMode::Spline;
|
||||
s.play.filterSpline.contour = flatAt(0.0, CurveDomain::Unipolar);
|
||||
PlayParams edited = s.play;
|
||||
edited.ampSpline.contour = flatAt3(0.25, CurveDomain::Unipolar);
|
||||
edited.pitchSpline.contour = flatAt3(0.0, CurveDomain::Bipolar);
|
||||
edited.filterSpline.contour = flatAt3(0.5, CurveDomain::Unipolar);
|
||||
|
||||
LiveContourBlock block;
|
||||
s.liveContours = █
|
||||
block.publish(foldContours(s.play));
|
||||
VoiceEngine engine(4, s);
|
||||
std::vector<AudioSample> buf(kBlock, 0.0f);
|
||||
const auto renderBlock = [&] {
|
||||
std::fill(buf.begin(), buf.end(), 0.0f);
|
||||
engine.render(buf.data(), kBlock);
|
||||
};
|
||||
|
||||
g_allocations.store(0);
|
||||
g_countAllocations.store(true);
|
||||
engine.noteOn(kRoot, 127);
|
||||
for (int b = 0; b < 4; ++b) renderBlock();
|
||||
g_countAllocations.store(false);
|
||||
block.publish(foldContours(edited)); // UI thread
|
||||
g_countAllocations.store(true);
|
||||
engine.noteOn(kRoot, 127); // startVoice's rebind + snap
|
||||
renderBlock(); // the block boundary's read finds nothing newer: already taken
|
||||
g_countAllocations.store(false);
|
||||
const double afterEdit = buf[kBlock - 1];
|
||||
block.publish(foldContours(s.play)); // and back, so the render path does the read itself
|
||||
g_countAllocations.store(true);
|
||||
for (int b = 0; b < 8; ++b) renderBlock();
|
||||
g_countAllocations.store(false);
|
||||
|
||||
CHECK(g_allocations.load() == 0);
|
||||
// Not vacuous: both swaps really landed inside the counted window, through a wide-open
|
||||
// low-pass whose DC gain is unity. Two voices on the edited 0.25 contour by the end of the
|
||||
// swap block (2.0 had it not landed), then both back on 1.0 through the render path's read.
|
||||
CHECK(std::fabs(afterEdit - 0.5) < 1e-3);
|
||||
CHECK(std::fabs(buf[kBlock - 1] - 2.0) < 1e-3);
|
||||
}
|
||||
|
||||
int main() {
|
||||
testTheFoldCarriesEveryPointAndItsHardFlag();
|
||||
testAHeldNoteFollowsAnAmpContourEditWithoutAStep();
|
||||
testANoteOnInTheSwapBlockRebindsTheSoundingVoiceAndSnapsTheNewOne();
|
||||
testANoteStruckAfterAnEditPlaysItFromItsFirstFrame();
|
||||
testAHeldNoteFollowsAPitchContourEdit();
|
||||
testAHeldNoteFollowsAFilterContourEdit();
|
||||
testAStagedEnvelopeIgnoresContourPublishes();
|
||||
testRepublishingAnUnchangedContourIsByteIdentical();
|
||||
testTwoEnginesSharingOneBlockBothFollow();
|
||||
testTheAllocationCounterSeesAnAllocation();
|
||||
testTheAudioSideOfAContourSwapAllocatesNothing();
|
||||
if (g_fail == 0) std::printf("live_contours tests passed\n");
|
||||
return g_fail == 0 ? 0 : 1;
|
||||
}
|
||||
Reference in New Issue
Block a user