Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 92 additions & 4 deletions pedalboard/ExternalPlugin.h
Original file line number Diff line number Diff line change
Expand Up @@ -516,7 +516,7 @@ class AbstractExternalPlugin : public Plugin {
};

template <typename ExternalPluginType>
class ExternalPlugin : public AbstractExternalPlugin {
class ExternalPlugin : public AbstractExternalPlugin, juce::AudioPlayHead {
public:
ExternalPlugin(
std::string &_pathToPluginFile,
Expand All @@ -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());

Expand Down Expand Up @@ -630,6 +645,10 @@ class ExternalPlugin : public AbstractExternalPlugin {
~ExternalPlugin() {
{
std::lock_guard<std::mutex> lock(EXTERNAL_PLUGIN_MUTEX);
if (pluginInstance) {
pluginInstance->setPlayHead(nullptr);
}

pluginInstance.reset();
NUM_ACTIVE_EXTERNAL_PLUGINS--;

Expand Down Expand Up @@ -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<float> 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);
Expand Down Expand Up @@ -732,6 +777,9 @@ class ExternalPlugin : public AbstractExternalPlugin {
{
std::lock_guard<std::mutex> lock(EXTERNAL_PLUGIN_MUTEX);
// Delete the plugin instance itself:
if (pluginInstance) {
pluginInstance->setPlayHead(nullptr);
}
pluginInstance.reset();
NUM_ACTIVE_EXTERNAL_PLUGINS--;
}
Expand All @@ -751,6 +799,7 @@ class ExternalPlugin : public AbstractExternalPlugin {
loadError.toStdString());
}

pluginInstance->setPlayHead(this);
pluginInstance->enableAllBuses();

auto mainInputBus = pluginInstance->getBus(true, 0);
Expand All @@ -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;
}
Expand All @@ -778,6 +830,7 @@ class ExternalPlugin : public AbstractExternalPlugin {
pathToPluginFile.toStdString() + ": " +
loadError.toStdString());
}
pluginInstance->setPlayHead(this);
}
}

Expand Down Expand Up @@ -949,7 +1002,10 @@ class ExternalPlugin : public AbstractExternalPlugin {
juce::AudioBuffer<float> 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();
Expand All @@ -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:
Expand All @@ -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)
Expand All @@ -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();

Expand Down Expand Up @@ -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;
}
}

Expand All @@ -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;
}
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -1288,6 +1360,11 @@ class ExternalPlugin : public AbstractExternalPlugin {
std::memset((void *)outputArrayPointer, 0,
sizeof(float) * numChannels * outputSampleCount);

juce::AudioBuffer<float> 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);
Expand All @@ -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;
Expand Down Expand Up @@ -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<juce::AudioPluginInstance>
createPluginInstance(const juce::PluginDescription &foundPluginDescription,
Expand Down Expand Up @@ -1416,6 +1503,7 @@ class ExternalPlugin : public AbstractExternalPlugin {
juce::String pathToPluginFile;
juce::AudioPluginFormatManager pluginFormatManager;
std::unique_ptr<juce::AudioPluginInstance> pluginInstance;
juce::AudioPlayHead::CurrentPositionInfo currentPositionInfo;

long samplesProvided = 0;
float initializationTimeout = DEFAULT_INITIALIZATION_TIMEOUT_SECONDS;
Expand Down Expand Up @@ -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<juce::PatchedVST3PluginFormat> &plugin) {
[](ExternalPlugin<juce::PatchedVST3PluginFormat> &plugin) {
juce::MemoryBlock presetData;
plugin.getPreset(presetData);
return py::bytes((const char *)presetData.getData(),
Expand Down
10 changes: 10 additions & 0 deletions tests/test_external_plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading