Hi! I have a cassette player inside my game which is a programmer instrument that uses songs from an AudioTable.
If I switch between the different cassettes I get this warning [FMOD] SoundI::getSubSound : Cannot switch sub-sounds on a playing stream, call Sound::stop first.
// Inside LateUpdate
musicEventInstance.getPlaybackState(out var playbackState);
switch (playbackState)
{
case PLAYBACK_STATE.PLAYING:
{
// Getting information to set track time, example: 01:50 - 3:52
musicEventInstance.getTimelinePosition(out int timelinePosition);
CassettePlayer.instance.UpdateTrackDuration(timelinePosition);
// Get max length of current playing track
if (!gotMaxLength)
{
var result = musicSound.getOpenState(out var openState, out _, out _, out _);
if (result == RESULT.OK)
{
if (openState != OPENSTATE.ERROR)
{
musicSound.getSubSound(subSoundIndex, out var subSound); <-- THIS THROWS THE WARNING
// Wait until the length have been fetched! //
result = subSound.getLength(out var length, TIMEUNIT.MS);
if (result == RESULT.OK)
{
CassettePlayer.instance.SetMaxTrackDuration((int) length);
gotMaxLength = true;
}
}
}
}
break;
}
}
And sometimes when I stop the programmer sound from playing I get this warning as well, might be a key to the puzzle I don’t know
[FMOD] EventInstance::flushReleaseQueue : Event instance has programmer sounds pending release, update thread will be bloocked until programmer sounds are released.
The key here is calling getSubSound actually performs a switching operation (it’s not the best name), so you get a warning if it’s currently playing. You might be better served by caching the length before you pass it into the programmer sound callback.
How would I cache it? I’ve tried using sound.getSubSound and getLength inside EVENT_CALLBACK_TYPE.CREATE_PROGRAMMER_SOUND and I want to remember it not working?
Here is my callback
[MonoPInvokeCallback(typeof(EVENT_CALLBACK))]
static RESULT PlayMusicEventCallback(EVENT_CALLBACK_TYPE type, IntPtr instancePtr, IntPtr parameterPtr)
{
EventInstance instance = new EventInstance(instancePtr);
// Retrieve the user data
instance.getUserData(out var stringPtr);
// Get the string object
GCHandle stringHandle = GCHandle.FromIntPtr(stringPtr);
string key = stringHandle.Target as string;
switch (type)
{
case EVENT_CALLBACK_TYPE.CREATE_PROGRAMMER_SOUND:
{
const MODE soundMode = MODE.LOOP_OFF | MODE.CREATESTREAM | MODE.NONBLOCKING;
var parameter = (PROGRAMMER_SOUND_PROPERTIES) Marshal.PtrToStructure(parameterPtr, typeof(PROGRAMMER_SOUND_PROPERTIES));
var keyResult = RuntimeManager.StudioSystem.getSoundInfo(key, out var musicSoundInfo);
if (keyResult != RESULT.OK)
{
Debug.LogError("AudioManager: Error getSoundInfo for music event!");
break;
}
// musicSound is a Sound variable which I'm storing (out musicSound)
var soundResult = RuntimeManager.CoreSystem.createSound(musicSoundInfo.name_or_data, soundMode | musicSoundInfo.mode, ref musicSoundInfo.exinfo, out musicSound);
if (soundResult == RESULT.OK)
{
parameter.sound = musicSound.handle;
parameter.subsoundIndex = musicSoundInfo.subsoundindex;
subSoundIndex = musicSoundInfo.subsoundindex; // Store the int to be used in LateUpdate in conjunction with getSubSound
Marshal.StructureToPtr(parameter, parameterPtr, false);
}
else
{
Debug.LogError("AudioManager: Error creating FMOD sound for music event!");
}
break;
}
case EVENT_CALLBACK_TYPE.STARTED:
{
gotMaxLength = false;
currentState = PlayerState.Playing;
break;
}
case EVENT_CALLBACK_TYPE.DESTROY_PROGRAMMER_SOUND:
{
var parameter = (PROGRAMMER_SOUND_PROPERTIES) Marshal.PtrToStructure(parameterPtr, typeof(PROGRAMMER_SOUND_PROPERTIES));
var sound = new Sound {handle = parameter.sound};
sound.release();
break;
}
case EVENT_CALLBACK_TYPE.DESTROYED:
{
// Now the event has been destroyed, unpin the string memory so it can be garbage collected
stringHandle.Free();
break;
}
}
return RESULT.OK;
}
And here’s how I play the tracks
public static void PlayMusic(Cassette_SO cassetteToPlay)
{
if (cassetteToPlay == null)
{
Debug.LogError("Tried playing a null cassette");
return;
}
if (musicIsPlaying)
{
// We need to wait for the current music to stop playing
// PlayMusic will be called again from LateUpdate when the song is stopped
StopMusic();
if(!musicQueue.Contains(cassetteToPlay))
musicQueue.Enqueue(cassetteToPlay);
return;
}
// Doing this to prevent a small snippet of the old sound to play, hard to describe in words -Niklas
musicEventInstance.setVolume(1);
musicEventInstance = RuntimeManager.CreateInstance("event:/UI/Menues/CassettePlayer_MusicEvent");
RuntimeManager.StudioSystem.setParameterByName("Music Key", cassetteToPlay.MyMusicKey); // Set global FMOD parameter
// Pin the key string in memory and pass a pointer through the user data
if(stringHandle.IsAllocated)
stringHandle.Free();
stringHandle = GCHandle.Alloc(cassetteToPlay.fmodKey, GCHandleType.Pinned);
musicEventInstance.setUserData(GCHandle.ToIntPtr(stringHandle));
musicEventInstance.setCallback(musicCallback);
musicEventInstance.start();
musicEventInstance.release();
// Music Duck snapshot
musicDuckSnapshotInstance = CreateEventInstance("snapshot:/CassetteDuck");
musicDuckSnapshotInstance.start();
musicDuckSnapshotInstance.release();
if(!CassettePlayer.instance.gameObject.activeInHierarchy && !MenuManager.instance.isMenuOpen)
{
// Don't show any new message if there's a menu open :)
OnScreenMessageManager.instance.QueueMessage(OnScreenMessageManager.instance.messageHeaderNowPlaying, cassetteToPlay.ToString(), OnScreenMessageType.Music, cassetteToPlay);
}
CassettePlayer.instance.OnPlayingTrack(cassetteToPlay);
musicIsPlaying = true;
onTrackedPlayed?.Invoke(cassetteToPlay);
}
When I do get the [FMOD] SoundI::getSubSound : Cannot switch sub-sounds on a playing stream, call Sound::stop first. warning it keeps logging warning inside the console until I stop the current playing track, aka the programmer instrument.
I think I’ve made some fundamental error here somewhere haha
One possibility is to do the createSound, getSubSound and getLength all synchronously before you call CreateInstance, so you’d cache the length and the subsound up front, then pass it in when prompted via the programmer sound callback.
You are likely continuing to receive the current error because gotMaxLength never makes it to being true? So it fires each frame.
I’m not sure how to do those functions properly as I don’t find anything in the documentation. Right now those things are handled by the callback which is copied over from your examples.
But if the Cannot switch sub-sounds on a playing stream, call Sound::stop first. warning is nothing to worry about I can get on with my day I guess.
I’d love to cache the duration of the track before-hand though!
It’s mostly just a matter of moving some of the things done in the programmer sound callback and calling them before you start the musicEventInstance. So in your PlayMusic function you’d call Studio.getSoundInfo, then System.createSound (as before), however remove the MODE.NONBLOCKING bit from the soundMode. Then call Sound.getSubSound and pass in musicSoundInfo.subsoundindex, this returns you the sub-sound you want to play. Now you can cache the length by calling Sound.getLength.
In your programmer sound callback, set parameter.sound to the sub-sound you fetched earlier, set parameter.subsoundindex to -1 (we’ve already fetched the subsound). That should be it.
If you just ignore the warning about switching subsound, you will get errors when trying to query the length as it will be an invalid handle.
Wow Mathew, it totally works, even works faster now that it’s not doing the loading NONBLOCKING!
Are these warnings something to worry about? [FMOD] EventInstance::flushReleaseQueue : Event instance has programmer sounds pending release, update thread will be bloocked until programmer sounds are released.
Also a typo in the warning haha bloocked
Either way, here’s the code just to make sure I got it right
void PlayMusic
{
musicEventInstance = RuntimeManager.CreateInstance("event:/UI/Menues/CassettePlayer_MusicEvent");
RuntimeManager.StudioSystem.getSoundInfo(cassetteToPlay.fmodKey, out var musicSubSoundInfo);
const MODE soundMode = MODE.LOOP_OFF | MODE.CREATESTREAM; // | MODE.NONBLOCKING
var soundResult = RuntimeManager.CoreSystem.createSound(musicSubSoundInfo.name_or_data, soundMode | musicSubSoundInfo.mode, ref musicSubSoundInfo.exinfo, out musicSound);
if (soundResult == RESULT.OK)
{
musicSound.getSubSound(musicSubSoundInfo.subsoundindex, out var subSound);
musicSound = subSound;
subSound.getLength(out var maxLength, TIMEUNIT.MS);
CassettePlayer.instance.SetMaxTrackDuration((int)maxLength);
}
else
{
Debug.LogError("AudioManager: Error creating FMOD sound for music event!");
musicEventInstance.stop(STOP_MODE.IMMEDIATE);
musicEventInstance.release();
return;
}
etc...
}
[MonoPInvokeCallback(typeof(EVENT_CALLBACK))]
static RESULT PlayMusicEventCallback(EVENT_CALLBACK_TYPE type, IntPtr instancePtr, IntPtr parameterPtr)
{
EventInstance instance = new EventInstance(instancePtr);
// Retrieve the user data
instance.getUserData(out var stringPtr);
// Get the string object
GCHandle stringHandle = GCHandle.FromIntPtr(stringPtr);
switch (type)
{
case EVENT_CALLBACK_TYPE.CREATE_PROGRAMMER_SOUND:
{
var parameter = (PROGRAMMER_SOUND_PROPERTIES) Marshal.PtrToStructure(parameterPtr, typeof(PROGRAMMER_SOUND_PROPERTIES));
parameter.sound = musicSound.handle;
parameter.subsoundIndex = -1;
Marshal.StructureToPtr(parameter, parameterPtr, false);
break;
}
etc...
}
Those warning mean an Event with programmer sounds is being unloaded while it’s playing, which causes a stall while we wait for the Event to stop. You can remove that warning by stopping the Event ahead of time, it’s not fatal though.
Thanks for the typo heads up too, I’ll get that fixed
You code all looks correct to me, the only thing I would advise is keeping a reference to the parent sound (the return from createSound) so when it comes time to clean up, you want to release the parent, not the subsound.
I’m will look into stopping the programmer sound ahead of time. The Sound generated from createSound is now released from all the same places as where I release the EventInstance
Example:
----- Creation, storing the Sound inside musicSound variable -----
var soundResult = RuntimeManager.CoreSystem.createSound(musicSubSoundInfo.name_or_data, soundMode | musicSubSoundInfo.mode, ref musicSubSoundInfo.exinfo, out musicSound);
.
static void StopCurrentPlayingTrack()
{
if (musicEventInstance.isValid())
{
RuntimeManager.StudioSystem.setParameterByName("Music Key", 4); // Revert global parameter to default value
musicEventInstance.setCallback(null);
musicEventInstance.setUserData(IntPtr.Zero);
musicEventInstance.setVolume(0f);
musicEventInstance.stop(STOP_MODE.IMMEDIATE);
musicSound.release(); <----------------------------- Here it is
onTrackedStopped?.Invoke();
}
}
The issue with releasing the Sound right after Stop is the Sound is still playing. Programmer sounds are “owned” by the Studio engine until you get the programmer sound destroyed callback. You don’t have to release the Sound in the destroyed callback, it can be done later but it’s important you don’t do it before as Studio still holds a reference to it.