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