Oh, you silly little NIL (Audio API)

I am fairly new to Lua. Raised on 65C02 Assembly, Pascal and C, this project I’ve been working on now for about 4 months is my first step into Lua-land.

I came across an issue with the Audio API that cost me several hours and much research in the forums, and I was compelled to share this information with people based on my findings. I don’t know if others will think I’m mistaken, that’s also part of reason that motivates this thread.

audio.stop() is a function that you use to kill audio (SFX or Music) that has been triggered by audio.play() and audio.play() allows many different parameters to be passed. Ultimately, you can assign volume and channels (in case you’ve reserved or assigned special channels for things like music) and then assign the channel that the sound was triggered on to a handle for later use/tracking.

handle=audio.play(mediaFile, {channel=MUSIC\_CH,onComplete=songOver}}  
  
songOver()  
 handle=nil  
end  

Imagine the music is playing along and it’s a 30 second ditty. But imagine before the song finishes, you need to stop the music because of a screen transition or a myriad of other reasons. So you use audio.stop()

audio.stop(handle)  

Now it should be noted that in this case, because we put the music on a designated channel, we could use the MUSIC_CH

audio.stop(MUSIC\_CH)  

so imagine we used the handle instead - because the two should be equivalent.

If you think about it, there are circumstances you can run into where handle may be NIL. Maybe your code tries to kill a SFX or Music that hasn’t been triggered yet, so the handle is still NIL. Or maybe the SFX or Music has finished, and the onComplete has made the handle NIL. Ultimately, here’s what happens and here is the rub:

audio.stop() kills all 32 channels.
audio.stop(0) kills all 32 channels.
audio.stop(nil) kills all 32 channels.

Now in Lua, NIL is by definition a different type. So it’s not equivalent. And in the context of this discussion, if a programmer meant to pass a handle/channel to audio.stop(handle) and handle became NIL, that programmer most likely certainly didn’t expect or want to kill ALL 32 channels of sound. Yet that’s the result. This isn’t mentioned in the API documentation (currently). This potentially will drive people “crazy” and they’ll think the API is flakey when in fact, it’s just - in my eyes - unexpected behavior. Most likely the C code, which treats NIL as NULL as (void *)0 is all equivalent. That’s just a guess. Regardless, you should use a wrapper on any audio.stop(channel) calls.

if handle ~= nil then  
 audio.stop(handle)  
 handle=nil  
end  

This wrapper will help prevent you from having SFX trigger, but not play because say for instance as you transition from your end of level to the next screen you trigger a SFX that indicates level is over, immediately followed by killing the music, but the handle is nil, and so the SFX you just triggered gets killed before you can hear it. That’s what lead me down this path and finding out my SFX was in fact being triggered, being assigned a handle, and also calling the onComplete function… but the trick was my sound was SO short (less than a second) that I couldn’t tell in my terminal window that the SFX was being killed before it could be heard by the audio.stop(nil) call. D’oh!!! I thought maybe the volume had somehow been set to 0, so I just didn’t hear it. I thought the channel assignment didn’t happen. But all error checking and code analysis indicated the sound triggered, was playing - but onComplete actually passes back an event parameter; a boolean called “completed” and this will tell you if audio.stop() killed your sound, or if it just didn’t finish playing for some reason.
completed

boolean: This value will be true if the audio stopped because it finished playing normally (played to the end of the sound). This value will be false if the audio was stopped because of other reasons.

Hopefully this will help someone out there and prevent HOURS of debugging.
[import]uid: 74844 topic_id: 16908 reply_id: 316908[/import]

Wow, Jerome, thank you for sharing your experience/observation.

Naomi [import]uid: 67217 topic_id: 16908 reply_id: 63379[/import]

jerome82: Good observations, but some clarifications are needed.

I want to drive home a point. audio.stop() does not take a handle as parameter. This is by design. The reason is that ALmixer and OpenAL were designed with the idea that you may be playing multiple instances of the same sound at the same time. Imagine multiple explosion effects all going off at about the same time. (I should also point out that all the explosions may not have started at exactly the same time and the effects overlap like in a chain reaction.) In this case, it doesn’t really make any sense to call audio.stop() on just the explosion_handle because the question becomes, which specific explosion did you want to stop? If you wanted to stop all of them (remember they may be starting at different times and are not sync’d), why would you only want to stop just the explosions and not the other sound effects like the bullets or fuses or whatever. Usually in this kind of case, you want to stop everything and not a specific handle. But if you did have some specific weird usage case where this is useful to you, this can be accomplished by tracking the channels you are playing specific effects on yourself.

This also means your audio.stop() call when handle==nil isn’t going to be an automatic pitfall since you aren’t supposed to pass handles to audio.stop().

Also, the documentation already does state that “No parameters” stops all active channels. And specifying 0 as the channel stops all channels. (People familiar with the secret audio APIs might note that you may specify a source instead of a channel.)

[import]uid: 7563 topic_id: 16908 reply_id: 63394[/import]

@ewing: my nomenclature of “handle” might be inappropriate; audio.play() returns a channel, correct? That is what is assigned to a variable (what I called a handle) and then audio.stop() does in fact accept a channel as a parameter to stop a specific channel, correct? This is how you would manage your audio; you track which SFX/Music is on which channel (or designate a channel for that purpose) and then stop the channel.

So I understand the multiple instance scenario you paint up above.

So if your SFX channel is no longer valid, because your SFX has stopped and so your channel needs to be flagged (ie nil), then ultimately the audio.stop() needs a wrapper regardless. Maybe I’m not explaining myself clearly, but I don’t think I’m the only one that has run into this… [import]uid: 74844 topic_id: 16908 reply_id: 63401[/import]

I think of ‘handle’ as the thing returned by audio.loadSound or audio.loadStream.

But, I think all your observations/points are good. Though if you stop a specific channel that is no longer playing (e.g. stop channel 4 but channel 4 is not playing), the function will behave pretty much like a no-op.
[import]uid: 7563 topic_id: 16908 reply_id: 63404[/import]

@ewing: correct, that’s fine… but if you’re using a channel variable to track your SFX/Music, and if you make your channel “nil” when the SFX is no longer active (ie being played), and IF you try to stop.sound(channel) for whatever reason AFTER the channel has been made nil, and you pass “nil” to audio.stop(channel), the result should NOT be that you kill all sounds. That’s my opinion. That’s my point.

EDIT: Yes, your usage of “handle” is more proper than mine.

EDIT: So either we have to wrapper EVERY single audio.stop(channel) call, or if the API call audio.stop() simple checked to see if a nil was passed, just ignore the request. Or am I wrong about this implementation being preferred? [import]uid: 74844 topic_id: 16908 reply_id: 63407[/import]

I think your observations/points are legitimate. For now, checking for nil before calling audio.stop() if probably the best option for you. I am not opposed to the idea of changing the behavior of audio.stop() to reject nil as a parameter, but there are fallout questions and I also don’t want to break people with an API change. So one fallout question is do I throw a lua error on this or make it a no-op?

One other change I eventually plan to do is make the handles full lua user data instead of light user data. Though both of these changes are low priority right now.
[import]uid: 7563 topic_id: 16908 reply_id: 63436[/import]

Right, I understand it would be low priority… it was high priority for me because I couldn’t ship my game when I thought the audio API was buggy. But I finally figured out what was happening in my code logic; which as you have stated it seems most Audio API issues are coding issues. HOWEVER, I think the API documentation should mention the handling of what occurs when NIL is passed to audio.stop() at the very least. That shouldn’t be hard to do.

So this thread was merely an attempt to let others know about the Audio API handling of NIL, that’s all. [import]uid: 74844 topic_id: 16908 reply_id: 63438[/import]

Okay, just dumped some of this into the API page.
[import]uid: 7563 topic_id: 16908 reply_id: 63446[/import]

I see that; thank you! [import]uid: 74844 topic_id: 16908 reply_id: 63448[/import]