How to connect slider to background MIDI process


#1

I'm preparing materials for a new course I'm teaching at Carleton College called Audio Programming in C++. I'd like to use JUCE for the GUI. I've been writing MIDI programs since 1986 and am trying to reproduce a desktop MIDI app that I originally wrote in WxWidgets. Here's a little background. The program is called MIDIDisplay allows students to enter text data in the following format:

     Time Status Data1 Data2

Those four numbers (int32, byte, byte, byte) are separated by tab characters and represent the time stamp and MIDI message. Students learn to deal with time stamps in milliseconds, difference between events in milliseconds, and PPQ. 

I've only been using JUCE for a couple weeks and already have the user interface written and have MIDI input and output working correctly. Text is entered into the CodeEditorComponent and is then read into a MidiMessage and added to a MidiBuffer. The output works perfectly using MidiOutput::sendBlockOfMessages(). 

My problem is how to connect the Tempo slider widget that controls the playback tempo to the MidiOutput background thread. I can freely move the tempo slider as the file is playing and see the slider update the tempo in the GUI; but the MidiOutput background thread never gets the tempo update. I can't seem to find a way to hook into the MIDI playback thread to make it see the tempo change.  Can you give me some guidance or point me to some example code?

I'm using the current version git version of JUCE from https://github.com/julianstorer/JUCE.git on Mac OX X 10.10.5.


#2

What kind of background thread are you using for the midi output? Are you using a predefined Juce class, or have you created your own background thread?

The thread, which the MidiOutput class is using is private. So I am not sure if it is possible to modify its timing.

Two solutions come to mind: You could use MidiOutput::sendMessageNow() and create you own background thread. Or you could change the timestamps of the midi events...

 


#3

I am using the default MidiOutput background thread and I realize it cannot not be sublcassed. 

In my previous wxWidgets program all  times were stored/saved using a tempo of 60 bpm with a quarter equaling 1000 ms. MIDI was sent using a process similar to the JUCE sendMessageNow function. Essentially the program used timers to fire the next MIDI message at a future time and responded to user interface changes in between.. In that system I had no problem getting updates from the tempo slider and adjusting MIDI time stamps on the fly. When I've tried using the MidiOutput:;sendMessageNow() and th Time::waitForMillisecondCounter() functions the slider is unresponsive. MIDI plays but I get the spinning beach ball while the MIDI plays and I cannot adjust the slider until it stops. This code works with sendBlockOfMessages and the GUI is responsive but the GUI cannot send to the private MidiOutput background thread.

void MidiUtils::TxAll()
{
    const bool LINE_BY_LINE = true;
    
    if ( LINE_BY_LINE )
        TxLineByLineMs();
    else
    {
        midiout->stopBackgroundThread();
        midiout->startBackgroundThread();
        MidiBuffer::Iterator MI(mbuf);
        MI.setNextSamplePosition(0);
        //        MidiMessage msg;
        
        double timeNow = (double)Time::getMillisecondCounter();
        const double samplesPerSecondForBuffer = 44100.0;
        midiout->sendBlockOfMessages( mbuf, timeNow, samplesPerSecondForBuffer );
    }
}

This code also works if I set LINE_BY_LINE = false above but I get the spinning beach ball with no GUI interaction. This is the TxLineByLine code using sendMessageNow and waitForMillisecondCounter.

void MidiUtils::TxLineByLineMs()
{
    // uses difference Ms time
    std::vector<MidiPacket> mpv = createDiffMsVector();
    
    const bool PRINT_DIFF_MS_VECTOR = false;
    if (PRINT_DIFF_MS_VECTOR) {
        for ( auto iv : mpv)
            printMidiPacket( iv );
    }
    
    midiout->stopBackgroundThread();
    midiout->startBackgroundThread();
    int nowTime;
    int pktTime;
    for ( auto iter : mpv )
    {
        nowTime = Time::getMillisecondCounter();
        pktTime = static_cast<int>( ( (iter.timeStamp * 60.0) / global::tempo ) );
        Time::waitForMillisecondCounter( nowTime + pktTime );
        
        midiout->sendMessageNow( MidiPacketToMidiMessage( iter ) );
    }
}

The MidiPackts vector items are structured like this:


JUCE v3.2.0
991    c0    32    0
0    90    28    114
9    99    40    90
41   c1     0      0
0    91    28    74
34   c7    105    0
0    97    59    66
25   97    67    74
233  97    67      0

The columns are respectively: timeStamps in ms, status, data1, data2 (data2 is ignored for the 0xCn  program change messages). The routine gets the time stamp from the first column and waits that amount of time before sending the next three bytes as a MIDI message. The code works, MIDI plays correctly, but the cursor becomes the spinning beach ball and the slider is unresponsive  so the tempo never gets updated.

The JUCE code for Time::waitForMillisecondCounter() does not seem to be yielding to GUI interaction.


void Time::waitForMillisecondCounter (const uint32 targetTime) noexcept
{
    for (;;)
    {
        const uint32 now = getMillisecondCounter();
        if (now >= targetTime)
            break;
        const int toWait = (int) (targetTime - now);
        if (toWait > 2)
        {
->            Thread::sleep (jmin (20, toWait >> 1));
        }
        else
        {
            // xxx should consider using mutex_pause on the mac as it apparently
            // makes it seem less like a spinlock and avoids lowering the thread pri.
            for (int i = 10; --i >= 0;)
->                Thread::yield();
        }
    }
}

I've tried experiments changing numbers in both lines with the -> with no luck.

I'm sure this problem has been solved by others using the JUCE framework but I'm not seeing an obvious solution at this point. Any ideas?


#4

If you are getting the spinning beach ball, then I am guessing that your GUI and you Midi code is running on the same thread.

In my Midi application I decoupled all Midi stuff from my GUI and gave it its own thread. That means, my Midi handling cannot freeze the GUI. Here is an excerpt from my code. Hopefully it can help you:

The class declaration:

class MidiThread : public juce::Thread
{
public:
    MidiThread();
    
    /** Inheritedt from juce::Thread.
        Here all the Midi logic is implemented.
    */
    void run() override;

private:

    /** The callback/collector for all midi events.
    */
    MidiMessageCollectorMod midiCollector;
    /** the midi player
    */
    MidiPlayer midiPlayer;
    /** "Sample rate" to use
    */
    static const double sampleRate;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(MidiThread);
};


and the implementation:

const double MidiThread::sampleRate = 1000;

MidiThread::MidiThread()
    : Thread("MidiThread")
{
    //highest possible priority for the midi thread
    setPriority(10);
    midiCollector.reset(sampleRate);
}

void MidiThread::run()
{
    double lastCallbackTime = Time::getMillisecondCounterHiRes();

    do
    {
        double timeNow = Time::getMillisecondCounterHiRes();
        double deltaTime = (timeNow - lastCallbackTime) * 0.001;
        //make sure, we dont run this faster than once every millisecond.
        while (deltaTime < 0.001)
        {
            wait(1);
            timeNow = Time::getMillisecondCounterHiRes();
            deltaTime = (timeNow - lastCallbackTime) * 0.001;
        }
        lastCallbackTime = timeNow;

        int numSamples = (int)(deltaTime * sampleRate);
        MidiBuffer incomingMidi;
        //Get data from the midi keyboard, which is connected as a MidiInput device. 
        midiCollector.removeNextBlockOfMessages(incomingMidi, numSamples);
        //Forward the Midi data to my Midi player
        midiPlayer.processNextMidiBlock(incomingMidi, 0, numSamples);

        if (threadShouldExit())
            return;
    } while (true);

}

As you can see, the run method of my Midi thread is doing an endless while loop. You can implement a similar Midi thread in you application.

Somewhere in you application you instantiate a MidiThread object and call its midiThread.startThread() method, in order to start the run method.

NOTE: my application is not a Midi player but a Midi pipe. So I am just forwarding the Midi events which come from the keyboard (and modify them). In your case you would have to add logic to the run method, which handles the Midi data you application creates.