MidiLoggerPluginDemo

Hi Folks
I’m pretty new to JUCE but excited with the Linux vst3 support under Linux.

My questions below are probably not restricted to Linux.

I was able to get the MidiLoggerPluginDemo to work (and created a local project based on that). It runs fine under Reaper but has issues under Bitwig (as I understand that is a Bitwig problem, and a solution is on the way).

Just by looking at the example code, there is quite a lot to digest. The example is particularly interesting since it demonstrates the interaction between the processor and editing parts.

My first question is about the safe sharing of data between those. So, the top/main class is based on the MidiLoggerPluginDemoProcessor. The Editor is added as a private member, and uses ownerIn to refer to the “Processor”.

  1. Is that any different from having the Processor and Editor separated, or just a matter of taste which approach is better?

Midi messages are pushed onto the queue (in the process event handling). Then the queue is poped in timerCallback and put into the model. (The model contains the whole history, while the queue will just contain the midi messages in between timerCallbacks, if I understand correctly.)

  1. So what is the threading model here? Can timerCallback be pre-empted by a process invocation. If so, is the backing implementation thread safe (e.g., by some internal lock, or backed lock free data stuctures). In the former case, what if the timerCallback would block the push. (I assume the AbstractFifo would be holding the solution, but a TL;DR would be great.)

  2. What would the OOM behavior be, is it graceful if the queueSize is reached (e.g., no more data will be pushed on the fifo, or will we have a controlled crash/panic, or will we have true memory unsafety (that it will write to somewhere in the memory, and only god knows what happens …)

I understand that those PIP’s are intended to just showcase minimal examples and thus do not attempt to explain everything in great detail. However I’m confused by the comment.

// Stores the last N messages. Safe to access from the message thread only

  1. What exactly is the message thread here, and what other threads should we consider when writing VST3 code.

I’m not a seasoned C++ programmer, but have a solid Rust background (it’s usually the other way around :), so bare with me for me not seeing the obvious here…

Any answers, and/or pointers greatly appreciated.
/Per

It’s normally better practice to keep the processor/editor slightly more separated. Normally, the editor shouldn’t be a member class type. The structure I’d recommend for ‘real-world’ projects would be to have a ‘basic processor’ class with no knowledge of the GUI. There should be a separate ‘editor’ which depends on this basic processor, and finally a ‘processor with editor’ class that just overrides the createEditor and hasEditor methods from the ‘basic processor’ to return an editor instance. The demo doesn’t do this just to keep things concise.

The synchronisation is handled by the ‘AbstractFifo’ contained in the ‘MidiQueue’. It is safe to call ‘MidiQueue::push’ and ‘pop’ simultaneously from separate threads, as long as there’s guaranteed to be a single reader and a single writer. We ‘push’ messages from processBlock, and pop in the timerCallback. Importantly, push is wait-free, so it’s safe to call from the audio thread.

If the queue is full for some reason, the AbstractFifo will report that there is no more space available, and it will not allow any messages to be added. write (1) will return an object containing the actual number of available spaces in the queue (either 1 or 0). The lambda passed to forEach will be called with the index of each available space. If there are no spaces available, the lambda will not be called.

fifo.write (1).forEach ([&] (int dest) { messages[(size_t) dest] = metadata.getMessage(); });

For the most past, you need to consider the ‘message thread’ and the ‘realtime thread’. Most GUI-related functions will be called from events triggered on the message thread (mouse button callbacks, keyboard callbacks, Timer::timerCallback…). The processBlock function will be called from the realtime thread.

There are a handful of plugin functions that must be thread-agonostic. These include parameter change callbacks, and the get/setStateInformation functions. Such functions may be triggered on the message thread or realtime thread, or potentially on a third thread, depending on the plugin host. The Midi Logger demo doesn’t have parameters or presets, so it doesn’t have any special handling for these functions. For more complicated plugins, I’d recommend testing with thread sanitizer in combination with Tracktion’s Pluginval.

2 Likes

Thanks a bunch @reuk, your answers were spot on!

I’ve now started a new project from scratch, and trying to log incoming midi events (in this case control messages). It works as expected, but I’m not sure I do it the idiomatic way. (Never really used C++ before).

void Test2AudioProcessor::processBlock(juce::AudioBuffer<float> &buffer, juce::MidiBuffer &midiMessages)
{
...
 for (const auto metadata : midiMessages)
    {
        auto msg = metadata.getMessage();
        if (msg.isController())
        {
            if (m_flogger)
                m_flogger->logMessage(msg.getDescription());
        };
    };

So of course, it’s a bad idea to do the logging from the realtime thread, but it seems to be fast enough not to crash.

Besides that, I’m curious how this iterator thingy really works, you get a handle (metadata) that you need to “promote” (not sure if that is the right name for it), to a message.

Is there a way to get a MidiMessage directly from the iterator?

I was trying

    auto iterator = juce::MidiBuffer::Iterator(midiMessages);
    juce::MidiMessage msg;
    int sampleNum;

    while (iterator.getNextEvent(msg, sampleNum))
    {
        if (msg.isController())
        {
            if (m_flogger)
                m_flogger->logMessage(msg.getDescription());
        }
    }

It also worked but that seems to be deprecated.

Also the second alternative is very strange/sketchy, as you are mutating msg and sampleNum under some implicit mutable ref, which just feels error prone and dangerous.

I’m looking for some more elegant way (the first alternative is almost there, but I don’t understand howto get rid of the getMessage part, I don’t even understand why I would need it).

/Per

The first version is the recommended approach, although logging directly in the audio callback is dangerous unless you can guarantee that the logging mechanism is wait-free. The ‘getMessage’ part is necessary because the buffer doesn’t really contain individual MidiMessages, it just stores a series of bytes. When you iterate over the buffer, the ‘metadata’ object just points to the first byte in each midi message. Creating a full MidiMessage instance is cheap but not free, and isn’t required in all contexts (sometimes we just want the message timestamps or the raw bytes), which is why you must call getMessage explicitly to create an instance of MidiMessage from the metadata instance. (Maybe I should add that to the docs.)

Thanks a bunch @reuk, that really helped my understanding. So the first alternative is the idiomatic way then. (I guess this is not my last question, since I’m totally new to all this…)

… and one more.

I want to inspect the controller data, in the trace I get.

Controller 30: 127 Channel 1
Controller 30: 1 Channel 1

Which is exactly what I expect.
I want to match the controller data, and update the state accordingly. Is the way to go here to getRawDataSize, and getRawData. It seems to work.

 for (const auto metadata : midiMessages)
    {
        auto msg = metadata.getMessage();
        if (msg.isController())
        {

            if (m_flogger)
            {
                m_flogger->logMessage(msg.getDescription());
                for (int i = 0; i < msg.getRawDataSize(); i++)
                {
                    juce::uint8 val = msg.getRawData()[i];
                    m_flogger->logMessage(std::to_string(val));
                };
            };
        };
    };

A slice with iterator would have been better, or even some overlay to deconstruct a control message. What is the best option here.

What I eventually want to do is to output the accumulated value as a new controller, or maybe automation parameter, with fairly high precision (float32 would do I guess). I read somewhere that VST3 was a bit dodgy on plugins producing midi, what is the rationale.

If I want to generate automation parameters would the process be different? If so are there any good examples on plugins output:ing automation/control values for recording in the DAW?
/Per

By the way the output is now something like.

Controller 30: 127 Channel 1
176
30
127

Relating memory safety. I was looking at the AudioPluginDemo. There is a variable lastPosInfo being updated in the audio thread while being periodically polled by the editor thread. Is the backing implementation atomic somehow, or is there some other mechanism at work here to avoid race, or is it just that we don’t care if there is race, since in the editor we just display the value. (Maybe its well behaved so it cannot crash the formatting, even if being inconsistent.)

I think this is a bug actually, well spotted! This needs some synchronisation to ensure that we don’t try to read lastPosInfo while it’s being written from the audio thread.

While at it. (Should perhaps be a separate thread, but I ask here in any case.) What about sample accurate automation. The example https://docs.juce.com/master/tutorial_audio_parameter.html seem to magically update parameters. I assume it’s done before the process gets called. So the param would be “valid” for the block being processed. Is there more to it. Steinberg claims to support sample accurate automation (which I need for my project). How would I best do that in JUCE (if at all)?
/Per

JUCE doesn’t currently support sample-accurate automation. The host may update parameter values at any time, and most plugins (that I’ve seen, anyway) will read the current parameter values at the top processBlock and then use those values for the duration of the processBlock call.