Audio clip overriding another

Started by Laura Hunt, Mon 10/08/2020 12:47:21

Previous topic - Next topic

Laura Hunt

I'm aware that this is such a random issue that maybe I won't get a solution, but I'm posting it anyway just in case somebody happens to have any galaxy-brain insights...

For context:

- In my game I have 1 channel reserved for Music, and 2 channels for Ambient audio. Since AGS automatically reserves channel 0 for speech, this means I have 4 channels left for other sounds (I don't use any Custom audio types).

- Because I made myself a very simple audio debugging GUI, I can always know which channels are playing audio, and what type of audio is playing.

ok, so in one room, I have a "Sound" type audio clip playing on an audiochannel I defined for this room script only:

Code: ags
Audiochannel* mrwilliams = aManCoughing.Play(eAudioPriorityNormal, eRepeat);


When I open my audio debug GUI, I see that this clip is playing on channel 4 and that all other channels are free.

Then, another sound gets triggered when a certain condition in repeatedly_execute_always() is met:

Code: ags
neighbours = aNeighboursArguing.Play(eAudioPriorityNormal, eOnce);


(the neighbours Audiochannel pointer was created through the Global Variables panel, because I need to do stuff with it in more than one room.)

And for some reason, sometimes (but only sometimes!) this clip decides to play on the same channel that the previous clip was playing, even when the audio debug GUI shows me clearly that there are three free slots and both sounds have the same eAudioPriorityNormal.

Anybody have any idea why this could be happening?

Laura Hunt

Might as well share my wacko theory, maybe it doesn't make sense at all but it's all I've got.

When you define any variable inside of a function, its scope is limited to that function. So because I created my mrwilliams Audiochannel inside a hHotspot_Interact() function, its properties such as Volume, Panning, etc, won't be available in repeatedly_execute. This I know and I'm aware of.

What I'm asking myself now is... is its Priority also constrained to that function? This might cause that when checking all the free audiochannels before playing the next clip, AGS would not be able to tell what priority this clip was played at, and would assume that the channel is either free or that has the lowest priority and so this clip would get clobbered by the next one that plays?

Crimson Wizard

#2
Quote from: Laura Hunt on Mon 10/08/2020 13:35:36
When you define any variable inside of a function, its scope is limited to that function. So because I created my mrwilliams Audiochannel inside a hHotspot_Interact() function, its properties such as Volume, Panning, etc, won't be available in repeatedly_execute. This I know and I'm aware of.

What I'm asking myself now is... is its Priority also constrained to that function? This might cause that when checking all the free audiochannels before playing the next clip, AGS would not be able to tell what priority this clip was played at, and would assume that the channel is either free or that has the lowest priority and so this clip would get clobbered by the next one that plays?

This is not accurate. You've created a pointer to AudioChannel in the function, and the pointer will indeed not exist outside of it, but the object itself will, because it's created elsewhere (in case of audio channels it's a global object created at the start of the game and only destroyed in the end). All the properties of an object are kept at all time until changed.

I.e. if you manage to get a new pointer to the same channel in any next function, you should be able to read same values you assigned to the properties.

CrashPL

Plugging into the topic, as I saw that several times in ZA too. The audio clip overriding each other, and the AudioChannels inner workings in general is sadly something I've never fully understood, despite working for 5 years in AGS. My only hotfix for that was to play some sounds with the low priority, so that they wouldn't interfere with much more important ones.

What's worse, if the audio channel that was overridden featured some kind of manual volume/panning, the new clip will play with the same settings, which will produce very jarring audio glitches.

Crimson Wizard

#4
One thought I have, but this requires investigating engine code further to confirm, and some tests: because your first sound is played with "Repeat", maybe there's a moment in time when it stopped playing but not started again yet, and if new sound is played in this exactly tick engine will mistakenly consider the previous clip to be "stopped" and thus allowed to be replaced.
Actually, maybe not. I looked in the wrong place...

But indeed, using varying priorities, as CrashPL mentioned, may be a good solution in any case. You only need to define which sounds are more important than others.

Laura Hunt

Quote from: Crimson Wizard on Mon 10/08/2020 13:42:19
This is not accurate. You've created a pointer to AudioChannel in the function, and the pointer will indeed not exist outside of it, but the object itself will, because it's created elsewhere (in case of audio channels it's a global object created at the start of the game and only destroyed in the end). All the properties of an object are kept at all time until changed.

I see. Well, I did say it was a wacko theory :-D

Quote from: CrashPL on Mon 10/08/2020 13:55:01
Plugging into the topic, as I saw that several times in ZA too. The audio clip overriding each other, and the AudioChannels inner workings in general is sadly something I've never fully understood, despite working for 5 years in AGS. My only hotfix for that was to play some sounds with the low priority, so that they wouldn't interfere with much more important ones.

Oh god, it's not just me, I'm not going crazy!

And yeah, that's my way around these issues too. Also, sometimes I'll change a clip's audio type simply because I know that a channel of that type is free at the moment, so I'll end up playing an environmental sound as a music clip or whatever. It's not pretty and kind of defeats the purpose of having my clips neatly arranged as music / ambient / sounds, but as a hack for very specific situations, it works (it's literally what I've done in this case in the end.)

Quote from: Crimson Wizard on Mon 10/08/2020 13:57:37
One thought I have, but this requires investigating engine code further to confirm, and some tests: because your first sound is played with "Repeat", maybe there's a moment in time when it stopped playing but not started again yet, and if new sound is played in this exactly tick engine will mistakenly consider the previous clip to be "stopped" and thus allowed to be replaced.
Actually, maybe not. I looked in the wrong place...

Are you absolutely sure this cannot happen? Because yesterday, Thomas asked me if I had removed a certain sound from the game and I was like "...no? What are you talking about?". He ran the game again (exact same .exe, no changes), and this time it worked, the same way it had worked the previous 200 times. And the only thing I could think of was exactly this: two clips trying to play at exactly the very same tick and one getting overriden by the other (this wouldn't be too weird when there are e.g., footstep sounds, since they're playing often enough that it might happen).

Crimson Wizard

Is it possible to have a test game where this problem reproduces, or an actual demo game where it happens? I could run this under debugger to see what the engine is doing.

Laura Hunt

The issue that Thomas reported is totally random, as it's only happened literally once in hundreds of test runs.

But I could undo my "fix" for the issue I posted about originally, and see if it starts happening again (it's somewhat conditional on the second clip starting while the second one is still playing, though). If I manage to reproduce it, I'll send you a WeTransfer link over DM.

Crimson Wizard

#8
@Laura Hunt,

after much testing, I found what is happening. This code in your room script is causing an issue:

Code: ags

if (IsTimerExpired(2)){
    if (neighborsarguing != null && neighborsarguing.IsPlaying) neighborsarguing.Stop();
    neighborsarguing = aNeighbors1EQ.Play(eAudioPriorityNormal, eOnce);
    ...
}


The problem here is that neighborsarguing is a global pointer which is used through several rooms, and it points to a channel that may be reused for something else.

What happens is:

1) in room A you call "neighborsarguing = aNeighbors4EQ.Play", thus assigning neighbours clip to the channel, which happens to be channel 4.
2) at the time player reaches room B this sound did already stop, so channel 4 is free again. But "neighborsarguing" pointer is still pointing to channel 4.
3) when player knocks the door you call "AudioChannel *mrwillamscough = aManCoughing2.Play". If you are "lucky" and no other sounds were playing at the same time (footsteps etc), the coughing clip gets to channel 4.
4) finally, when timer runs out, you do "neighborsarguing.Stop()", which stops channel 4, where man coughing is played.


This is a common scripting mistake, and one of the reasons why using global AudioChannel pointers is very bug prone. First of all, you may stop the channel yourself somewhere and forget to set variable to null. More importantly, this clip may be replaced by another if there's no free channel (or if it's of higher priority), and there's no way to catch such event except for checking for your channel's contents in rep-exec.

I may suggest to safeguard every global audio channel you have by keeping also last played AudioClip along with them, and start/stop sounds by calling functions, like:
Spoiler

Code: ags

void PlayNeighboursArguing(AudioClip* clip)
{
    if (neighborsarguing != null && neighborsarguing.PlayingClip == neighborsarguingCLIP)
        neighborsarguing.Stop();
    neighborsarguing = clip.Play(eAudioPriorityNormal, eRepeat);
    neighborsarguingCLIP = clip;
}

void StopNeighboursArguing()
{
     if (neighborsarguing != null && neighborsarguing.PlayingClip == neighborsarguingCLIP)
        neighborsarguing.Stop();
     neighborsarguing = null;
     neighborsarguingCLIP = null;
}

[close]




For a more universal system you could have a global array of AudioChannel and AudioClip pointers and enumeration refering to their in-game uses. In which case you don't need separate function for each channel pointer, but something like
Spoiler

Code: ags

void PlayGlobalAudio(int index, AudioClip* clip)
{
    if (globalAudioChan[index] != null && globalAudioChan[index].PlayingClip == globalAudio[index])
        globalAudioChan[index].Stop();
    globalAudioChan[index] = clip.Play(eAudioPriorityNormal, eRepeat);
    globalAudio[index] = clip;
}

