From 00ca23ac86441f816ad494e48d4157f662630200 Mon Sep 17 00:00:00 2001 From: Rob Clouth Date: Thu, 23 Jun 2022 16:28:17 +0200 Subject: [PATCH 1/2] Added midi messages args --- pedalboard/ExternalPlugin.h | 12 +++---- pedalboard/JucePlugin.h | 2 +- pedalboard/MidiUtils.h | 34 +++++++++++++++++++ pedalboard/Plugin.h | 2 +- pedalboard/RubberbandPlugin.h | 2 +- pedalboard/plugin_templates/FixedBlockSize.h | 12 +++---- pedalboard/plugin_templates/ForceMono.h | 8 ++--- .../plugin_templates/PrimeWithSilence.h | 8 ++--- pedalboard/plugin_templates/Resample.h | 6 ++-- pedalboard/plugins/AddLatency.h | 2 +- pedalboard/plugins/Bitcrush.h | 2 +- pedalboard/plugins/Chain.h | 14 ++++---- pedalboard/plugins/Delay.h | 2 +- pedalboard/plugins/GSMFullRateCompressor.h | 2 +- pedalboard/plugins/Invert.h | 2 +- pedalboard/plugins/MP3Compressor.h | 2 +- pedalboard/plugins/Mix.h | 4 +-- pedalboard/process.h | 16 ++++++--- pedalboard/python_bindings.cpp | 18 +++++----- 19 files changed, 98 insertions(+), 52 deletions(-) create mode 100644 pedalboard/MidiUtils.h diff --git a/pedalboard/ExternalPlugin.h b/pedalboard/ExternalPlugin.h index 08990a16..09ee8f43 100644 --- a/pedalboard/ExternalPlugin.h +++ b/pedalboard/ExternalPlugin.h @@ -643,7 +643,7 @@ template class ExternalPlugin : public Plugin { { juce::dsp::AudioBlock block(audioBuffer); juce::dsp::ProcessContextReplacing context(block); - process(context); + process(context, emptyMidiBuffer); } } @@ -666,7 +666,7 @@ template class ExternalPlugin : public Plugin { } juce::dsp::AudioBlock block(audioBuffer); juce::dsp::ProcessContextReplacing context(block); - process(context); + process(context, emptyMidiBuffer); } auto signalVolume = audioBuffer.getMagnitude(0, bufferSize); @@ -679,7 +679,7 @@ template class ExternalPlugin : public Plugin { { juce::dsp::AudioBlock block(audioBuffer); juce::dsp::ProcessContextReplacing context(block); - process(context); + process(context, emptyMidiBuffer); } auto magnitudeOfSilentBuffer = audioBuffer.getMagnitude(0, bufferSize); @@ -753,10 +753,10 @@ template class ExternalPlugin : public Plugin { } int process( - const juce::dsp::ProcessContextReplacing &context) override { + const juce::dsp::ProcessContextReplacing &context, juce::MidiBuffer &midiBuffer) override { if (pluginInstance) { - juce::MidiBuffer emptyMidiBuffer; + ; if (context.usesSeparateInputAndOutputBlocks()) { throw std::runtime_error("Not implemented yet - " "no support for using separate " @@ -813,7 +813,7 @@ template class ExternalPlugin : public Plugin { channelPointers.size(), outputBlock.getNumSamples()); - pluginInstance->processBlock(audioBuffer, emptyMidiBuffer); + pluginInstance->processBlock(audioBuffer, midiBuffer); samplesProvided += outputBlock.getNumSamples(); // To compensate for any latency added by the plugin, diff --git a/pedalboard/JucePlugin.h b/pedalboard/JucePlugin.h index 8c2526aa..73748e90 100644 --- a/pedalboard/JucePlugin.h +++ b/pedalboard/JucePlugin.h @@ -59,7 +59,7 @@ template class JucePlugin : public Plugin { } int process( - const juce::dsp::ProcessContextReplacing &context) override { + const juce::dsp::ProcessContextReplacing &context, juce::MidiBuffer &midiBuffer) override { dspBlock.process(context); return context.getOutputBlock().getNumSamples(); } diff --git a/pedalboard/MidiUtils.h b/pedalboard/MidiUtils.h new file mode 100644 index 00000000..3f7f6b98 --- /dev/null +++ b/pedalboard/MidiUtils.h @@ -0,0 +1,34 @@ +/* + * pedalboard + * Copyright 2021 Spotify AB + * + * Licensed under the GNU Public License, Version 3.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.gnu.org/licenses/gpl-3.0.html + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once +#include "JuceHeader.h" + +#include +#include + +namespace Pedalboard { + +juce::MidiBuffer +copyPyArrayIntoJuceMidiBuffer(const py::array_t midiMessages) { + // Numpy/Librosa convention is (num_samples, num_channels) + py::buffer_info inputInfo = midiMessages.request(); + + return juce::MidiBuffer(); +} + +} // namespace Pedalboard \ No newline at end of file diff --git a/pedalboard/Plugin.h b/pedalboard/Plugin.h index 9ad7d7dd..ce2b6df5 100644 --- a/pedalboard/Plugin.h +++ b/pedalboard/Plugin.h @@ -46,7 +46,7 @@ class Plugin { * (i.e.: they should come last). */ virtual int - process(const juce::dsp::ProcessContextReplacing &context) = 0; + process(const juce::dsp::ProcessContextReplacing &context, juce::MidiBuffer &midiBuffer) = 0; /** * Reset this plugin's state, clearing any internal buffers or delay lines. diff --git a/pedalboard/RubberbandPlugin.h b/pedalboard/RubberbandPlugin.h index ddb3bbb2..c87db039 100644 --- a/pedalboard/RubberbandPlugin.h +++ b/pedalboard/RubberbandPlugin.h @@ -49,7 +49,7 @@ class RubberbandPlugin : public Plugin { } int process( - const juce::dsp::ProcessContextReplacing &context) override final { + const juce::dsp::ProcessContextReplacing &context, juce::MidiBuffer &midiBuffer) override final { if (rbPtr) { auto inBlock = context.getInputBlock(); auto outBlock = context.getOutputBlock(); diff --git a/pedalboard/plugin_templates/FixedBlockSize.h b/pedalboard/plugin_templates/FixedBlockSize.h index 16ca0159..336a021e 100644 --- a/pedalboard/plugin_templates/FixedBlockSize.h +++ b/pedalboard/plugin_templates/FixedBlockSize.h @@ -65,7 +65,7 @@ class FixedBlockSize : public Plugin { } virtual int - process(const juce::dsp::ProcessContextReplacing &context) { + process(const juce::dsp::ProcessContextReplacing &context, juce::MidiBuffer &midiBuffer) { auto ioBlock = context.getOutputBlock(); if (lastSpec.maximumBlockSize % blockSize == 0) { @@ -81,7 +81,7 @@ class FixedBlockSize : public Plugin { juce::dsp::AudioBlock subBlock = ioBlock.getSubBlock(i, blockSize); juce::dsp::ProcessContextReplacing subContext(subBlock); - int samplesOutputThisBlock = plugin.process(subContext); + int samplesOutputThisBlock = plugin.process(subContext, midiBuffer); if (samplesOutput > 0 && samplesOutputThisBlock < blockSize) { throw std::runtime_error( @@ -106,7 +106,7 @@ class FixedBlockSize : public Plugin { subBlock.copyFrom(ioBlock.getSubBlock(offset, remainderInSamples)); juce::dsp::ProcessContextReplacing subContext(subBlock); - int samplesOutputThisBlock = plugin.process(subContext); + int samplesOutputThisBlock = plugin.process(subContext, midiBuffer); // Copy the output back into ioBlock, right-aligned: ioBlock @@ -164,7 +164,7 @@ class FixedBlockSize : public Plugin { juce::dsp::AudioBlock subBlock = inputBlock.getSubBlock(i, blockSize); juce::dsp::ProcessContextReplacing subContext(subBlock); - int samplesProcessedThisBlock = plugin.process(subContext); + int samplesProcessedThisBlock = plugin.process(subContext, midiBuffer); inputSamplesConsumed += blockSize; if (samplesProcessedThisBlock > 0) { @@ -272,12 +272,12 @@ class ExpectsFixedBlockSize : public AddLatency { } virtual int - process(const juce::dsp::ProcessContextReplacing &context) { + process(const juce::dsp::ProcessContextReplacing &context, juce::MidiBuffer &midiBuffer) { if (context.getInputBlock().getNumSamples() != expectedBlockSize) { throw std::runtime_error("Expected maximum block size of exactly " + std::to_string(expectedBlockSize) + "!"); } - return AddLatency::process(context); + return AddLatency::process(context, midiBuffer); } virtual void reset() { AddLatency::reset(); } diff --git a/pedalboard/plugin_templates/ForceMono.h b/pedalboard/plugin_templates/ForceMono.h index 70ba24aa..b03114be 100644 --- a/pedalboard/plugin_templates/ForceMono.h +++ b/pedalboard/plugin_templates/ForceMono.h @@ -41,7 +41,7 @@ class ForceMono : public Plugin { } virtual int - process(const juce::dsp::ProcessContextReplacing &context) { + process(const juce::dsp::ProcessContextReplacing &context, juce::MidiBuffer &midiBuffer) { auto ioBlock = context.getOutputBlock(); // Mix all channels to mono first, if necessary. @@ -62,7 +62,7 @@ class ForceMono : public Plugin { juce::dsp::AudioBlock monoBlock = ioBlock.getSingleChannelBlock(0); juce::dsp::ProcessContextReplacing subContext(monoBlock); - int samplesProcessed = plugin.process(monoBlock); + int samplesProcessed = plugin.process(monoBlock, midiBuffer); // Copy the mono signal back out to all other channels: if (ioBlock.getNumChannels() > 1) { @@ -98,11 +98,11 @@ class ExpectsMono : public AddLatency { } virtual int - process(const juce::dsp::ProcessContextReplacing &context) { + process(const juce::dsp::ProcessContextReplacing &context, juce::MidiBuffer &midiBuffer) { if (context.getInputBlock().getNumChannels() != 1) { throw std::runtime_error("Expected mono input!"); } - return AddLatency::process(context); + return AddLatency::process(context, midiBuffer); } }; diff --git a/pedalboard/plugin_templates/PrimeWithSilence.h b/pedalboard/plugin_templates/PrimeWithSilence.h index 603d5293..38a5525d 100644 --- a/pedalboard/plugin_templates/PrimeWithSilence.h +++ b/pedalboard/plugin_templates/PrimeWithSilence.h @@ -54,11 +54,11 @@ class PrimeWithSilence } virtual int - process(const juce::dsp::ProcessContextReplacing &context) override { + process(const juce::dsp::ProcessContextReplacing &context, juce::MidiBuffer &midiBuffer) override { this->getDSP().process(context); // Context now has a delayed signal in it: - int samplesProcessed = plugin.process(context); + int samplesProcessed = plugin.process(context, midiBuffer); samplesOutput += samplesProcessed; return std::max( @@ -103,7 +103,7 @@ class ExpectsToBePrimed : public AddLatency { } virtual int - process(const juce::dsp::ProcessContextReplacing &context) { + process(const juce::dsp::ProcessContextReplacing &context, juce::MidiBuffer &midiBuffer) { auto inputBlock = context.getInputBlock(); for (int i = 0; i < inputBlock.getNumSamples(); i++) { @@ -130,7 +130,7 @@ class ExpectsToBePrimed : public AddLatency { seenSilentSamples++; } - return AddLatency::process(context); + return AddLatency::process(context, midiBuffer); } virtual void reset() { diff --git a/pedalboard/plugin_templates/Resample.h b/pedalboard/plugin_templates/Resample.h index 4c6e1aba..da2e26d3 100644 --- a/pedalboard/plugin_templates/Resample.h +++ b/pedalboard/plugin_templates/Resample.h @@ -162,7 +162,7 @@ template class Passthrough : public Plugin { virtual void prepare(const juce::dsp::ProcessSpec &spec) {} virtual int - process(const juce::dsp::ProcessContextReplacing &context) { + process(const juce::dsp::ProcessContextReplacing &context, juce::MidiBuffer &midiBuffer) { return context.getInputBlock().getNumSamples(); } @@ -235,7 +235,7 @@ class Resample : public Plugin { plugin.prepare(subSpec); } - int process(const juce::dsp::ProcessContextReplacing &context) + int process(const juce::dsp::ProcessContextReplacing &context, juce::MidiBuffer &midiBuffer) override final { auto ioBlock = context.getOutputBlock(); @@ -322,7 +322,7 @@ class Resample : public Plugin { processedSamplesInResampledBuffer, cleanSamplesToProcess); juce::dsp::ProcessContextReplacing subContext(subBlock); - int resampledSamplesOutput = plugin.process(subContext); + int resampledSamplesOutput = plugin.process(subContext, midiBuffer); if (resampledSamplesOutput < cleanSamplesToProcess) { // Move all remaining samples to the left of the buffer: diff --git a/pedalboard/plugins/AddLatency.h b/pedalboard/plugins/AddLatency.h index 86bf7199..2e89c4cf 100644 --- a/pedalboard/plugins/AddLatency.h +++ b/pedalboard/plugins/AddLatency.h @@ -37,7 +37,7 @@ class AddLatency : public JucePlugin &context) override { + process(const juce::dsp::ProcessContextReplacing &context, juce::MidiBuffer &midiBuffer) override { getDSP().process(context); int blockSize = context.getInputBlock().getNumSamples(); samplesProvided += blockSize; diff --git a/pedalboard/plugins/Bitcrush.h b/pedalboard/plugins/Bitcrush.h index 022445cd..594a920f 100644 --- a/pedalboard/plugins/Bitcrush.h +++ b/pedalboard/plugins/Bitcrush.h @@ -36,7 +36,7 @@ template class Bitcrush : public Plugin { virtual void reset() override {} virtual int process( - const juce::dsp::ProcessContextReplacing &context) override { + const juce::dsp::ProcessContextReplacing &context, juce::MidiBuffer &midiBuffer) override { auto block = context.getOutputBlock(); block.multiplyBy(scaleFactor); diff --git a/pedalboard/plugins/Chain.h b/pedalboard/plugins/Chain.h index ec1add10..61149026 100644 --- a/pedalboard/plugins/Chain.h +++ b/pedalboard/plugins/Chain.h @@ -40,7 +40,7 @@ class Chain : public PluginContainer { } virtual int - process(const juce::dsp::ProcessContextReplacing &context) { + process(const juce::dsp::ProcessContextReplacing &context, juce::MidiBuffer &midiBuffer) { // assuming process context replacing auto ioBlock = context.getOutputBlock(); @@ -52,7 +52,7 @@ class Chain : public PluginContainer { juce::AudioBuffer ioBuffer(channels, ioBlock.getNumChannels(), ioBlock.getNumSamples()); - return ::Pedalboard::process(ioBuffer, lastSpec, plugins, false); + return ::Pedalboard::process(ioBuffer, midiBuffer, lastSpec, plugins, false); } virtual void reset() { @@ -104,31 +104,33 @@ inline void init_chain(py::module &m) { "process", [](std::shared_ptr self, const py::array_t inputArray, + const py::array_t midiMessages, double sampleRate, unsigned int bufferSize, bool reset) { - return process(inputArray, sampleRate, self->getPlugins(), + return process(inputArray, midiMessages, sampleRate, self->getPlugins(), bufferSize, reset); }, "Run a 32-bit floating point audio buffer through this plugin." "(Note: if calling this multiple times with multiple plugins, " "consider using pedalboard.process(...) instead.)", - py::arg("input_array"), py::arg("sample_rate"), + py::arg("input_array"), py::arg("midi_messages"), py::arg("sample_rate"), py::arg("buffer_size") = DEFAULT_BUFFER_SIZE, py::arg("reset") = true) .def( "process", [](std::shared_ptr self, const py::array_t inputArray, + const py::array_t midiMessages, double sampleRate, unsigned int bufferSize, bool reset) { const py::array_t float32InputArray = inputArray.attr("astype")("float32"); - return process(float32InputArray, sampleRate, self->getPlugins(), + return process(float32InputArray, midiMessages, sampleRate, self->getPlugins(), bufferSize, reset); }, "Run a 64-bit floating point audio buffer through this plugin." "(Note: if calling this multiple times with multiple plugins, " "consider using pedalboard.process(...) instead.) The buffer " "will be converted to 32-bit for processing.", - py::arg("input_array"), py::arg("sample_rate"), + py::arg("input_array"), py::arg("midi_messages"), py::arg("sample_rate"), py::arg("buffer_size") = DEFAULT_BUFFER_SIZE, py::arg("reset") = true); } diff --git a/pedalboard/plugins/Delay.h b/pedalboard/plugins/Delay.h index f8010d73..a1ac7ab2 100644 --- a/pedalboard/plugins/Delay.h +++ b/pedalboard/plugins/Delay.h @@ -61,7 +61,7 @@ class Delay : public JucePlugingetDSP().reset(); } virtual int process( - const juce::dsp::ProcessContextReplacing &context) override { + const juce::dsp::ProcessContextReplacing &context, juce::MidiBuffer &midiBuffer) override { // TODO: More advanced mixing rules than "linear?" SampleType dryVolume = 1.0f - getMix(); SampleType wetVolume = getMix(); diff --git a/pedalboard/plugins/GSMFullRateCompressor.h b/pedalboard/plugins/GSMFullRateCompressor.h index 5bf54b89..92c8c7c4 100644 --- a/pedalboard/plugins/GSMFullRateCompressor.h +++ b/pedalboard/plugins/GSMFullRateCompressor.h @@ -81,7 +81,7 @@ class GSMFullRateCompressorInternal : public Plugin { } int process( - const juce::dsp::ProcessContextReplacing &context) override final { + const juce::dsp::ProcessContextReplacing &context, juce::MidiBuffer &midiBuffer) override final { auto ioBlock = context.getOutputBlock(); if (ioBlock.getNumSamples() != GSM_FRAME_SIZE_SAMPLES) { diff --git a/pedalboard/plugins/Invert.h b/pedalboard/plugins/Invert.h index 2965eb55..174e5592 100644 --- a/pedalboard/plugins/Invert.h +++ b/pedalboard/plugins/Invert.h @@ -25,7 +25,7 @@ namespace py = pybind11; namespace Pedalboard { template class Invert : public Plugin { virtual void prepare(const juce::dsp::ProcessSpec &spec) override {} - int process(const juce::dsp::ProcessContextReplacing &context) + int process(const juce::dsp::ProcessContextReplacing &context, juce::MidiBuffer &midiBuffer) override final { context.getOutputBlock().negate(); return context.getOutputBlock().getNumSamples(); diff --git a/pedalboard/plugins/MP3Compressor.h b/pedalboard/plugins/MP3Compressor.h index d1a8ff26..498822ea 100644 --- a/pedalboard/plugins/MP3Compressor.h +++ b/pedalboard/plugins/MP3Compressor.h @@ -253,7 +253,7 @@ class MP3Compressor : public Plugin { } int process( - const juce::dsp::ProcessContextReplacing &context) override final { + const juce::dsp::ProcessContextReplacing &context, juce::MidiBuffer &midiBuffer) override final { auto ioBlock = context.getOutputBlock(); if (mp3BufferBytesFilled > 0) { diff --git a/pedalboard/plugins/Mix.h b/pedalboard/plugins/Mix.h index 9efc830d..31b9ba45 100644 --- a/pedalboard/plugins/Mix.h +++ b/pedalboard/plugins/Mix.h @@ -47,7 +47,7 @@ class Mix : public PluginContainer { } virtual int - process(const juce::dsp::ProcessContextReplacing &context) { + process(const juce::dsp::ProcessContextReplacing &context, juce::MidiBuffer &midiBuffer) { auto ioBlock = context.getOutputBlock(); for (int i = 0; i < plugins.size(); i++) { @@ -75,7 +75,7 @@ class Mix : public PluginContainer { channelPointers, buffer.getNumChannels(), ioBlock.getNumSamples()); juce::dsp::ProcessContextReplacing subContext(subBlock); - int samplesRendered = plugin->process(subContext); + int samplesRendered = plugin->process(subContext, midiBuffer); samplesAvailablePerPlugin[i] += samplesRendered; if (samplesRendered < subBlock.getNumSamples()) { diff --git a/pedalboard/process.h b/pedalboard/process.h index 85586732..ae036f35 100644 --- a/pedalboard/process.h +++ b/pedalboard/process.h @@ -22,6 +22,8 @@ #include #include "BufferUtils.h" +#include "MidiUtils.h" + #include "Plugin.h" namespace py = pybind11; @@ -34,11 +36,12 @@ namespace Pedalboard { template py::array_t process(const py::array_t inputArray, + const py::array_t midiMessages, double sampleRate, const std::vector> plugins, unsigned int bufferSize, bool reset) { const py::array_t float32InputArray = inputArray.attr("astype")("float32"); - return process(float32InputArray, sampleRate, plugins, bufferSize, reset); + return process(float32InputArray, midiMessages, sampleRate, plugins, bufferSize, reset); } /** @@ -47,14 +50,16 @@ process(const py::array_t inputArray, template py::array_t processSingle(const py::array_t inputArray, + const py::array_t midiMessages, double sampleRate, std::shared_ptr plugin, unsigned int bufferSize, bool reset) { std::vector> plugins{plugin}; - return process(inputArray, sampleRate, plugins, bufferSize, + return process(inputArray, midiMessages, sampleRate, plugins, bufferSize, reset); } inline int process(juce::AudioBuffer &ioBuffer, + juce::MidiBuffer& midiBuffer, juce::dsp::ProcessSpec spec, const std::vector> &plugins, bool isProbablyLastProcessCall) { @@ -103,7 +108,7 @@ inline int process(juce::AudioBuffer &ioBuffer, blockStart, blockSize); juce::dsp::ProcessContextReplacing context(ioBlock); - int outputSamples = plugin->process(context); + int outputSamples = plugin->process(context, midiBuffer); if (outputSamples < 0) { throw std::runtime_error( "A plugin returned a negative number of output samples! " @@ -187,10 +192,13 @@ inline int process(juce::AudioBuffer &ioBuffer, template <> py::array_t process(const py::array_t inputArray, + const py::array_t midiMessages, double sampleRate, std::vector> plugins, unsigned int bufferSize, bool reset) { const ChannelLayout inputChannelLayout = detectChannelLayout(inputArray); juce::AudioBuffer ioBuffer = copyPyArrayIntoJuceBuffer(inputArray); + juce::MidiBuffer midiBuffer = copyPyArrayIntoJuceMidiBuffer(midiMessages); + int totalOutputLatencySamples; { @@ -258,7 +266,7 @@ process(const py::array_t inputArray, } // Actually run the process method of all plugins. - int samplesReturned = process(ioBuffer, spec, plugins, reset); + int samplesReturned = process(ioBuffer, midiBuffer, spec, plugins, reset); totalOutputLatencySamples = ioBuffer.getNumSamples() - samplesReturned; } diff --git a/pedalboard/python_bindings.cpp b/pedalboard/python_bindings.cpp index f33aacad..3e3a4031 100644 --- a/pedalboard/python_bindings.cpp +++ b/pedalboard/python_bindings.cpp @@ -71,20 +71,20 @@ PYBIND11_MODULE(pedalboard_native, m) { m.def("process", process, "Run a 32-bit floating point audio buffer through a list of Pedalboard " "plugins.", - py::arg("input_array"), py::arg("sample_rate"), py::arg("plugins"), + py::arg("input_array"), py::arg("midi_messages"), py::arg("sample_rate"), py::arg("plugins"), py::arg("buffer_size") = DEFAULT_BUFFER_SIZE, py::arg("reset") = true); m.def("process", process, "Run a 64-bit floating point audio buffer through a list of Pedalboard " "plugins. The buffer will be converted to 32-bit for processing.", - py::arg("input_array"), py::arg("sample_rate"), py::arg("plugins"), + py::arg("input_array"), py::arg("midi_messages"), py::arg("sample_rate"), py::arg("plugins"), py::arg("buffer_size") = DEFAULT_BUFFER_SIZE, py::arg("reset") = true); m.def("process", processSingle, "Run a 32-bit floating point audio buffer through a single Pedalboard " "plugin. (Note: if calling this multiple times with multiple plugins, " "consider passing a list of plugins instead.)", - py::arg("input_array"), py::arg("sample_rate"), py::arg("plugin"), + py::arg("input_array"), py::arg("midi_messages"), py::arg("sample_rate"), py::arg("plugin"), py::arg("buffer_size") = DEFAULT_BUFFER_SIZE, py::arg("reset") = true); m.def("process", processSingle, @@ -92,7 +92,7 @@ PYBIND11_MODULE(pedalboard_native, m) { "plugin. (Note: if calling this multiple times with multiple plugins, " "consider passing a list of plugins instead.) The buffer will be " "converted to 32-bit for processing.", - py::arg("input_array"), py::arg("sample_rate"), py::arg("plugin"), + py::arg("input_array"), py::arg("midi_messages"), py::arg("sample_rate"), py::arg("plugin"), py::arg("buffer_size") = DEFAULT_BUFFER_SIZE, py::arg("reset") = true); auto plugin = @@ -119,14 +119,15 @@ PYBIND11_MODULE(pedalboard_native, m) { "process", [](std::shared_ptr self, const py::array_t inputArray, + const py::array_t midiMessages, double sampleRate, unsigned int bufferSize, bool reset) { - return process(inputArray, sampleRate, {self}, bufferSize, + return process(inputArray, midiMessages, sampleRate, {self}, bufferSize, reset); }, "Run a 32-bit floating point audio buffer through this plugin." "(Note: if calling this multiple times with multiple plugins, " "consider using pedalboard.process(...) instead.)", - py::arg("input_array"), py::arg("sample_rate"), + py::arg("input_array"), py::arg("midi_messages"), py::arg("sample_rate"), py::arg("buffer_size") = DEFAULT_BUFFER_SIZE, py::arg("reset") = true) @@ -134,17 +135,18 @@ PYBIND11_MODULE(pedalboard_native, m) { "process", [](std::shared_ptr self, const py::array_t inputArray, + const py::array_t midiMessages, double sampleRate, unsigned int bufferSize, bool reset) { const py::array_t float32InputArray = inputArray.attr("astype")("float32"); - return process(float32InputArray, sampleRate, {self}, + return process(float32InputArray, midiMessages, sampleRate, {self}, bufferSize, reset); }, "Run a 64-bit floating point audio buffer through this plugin." "(Note: if calling this multiple times with multiple plugins, " "consider using pedalboard.process(...) instead.) The buffer " "will be converted to 32-bit for processing.", - py::arg("input_array"), py::arg("sample_rate"), + py::arg("input_array"), py::arg("midi_messages"), py::arg("sample_rate"), py::arg("buffer_size") = DEFAULT_BUFFER_SIZE, py::arg("reset") = true); plugin.attr("__call__") = plugin.attr("process"); From 2559a400cc3dca57fb28536cade3ea75429bd620 Mon Sep 17 00:00:00 2001 From: Rob Clouth Date: Thu, 23 Jun 2022 19:30:02 +0200 Subject: [PATCH 2/2] Add note on and note off --- pedalboard/MidiUtils.h | 27 ++++++++++++++--- pedalboard/midi/__init__.py | 1 + pedalboard/midi/midi_messages.py | 7 +++++ pedalboard/plugins/Chain.h | 4 +-- pedalboard/process.h | 50 ++++++++++++++++++++++++++++---- pedalboard/python_bindings.cpp | 4 +-- requirements.txt | 3 +- 7 files changed, 81 insertions(+), 15 deletions(-) create mode 100644 pedalboard/midi/__init__.py create mode 100644 pedalboard/midi/midi_messages.py diff --git a/pedalboard/MidiUtils.h b/pedalboard/MidiUtils.h index 3f7f6b98..d82f4dec 100644 --- a/pedalboard/MidiUtils.h +++ b/pedalboard/MidiUtils.h @@ -23,12 +23,31 @@ namespace Pedalboard { -juce::MidiBuffer -copyPyArrayIntoJuceMidiBuffer(const py::array_t midiMessages) { - // Numpy/Librosa convention is (num_samples, num_channels) +juce::MidiMessageSequence +copyPyArrayIntoJuceMidiMessageSequence(const py::array_t midiMessages) { py::buffer_info inputInfo = midiMessages.request(); - return juce::MidiBuffer(); + if(inputInfo.shape[1] != 4) { + throw std::runtime_error("Each element must have length 4 (got " + + std::to_string(inputInfo.shape[1]) + ")."); + } + + juce::MidiMessageSequence midiSequence; + + float *data = static_cast(inputInfo.ptr); + + for(int i = 0; i < inputInfo.shape[0]; i++) { + int byte1 = (int) data[i * 4 + 0]; + int byte2 = (int) data[i * 4 + 1]; + int byte3 = (int) data[i * 4 + 2]; + float timeSeconds = data[i * 4 + 3]; + + midiSequence.addEvent(juce::MidiMessage(byte1, byte2, byte3), timeSeconds); + + DBG( byte1 ); + } + + return midiSequence; } } // namespace Pedalboard \ No newline at end of file diff --git a/pedalboard/midi/__init__.py b/pedalboard/midi/__init__.py new file mode 100644 index 00000000..4a29b452 --- /dev/null +++ b/pedalboard/midi/__init__.py @@ -0,0 +1 @@ +from .midi_messages import * \ No newline at end of file diff --git a/pedalboard/midi/midi_messages.py b/pedalboard/midi/midi_messages.py new file mode 100644 index 00000000..a83b1d6e --- /dev/null +++ b/pedalboard/midi/midi_messages.py @@ -0,0 +1,7 @@ +import mido + +def note_on(time:float, note: int, velocity: int, channel: int = 1): + return mido.Message('note_on', note=note, velocity=velocity, channel=channel).bytes() + [time] + +def note_off(time:float, note: int, velocity: int,channel: int = 1): + return mido.Message('note_off', note=note, velocity=velocity, channel=channel).bytes() + [time] \ No newline at end of file diff --git a/pedalboard/plugins/Chain.h b/pedalboard/plugins/Chain.h index 61149026..31695f02 100644 --- a/pedalboard/plugins/Chain.h +++ b/pedalboard/plugins/Chain.h @@ -104,7 +104,7 @@ inline void init_chain(py::module &m) { "process", [](std::shared_ptr self, const py::array_t inputArray, - const py::array_t midiMessages, + const py::array_t midiMessages, double sampleRate, unsigned int bufferSize, bool reset) { return process(inputArray, midiMessages, sampleRate, self->getPlugins(), bufferSize, reset); @@ -119,7 +119,7 @@ inline void init_chain(py::module &m) { "process", [](std::shared_ptr self, const py::array_t inputArray, - const py::array_t midiMessages, + const py::array_t midiMessages, double sampleRate, unsigned int bufferSize, bool reset) { const py::array_t float32InputArray = inputArray.attr("astype")("float32"); diff --git a/pedalboard/process.h b/pedalboard/process.h index ae036f35..19a945f5 100644 --- a/pedalboard/process.h +++ b/pedalboard/process.h @@ -36,7 +36,7 @@ namespace Pedalboard { template py::array_t process(const py::array_t inputArray, - const py::array_t midiMessages, + const py::array_t midiMessages, double sampleRate, const std::vector> plugins, unsigned int bufferSize, bool reset) { const py::array_t float32InputArray = @@ -50,7 +50,7 @@ process(const py::array_t inputArray, template py::array_t processSingle(const py::array_t inputArray, - const py::array_t midiMessages, + const py::array_t midiMessages, double sampleRate, std::shared_ptr plugin, unsigned int bufferSize, bool reset) { std::vector> plugins{plugin}; @@ -58,8 +58,9 @@ processSingle(const py::array_t inputArray, reset); } + inline int process(juce::AudioBuffer &ioBuffer, - juce::MidiBuffer& midiBuffer, + juce::MidiMessageSequence& midiSequence, juce::dsp::ProcessSpec spec, const std::vector> &plugins, bool isProbablyLastProcessCall) { @@ -108,6 +109,24 @@ inline int process(juce::AudioBuffer &ioBuffer, blockStart, blockSize); juce::dsp::ProcessContextReplacing context(ioBlock); + juce::MidiBuffer midiBuffer; + auto startTime = blockStart / spec.sampleRate; + auto endTime = blockEnd / spec.sampleRate; + + for (auto i = midiSequence.getNextIndexAtTime(startTime); i < midiSequence.getNumEvents(); i++) + { + juce::MidiMessageSequence::MidiEventHolder *event = midiSequence.getEventPointer(i); + + if (event->message.getTimeStamp() >= startTime && event->message.getTimeStamp() < endTime) + { + auto samplePosition = juce::roundToInt((event->message.getTimeStamp() - startTime) * spec.sampleRate); + midiBuffer.addEvent(event->message, samplePosition); + } else { + break; + } + } + + int outputSamples = plugin->process(context, midiBuffer); if (outputSamples < 0) { throw std::runtime_error( @@ -184,6 +203,25 @@ inline int process(juce::AudioBuffer &ioBuffer, return intendedOutputBufferSize - totalOutputLatencySamples; } +inline int process(juce::AudioBuffer &ioBuffer, + juce::MidiBuffer& midiBuffer, + juce::dsp::ProcessSpec spec, + const std::vector> &plugins, + bool isProbablyLastProcessCall) { + juce::MidiMessageSequence midiSequence; + + int time; + juce::MidiMessage m; + + for (juce::MidiBuffer::Iterator i(midiBuffer); i.getNextEvent (m, time);) + { + midiSequence.addEvent(m, time / spec.sampleRate); + } + + return process(ioBuffer, midiSequence, spec, plugins, isProbablyLastProcessCall); +} + + /** * Process a given audio buffer through a list of * Pedalboard plugins at a given sample rate. @@ -192,12 +230,12 @@ inline int process(juce::AudioBuffer &ioBuffer, template <> py::array_t process(const py::array_t inputArray, - const py::array_t midiMessages, + const py::array_t midiMessages, double sampleRate, std::vector> plugins, unsigned int bufferSize, bool reset) { const ChannelLayout inputChannelLayout = detectChannelLayout(inputArray); juce::AudioBuffer ioBuffer = copyPyArrayIntoJuceBuffer(inputArray); - juce::MidiBuffer midiBuffer = copyPyArrayIntoJuceMidiBuffer(midiMessages); + juce::MidiMessageSequence midiSequence = copyPyArrayIntoJuceMidiMessageSequence(midiMessages); int totalOutputLatencySamples; @@ -266,7 +304,7 @@ process(const py::array_t inputArray, } // Actually run the process method of all plugins. - int samplesReturned = process(ioBuffer, midiBuffer, spec, plugins, reset); + int samplesReturned = process(ioBuffer, midiSequence, spec, plugins, reset); totalOutputLatencySamples = ioBuffer.getNumSamples() - samplesReturned; } diff --git a/pedalboard/python_bindings.cpp b/pedalboard/python_bindings.cpp index 3e3a4031..02ad6d06 100644 --- a/pedalboard/python_bindings.cpp +++ b/pedalboard/python_bindings.cpp @@ -119,7 +119,7 @@ PYBIND11_MODULE(pedalboard_native, m) { "process", [](std::shared_ptr self, const py::array_t inputArray, - const py::array_t midiMessages, + const py::array_t midiMessages, double sampleRate, unsigned int bufferSize, bool reset) { return process(inputArray, midiMessages, sampleRate, {self}, bufferSize, reset); @@ -135,7 +135,7 @@ PYBIND11_MODULE(pedalboard_native, m) { "process", [](std::shared_ptr self, const py::array_t inputArray, - const py::array_t midiMessages, + const py::array_t midiMessages, double sampleRate, unsigned int bufferSize, bool reset) { const py::array_t float32InputArray = inputArray.attr("astype")("float32"); diff --git a/requirements.txt b/requirements.txt index 296d6545..9df44b0a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ -numpy \ No newline at end of file +numpy +midi==1.2.10 \ No newline at end of file