#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 || 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 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); 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& accented) -> double { for (int i = 0; i < monostep::numSteps; ++i) processor.setStepAccent (i, accented (i)); processor.reset(); processor.prepareToPlay (44100.0, 512); juce::AudioBuffer 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 [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; }