PDSynths playing overlapping notes prevents noteOff from working

Another audio bug I'm afraid. This one is a bit of a showstopper for me, because it causes notes get stuck on, resulting in messed up music.

I've discovered that having two PDSynths playing overlapping notes prevents noteOff() from functioning, at least when both playNote / playMIDINote and noteOff have been scheduled for some point in the future, rather than passing 0 to their when parameter. Here's some code to put in the update callback that appears to reproduce the bug 100% of the time:

static PDSynth *synth1 = NULL, *synth2 = NULL;
static uint32_t lastTime = 0;
static int goodFrameCount = 0;
static uint32_t startTime = 0;
uint32_t currentTime = pd->sound->getCurrentTime();

if (!synth1 || !synth2) {
    synth1 = pd->sound->synth->newSynth();
    pd->sound->synth->setWaveform(synth1, kWaveformTriangle);
    pd->sound->channel->addSource(pd->sound->getDefaultChannel(), (SoundSource *)synth1);
    
    synth2 = pd->sound->synth->newSynth();
    pd->sound->synth->setWaveform(synth2, kWaveformTriangle);
    pd->sound->channel->addSource(pd->sound->getDefaultChannel(), (SoundSource *)synth2);
}

if (startTime == 0) {
    // Need to make sure we're getting consistently spaced frames after startup
    // before proceeding:
    goodFrameCount = (currentTime - lastTime > 500) ? goodFrameCount+1 : 0;
    if (goodFrameCount == 5) {
        pd->system->logToConsole("Starting...");
        startTime = currentTime;
    }
}

if (startTime != 0) {
    #define STEP(x) (startTime + 5000*x)
    
    if (currentTime >= STEP(0) && lastTime < STEP(0)) {
        pd->system->logToConsole("playing synth 1");
        pd->sound->synth->playMIDINote(synth1, NOTE_C4, 0.5, -1.0, STEP(2));
    }
    if (currentTime >= STEP(2) && lastTime < STEP(2)) {
        pd->system->logToConsole("playing synth2 / stopping synth1");
        pd->sound->synth->noteOff(synth1, STEP(5));
        pd->sound->synth->playMIDINote(synth2, NOTE_C4 + 2, 0.5, -1.0, STEP(4));
    }
    if (currentTime >= STEP(4) && lastTime < STEP(4)) {
        pd->system->logToConsole("playing synth1 / stopping synth2");
        pd->sound->synth->noteOff(synth2, STEP(7));
        pd->sound->synth->playMIDINote(synth1, NOTE_C4 + 4, 0.5, -1.0, STEP(6));
    }
    if (currentTime >= STEP(6) && lastTime < STEP(6)) {
        pd->system->logToConsole("stopping synth1");
        pd->sound->synth->noteOff(synth1, STEP(9));
    }
    if (currentTime >= STEP(10) && lastTime < STEP(10)) {
        if (pd->sound->synth->isPlaying(synth1)) {
            pd->system->logToConsole("Oh no! synth1 is still playing!");
        }
    }
}

lastTime = currentTime;

(Note: the bit with startTime is there because I've noticed the Playdate Simulator doesn't always start processing frames at consistent intervals for the first few seconds it's running. That's just there to make sure everything is running smoothly before proceeding with playing any audio, since the code is timing dependent.)

If I set the when parameter of playMIDINote to 0, then the bug doesn't occur. So this seems to have something to do with notes being scheduled to play in the future.

So far I still haven't found a workaround, but I'm going to keep bashing my head against it, since my whole music engine is reliant on being able to schedule playing and releasing notes.

edit: This very well may be my bug actually -- still investigating

Yes, this is my bug. Overlapping notes wasn't even the trigger, but rather telling a PDSynth to play at a point in the future when it still had a "note off" event scheduled. The example code I posted did this, so naturally it reproduced the issue 100% of the time!

However, the real issue I was trying to solve was that a "note off" event wouldn't work even when I wasn't scheduling a "note on" while there was still a pending "note off", and when this happened was inconsistent and difficult to consistency reproduce. What I discovered is that scheduling it to play when it has a "note off" event that was set to occur around 500 samples / 11 milliseconds ago can still trigger the issue where a "note off" won't work. I figure this is some kind of race condition between the main thread and audio thread, and I can avoid the issue by just being careful about when I schedule a note on or off.

Basically, if I avoid scheduling a "note on" so long as ~1000 samples or ~20 milliseconds have passed since a "note off" was scheduled, everything seems to work okay. I can increase that window if necessary.