diff --git a/pedalboard/ExternalPlugin.h b/pedalboard/ExternalPlugin.h index 98584e03..0cdecbee 100644 --- a/pedalboard/ExternalPlugin.h +++ b/pedalboard/ExternalPlugin.h @@ -516,7 +516,7 @@ class AbstractExternalPlugin : public Plugin { }; template -class ExternalPlugin : public AbstractExternalPlugin { +class ExternalPlugin : public AbstractExternalPlugin, juce::AudioPlayHead { public: ExternalPlugin( std::string &_pathToPluginFile, @@ -529,6 +529,21 @@ class ExternalPlugin : public AbstractExternalPlugin { // Without this, we get an assert(false) from JUCE at runtime juce::MessageManager::getInstance(); + // Initialize currentPositionInfo to sensible defaults before any plugin + // loading, as plugins may query the playhead during initialization. + currentPositionInfo.bpm = 120.0; + currentPositionInfo.timeSigNumerator = 4; + currentPositionInfo.timeSigDenominator = 4; + currentPositionInfo.timeInSamples = 0; + currentPositionInfo.timeInSeconds = 0.0; + currentPositionInfo.ppqPosition = 0.0; + currentPositionInfo.ppqPositionOfLastBarStart = 0.0; + currentPositionInfo.isPlaying = false; + currentPositionInfo.isRecording = false; + currentPositionInfo.isLooping = false; + currentPositionInfo.editOriginTime = 0.0; + currentPositionInfo.frameRate = juce::AudioPlayHead::fpsUnknown; + pluginFormatManager.addDefaultFormats(); pluginFormatManager.addFormat(new juce::PatchedVST3PluginFormat()); @@ -630,6 +645,10 @@ class ExternalPlugin : public AbstractExternalPlugin { ~ExternalPlugin() { { std::lock_guard lock(EXTERNAL_PLUGIN_MUTEX); + if (pluginInstance) { + pluginInstance->setPlayHead(nullptr); + } + pluginInstance.reset(); NUM_ACTIVE_EXTERNAL_PLUGINS--; @@ -695,7 +714,33 @@ class ExternalPlugin : public AbstractExternalPlugin { } }; - void getPreset(juce::MemoryBlock &dest) const { + void getPreset(juce::MemoryBlock &dest) { + // Flush any pending parameter changes to the plugin's internal state + // by calling processBlock with a small buffer. Some VST3 plugins defer + // parameter state synchronization until the audio processing callback, + // so without this flush, getPreset() may return stale parameter values. + if (pluginInstance) { + int numChannels = std::max(pluginInstance->getTotalNumInputChannels(), + pluginInstance->getTotalNumOutputChannels()); + if (numChannels < 1) + numChannels = 2; + + // Only prepare if the plugin hasn't been prepared yet. + bool wasPrepared = (lastSpec.numChannels != 0); + if (!wasPrepared) { + pluginInstance->prepareToPlay(44100.0, 1); + } + + juce::AudioBuffer flushBuffer(numChannels, 1); + flushBuffer.clear(); + juce::MidiBuffer emptyMidi; + pluginInstance->processBlock(flushBuffer, emptyMidi); + + if (!wasPrepared) { + pluginInstance->releaseResources(); + } + } + // Get the plugin state's .vstpreset representation if possible. GetPresetVisitor visitor(dest); pluginInstance->getExtensions(visitor); @@ -732,6 +777,9 @@ class ExternalPlugin : public AbstractExternalPlugin { { std::lock_guard lock(EXTERNAL_PLUGIN_MUTEX); // Delete the plugin instance itself: + if (pluginInstance) { + pluginInstance->setPlayHead(nullptr); + } pluginInstance.reset(); NUM_ACTIVE_EXTERNAL_PLUGINS--; } @@ -751,6 +799,7 @@ class ExternalPlugin : public AbstractExternalPlugin { loadError.toStdString()); } + pluginInstance->setPlayHead(this); pluginInstance->enableAllBuses(); auto mainInputBus = pluginInstance->getBus(true, 0); @@ -760,6 +809,9 @@ class ExternalPlugin : public AbstractExternalPlugin { auto exception = std::invalid_argument( "Plugin '" + pluginInstance->getName().toStdString() + "' does not produce audio output."); + if (pluginInstance) { + pluginInstance->setPlayHead(nullptr); + } pluginInstance.reset(); throw exception; } @@ -778,6 +830,7 @@ class ExternalPlugin : public AbstractExternalPlugin { pathToPluginFile.toStdString() + ": " + loadError.toStdString()); } + pluginInstance->setPlayHead(this); } } @@ -949,7 +1002,10 @@ class ExternalPlugin : public AbstractExternalPlugin { juce::AudioBuffer audioBuffer(numOutputChannels, bufferSize); audioBuffer.clear(); + currentPositionInfo.isPlaying = true; pluginInstance->processBlock(audioBuffer, emptyNoteBuffer); + currentPositionInfo.isPlaying = false; + currentPositionInfo.timeInSamples += bufferSize; auto noiseFloor = audioBuffer.getMagnitude(0, bufferSize); audioBuffer.clear(); @@ -959,7 +1015,10 @@ class ExternalPlugin : public AbstractExternalPlugin { // the messages in a MidiBuffer get erased every time we call processBlock! { juce::MidiBuffer noteOnBuffer(noteOn); + currentPositionInfo.isPlaying = true; pluginInstance->processBlock(audioBuffer, noteOnBuffer); + currentPositionInfo.isPlaying = false; + currentPositionInfo.timeInSamples += bufferSize; } // Then keep pumping the message thread until we get some louder output: @@ -982,8 +1041,11 @@ class ExternalPlugin : public AbstractExternalPlugin { audioBuffer.clear(); { + currentPositionInfo.isPlaying = true; juce::MidiBuffer noteOnBuffer(noteOn); pluginInstance->processBlock(audioBuffer, noteOnBuffer); + currentPositionInfo.isPlaying = false; + currentPositionInfo.timeInSamples += bufferSize; } if (juce::Time::currentTimeMillis() >= endTime) @@ -995,8 +1057,12 @@ class ExternalPlugin : public AbstractExternalPlugin { audioBuffer.clear(); { juce::MidiBuffer allNotesOffBuffer(allNotesOff); + currentPositionInfo.isPlaying = true; pluginInstance->processBlock(audioBuffer, allNotesOffBuffer); + currentPositionInfo.isPlaying = false; + currentPositionInfo.timeInSamples += bufferSize; } + currentPositionInfo.timeInSamples = 0; pluginInstance->reset(); pluginInstance->releaseResources(); @@ -1114,6 +1180,7 @@ class ExternalPlugin : public AbstractExternalPlugin { // Force prepare() to be called again later by invalidating lastSpec: lastSpec.maximumBlockSize = 0; samplesProvided = 0; + currentPositionInfo.timeInSamples = 0; } } @@ -1139,6 +1206,8 @@ class ExternalPlugin : public AbstractExternalPlugin { pluginInstance->setNonRealtime(true); pluginInstance->prepareToPlay(spec.sampleRate, spec.maximumBlockSize); + currentPositionInfo.timeInSamples = 0; + currentPositionInfo.isPlaying = false; lastSpec = spec; } @@ -1210,8 +1279,11 @@ class ExternalPlugin : public AbstractExternalPlugin { channelPointers.size(), outputBlock.getNumSamples()); + currentPositionInfo.isPlaying = true; pluginInstance->processBlock(audioBuffer, emptyMidiBuffer); + samplesProvided += outputBlock.getNumSamples(); + currentPositionInfo.timeInSamples += outputBlock.getNumSamples(); // To compensate for any latency added by the plugin, // only tell Pedalboard to use the last _n_ samples. @@ -1288,6 +1360,11 @@ class ExternalPlugin : public AbstractExternalPlugin { std::memset((void *)outputArrayPointer, 0, sizeof(float) * numChannels * outputSampleCount); + juce::AudioBuffer emptyBuffer(numChannels, 0); + juce::MidiBuffer emptyMidiBuffer; + + currentPositionInfo.isPlaying = true; + for (unsigned long i = 0; i < outputSampleCount; i += bufferSize) { unsigned long chunkSampleCount = std::min((unsigned long)bufferSize, outputSampleCount - i); @@ -1305,9 +1382,14 @@ class ExternalPlugin : public AbstractExternalPlugin { juce::MidiBuffer midiChunk; midiChunk.addEvents(midiInputBuffer, i, chunkSampleCount, -i); - pluginInstance->processBlock(audioChunk, midiChunk); + currentPositionInfo.timeInSamples += chunkSampleCount; } + + currentPositionInfo.isPlaying = false; + // Pump the processBlock callback to tell the VST that we've stopped + // playing: + pluginInstance->processBlock(emptyBuffer, emptyMidiBuffer); } return outputArray; @@ -1382,6 +1464,11 @@ class ExternalPlugin : public AbstractExternalPlugin { ExternalPluginReloadType reloadType = ExternalPluginReloadType::Unknown; juce::PluginDescription foundPluginDescription; + bool getCurrentPosition(CurrentPositionInfo &result) override { + result = currentPositionInfo; + return true; + } + private: std::unique_ptr createPluginInstance(const juce::PluginDescription &foundPluginDescription, @@ -1416,6 +1503,7 @@ class ExternalPlugin : public AbstractExternalPlugin { juce::String pathToPluginFile; juce::AudioPluginFormatManager pluginFormatManager; std::unique_ptr pluginInstance; + juce::AudioPlayHead::CurrentPositionInfo currentPositionInfo; long samplesProvided = 0; float initializationTimeout = DEFAULT_INITIALIZATION_TIMEOUT_SECONDS; @@ -1675,7 +1763,7 @@ example: a Windows VST3 plugin bundle will not load on Linux or macOS.) py::arg("preset_file_path")) .def_property( "preset_data", - [](const ExternalPlugin &plugin) { + [](ExternalPlugin &plugin) { juce::MemoryBlock presetData; plugin.getPreset(presetData); return py::bytes((const char *)presetData.getData(), diff --git a/tests/test_external_plugins.py b/tests/test_external_plugins.py index 1c570e53..0ccd857f 100644 --- a/tests/test_external_plugins.py +++ b/tests/test_external_plugins.py @@ -233,11 +233,21 @@ def delete_installed_plugins(): if os.environ.get("ENABLE_TESTING_WITH_LOCAL_PLUGINS", False): for plugin_class in pedalboard._AVAILABLE_PLUGIN_CLASSES: for plugin_path in plugin_class.installed_plugins: + if os.environ.get("LOCAL_PLUGIN_NAMES", "").lower() not in plugin_path.lower(): + continue + if any( + x in plugin_path.lower() + for x in os.environ.get("NOT_LOCAL_PLUGIN_NAMES", "").lower().split(",") + ): + continue try: load_test_plugin(plugin_path) AVAILABLE_EFFECT_PLUGINS_IN_TEST_ENVIRONMENT.append(plugin_path) except Exception as e: print(f"Tried to load {plugin_path} for local testing, but failed with: {e}") + import traceback + + traceback.print_exception(e) # Even if the plugin failed to load, add it to # the list of known container plugins if necessary: