Canno switch sub-sounds on playing stream

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.

It also crashed some times…

// 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 :slight_smile:

[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 :slight_smile:

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! :slight_smile:

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 :slight_smile:

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.

1 Like

Cheers once again Mathew.

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();
	}
}

Okay with the changes I did above I get this and the Unity editor crashes.

[FMOD] AsyncManager::asyncThreadLoop : System::update returned error 60.

UnityEngine.DebugLogHandler:Internal_Log(LogType, LogOption, String, Object)
UnityEngine.DebugLogHandler:LogFormat(LogType, Object, String, Object[])
UnityEngine.Logger:Log(LogType, Object)
UnityEngine.Debug:LogError(Object)
FMODUnity.RuntimeManager:DEBUG_CALLBACK(DEBUG_FLAGS, IntPtr, Int32, IntPtr, IntPtr) (at Assets\Plugins\FMOD\src\Runtime\RuntimeManager.cs:32)

========== OUTPUTTING STACK TRACE ==================

0x00007FFEC1F9B8A5 (fmodstudioL) FMOD_System_Update
0x00007FFEC1FD69E9 (fmodstudioL) FMOD::SystemI::createMemoryFile
0x00007FFEC1FD70C9 (fmodstudioL) FMOD::SystemI::createMemoryFile
0x00007FFEC1F7934A (fmodstudioL) FMOD::System::update
  ERROR: SymGetSymFromAddr64, GetLastError: 'Attempt to access invalid address.' (Address: 00007FFEC1DF710C)
0x00007FFEC1DF710C (fmodstudioL) (function-name not available)
  ERROR: SymGetSymFromAddr64, GetLastError: 'Attempt to access invalid address.' (Address: 00007FFEC1DC2E2C)
0x00007FFEC1DC2E2C (fmodstudioL) (function-name not available)
0x00007FFEC1E7607D (fmodstudioL) FMOD_Studio_VCA_SetVolume
0x00007FFEC1F80BD1 (fmodstudioL) FMOD_File_SetDiskBusy
0x00007FFEC1F8A9B7 (fmodstudioL) FMOD_Thread_SetAttributes
0x00007FFEC201007D (fmodstudioL) FMOD::SystemI::createMemoryFile
0x00007FFF4BE87C24 (KERNEL32) BaseThreadInitThunk
0x00007FFF4D64D4D1 (ntdll) RtlUserThreadStart

========== END OF STACKTRACE ===========

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.