// Standalone tests for LIVE PARAMETER DELIVERY into a sounding voice — no VST3, no REAPER, no
// framework. The block's own publication contract is live_params_tests; this file asserts what
// reaches the audio: the mid-stage rule holds normalized position, a level move glides, a
// fresh note takes the newest block outright, every stage time and stage level on all three
// envelopes moves the note already sounding, a filter knob does too, two snapshots sharing one
// block behave identically (the drain slot), what stays latched at note-on stays latched, and
// an unmoved block renders byte-identically to the engine with no block at all.

#include "../src/core/instrument/engine/voice_engine.h"

#include <algorithm>
#include <cmath>
#include <cstdio>
#include <vector>

using namespace reasampler;
using instrument::engine::LiveParams;
using instrument::engine::LiveValues;
using instrument::engine::foldLive;
namespace flt = reasampler::instrument::engine::filter;

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;

static SampleData periodicSine(std::size_t frames, double period) {
    SampleData s;
    s.frames.resize(frames);
    for (std::size_t i = 0; i < frames; ++i) {
        s.frames[i] = static_cast<float>(std::sin(2.0 * kPi * static_cast<double>(i) / period));
    }
    s.sampleRate = kRate;
    s.rootNote = 60;
    // Amp held wide open so a rendered frame is the (filtered) source, undisturbed by the
    // envelope under test elsewhere in this file.
    s.play.adsr.sustainLevel = 1.0;
    return s;
}

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) {
        const double d = std::fabs(static_cast<double>(v[i]) - static_cast<double>(v[i - 1]));
        if (d > worst) worst = d;
    }
    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;
}

static SampleData filteredSine() {
    SampleData s = periodicSine(200000, 64.0);
    s.play.filter.enabled = true;
    s.play.filter.settings.cutoffNorm = 0.8f;
    s.play.filter.settings.resonanceNorm = 0.9f;
    s.play.filter.settings.morphNorm = 1.0f;
    // A drawn velocity curve, so the velocity-DEPTH knob has something to scale. It costs
    // every other case nothing: the depth is 0 until a case moves it, so the product is 0.
    s.play.filter.velocityCurve = VelocityCurve::fromPoints(
        {{0.0, 0.0}, {127.0, -1.0}}, instrument::engine::CurveDomain::Bipolar);
    return s;
}

// A low corner with real envelope depth, so the filter ENVELOPE's shape is what the timbre
// depends on rather than the static knob position.
static void filterSweep(SampleData& s) {
    s.play.filter.enabled = true;
    s.play.filter.settings.cutoffNorm = 0.15f;
    s.play.filter.settings.resonanceNorm = 0.6f;
    s.play.filter.settings.morphNorm = 1.0f;
    s.play.filter.modAmount = 0.8;
}

// Renders `blocks` blocks of `blockFrames` through a one-voice engine over `sample`,
// republishing `changed` at the top of block `changeAfter` and gating the note off at the top
// of `noteOffBlock` (-1 holds it). Voice-major render order is the engine's, so a fixed block
// size is what makes two runs comparable.
struct Run {
    std::vector<AudioSample> out;
};

// The note is an octave above the root on purpose: key-tracking scales (note - root), so a
// root-note test would leave the key-track control with nothing to move.
constexpr int kTestNote = 72;

static Run renderWithLive(SampleData& sample, LiveParams* block, int blockFrames, int blocks,
                          int changeAfter, const LiveValues* changed, int noteOffBlock = -1,
                          int velocity = 100) {
    sample.live = block;
    if (block) block->publish(foldLive(sample.play, sample.keyTrack));
    VoiceEngine engine(1, sample);
    engine.noteOn(kTestNote, velocity);
    Run r;
    for (int b = 0; b < blocks; ++b) {
        if (block && changed && b == changeAfter) block->publish(*changed);
        if (b == noteOffBlock) engine.noteOff(kTestNote);
        engine.render(r.out, static_cast<std::size_t>(blockFrames));
    }
    return r;
}

// --- The mid-stage rule (candidate iv): hold normalized stage position ------------------

static void testStageDurationChangeHoldsPhase() {
    AdsrParams p;
    p.attackFrames = 1000;
    p.sustainLevel = 1.0;

    AdsrEnvelope unedited, edited;
    unedited.configure(p);
    edited.configure(p);
    unedited.noteOn();
    edited.noteOn();
    for (int i = 0; i < 500; ++i) { unedited.tick(); edited.tick(); }

    AdsrParams longer = p;
    longer.attackFrames = 2000;  // doubled while the voice sits halfway up the attack
    edited.applyLive(longer);

    // Continuity: the very next frame is UNCHANGED by the edit. Exact, not approximate —
    // phi is held, and the level is a pure function of phi.
    const double a = unedited.tick();
    const double b = edited.tick();
    CHECK(a == b);
    CHECK(std::fabs(b - 0.5) < 1e-12);  // and it is genuinely mid-attack, not a degenerate 0/1

    // The remainder takes its share of the NEW duration: half of 2000 frames left to run.
    for (int i = 0; i < 998; ++i) edited.tick();
    CHECK(edited.stage() == AdsrEnvelope::Stage::Attack);
    edited.tick();
    CHECK(edited.stage() != AdsrEnvelope::Stage::Attack);
}

static void testShortenedStageStillLandsContinuously() {
    AdsrParams p;
    p.attackFrames = 1000;
    p.sustainLevel = 1.0;
    AdsrEnvelope env;
    env.configure(p);
    env.noteOn();
    double last = 0.0;
    for (int i = 0; i < 800; ++i) last = env.tick();

    AdsrParams shorter = p;
    shorter.attackFrames = 100;  // now SHORTER than the frames already elapsed
    env.applyLive(shorter);
    const double next = env.tick();
    // Recomputing from absolute elapsed (800/100) would clamp to 1.0 — a step from ~0.8. The
    // phi rule keeps the level where it was and finishes the remaining 20% over 20 frames.
    CHECK(std::fabs(next - last) < 2e-3);
    for (int i = 0; i < 19; ++i) env.tick();
    CHECK(env.stage() != AdsrEnvelope::Stage::Attack);
}

static void testSustainLevelChangeGlides() {
    AdsrParams p;
    p.sustainLevel = 1.0;
    p.releaseFrames = 100000;
    AdsrEnvelope env;
    env.configure(p);
    env.noteOn();
    for (int i = 0; i < 50; ++i) env.tick();
    CHECK(env.stage() == AdsrEnvelope::Stage::Sustain);

    AdsrParams quieter = p;
    quieter.sustainLevel = 0.2;
    env.applyLive(quieter);

    double prev = 1.0;
    double worstStep = 0.0;
    double v = 0.0;
    for (int i = 0; i < 600; ++i) {
        v = env.tick();
        // The first frame reproduces the pre-change level. Bounded rather than compared
        // exactly: 0.2 + fl(1.0 - 0.2) does round to exactly 1.0 for THESE operands, but the
        // property under test is continuity, not a bit-exactness the smoother never promised.
        if (i == 0) CHECK(std::fabs(v - 1.0) < 1e-15);
        const double step = std::fabs(v - prev);
        if (step > worstStep) worstStep = step;
        prev = v;
    }
    // A raw parameter swap would step 0.8 in one frame; the glide's largest single step is a
    // small fraction of that, and it terminates exactly on the new level.
    CHECK(worstStep < 0.05);
    CHECK(v == 0.2);
}

static void testPitchEnvelopeHoldsPhaseAndGlidesDepth() {
    // No hold stage, so the shape is the attack-decay one the pre-AHD envelope had.
    PitchEnvParams p;
    p.enabled = true;
    p.peakSemitones = 12.0;
    p.shape.attackFrames = 0;
    p.shape.decayFrames = 1000;
    p.shape.holdFraction = 0.0;
    PitchEnvelope a, b;
    a.configure(100000, p);
    b.configure(100000, p);
    a.noteOn();
    b.noteOn();
    for (int i = 0; i < 400; ++i) { a.tick(); b.tick(); }

    PitchEnvParams longer = p;
    longer.shape.decayFrames = 2000;
    b.applyLive(100000, longer);  // decay doubled mid-decay, same span
    CHECK(a.tick() == b.tick());  // phi held: the semitone offset is unchanged this frame

    // A depth move is a level step, so it glides rather than jumping: the first frame after
    // the edit is exactly what the unedited peer emits.
    PitchEnvelope c, d;
    c.configure(100000, p);
    d.configure(100000, p);
    c.noteOn();
    d.noteOn();
    for (int i = 0; i < 400; ++i) { c.tick(); d.tick(); }
    PitchEnvParams noDepth = p;
    noDepth.peakSemitones = 0.0;
    c.applyLive(100000, noDepth);  // depth to zero mid-decay
    CHECK(c.tick() == d.tick());
    // ...and it does eventually reach the new depth rather than staying put.
    for (int i = 0; i < 400; ++i) c.tick();
    CHECK(c.tick() == 0.0);
}

// The pitch envelope's new middle stage, on the same phi rule: a hold dialled mid-hold keeps
// the level (flat by definition) and moves the boundary, and the fraction is taken against
// what attack and decay left rather than against the whole span.
static void testPitchEnvelopeHoldStagePlaysAndHoldsPhase() {
    PitchEnvParams p;
    p.enabled = true;
    p.peakSemitones = 12.0;
    p.shape.attackFrames = 100;
    p.shape.decayFrames = 100;
    p.shape.holdFraction = 0.5;  // half of (1000 - 200) = 400 frames of hold
    PitchEnvelope e;
    e.configure(1000, p);
    e.noteOn();
    for (int i = 0; i < 100; ++i) e.tick();      // through the attack
    CHECK(e.tick() == 12.0);                     // frame 100: at the peak, holding
    for (int i = 0; i < 398; ++i) e.tick();      // to the last frame of the hold
    CHECK(e.tick() == 12.0);                     // frame 499: still holding
    CHECK(e.tick() == 12.0);                     // frame 500: decay's own first frame
    CHECK(std::fabs(e.tick() - 12.0 * (1.0 - 1.0 / 100.0)) < 1e-12);  // frame 501: descending

    // A live hold change mid-hold is continuous (the stage is flat) and the envelope still
    // finishes inside the span.
    PitchEnvelope f;
    f.configure(1000, p);
    f.noteOn();
    for (int i = 0; i < 300; ++i) f.tick();
    PitchEnvParams wider = p;
    wider.shape.holdFraction = 1.0;
    f.applyLive(1000, wider);
    CHECK(f.tick() == 12.0);
    for (int i = 0; i < 1200; ++i) f.tick();
    CHECK(f.tick() == 0.0);
}

// --- The fresh-note path: snap, never the phi rule ---------------------------------------

static void testAFreshEnvelopeTakesANewlyDialledStageTimeOutright() {
    // Regression, both directions. The snap path once ran applyLive's phi rule, which reads a
    // stale duration of 0 as "this stage is already complete" and threw the newly-dialled
    // attack away for every note until the next reload.
    AdsrParams stale;  // the AdsrParams default: every stage zero
    stale.sustainLevel = 1.0;
    AdsrEnvelope env;
    env.configure(stale);
    env.noteOn();
    AdsrParams dialled = stale;
    dialled.attackFrames = 100;
    env.snapLive(dialled);
    CHECK(env.tick() == 0.0);  // frame 0 of a 100-frame attack, not an instant 1.0
    for (int i = 0; i < 49; ++i) env.tick();
    CHECK(std::fabs(env.tick() - 0.5) < 1e-12);

    // Reverse: a stale non-zero attack against a newly-dialled ZERO one must not absorb a
    // full-scale step into a voice that has emitted nothing — that fades in a note the user
    // asked to be instant.
    AdsrParams staleLong;
    staleLong.attackFrames = 1000;
    staleLong.sustainLevel = 1.0;
    AdsrEnvelope instant;
    instant.configure(staleLong);
    instant.noteOn();
    AdsrParams zeroAttack = staleLong;
    zeroAttack.attackFrames = 0;
    instant.snapLive(zeroAttack);
    CHECK(instant.tick() == 1.0);
}

static void testAFreshPitchEnvelopeTakesTheNewTimesOutright() {
    PitchEnvParams stale;  // enabled, but every leg zero
    stale.enabled = true;
    PitchEnvelope env;
    env.configure(100000, stale);
    env.noteOn();
    PitchEnvParams dialled = stale;
    dialled.peakSemitones = 12.0;
    dialled.shape.decayFrames = 1000;
    env.snapLive(100000, dialled);
    CHECK(env.tick() == 12.0);  // at the top of the new decay leg, not past the envelope
    for (int i = 0; i < 499; ++i) env.tick();
    CHECK(std::fabs(env.tick() - 6.0) < 1e-12);
}

static void testANoteStartedAfterAPublishSoundsThePublishedEnvelope() {
    // End-to-end shape of the snap path: a live commit deliberately leaves the snapshot's own
    // sample.play stale, so the ONLY thing standing between a new note and a stale envelope is
    // the snap. This is the coverage whose absence let the phi-on-snap bug through.
    SampleData s = periodicSine(200000, 64.0);  // adsr default: attack 0, sustain 1.0
    LiveParams block;
    s.live = &block;
    LiveValues dialled = foldLive(s.play, s.keyTrack);
    dialled.adsr.attackFrames = 24000;  // half a second of attack, dialled before the note
    block.publish(dialled);
    VoiceEngine engine(1, s);
    engine.noteOn(kTestNote, 100);
    std::vector<AudioSample> out;
    engine.render(out, 512);

    // Control: the same stale snapshot with no block at all speaks at full level immediately.
    SampleData bare = periodicSine(200000, 64.0);
    VoiceEngine bareEngine(1, bare);
    bareEngine.noteOn(kTestNote, 100);
    std::vector<AudioSample> bareOut;
    bareEngine.render(bareOut, 512);
    const double barePeak = peakOf(bareOut, 0, bareOut.size());
    const double peak = peakOf(out, 0, out.size());
    CHECK(barePeak > 0.9);
    CHECK(peak < barePeak * 0.1);  // 512 frames into a 24000-frame attack: ~2% of full scale

    // Reverse: a stale LONG attack against a published zero one. The note must speak at full
    // level within its first cycle rather than fading in over the smoother's decay.
    SampleData slow = periodicSine(200000, 64.0);
    slow.play.adsr.attackFrames = 24000;
    LiveParams block2;
    slow.live = &block2;
    LiveValues snappy = foldLive(slow.play, slow.keyTrack);
    snappy.adsr.attackFrames = 0;
    block2.publish(snappy);
    VoiceEngine fast(1, slow);
    fast.noteOn(kTestNote, 100);
    std::vector<AudioSample> fastOut;
    fast.render(fastOut, 512);
    // Source period 64 read at ratio 2 peaks at output frame 8; a spurious smoother fade-in
    // would still be at ~0.34 there.
    CHECK(peakOf(fastOut, 0, 32) > 0.9);
}

