Improving how MIDI is played in an AudioAppComponent

Based on this 2016 post by @_pi, I was able to get a basic midi player working. Thanks _pi!

That was several years ago and I’m now wondering if there are better ways to accomplish the code below.

Here’s my code:

double totalSampleLength = 0;
std::vector<juce::MidiMessage> midiMessages;
int samplesPlayed = 0;

void initMidi(const juce::String filename)
{
    juce::FileInputStream fileStream((juce::File(filename)));
    juce::MidiFile midiFile;
    midiFile.readFrom(fileStream);
    midiFile.convertTimestampTicksToSeconds();

    const juce::MidiMessageSequence *track = midiFile.getTrack(0);
    for (int i = 0; i < track->getNumEvents(); i++)
    {
        juce::MidiMessage &m = track->getEventPointer(i)->message;
        if (m.getMetaEventType() == -1) // Ignore events that aren't note on/off
        {
            midiMessages.push_back(m);
        }
    }
}
void prepareToPlay(int samplesPerBlockExpected, double sampleRate)
{
    localSampleRate = sampleRate;
    synth.setCurrentPlaybackSampleRate(sampleRate);

    midiBuffer.clear();
    auto samplePosition = 0;
    for (int i = 0; i < midiMessages.size() / 2; i++)
    {
        auto onIndex = i * 2;
        auto offIndex = onIndex + 1;
        auto note = midiMessages[onIndex];
        midiBuffer.addEvent(note, samplePosition); // Note On event
        // Calc the next samplePosition based on the length of the note
        samplePosition += (sampleRate * note.getTimeStamp());
        midiBuffer.addEvent(midiMessages[offIndex], samplePosition); // Not Off event
    }
    totalSampleLength = samplePosition;
}

My getNextAudioBlock

void getNextAudioBlock(const juce::AudioSourceChannelInfo &bufferToFill)
{
    bufferToFill.clearActiveBufferRegion();

    juce::MidiBuffer incomingMidi;
    int sampleDeltaToAdd = -samplesPlayed;
    incomingMidi.addEvents(midiBuffer, samplesPlayed, bufferToFill.numSamples, sampleDeltaToAdd);
    samplesPlayed += bufferToFill.numSamples;

    synth.renderNextBlock(*bufferToFill.buffer,
                          incomingMidi,
                          0 /*startSample*/,
                          bufferToFill.numSamples);

    if (samplesPlayed >= totalSampleLength)
    {
        // Loop
        samplesPlayed = 0;
    }
}

Thank you in advance for helping.

This is my implementation of a MidiFile player:

#pragma once

#include <JuceHeader.h>

class JuceMidiFilePlayer
{
public:
    JuceMidiFilePlayer()
    {
        PlayMidifile = false;
    }

    ~JuceMidiFilePlayer()
    {
        PlayMidifile = false;
    }

    void setMidiFileObject(const juce::MidiFile& newMidifile)
    {
        // Make sure the playback is stopped before loading a new file
        StartStopMidiFile(false);

        midifile = newMidifile;

        FileLoaded = true;
        tick_count = 0;
        sample_count = 0;
        last_tick = midifile.getLastTimestamp();
        TPQ = midifile.getTimeFormat();
        lastSampleOffset = 0.0;
        trackEventStart.clear();
        trackEventStart.insertMultiple(0, 0, midifile.getNumTracks());

        setTempo(tempoBPM);
        if (onLoad != nullptr)
            onLoad();

        DBG("MidiFile loaded.");
        DBG("midifile.getLastTimestamp() = " << midifile.getLastTimestamp()); // last tick
        DBG("midifile.getTimeFormat() = " << midifile.getTimeFormat()); // tpq
        DBG("midifile.getNumTracks() = " << midifile.getNumTracks());
    }

    void LoadMidiFileFromURL(const URL& url)
    {
        jassert(processMidi != nullptr);
        jassert(allNotesOff != nullptr);

        std::unique_ptr<juce::InputStream> inputStream(juce::URLInputSource(url).createInputStream());

        // Make sure the playback is stopped before loading a new file
        StartStopMidiFile(false);

        juce::MidiFile mf;
        if (mf.readFrom(*inputStream.get()))
            setMidiFileObject(mf);
    }

    void Unload()
    {
        StartStopMidiFile(false);
        if (onEOF != nullptr) onEOF();

        midifile.clear();
        FileLoaded = false;
    }

    void Rewind()
    {
        tick_count = 0;
        trackEventStart.fill(0);
    }

    void StartStopMidiFile(bool b)
    {
        // Call AllNotesOff when Stop button is hit
        if (!b) allNotesOff();

        // Don't play if no file is loaded
        if (!FileLoaded)
        {
            if (onEOF != nullptr) onEOF();
            return;
        }

        // No double play
        if (b && PlayMidifile) return;

        // Rewind
        if (!b && !PlayMidifile)
        {
            sample_count = 0;
            lastSampleOffset = 0.0;
            Rewind();
        }

        // Store status
        PlayMidifile = b;
    }

    void setTempo(double _tempoBPM)
    {
        tempoBPM = _tempoBPM;

        // A single tick duration is 60 / BPM / TPQ
        // Assuming tempo is 120 BPM and TPQ is 480, a tick lasts 1,0416666666666666666666666666667 milliseconds = 1041 microseconds

        tick_duration = 60.0 / tempoBPM / (double)TPQ;
        auto d = (lastSampleOffset + tick_duration * sampleRate);
        tick_duration_in_samples = (int)d;
        lastSampleOffset = d - (double)tick_duration_in_samples;
    }

