#include #include #include #include "../PluginProcessor.h" // =========================================================================== // Headless QA harness: instantiates the processor, plays notes through every // factory preset and reports NaN/inf and silence failures. // =========================================================================== namespace { const int notes[3] = { 48, 60, 67 }; bool hasNaN (const juce::AudioBuffer& b) { for (int ch = 0; ch < b.getNumChannels(); ++ch) { const float* d = b.getReadPointer (ch); for (int i = 0; i < b.getNumSamples(); ++i) if (! std::isfinite (d[i])) return true; } return false; } float peak (const juce::AudioBuffer& b) { float p = 0.0f; for (int ch = 0; ch < b.getNumChannels(); ++ch) { const float* d = b.getReadPointer (ch); for (int i = 0; i < b.getNumSamples(); ++i) p = std::max (p, std::abs (d[i])); } return p; } // Plays three notes through the current preset, releases them partway, and // reports whether the render stayed finite and non-silent overall. bool runPreset (serum::SerumAltAudioProcessor& processor, const juce::String& label, double maxPeakFloor) { juce::MidiBuffer midi; for (int n : notes) midi.addEvent (juce::MidiMessage::noteOn (1, n, (juce::uint8) 100), 0); juce::AudioBuffer buffer (2, 512); float maxPeak = 0.0f; bool nan = false; for (int block = 0; block < 240; ++block) { buffer.clear(); processor.processBlock (buffer, midi); midi.clear(); if (block == 180) for (int n : notes) midi.addEvent (juce::MidiMessage::noteOff (1, n), 0); nan = nan || hasNaN (buffer); maxPeak = std::max (maxPeak, peak (buffer)); } const bool ok = ! nan && maxPeak > (float) maxPeakFloor; std::cout << (ok ? "PASS" : "FAIL") << " [" << label << "]" << " peak=" << maxPeak << (nan ? " " : "") << "\n"; return ok; } } int main() { juce::ScopedJuceInitialiser_GUI initialiser; serum::SerumAltAudioProcessor processor; processor.prepareToPlay (44100.0, 512); const int numPresets = processor.getNumPrograms(); int failures = 0; // Default (init) preset. if (! runPreset (processor, "Init", 0.001)) ++failures; // Every factory preset. for (int preset = 0; preset < numPresets; ++preset) { processor.setCurrentProgram (preset); const juce::String name = processor.getProgramName (preset); if (! runPreset (processor, name, 0.001)) ++failures; } // RAVE toggle + parameter churn while playing (no crash / NaN). { processor.setCurrentProgram (0); juce::MidiBuffer midi; for (int n : notes) midi.addEvent (juce::MidiMessage::noteOn (1, n, (juce::uint8) 100), 0); processor.setRaveEnabled (true); juce::AudioBuffer buffer (2, 512); bool nan = false; for (int block = 0; block < 120; ++block) { buffer.clear(); processor.processBlock (buffer, midi); midi.clear(); if (block == 60) processor.setRaveEnabled (false); nan = nan || hasNaN (buffer); } const bool ok = ! nan; if (! ok) ++failures; std::cout << (ok ? "PASS" : "FAIL") << " [RAVE toggle / param churn]" << (nan ? " " : "") << "\n"; } // FX stability: a preset change selects an FX type and its parameters, so // every unit has to survive the extremes of its own parameter range. A unit // that lets its state run away keeps producing inf/NaN, which is heard as // grainy audio that gets no better. { const char* const typeLabels[] = { "Hyper", "Chorus", "Flanger", "Phaser", "Distortion", "EQ", "Compressor", "Delay", "Reverb" }; for (int type = 1; type <= 9; ++type) { processor.setCurrentProgram (0); auto set = [&processor] (const char* id, float value) { if (auto* p = processor.parameters.getParameter (id)) p->setValueNotifyingHost (value); }; set ("fx1Type", (float) type / 9.0f); set ("fx1Mix", 1.0f); set ("fx1P1", 1.0f); set ("fx1P2", 1.0f); set ("fx1P3", 1.0f); set ("fx1P4", 1.0f); juce::MidiBuffer midi; midi.addEvent (juce::MidiMessage::noteOn (1, 60, (juce::uint8) 100), 0); juce::AudioBuffer buffer (2, 512); bool nan = false; for (int block = 0; block < 120; ++block) { buffer.clear(); processor.processBlock (buffer, midi); midi.clear(); nan = nan || hasNaN (buffer); } const bool ok = ! nan; if (! ok) ++failures; std::cout << (ok ? "PASS" : "FAIL") << " [FX " << typeLabels[type - 1] << " at full parameter range]" << (nan ? " " : "") << "\n"; } } // A preset change starts from clean DSP state: the tail of the preset being // left behind must not bleed into the one being loaded. { juce::AudioBuffer buffer (2, 512); processor.setCurrentProgram (9); // Glass Bell, reverb mix 0.6 juce::MidiBuffer midi; for (int n : notes) midi.addEvent (juce::MidiMessage::noteOn (1, n, (juce::uint8) 100), 0); for (int block = 0; block < 40; ++block) { buffer.clear(); processor.processBlock (buffer, midi); midi.clear(); } for (int n : notes) midi.addEvent (juce::MidiMessage::noteOff (1, n), 0); for (int block = 0; block < 10; ++block) { buffer.clear(); processor.processBlock (buffer, midi); midi.clear(); } const float tail = peak (buffer); processor.setCurrentProgram (0); // the change under test float residual = 0.0f; for (int block = 0; block < 2; ++block) { buffer.clear(); juce::MidiBuffer empty; processor.processBlock (buffer, empty); residual = std::max (residual, peak (buffer)); } const bool ok = tail > 0.01f && residual < 1.0e-4f; if (! ok) ++failures; std::cout << (ok ? "PASS" : "FAIL") << " [preset change clears the previous patch]" << " tail=" << tail << " residual=" << residual << "\n"; } std::cout << "\n" << (failures == 0 ? "ALL QA CHECKS PASSED" : "QA FAILURES DETECTED") << " (" << failures << " failures)\n"; return failures == 0 ? 0 : 1; }