Null audio checks, max channels and audio debug GUI

Started by Laura Hunt, Thu 21/05/2020 08:17:22

Previous topic - Next topic

Laura Hunt

Hey all,

Apologies in advance for throwing three issues into a single post, but since two of them are pretty short questions and they're all kind of related, I thought I would save time this way.

1) How do I properly check for null pointers?

For example, let's say I have this in room 2 of my game (tickingclock has already been defined through the global variables panel):

Code: ags
tickingclock = aClock.Play(eAudioPriorityNormal, eRepeat);
tickingclock.Volume = 50;


And this in room_load() in room 3:

Code: ags
if (tickingclock != null) tickingclock.Volume = 10;


I still get an NPE if tickingclock has not been initialized (for example, if I teleport from room 1 directly to room 3 via the debug key combo Ctrl+x). Is this not the correct way to check if an audiochannel is null? I seem to recall I've done it this way before and it worked ???


2) This section of the manual confuses me (talking about the Play command):

QuoteThis command searches through all the available audio channels to find one that is available for this type of audio. If no spare channels are found, it will try to find one that is playing a clip with a lower or equal priority, and interrupt it to replace it with this new sound.

So if I have reserved 2 max channels for "Ambient sound" audiotype clips and I do this (again, assuming myatmo1, myatmo2 etc are defined in the global variables panel and all these audio clips are "Ambient sound" types):

Code: ags
myatmo1 = aAtmos1.Play(eAudioPriorityHigh, eRepeat);
myatmo2 = aAtmos2.Play(eAudioPriorityHigh, eRepeat);


And later in the game I do this:

Code: ags
myatmo3 = aAtmos3.Play(eAudioPriorityNormal, eRepeat);


I would expect for aAtmos3 not to play at all, since the two slots for ambient sounds are already occupied and both have a higher priority. However, what I gather from the quote above is that AGS will simply look for another empty slot and play it there. Am I reading this correctly? Doesn't this defeat the purpose of limiting the amount of clips of a certain type that can be played simultaneously?


3) Creating an audio debug GUI

In order to help me sort out these issues, I'm trying to create a simple GUI that I can bring up with a key (say, F2) which will display which clip, if any, is playing in each of AGS' audio buses. So I would simply have 8 labels saying "Channel 0", "Channel 1", etc, and next to each, a label that would be populated with either the name of the clip (aAtmo1, aMusicTrack, aClock, etc) or "Nothing" if there's nothing playing.

But I'm totally lost, as I have no idea how I would get the audio clip that's playing on each channel and then display its name in the label. I tried this (in my global script's repeatedly_execute_always)

Code: ags
if (gAudioDebugUI.Visible) {
  for (int i = 0; i < 8; i++) {
      if (System.AudioChannels[i].PlayingClip == null) gAudioDebugUI.Controls[i+8].Text = "Nothing";
      else gAudioDebugUI.Controls[i+8].Text = System.AudioChannels[i].PlayingClip;  // (using "i+8" because the relevant label indexes go from 8 to 15)
  }
}


...but I'm getting a "GlobalScript.asc(249): Error (line 249): must have an instance of the struct to access a non-static member" error. What am I doing wrong?

Thanks in advance for any tips you can give me!


EDIT: I found this post by Snarky in which he does something similar, and apparently it's not possible to get the name of an audio clip? If that's the case then... I guess what I want to do is simply not possible? (I have several dozen audio clips in my game, I'm not going to index them one by one like Snarky does in this example.)

Khris

1)
weird, that check looks perfectly fine and should prevent null pointer errors

2)
I'm reading that manual part as the 3rd sound not being played, since there's no free channel and no occupied channel with a lower or equal priority.

3)
.PlayingClip returns an AudioClip*, so from a purely technical standpoint you cannot assign that to a String property. What you can do is something like
Code: ags
  else {
    AudioClip* ac = System.AudioChannels[i].PlayingClip;
    gAudioDebugUI.Controls[i+8].Text = String.Format("ID: %d, type: %d", ac.ID, ac.Type);
  }

Maybe AudioClips will get a description in the editor we can access using .Name in the future?

Laura Hunt

#2
Quote from: Khris on Thu 21/05/2020 09:10:21
1)
weird, that check looks perfectly fine and should prevent null pointer errors

What's really weird is that I added if (tickingclock != null && tickingclock.IsPlaying) and it works, and then I went back and used the original check if (tickingclock != null) and... it also works now. No NPE. I have no idea what was going on, and I swear I don't drink :P If something like this happens again, I will double and triple check that there's nothing else going on that's keeping the check from running.

Quote from: Khris on Thu 21/05/2020 09:10:21
2)
I'm reading that manual part as the 3rd sound not being played, since there's no free channel and no occupied channel with a lower or equal priority.

Ah, that's certainly a way to read it! The way it's written is kind of ambiguous, but I see what you mean and yeah, you may be right.

Quote from: Khris on Thu 21/05/2020 09:10:21
3)
.PlayingClip returns an AudioClip*, so from a purely technical standpoint you cannot assign that to a String property. What you can do is something like
Code: ags
  else {
    AudioClip* ac = System.AudioChannels[i].PlayingClip;
    gAudioDebugUI.Controls[i+8].Text = String.Format("ID: %d, type: %d", ac.ID, ac.Type);
  }

Maybe AudioClips will get a description in the editor we can access using .Name in the future?

Hmm I'm still getting the same error at
Code: ags
AudioClip* ac = System.AudioChannels[i].PlayingClip;


EDIT: Oh great, there's no AudioClip.ID for some reason. This is the best I've been able to manage:

Code: ags
if (gAudioDebugUI.Visible) {
    
    for (int i = 0; i < 8; i++) {
      AudioChannel* testtest = System.AudioChannels[i];
      AudioClip* ac = testtest.PlayingClip;
      GUIControl* testguicontrol = gAudioDebugUI.Controls[i+8];
      
      if (ac == null) testguicontrol.AsLabel.Text = "Nothing";
      else testguicontrol.AsLabel.Text = String.Format("Type: %d", ac.Type);
      
    }
  }


It works perfectly but only shows me the audio type. It's kind of useful, but it would be great to be able to access names or at least IDs  :-\

Khris

Looks like AudioClip.ID is a very recent addition; it's in the 3.5.0 manual but not the online one.

Laura Hunt

#4
Quote from: Khris on Thu 21/05/2020 10:13:16
Looks like AudioClip.ID is a very recent addition; it's in the 3.5.0 manual but not the online one.

Whoops. I'm using 3.4.3. Crap.

Thanks for checking!

(One thing this AudioDebugUI, primitive as it is, has helped me realize, is that there are moments in which the high movement speed of my character, combined with too-long footstep audio clips, means that at times all 4 free audiochannels -- reserving one channel for speech, one for music and two for ambient sounds leaves me with only 4 channels available for other sounds -- are occupied only with footstep sounds. Ugh. No wonder I was having sounds that would not play or would get mysteriously cut off. Need to work on fixing that!)

Crimson Wizard

In 3.4.3 you could do a big switch, but you'll have to manage and update it by hand:
Code: ags

String GetAudioClipName(AudioClip* clip)
{
   switch (clip)
   {
       case aMusic: return "MyMusic";
       case aSound: return "MySound";
   }
}

Laura Hunt

Quote from: Crimson Wizard on Thu 21/05/2020 10:34:16
In 3.4.3 you could do a big switch, but you'll have to manage and update it by hand:
Code: ags

String GetAudioClipName(AudioClip* clip)
{
   switch (clip)
   {
       case aMusic: return "MyMusic";
       case aSound: return "MySound";
   }
}


That's what Snarky did in his example, but I simply have too many clips to do this. It's ok though, simply being able to showithe audio types and whether the channels are playing something or not has already helped me a lot :)

Gal Shemesh

I know the last reply on this thread is from a few years ago, but just wondered if we can easily grab the name of audio clips for debugging by now?

Like @Laura Hunt, I also built a custom 'audio channels logic' where I have global variables for either Music, Ambiance and SFX, where each is set as Audiochannel*, and I play music for example like so:

Code: ags
globalChannelMusic1 = aMain_Menu.PlayOnChannel(1, eAudioPriorityNormal, eRepeat);

I'm already debugging the 'Volume' of my 'globalChannel' variables, but I'm also interesting in seeing the actual name of the audioclips in a label on my debugging GUI.

Thanks
Gal Shemesh,
goldeng

Crimson Wizard

#8
Quote from: Gal Shemesh on Sat 02/09/2023 12:03:47I know the last reply on this thread is from a few years ago, but just wondered if we can easily grab the name of audio clips for debugging by now?

At the moment this may only be done in AGS 4, where I added ScriptName properties to everything which has them, as well as GetByName method.

I guess I could backport these to 3.6.1...

In regards to a custom solution, earlier I mentioned a switch, but perhaps array of Strings would be better.

Crimson Wizard

#9
@Gal Shemesh I don't know which version of AGS are you using, but i copied ScriptName property and GetByName function to 3.6.1, for all types that may have them. Here's a temp build that may be tried:
https://cirrus-ci.com/task/6689087789203456