// --- Every envelope stage, end to end through the engine ---------------------------------

// Renders the same note twice — once untouched, once with `mutate` published mid-note — and
// asserts the field reached the SOUNDING voice (the tail diverges) and only after its publish.
static void assertLiveFieldMovesTheSoundingNote(const char* name, void (*rig)(SampleData&),
                                                void (*mutate)(LiveValues&), int noteOffBlock) {
    SampleData still = periodicSine(200000, 64.0);
    SampleData moved = periodicSine(200000, 64.0);
    rig(still);
    rig(moved);
    LiveParams blockA, blockB;
    LiveValues target = foldLive(moved.play, moved.keyTrack);
    mutate(target);

    const Run baseline = renderWithLive(still, &blockA, 512, 24, -1, nullptr, noteOffBlock);
    const Run edited = renderWithLive(moved, &blockB, 512, 24, 8, &target, noteOffBlock);

    CHECK(baseline.out.size() == edited.out.size());
    double tailDiff = 0.0;
    for (std::size_t i = 512 * 9; i < baseline.out.size() && i < edited.out.size(); ++i) {
        tailDiff += std::fabs(static_cast<double>(edited.out[i]) -
                              static_cast<double>(baseline.out[i]));
    }
    if (!(tailDiff > 1.0)) std::printf("  (never reached the voice: %s)\n", name);
    CHECK(tailDiff > 1.0);

    bool preChangeIdentical = true;
    for (std::size_t i = 0; i < 512 * 8 && i < baseline.out.size(); ++i) {
        if (edited.out[i] != baseline.out[i]) { preChangeIdentical = false; break; }
    }
    if (!preChangeIdentical) std::printf("  (moved before its publish: %s)\n", name);
    CHECK(preChangeIdentical);
}

static void testEveryEnvelopeStageTimeAndLevelMovesTheSoundingNote() {
    // Each rig puts the voice INSIDE the stage under test at the publish (block 8, output
    // frame 4096) — a stage already passed cannot move, which is the physics, not a gap.
    struct Case {
        const char* name;
        void (*rig)(SampleData&);
        void (*mutate)(LiveValues&);
        int noteOffBlock;
    };
    const Case cases[] = {
        {"amp attack",
         [](SampleData& s) { s.play.adsr.attackFrames = 48000; },
         [](LiveValues& v) { v.adsr.attackFrames = 4000; }, -1},
        {"amp hold",
         [](SampleData& s) {
             s.play.adsr.holdFrames = 48000;
             s.play.adsr.decayFrames = 4000;
             s.play.adsr.sustainLevel = 0.1;
         },
         [](LiveValues& v) { v.adsr.holdFrames = 5000; }, -1},
        {"amp decay",
         [](SampleData& s) {
             s.play.adsr.decayFrames = 48000;
             s.play.adsr.sustainLevel = 0.0;
         },
         [](LiveValues& v) { v.adsr.decayFrames = 8000; }, -1},
        {"amp sustain",
         [](SampleData& s) { s.play.adsr.sustainLevel = 1.0; },
         [](LiveValues& v) { v.adsr.sustainLevel = 0.2; }, -1},
        {"amp release",
         [](SampleData& s) { s.play.adsr.releaseFrames = 48000; },
         [](LiveValues& v) { v.adsr.releaseFrames = 6000; }, 2},

        // The filter envelope: swept over a low corner with real depth, so its shape is the
        // only thing the timbre depends on. The amp release is long so a gated-off voice
        // keeps sounding while the filter release is measured.
        {"filter env attack",
         [](SampleData& s) { filterSweep(s); s.play.filter.env.attackFrames = 48000; },
         [](LiveValues& v) { v.filterEnv.attackFrames = 4000; }, -1},
        {"filter env hold",
         [](SampleData& s) {
             filterSweep(s);
             s.play.filter.env.holdFrames = 48000;
             s.play.filter.env.decayFrames = 4000;
             s.play.filter.env.sustainLevel = 0.0;
         },
         [](LiveValues& v) { v.filterEnv.holdFrames = 5000; }, -1},
        {"filter env decay",
         [](SampleData& s) {
             filterSweep(s);
             s.play.filter.env.decayFrames = 48000;
             s.play.filter.env.sustainLevel = 0.0;
         },
         [](LiveValues& v) { v.filterEnv.decayFrames = 8000; }, -1},
        {"filter env sustain",
         [](SampleData& s) { filterSweep(s); },
         [](LiveValues& v) { v.filterEnv.sustainLevel = 0.0; }, -1},
        {"filter env release",
         [](SampleData& s) {
             filterSweep(s);
             s.play.filter.env.releaseFrames = 48000;
             s.play.adsr.releaseFrames = 480000;
         },
         [](LiveValues& v) { v.filterEnv.releaseFrames = 6000; }, 2},

        {"pitch env attack",
         [](SampleData& s) {
             s.play.pitchEnv.enabled = true;
             s.play.pitchEnv.shape.attackFrames = 48000;
             s.play.pitchEnv.shape.decayFrames = 48000;
             s.play.pitchEnv.peakSemitones = 12.0;
         },
         [](LiveValues& v) { v.pitchEnv.shape.attackFrames = 4000; }, -1},
        {"pitch env decay",
         [](SampleData& s) {
             s.play.pitchEnv.enabled = true;
             s.play.pitchEnv.shape.decayFrames = 48000;
             s.play.pitchEnv.peakSemitones = 12.0;
         },
         [](LiveValues& v) { v.pitchEnv.shape.decayFrames = 8000; }, -1},
        {"pitch env depth",
         [](SampleData& s) {
             s.play.pitchEnv.enabled = true;
             s.play.pitchEnv.shape.decayFrames = 480000;
             s.play.pitchEnv.peakSemitones = 12.0;
         },
         [](LiveValues& v) { v.pitchEnv.peakSemitones = 0.0; }, -1},
        // A small hold that finishes the envelope well inside the render window (Hold ends at
        // frame ~5090, comfortably short of the window) vs. a live move that opens the hold out
        // near the whole span: with the fraction alone unmoved, the boundary the two renders
        // cross (or don't) inside the observed tail is what makes this audible, not a level
        // change — Hold's own output is flat regardless of exactly where inside it pos_ sits.
        {"pitch env hold fraction",
         [](SampleData& s) {
             s.play.pitchEnv.enabled = true;
             s.play.pitchEnv.peakSemitones = 12.0;
             s.play.pitchEnv.shape.attackFrames = 100;
             s.play.pitchEnv.shape.decayFrames = 100;
             s.play.pitchEnv.shape.holdFraction = 0.05;
         },
         [](LiveValues& v) { v.pitchEnv.shape.holdFraction = 1.0; }, -1},

        // The Trigger AHDs — Voice::applyLive's ampAhd_/filterAhd_ branches, otherwise
        // unexercised by this table (every case above is Gate/AdsrEnvelope).
        {"trigger amp attack (AHD)",
         [](SampleData& s) {
             s.play.playMode = PlayMode::Trigger;
             s.play.trigger.lengthFraction = 1.0;
             s.play.trigAhd.attackFrames = 48000;
         },
         [](LiveValues& v) { v.ampAhd.attackFrames = 4000; }, -1},
        {"trigger filter attack (AHD)",
         [](SampleData& s) {
             s.play.playMode = PlayMode::Trigger;
             s.play.trigger.lengthFraction = 1.0;
             filterSweep(s);
             s.play.filter.trigEnv.attackFrames = 48000;
         },
         [](LiveValues& v) { v.filterAhd.attackFrames = 4000; }, -1},

        // One curve exponent per envelope (amp/pitch/filter), reusing each envelope's own
        // attack/decay rig above so only the mutated field differs.
        {"amp attack curve",
         [](SampleData& s) { s.play.adsr.attackFrames = 48000; },
         [](LiveValues& v) { v.adsr.attackCurve = 5.0; }, -1},
        {"amp release curve",
         [](SampleData& s) { s.play.adsr.releaseFrames = 48000; },
         [](LiveValues& v) { v.adsr.releaseCurve = 5.0; }, 2},
        {"pitch env attack curve",
         [](SampleData& s) {
             s.play.pitchEnv.enabled = true;
             s.play.pitchEnv.shape.attackFrames = 48000;
             s.play.pitchEnv.shape.decayFrames = 48000;
             s.play.pitchEnv.peakSemitones = 12.0;
         },
         [](LiveValues& v) { v.pitchEnv.shape.attackCurve = 5.0; }, -1},
        {"filter env decay curve",
         [](SampleData& s) {
             filterSweep(s);
             s.play.filter.env.decayFrames = 48000;
             s.play.filter.env.sustainLevel = 0.0;
         },
         [](LiveValues& v) { v.filterEnv.decayCurve = 5.0; }, -1},
    };
    for (const Case& c : cases) {
        assertLiveFieldMovesTheSoundingNote(c.name, c.rig, c.mutate, c.noteOffBlock);
    }
}

