Simulator crashes with music sequence callback with bad function

This is on SDK/simulator 1.9.3 on MacBookPro running MacOS 11.2.2

when i run the attached code, and you press the A and B buttons it plays various note sequences. I know this isn't the right way to do this, but it probably shouldn't kill the simulator.

Press A or B and it spins up a new synth, track, and sequencer, and Play()'s it. the Play has a callback that gets called when the note sequence ends.

If you wait until the playing sequence ends, the callback works just fine.

So if you press the button before the previous sequence ends, it seems to overwrite it (i'm not entirely sure how the engine works or Lua handles garbage collection and all of that, so i'm speculating a lot here).
When the sequence ends, it attempts to call the callback for the ending sequence, but that one has already been replaced by a new one.

I think the garbage collection is getting rid of the old one, since it's a local within the function, so when it goes to get the callback function name, it faults out.

change the line within the function from

    local seq = snd.sequence.new()

to just

   seq = snd.sequence.new()

then it will use the "local" version of the variable outside of that function instead, due to scope. It will then fail but in a safe way, with "attempt to call a nil value" in red in the console. The running pdx is crashed, but the simulator is graceful about it.

I would expect it to fail more gracefully, perhaps the sequence just ends without the callback being called, or some other behavior... Having the sequencer just up and die is... unexpected and surprising, ;D

Regardless, I don't expect what I'm doing to work. This isn't an issue with the audio code or garbage collection or anything like that... but of how the simulator handles the bad variable. :}


import "CoreLibs/utilities/where"

import "CoreLibs/object"
import "CoreLibs/timer"
import "CoreLibs/ui"

------------

class('Tunez').extends( "Object" )

local all_songs <const> = {
    default = {
        tempo = 12,
        env = { a=0.0, d=0.3, s=0.0, r=0.2 }, -- standard tone
    },
    cursor = {
        prio = 0,
        tempo = 2,
        env = { a=0.0, d=0.1, s=0.0, r=0.2 },   -- bleep
        seq = { 'A4' }
    },
    pitfall = {
        prio = 25,
        tempo = 48,
        seq = { "G5", "F#5", "F5", "E5", "D#5", "D5", "C#5", "C5",
                "B4", "A#4", "A4", "G#4", "G4", "F#4", "F4", "E4",
                "END"
        }
    },
}
snd = playdate.sound


function tnz_finished_cb( seq )
    -- trampoline
    print( "CB ", seq)
end

local seq = nil

function PlaySong( nom )
    print( seq )
    if( seq ~= nil ) then
        seq:stop()
        seq = nil
    end

    local the_song = all_songs[ nom ]
    if( the_song == nil ) then return end

    local synth1 = playdate.sound.synth.new( playdate.sound.kWaveSquare )
    local env = the_song.env or all_songs.default.env
    synth1:setADSR( env.a, env.d, env.s, env.r )

    t = snd.track.new()
    t:setInstrument( synth1 )

    local ts = 0
    for i,p in pairs( the_song.seq ) do
        if( p ~= 'END' and p ~= 'REST' ) then
            t:addNote(1+ ( ts*3), p, 2 )
        end
        ts += 1
    end

    local seq = snd.sequence.new()
    seq:setTempo( the_song.tempo or all_songs.default.tempo )
    seq:addTrack(t)
    seq:play( tnz_finished_cb )
end

------------
local gfx = playdate.graphics

gfx.setColor(gfx.kColorBlack)

function playdate.update()
    playdate.timer.updateTimers()
    gfx.sprite.update()

    gfx.fillRect(0, 0, 400, 240)
    playdate.drawFPS(0,0)
end

function playdate.AButtonDown()
    print( "A" )
    PlaySong( 'pitfall' )
end
function playdate.BButtonDown()
    print( "B" )
    PlaySong( 'cursor' )
end

Oof. That's a hairy one. The root problem is that the Lua layer was only expecting one sequence played at a time (even though the audio code supports multiple) so it has a single global reference to the last sequence that play() was called on. But that should be fine in your case since you're stopping the first one before playing the next, right? Nope, there's another wrinkle: when you call stop() on the first one the action is actually delayed until the end of the run loop so that the audio code can call it while the Lua stack isn't active. So play() sets the reference to the new sequence, but then the delayed stop() clears it, and then the garbage collector throws the sequence away even though it's still playing.

I just need to change that single reference to a table of playing sequences and it should work as expected.

Thanks for catching this, and especially for the straightforward repro example!

1 Like

oh geez. Even weirder, this only happens when the sequence has a finish callback. When it doesn't the old reference leaks instead of clearing. What a mess.

1 Like

Yeah... sorry. :grimacing:
godspeed. :wink:

1 Like