EDIT: simple example of how this may be used:
Code: ags
System.Log(eLogInfo, "This game's audio clips:");
for (int i = 0; i < Game.AudioClipCount; i++)
{
    System.Log(eLogInfo, "---> %d : %s", i, Game.AudioClips[i].ScriptName);
}

Gal Shemesh

#10
Awesome, @Crimson Wizard! :) I'm always using the latest stable release, currently Build 3.6.0.50, v3.6.0, July 2023. But I do look forward for the official release of 3.6.1 with all the new added features - especially the fully supported right-to-left Hebrew fix for GUI buttons.

I have downloaded the 3.6.1 version from your link for a test run and monitored this function in the new Log Panel when the game starts - this is really awesome! I see that it prints all the imported sounds in an array and that each can be retrieved by its number from the log.

Can the retrieval be dynamic? I mean, if a global variable is set as an Audiochannel* and I dynamically change it to other sounds, can the name of the actual sound that plays be retrived? I have this label in my debbuger GUI and I'm currently running this in the 'repeatedly_execute_always()' function in the Global Script for monitoring the sound volume, but I wish to add the sound name as well:

Code: ags
if (globalChannelMusic1 != null)
{
  lblMus1Vol.Text = String.Format("Music (Ch1) Vol: %d", globalChannelMusic1.Volume);
}
else
{
  lblMus1Vol.Text = String.Format("Music (Ch1) Vol: 0");
}

Thanks
Gal Shemesh,
goldeng

Crimson Wizard

#11
Quote from: Gal Shemesh on Sun 03/09/2023 04:48:52Can the retrieval be dynamic? I mean, if a global variable is set as an Audiochannel* and I dynamically change it to other sounds, can the name of the actual sound that plays be retrived?

Yes, you can access a played clip properties through channel.PlayingClip.

Gal Shemesh

QuoteYes, you can access a played clip properties through channel.PlayingClip.
Mm... I tried pulling the name as a string this way but it doesn't work... I guess my code is incorrect.
Code: ags
if (globalChannelMusic1 != null)
{
  lblMus1Vol.Text = String.Format("Music (Ch1): %s[Vol: %d", globalChannelMusic1.PlayingClip, 
  globalChannelMusic1.Volume);
}
else
 {
   lblMus1Vol.Text = String.Format("Music (Ch1): [Vol: 0");
 }
Gal Shemesh,
goldeng

Crimson Wizard

#13
Quote from: Gal Shemesh on Sun 03/09/2023 17:13:52
QuoteYes, you can access a played clip properties through channel.PlayingClip.
Mm... I tried pulling the name as a string this way but it doesn't work... I guess my code is incorrect.

AudioChannel.PlayingClip is of type AudioClip*:
https://adventuregamestudio.github.io/ags-manual/AudioChannel.html#audiochannelplayingclip

If you want to access particular clip's property, you need to also add this property to command, like "channel.PlayingClip.ScriptName".



Gal Shemesh

#14
QuoteIf you want to access particular clip's property, you need to also add this property to command, like "channel.PlayingClip.ScriptName".
Awesome!!!  8-)  8-)  8-)  Thank you so much! This works flawlessly.

EDIT:
See @Crimson Wizard's clarification below; an additional check is required to prevent null pointer crash.
Gal Shemesh,
goldeng

Crimson Wizard

By the way, I think you also need to test if PlayingClip is not null in the above code, in case the channel was assigned, but it does not play anything at the moment.

Code: ags
if (globalChannelMusic1 != null && globalChannelMusic1.PlayingClip != null)

Gal Shemesh

#16
Quote from: Crimson Wizard on Sun 03/09/2023 18:21:38By the way, I think you also need to test if PlayingClip is not null in the above code, in case the channel was assigned, but it does not play anything at the moment.
Correct, thanks! I just came to update my previous post to mention this in case someone else wishes to do the same, as my previous code will cause the game to crash if there's no sound playing, due to the null pointer. So one can just check for 'Game.IsAudioPlaying' instead of checking if the Audiochannel is not null, as you previously suggested me to check instead of checking the Audiochannel type.

Code: ags
if (Game.IsAudioPlaying(eAudioTypeMusic))
{
  lblMus1Vol.Text = String.Format("Music clip: %s[Vol: %d", 
  globalChannelMusic1.PlayingClip.ScriptName, globalChannelMusic1.Volume);
}
else
{
 lblMus1Vol.Text = String.Format("Music clip:[Vol: 0");
}

This works great and does not crash when there's no sound playing, but for my scenario with the different 'audio channels logic' using global variables, I should fix it according to what you just added. Many thanks! :)
Gal Shemesh,
goldeng

SMF spam blocked by CleanTalk