// --- The filter DSP's glide property, finally exercised ---------------------------------

static void testCutoffMoveAcrossPrepareDoesNotStep() {
    flt::FilterSettings s;
    s.cutoffNorm = 0.8f;
    s.resonanceNorm = 1.0f;  // maximum Q: the worst case for a coefficient step
    s.morphNorm = 1.0f;

    flt::VoiceFilter glide, cut;
    glide.prepare(s, kRate);
    cut.prepare(s, kRate);

    std::vector<AudioSample> a, b;
    const int boundary = 2000;
    for (int i = 0; i < 4000; ++i) {
        if (i == boundary) {
            flt::FilterSettings moved = s;
            moved.cutoffNorm = 0.3f;
            glide.prepare(moved, kRate);   // state PRESERVED — the documented glide property
            cut.prepare(moved, kRate);
            cut.reset();                   // the control: state cleared, as at note-on
        }
        const float x = static_cast<float>(std::sin(2.0 * kPi * static_cast<double>(i) / 48.0));
        a.push_back(glide.process(0, x));
        b.push_back(cut.process(0, x));
    }

    const double localMax = maxAbsDelta(a, boundary - 400, boundary - 1);
    const double glideStep = std::fabs(static_cast<double>(a[boundary]) -
                                       static_cast<double>(a[boundary - 1]));
    const double cutStep = std::fabs(static_cast<double>(b[boundary]) -
                                     static_cast<double>(b[boundary - 1]));
    // Preserving state keeps the boundary frame inside the signal's own frame-to-frame range;
    // clearing it does not — which is what proves this assertion discriminates rather than
    // passing on any pair of numbers.
    CHECK(glideStep <= localMax);
    CHECK(cutStep > glideStep * 4.0);
}

// --- Delivery into a sounding voice ------------------------------------------------------

static void testUnmovedBlockIsByteIdenticalToNoBlockAtAll() {
    SampleData bare = filteredSine();
    SampleData blocked = filteredSine();
    LiveParams block;
    const Run without = renderWithLive(bare, nullptr, 512, 20, -1, nullptr);
    const Run with = renderWithLive(blocked, &block, 512, 20, -1, nullptr);
    CHECK(without.out.size() == with.out.size());
    bool identical = true;
    for (std::size_t i = 0; i < without.out.size() && i < with.out.size(); ++i) {
        if (without.out[i] != with.out[i]) { identical = false; break; }
    }
    // Also the migration bar: a blob saved before this change folds to exactly the values the
    // build already resolved, so reopening it sounds identical rather than merely close.
    CHECK(identical);
}

