Files
serumalt/Source/Tests/TestMain.cpp
T
biggy 4b0db50271 refactor(source): move every implementation into its module directory
- 36 files move into the module whose AGENTS.md already claimed them:
  Engine/, Synth/, Modulation/, Params/, Plugin/, GUI/, EffectUnits/, Resources/
- Source/engine, modulation, params, plugin and synth become Engine,
  Modulation, Params, Plugin and Synth, matching the directories that already
  hold code
- ExactCompare.h stays at the Source/ root as the one shared header
- SERUMALT_INCLUDE_DIRS adds one include directory per module, so the 31
  parent-relative includes become plain names with no path prefix
- the module AGENTS.md files list owned files instead of describing a move
  that had not happened, and README.md and AGENTS.md describe the real tree

Verified: the harness passes CTest, the Linux VST3 and Standalone link, the
Windows cross-build passes its module checks, each configure exposes only its
own targets, no first-party warning line appears, and the generated Windows
manifest is unchanged.
2026-10-06 09:17:49 +02:00

206 lines
7.0 KiB
C++

#include <JuceHeader.h>
#include <cmath>
#include <iostream>
#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<float>& 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<float>& 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<float> 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 ? " <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<float> 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 ? " <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<float> 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 ? " <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<float> 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;
}