void StopGlobalAudio(int index)
{
     if (globalAudioChan[index]!= null && globalAudioChan[index].PlayingClip == globalAudio[index])
        globalAudioChan[index].Stop();
     globalAudioChan[index]= null;
     globalAudio[index]= null;
}

and called like
Code: ags

PlayGlobalAudio(eGANeighbours, aNeighbors1EQ);

[close]

Laura Hunt

#9
Hey CW, thanks for the thorough testing and the detailed explanation. Seems like I learn something new here every day.

Quote from: Crimson Wizard on Tue 11/08/2020 00:14:11
2) at the time player reaches room B this sound did already stop, so channel 4 is free again. But "neighborsarguing" pointer is still pointing to channel 4.

OK, I didn't know this. Everything makes more sense now.

Quote from: Crimson Wizard on Tue 11/08/2020 00:14:11
4) finally, when timer runs out, you do "neighborsarguing.Stop()", which stops channel 4, where man coughing is played.

So what if I don't do "neighborsarguing.Stop()" and instead, do neighborsarguing = aNeighbors1EQ.Play() directly? Will neighboursarguing look for a new, free channel, or will it keep pointing to channel 4 forever? (or until I make it null manually).

Quote from: Crimson Wizard on Tue 11/08/2020 00:14:11I may suggest to safeguard every global audio channel you have by keeping also last played AudioClip along with them, and start/stop sounds by calling functions, like:

Thanks! Right now I don't need this because I solved this specific issue simply by changing the audio type of aManCoughing2 to Ambient, since I know I have a free channel in that room. But I'll make a note of it in case I need it in the future. Luckily, most of my global Audiochannel pointers are used only for Music and Ambient clips, and because I've limited the number of channels for those audio types to 1 and 2 respectively, it's much easier to keep track of what's going on there (and probably also the reason I hadn't run into this issue until now).

Quote from: Crimson Wizard on Tue 11/08/2020 00:14:11For a more universal system you could have a global array of AudioChannel and AudioClip pointers and enumeration refering to their in-game uses. In which case you don't need separate function for each channel pointer, but something like

I was wondering if another way to keep a stricter control over what's happening in each channel would be to create 7 global Audiochanel pointers in the variables panel, and at the start of my game initialize them with a 1-second, zero-volume clip. Something like:

