Block-based midi processing in a right way?

Is MidiMessageCollector is somehow useful in midi processing or is it just for JUCE's internals?

I need a structure to accumulate midi events produced by a plugin that I'll then pass to host (MidiBuffer) according to block's sample offset on a per-block basis.

I currently use MidiSequence and it's not so good in per-block-processing or maybe I use it in a wrong way.

What could I tell you that isn't already explained in the class's comments? It does what exactly it says it does - i.e. collects messages and packages them into blocks. Whether or not that's useful for what you're doing depends on what you're doing!

I have an idea for a midi effect plugin, but as in any case I need to start from the basics tasks.

So, the first tasks i put on myself is to do a simple midi-arp functionality like this:
1. plugin receives midi msg note-on (C3 - for example)
2. plugin produce (1/16 - for example) midi note (C3) on/off msgs and sends them to host constantly 
while input midi msg note-off (C3) not received.

midi event   :     on       off    on off
plug's input :     C . . . . C     C - C      
plug's output:     C . C . C .     C . C       (1/16th notes)

I'm struggling to find the right structure to hold produced midi messages, so it will be use to use it in a processBlock() to forward them to host. 

(currently I store produced midi messages in a MidiMessageSequence, but I have problem with keeping right offset)

For icoming midi messages I have in-house MidiReceiver class that handles all the necessary info (pressedKeysNumber, lastNoteNumer, lastVelocity, lastNoteOffSamplePos, etc...) that a plugin will need to interpret.

 

Here is my processBlock() and it looks/works not good: smiley


void MyPluginAudioProcessor::processBlock (AudioSampleBuffer& buffer, MidiBuffer& midiMessages)
{
    const int blockSamples = buffer.getNumSamples();

    // TODO: clean this...
    double noteMiliseconds      = (60000.0 / lastPosInfo.bpm) * rate;
    double samplesPerMilisecond = getSampleRate(); 
    samplesPerMilisecond /= 1000.0;
    int noteSamples = noteMiliseconds * samplesPerMilisecond;

    // Now pass any incoming midi messages to our keyboard state object, and let it
    // add messages to the buffer if the user is clicking on the on-screen keys
    keyboardState.processNextMidiBuffer (midiMessages, 0, blockSamples, true);

    // track incoming midi messages...
    midiReceiver.onMessagesReceived (midiMessages);
    midiMessages.clear();

    // This is the place where you'd normally do the guts of your plugin's
    MidiMessage midiMsg (0xf0);
    int         midiMsgPos = 0.0;

    // reset offset on the first pressed key...
    if (midiReceiver.getFirstNoteOnSamplePos() >= 0)
    {
        offsetSamples = midiReceiver.getFirstNoteOnSamplePos();
        midiReceiver.setFirstNoteOnSamplePos (-2); // set to undefined state
    }

    // clear midi sequence events after last key was released...
    for (int i = 0; i < midiSequence.getNumEvents(); ++i)
    { 
        midiMsg    = midiSequence.getEventPointer(i)->message;
        midiMsgPos = midiSequence.getEventTime(i);
        if (midiMsgPos > midiReceiver.getLastNoteOffSamplePos())
        { // after last key was released...
            if (midiMsg.isNoteOff())
            {
                midiMessages.addEvent (midiMsg, midiReceiver.getLastNoteOffSamplePos());
            }
            midiSequence.clear();
            midiReceiver.setLastNoteOffSamplePos (INT_MAX);
            break;
        }
    }

    // add new events to midi sequence...
    if (midiReceiver.getPressedKeys() > 0 && midiSequence.getNumEvents() == 0)
    { 
        midiMsg = MidiMessage::noteOn (1, midiReceiver.getLastNoteNumber(), midiReceiver.getLastVelocity());
        midiSequence.addEvent (midiMsg, offsetSamples);
        offsetSamples += noteSamples;
        midiMsg = MidiMessage::noteOff (1, midiReceiver.getLastNoteNumber());
        midiSequence.addEvent (midiMsg, offsetSamples);
        offsetSamples += noteSamples;
        midiSequence.updateMatchedPairs(); // connect the note-on and note-off messages 
    }

    // add midi sequence's events to midi buffer and update offsetSamples of events...
    for (int i = 0; i < midiSequence.getNumEvents(); ++i)
    { 
        midiMsg    = midiSequence.getEventPointer(i)->message;
        midiMsgPos = midiSequence.getEventTime(i);
        if (midiMsgPos < blockSamples)
        { // event should appear in current block
            midiMessages.addEvent (midiMsg, midiMsgPos);
            midiSequence.deleteEvent (i--, false);
            // TODO: update event's offsets (and offsetSamples?)
            midiSequence.updateMatchedPairs();
        }
        else
        { // advance midi sequence's events to next block
            midiSequence.addTimeToMessages (-blockSamples); // TODO: update event's offsets (and offsetSamples?)
            midiSequence.updateMatchedPairs();
            break;
        }
    }
}

People, please help!smiley

My first advice is refine and concentrate on the main thing which as I understand is the arpegiator. Skip all other stuff like keyboardstates etc. until later.

I haven't worked on any midi plugins myself but this is how I would go about it:

  1. In processBlock, wait for a note-on event.
  2. When received, record its samplePos and add ½ second (44 100/2 = 22 050) 
  3. Wait for the frame that contains this samplePos
  4. Add a note-on event to the MidiBuffer and another 22 050 to the samplePos
  5. Wait for the frame that contains this samplePos
  6. Add a note-off event to the MidiBuffer and another 22 050 to the samplePos
  7. Goto 3.
  8. Loop until a note-off is recived, add a note-off event to MidiBuffer if the last event added was a note-on, then go back to 1.

samplePos is a running counter that you increment w buffer.getNumSamples() for every processBlock call.

Make this work for any single key. Then make it work various bpm's (that should alter the ½ seconds). Then make it work for several simultaneous keys, different velocities etc.

 

My head feels better now so I'll have to rethink my processBlock(). smiley