Synth::noteOn stalling UI thread at launch (only)


#1

Upon start-up my app  (EDIT: on Windows) displays a window and plays a CEG triad via the synth's noteOn.

Strangely, the window only displays at the moment the sound finishes.

If I enclose the noteOn call within an AsyncUpdate, it synchronises correctly!

This puzzles me, I've written a synth (in C# yuck) and noteOn should be nonblocking as far as I can see (just add the event to a queue, which will be processed during the audio render callback).

What am I missing?

π

PS if I wire up a button to play the chord, it doesn't interfere with the UI -- the UI is responsive as it plays.


#2

Sounds like you're a bit shaky on threading and how the message thread works. Golden rule: Don't block the message thread!!


#3

Certainly I haven't worked much with threading. Most of my understanding comes from browsing "Linux Kernel Internals" by Tigran Veritas about 20 years ago. But I think I understand the basic principles...

I am failing to integrate the hint:  I'm not using the message thread -- I'm invoking Synth::noteOn from my main component's constructor (EDIT: just learned that this IS the message thread!), and I can't imagine how any blocking could possibly occur in my code. noteOn should surely add a MIDI event to a 'pending' queue (EDIT: my bad, an 'active/playing' list is what I meant) and return instantly. DBG before and after verifies it returns instantly.

Here is my code:

struct PiSynthAudioSource : public AudioSource, private AsyncUpdater {
    Synthesiser synth; int _note;
    void playToneme(int n) {
        _note = n;
        //triggerAsyncUpdate();  // works <-- UNCOMMENT ONE!
        //playNote();            // fails
    }
    void handleAsyncUpdate() override { playNote(); }
    void playNote() {
        DBG("A");
        synth.noteOn(0, 60 + _note, 1.0f);
        DBG("B"); // always AB together, so no delay here!
    }

    void getNextAudioBlock(const AudioSourceChannelInfo& bufferToFill) override {
        bufferToFill.clearActiveBufferRegion();
        MidiBuffer incomingMidi; // not used! 
        synth.renderNextBlock(*bufferToFill.buffer, incomingMidi, 0, bufferToFill.numSamples); 
    }
};

I'm bypassing populating a MidiBuffer in getNextAudioBlock (as I don't meet precise event timing).

I suspect this may be where the problem is.

But I can't see why my approach is failing.

π


#4

Pi, Synthesizer's noteOn method doesn't work the way you are thinking. It starts a note syncronously - there is no 'pending' MIDI queue in Synthesizer. If you look inside the definition of Synthesizer::noteOn you will see that it takes a lock. 

This means that in your constructor with noteOn calls, you are waiting to take a lock from the audio thread. Since the message thread is a lower priority thread than the audio thread, this might take some time! Usually I don't have a problem doing this in my running program as the wait isn't noticable, but you are doing it before your UI has instantiated, which might be the cause of your problem.

Suggested ways around this? Perhaps the easiest would be to try calling noteOn somewhere after your UI has been instantiated :)


#5

I dug into the source code. But I still can't see any explanation for what is going on.

It seems that I need to allow the system to complete initialisation before I play a sound.

I've avoided direct noteOn calls by using MidiCollector:

    MidiCollector midiCollector;
    void getNextAudioBlock(const AudioSourceChannelInfo& bufferToFill) override {
        bufferToFill.clearActiveBufferRegion();
        MidiBuffer incomingMidi;
        midiCollector.removeNextBlockOfMessages(incomingMidi, bufferToFill.numSamples);
        synth.renderNextBlock(*bufferToFill.buffer, incomingMidi, 0, bufferToFill.numSamples);
    }
    void playNote(int n) {
        MidiMessage m = MidiMessage::noteOn(1, 60 + n, 1.f);
        m.setTimeStamp(Time::getMillisecondCounter() / 1000.f);
        midiCollector.addMessageToQueue(m);
    }

 ... but it doesn't solve the problem.

Synth::noteOn does create a lock, but that's just to prevent re-entrancy. I can't see how that could stall anything.

btw there is no need to solve this. I don't need to play a sound upon start-up. It was just a test. But tracking down UB is usually worthwhile.

π

PS I can't resolve what widdershins is saying: "This means that in your constructor with noteOn calls, you are waiting to take a lock from the audio thread. Since the message thread is a lower priority thread than the audio thread, this might take some time!"

"Waiting to take a lock from the audio thread" <-- but you don't take a lock from a thread. A thread acquires a lock. And then any other thread that hits the lock spins until your initial thread has released it. This code is just saying "If multiple threads are simultaneously invoking noteOn, get them in line! Process them sequentially!"


#6

Stalls and deadlocks are usually pretty trivial to debug.. Just pause it in the debugger and see what's waiting, right?


#7

Good idea!

https://github.com/julianstorer/JUCE/blob/master/modules/juce_audio_devices/native/juce_win32_WASAPI.cpp#L1242

^ this is what's waiting... (tested three times).

π


#8

No.. you're misinterpreting that. Obviously it'll wait there often, but won't be stuck there.


#9

I'm going to let this one go for the time being, as this (to me) unexpected behaviour isn't actually causing me any trouble.

I wanted to do a bit of digging so as to leave this thread in a resolved state, also I wanted to check I wasn't doing something blatantly wrong, also because I might learn something useful.

But it's starting to look like effort that I should probably be spending more productively elsewhere. Plenty more basic machinery for me to assimilate before I start looking for trouble.

π