I’m trying to create a simple plugin that allows me to export a chord, for instance, a Cmaj7, where the E note is detuned 13 cents by implementing MPE.
First of all, is that possible? Can I use the MIDI file in an app like Ableton Live?
The code I’m testing is this one:
#include "PluginProcessor.h"
#include "PluginEditor.h"
//==============================================================================
MPE_TestAudioProcessorEditor::MPE_TestAudioProcessorEditor (MPE_TestAudioProcessor& p)
: AudioProcessorEditor (&p), audioProcessor (p)
{
setSize (400, 300);
// Add and configure the button
addAndMakeVisible(generateMidiButton);
generateMidiButton.setButtonText("Generate MIDI");
generateMidiButton.addListener(this);
}
MPE_TestAudioProcessorEditor::~MPE_TestAudioProcessorEditor()
{
generateMidiButton.removeListener(this);
}
//==============================================================================
void MPE_TestAudioProcessorEditor::paint (juce::Graphics& g)
{
g.fillAll (getLookAndFeel().findColour (juce::ResizableWindow::backgroundColourId));
g.setColour (juce::Colours::white);
g.setFont (15.0f);
g.drawFittedText ("Hello World!", getLocalBounds(), juce::Justification::centred, 1);
}
void MPE_TestAudioProcessorEditor::resized()
{
generateMidiButton.setBounds (10, 10, getWidth() - 20, 30);
}
void MPE_TestAudioProcessorEditor::buttonClicked (juce::Button* button)
{
if (button == &generateMidiButton)
{
juce::File midiFile(juce::File::getSpecialLocation(juce::File::userDesktopDirectory).getChildFile("detuned_Cmaj7_chord_mpe.mid"));
auto createDetunedCmaj7MPE = [](const juce::File& midiFile)
{
if (!midiFile.getParentDirectory().exists())
{
juce::AlertWindow::showMessageBoxAsync(juce::AlertWindow::WarningIcon,
"Error",
"Destination directory does not exist.");
return;
}
juce::MidiFile midi;
juce::MidiMessageSequence sequence;
const int tpq = 960; // Ticks per quarter note
midi.setTicksPerQuarterNote(tpq);
// Define the MIDI note numbers for Cmaj7 chord
const int C_note = 60; // Middle C
const int E_note = 64; // E above middle C
const int G_note = 67; // G above middle C
const int B_note = 71; // B above middle C
// Define MIDI channels for MPE (channels 2-15 for notes, channel 1 is reserved for global control)
const int C_channel = 2; // Channel 2 (MPE Lower Zone Member Channel)
const int E_channel = 3; // Channel 3 (MPE Lower Zone Member Channel)
const int G_channel = 4; // Channel 4 (MPE Lower Zone Member Channel)
const int B_channel = 5; // Channel 5 (MPE Lower Zone Member Channel)
// Define note on/off times in ticks
const int noteOnTime = 0;
const int noteOffTime = tpq; // Duration of a quarter note
// Set up MPE zone configuration
juce::MidiBuffer mpeConfig = juce::MPEMessages::setLowerZone(14, 48, 2); // Lower zone with 14 member channels, pitch bend range of ±48 semitones, master pitch bend range of ±2 semitones
for (const auto& m : mpeConfig)
{
const auto message = m.getMessage();
sequence.addEvent(message, noteOnTime);
}
// Add the C note to the sequence (no detuning)
sequence.addEvent(juce::MidiMessage::noteOn(C_channel, C_note, (juce::uint8)100), noteOnTime);
sequence.addEvent(juce::MidiMessage::noteOff(C_channel, C_note, (juce::uint8)64), noteOffTime);
// Add the E note with detuning (28 cents down)
const float pitchBendRange = 48.0f; // ±48 semitones for MPE
const float pitchBendCents = -28.0f / 100.0f; // Convert cents to semitones
juce::int16 pitchBendValue = juce::MidiMessage::pitchbendToPitchwheelPos(pitchBendCents, pitchBendRange);
sequence.addEvent(juce::MidiMessage::noteOn(E_channel, E_note, (juce::uint8)100), noteOnTime);
sequence.addEvent(juce::MidiMessage::pitchWheel(E_channel, pitchBendValue), noteOnTime + 1); // Apply pitch bend right after note on
sequence.addEvent(juce::MidiMessage::noteOff(E_channel, E_note, (juce::uint8)64), noteOffTime);
sequence.addEvent(juce::MidiMessage::pitchWheel(E_channel, juce::MidiMessage::pitchbendToPitchwheelPos(0.0f, pitchBendRange)), noteOffTime + 1); // Reset pitch bend after note off
// Add the G note to the sequence (no detuning)
sequence.addEvent(juce::MidiMessage::noteOn(G_channel, G_note, (juce::uint8)100), noteOnTime);
sequence.addEvent(juce::MidiMessage::noteOff(G_channel, G_note, (juce::uint8)64), noteOffTime);
// Add the B note to the sequence (no detuning)
sequence.addEvent(juce::MidiMessage::noteOn(B_channel, B_note, (juce::uint8)100), noteOnTime);
sequence.addEvent(juce::MidiMessage::noteOff(B_channel, B_note, (juce::uint8)64), noteOffTime);
// Add the sequence to a track
juce::MidiMessageSequence track;
for (int i = 0; i < sequence.getNumEvents(); ++i)
track.addEvent(sequence.getEventPointer(i)->message, sequence.getEventTime(i));
midi.addTrack(track);
// Write the MIDI file to disk
juce::FileOutputStream outputStream(midiFile);
if (!outputStream.openedOk())
{
juce::AlertWindow::showMessageBoxAsync(juce::AlertWindow::WarningIcon,
"Error",
"Unable to create MIDI file.");
return;
}
midi.writeTo(outputStream);
};
createDetunedCmaj7MPE(midiFile);
}
}