monostep/tests/TestHost.cpp
Armin 662a1f7383 Add delay/reverb dry-wet, granular randomize toggles, rework accent DSP
- Add delay and reverb Dry/Wet parameters and knobs, wired into presets
- Add Accent/Slide/Fine/Rate toggles to the Randomize panel with new
  per-aspect pattern randomizers
- Soften accent: modest volume lift plus small cutoff and filter-attack
  modulation instead of the heavy 5x gain boost
- Halve reverb comb/allpass delays so the wet speaks sooner
- Regenerate a fresh build stamp on every build
2026-08-06 18:38:32 +02:00

405 lines
12 KiB
C++

#include <JuceHeader.h>
#include "PluginProcessor.h"
#include <cmath>
static double measureRms (const juce::AudioBuffer<float>& buffer)
{
double sum = 0.0;
int samples = 0;
for (int c = 0; c < buffer.getNumChannels(); ++c)
for (int i = 0; i < buffer.getNumSamples(); ++i)
{
const float s = buffer.getSample (c, i);
sum += (double) s * s;
++samples;
}
return std::sqrt (sum / (double) std::max (1, samples));
}
static bool patternsEqual (const monostep::StepSequencer& a, const monostep::StepSequencer& b)
{
for (int i = 0; i < monostep::numSteps; ++i)
{
const auto& sa = a.step (i);
const auto& sb = b.step (i);
if (sa.gate != sb.gate || sa.semitone != sb.semitone
|| std::fabs (sa.cents - sb.cents) > 0.001f
|| sa.slide != sb.slide || sa.accent != sb.accent)
return false;
}
return true;
}
// Part A: verify the actual .vst3 binary loads as a VST3 module.
static int testVst3Binary (const juce::File& pluginFile)
{
juce::VST3PluginFormatHeadless format;
juce::OwnedArray<juce::PluginDescription> types;
format.findAllTypesForFile (types, pluginFile.getFullPathName());
if (types.isEmpty())
{
std::cout << "FAILED: VST3 binary did not scan as a valid VST3 plugin\n";
return 1;
}
std::cout << "VST3 scan OK - found " << types.size() << " type(s): "
<< types[0]->name << " (uid " << juce::String::toHexString ((int) types[0]->uniqueId) << ")\n";
if (! format.doesPluginStillExist (*types[0]))
{
std::cout << "FAILED: plugin file does not exist per format check\n";
return 1;
}
return 0;
}
// Part B: drive the actual processor source and render audio.
static int testProcessorRendering()
{
MonostepAudioProcessor processor;
processor.prepareToPlay (44100.0, 512);
const int totalSamples = (int) (44100.0 * 8.0);
const int blockSize = 512;
const int numBlocks = totalSamples / blockSize;
juce::AudioBuffer<float> block (2, blockSize);
juce::AudioBuffer<float> out (2, totalSamples);
juce::MidiBuffer midi;
// A held MIDI note is what gates the sequencer into running.
midi.addEvent (juce::MidiMessage::noteOn (1, 60, 0.9f), 0);
for (int b = 0; b < numBlocks; ++b)
{
block.clear();
processor.processBlock (block, midi);
for (int c = 0; c < 2; ++c)
out.copyFrom (c, b * blockSize, block, c, 0, blockSize);
}
const double rms = measureRms (out);
std::cout << "Rendered " << totalSamples << " samples, RMS = " << rms << "\n";
if (rms < 1e-4)
{
std::cout << "FAILED: output is silent\n";
return 1;
}
// Without a MIDI note the sequencer must stay silent.
MonostepAudioProcessor idleProcessor;
idleProcessor.prepareToPlay (44100.0, 512);
double idleSum = 0.0;
juce::MidiBuffer emptyMidi;
for (int b = 0; b < numBlocks; ++b)
{
block.clear();
emptyMidi.clear();
idleProcessor.processBlock (block, emptyMidi);
for (int c = 0; c < 2; ++c)
for (int i = 0; i < blockSize; ++i)
idleSum += (double) block.getSample (c, i) * block.getSample (c, i);
}
if (std::sqrt (idleSum / (double) (numBlocks * blockSize * 2)) > 1e-5)
{
std::cout << "FAILED: sequencer plays without a MIDI trigger\n";
return 1;
}
std::cout << "Silent without MIDI trigger OK\n";
// parameter sanity
std::cout << "Num parameters: " << processor.getNumParameters() << "\n";
// pattern editing + fine tune
processor.setStepGate (0, true);
processor.setStepNote (0, 7);
processor.setStepCents (0, 42.0f);
processor.setStepSlide (1, true);
processor.setStepAccent (0, true);
if (std::fabs (processor.getSequencer().step (0).cents - 42.0f) > 0.001f)
{
std::cout << "FAILED: fine-tune per step did not apply\n";
return 1;
}
// accent DSP: a uniform continuous pattern (all same note, gate held open)
// so no per-note retrigger masks whether accent follows the step.
*processor.getAPVTS().getRawParameterValue ("accent") = 1.0f;
*processor.getAPVTS().getRawParameterValue ("seqGateLen") = 1.0f;
for (int i = 0; i < monostep::numSteps; ++i)
{
processor.setStepGate (i, true);
processor.setStepNote (i, 0);
processor.setStepSlide (i, false);
}
const auto renderAccent = [&] (const std::function<bool (int)>& accented) -> double
{
for (int i = 0; i < monostep::numSteps; ++i)
processor.setStepAccent (i, accented (i));
processor.reset();
processor.prepareToPlay (44100.0, 512);
juce::AudioBuffer<float> accBlock (2, blockSize);
juce::MidiBuffer accMidi;
double sum = 0.0;
for (int b = 0; b < numBlocks; ++b)
{
accBlock.clear();
accMidi.clear();
if (b == 0)
accMidi.addEvent (juce::MidiMessage::noteOn (1, 60, 0.9f), 0);
processor.processBlock (accBlock, accMidi);
for (int c = 0; c < 2; ++c)
for (int i = 0; i < blockSize; ++i)
sum += (double) accBlock.getSample (c, i) * accBlock.getSample (c, i);
}
return std::sqrt (sum / (double) (numBlocks * blockSize * 2));
};
const double accRms = renderAccent ([] (int) { return true; });
const double noAccRms = renderAccent ([] (int) { return false; });
const double altRms = renderAccent ([] (int i) { return (i % 2) == 0; });
if (accRms < noAccRms * 1.05f)
{
std::cout << "FAILED: accent does not boost the step (acc=" << accRms
<< " noacc=" << noAccRms << ")\n";
return 1;
}
if (altRms < noAccRms * 1.05f || altRms > accRms * 0.9f)
{
std::cout << "FAILED: accent does not track individual steps (all=" << accRms
<< " alt=" << altRms << " noacc=" << noAccRms << ")\n";
return 1;
}
std::cout << "Accent boosts step volume OK (all=" << accRms
<< " alternating=" << altRms << " none=" << noAccRms << ")\n";
// pattern shift rotates the steps
processor.setStepAccent (0, true);
processor.setStepNote (0, 7);
processor.setStepNote (1, 3);
processor.shiftPattern (1);
if (processor.getSequencer().step (1).semitone != 7)
{
std::cout << "FAILED: shiftPattern did not rotate steps\n";
return 1;
}
processor.shiftPattern (-1);
if (processor.getSequencer().step (0).semitone != 7 || ! processor.getSequencer().step (0).accent)
{
std::cout << "FAILED: shiftPattern did not rotate back\n";
return 1;
}
std::cout << "Pattern shift rotates steps OK\n";
// state round trip through a fresh instance
juce::MemoryBlock state;
processor.getStateInformation (state);
std::cout << "State bytes: " << (int) state.getSize() << "\n";
MonostepAudioProcessor processor2;
processor2.prepareToPlay (44100.0, 512);
processor2.setStateInformation (state.getData(), (int) state.getSize());
for (int i = 0; i < 4; ++i)
{
const auto& a = processor.getSequencer().step (i);
const auto& b = processor2.getSequencer().step (i);
std::cout << "step " << i << ": gate=" << a.gate << "/" << b.gate
<< " note=" << a.semitone << "/" << b.semitone
<< " cents=" << a.cents << "/" << b.cents
<< " slide=" << a.slide << "/" << b.slide
<< " accent=" << a.accent << "/" << b.accent << "\n";
}
if (! patternsEqual (processor.getSequencer(), processor2.getSequencer()))
{
std::cout << "FAILED: pattern did not survive state save/load\n";
return 1;
}
std::cout << "State round-trip preserves pattern OK\n";
// pattern length parameter
if (processor.getPatternLength() != 16)
{
std::cout << "FAILED: default pattern length is not 16\n";
return 1;
}
*processor.getAPVTS().getRawParameterValue ("seqLen") = 8.0f;
if (processor.getPatternLength() != 8)
{
std::cout << "FAILED: pattern length did not change\n";
return 1;
}
std::cout << "Pattern length OK\n";
*processor.getAPVTS().getRawParameterValue ("seqLen") = 16.0f;
// master = 0 should silence the output while a note is held
*processor.getAPVTS().getRawParameterValue ("master") = 0.0f;
processor.reset();
processor.prepareToPlay (44100.0, 512);
double silentSum = 0.0;
for (int b = 0; b < numBlocks; ++b)
{
block.clear();
midi.clear();
if (b == 0)
midi.addEvent (juce::MidiMessage::noteOn (1, 60, 0.9f), 0);
processor.processBlock (block, midi);
for (int c = 0; c < 2; ++c)
for (int i = 0; i < blockSize; ++i)
silentSum += (double) block.getSample (c, i) * block.getSample (c, i);
}
if (std::sqrt (silentSum / (double) (numBlocks * blockSize * 2)) > 1e-5)
{
std::cout << "FAILED: master=0 did not silence output\n";
return 1;
}
std::cout << "Master=0 silences output OK\n";
// delay + reverb FX: after the note releases, the FX tail must ring out
// (delay repeats / reverb decay) instead of going silent, with no NaN/blow-up.
*processor.getAPVTS().getRawParameterValue ("master") = 1.0f;
*processor.getAPVTS().getRawParameterValue ("release") = 0.01f;
*processor.getAPVTS().getRawParameterValue ("delayStrength") = 0.0f;
*processor.getAPVTS().getRawParameterValue ("reverbStrength") = 0.0f;
*processor.getAPVTS().getRawParameterValue ("delayDryWet") = 0.0f;
*processor.getAPVTS().getRawParameterValue ("reverbDryWet") = 0.0f;
const int releaseBlock = numBlocks / 2;
const auto renderFxTail = [&] () -> double
{
processor.reset();
processor.prepareToPlay (44100.0, 512);
juce::AudioBuffer<float> fxBlock (2, blockSize);
juce::MidiBuffer fxMidi;
double tailSum = 0.0;
int tailSamples = 0;
bool finite = true;
for (int b = 0; b < numBlocks; ++b)
{
fxBlock.clear();
fxMidi.clear();
if (b == 0)
fxMidi.addEvent (juce::MidiMessage::noteOn (1, 60, 0.9f), 0);
else if (b == releaseBlock)
fxMidi.addEvent (juce::MidiMessage::noteOff (1, 60), 0);
processor.processBlock (fxBlock, fxMidi);
if (b > releaseBlock)
for (int c = 0; c < 2; ++c)
for (int i = 0; i < blockSize; ++i)
{
const float s = fxBlock.getSample (c, i);
if (! std::isfinite (s))
finite = false;
tailSum += (double) s * s;
++tailSamples;
}
}
if (! finite)
return -1.0;
return std::sqrt (tailSum / (double) std::max (1, tailSamples));
};
const double dryTail = renderFxTail();
if (dryTail > 1e-3)
{
std::cout << "FAILED: dry tail after release should be silent, got " << dryTail << "\n";
return 1;
}
*processor.getAPVTS().getRawParameterValue ("delayStrength") = 0.8f;
*processor.getAPVTS().getRawParameterValue ("delayTime") = 0.4f;
*processor.getAPVTS().getRawParameterValue ("delaySync") = 1.0f;
*processor.getAPVTS().getRawParameterValue ("delayDryWet") = 0.5f;
*processor.getAPVTS().getRawParameterValue ("reverbStrength") = 0.8f;
*processor.getAPVTS().getRawParameterValue ("reverbRoom") = 0.9f;
*processor.getAPVTS().getRawParameterValue ("reverbDiffusion") = 1.0f;
*processor.getAPVTS().getRawParameterValue ("reverbDryWet") = 0.5f;
const double wetTail = renderFxTail();
if (wetTail < 0.0)
return 1;
if (wetTail < 1e-3)
{
std::cout << "FAILED: delay/reverb tail is inaudible (wet=" << wetTail << ")\n";
return 1;
}
std::cout << "Delay + reverb FX OK (dryTail=" << dryTail << " wetTail=" << wetTail << ")\n";
return 0;
}
int main (int argc, char** argv)
{
if (argc < 2)
{
std::cout << "Usage: MonostepTestHost <plugin.vst3> [out.wav]\n";
return 1;
}
const juce::File pluginFile (argv[1]);
if (testVst3Binary (pluginFile) != 0)
return 1;
if (testProcessorRendering() != 0)
return 1;
std::cout << "ALL TESTS PASSED\n";
return 0;
}