// Offline-only measurements. All sound is produced by the linked production classes.
#include "PluginProcessor.h"
#include "Presets/DeepPluckPresets.h"
#include "Synth/DeepPluckEnvelope.h"
#include "Synth/DeepPluckFilter.h"
#include "Synth/DeepPluckOscillator.h"
#include "Synth/DeepPluckUnison.h"
#include "Synth/DeepPluckTransientNoise.h"
#include <juce_audio_utils/juce_audio_utils.h>
#include <juce_dsp/juce_dsp.h>
#include <float.h>
#include <algorithm>
#include <array>
#include <cmath>
#include <iostream>
#include <stdexcept>
#include <vector>

namespace
{
using Values = deeppluck::DeepPluckParameterValues;
using Buffer = juce::AudioBuffer<float>;
using Object = juce::DynamicObject;
using Var = juce::var;
using File = juce::File;
int failures = 0;
bool exportCpuNullCorpus = false;

void check (bool condition, const juce::String& message)
{
    if (! condition) { ++failures; std::cerr << "FAIL: " << message << '\n'; }
}
// Keep passing sample-level checks allocation-free in the offline analyzer too.
void check (bool condition, const char* message)
{
    if (! condition) { ++failures; std::cerr << "FAIL: " << message << '\n'; }
}
Var object() { return Var (new Object); }
void put (Var& value, const char* key, const Var& item)
{ value.getDynamicObject()->setProperty (key, item); }
template <typename Range> Var array (const Range& values)
{
    juce::Array<Var> result;
    for (const auto value : values) result.add (Var (value));
    return result;
}
void saveJson (const File& file, const Var& value)
{
    if (! file.replaceWithText (juce::JSON::toString (value, false)))
        throw std::runtime_error ("Cannot write audit JSON");
}
void saveWave (const File& file, const Buffer& audio, double rate)
{
    // Floating WAV preserves raw >0 dBFS stress values; never clip to PCM.
    std::unique_ptr<juce::OutputStream> stream = file.createOutputStream();
    if (stream == nullptr) throw std::runtime_error ("Cannot create WAV");
    stream->setPosition (0);
    juce::WavAudioFormat format;
    auto writer = format.createWriterFor (stream, juce::AudioFormatWriterOptions {}
        .withSampleRate (rate).withNumChannels (audio.getNumChannels())
        .withBitsPerSample (32)
        .withSampleFormat (juce::AudioFormatWriterOptions::SampleFormat::floatingPoint));
    if (writer == nullptr || ! writer->writeFromAudioSampleBuffer (audio, 0, audio.getNumSamples()))
        throw std::runtime_error ("Cannot write floating WAV");
}
void saveCpuNullRaw (const File& file, const Buffer& audio)
{
    if (! exportCpuNullCorpus) return;
    if (file.exists()) throw std::runtime_error ("CPU null reference already exists");
    std::vector<float> interleaved (static_cast<size_t> (audio.getNumSamples()) * 2);
    for (int i=0;i<audio.getNumSamples();++i)
    {
        interleaved[2*static_cast<size_t>(i)]=audio.getSample(0,i);
        interleaved[2*static_cast<size_t>(i)+1]=audio.getSample(1,i);
    }
    juce::FileOutputStream stream(file);
    if (!stream.openedOk() || !stream.write(interleaved.data(),interleaved.size()*sizeof(float)))
        throw std::runtime_error ("Cannot write complete CPU null corpus");
    stream.flush();
    if (stream.getStatus().failed()) throw std::runtime_error ("CPU null corpus flush failed");
}
Values defaults()
{
    DeepPluckAudioProcessor processor;
    return deeppluck::captureParameterValues (processor.getValueTreeState());
}
void applyValues (DeepPluckAudioProcessor& processor, const Values& values)
{
    auto& state = processor.getValueTreeState();
    for (size_t index = 0; index < values.size(); ++index)
    {
        auto* p = state.getParameter (deeppluck::allParameterIds[index]);
        p->setValueNotifyingHost (p->convertTo0to1 (values[index]));
    }
}
struct Event { int sample; int note; int velocity; }; // -1 note = all sound off
struct Change { int sample; size_t parameter; float value; };
Var eventsJson (const std::vector<Event>& events)
{
    juce::Array<Var> result;
    for (const auto& event : events)
        result.add (array (std::array { event.sample, event.note, event.velocity }));
    return result;
}
struct Metrics
{
    double peakL = 0, peakR = 0, peakM = 0, peakS = 0;
    double ll = 0, rr = 0, mm = 0, ss = 0, lr = 0, l = 0, r = 0;
    int count = 0;
    void add (float left, float right)
    {
        check (std::isfinite (left) && std::isfinite (right), "finite raw sample");
        const double a = left, b = right, mid = (a + b) * .5, side = (a - b) * .5;
        peakL = std::max (peakL, std::abs (a)); peakR = std::max (peakR, std::abs (b));
        peakM = std::max (peakM, std::abs (mid)); peakS = std::max (peakS, std::abs (side));
        ll += a*a; rr += b*b; mm += mid*mid; ss += side*side; lr += a*b;
        l += a; r += b; ++count;
    }
    double rms() const { return count > 0 ? std::sqrt ((ll + rr) / (2 * count)) : 0; }
    double peak() const { return std::max (peakL, peakR); }
    Var json() const
    {
        auto value = object(); const double n = std::max (1, count);
        put (value, "samples", count); put (value, "peakL", peakL); put (value, "peakR", peakR);
        put (value, "peakMid", peakM); put (value, "peakSide", peakS);
        put (value, "rmsL", std::sqrt (ll/n)); put (value, "rmsR", std::sqrt (rr/n));
        put (value, "rmsMid", std::sqrt (mm/n)); put (value, "rmsSide", std::sqrt (ss/n));
        put (value, "rms", rms()); put (value, "dcL", l/n); put (value, "dcR", r/n);
        put (value, "crestDb", rms() > 1e-15 ? Var (20*std::log10 (peak()/rms())) : Var());
        const double den = (ll-l*l/n)*(rr-r*r/n);
        put (value, "correlation", den > 1e-24 ? Var ((lr-l*r/n)/std::sqrt (den)) : Var());
        return value;
    }
};
Metrics measure (const Buffer& audio, int start = 0, int length = -1)
{
    Metrics result;
    const auto end = length < 0 ? audio.getNumSamples() : std::min (audio.getNumSamples(), start+length);
    for (int sample = start; sample < end; ++sample)
        result.add (audio.getSample (0, sample), audio.getSample (1, sample));
    return result;
}
// Diagnostic spectral bands, explicitly Hann-windowed 4096-point FFT, not LUFS.
Var bands (const Buffer& audio, double rate, int start)
{
    constexpr int size = 4096;
    std::array<float, size*2> data {};
    for (int i = 0; i < size && start+i < audio.getNumSamples(); ++i)
        data[static_cast<size_t> (i)] = .5f * (audio.getSample (0, start+i)+audio.getSample (1, start+i))
            * static_cast<float> (.5-.5*std::cos (juce::MathConstants<double>::twoPi*i/(size-1)));
    juce::dsp::FFT fft (12); fft.performFrequencyOnlyForwardTransform (data.data());
    std::array<double, 4> energy {};
    for (int bin = 1; bin < size/2; ++bin)
    {
        const auto hz = rate*bin/size;
        const size_t band = hz < 200 ? 0 : hz < 2000 ? 1 : hz < 8000 ? 2 : 3;
        energy[band] += static_cast<double> (data[static_cast<size_t> (bin)])*data[static_cast<size_t> (bin)];
    }
    return array (energy);
}
struct Tempo final : juce::AudioPlayHead
{
    juce::Optional<PositionInfo> getPosition() const override
    { PositionInfo info; info.setBpm (120.0); return info; }
};
Buffer render (const Values& values, double rate, int blockSize, int total,
               const std::vector<Event>& events, const std::vector<Change>& changes = {})
{
    DeepPluckAudioProcessor processor;
    applyValues (processor, values);
    Tempo tempo; processor.setPlayHead (&tempo);
    processor.prepareToPlay (rate, blockSize);
    Buffer result (2, total); result.clear();
    size_t eventIndex = 0, changeIndex = 0;
    for (int offset = 0; offset < total;)
    {
        while (changeIndex < changes.size() && changes[changeIndex].sample == offset)
        {
            const auto& change = changes[changeIndex++];
            auto* p = processor.getValueTreeState().getParameter (deeppluck::allParameterIds[change.parameter]);
            p->setValueNotifyingHost (p->convertTo0to1 (change.value));
        }
        auto length = std::min (blockSize, total-offset);
        if (changeIndex < changes.size()) length = std::min (length, changes[changeIndex].sample-offset);
        Buffer block (result.getArrayOfWritePointers(), 2, offset, length);
        juce::MidiBuffer midi;
        while (eventIndex < events.size() && events[eventIndex].sample < offset+length)
        {
            const auto& event = events[eventIndex++];
            const auto message = event.note < 0 ? juce::MidiMessage::allSoundOff (1)
                : event.velocity == 0 ? juce::MidiMessage::noteOff (1, event.note)
                : juce::MidiMessage::noteOn (1, event.note, static_cast<juce::uint8> (event.velocity));
            midi.addEvent (message, event.sample-offset);
        }
        _clearfp();
        processor.processBlock (block, midi);
        const auto flags = _statusfp();
        check ((flags & (_SW_INVALID | _SW_ZERODIVIDE | _SW_OVERFLOW)) == 0,
               "legal callback has no floating fault before finite fallbacks");
        offset += length;
    }
    processor.releaseResources(); return result;
}
double delta (const Buffer& a, const Buffer& b)
{
    double maximum = 0;
    check (a.getNumSamples() == b.getNumSamples(), "comparable render length");
    for (int channel = 0; channel < 2; ++channel)
        for (int sample = 0; sample < std::min (a.getNumSamples(), b.getNumSamples()); ++sample)
            maximum = std::max (maximum, std::abs (static_cast<double> (a.getSample (channel, sample))-b.getSample (channel, sample)));
    return maximum;
}
std::vector<Event> notes (double rate, int pattern, int polyphony, int velocity)
{
    std::vector<Event> result;
    const int repetitions = pattern == 0 ? 1 : 4;
    for (int repeat = 0; repeat < repetitions; ++repeat)
        for (int note = 0; note < polyphony; ++note)
        {
            const auto onset = 37 + static_cast<int> (rate*(repeat*.25 + (pattern == 3 ? note*.008 : 0)));
            const auto pitch = 48 + (pattern == 1 ? 0 : pattern == 3 ? repeat*2 : 0) + note;
            result.push_back ({ onset, pitch, velocity });
            result.push_back ({ onset+static_cast<int> (rate*.16), pitch, 0 });
        }
    std::stable_sort (result.begin(), result.end(), [] (auto a, auto b) { return a.sample < b.sample; });
    return result;
}
void contracts (const File& root, const File& input)
{
    DeepPluckAudioProcessor processor; auto& state = processor.getValueTreeState();
    auto report = object(); juce::Array<Var> manifest;
    for (size_t index = 0; index < deeppluck::allParameterIds.size(); ++index)
    {
        auto* p = state.getParameter (deeppluck::allParameterIds[index]);
        auto item = object(); const auto& range = p->getNormalisableRange();
        put (item, "index", static_cast<int> (index)); put (item, "id", p->paramID);
        put (item, "name", p->getName (128)); put (item, "hint", p->getVersionHint());
        put (item, "automatable", p->isAutomatable()); put (item, "defaultNormalized", p->getDefaultValue());
        put (item, "min", range.start); put (item, "max", range.end); put (item, "interval", range.interval);
        put (item, "skew", range.skew); put (item, "symmetricSkew", range.symmetricSkew);
        put (item, "kind", dynamic_cast<juce::AudioParameterFloat*> (p) ? "float"
            : dynamic_cast<juce::AudioParameterInt*> (p) ? "int"
            : dynamic_cast<juce::AudioParameterBool*> (p) ? "bool" : "choice");
        if (auto* choice = dynamic_cast<juce::AudioParameterChoice*> (p)) put (item, "choices", array (choice->choices));
        juce::Array<Var> grid;
        for (int n = 0; n <= 100; ++n)
        {
            const auto normalized = n/100.0f; auto point = object();
            put (point, "raw", p->convertFrom0to1 (normalized));
            put (point, "text", p->getText (normalized, 64));
            put (point, "parsed", p->getValueForText (p->getText (normalized, 64)));
            grid.add (point);
        }
        put (item, "grid", grid); manifest.add (item);
    }
    put (report, "manifest", manifest);
    juce::Array<Var> factories;
    for (int i = 0; i < 4; ++i)
    {
        deeppluck::applyFactoryPreset (state, i); auto item = object();
        put (item, "name", deeppluck::factoryPresets[static_cast<size_t> (i)].name);
        put (item, "values", array (deeppluck::captureParameterValues (state))); factories.add (item);
    }
    put (report, "factories", factories);
    applyValues (processor, defaults());
    deeppluck::DeepPluckRandomization random (state, 0x1125ABCDEFULL);
    juce::Array<Var> randomValues;
    for (int i = 0; i < 10000; ++i)
    {
        random.setAmount (std::array { .1f, .25f, .5f, 1.f }[static_cast<size_t> (i%4)]);
        const auto module = static_cast<deeppluck::RandomizationModule> (i%10);
        random.setModuleLocked (module, i%13 == 0);
        static_cast<void> (i%11 == 0 ? random.randomizeAll() : random.randomizeModule (module));
        if (i%17 == 0) static_cast<void> (random.undo());
        randomValues.add (array (deeppluck::captureParameterValues (state)));
    }
    put (report, "seeded10000Actions", randomValues);
    saveJson (root.getChildFile ("contract.json"), report);
    const auto stateRoot = root.getChildFile ("states"); stateRoot.createDirectory();
    // Real serialized states from the recorded executable running this audit.
    // Trimmed prefixes are compatibility probes, not archived older binaries.
    for (const int prefix : { 2, 6, 16, 24, 29, 32, 40, 44, 62 })
    {
        auto tree = state.copyState();
        for (int i = tree.getNumChildren(); --i >= 0;)
            if (deeppluck::findParameterIndex (tree.getChild (i)["id"].toString()) >= prefix)
                tree.removeChild (i, nullptr);
        juce::MemoryBlock binary;
        juce::AudioProcessor::copyXmlToBinary (*tree.createXml(), binary);
        const auto name = "prefix-"+juce::String (prefix);
        check (stateRoot.getChildFile (name+".state").replaceWithData (binary.getData(), binary.getSize()), "state fixture saved");
        DeepPluckAudioProcessor restored;
        restored.setStateInformation (binary.getData(), static_cast<int> (binary.getSize()));
        saveJson (stateRoot.getChildFile (name+".json"), array (deeppluck::captureParameterValues (restored.getValueTreeState())));
    }
    if (input != File {})
    {
        const auto fixtures = input.getChildFile ("states").findChildFiles (File::findFiles, false, "*.state");
        check (fixtures.size() == 9, "cross-reader requires all nine real serialized fixtures");
        for (const auto& file : fixtures)
        {
            juce::MemoryBlock binary;
            check (file.loadFileAsData (binary), "cross-reader fixture loaded");
            // Legacy missing values depend on the declared preload. Each
            // fixture's oracle was captured with a fresh instance, not the
            // preceding randomized or restored state.
            DeepPluckAudioProcessor reader;
            reader.setStateInformation (binary.getData(), static_cast<int> (binary.getSize()));
            const auto expected = juce::JSON::parse (file.withFileExtension ("json"));
            const auto actual = deeppluck::captureParameterValues (reader.getValueTreeState());
            check (expected.size() == 62, "reference fixture has 62 expected values");
            for (int i = 0; i < std::min (62, expected.size()); ++i)
                check (std::abs (actual[static_cast<size_t> (i)]-static_cast<float> (expected[i])) < 1e-5f,
                       "genuine cross-version serialized state values retained");
        }
        auto cross = object(); put (cross,"fixtures",fixtures.size());
        put (cross,"input",input.getFullPathName()); put (cross,"failures",failures);
        saveJson (root.getChildFile ("cross-state.json"),cross);
    }
}
void envelopes (const File& root)
{
    juce::Array<Var> result;
    using Envelope = deeppluck::DeepPluckEnvelope;
    for (double rate : { 44100., 48000., 96000. })
        for (float sustain : { 0.f, .001f, .5f, 1.f })
            for (float duration : { .001f, .002f, .003f, .010f, .180f, .320f, 1.f, 2.f, 5.f, 10.f })
            {
                Envelope env; env.prepare (rate);
                const float attack = std::min (duration, 2.f), decay = std::min (duration, 5.f);
                env.setParameters ({ attack, decay, sustain, duration }); env.noteOn();
                auto item = object(); put (item, "rate", rate); put (item, "duration", duration); put (item, "sustain", sustain);
                const int an = static_cast<int> (std::round (attack*rate));
                const int dn = static_cast<int> (std::round (decay*rate));
                const int rn = static_cast<int> (std::round (duration*rate));
                double attackSum = 0; float previous = 0;
                for (int i = 0; i < an; ++i) { const auto v = env.getNextSample(); check (v >= previous, "monotonic attack"); previous=v; attackSum += v; }
                check (env.getCurrentLevel() == 1.f, "exact attack endpoint"); put (item, "attackSum", attackSum);
                juce::Array<Var> curve; double maximumDecayRise=0, decayEnergy=0;
                for (int i = 0; i < dn; ++i)
                {
                    const auto v = env.getNextSample();
                    maximumDecayRise=std::max (maximumDecayRise,static_cast<double> (v)-previous);
                    decayEnergy+=static_cast<double> (v)*v; previous=v;
                    if (sustain<1.f && i<dn-1) check (env.getStage()==Envelope::Stage::decay,"decay stage never ends early");
                    if (i == dn/10 || i == dn/4 || i == dn/2 || i == dn-1) curve.add (v);
                }
                check (maximumDecayRise<=1e-6,"monotonic decay rate="+juce::String (rate)+" time="+juce::String (duration)+" sustain="+juce::String (sustain)+" rise="+juce::String (maximumDecayRise,10));
                put (item, "decay10_25_50_100", curve);
                put (item,"maximumDecayRise",maximumDecayRise); put (item,"decayEnergy",decayEnergy);
                check (env.getStage() == Envelope::Stage::sustain && env.getCurrentLevel() == sustain, "exact decay endpoint");
                env.noteOff(); double releaseEnergy=0;
                for (int i = 0; i < rn; ++i)
                {
                    const auto level=env.getNextSample(); releaseEnergy+=static_cast<double> (level)*level;
                    if (sustain>0.f && i<rn-1) check (env.getStage()==Envelope::Stage::release,"release stage never ends early");
                }
                put (item,"releaseEnergy",releaseEnergy);
                check (! env.isActive() && env.getCurrentLevel() == 0.f, "exact release endpoint");
                put (item, "attackSamples", an); put (item, "decaySamples", dn); put (item, "releaseSamples", rn);
                result.add (item);
            }
    saveJson (root.getChildFile ("envelopes.json"), result);
}
void oscillators (const File& root)
{
    juce::Array<Var> result;
    for (double rate : { 44100., 48000., 96000. })
        for (int oscillator : { 1, 2 })
            for (int wave = 0; wave < 5; ++wave)
                for (int note : { 36, 48, 60, 72, 84 })
                    for (float width : { .05f, .25f, .5f, .75f, .95f })
                    {
                        if (wave != 4 && width != .5f) continue;
                        deeppluck::DeepPluckOscillator osc; osc.prepare (rate);
                        osc.reset (oscillator == 1 ? 0.0 : .25,
                            static_cast<deeppluck::DeepPluckOscillatorWaveform> (wave), width);
                        osc.setFrequency (juce::MidiMessage::getMidiNoteInHertz (note));
                        Buffer audio (2, static_cast<int> (rate*.3));
                        for (int i = 0; i < audio.getNumSamples(); ++i)
                        { const auto v = osc.processSample(); audio.setSample (0,i,v); audio.setSample (1,i,v); }
                        auto item = object(); put (item,"rate",rate); put (item,"oscillator",oscillator);
                        put (item,"waveform",wave); put (item,"note",note); put (item,"width",width);
                        put (item,"start10ms",measure (audio,0,static_cast<int> (rate*.01)).json());
                        put (item,"start50ms",measure (audio,0,static_cast<int> (rate*.05)).json());
                        put (item,"steady",measure (audio,static_cast<int> (rate*.1)).json());
                        put (item,"bands",bands (audio,rate,static_cast<int> (rate*.1)));
                        deeppluck::DeepPluckFilter filter; filter.prepare (rate);
                        filter.setCutoffFrequencyHz (1800); filter.setResonance (.18f); filter.setDriveDb (3); filter.reset();
                        for (int i = 0; i < audio.getNumSamples(); ++i)
                        { const auto v = filter.processSample (audio.getSample (0,i)); audio.setSample (0,i,v); audio.setSample (1,i,v); }
                        put (item,"filtered",measure (audio).json()); result.add (item);
                    }
    saveJson (root.getChildFile ("oscillators.json"), result);
}
void filters (const File& root)
{
    juce::Array<Var> result;
    for (double rate : { 44100., 48000., 96000. })
        for (float cutoff : { 20.f, 100.f, 500.f, 1800.f, 5000.f, 10000.f, 20000.f })
            for (float resonance : { 0.f, .18f, .5f, .8f, 1.f })
                for (float drive : { 0.f, 3.f, 9.f, 18.f })
                    for (double ratio : { .125, 1., 2. })
                    {
                        deeppluck::DeepPluckFilter filter; filter.prepare (rate);
                        filter.setCutoffFrequencyHz (cutoff); filter.setResonance (resonance); filter.setDriveDb (drive); filter.reset();
                        Metrics metrics; _clearfp();
                        for (int i=0; i<32768; ++i)
                        {
                            const float input = .2f*static_cast<float> (std::sin (juce::MathConstants<double>::twoPi*std::min (rate*.4, cutoff*ratio)*i/rate));
                            const auto output = filter.processSample (input);
                            if (i >= 16384) metrics.add (output,output);
                        }
                        check ((_statusfp() & (_SW_INVALID|_SW_OVERFLOW|_SW_ZERODIVIDE)) == 0, "filter floating faults");
                        check (metrics.peak() < 4, "isolated filter stress bound");
                        auto item=object(); put (item,"rate",rate); put (item,"cutoff",cutoff); put (item,"resonance",resonance);
                        put (item,"drive",drive); put (item,"probeRatio",ratio); put (item,"metrics",metrics.json()); result.add (item);
                    }
    saveJson (root.getChildFile ("filters.json"), result);
}
void corpus (const File& root)
{
    juce::Array<Var> cases;
    const auto raw = root.getChildFile ("raw"); raw.createDirectory();
    const auto record = [&] (const juce::String& id, const Values& values, double rate,
                             const std::vector<Event>& midi, double seconds, bool waveFile, double peakLimit=.95)
    {
        const auto audio = render (values,rate,127,static_cast<int> (rate*seconds),midi);
        const auto metrics = measure (audio); check (metrics.peak() <= peakLimit,id+" raw peak bound");
        auto item=object(); put (item,"id",id); put (item,"values",array (values)); put (item,"rate",rate);
        put (item,"block",127); put (item,"tempo",120); put (item,"events",eventsJson (midi)); put (item,"seconds",seconds);
        put (item,"reset","fresh processor, parameters before prepare, no audio preload; deterministic per-voice noise");
        put (item,"metrics",metrics.json()); put (item,"attack10ms",measure (audio,37,static_cast<int> (rate*.01)).json());
        put (item,"attack50ms",measure (audio,37,static_cast<int> (rate*.05)).json()); put (item,"bands",bands (audio,rate,37));
        put (item,"peakLimit",peakLimit); put (item,"rawFile",waveFile ? "raw/"+id+".wav" : "");
        if (waveFile) saveWave (raw.getChildFile (id+".wav"),audio,rate);
        saveCpuNullRaw(raw.getChildFile(id+".f32"),audio);
        cases.add (item);
    };
    for (int preset=-1; preset<4; ++preset)
    {
        DeepPluckAudioProcessor p;
        if (preset >= 0) deeppluck::applyFactoryPreset (p.getValueTreeState(),preset);
        const auto values=deeppluck::captureParameterValues (p.getValueTreeState());
        for (int pattern=0; pattern<4; ++pattern)
            for (int velocity : { 32,64,96,127 })
                for (int poly : { 1,4,8,16 })
                    record ("preset"+juce::String (preset)+"-p"+juce::String (pattern)+"-v"+juce::String (velocity)+"-n"+juce::String (poly),
                            values,48000,notes (48000,pattern,poly,velocity),2.2,velocity==96 && poly==4);
    }
    for (int primary=0; primary<5; ++primary)
        for (int secondary=0; secondary<5; ++secondary)
            for (int count : { 1,4 })
            {
                auto values=defaults(); values[40]=static_cast<float> (primary); values[42]=static_cast<float> (secondary);
                values[29]=static_cast<float> (count);
                record ("pair"+juce::String (primary)+juce::String (secondary)+"-n"+juce::String (count),values,48000,notes (48000,3,4,96),2.2,primary==secondary);
            }
    for (int count : { 1,2,3,4 })
        for (float detune : { 0.f,4.f,9.f,30.f })
            for (float spread : { 0.f,.35f,.75f,1.f })
                for (int mod : { 0,1 })
                {
                    auto v=defaults(); v[29]=static_cast<float> (count); v[30]=detune; v[31]=spread;
                    v[38]=mod ? 10.f : 0.f; v[39]=mod ? .5f : 0.f;
                    const auto id="unison"+juce::String (count)+"-d"+juce::String (detune)+"-s"+juce::String (spread)+"-m"+juce::String (mod);
                    record (id,v,48000,notes (48000,3,4,96),2.2,detune==9.f && spread==.75f && mod==0);
                }
    for (int order=0; order<6; ++order)
        for (int selection=0; selection<5; ++selection)
            for (int mix=0; mix<3; ++mix)
            {
                auto v=defaults(); v[61]=static_cast<float> (order);
                v[44]=selection==1 || selection==4 ? 0.f : 1.f;
                v[49]=selection==2 || selection==4 ? 0.f : 1.f;
                v[56]=selection==3 || selection==4 ? 0.f : 1.f;
                if (mix!=1) for (size_t i : { 48u,55u,60u }) v[i]=mix==0 ? 0.f : 1.f;
                record ("fx-o"+juce::String (order)+"-s"+juce::String (selection)+"-m"+juce::String (mix),v,48000,notes (48000,3,4,96),8.0,order==0 && mix==1);
            }
    for (float output : { -3.f,0.f,6.f })
        for (float key : { 0.f,.35f,1.f })
            for (float amount : { -5.f,0.f,5.f })
                for (float cutoff : { 20.f,1800.f,20000.f })
                {
                    auto v=defaults(); v[0]=output; v[7]=cutoff; v[8]=1; v[9]=18; v[10]=key; v[11]=amount;
                    v[17]=0; v[21]=0; v[25]=0; v[29]=4; v[30]=30; v[31]=1; v[37]=2; v[38]=50; v[39]=1;
                    record ("stress-o"+juce::String (output)+"-k"+juce::String (key)+"-e"+juce::String (amount)+"-c"+juce::String (cutoff),
                            v,48000,notes (48000,3,16,127),2.2,false,output==-3.f ? .95 : 4.0);
                }
    auto v=defaults(); v[5]=.02f;
    for (double rate : { 44100.,48000.,96000. })
    {
        auto midi=notes (rate,1,16,127);
        midi.push_back ({ 111,90,96 }); midi.push_back ({ 211,90,0 });
        midi.push_back ({ static_cast<int> (rate*.9),-1,0 });
        std::stable_sort (midi.begin(),midi.end(),[] (auto a,auto b) { return a.sample<b.sample; });
        const int total=static_cast<int> (rate*1.2);
        const std::vector<Change> changes { { 2048,38,12.f },{ 4096,39,.5f },{ 8192,5,.05f },{ 12288,5,.5f } };
        for (int dynamic : { 0,1 })
        {
            const auto edits=dynamic ? changes : std::vector<Change>{};
            const auto control=render (v,rate,512,total,midi,edits);
            for (int block : { 1,17,32,64,127,512,1024 })
            {
                const auto alternate=render (v,rate,block,total,midi,edits);
                const auto error=delta (control,alternate);
                check (error<=1e-6,"absolute MIDI/snapshot partition invariance");
                auto item=object(); put (item,"id","partition-"+juce::String (rate)+"-"+juce::String (block)+"-"+juce::String (dynamic));
                put (item,"delta",error); cases.add (item);
                saveCpuNullRaw(raw.getChildFile("partition-"+juce::String(rate)+"-"+juce::String(block)+"-"+juce::String(dynamic)+".f32"),alternate);
            }
        }
    }
    saveJson (root.getChildFile ("cases.json"),cases);
}

// Production component probes, not a replacement sound renderer. The second
// half measures actual synth -> rack -> Output and null-tests that composition
// against the complete processor. No audio-thread diagnostics are introduced.
void gainChain (const File& root)
{
    const juce::ScopedNoDenormals noDenormals;
    juce::Array<Var> probes, chains;
    constexpr double rate=48000;
    constexpr int total=105600, blockSize=127;
    for (int count : { 1,2,3,4 })
    {
        deeppluck::DeepPluckUnison unison (0); unison.prepare (rate);
        unison.noteOn (48,count,4.f,.35f);
        deeppluck::DeepPluckTransientNoise layer (0); layer.prepare (rate); layer.noteOn (48);
        std::array<deeppluck::DeepPluckFilter,2> filter;
        for (auto& f : filter) { f.prepare (rate); f.setCutoffFrequencyHz (1800); f.setResonance (.18f); f.setDriveDb (3); f.reset(); }
        Metrics tonal, layers, preFilter, postFilter;
        const auto frequency=juce::MidiMessage::getMidiNoteInHertz (48);
        for (int i=0; i<total; ++i)
        {
            const auto t=unison.processSample (frequency,frequency*std::exp2 (-7./1200.),juce::Decibels::decibelsToGain (-15.f),1.f);
            const auto l=layer.processSample();
            tonal.add (t.left,t.right); layers.add (l,l); preFilter.add (t.left+l,t.right+l);
            postFilter.add (filter[0].processSample (t.left+l),filter[1].processSample (t.right+l));
        }
        auto probe=object(); put (probe,"unison",count); put (probe,"normalisation",unison.getNormalisationGain());
        put (probe,"tonal",tonal.json()); put (probe,"layers",layers.json());
        put (probe,"beforeFilter",preFilter.json()); put (probe,"afterFilter",postFilter.json());
        put (probe,"conditions","isolated components; note 48; Saw/Saw; PW .5; Osc2 -15 dB/-7 ct; Detune 4 ct; Spread .35; layers default; cutoff 1800 Hz/res .18/drive 3 dB; constant cutoff; no Amp envelope; 48000 Hz; 2.2 s");
        probes.add (probe);
        for (int poly : { 1,16 })
            for (int enabled : { 0,1 })
                for (int fx : { 0,1 })
                {
                    auto values=defaults(); values[29]=static_cast<float> (count);
                    values[6]=static_cast<float> (enabled); values[10]=0; values[11]=0;
                    values[44]=values[49]=values[56]=fx ? 0.f : 1.f;
                    DeepPluckAudioProcessor holder; applyValues (holder,values);
                    deeppluck::DeepPluckParameterCache cache (holder.getValueTreeState());
                    const auto snapshot=cache.getSnapshot();
                    deeppluck::DeepPluckSynthesiser synth;
                    deeppluck::configureDeepPluckSynthesiser (synth,snapshot);
                    synth.setCurrentPlaybackSampleRate (rate); synth.setHostTempoBpm (120);
                    deeppluck::DeepPluckEffectsRack rack; rack.prepare (rate,blockSize);
                    const auto events=notes (rate,0,poly,127);
                    Buffer result (2,total); result.clear();
                    Metrics sum, postFx;
                    size_t next=0;
                    for (int offset=0; offset<total; offset+=blockSize)
                    {
                        const int length=std::min (blockSize,total-offset);
                        Buffer block (result.getArrayOfWritePointers(),2,offset,length);
                        juce::MidiBuffer midi;
                        while (next<events.size() && events[next].sample<offset+length)
                        {
                            const auto& e=events[next++];
                            midi.addEvent (e.velocity==0 ? juce::MidiMessage::noteOff (1,e.note)
                                : juce::MidiMessage::noteOn (1,e.note,static_cast<juce::uint8> (e.velocity)),e.sample-offset);
                        }
                        synth.renderNextBlock (block,midi,0,length);
                        for (int i=0;i<length;++i) sum.add (block.getSample (0,i),block.getSample (1,i));
                        rack.process (block,snapshot,120);
                        for (int i=0;i<length;++i) postFx.add (block.getSample (0,i),block.getSample (1,i));
                        block.applyGain (juce::Decibels::decibelsToGain (snapshot.outputGainDb));
                    }
                    const auto complete=render (values,rate,blockSize,total,events);
                    const auto error=delta (complete,result);
                    check (error<=1e-7,"measured gain-chain composition equals complete processor");
                    check (measure (result).peak()<=.95,"gain-chain musical headroom");
                    auto item=object(); put (item,"unison",count); put (item,"polyphony",poly);
                    put (item,"filter",enabled); put (item,"fx",fx); put (item,"rate",rate); put (item,"block",blockSize);
                    put (item,"values",array (deeppluck::captureParameterValues (holder.getValueTreeState())));
                    put (item,"events",eventsJson (events)); put (item,"seconds",2.2);
                    put (item,"voiceSumBeforeFx",sum.json()); put (item,"afterFx",postFx.json());
                    put (item,"afterOutput",measure (result).json()); put (item,"processorDelta",error);
                    chains.add (item);
                }
    }
    auto report=object(); put (report,"isolatedProbes",probes); put (report,"productionChains",chains);
    saveJson (root.getChildFile ("gain-chain.json"),report);
}
}
int main (int argc,char** argv)
{
    juce::ScopedJuceInitialiser_GUI initialiser;
    if (argc<2 || argc>4 || (argc==4 && juce::String (argv[3])!="--supplement"
                           && juce::String (argv[3])!="--cpu-null")) return 2;
    exportCpuNullCorpus=argc==4 && juce::String(argv[3])=="--cpu-null";
    try
    {
        File root (argv[1]); check (root.createDirectory().wasOk(),"audit output directory");
        auto info=object(); put (info,"version",DEEPPLUCK_AUDIT_VERSION);
#if JUCE_DEBUG
        put (info,"configuration","Debug");
#else
        put (info,"configuration","Release");
#endif
        put (info,"soundSource","linked real production code; no reference formula reconstruction");
        saveJson (root.getChildFile ("run.json"),info);
        contracts (root,argc>=3 && juce::String(argv[2])!="-" ? File (argv[2]) : File {});
        if (argc!=4 || exportCpuNullCorpus) { envelopes (root); oscillators (root); filters (root); corpus (root); }
        gainChain (root);
        put (info,"failures",failures); saveJson (root.getChildFile ("run.json"),info);
        std::cout << "Tone/voicing audit: " << failures << " failures\n";
    }
    catch (const std::exception& e) { std::cerr << e.what() << '\n'; return 1; }
    return failures==0 ? 0 : 1;
}
