Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Midi support #110

Closed
wants to merge 2 commits into from
Closed
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
12 changes: 6 additions & 6 deletions pedalboard/ExternalPlugin.h
Original file line number Diff line number Diff line change
Expand Up @@ -643,7 +643,7 @@ template <typename ExternalPluginType> class ExternalPlugin : public Plugin {
{
juce::dsp::AudioBlock<float> block(audioBuffer);
juce::dsp::ProcessContextReplacing<float> context(block);
process(context);
process(context, emptyMidiBuffer);
}
}

Expand All @@ -666,7 +666,7 @@ template <typename ExternalPluginType> class ExternalPlugin : public Plugin {
}
juce::dsp::AudioBlock<float> block(audioBuffer);
juce::dsp::ProcessContextReplacing<float> context(block);
process(context);
process(context, emptyMidiBuffer);
}

auto signalVolume = audioBuffer.getMagnitude(0, bufferSize);
Expand All @@ -679,7 +679,7 @@ template <typename ExternalPluginType> class ExternalPlugin : public Plugin {
{
juce::dsp::AudioBlock<float> block(audioBuffer);
juce::dsp::ProcessContextReplacing<float> context(block);
process(context);
process(context, emptyMidiBuffer);
}

auto magnitudeOfSilentBuffer = audioBuffer.getMagnitude(0, bufferSize);
Expand Down Expand Up @@ -753,10 +753,10 @@ template <typename ExternalPluginType> class ExternalPlugin : public Plugin {
}

int process(
const juce::dsp::ProcessContextReplacing<float> &context) override {
const juce::dsp::ProcessContextReplacing<float> &context, juce::MidiBuffer &midiBuffer) override {

if (pluginInstance) {
juce::MidiBuffer emptyMidiBuffer;
;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
;

if (context.usesSeparateInputAndOutputBlocks()) {
throw std::runtime_error("Not implemented yet - "
"no support for using separate "
Expand Down Expand Up @@ -813,7 +813,7 @@ template <typename ExternalPluginType> 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,
Expand Down
2 changes: 1 addition & 1 deletion pedalboard/JucePlugin.h
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ template <typename DSPType> class JucePlugin : public Plugin {
}

int process(
const juce::dsp::ProcessContextReplacing<float> &context) override {
const juce::dsp::ProcessContextReplacing<float> &context, juce::MidiBuffer &midiBuffer) override {
dspBlock.process(context);
return context.getOutputBlock().getNumSamples();
}
Expand Down
53 changes: 53 additions & 0 deletions pedalboard/MidiUtils.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* 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 <pybind11/numpy.h>
#include <pybind11/pybind11.h>

namespace Pedalboard {

juce::MidiMessageSequence
copyPyArrayIntoJuceMidiMessageSequence(const py::array_t<float, py::array::c_style> midiMessages) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This helper seems to re-interpret floating-point data into integer data, which seems a bit risky and confusing. Given that MIDI messages are probably going to be sparse (i.e.: not nearly as large as audio data) we could probably just use a std::vector<std::tuple<int, int, int, float>> or could create our own thin wrapper class and use that (i.e.: std::vector<pedalboard.MIDIMessage>).

py::buffer_info inputInfo = midiMessages.request();

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<float *>(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 );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
DBG( byte1 );

}

return midiSequence;
}

} // namespace Pedalboard
2 changes: 1 addition & 1 deletion pedalboard/Plugin.h
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ class Plugin {
* (i.e.: they should come last).
*/
virtual int
process(const juce::dsp::ProcessContextReplacing<float> &context) = 0;
process(const juce::dsp::ProcessContextReplacing<float> &context, juce::MidiBuffer &midiBuffer) = 0;

/**
* Reset this plugin's state, clearing any internal buffers or delay lines.
Expand Down
2 changes: 1 addition & 1 deletion pedalboard/RubberbandPlugin.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ class RubberbandPlugin : public Plugin {
}

int process(
const juce::dsp::ProcessContextReplacing<float> &context) override final {
const juce::dsp::ProcessContextReplacing<float> &context, juce::MidiBuffer &midiBuffer) override final {
if (rbPtr) {
auto inBlock = context.getInputBlock();
auto outBlock = context.getOutputBlock();
Expand Down
1 change: 1 addition & 0 deletions pedalboard/midi/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .midi_messages import *
7 changes: 7 additions & 0 deletions pedalboard/midi/midi_messages.py
Original file line number Diff line number Diff line change
@@ -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]
12 changes: 6 additions & 6 deletions pedalboard/plugin_templates/FixedBlockSize.h
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ class FixedBlockSize : public Plugin {
}

virtual int
process(const juce::dsp::ProcessContextReplacing<SampleType> &context) {
process(const juce::dsp::ProcessContextReplacing<SampleType> &context, juce::MidiBuffer &midiBuffer) {
auto ioBlock = context.getOutputBlock();

if (lastSpec.maximumBlockSize % blockSize == 0) {
Expand All @@ -81,7 +81,7 @@ class FixedBlockSize : public Plugin {
juce::dsp::AudioBlock<SampleType> subBlock =
ioBlock.getSubBlock(i, blockSize);
juce::dsp::ProcessContextReplacing<SampleType> subContext(subBlock);
int samplesOutputThisBlock = plugin.process(subContext);
int samplesOutputThisBlock = plugin.process(subContext, midiBuffer);

if (samplesOutput > 0 && samplesOutputThisBlock < blockSize) {
throw std::runtime_error(
Expand All @@ -106,7 +106,7 @@ class FixedBlockSize : public Plugin {
subBlock.copyFrom(ioBlock.getSubBlock(offset, remainderInSamples));

juce::dsp::ProcessContextReplacing<SampleType> subContext(subBlock);
int samplesOutputThisBlock = plugin.process(subContext);
int samplesOutputThisBlock = plugin.process(subContext, midiBuffer);

// Copy the output back into ioBlock, right-aligned:
ioBlock
Expand Down Expand Up @@ -164,7 +164,7 @@ class FixedBlockSize : public Plugin {
juce::dsp::AudioBlock<SampleType> subBlock =
inputBlock.getSubBlock(i, blockSize);
juce::dsp::ProcessContextReplacing<SampleType> subContext(subBlock);
int samplesProcessedThisBlock = plugin.process(subContext);
int samplesProcessedThisBlock = plugin.process(subContext, midiBuffer);
inputSamplesConsumed += blockSize;

if (samplesProcessedThisBlock > 0) {
Expand Down Expand Up @@ -272,12 +272,12 @@ class ExpectsFixedBlockSize : public AddLatency {
}

virtual int
process(const juce::dsp::ProcessContextReplacing<float> &context) {
process(const juce::dsp::ProcessContextReplacing<float> &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(); }
Expand Down
8 changes: 4 additions & 4 deletions pedalboard/plugin_templates/ForceMono.h
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ class ForceMono : public Plugin {
}

virtual int
process(const juce::dsp::ProcessContextReplacing<SampleType> &context) {
process(const juce::dsp::ProcessContextReplacing<SampleType> &context, juce::MidiBuffer &midiBuffer) {
auto ioBlock = context.getOutputBlock();

// Mix all channels to mono first, if necessary.
Expand All @@ -62,7 +62,7 @@ class ForceMono : public Plugin {
juce::dsp::AudioBlock<SampleType> monoBlock =
ioBlock.getSingleChannelBlock(0);
juce::dsp::ProcessContextReplacing<SampleType> 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) {
Expand Down Expand Up @@ -98,11 +98,11 @@ class ExpectsMono : public AddLatency {
}

virtual int
process(const juce::dsp::ProcessContextReplacing<float> &context) {
process(const juce::dsp::ProcessContextReplacing<float> &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);
}
};

Expand Down
8 changes: 4 additions & 4 deletions pedalboard/plugin_templates/PrimeWithSilence.h
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,11 @@ class PrimeWithSilence
}

virtual int
process(const juce::dsp::ProcessContextReplacing<float> &context) override {
process(const juce::dsp::ProcessContextReplacing<float> &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(
Expand Down Expand Up @@ -103,7 +103,7 @@ class ExpectsToBePrimed : public AddLatency {
}

virtual int
process(const juce::dsp::ProcessContextReplacing<float> &context) {
process(const juce::dsp::ProcessContextReplacing<float> &context, juce::MidiBuffer &midiBuffer) {
auto inputBlock = context.getInputBlock();

for (int i = 0; i < inputBlock.getNumSamples(); i++) {
Expand All @@ -130,7 +130,7 @@ class ExpectsToBePrimed : public AddLatency {
seenSilentSamples++;
}

return AddLatency::process(context);
return AddLatency::process(context, midiBuffer);
}

virtual void reset() {
Expand Down
6 changes: 3 additions & 3 deletions pedalboard/plugin_templates/Resample.h
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ template <typename SampleType> class Passthrough : public Plugin {
virtual void prepare(const juce::dsp::ProcessSpec &spec) {}

virtual int
process(const juce::dsp::ProcessContextReplacing<float> &context) {
process(const juce::dsp::ProcessContextReplacing<float> &context, juce::MidiBuffer &midiBuffer) {
return context.getInputBlock().getNumSamples();
}

Expand Down Expand Up @@ -235,7 +235,7 @@ class Resample : public Plugin {
plugin.prepare(subSpec);
}

int process(const juce::dsp::ProcessContextReplacing<SampleType> &context)
int process(const juce::dsp::ProcessContextReplacing<SampleType> &context, juce::MidiBuffer &midiBuffer)
override final {
auto ioBlock = context.getOutputBlock();

Expand Down Expand Up @@ -322,7 +322,7 @@ class Resample : public Plugin {
processedSamplesInResampledBuffer, cleanSamplesToProcess);
juce::dsp::ProcessContextReplacing<SampleType> 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:
Expand Down
2 changes: 1 addition & 1 deletion pedalboard/plugins/AddLatency.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class AddLatency : public JucePlugin<juce::dsp::DelayLine<
}

virtual int
process(const juce::dsp::ProcessContextReplacing<float> &context) override {
process(const juce::dsp::ProcessContextReplacing<float> &context, juce::MidiBuffer &midiBuffer) override {
getDSP().process(context);
int blockSize = context.getInputBlock().getNumSamples();
samplesProvided += blockSize;
Expand Down
2 changes: 1 addition & 1 deletion pedalboard/plugins/Bitcrush.h
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ template <typename SampleType> class Bitcrush : public Plugin {
virtual void reset() override {}

virtual int process(
const juce::dsp::ProcessContextReplacing<SampleType> &context) override {
const juce::dsp::ProcessContextReplacing<SampleType> &context, juce::MidiBuffer &midiBuffer) override {
auto block = context.getOutputBlock();

block.multiplyBy(scaleFactor);
Expand Down
14 changes: 8 additions & 6 deletions pedalboard/plugins/Chain.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ class Chain : public PluginContainer {
}

virtual int
process(const juce::dsp::ProcessContextReplacing<float> &context) {
process(const juce::dsp::ProcessContextReplacing<float> &context, juce::MidiBuffer &midiBuffer) {
// assuming process context replacing
auto ioBlock = context.getOutputBlock();

Expand All @@ -52,7 +52,7 @@ class Chain : public PluginContainer {

juce::AudioBuffer<float> ioBuffer(channels, ioBlock.getNumChannels(),
ioBlock.getNumSamples());
return ::Pedalboard::process(ioBuffer, lastSpec, plugins, false);
return ::Pedalboard::process(ioBuffer, midiBuffer, lastSpec, plugins, false);
}

virtual void reset() {
Expand Down Expand Up @@ -104,31 +104,33 @@ inline void init_chain(py::module &m) {
"process",
[](std::shared_ptr<Chain> self,
const py::array_t<float, py::array::c_style> inputArray,
const py::array_t<float, py::array::c_style> 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<Chain> self,
const py::array_t<double, py::array::c_style> inputArray,
const py::array_t<float, py::array::c_style> midiMessages,
double sampleRate, unsigned int bufferSize, bool reset) {
const py::array_t<float, py::array::c_style> 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);
}
Expand Down
2 changes: 1 addition & 1 deletion pedalboard/plugins/Delay.h
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ class Delay : public JucePlugin<juce::dsp::DelayLine<
virtual void reset() override { this->getDSP().reset(); }

virtual int process(
const juce::dsp::ProcessContextReplacing<SampleType> &context) override {
const juce::dsp::ProcessContextReplacing<SampleType> &context, juce::MidiBuffer &midiBuffer) override {
// TODO: More advanced mixing rules than "linear?"
SampleType dryVolume = 1.0f - getMix();
SampleType wetVolume = getMix();
Expand Down
2 changes: 1 addition & 1 deletion pedalboard/plugins/GSMFullRateCompressor.h
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ class GSMFullRateCompressorInternal : public Plugin {
}

int process(
const juce::dsp::ProcessContextReplacing<float> &context) override final {
const juce::dsp::ProcessContextReplacing<float> &context, juce::MidiBuffer &midiBuffer) override final {
auto ioBlock = context.getOutputBlock();

if (ioBlock.getNumSamples() != GSM_FRAME_SIZE_SAMPLES) {
Expand Down
2 changes: 1 addition & 1 deletion pedalboard/plugins/Invert.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ namespace py = pybind11;
namespace Pedalboard {
template <typename SampleType> class Invert : public Plugin {
virtual void prepare(const juce::dsp::ProcessSpec &spec) override {}
int process(const juce::dsp::ProcessContextReplacing<SampleType> &context)
int process(const juce::dsp::ProcessContextReplacing<SampleType> &context, juce::MidiBuffer &midiBuffer)
override final {
context.getOutputBlock().negate();
return context.getOutputBlock().getNumSamples();
Expand Down
2 changes: 1 addition & 1 deletion pedalboard/plugins/MP3Compressor.h
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ class MP3Compressor : public Plugin {
}

int process(
const juce::dsp::ProcessContextReplacing<float> &context) override final {
const juce::dsp::ProcessContextReplacing<float> &context, juce::MidiBuffer &midiBuffer) override final {
auto ioBlock = context.getOutputBlock();

if (mp3BufferBytesFilled > 0) {
Expand Down
Loading