static void testEveryLiveFilterControlMovesTheSoundingNote() {
    struct Case { const char* name; void (*mutate)(LiveValues&); };
    const Case cases[] = {
        {"cutoff", [](LiveValues& v) { v.filterSettings.cutoffNorm = 0.15f; }},
        {"Q",      [](LiveValues& v) { v.filterSettings.resonanceNorm = 0.1f; }},
        {"morph",  [](LiveValues& v) { v.filterSettings.morphNorm = 0.0f; }},
        {"drive",  [](LiveValues& v) { v.filterSettings.driveNorm = 1.0f; }},
        {"mod",    [](LiveValues& v) { v.filterModAmount = 1.0; }},
        // The velocity DEPTH is live even though the velocity itself is latched: the depth is
        // a control over the note's latched curve value, the same shape key-track has over the
        // note's latched number (deck_groups.h).
        {"velamount", [](LiveValues& v) { v.filterVelAmount = 1.0; }},
        {"keytrack", [](LiveValues& v) { v.filterKeyTrack = 2.0; }},
    };

    for (const Case& c : cases) {
        SampleData still = filteredSine();
        SampleData moved = filteredSine();
        LiveParams blockA, blockB;
        LiveValues target = foldLive(moved.play, moved.keyTrack);
        c.mutate(target);

        const Run baseline = renderWithLive(still, &blockA, 512, 24, -1, nullptr);
        const Run swept = renderWithLive(moved, &blockB, 512, 24, 8, &target);

        // It moved THIS note: the tail after the publish differs audibly from the untouched
        // render of the same note.
        double tailDiff = 0.0;
        for (std::size_t i = 512 * 12; i < baseline.out.size(); ++i) {
            tailDiff += std::fabs(static_cast<double>(swept.out[i]) -
                                  static_cast<double>(baseline.out[i]));
        }
        if (!(tailDiff > 1.0)) std::printf("  (control: %s)\n", c.name);
        CHECK(tailDiff > 1.0);

        // Nothing before the publish moved (the block is observed at block boundaries only).
        bool preChangeIdentical = true;
        for (std::size_t i = 0; i < 512 * 8; ++i) {
            if (swept.out[i] != baseline.out[i]) { preChangeIdentical = false; break; }
        }
        CHECK(preChangeIdentical);

        // And it ARRIVED as a glide, not as a step. Measured as how far the swept render has
        // departed from the untouched one in the first frames after the publish, against how
        // far it departs once settled: a glide has barely begun to diverge, a snapped delivery
        // is already all the way there.
        //
        // This is the assertion that discriminates. A single-frame-spike metric does NOT: the
        // TPT filter preserves state across prepare(), so even an instantaneous coefficient
        // jump produces no isolated output spike — measured, by defeating the ramp and
        // re-running, the spike statistic was unchanged while these two numbers converged.
        //
        // The window is a FRACTION OF THE GLIDE, not a frame count: kLiveRampSeconds is the
        // full travel time, so at 1/240 of it a working glide has barely started when the
        // window closes. kGlideMargin then puts the bound at the geometric middle of the two
        // MEASURED populations — with the ramp in place these seven controls ratio 0.0005..0.060;
        // with it defeated (every live move delivered as a snap, run) they ratio 0.52..1.10.
        // The bound lands at 0.175: ~3x above the worst glide, ~3x below the tamest snap.
        const std::size_t rampFrames =
            static_cast<std::size_t>(instrument::engine::kLiveRampSeconds * kRate);
        const std::size_t window = rampFrames / 240;
        const double kGlideMargin = 42.0;
        const double bound = kGlideMargin * static_cast<double>(window) /
                             static_cast<double>(rampFrames);
        double immediate = 0.0;
        for (std::size_t i = 512 * 8; i < 512 * 8 + window; ++i) {
            immediate = (std::max)(immediate, std::fabs(static_cast<double>(swept.out[i]) -
                                                        static_cast<double>(baseline.out[i])));
        }
        double settled = 0.0;
        for (std::size_t i = 512 * 14; i < baseline.out.size(); ++i) {
            settled = (std::max)(settled, std::fabs(static_cast<double>(swept.out[i]) -
                                                     static_cast<double>(baseline.out[i])));
        }
        if (!(immediate <= settled * bound))
            std::printf("  (glide: %s ratio %f vs bound %f)\n", c.name,
                        settled > 0.0 ? immediate / settled : -1.0, bound);
        CHECK(immediate <= settled * bound);
    }
}

static void testOneBlockServesTwoIndependentObservers() {
    // Two snapshots, one block — exactly the processor's live_/draining_ shape. The claim is
    // narrow and specific: read() does NOT consume the generation, so the second engine to
    // observe a publish sees it as fully as the first. Two identically-built engines are
    // otherwise identical by construction, so that is the only thing the comparison pins.
    SampleData liveSnapshot = filteredSine();
    SampleData drainSnapshot = filteredSine();
    LiveParams block;
    liveSnapshot.live = &block;
    drainSnapshot.live = &block;
    block.publish(foldLive(liveSnapshot.play, liveSnapshot.keyTrack));

    VoiceEngine liveEngine(1, liveSnapshot);
    VoiceEngine drainEngine(1, drainSnapshot);
    liveEngine.noteOn(60, 100);
    drainEngine.noteOn(60, 100);

    std::vector<AudioSample> a, b;
    LiveValues moved = foldLive(liveSnapshot.play, liveSnapshot.keyTrack);
    moved.filterSettings.cutoffNorm = 0.2f;
    for (int blk = 0; blk < 24; ++blk) {
        if (blk == 8) block.publish(moved);
        liveEngine.render(a, 512);
        drainEngine.render(b, 512);
    }
    CHECK(a.size() == b.size());
    bool same = true;
    for (std::size_t i = 0; i < a.size() && i < b.size(); ++i) {
        if (a[i] != b[i]) { same = false; break; }
    }
    CHECK(same);
    // The shared block genuinely moved the sound, so "identical" is a claim about both
    // observers having seen it rather than about nothing having happened.
    double moveEnergy = 0.0;
    for (std::size_t i = 512 * 12; i < a.size(); ++i) moveEnergy += std::fabs(a[i]);
    CHECK(moveEnergy > 1.0);
    // And a THIRD observer, after both engines have read it, still sees the same publish.
    LiveValues seen;
    CHECK(block.read(seen) != 0);
    CHECK(seen.filterSettings.cutoffNorm == 0.2f);
}

// --- The third commit class: published live, read only at note-on -------------------------

// A ramp source read under Varispeed, so every output frame IS the read position — a moved read
// increment shows up directly rather than as a timbre change. The claim has two halves and both
// are asserted: the sounding note is byte-identical to one that never saw the publish, AND the
// next note-on takes the new rate. Asserting only the first would pass on a rate that never
// arrived at all.
static SampleData rampForReadRate() {
    SampleData s;
    s.frames.resize(200000);
    for (std::size_t i = 0; i < s.frames.size(); ++i) {
        s.frames[i] = static_cast<float>(static_cast<double>(i) / 200000.0);
    }
    s.sampleRate = kRate;
    s.rootNote = 60;
    s.play.adsr.sustainLevel = 1.0;
    return s;
}

// A one-voice engine with its Preserve shifters actually SIZED, unlike renderWithLive's — the
// shared harness leaves them unconfigured, which silently routes a Preserve voice down the
// varispeed read and would make "in both engines" mean one engine twice.
static std::vector<AudioSample> renderPreserveCapable(SampleData& s, LiveParams& block,
                                                      const LiveValues* changed, int changeAfter,
                                                      int note) {
    s.live = &block;
    block.publish(foldLive(s.play, s.keyTrack));
    VoiceEngine engine(1, s, /*preserveVoiceCap=*/0, /*preserveWindowFrames=*/2048);
    engine.noteOn(note, 100);
    std::vector<AudioSample> out;
    for (int b = 0; b < 24; ++b) {
        if (changed && b == changeAfter) block.publish(*changed);
        engine.render(out, 512);
    }
    return out;
}