Code: ags
musicchannel = aNullMusicClip.Play(eAudioPriorityNormal, eOnce);
atmochannel1 = aNullAtmoClip.Play(eAudioPriorityNormal, eOnce);
atmochannel2 = aNullAtmoClip.Play(eAudioPriorityNormal, eOnce);
soundschann1 = aNullSoundClip.Play(eAudioPriorityNormal, eOnce);
soundschann2 = aNullSoundClip.Play(eAudioPriorityNormal, eOnce);
soundschann3 = aNullSoundClip.Play(eAudioPriorityNormal, eOnce);
soundschann4 = aNullSoundClip.Play(eAudioPriorityNormal, eOnce);


If I ONLY use these pointers to play sounds, I guess that because each pointer is now "tied" to its own audio channel, I can be confident that they're not going to overlap, and because they've all been initialized, I should not have to check for null pointers anymore, right? Or does this not make sense at all?

I wanted to ask you something else but I can't for the life of me remember what it was right now, so I'll just PM you if/when I remember :)

Thanks again for your help!

EDIT: Oh yeah, I just remembered at least one thing I wanted to ask: Why the 8-channel limit? Is it due to actual limitations of the engine, because of performance concerns, or was it just a case of "8 channels ought to be enough for everybody"?


Crimson Wizard

#10
There seem to be a serious misconception in above.

Your audio channel pointers do no look for a channel themselves, nor define a channel the sound is played on. They simply store returned values from Play() (or whichever value you set yourself).
It's Play function that looks for a channel and returns one if it was able to start a playback. "neighborsarguing = aNeighbors1EQ.Play()" expression assigns return value of Play function to the variable, which is same as if you'd assigned something directly e.g. "neighborsarguing = System.AudioChannels[0]".

Expressions like "neighborsarguing = aNeighbors1EQ.Play()" have no impact on which channel will be chosen whatsoever, simply by rules of scripting syntax. For the channel pointer to have any impact it should be either used to call a member function (like if it were "neighborsarguing.Play"), or be passed as an argument (like if it were "aNeighbors1EQ.Play(neighborsarguing)").

As of now in AGS you cannot enforce which channel the sound is played on with your pointer, but only by reserving a channel for audio type.

Play can always return null pointer in case of a failure. If you assign Play's return value to your variable, you must expect that this variable may be null.



Quote from: Laura Hunt on Tue 11/08/2020 13:02:29
EDIT: Oh yeah, I just remembered at least one thing I wanted to ask: Why the 8-channel limit? Is it due to actual limitations of the engine, because of performance concerns, or was it just a case of "8 channels ought to be enough for everybody"?

It's probably a combination of reason 2 and reason 3. Maybe it will be increased at some point, but more importantly, the audio system in AGS now is too awkward and has to be redesigned.

Laura Hunt

Quote from: Crimson Wizard on Tue 11/08/2020 13:23:50
Expressions like "neighborsarguing = aNeighbors1EQ.Play()" have no impact on which channel will be chosen whatsoever, simply by rules of scripting syntax. For the channel pointer to have any impact it should be either used to call a member function (like if it were "neighborsarguing.Play"), or be passed as an argument like if it were "aNeighbors1EQ.Play(neighborsarguing)".

As of now in AGS you cannot enforce which channel the sound is played on with your pointer, but only by reserving a channel for audio type.

I know, but this is not what I'm trying to do. Maybe I did not explain myself properly.