    double getTempo()
    {
        return 60.0 / (double)TPQ / tick_duration;
    }

    float getPosition01()
    {
        return (float)tick_count / (float)last_tick;
    }

    void setPosition01(float pos)
    {
        allNotesOff();
        tick_count = juce::jmap<float>(pos, 0, last_tick);
        trackEventStart.fill(0);
    }

    void setSampleRate(double sr)
    {
        sampleRate = sr;

        // Set a starting tick_length for a tempo of 120 BPM and 480 TPQ
        //tick_duration = 60000.0 / 120.0 / 480.0;
        //tick_duration_in_samples = tick_duration * sampleRate / 1000.f;
        setTempo(tempoBPM);

        sample_count = 0;
        lastSampleOffset = 0.0;
        Rewind();
    }

    // To be called in the audio loop for each single sample
    void Play()
    {
        if (!PlayMidifile) return;

        // Shift to the next tick after the required tick duration
        if (++sample_count >= tick_duration_in_samples)
        {
            sample_count = 0;

            // Play events from all tracks in the Midi File
            for (int t = 0; t < midifile.getNumTracks(); t++)
            {
                // Get single track
                auto Sequence = midifile.getTrack(t);

                // Search events in track having a timestamp that matches the current tick_count
                for (int e = trackEventStart[t]; e < Sequence->getNumEvents(); ++e)
                {
                    auto msg = Sequence->getEventPointer(e)->message;

                    // Play all events matching this timestamp
                    if (msg.getTimeStamp() == (double)tick_count)
                    {
                        // Update tempo if it's a tempo event
                        if (msg.isTempoMetaEvent())
                        {
                            TPQ = midifile.getTimeFormat();
                            tick_duration = msg.getTempoMetaEventTickLength(TPQ);
                            setTempo(getTempo());
                        }

                        // Send Midi
                        processMidi(msg.getRawData(), msg.getRawDataSize());
                    }

                    // If the timestamp goes past the tick_count, skip scanning the rest of the track
                    if (msg.getTimeStamp() > (double)tick_count)
                    {
                        // Store last event index
                        trackEventStart.set(t, e);

                        break;
                    }
                }
            }

            // Advance to the next tick
            tick_count++;

            // Check end of file and stop
            if (tick_count >= last_tick)
            {
				PlayMidifile = false;
				allNotesOff();
				if (onEOF != nullptr) onEOF();
            }
        }
    }

    //==============================================================================

    // These public variables tell whether the file is loaded and is playing
    MidiFile midifile;
    bool PlayMidifile = false;
    bool FileLoaded = false;
    int TPQ = 480;

    // Set a lambda for receiving Midi Data
    std::function<void(const unsigned char* midiData, int chunkLength)> processMidi;

    // Set a lambda for calling the All-notes-off event on stop
    std::function<void(void)> allNotesOff;

    // Set a lambda that, if defined, is called when a new Midi File is loaded
    std::function<void(void)> onLoad = nullptr;

    // Set a lambda that, if defined, is called when the playback reaches the end of the file
    std::function<void(void)> onEOF = nullptr;


private:
    juce::Array<int> trackEventStart;
    double tick_duration = 1.041; // 120 BPM @ 480 TPQ
    long tick_count = 0, last_tick = 1;
    int sample_count = 0, tick_duration_in_samples = 0;
    double sampleRate = 44100.0, lastSampleOffset = 0.0;
    double tempoBPM = 120.0;
};

Call the Play() function for each sample in the processBlock() function.

2 Likes

Thanks for sharing your code @ZioGuido! This is pretty comprehensive. I hadn’t even thought about tempo changes in the midi file.

I noticed that you have a processMidi callback…how are those defined?

// Set a lambda for receiving Midi Data
std::function<void(const unsigned char *midiData, int chunkLength)> processMidi;

Basically, in your constructor you create the object and define the callbacks:


Constructor()
{
	// You have declared a class member: 
	// std::unique_ptr<JuceMidiFilePlayer> player 
	player.reset(new JuceMidiFilePlayer());

	// Define the callbacks
	player->processMidi = [&](const unsigned char* midiData, int chunkLength) 
	{ 
		// This or any other way to send Midi Data to your synth
		mySynth->processMidi(midiData, chunkLength);
	};

	player->allNotesOff = [&] { mySynth->allNotesOff(); };
	
	player->onLoad = [&] 
	{
		// Things to do as soon as a new Midifile has been loaded...
	};
	
	player->onEOF = [&]
	{
		// Things to do when the player reaches the end of the Midifile...
	};
}

// Where you know the sampleRate...

void prepareToPlay(double sampleRate, int samplesPerBlock)
{
	player->setSampleRate(sampleRate);
}

// Where you load the Midifile...

void loadMidiFile()
{
	// create file chooser and get the file...
	// ...
	
	player->LoadMidiFileFromURL(myFile);
}

// In the audio loop

void processBlock(AudioBuffer<float>& buffer, MidiBuffer& midiMessages)
{

	// For each sample...
	for (int s = 0; s < buffer.getNumOfSamples(); s++)
	{
		player->Play();
	}

}
1 Like

Thank you @ZioGuido!