Playing MIDI file thru Synth


#1

Supposing I already have a working synth player, I'm trying to play a MIDI file. It looks as though this is quite involved.

https://www.juce.com/doc/classMidiFile <-- I can see how to use this to extract a const MidiMessageSequence* for each channel.

Looking at it from the other end, my synth's getNextAudioBlock looks like the demo's: https://github.com/julianstorer/JUCE/blob/master/examples/Demo/Source/Demos/AudioSynthesiserDemo.cpp#L206

i.e.

MidiMessageCollector midiCollector;
void prepareToPlay (int /*samplesPerBlockExpected*/, double sampleRate) override { 
    midiCollector.reset (sampleRate); 
    synth.setCurrentPlaybackSampleRate (sampleRate);
}

void getNextAudioBlock (const AudioSourceChannelInfo& bufferToFill) override { 
    bufferToFill.clearActiveBufferRegion(); 
    MidiBuffer incomingMidi; 
    midiCollector.removeNextBlockOfMessages (incomingMidi, bufferToFill.numSamples); 
    keyboardState.processNextMidiBuffer (incomingMidi, 0, bufferToFill.numSamples, true); 
    synth.renderNextBlock (*bufferToFill.buffer, incomingMidi, 0, bufferToFill.numSamples); 
}

I think I want to ignore MidiMessageCollector as that is for real-time collection.
So it looks like somehow I need to extract a MidiBuffer from my MidiMessageSequence

http://www.juce.com/forum/topic/play-midi-file-midimessagesequence-midibuffer says I need to do it myself:

    ScopedPointer<MidiFile> midiFile = new MidiFile();
    Array<int> currMidiEvent;
    double midiTimeElapsed;
    bool midiIsPlaying = false;

    void setMidiFile(String file) {
        FileInputStream fileStream(file);
        midiFile->readFrom(fileStream);
        midiFile->convertTimestampTicksToSeconds();
        currMidiEvent.clear(); // last used event in each channel
        for (int i = 0; i < midiFile->getNumTracks(); i++)
            currMidiEvent.add(0);
        midiTimeElapsed = 0.0;
        midiIsPlaying = true;
    }

    void getNextAudioBlock(const AudioSourceChannelInfo& bufferToFill) override {
        bufferToFill.clearActiveBufferRegion();
        MidiBuffer incomingMidi;
        midiCollector.removeNextBlockOfMessages(incomingMidi, bufferToFill.numSamples);

        while (midiIsPlaying) {
            double sampleRate = 48000.f, // fix this later
                startTime = midiTimeElapsed,
                timeslice = bufferToFill.numSamples / sampleRate;

            for (int t = 0; t < midiFile->getNumTracks(); t++) {
                const MidiMessageSequence* track = midiFile->getTrack(t);
                // read in events for this track
                while (true) {
                    if (currMidiEvent[t] >= track->getNumEvents()) 
                        break;
                    MidiMessage& m = track->getEventPointer(currMidiEvent[t])->message;
                    double timeOffset = m.getTimeStamp() - startTime;
                    if (timeOffset > timeslice)
                        break;
                    int sampleOffset = (int)(sampleRate * timeOffset);
                    //DBG(sampleOffset); <-- seems ok!
                    
                    incomingMidi.addEvent(m, sampleOffset); // <-- is something wrong here?
                    currMidiEvent.set(t, currMidiEvent[t] + 1);
                }
            }
            midiTimeElapsed = startTime + timeslice;
        }

        const int startSample = 0;
        const bool injectIndirectEvents = true;
        keyboardState.processNextMidiBuffer(incomingMidi, startSample, bufferToFill.numSamples, injectIndirectEvents);
        synth.renderNextBlock(*bufferToFill.buffer, incomingMidi, startSample, bufferToFill.numSamples);
    }

I can't see why no notes are playing... everything seems to check out.

π

PS EDIT: looks like I should be using MidiBuffer::addEvents so I can add a block of events in one go.  So I think my strategy should be to flatten all channels into a single MidiBuffer upon load. But https://www.juce.com/doc/classMidiBuffer#a9607fef1521aa115337f012cff244950 confuses me:

i.e. events in the source buffer whose timestamp is greater than or equal to (startSample + numSamples) will be ignored.

But MidiMessage doesn't store timestamps as samples; either it is midi ticks or seconds.


#2
    ScopedPointer<MidiBuffer> midiBuffer = new MidiBuffer();
    int samplesPlayed;
    bool midiIsPlaying = false;


    void setMidiFile(String file) {
        FileInputStream fileStream(file);
        MidiFile M;
        M.readFrom(fileStream);
        M.convertTimestampTicksToSeconds();
        midiBuffer->clear();
        
        double sampleRate = synth.getSampleRate(); // <-- tx MatKat
        for (int t = 0; t < M.getNumTracks(); t++) {
            const MidiMessageSequence* track = M.getTrack(t);
            for (int i = 0; i < track->getNumEvents(); i++) {
                MidiMessage& m = track->getEventPointer(i)->message;
                int sampleOffset = (int)(sampleRate * m.getTimeStamp());
                midiBuffer->addEvent(m, sampleOffset);
            }
        }
        samplesPlayed = 0;
        midiIsPlaying = true;
    }


    void getNextAudioBlock(const AudioSourceChannelInfo& bufferToFill) override {
        bufferToFill.clearActiveBufferRegion();
        MidiBuffer incomingMidi;
        midiCollector.removeNextBlockOfMessages(incomingMidi, bufferToFill.numSamples);

        // add events from playing midi-file
        if (midiIsPlaying) {
            int sampleDeltaToAdd = -samplesPlayed;
            incomingMidi.addEvents(*midiBuffer, samplesPlayed, bufferToFill.numSamples, sampleDeltaToAdd);
            samplesPlayed += bufferToFill.numSamples;
        }


        // pass these messages to the keyboard state so that it can update the component
        // to show on-screen which keys are being pressed on the physical midi keyboard.    
        const bool injectIndirectEvents = true;  // add midi messages generated by clicking on the on-screen keyboard.
        keyboardState.processNextMidiBuffer(incomingMidi, 0 /*startSample*/, bufferToFill.numSamples, injectIndirectEvents);
        synth.renderNextBlock(*bufferToFill.buffer, incomingMidi, 0 /*startSample*/, bufferToFill.numSamples);
    }

π


#3

dont hardcode samplerates. Always query the device to get them at runtime


#4

woopsles, forgot to implement that.

Thanks, fixed.

π