How to loop midi file

Hi,

I want to create a plugin, that can load a midi file and play it in a loop. Based on the example of @IvanC Playing a midi file (revisited), I am currently able to play a single track of a midi file. But, it is only played once. What do I have to do, that the midi data is processed again, once it reached the end?

The way to do that is to first read the whole midi file, and store the midi events in datastructures of your own. Instead of fetching next events in the file, you iterate over the events in that data structure. And when you’re at the end, you reset the iterator to the start of the midi clip.

I have an open source plugin that does that: https://github.com/tomto66/Topiary-Beatz

Thanks @tomto66 for the sample. If I understood it correctly, everything that effects the midi output has to happen in the processBlock() method. This method is called repeatedly by the framework.

What I do not understand is: Everything that happens in that method is let’s say executed 50 times per second. This also means that the midiMessage buffer is written 50 times a second, collecting the data from my data structure (a MidiMessageSequence that contains the file content). But how does it work to play the midi notes smoothly in time?

Is the calculated result of the first execution of processBlock() passed to another thread that takes care to “perform” the midi data and ignores new input from processBlock() while running?

If this is the case, why does it not take the data again from processBlock() that is produced 50 times a second once it has finished?

Is the calculated result of the first execution of processBlock() passed to another thread that takes care to “perform” the midi data and ignores new input from processBlock() while running?

Still haven’t touched MIDI stuff, but AFAIK everything (both audio & midi) is processed in the audio thread which can’t be blocked or can’t wait for events to happen, it just runs an you must make sure that everything you put in it is fast enoguh to end before the next block is called to be processed. So you don’t wanna use locks, waits, memory allocation during processBlock() or having a shitton of processing to the point of the CPU not being able to handle it in time.
But when it comes to a midi file/audio file, yes you can use a background thread to load them and use as you wish (i.e storing midi/audio stuff from the file in a buffer) while the audio thread does it’s own stuff (so you can be loading a sound/midi file while other sounds are playing instead of stopping the audio). Then you just operate with that buffer/data structure you filled from the fille in the audio thread. For instance, you got an example about this in the tutorials.

OK, thanks first of all for your support! You keep me ongoing even if it stuck.

After some more investigation, it looks like I already did it right regarding fetching the events from a “second” source and then put it in the “real” buffer.
The second thing I experienced is that the playHead continues but I need to reset the start time for the midi events so it starts over again. This works now as well.
But starting from the second loop, the sound coming from the synth chained after the plugin is not smooth anymore but like pizzicato. This keeps on for all the following loops. Do you have any idea, what the reason could be for that?
If I observe the midi events with the with a midi logger in MainStage, it says “…console bandwidth exceeded, thinning some traces…”. Don’t know, if this gives a helpful hint.

You mean the first loop plays it well, but the second iteration/loop you play that file it sounds weird? Instead of debugging it in MainStage you could add a little component in your plugin showing the midi events in text (you got an example in the tutorials, the MIDI one I believe), or just putting them in the console with DBG messages so you can check what’s going on and how the midi messages of the next loop differ compared to the first one (if they do).

Anyway post some code so we can see what’s going on in your loop, it’s hard to guess without it since it could be many things.

Instead of debugging it in MainStage you could add a little component in your plugin showing the midi events in text …

Ah, good point. The reason, why I test it in MainStage (what is very annoying and time-consuming) is that the JUCE AudioPluginHost does not provide an AudioPlayHead that can be started to get the playTime. If you know a better solution for how to test a plugin and how I can use the debug console in XCode it would be great!

Anyway post some code so we can see what’s going on in your loop, it’s hard to guess without it since it could be many things.

I will try two more things and if this will not be successful, I will post some code.

I’m not doing a plugin but afaik, you can run it in Xcode as if it was an standalone app and test the basic functionalities that don’t require DAW related stuff, and even then you could writte a mockup function to test those (i.e instead of waiting for an external instrument to send you MIDI to see what happens, just write a bunch of midi messages by hand to see if the other parts of your plugin work).

Said so, you can either use break points and debug it, where you will see the call trace and the value of all the variables, or what I mentioned before. In Xcode for things like arrays I usually use DBG messages (but std::cout works too) to show the values which makes it more visual than debugging step by step. Is there a much more efficient/better method? Probably, maybe someone else can enlighten us.

