audio.play not calling onComplete when called in coroutine?

HI! I’d like to use coroutines for playing some sounds such that one sound plays one right after the other, sequentially. In other words, the onComplete callback should trigger the next sound to play. But I can’t get the audio onComplete callback to work within a coroutine. Here’s a very simplified version of my code:

local thread  
  
function audioCompleted( event )  
 print( "audioCompleted!" ) -- See, for some reason this never gets printed.  
 coroutine.resume( thread )  
end  
  
function myCoroutineFunction()  
 audio.play( audio.loadSound( "sound1.mp3" ), {onComplete=audioCompleted} )  
 coroutine.yield() -- Come back here after the callback has happened.  
 audio.play( audio.loadSound( "sound2.mp3" ), {onComplete=audioCompleted} )  
 coroutine.yield()  
 --- Etc...  
end  
  
thread = coroutine.create ( myCoroutineFunction )  
coroutine.resume( thread )  

The first sound plays, but onComplete is never called. Without coroutines, onComplete does get called. I’m pretty new at this stuff, especially coroutines. Am I asking too much by registering a callback and then yielding?
Thanks for any help. [import]uid: 131050 topic_id: 35305 reply_id: 335305[/import]

With an event based model, you could probably skip the coroutines.

In the case of your code, you’d be better off utilizing the onComplete handler like so:

[lua]–forwards
local soundClips = {}
local clipIndex = 1
local totalSounds = 5 --whatever your limit is

–load em up
table.insert( soundClips, audio.loadSound( “sound1.mp3” ) )
table.insert( soundClips, audio.loadSound( “sound2.mp3” ) )

local function onSoundComplete( event )
–inc
clipIndex = clipIndex + 1
if ( clipIndex > totalSounds ) then
clipIndex = 1
end

audio.play( soundClips[clipIndex], onComplete = onSoundComplete )
end

–init first sound
audio.play( soundClips[clipIndex], onComplete = onSoundComplete )[/lua]

Code is untested, and rough, but hopefully that can help.

Cheers. [import]uid: 202223 topic_id: 35305 reply_id: 140361[/import]

This is a limitation in Corona. Corona was not designed to call Lua listeners in coroutines. And changing Corona’s design to support it would be such a huge undertaking that its not worth it on our end. So, my advise is to not use coroutines for something like this.
[import]uid: 32256 topic_id: 35305 reply_id: 140396[/import]

Thank you develephant. That’s pretty much what I ended up doing instead, and it works fine. I appreciate the response! [import]uid: 131050 topic_id: 35305 reply_id: 140426[/import]

Ok thanks Joshua, I’m so glad I asked quick instead of banging my head against this for days! I ended up going another route than coroutines.

(Just to clarify, I was anticipating the listener being called in the “main thread,” not the coroutine. The listener was being registered in a coroutine though, so maybe that’s just as out of bounds in Corona. I wanted the listener, running in the main thread, to unblock the coroutine, basically. Perhaps there’s a way to allow this in the future without overhauling the design?) [import]uid: 131050 topic_id: 35305 reply_id: 140431[/import]

Sorry if it caused you any trouble. We do have this written up as a bug. We’re just not quite prepared to deal with it right now.

Just to let you know, creating a coroutine in Lua creates a new lua_State in native C/C++, which allocates at least 2 KB of memory because it has its own stack. Not a huge amount of memory, but not cheap either. If you do use a coroutine then it’s best to use it for long operations instead of a lot of little operations. Coroutine don’t run on a separate thread either. They’re really multitasking. So, any long blocking operations such as [lua]audio.loadSound()[/lua] will block the main Lua script’s execution as well. [import]uid: 32256 topic_id: 35305 reply_id: 140436[/import]

No trouble at all. Yeah, coroutines are of limited use, but sort of nice for keeping some kinds of code more understandable I guess.

I switched to a non-coroutine approach, but for anyone else who reads this and still wants to use coroutines, I figured out a workaround for this problem. (Also, maybe Corona’s developers could adapt this workaround to easily fix the problem “under the hood” without a massive overhaul of Corona?)

The workaround works by guaranteeing that all callbacks happen in the main thread. It achieves this by detecting when a callback is being registered in a coroutine, and ‘postponing’ the call for the main thread to handle later. This is done via a timer, whose callbacks, unlike audio callbacks, seem to work when registered in a coroutine. (At least in the simulator.)

function runInMainThread ( func, packedArgs )  
 local thread = coroutine.running()  
 if thread then  
 timer.performWithDelay( 1,   
 function ( event )  
 local r  
 -- We're in the main thread now.  
 r = { func(unpack(packedArgs,1,table.maxn(packedArgs))) }  
 -- Let the coroutine know the result of the function call:  
 coroutine.resume ( thread, r )  
 end )  
 return unpack ( coroutine.yield() ) -- Wait for function to be called.  
 else  
 -- This is the main thread, so call normally.  
 return func ( unpack ( packedArgs, 1, table.maxn(packedArgs) ) )   
 end  
end  

So a “coroutine-safe” version of audio.play might look like:

function cosafe\_audioplay( h, params )  
 if coroutine.running() and params and params.onComplete then  
 runInMainThread ( audio.play, { h, params } )  
 else  
 audio.play( h, params )  
 end  
end  

And a blocking version of audio.play (for calling within a coroutine) could be written like this:

function blocking\_audioplay( h, params )  
 -- Only allow when called in a coroutine.  
 local thread = coroutine.running()  
 if thread then  
 -- Save the caller's onComplete and replace with our own.  
 if not params then params = {} end  
 orig\_onComplete = params.onComplete  
 -- Our new onComplete will be called in the main thread.  
 params.onComplete = function ( event )   
 if orig\_onComplete then  
 orig\_onComplete ( event )  
 end  
 coroutine.resume( thread )  
 end   
 cosafe\_audioplay ( h, params )  
 coroutine.yield()  
 end  
end  

And then code like this works as expected:

local h1 = audio.loadSound( "sound1.mp3" )  
local h2 = audio.loadSound( "sound2.mp3" )  
local h3 = audio.loadSound( "sound3.mp3" )  
  
function cofunction()  
 -- These sounds will play back to back now.  
 blocking\_audioplay( h1 )  
 blocking\_audioplay( h2, { onComplete=function() print("sound 2 done.") end } )  
 blocking\_audioplay( h3 )  
end  
  
local thread = coroutine.create( cofunction )  
coroutine.resume( thread )  

Cheers,
bc [import]uid: 131050 topic_id: 35305 reply_id: 140487[/import]

With an event based model, you could probably skip the coroutines.

In the case of your code, you’d be better off utilizing the onComplete handler like so:

[lua]–forwards
local soundClips = {}
local clipIndex = 1
local totalSounds = 5 --whatever your limit is

–load em up
table.insert( soundClips, audio.loadSound( “sound1.mp3” ) )
table.insert( soundClips, audio.loadSound( “sound2.mp3” ) )

local function onSoundComplete( event )
–inc
clipIndex = clipIndex + 1
if ( clipIndex > totalSounds ) then
clipIndex = 1
end

audio.play( soundClips[clipIndex], onComplete = onSoundComplete )
end

–init first sound
audio.play( soundClips[clipIndex], onComplete = onSoundComplete )[/lua]

Code is untested, and rough, but hopefully that can help.

Cheers. [import]uid: 202223 topic_id: 35305 reply_id: 140361[/import]

This is a limitation in Corona. Corona was not designed to call Lua listeners in coroutines. And changing Corona’s design to support it would be such a huge undertaking that its not worth it on our end. So, my advise is to not use coroutines for something like this.
[import]uid: 32256 topic_id: 35305 reply_id: 140396[/import]

Thank you develephant. That’s pretty much what I ended up doing instead, and it works fine. I appreciate the response! [import]uid: 131050 topic_id: 35305 reply_id: 140426[/import]

Ok thanks Joshua, I’m so glad I asked quick instead of banging my head against this for days! I ended up going another route than coroutines.