What I mean is this: let's say, like I mentioned above, that at the start of my game I do "soundschann1 = aRandomClip.Play()", and let's say this happens to play on channel 5. According to what you said earlier, now soundschann1 will always point to channel 5, so anytime I do anything with that pointer, such as soundschann1 = aSomeOtherclip.Play(), I can expect this to always play on channel 5. Correct? (I know my terminology is not always super precise, but I'm sure you know what I mean here.)

Quote from: Crimson Wizard on Tue 11/08/2020 13:23:50It's probably a combination of reason 2 and reason 3. Maybe it will be increased at some point, but more importantly, the audio system in AGS now is too awkward and has to be redesigned.

Most definitely. I love AGS because it does what I need for the type of games I want to make and I don't really want any more bells and whistles, but the sound system is the one and only thing that is making me think of giving Unity a try for my next game. A solid, reliable, flexible and easy-to-use sound system would be a blessing.

Crimson Wizard

#12
Quote from: Laura Hunt on Tue 11/08/2020 14:23:57
What I mean is this: let's say, like I mentioned above, that at the start of my game I do "soundschann1 = aRandomClip.Play()", and let's say this happens to play on channel 5. According to what you said earlier, now soundschann1 will always point to channel 5, so anytime I do anything with that pointer, such as soundschann1 = aSomeOtherclip.Play(), I can expect this to always play on channel 5. Correct?

No, it's not correct, this is something I tried to explain in my previous post. Expression "soundschann1 = aSomeOtherclip.Play()" assigns new value to the pointer, which may be anything. The previous value of the pointer (channel 5) is not even used when deciding which channel to play sound on.

This expression syntactically is equivalent to "x = y". The result will in no way depend on "x", but only on "y". In above case it depends on result of calling "Play()" function, which has its own internal logic independent of what AudioChannel pointers you've got in the script.


If, on other hand, you will do only "aSomeOtherclip.Play();", and that sound will happen to play on channel 5 again, then one of your pointers refering to channel 5 will be controlling this new clip. But idk if there's any actual use to this.


Also, if you want to always have a pointer to exact channel, then you do not need to initialize it with playing a dummy sound, you could simply do "soundschann1 = System.AudioChannels[4];". But then again, you could use System.AudioChannels[4] directly without extra pointers too.

Privateer Puddin'

Quote from: Laura Hunt on Tue 11/08/2020 13:02:29
EDIT: Oh yeah, I just remembered at least one thing I wanted to ask: Why the 8-channel limit? Is it due to actual limitations of the engine, because of performance concerns, or was it just a case of "8 channels ought to be enough for everybody"?

I think Snarky's post here probably pins down the original reason for 8

https://www.adventuregamestudio.co.uk/forums/index.php?topic=56079.msg636602071#msg636602071

Laura Hunt

Quote from: Crimson Wizard on Tue 11/08/2020 15:05:43
Quote from: Laura Hunt on Tue 11/08/2020 14:23:57
What I mean is this: let's say, like I mentioned above, that at the start of my game I do "soundschann1 = aRandomClip.Play()", and let's say this happens to play on channel 5. According to what you said earlier, now soundschann1 will always point to channel 5, so anytime I do anything with that pointer, such as soundschann1 = aSomeOtherclip.Play(), I can expect this to always play on channel 5. Correct?

No, it's not correct, this is something I tried to explain in my previous post. Expression "soundschann1 = aSomeOtherclip.Play()" assigns new value to the pointer, which may be anything. The previous value of the pointer (channel 5) is not even used when deciding which channel to play sound on.

Ah, all clear now. Thanks again!

Quote from: Crimson Wizard on Tue 11/08/2020 15:05:43If, on other hand, you will do only "aSomeOtherclip.Play();", and that sound will happen to play on channel 5 again, then one of your pointers refering to channel 5 will be controlling this new clip. But idk if there's any actual use to this.

Well, it makes it possible for devs to introduce seemingly random bugs into scripts, which is an important feature any engine should have :-D

Quote from: Crimson Wizard on Tue 11/08/2020 15:05:43Also, if you want to always have a pointer to exact channel, then you do not need to initialize it with playing a dummy sound, you could simply do "soundschann1 = System.AudioChannels[4];". But then again, you could use System.AudioChannels[4] directly without extra pointers too.

...this actually sounds like a great idea. Frame-linked sounds would be a problem, though...

Quote from: Privateer Puddin' on Tue 11/08/2020 15:09:16
I think Snarky's post here probably pins down the original reason for 8

https://www.adventuregamestudio.co.uk/forums/index.php?topic=56079.msg636602071#msg636602071

I see! Thanks :)



SMF spam blocked by CleanTalk