static void testARateChangeSpareTheSoundingNoteAndReachesTheNextOne() {
    for (PitchEngine eng : {PitchEngine::Varispeed, PitchEngine::Preserve}) {
        SampleData still = rampForReadRate();
        SampleData moved = rampForReadRate();
        still.play.pitchEngine = eng;
        moved.play.pitchEngine = eng;

        LiveParams blockA, blockB;
        LiveValues halfRate = foldLive(moved.play, moved.keyTrack);
        halfRate.playRate = 0.5;

        // At the ROOT note, so Preserve's shifter runs at shift 1.0 and never splices — the
        // output is then the source at the read head under both engines, which is what makes
        // the ramp readable as a read rate at all.
        const std::vector<AudioSample> baseline =
            renderPreserveCapable(still, blockA, nullptr, -1, 60);
        const std::vector<AudioSample> swept =
            renderPreserveCapable(moved, blockB, &halfRate, 8, 60);

        // BYTE-identical, not merely close: the sounding voice never reads the field.
        CHECK(baseline.size() == swept.size());
        bool untouched = true;
        for (std::size_t i = 0; i < baseline.size() && i < swept.size(); ++i) {
            if (baseline[i] != swept[i]) { untouched = false; break; }
        }
        CHECK(untouched);

        // The next note-on takes it — measured as the note's LIFETIME, which is what Rate
        // controls in both engines. (The ramp's instantaneous value is a read-position probe
        // under Varispeed only: under Preserve the shifter's tap sits behind the feed and
        // relocates at every splice, so the value at a given output frame is not the source
        // there.) The rate is carried ONLY by the published block — sample.play keeps unity —
        // so a lifetime that doubles can only have come from the block.
        auto blocksAlive = [&](double rate) {
            SampleData fresh = rampForReadRate();
            fresh.play.pitchEngine = eng;
            LiveParams block;
            fresh.live = &block;
            LiveValues published = foldLive(fresh.play, fresh.keyTrack);
            published.playRate = rate;
            block.publish(published);
            VoiceEngine engine(1, fresh, /*preserveVoiceCap=*/0, /*preserveWindowFrames=*/2048);
            engine.noteOn(60, 100);
            std::vector<AudioSample> out;
            int blocks = 0;
            while (engine.activeVoiceCount() > 0 && blocks < 4000) {
                engine.render(out, 512);
                ++blocks;
            }
            return blocks;
        };
        const int atUnity = blocksAlive(1.0);
        const int atHalf = blocksAlive(0.5);
        CHECK(atUnity > 100 && atUnity < 4000);  // the note really did run to its own end
        CHECK(std::fabs(static_cast<double>(atHalf) - 2.0 * atUnity) < 0.05 * atUnity);
    }
}

// Pitch is the other side of the same coin: it DOES move the note already sounding, under both
// engines — one more factor of the read increment under Varispeed, an addend to the shift under
// Preserve. Measured as a tail that departs from the untouched render while the frames before
// the publish stay byte-identical.
static void testAPitchOffsetChangeMovesTheSoundingNoteInBothEngines() {
    for (PitchEngine eng : {PitchEngine::Varispeed, PitchEngine::Preserve}) {
        SampleData still = periodicSine(200000, 64.0);
        SampleData moved = periodicSine(200000, 64.0);
        still.play.pitchEngine = eng;
        moved.play.pitchEngine = eng;

        LiveParams blockA, blockB;
        LiveValues target = foldLive(moved.play, moved.keyTrack);
        target.pitchOffsetSemitones = -12.0;

        const std::vector<AudioSample> baseline =
            renderPreserveCapable(still, blockA, nullptr, -1, kTestNote);
        const std::vector<AudioSample> swept =
            renderPreserveCapable(moved, blockB, &target, 8, kTestNote);

        double tailDiff = 0.0;
        for (std::size_t i = 512 * 12; i < baseline.size(); ++i) {
            tailDiff += std::fabs(static_cast<double>(swept[i]) -
                                  static_cast<double>(baseline[i]));
        }
        CHECK(tailDiff > 1.0);

        bool preChangeIdentical = true;
        for (std::size_t i = 0; i < 512 * 8; ++i) {
            if (swept[i] != baseline[i]) { preChangeIdentical = false; break; }
        }
        CHECK(preChangeIdentical);
    }
}

// --- The live Pitch offset reaches the note's TIME domains, not only its pitch -------------

// A block published BEFORE the note starts is the snapLive path, and the snapshot's own copy of
// the offset is deliberately stale there — so this is where a Pitch offset has to be in hand
// already when the note's envelopes are fitted against the read rate. Answers how many output
// frames the voice sounded for, to a 256-frame block.
static std::size_t soundingBlocksWithPublishedPitch(SampleData& s, double offsetSemis,
                                                    std::size_t capFrames) {
    LiveParams block;
    LiveValues v = foldLive(s.play, s.keyTrack);   // s.play keeps its own (zero) offset: the stale copy
    v.pitchOffsetSemitones = offsetSemis;
    block.publish(v);
    s.live = &block;
    VoiceEngine engine(1, s, /*preserveVoiceCap=*/0, /*preserveWindowFrames=*/2048);
    engine.noteOn(60, 127);
    std::vector<AudioSample> out;
    std::size_t life = 0;
    while (out.size() < capFrames && engine.activeVoiceCount() > 0) {
        engine.render(out, 256);
        life = out.size();
    }
    return life;
}

// Under Varispeed the Pitch offset is a factor of the read increment, and the staged AHD is
// evaluated at the SOURCE offset that increment advances — so its stage frames are fitted to the
// offset the note will ACTUALLY play at, exactly as they are to Rate. The attack therefore
// completes on the same output frame at every offset. Fitting against the snapshot's stale zero
// instead is what this catches.
static void testAPublishedPitchOffsetLeavesTheStagedAttackWallClock() {
    constexpr std::int64_t kAttack = 2000;
    for (double semis : {-12.0, 0.0, 12.0}) {
        SampleData s;
        s.frames.assign(96000, 1.0f);   // DC: the output IS the amp envelope
        s.sampleRate = kRate;
        s.rootNote = 60;
        s.play.playMode = PlayMode::Trigger;
        s.play.pitchEngine = PitchEngine::Varispeed;
        s.play.trigAhd = AhdParams{kAttack, 0, 1.0, util::kCurveNeutral, util::kCurveNeutral};

        LiveParams block;
        LiveValues v = foldLive(s.play, s.keyTrack);
        v.pitchOffsetSemitones = semis;
        block.publish(v);
        s.live = &block;
        VoiceEngine engine(1, s, /*preserveVoiceCap=*/0, /*preserveWindowFrames=*/2048);
        engine.noteOn(60, 127);
        std::vector<AudioSample> out;
        engine.render(out, 8000);
        std::size_t reachedFull = 0;
        for (std::size_t i = 0; i < out.size(); ++i) {
            if (out[i] > 0.99f) { reachedFull = i; break; }
        }
        const bool ok = reachedFull > 0 &&
                        std::fabs(static_cast<double>(reachedFull) -
                                  static_cast<double>(kAttack)) < 40.0;
        CHECK(ok);
        if (!ok) std::printf("  pitch %+.1f st: attack completed at %zu\n", semis, reachedFull);
    }
}

