/* Copyright (c) 2023 Alex Diener This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. 3. This notice may not be removed or altered from any source distribution. Alex Diener alex@ludobloom.com */ #include "audioplayer/AudioManager.h" #include "audioplayer/AudioPlayer.h" #include "audioplayer/MusicAudioStream.h" #include "shell/Shell.h" #include "utilities/AsyncTaskQueue.h" #include "utilities/HashTable.h" #include "stem_core.h" #include #include struct AudioManager_soundOverride { SoundID overridingSound; unsigned int overriddenSoundCount; SoundID * overriddenSounds; }; struct AudioManager_soundInterrupt { SoundID interruptingSound; unsigned int interruptedSoundCount; SoundID * interruptedSounds; }; struct AudioManager_registeredSoundData { PCMAudio * audio; PCMAudio * asyncLoadedAudio; AudioPlayer_category category; int loadPriority; bool asyncLoadInProgress; }; struct AudioManager_registeredMusicData { PCMAudio * audio; PCMAudio * asyncLoadedAudio; AudioPlayer_category category; int loadPriority; bool asyncLoadInProgress; AudioFrameIndex loopDuration; AudioFrameIndex asyncLoadedLoopDuration; AudioFrameIndex playbackFrame; }; struct AudioManager_musicLane { MusicAudioStream * musicStream; MusicID playingMusicID; AudioPlayer_soundInstanceID playingMusicInstanceID; unsigned int musicPaused; ShellTimer crossfadeTimerID; struct AudioManager_playMusicCommand * asyncQueuedPlayMusicCommand; }; struct AudioManager_playMusicCommand { MusicID musicID; struct AudioManager_registeredMusicData * registeredMusic; unsigned int lane; double fadeInDuration; bool loop; bool resume; bool allowAsyncLoad; }; static AsyncTaskQueue * audioManagerTaskQueue; static AudioManager_loadSoundCallback audioManagerLoadSoundCallback; static AudioManager_loadMusicCallback audioManagerLoadMusicCallback; static void * audioManagerLoadCallbackContext; static HashTable * audioManagerRegisteredSounds; static HashTable * audioManagerRegisteredMusic; static unsigned int overridingSoundCount; static struct AudioManager_soundOverride * soundOverrides; static unsigned int interruptingSoundCount; static struct AudioManager_soundInterrupt * soundInterrupts; static HashTable * soundGroupSet, * currentGroupSoundInstanceIDs, * previousGroupSoundInstanceIDs; static unsigned int inSoundGroup; static unsigned int audioManagerMusicLaneCount; static struct AudioManager_musicLane * audioManagerMusicLanes; static unsigned int audioManagerNextPlayMusicCommandIndex; static struct AudioManager_playMusicCommand audioManagerPlayMusicCommands[16]; void AudioManager_init(unsigned int musicLaneCount, unsigned int threadCount, unsigned int taskCountMaxPerThread, AudioManager_loadSoundCallback loadSoundCallback, AudioManager_loadMusicCallback loadMusicCallback, void * callbackContext) { if (audioManagerTaskQueue == NULL) { audioManagerTaskQueue = AsyncTaskQueue_create(threadCount, taskCountMaxPerThread); soundGroupSet = HashTable_create(0); currentGroupSoundInstanceIDs = HashTable_create(sizeof(AudioPlayer_soundInstanceID)); previousGroupSoundInstanceIDs = HashTable_create(sizeof(AudioPlayer_soundInstanceID)); audioManagerLoadSoundCallback = loadSoundCallback; audioManagerLoadMusicCallback = loadMusicCallback; audioManagerLoadCallbackContext = callbackContext; audioManagerRegisteredSounds = HashTable_create(sizeof(struct AudioManager_registeredSoundData)); audioManagerRegisteredMusic = HashTable_create(sizeof(struct AudioManager_registeredMusicData)); audioManagerMusicLaneCount = musicLaneCount; audioManagerMusicLanes = malloc(musicLaneCount * sizeof(*audioManagerMusicLanes)); for (unsigned int laneIndex = 0; laneIndex < musicLaneCount; laneIndex++) { audioManagerMusicLanes[laneIndex].musicStream = NULL; audioManagerMusicLanes[laneIndex].playingMusicID = MUSIC_ID_NONE; audioManagerMusicLanes[laneIndex].playingMusicInstanceID = AUDIO_PLAYER_INSTANCE_ID_NONE; audioManagerMusicLanes[laneIndex].musicPaused = false; audioManagerMusicLanes[laneIndex].crossfadeTimerID = SHELL_TIMER_INVALID; audioManagerMusicLanes[laneIndex].asyncQueuedPlayMusicCommand = NULL; } } } void AudioManager_registerSound(SoundID soundID, AudioPlayer_category category, int loadPriority) { struct AudioManager_registeredSoundData data = {NULL, NULL, category, loadPriority, false}; HashTable_set(audioManagerRegisteredSounds, HashTable_uint32Key(soundID), &data); } void AudioManager_registerMusic(MusicID musicID, AudioPlayer_category category, int loadPriority) { struct AudioManager_registeredMusicData data = {NULL, NULL, category, loadPriority, false, 0, 0, 0}; HashTable_set(audioManagerRegisteredMusic, HashTable_uint32Key(musicID), &data); } void AudioManager_registerSoundOverride(SoundID overridingSoundID, SoundID overriddenSoundID) { for (unsigned int overrideIndex = 0; overrideIndex < overridingSoundCount; overrideIndex++) { if (soundOverrides[overrideIndex].overridingSound == overridingSoundID) { soundOverrides[overrideIndex].overriddenSounds = realloc(soundOverrides[overrideIndex].overriddenSounds, (soundOverrides[overrideIndex].overriddenSoundCount + 1) * sizeof(*soundOverrides[overrideIndex].overriddenSounds)); soundOverrides[overrideIndex].overriddenSounds[soundOverrides[overrideIndex].overriddenSoundCount++] = overriddenSoundID; return; } } soundOverrides = realloc(soundOverrides, (overridingSoundCount + 1) * sizeof(*soundOverrides)); soundOverrides[overridingSoundCount].overridingSound = overridingSoundID; soundOverrides[overridingSoundCount].overriddenSoundCount = 1; soundOverrides[overridingSoundCount].overriddenSounds = malloc(sizeof(*soundOverrides[overridingSoundCount].overriddenSounds)); soundOverrides[overridingSoundCount].overriddenSounds[0] = overriddenSoundID; overridingSoundCount++; } void AudioManager_registerSoundInterrupt(SoundID interruptingSoundID, SoundID interruptedSoundID) { for (unsigned int interruptIndex = 0; interruptIndex < interruptingSoundCount; interruptIndex++) { if (soundInterrupts[interruptIndex].interruptingSound == interruptingSoundID) { soundInterrupts[interruptIndex].interruptedSounds = realloc(soundInterrupts[interruptIndex].interruptedSounds, (soundInterrupts[interruptIndex].interruptedSoundCount + 1) * sizeof(*soundInterrupts[interruptIndex].interruptedSounds)); soundInterrupts[interruptIndex].interruptedSounds[soundInterrupts[interruptIndex].interruptedSoundCount++] = interruptedSoundID; return; } } soundInterrupts = realloc(soundInterrupts, (interruptingSoundCount + 1) * sizeof(*soundInterrupts)); soundInterrupts[interruptingSoundCount].interruptingSound = interruptingSoundID; soundInterrupts[interruptingSoundCount].interruptedSoundCount = 1; soundInterrupts[interruptingSoundCount].interruptedSounds = malloc(sizeof(*soundInterrupts[interruptingSoundCount].interruptedSounds)); soundInterrupts[interruptingSoundCount].interruptedSounds[0] = interruptedSoundID; interruptingSoundCount++; } static bool setPlaybackFrame(HashTable * hashTable, HashTable_key key, void * value, void * context) { MusicAudioStream * stream = context; struct AudioManager_registeredMusicData * registeredMusic = value; if (registeredMusic->audio == stream->audio) { registeredMusic->playbackFrame = stream->framePlayedCountTotal; return false; } return true; } static void streamEndedCallback(PCMAudioStream * stream, bool canceled, void * context) { HashTable_foreach(audioManagerRegisteredMusic, setPlaybackFrame, stream); call_virtual(dispose, stream); } static void loadMusicImmediate(MusicID musicID, struct AudioManager_registeredMusicData * registeredMusic) { if (registeredMusic->audio == NULL) { registeredMusic->audio = audioManagerLoadMusicCallback(musicID, ®isteredMusic->loopDuration, audioManagerLoadCallbackContext); } } static void performPlayMusicCommand(struct AudioManager_playMusicCommand * command) { unsigned int lane = command->lane; if (command->allowAsyncLoad && command->registeredMusic->audio == NULL && command->registeredMusic->asyncLoadInProgress) { audioManagerMusicLanes[lane].asyncQueuedPlayMusicCommand = command; } else { audioManagerMusicLanes[lane].asyncQueuedPlayMusicCommand = NULL; loadMusicImmediate(command->musicID, command->registeredMusic); AudioFrameIndex playbackFrame = command->resume ? command->registeredMusic->playbackFrame : 0; audioManagerMusicLanes[lane].musicStream = MusicAudioStream_create(command->registeredMusic->audio, playbackFrame, command->registeredMusic->loopDuration); if (playbackFrame > 0 && command->fadeInDuration > 0.0) { call_virtual(rampVolume, audioManagerMusicLanes[lane].musicStream, true, command->fadeInDuration); } audioManagerMusicLanes[lane].playingMusicInstanceID = AudioPlayer_startAudioStream((PCMAudioStream *) audioManagerMusicLanes[lane].musicStream, command->loop, true, command->registeredMusic->category, streamEndedCallback, NULL); if (audioManagerMusicLanes[lane].musicPaused) { AudioPlayer_setAudioStreamPaused(audioManagerMusicLanes[lane].playingMusicInstanceID, true); } audioManagerMusicLanes[lane].crossfadeTimerID = SHELL_TIMER_INVALID; } } static void asyncLoadSoundWorkCallback(void * context1, void * context2) { SoundID soundID = (uintptr_t) context1; struct AudioManager_registeredSoundData * registeredSound = HashTable_get(audioManagerRegisteredSounds, HashTable_uint32Key(soundID)); registeredSound->asyncLoadedAudio = audioManagerLoadSoundCallback(soundID, audioManagerLoadCallbackContext); } static void asyncLoadSoundCompleteCallback(void * context1, void * context2) { SoundID soundID = (uintptr_t) context1; struct AudioManager_registeredSoundData * registeredSound = HashTable_get(audioManagerRegisteredSounds, HashTable_uint32Key(soundID)); if (registeredSound->audio == NULL) { registeredSound->audio = registeredSound->asyncLoadedAudio; } else { // Sound was synchronously loaded before the asynchronous loading could complete PCMAudio_dispose(registeredSound->asyncLoadedAudio); } registeredSound->asyncLoadInProgress = false; } static void asyncLoadMusicWorkCallback(void * context1, void * context2) { MusicID musicID = (uintptr_t) context1; struct AudioManager_registeredMusicData * registeredMusic = HashTable_get(audioManagerRegisteredMusic, HashTable_uint32Key(musicID)); registeredMusic->asyncLoadedAudio = audioManagerLoadMusicCallback(musicID, ®isteredMusic->asyncLoadedLoopDuration, audioManagerLoadCallbackContext); } static void asyncLoadMusicCompleteCallback(void * context1, void * context2) { MusicID musicID = (uintptr_t) context1; struct AudioManager_registeredMusicData * registeredMusic = HashTable_get(audioManagerRegisteredMusic, HashTable_uint32Key(musicID)); if (registeredMusic->audio == NULL) { registeredMusic->audio = registeredMusic->asyncLoadedAudio; registeredMusic->loopDuration = registeredMusic->asyncLoadedLoopDuration; } else { // Music was synchronously loaded before the asynchronous loading could complete PCMAudio_dispose(registeredMusic->asyncLoadedAudio); } registeredMusic->asyncLoadInProgress = false; for (unsigned int laneIndex = 0; laneIndex < audioManagerMusicLaneCount; laneIndex++) { if (audioManagerMusicLanes[laneIndex].asyncQueuedPlayMusicCommand != NULL && audioManagerMusicLanes[laneIndex].asyncQueuedPlayMusicCommand->musicID == musicID) { performPlayMusicCommand(audioManagerMusicLanes[laneIndex].asyncQueuedPlayMusicCommand); break; } } } struct asyncLoadItem { uint32_t identifier; int loadPriority; AsyncTaskQueue_callback workCallback; AsyncTaskQueue_callback completeCallback; }; struct asyncLoadContext { unsigned int itemIndex; struct asyncLoadItem * items; }; static bool asyncForeachSoundCallback(HashTable * hashTable, HashTable_key key, void * value, void * context) { struct asyncLoadContext * contextStruct = context; struct AudioManager_registeredSoundData * dataStruct = value; contextStruct->items[contextStruct->itemIndex].identifier = key.data.uint32; contextStruct->items[contextStruct->itemIndex].loadPriority = dataStruct->loadPriority; contextStruct->items[contextStruct->itemIndex].workCallback = asyncLoadSoundWorkCallback; contextStruct->items[contextStruct->itemIndex].completeCallback = asyncLoadSoundCompleteCallback; contextStruct->itemIndex++; dataStruct->asyncLoadInProgress = true; return true; } static bool asyncForeachMusicCallback(HashTable * hashTable, HashTable_key key, void * value, void * context) { struct asyncLoadContext * contextStruct = context; struct AudioManager_registeredMusicData * dataStruct = value; contextStruct->items[contextStruct->itemIndex].identifier = key.data.uint32; contextStruct->items[contextStruct->itemIndex].loadPriority = dataStruct->loadPriority; contextStruct->items[contextStruct->itemIndex].workCallback = asyncLoadMusicWorkCallback; contextStruct->items[contextStruct->itemIndex].completeCallback = asyncLoadMusicCompleteCallback; contextStruct->itemIndex++; dataStruct->asyncLoadInProgress = true; return true; } static int compareItemLoadPriority(const void * lhsUntyped, const void * rhsUntyped) { const struct asyncLoadItem * lhs = lhsUntyped, * rhs = rhsUntyped; return (lhs->loadPriority > rhs->loadPriority) * 2 - 1; } void AudioManager_loadAllAudioAsync(void) { unsigned int itemCount = audioManagerRegisteredSounds->count + audioManagerRegisteredMusic->count; struct asyncLoadItem itemsToLoad[itemCount]; struct asyncLoadContext contextStruct = {0, itemsToLoad}; HashTable_foreach(audioManagerRegisteredSounds, asyncForeachSoundCallback, &contextStruct); HashTable_foreach(audioManagerRegisteredMusic, asyncForeachMusicCallback, &contextStruct); qsort(itemsToLoad, itemCount, sizeof(*itemsToLoad), compareItemLoadPriority); for (unsigned int itemIndex = 0; itemIndex < itemCount; itemIndex++) { AsyncTaskQueue_queueTask(audioManagerTaskQueue, itemsToLoad[itemIndex].workCallback, itemsToLoad[itemIndex].completeCallback, (void *) (uintptr_t) itemsToLoad[itemIndex].identifier, NULL); } } static bool syncForeachSoundCallback(HashTable * hashTable, HashTable_key key, void * value, void * context) { struct AudioManager_registeredSoundData * dataStruct = value; dataStruct->audio = audioManagerLoadSoundCallback(key.data.uint32, audioManagerLoadCallbackContext); return true; } static bool syncForeachMusicCallback(HashTable * hashTable, HashTable_key key, void * value, void * context) { struct AudioManager_registeredMusicData * dataStruct = value; dataStruct->audio = audioManagerLoadMusicCallback(key.data.uint32, &dataStruct->loopDuration, audioManagerLoadCallbackContext); return true; } void AudioManager_loadAllAudioImmediate(void) { HashTable_foreach(audioManagerRegisteredSounds, syncForeachSoundCallback, NULL); HashTable_foreach(audioManagerRegisteredMusic, syncForeachMusicCallback, NULL); } static void loadSoundImmediate(SoundID soundID, struct AudioManager_registeredSoundData * registeredSound) { if (registeredSound->audio == NULL) { registeredSound->audio = audioManagerLoadSoundCallback(soundID, audioManagerLoadCallbackContext); } } static AudioPlayer_soundInstanceID playSoundImmediate(SoundID soundID) { struct AudioManager_registeredSoundData * registeredSound = HashTable_get(audioManagerRegisteredSounds, HashTable_uint32Key(soundID)); if (registeredSound != NULL) { loadSoundImmediate(soundID, registeredSound); return AudioPlayer_playAudioData(registeredSound->audio, registeredSound->category, 1.0f, 0, 0, NULL, NULL); } return AUDIO_PLAYER_INSTANCE_ID_NONE; } void AudioManager_playSound(SoundID soundID) { if (inSoundGroup) { HashTable_set(soundGroupSet, HashTable_uint32Key(soundID), NULL); } else { playSoundImmediate(soundID); } } void AudioManager_beginSoundGroup(void) { inSoundGroup++; AudioPlayer_beginPlayGroup(); } static bool playGroupedSound(HashTable * hashTable, HashTable_key key, void * value, void * context) { AudioPlayer_soundInstanceID instanceID = playSoundImmediate(key.data.uint32); if (instanceID != AUDIO_PLAYER_INSTANCE_ID_NONE) { HashTable_set(currentGroupSoundInstanceIDs, key, &instanceID); } for (unsigned int interruptingSoundIndex = 0; interruptingSoundIndex < interruptingSoundCount; interruptingSoundIndex++) { if (soundInterrupts[interruptingSoundIndex].interruptingSound == key.data.uint32) { for (unsigned int interruptedSoundIndex = 0; interruptedSoundIndex < soundInterrupts[interruptingSoundIndex].interruptedSoundCount; interruptedSoundIndex++) { AudioPlayer_soundInstanceID * instanceID = HashTable_get(previousGroupSoundInstanceIDs, HashTable_uint32Key(soundInterrupts[interruptingSoundIndex].interruptedSounds[interruptedSoundIndex])); if (instanceID != NULL) { AudioPlayer_cancelSoundEffect(*instanceID); } } break; } } return true; } void AudioManager_endSoundGroup(void) { if (inSoundGroup == 0) { #ifdef DEBUG fprintf(stderr, "Warning: AudioManager_endSoundGroup() underflow\n"); #endif } else { inSoundGroup--; if (inSoundGroup == 0) { inSoundGroup = false; for (unsigned int overrideIndex = 0; overrideIndex < overridingSoundCount; overrideIndex++) { if (HashTable_get(soundGroupSet, HashTable_uint32Key(soundOverrides[overrideIndex].overridingSound)) != NULL) { for (unsigned int overriddenSoundIndex = 0; overriddenSoundIndex < soundOverrides[overrideIndex].overriddenSoundCount; overriddenSoundIndex++) { HashTable_delete(soundGroupSet, HashTable_uint32Key(soundOverrides[overrideIndex].overriddenSounds[overriddenSoundIndex])); } } } HashTable_foreach(soundGroupSet, playGroupedSound, NULL); AudioPlayer_endPlayGroup(); HashTable_deleteAll(soundGroupSet); HashTable * swap = previousGroupSoundInstanceIDs; previousGroupSoundInstanceIDs = currentGroupSoundInstanceIDs; currentGroupSoundInstanceIDs = swap; HashTable_deleteAll(currentGroupSoundInstanceIDs); } } } static void playMusicTimerCallback(ShellTimer timerID, void * context) { performPlayMusicCommand(context); } void AudioManager_playMusic(MusicID musicID, unsigned int lane, bool loop, bool resume, bool allowAsyncLoad, double fadeOutDuration, double crossfadeDuration, double fadeInDuration) { if (lane < audioManagerMusicLaneCount && musicID != audioManagerMusicLanes[lane].playingMusicID) { if (audioManagerMusicLanes[lane].musicStream != NULL) { struct AudioManager_registeredMusicData * registeredMusic = HashTable_get(audioManagerRegisteredMusic, HashTable_uint32Key(audioManagerMusicLanes[lane].playingMusicID)); if (registeredMusic != NULL) { // This is a fallback in case of the same music being restarted before its fadeout can naturally end. // streamEndedCallback sets playbackFrame to a more accurate value, but this allows it to be // approximately correct even in the degenerate case. registeredMusic->playbackFrame = audioManagerMusicLanes[lane].musicStream->framePlayedCountTotal + fmax(0.0, fadeOutDuration) * AudioPlayer_getSampleFormat().sampleRate; } call_virtual(rampVolume, audioManagerMusicLanes[lane].musicStream, false, fadeOutDuration); audioManagerMusicLanes[lane].musicStream = NULL; audioManagerMusicLanes[lane].playingMusicInstanceID = AUDIO_PLAYER_INSTANCE_ID_NONE; } Shell_cancelTimer(audioManagerMusicLanes[lane].crossfadeTimerID); audioManagerMusicLanes[lane].asyncQueuedPlayMusicCommand = NULL; if (musicID != MUSIC_ID_NONE) { struct AudioManager_registeredMusicData * registeredMusic = HashTable_get(audioManagerRegisteredMusic, HashTable_uint32Key(musicID)); if (registeredMusic != NULL) { audioManagerPlayMusicCommands[audioManagerNextPlayMusicCommandIndex].musicID = musicID; audioManagerPlayMusicCommands[audioManagerNextPlayMusicCommandIndex].registeredMusic = registeredMusic; audioManagerPlayMusicCommands[audioManagerNextPlayMusicCommandIndex].lane = lane; audioManagerPlayMusicCommands[audioManagerNextPlayMusicCommandIndex].fadeInDuration = fadeInDuration; audioManagerPlayMusicCommands[audioManagerNextPlayMusicCommandIndex].loop = loop; audioManagerPlayMusicCommands[audioManagerNextPlayMusicCommandIndex].resume = resume; audioManagerPlayMusicCommands[audioManagerNextPlayMusicCommandIndex].allowAsyncLoad = allowAsyncLoad; if (crossfadeDuration <= 0.0) { performPlayMusicCommand(&audioManagerPlayMusicCommands[audioManagerNextPlayMusicCommandIndex]); } else { audioManagerMusicLanes[lane].crossfadeTimerID = Shell_setTimer(crossfadeDuration, false, playMusicTimerCallback, &audioManagerPlayMusicCommands[audioManagerNextPlayMusicCommandIndex]); } audioManagerNextPlayMusicCommandIndex = (audioManagerNextPlayMusicCommandIndex + 1) % sizeof_count(audioManagerPlayMusicCommands); } } audioManagerMusicLanes[lane].playingMusicID = musicID; } } void AudioManager_pauseMusic(unsigned int lane, double fadeOutDuration) { if (lane < audioManagerMusicLaneCount) { if (audioManagerMusicLanes[lane].musicPaused == 0) { AudioPlayer_setAudioStreamPaused(audioManagerMusicLanes[lane].playingMusicInstanceID, true); } audioManagerMusicLanes[lane].musicPaused++; } } void AudioManager_unpauseMusic(unsigned int lane, double fadeInDuration) { if (lane < audioManagerMusicLaneCount) { if (audioManagerMusicLanes[lane].musicPaused == 0) { #ifdef DEBUG fprintf(stderr, "Warning: AudioManager_unpauseMusic() underflow in lane %u\n", lane); #endif } else { audioManagerMusicLanes[lane].musicPaused--; if (audioManagerMusicLanes[lane].musicPaused == 0) { AudioPlayer_setAudioStreamPaused(audioManagerMusicLanes[lane].playingMusicInstanceID, false); } } } } void AudioManager_stopMusic(unsigned int lane, double fadeOutDuration) { if (lane < audioManagerMusicLaneCount) { AudioPlayer_stopAudioStream(audioManagerMusicLanes[lane].playingMusicInstanceID); audioManagerMusicLanes[lane].playingMusicID = MUSIC_ID_NONE; } } MusicID AudioManager_getPlayingMusicID(unsigned int lane) { if (lane < audioManagerMusicLaneCount) { return audioManagerMusicLanes[lane].playingMusicID; } return MUSIC_ID_NONE; } PCMAudio * AudioManager_getSoundData(SoundID soundID) { struct AudioManager_registeredSoundData * registeredSound = HashTable_get(audioManagerRegisteredSounds, HashTable_uint32Key(soundID)); if (registeredSound == NULL) { return NULL; } loadSoundImmediate(soundID, registeredSound); return registeredSound->audio; } PCMAudio * AudioManager_getMusicData(MusicID musicID) { struct AudioManager_registeredMusicData * registeredMusic = HashTable_get(audioManagerRegisteredMusic, HashTable_uint32Key(musicID)); if (registeredMusic == NULL) { return NULL; } loadMusicImmediate(musicID, registeredMusic); return registeredMusic->audio; } AudioFrameIndex AudioManager_getMusicLoopDuration(MusicID musicID) { struct AudioManager_registeredMusicData * registeredMusic = HashTable_get(audioManagerRegisteredMusic, HashTable_uint32Key(musicID)); if (registeredMusic == NULL) { return 0; } loadMusicImmediate(musicID, registeredMusic); return registeredMusic->loopDuration; }