#include #include "PluginProcessor.h" #include static double measureRms (const juce::AudioBuffer& 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 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 block (2, blockSize); juce::AudioBuffer 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 [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; }