// The pitch envelope's SPAN is a wall-clock duration converted from the same read rate, so it
// follows the published offset too. Read out as the note's LIFETIME: the envelope's depth
// cancels the offset while it holds, so the read runs at unity for the hold and at the offset
// ratio after it — which makes the lifetime a direct readout of where the hold ended.
//   12000 source frames, offset -12 st (read at 0.5): the span is 24000 output frames, its
//   half-span hold is 12000 of them at unity, and the source is exhausted exactly there.
//   A span fitted to the stale zero offset is 12000, holds for 6000, and the remaining 6000
//   source frames then take 12000 more output frames — 18000 in total.
static void testAPublishedPitchOffsetRefitsThePitchEnvelopeSpan() {
    SampleData s;
    s.frames.assign(12000, 1.0f);
    s.sampleRate = kRate;
    s.rootNote = 60;
    s.play.playMode = PlayMode::Trigger;
    s.play.pitchEngine = PitchEngine::Varispeed;
    s.play.trigAhd = AhdParams{0, 0, 1.0, util::kCurveNeutral, util::kCurveNeutral};
    s.play.pitchEnv.enabled = true;
    s.play.pitchEnv.peakSemitones = 12.0;    // cancels the -12 offset while it holds
    s.play.pitchEnv.shape.attackFrames = 0;
    s.play.pitchEnv.shape.decayFrames = 0;
    s.play.pitchEnv.shape.holdFraction = 0.5;

    const std::size_t life = soundingBlocksWithPublishedPitch(s, -12.0, 60000);
    CHECK(life > 11000 && life < 13000);
    if (!(life > 11000 && life < 13000)) std::printf("  refit span: life %zu\n", life);
}

// Key-track is the second member of that class, and it is a PITCH-RATIO scalar: a sounding note
// must not be retuned by it, the next note-on must take it. Measured at a note away from the root
// (the ratio is 1.0 at the root whatever key-track says, so the root would prove nothing).
static void testAKeyTrackChangeSparesTheSoundingNoteAndReachesTheNextOne() {
    SampleData still = rampForReadRate();
    SampleData moved = rampForReadRate();
    LiveParams blockA, blockB;
    LiveValues halfTrack = foldLive(moved.play, moved.keyTrack);
    halfTrack.keyTrack = 0.5;  // half key-tracking: an octave up reads at ratio ~1.414, not 2.0

    const std::vector<AudioSample> baseline =
        renderPreserveCapable(still, blockA, nullptr, -1, 72);
    const std::vector<AudioSample> swept =
        renderPreserveCapable(moved, blockB, &halfTrack, 8, 72);
    CHECK(baseline.size() == swept.size());
    bool untouched = true;
    for (std::size_t i = 0; i < baseline.size() && i < swept.size(); ++i) {
        if (baseline[i] != swept[i]) { untouched = false; break; }
    }
    CHECK(untouched);

    // The next note-on takes it, read straight off the ramp: under Varispeed the output value at
    // frame i IS the read position, so the slope over one block is the pitch ratio.
    auto slopePerFrame = [&](double keyTrack) {
        SampleData fresh = rampForReadRate();
        LiveParams block;
        fresh.live = &block;
        LiveValues published = foldLive(fresh.play, fresh.keyTrack);
        published.keyTrack = keyTrack;
        block.publish(published);
        VoiceEngine engine(1, fresh);
        engine.noteOn(72, 100);
        std::vector<AudioSample> out;
        engine.render(out, 512);
        return (static_cast<double>(out.back()) - static_cast<double>(out.front())) /
               static_cast<double>(out.size() - 1) * 200000.0;
    };
    // kKeyTrackDefault is 1.0 — full tracking, so an octave up reads at 2.0.
    CHECK(std::fabs(slopePerFrame(1.0) - 2.0) < 0.01);
    CHECK(std::fabs(slopePerFrame(0.5) - std::pow(2.0, 0.5)) < 0.01);
    // And the value really is carried by the BLOCK: sample.play/keyTrack never moved.
    CHECK(std::fabs(slopePerFrame(0.0) - 1.0) < 0.01);
}

// Trigger length is the third: it resolves playEnd_, so it re-spans the NEXT note and leaves the
// sounding one at the span it was struck with.
static void testATriggerLengthChangeSparesTheSoundingNoteAndReachesTheNextOne() {
    auto triggerSource = [] {
        SampleData s = rampForReadRate();
        s.play.playMode = PlayMode::Trigger;
        s.play.trigger.lengthFraction = 1.0;
        s.play.trigAhd.holdFraction = 1.0;  // flat through the span, so the span IS the lifetime
        return s;
    };
    // The note's LIFETIME is what the fraction spans, so blocks-alive measures it directly.
    auto blocksAlive = [&](double fraction) {
        SampleData fresh = triggerSource();
        LiveParams block;
        fresh.live = &block;
        LiveValues published = foldLive(fresh.play, fresh.keyTrack);
        published.lengthFraction = fraction;
        block.publish(published);
        VoiceEngine engine(1, fresh);
        engine.noteOn(60, 100);
        std::vector<AudioSample> out;
        int blocks = 0;
        while (engine.activeVoiceCount() > 0 && blocks < 4000) {
            engine.render(out, 512);
            ++blocks;
        }
        return blocks;
    };
    const int whole = blocksAlive(1.0);
    const int quarterSpan = blocksAlive(0.25);
    CHECK(whole > 100 && whole < 4000);
    CHECK(std::fabs(static_cast<double>(quarterSpan) - 0.25 * whole) < 0.05 * whole);

    // And the sounding note is spared. The published fraction is small enough that its span ENDS
    // inside the window rendered — asserted, not assumed, because a fraction whose playEnd_ still
    // sat past the render would leave the two runs identical whether the field were live or not.
    constexpr int kSweepBlocks = 24;  // renderPreserveCapable's own loop count
    CHECK(blocksAlive(0.05) < kSweepBlocks);

    SampleData still = triggerSource();
    SampleData moved = triggerSource();
    LiveParams blockA, blockB;
    LiveValues shortened = foldLive(moved.play, moved.keyTrack);
    shortened.lengthFraction = 0.05;
    const std::vector<AudioSample> baseline =
        renderPreserveCapable(still, blockA, nullptr, -1, 60);
    const std::vector<AudioSample> swept =
        renderPreserveCapable(moved, blockB, &shortened, 8, 60);
    CHECK(baseline.size() == swept.size());
    bool untouched = true;
    for (std::size_t i = 0; i < baseline.size() && i < swept.size(); ++i) {
        if (baseline[i] != swept[i]) { untouched = false; break; }
    }
    CHECK(untouched);
}

// --- What stays latched at note-on -------------------------------------------------------

