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.