Porting to JUCE from RTMidi/VST2?

I’ve been commissioned to port an old mixer automation plugin that was made with VST2 to be updated to have 64-bit audio support, amongst other things. It uses RTMidi as well and the code dates back to 2012. The original author of this plugin has given me permission to rewrite his VST plugin to accomplish the new features, but not open source the original code. Seeing that VST3 accomplishes a lot of what’s been requested I add in, I was referred to JUCE seeing as VST3 is a pain to write code for.

JUCE seems mostly straightforward, but I’m stuck trying to figure out a counterpart to RTMidi’s callback, which, in the plugin, processes MIDI input:

typedef void (*RtMidiCallback)( double timeStamp, std::vector<unsigned char> *message, void *userData);

Judging from the API, I’m certain AudioProcessor::processBlock is the function I’m looking for, but whereas RTMidi has RtMidiIn and RtMidiOut, I’m uncertain what variables to use for JUCE. processBlock seems to take 2 buffer arguments: AudioBuffer and MidiBuffer. I’m not sure how to use either.

You are not generally supposed to call the processBlock method yourself since it’s a callback that’s automatically handled by JUCE with the plugin formats. When the method is called, the AudioBuffer has the audio you should process (into the same buffer) and the MidiBuffer has the MIDI events that are coming from the host. If you need to output MIDI, you can clear the MIDI buffer and add your new MIDI events into it. processBlock is only for audio and MIDI that the host application deals with. (JUCE allows to do standalone applications from plugin projects too, though. In that case the audio and MIDI come and go directly from/to the hardware.)

If you need to attempt something like using the audio or MIDI hardware on the computer directly, you will need to use different JUCE classes to do that. (And be prepared for it to not work within a plugin context, since the host may already have reserved the hardware for itself.)

There’s a tiny comment above the function which might help!

/** Renders the next block.

    When this method is called, the buffer contains a number of channels which is
    at least as great as the maximum number of input and output channels that
    this processor is using. It will be filled with the processor's input data and
    should be replaced with the processor's output.

    So for example if your processor has a total of 2 input channels and 4 output
    channels, then the buffer will contain 4 channels, the first two being filled
    with the input data. Your processor should read these, do its processing, and
    replace the contents of all 4 channels with its output.

    Or if your processor has a total of 5 inputs and 2 outputs, the buffer will have 5
    channels, all filled with data, and your processor should overwrite the first 2 of
    these with its output. But be VERY careful not to write anything to the last 3
    channels, as these might be mapped to memory that the host assumes is read-only!

    If your plug-in has more than one input or output buses then the buffer passed
    to the processBlock methods will contain a bundle of all channels of each bus.
    Use getBusBuffer to obtain an audio buffer for a particular bus.

    Note that if you have more outputs than inputs, then only those channels that
    correspond to an input channel are guaranteed to contain sensible data - e.g.
    in the case of 2 inputs and 4 outputs, the first two channels contain the input,
    but the last two channels may contain garbage, so you should be careful not to
    let this pass through without being overwritten or cleared.

    Also note that the buffer may have more channels than are strictly necessary,
    but you should only read/write from the ones that your processor is supposed to
    be using.

    The number of samples in these buffers is NOT guaranteed to be the same for every
    callback, and may be more or less than the estimated value given to prepareToPlay().
    Your code must be able to cope with variable-sized blocks, or you're going to get
    clicks and crashes!

    Also note that some hosts will occasionally decide to pass a buffer containing
    zero samples, so make sure that your algorithm can deal with that!

    If the processor is receiving a MIDI input, then the midiMessages array will be filled
    with the MIDI messages for this block. Each message's timestamp will indicate the
    message's time, as a number of samples from the start of the block.

    Any messages left in the MIDI buffer when this method has finished are assumed to
    be the processor's MIDI output. This means that your processor should be careful to
    clear any incoming messages from the array if it doesn't want them to be passed-on.

    If you have implemented the getBypassParameter method, then you need to check the
    value of this parameter in this callback and bypass your processing if the parameter
    has a non-zero value.

    Note that when calling this method as a host, the result may still be bypassed as
    the parameter that controls the bypass may be non-zero.

    Be very careful about what you do in this callback - it's going to be called by
    the audio thread, so any kind of interaction with the UI is absolutely
    out of the question. If you change a parameter in here and need to tell your UI to
    update itself, the best way is probably to inherit from a ChangeBroadcaster, let
    the UI components register as listeners, and then call sendChangeMessage() inside the
    processBlock() method to send out an asynchronous message. You could also use
    the AsyncUpdater class in a similar way.

    @see getBusBuffer
*/
virtual void processBlock (AudioBuffer<float>& buffer,
                           MidiBuffer& midiMessages) = 0;
4 Likes