OK, I see. I guess it makes sense to write a wrapper to finally save time and get improved debugging capabilities. Before I needed the AudioPlayHead, I used std::cout to see what’s going on. To get some more information from debugging in MainStage, I created a log file and surprise, I could figure out what the problem was with the pizzicato notes. There was a wrong variable in an if-statement that forced a sendAllNotesOff and therefore cut my notes.
Now only the first note is still missing starting from the second loop but I am quite optimistic to figure that out as well.

OK, I got it finally running. Thanks for all your support! Here the code of the processBlock() function, if anybody faces the same or similar situation. Since I am a beginner in C++ there might be a more efficient or better way to implement this. If you have any suggestions to improve the code, I would appreciate.

void SimpleMidiplayerAudioProcessor::processBlock (AudioBuffer<float>& buffer, MidiBuffer& midiMessages)
{
    ScopedNoDenormals noDenormals;
    
    auto totalNumOutputChannels = getTotalNumOutputChannels();

    // We clear all the incoming audio here
    for (auto i = 0; i < totalNumOutputChannels; ++i)
        buffer.clear (i, 0, buffer.getNumSamples());

    AudioPlayHead::CurrentPositionInfo thePositionInfo;
    getPlayHead()->getCurrentPosition(thePositionInfo);

    if (numTracks.load() > 0)
    {
        // The MIDI file is played only when the transport is active
        if (thePositionInfo.isPlaying)
        {
            const MidiMessageSequence *midiSeq = finalMidiFile.getTrack(currentTrack.load());
            auto startTime = thePositionInfo.timeInSeconds;              
            double loopOffset = fmod(startTime, midiSeq->getEndTime());
            loop = startTime - loopOffset;
            auto sampleStartTime = startTime - loop;
            
            if (lastSampleStartTime > sampleStartTime)
                sampleStartTime = 0.0; // set to 0 if new loop starts
            
            lastSampleStartTime = sampleStartTime;
            auto sampleEndTime = sampleStartTime + buffer.getNumSamples() / getSampleRate();
            auto sampleLength = 1.0 / getSampleRate();

            // If the transport bar position has been moved by the user or because of looping
            if (std::abs(sampleStartTime - nextStartTime) > sampleLength && nextStartTime > 0.0)
                sendAllNotesOff(midiMessages);

            nextStartTime = sampleEndTime;

            // If the MIDI file doesn't contain any event anymore
            if (isPlayingSomething && sampleStartTime >= midiSeq->getEndTime())
            {
                sendAllNotesOff(midiMessages);
            }
            else
            {
                // Called when the user changes the track during playback
                if (trackHasChanged)
                {
                    trackHasChanged = false;
                    sendAllNotesOff(midiMessages);
                }
                int curTranspose = transpose, lastTranspose = transpose;
                
                // Iterating through the MIDI file contents and trying to find an event that
                // needs to be called in the current time frame
                for (auto i = 0; i < midiSeq->getNumEvents(); i++)
                {
                    MidiMessageSequence::MidiEventHolder event = *midiSeq->getEventPointer(i);

                    if (event.message.getTimeStamp() >= sampleStartTime && event.message.getTimeStamp() < sampleEndTime)
                    {
                        auto samplePosition = roundToInt((event.message.getTimeStamp() - sampleStartTime) * getSampleRate());
                        midiMessages.addEvent(event.message, samplePosition);
                        
                        /* to avoid that the first element of the next loop will be missed because it has to be sent in the same time frame, send it in the same time frame.
                        Needs to be improved:
                        - only send the first event, if it really is part of the same time frame
                        - could also be more the one event.
                        */
                        if(fmod(startTime, midiSeq->getEndTime()) > fmod(sampleEndTime, midiSeq->getEndTime()))
                        {
                            MidiMessageSequence::MidiEventHolder event2 = *midiSeq->getEventPointer(0);
                            auto samplePosition = roundToInt((event2.message.getTimeStamp()) * getSampleRate());
                            midiMessages.addEvent(event2.message, samplePosition);
                        }

                        isPlayingSomething = true;
                    }
                }
            }
        }
    }
    else
    {
        // If the transport isn't active anymore
        if (isPlayingSomething)
            sendAllNotesOff(midiMessages);
    }
}