Exporting MIDI files using MPE data

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);
    }
}

short answer: yes.
long answer: Ableton Live does not have very good support for MPE, last time I checked you had to use multiple tracks (one per MIDI channel) with some complicated MIDI routing.

Ableton’s support for MPE is very good now, since at least a year or so. Our plugin Entonal Studio works really well in Ableton since that update.

1 Like

Thanks for the answer. But the main question is, what am I doing wrong with my code? Why does it not generate MPE data per note?

your code looks at a glance correct. But the only way to be sure is to run it. Perhaps you can attach the MIDI file that it creates?