I posted about this in the Audio Programmer Discord but it’s probably best to have a dedicated thread about it.
My goal is to have a delay plugin that will respond to MIDI events on a piano roll in a DAW, and set the Delay Time to the proper milliseconds so it produces an equivalent pitch.
I built a delay plugin using the ‘Complete Beginner’s Guide to Audio Plug-in Development’ by Matthijs Hollemans. I used his other book ('Creating Synthesizer Plug-Ins with C++ and Juce - a.k.a. the JX11 book) to implement MIDI into it. However, I’m not sure how to express the logic to do the tuning.
Assume I have Logic set up to do this (one track with an ‘AU MIDI-controlled effect’ and one channel producing audio routed into the effect channel). If I’m playing A3 (note 69) at 440.00Hz that should convert to 2.273ms on the delay time parameter. So any audio coming through will be tuned to A above middle C when there’s a MIDI note drawn in the piano roll.
How do I express that and where would that be? The JX11 book did the tuning in the noteOn() function so I moved that into PluginProcessor.cpp.
Here’s my code from processBlock and the relevant functions.
void DelayAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages)
{
juce::ScopedNoDenormals noDenormals;
auto totalNumInputChannels = getTotalNumInputChannels();
auto totalNumOutputChannels = getTotalNumOutputChannels();
for (auto i = totalNumInputChannels; i < totalNumOutputChannels; ++i)
buffer.clear (i, 0, buffer.getNumSamples());
params.update();
tempo.update(getPlayHead());
float syncedTime = float(tempo.getMillisecondsForNoteLength(params.delayNote));
if (syncedTime > Parameters::maxDelayTime)
{
syncedTime = Parameters::maxDelayTime;
}
float sampleRate = float(getSampleRate());
auto mainInput = getBusBuffer(buffer, true, 0);
auto mainInputChannels = mainInput.getNumChannels();
auto isMainInputStereo = mainInputChannels > 1;
const float* inputDataL = mainInput.getReadPointer(0);
const float* inputDataR = mainInput.getReadPointer(isMainInputStereo ? 1 : 0);
auto mainOutput = getBusBuffer(buffer, false, 0);
auto mainOutputChannels = mainOutput.getNumChannels();
auto isMainOutputStereo = mainOutputChannels > 1;
float* outputDataL = mainOutput.getWritePointer(0);
float* outputDataR = mainOutput.getWritePointer(isMainOutputStereo ? 1 : 0);
float maxL = 0.0f;
float maxR = 0.0f;
for (int sample = 0; sample < buffer.getNumSamples(); ++sample)
{
params.smoothen();
if (xfade == 0.0f)
{
float delayTime = params.tempoSync ? syncedTime : params.delayTime;
targetDelay = delayTime / 1000.0f * sampleRate;
if (delayInSamples == 0.0f) { // first time
delayInSamples = targetDelay;
}
else if (targetDelay != delayInSamples) { // start crossfade
xfade = xfadeInc;
}
}
if (params.lowCut != lastLowCut) {
lowCutFilter.setCutoffFrequency(params.lowCut);
lastLowCut = params.lowCut;
}
if (params.highCut != lastHighCut) {
highCutFilter.setCutoffFrequency(params.highCut);
lastHighCut = params.highCut;
}
float dryL = inputDataL[sample];
float dryR = inputDataR[sample];
float mono = (dryL + dryR) * 0.5f; // convert stereo to mono
delayLineL.write(mono*params.panL + feedbackR);
delayLineR.write(mono*params.panR + feedbackL);
float wetL = delayLineL.read(delayInSamples);
float wetR = delayLineR.read(delayInSamples);
if (xfade > 0.0f)
{ // crossfading
float newL = delayLineL.read(targetDelay);
float newR = delayLineR.read(targetDelay);
wetL = (1.0f - xfade) * wetL + xfade * newL;
wetR = (1.0f - xfade) * wetR + xfade * newR;
xfade += xfadeInc;
if (xfade >= 1.0f) {
delayInSamples = targetDelay;
xfade = 0.0f;
}
}
feedbackL = wetL * params.feedback;
feedbackL = lowCutFilter.processSample(0, feedbackL);
feedbackL = highCutFilter.processSample(0, feedbackL);
feedbackR = wetR * params.feedback;
feedbackR = lowCutFilter.processSample(1, feedbackR);
feedbackR = highCutFilter.processSample(1, feedbackR);
float mixL = dryL + wetL * params.mix;
float mixR = dryR + wetR * params.mix;
float outL = mixL * params.gain;
float outR = mixR * params.gain;
if (params.bypassed)
{
outL = dryL;
outR = dryR;
}
outputDataL[sample] = outL;
outputDataR[sample] = outR;
maxL = std::max(maxL, std::abs(outL));
maxR = std::max(maxR, std::abs(outR));
}
splitBufferByEvents(buffer, midiMessages);
levelL.updateIfGreater(maxL);
levelR.updateIfGreater(maxR);
#if JUCE_DEBUG
protectYourEars(buffer);
#endif
}
// MIDI implementation from the private members in PluginProcessor.h
void DelayAudioProcessor::splitBufferByEvents(juce::AudioBuffer<float>& buffer,
juce::MidiBuffer& midiMessages)
{
int bufferOffset = 0;
for (const auto metadata : midiMessages)
{
// Render the audio that happens before this event (if any).
int samplesThisSegment = metadata.samplePosition - bufferOffset;
if (samplesThisSegment > 0)
{
render(buffer, samplesThisSegment, bufferOffset);
bufferOffset += samplesThisSegment;
}
// Handle the event. Ignore MIDI messages such as sysex.
if (metadata.numBytes <= 3)
{
uint8_t data1 = (metadata.numBytes >= 2) ? metadata.data[1] : 0;
uint8_t data2 = (metadata.numBytes == 3) ? metadata.data[2] : 0;
handleMIDI(metadata.data[0], data1, data2);
}
}
// Render the audio after the last MIDI event. If there were no
// MIDI events at all, this renders the entire buffer.
int samplesLastSegment = buffer.getNumSamples() - bufferOffset;
if (samplesLastSegment > 0) {
render(buffer, samplesLastSegment, bufferOffset);
}
midiMessages.clear();
}
void DelayAudioProcessor::midiMessage(uint8_t data0, uint8_t data1, uint8_t data2)
{
switch (data0 & 0xF0)
{
// Note off
case 0x80:
noteOff(data1 & 0x7F);
break;
// Note on
case 0x90: {
uint8_t note = data1 & 0x7F;
uint8_t velo = data2 & 0x7F;
if (velo > 0) {
noteOn(note, velo);
} else {
noteOff(note);
}
break;
}
}
}
void DelayAudioProcessor::noteOn(int note, int velocity)
{
this->note = note;
float periodFreq = 2.2727f * std::exp2(float(note - 69) / 12.0f);
this->velocity = velocity;
}
void DelayAudioProcessor::noteOff(int note)
{
if (note == this->note)
{
this->note = 0;
this->velocity = 0;
}
}
void DelayAudioProcessor::handleMIDI(uint8_t data0, uint8_t data1, uint8_t data2)
{
midiMessage(data0, data1, data2);
char s[16];
snprintf(s, 16, "%02hhX %02hhX %02hhX", data0, data1, data2);
DBG(s);
}
void DelayAudioProcessor::render(juce::AudioBuffer<float>& buffer, int sampleCount, int bufferOffset)
{
// do nothing yet
}
Let me know if I need to explain in more detail or make something clearer.
