mirror of
https://codeberg.org/armin/monostep.git
synced 2026-09-01 12:20:46 +02:00
235 lines
6.6 KiB
C++
235 lines
6.6 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)
|
|
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);
|
|
|
|
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;
|
|
}
|
|
|
|
// 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 << "\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";
|
|
|
|
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;
|
|
}
|