(Just to clarify, I was anticipating the listener being called in the “main thread,” not the coroutine. The listener was being registered in a coroutine though, so maybe that’s just as out of bounds in Corona. I wanted the listener, running in the main thread, to unblock the coroutine, basically. Perhaps there’s a way to allow this in the future without overhauling the design?) [import]uid: 131050 topic_id: 35305 reply_id: 140431[/import]

Sorry if it caused you any trouble. We do have this written up as a bug. We’re just not quite prepared to deal with it right now.

Just to let you know, creating a coroutine in Lua creates a new lua_State in native C/C++, which allocates at least 2 KB of memory because it has its own stack. Not a huge amount of memory, but not cheap either. If you do use a coroutine then it’s best to use it for long operations instead of a lot of little operations. Coroutine don’t run on a separate thread either. They’re really multitasking. So, any long blocking operations such as [lua]audio.loadSound()[/lua] will block the main Lua script’s execution as well. [import]uid: 32256 topic_id: 35305 reply_id: 140436[/import]

No trouble at all. Yeah, coroutines are of limited use, but sort of nice for keeping some kinds of code more understandable I guess.

I switched to a non-coroutine approach, but for anyone else who reads this and still wants to use coroutines, I figured out a workaround for this problem. (Also, maybe Corona’s developers could adapt this workaround to easily fix the problem “under the hood” without a massive overhaul of Corona?)

The workaround works by guaranteeing that all callbacks happen in the main thread. It achieves this by detecting when a callback is being registered in a coroutine, and ‘postponing’ the call for the main thread to handle later. This is done via a timer, whose callbacks, unlike audio callbacks, seem to work when registered in a coroutine. (At least in the simulator.)

function runInMainThread ( func, packedArgs )  
 local thread = coroutine.running()  
 if thread then  
 timer.performWithDelay( 1,   
 function ( event )  
 local r  
 -- We're in the main thread now.  
 r = { func(unpack(packedArgs,1,table.maxn(packedArgs))) }  
 -- Let the coroutine know the result of the function call:  
 coroutine.resume ( thread, r )  
 end )  
 return unpack ( coroutine.yield() ) -- Wait for function to be called.  
 else  
 -- This is the main thread, so call normally.  
 return func ( unpack ( packedArgs, 1, table.maxn(packedArgs) ) )   
 end  
end  

So a “coroutine-safe” version of audio.play might look like:

function cosafe\_audioplay( h, params )  
 if coroutine.running() and params and params.onComplete then  
 runInMainThread ( audio.play, { h, params } )  
 else  
 audio.play( h, params )  
 end  
end  

And a blocking version of audio.play (for calling within a coroutine) could be written like this:

function blocking\_audioplay( h, params )  
 -- Only allow when called in a coroutine.  
 local thread = coroutine.running()  
 if thread then  
 -- Save the caller's onComplete and replace with our own.  
 if not params then params = {} end  
 orig\_onComplete = params.onComplete  
 -- Our new onComplete will be called in the main thread.  
 params.onComplete = function ( event )   
 if orig\_onComplete then  
 orig\_onComplete ( event )  
 end  
 coroutine.resume( thread )  
 end   
 cosafe\_audioplay ( h, params )  
 coroutine.yield()  
 end  
end  

And then code like this works as expected:

local h1 = audio.loadSound( "sound1.mp3" )  
local h2 = audio.loadSound( "sound2.mp3" )  
local h3 = audio.loadSound( "sound3.mp3" )  
  
function cofunction()  
 -- These sounds will play back to back now.  
 blocking\_audioplay( h1 )  
 blocking\_audioplay( h2, { onComplete=function() print("sound 2 done.") end } )  
 blocking\_audioplay( h3 )  
end  
  
local thread = coroutine.create( cofunction )  
coroutine.resume( thread )  

Cheers,
bc [import]uid: 131050 topic_id: 35305 reply_id: 140487[/import]

Hi bobbycircle, many thanks for the solution. I’m stuck in a similar issue, and your code will be a big help, I’ll try it tonight!

Hi bobbycircle, many thanks for the solution. I’m stuck in a similar issue, and your code will be a big help, I’ll try it tonight!