static void testPitchRatioAndVelocityGainStayLatched() {
    // A ramp source read under Varispeed: every output frame is (source at readPos) * velocity
    // gain, so a moved pitch ratio or a moved velocity gain would show up directly.
    //
    // The filter and the pitch envelope are OFF here on purpose — that is what makes the read
    // rate provable arithmetic. It also means the block's filter and pitch-envelope fields
    // cannot land on this voice; that they DO land on a voice that has them enabled, and still
    // leave the velocity gain alone, is the next test's job.
    SampleData s;
    s.frames.resize(100000);
    for (std::size_t i = 0; i < s.frames.size(); ++i) {
        s.frames[i] = static_cast<float>(static_cast<double>(i) / 100000.0);
    }
    s.sampleRate = kRate;
    s.rootNote = 60;
    s.velocityCurve = VelocityCurve::linear();
    s.play.adsr.sustainLevel = 1.0;

    LiveParams block;
    s.live = &block;
    block.publish(foldLive(s.play, s.keyTrack));
    VoiceEngine engine(1, s);
    engine.noteOn(72, 64);  // an octave up: ratio 2.0

    std::vector<AudioSample> out;
    LiveValues hostile = foldLive(s.play, s.keyTrack);
    // Everything the block CAN carry, moved as far as it goes. keyTrack and lengthFraction DO
    // name the pitch ratio and the play span — they are here precisely because a SOUNDING voice
    // must not read either, which is what makes them note-on-latched rather than live; the tests
    // above are what prove the next note does take them.
    hostile.keyTrack = 0.0;
    hostile.lengthFraction = 0.05;
    hostile.filterKeyTrack = 2.0;
    hostile.filterSettings.cutoffNorm = 0.0f;
    hostile.filterModAmount = 1.0;
    hostile.filterVelAmount = 1.0;
    hostile.pitchEnv.shape.attackFrames = 4800;
    hostile.pitchEnv.shape.decayFrames = 4800;
    hostile.pitchEnv.peakSemitones = 24.0;
    hostile.adsr.attackFrames = 96000;  // a timed stage the voice is already past
    for (int blk = 0; blk < 8; ++blk) {
        if (blk == 2) block.publish(hostile);
        engine.render(out, 512);
    }

    const double velocityGain = s.velocityCurve.eval(64.0);
    bool pitchAndGainHeld = true;
    for (std::size_t i = 0; i < out.size(); ++i) {
        const double expected =
            (static_cast<double>(2 * i) / 100000.0) * velocityGain;  // ratio 2.0, latched gain
        if (std::fabs(static_cast<double>(out[i]) - expected) > 1e-6) {
            pitchAndGainHeld = false;
            break;
        }
    }
    CHECK(pitchAndGainHeld);

    // Positive control on the same rig: a field that IS live does change the output, so the
    // assertion above is not simply proving the block was ignored wholesale.
    SampleData s2 = s;
    LiveParams block2;
    s2.live = &block2;
    block2.publish(foldLive(s2.play, s2.keyTrack));
    VoiceEngine engine2(1, s2);
    engine2.noteOn(72, 64);
    std::vector<AudioSample> out2;
    LiveValues quieter = foldLive(s2.play, s2.keyTrack);
    quieter.adsr.sustainLevel = 0.25;
    for (int blk = 0; blk < 8; ++blk) {
        if (blk == 2) block2.publish(quieter);
        engine2.render(out2, 512);
    }
    CHECK(std::fabs(static_cast<double>(out2.back()) - static_cast<double>(out.back())) > 1e-4);
}

static void testVelocityGainSurvivesAHostilePublishThatReallyLands() {
    // Filter AND pitch envelope enabled, so every field the block carries actually reaches the
    // voice. The filter and pitch velocity curves are left at their off defaults, so velocity
    // enters the render exactly once — as the amp gain latched at note-on — which makes two
    // runs at different velocities exactly proportional unless the publish moved that gain (a
    // re-derived gain would have to preserve the ratio 100:64 to slip through).
    SampleData rig = periodicSine(200000, 64.0);
    rig.velocityCurve = VelocityCurve::linear();
    filterSweep(rig);
    rig.play.pitchEnv.enabled = true;
    rig.play.pitchEnv.shape.decayFrames = 24000;
    rig.play.pitchEnv.peakSemitones = 3.0;

    LiveValues hostile = foldLive(rig.play, rig.keyTrack);
    hostile.filterKeyTrack = 2.0;
    hostile.filterSettings.cutoffNorm = 0.9f;
    hostile.filterModAmount = -1.0;
    hostile.filterEnv.decayFrames = 4800;
    hostile.filterEnv.sustainLevel = 0.0;
    hostile.pitchEnv.shape.attackFrames = 4800;
    hostile.pitchEnv.shape.decayFrames = 4800;
    hostile.pitchEnv.peakSemitones = 24.0;
    hostile.adsr.sustainLevel = 0.4;

    SampleData quiet = rig, loud = rig, untouched = rig;
    LiveParams blockQuiet, blockLoud, blockUntouched;
    const Run atQuiet = renderWithLive(quiet, &blockQuiet, 512, 16, 2, &hostile, -1, 64);
    const Run atLoud = renderWithLive(loud, &blockLoud, 512, 16, 2, &hostile, -1, 100);
    const Run noPublish = renderWithLive(untouched, &blockUntouched, 512, 16, -1, nullptr, -1, 64);

    // The publish is not inert: it moved the note it was published into.
    double landed = 0.0;
    for (std::size_t i = 512 * 3; i < atQuiet.out.size() && i < noPublish.out.size(); ++i) {
        landed += std::fabs(static_cast<double>(atQuiet.out[i]) -
                            static_cast<double>(noPublish.out[i]));
    }
    CHECK(landed > 1.0);

    // ...and through all of it the two velocities differ by exactly the curve's ratio.
    const double ratio = rig.velocityCurve.eval(100.0) / rig.velocityCurve.eval(64.0);
    CHECK(ratio > 1.5);  // the curve really does separate these two velocities
    bool proportional = true;
    for (std::size_t i = 0; i < atQuiet.out.size() && i < atLoud.out.size(); ++i) {
        if (std::fabs(static_cast<double>(atLoud.out[i]) -
                      static_cast<double>(atQuiet.out[i]) * ratio) > 1e-6) {
            proportional = false;
            break;
        }
    }
    CHECK(proportional);
}

int main() {
    testStageDurationChangeHoldsPhase();
    testShortenedStageStillLandsContinuously();
    testSustainLevelChangeGlides();
    testPitchEnvelopeHoldsPhaseAndGlidesDepth();
    testPitchEnvelopeHoldStagePlaysAndHoldsPhase();
    testAFreshEnvelopeTakesANewlyDialledStageTimeOutright();
    testAFreshPitchEnvelopeTakesTheNewTimesOutright();
    testCutoffMoveAcrossPrepareDoesNotStep();
    testUnmovedBlockIsByteIdenticalToNoBlockAtAll();
    testANoteStartedAfterAPublishSoundsThePublishedEnvelope();
    testEveryEnvelopeStageTimeAndLevelMovesTheSoundingNote();
    testEveryLiveFilterControlMovesTheSoundingNote();
    testOneBlockServesTwoIndependentObservers();
    testARateChangeSpareTheSoundingNoteAndReachesTheNextOne();
    testAKeyTrackChangeSparesTheSoundingNoteAndReachesTheNextOne();
    testATriggerLengthChangeSparesTheSoundingNoteAndReachesTheNextOne();
    testAPitchOffsetChangeMovesTheSoundingNoteInBothEngines();
    testAPublishedPitchOffsetLeavesTheStagedAttackWallClock();
    testAPublishedPitchOffsetRefitsThePitchEnvelopeSpan();
    testPitchRatioAndVelocityGainStayLatched();
    testVelocityGainSurvivesAHostilePublishThatReallyLands();
    if (g_fail == 0) std::printf("live_delivery tests passed\n");
    return g_fail == 0 ? 0 : 1;
}
