/* Copyright (c) 2019 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/AudioPlayer.h" #include "audiosynth/AudioMath.h" #include "nativeaudio/AudioOut.h" #include "shell/ShellThreads.h" #include #include #include #define AUDIO_STREAM_BUFFER_CHUNK_FRAME_COUNT 44100 #define AUDIO_STREAM_BUFFER_CHUNK_COUNT 2 #define AUDIO_STREAM_BUFFER_CHUNK_SIZE (AUDIO_STREAM_BUFFER_CHUNK_FRAME_COUNT * 2 * 2) #define MIX_BUFFER_FRAME_COUNT 4096 struct AudioPlayer_sourceState { bool allocated; // Writable only by main thread bool canceled; // Writable only by main thread bool ended; // Writable only by audio thread bool paused; // Writable only by main thread unsigned int playGroupID; // Writable only by main thread AudioFrameIndex frameIndex; // Writable only by audio thread AudioFrameIndex endFrameCount; Atom identifier; PCMAudio * audioData; AudioResampleState resampleState; AudioPlayer_soundInstanceID instanceID; AudioPlayer_category category; float amplitude; void (* endedCallback)(PCMAudio * audio, void * context); void * callbackContext; }; struct AudioPlayer_streamState { bool allocated; // Writable only by main thread bool canceled; // Writable only by main thread bool ended; // Writable only by audio thread bool paused; // Writable only by main thread unsigned int playGroupID; // Writable only by main thread bool bufferEnded; // Writable only by main thread PCMAudioStream * audioStream; AudioResampleState resampleState; bool loop; bool lowLatency; AudioFrameIndex frameIndex; void * buffer; AudioFrameIndex framesBuffered; AudioFrameIndex nextBufferThreshold; AudioPlayer_soundInstanceID instanceID; AudioPlayer_category category; void (* endedCallback)(PCMAudioStream * stream, bool canceled, void * context); void * callbackContext; }; static AudioPlayer_referenceSoundEffectDataCallback referenceSoundEffectDataCallback; static AudioPlayer_releaseSoundEffectDataCallback releaseSoundEffectDataCallback; static void * callbackContext; static unsigned int sourceCountMax; static struct AudioPlayer_sourceState * sourceStates; static unsigned int streamCountMax; static struct AudioPlayer_streamState * streamStates; static unsigned int nextInstanceID; static bool audioOutputActive; static void * mixBuffer; static void * lowLatencyStreamBuffer; static bool paused; static unsigned int categoryCount; static float * categoryVolumes; static bool pauseWhenIdle = true; static bool inited; static AudioOut_sampleFormat transportFormat; static unsigned int inPlayGroup; static unsigned int lastPlayGroupID; static bool audioOutputEnabled = true; static bool advanceStreamsAtAmplitudeZero = true; static void mixAllSources(void * outSamples, unsigned long frameCount) { while (frameCount > 0) { AudioFrameIndex mixFrameCount = frameCount; if (mixFrameCount > MIX_BUFFER_FRAME_COUNT) { mixFrameCount = MIX_BUFFER_FRAME_COUNT; } memset(mixBuffer, 0, mixFrameCount * transportFormat.channelCount * transportFormat.bytesPerSample); unsigned int playGroupID = lastPlayGroupID; for (unsigned int sourceIndex = 0; sourceIndex < sourceCountMax; sourceIndex++) { if (!sourceStates[sourceIndex].allocated || sourceStates[sourceIndex].playGroupID > playGroupID) { continue; } if (sourceStates[sourceIndex].canceled) { sourceStates[sourceIndex].ended = true; } if (sourceStates[sourceIndex].ended || sourceStates[sourceIndex].paused) { continue; } AudioFrameIndex inFrameIndex = sourceStates[sourceIndex].frameIndex; AudioFrameIndex inFrameCountMax = sourceStates[sourceIndex].endFrameCount; AudioFrameIndex framesRead; float amplitudeMultiplier = categoryVolumes[sourceStates[sourceIndex].category] * sourceStates[sourceIndex].amplitude; if (amplitudeMultiplier == 0.0f) { inFrameIndex += mixFrameCount > inFrameCountMax ? inFrameCountMax : mixFrameCount; } else { float channelMultipliers[transportFormat.channelCount]; for (unsigned int channelIndex = 0; channelIndex < transportFormat.channelCount; channelIndex++) { channelMultipliers[channelIndex] = amplitudeMultiplier; } mixAudioSamples(sourceStates[sourceIndex].audioData->samples + inFrameIndex * sourceStates[sourceIndex].audioData->channelCount * sourceStates[sourceIndex].audioData->bytesPerSample, sourceStates[sourceIndex].audioData->channelCount, sourceStates[sourceIndex].audioData->sampleRate, sourceStates[sourceIndex].audioData->bytesPerSample, mixBuffer, transportFormat.channelCount, transportFormat.sampleRate, transportFormat.bytesPerSample, channelMultipliers, inFrameCountMax - inFrameIndex, mixFrameCount, &framesRead, NULL, &sourceStates[sourceIndex].resampleState); inFrameIndex += framesRead; } sourceStates[sourceIndex].frameIndex = inFrameIndex; if (inFrameIndex >= inFrameCountMax) { sourceStates[sourceIndex].ended = true; } } for (unsigned int streamIndex = 0; streamIndex < streamCountMax; streamIndex++) { if (!streamStates[streamIndex].allocated || streamStates[streamIndex].playGroupID > playGroupID) { continue; } if (streamStates[streamIndex].canceled) { streamStates[streamIndex].ended = true; } if (streamStates[streamIndex].ended || streamStates[streamIndex].paused) { continue; } if (!advanceStreamsAtAmplitudeZero && categoryVolumes[streamStates[streamIndex].category] == 0.0f) { continue; } if (streamStates[streamIndex].lowLatency) { AudioFrameIndex framesRead = call_virtual(read, streamStates[streamIndex].audioStream, mixFrameCount, lowLatencyStreamBuffer, streamStates[streamIndex].loop); if (framesRead == 0) { streamStates[streamIndex].ended = true; } else { AudioFrameIndex inFrameIndex = 0; AudioFrameIndex inFrameCountMax = framesRead; float amplitudeMultiplier = categoryVolumes[streamStates[streamIndex].category]; if (amplitudeMultiplier == 0) { inFrameIndex += mixFrameCount > inFrameCountMax ? inFrameCountMax : mixFrameCount; } else { float channelMultipliers[transportFormat.channelCount]; for (unsigned int channelIndex = 0; channelIndex < transportFormat.channelCount; channelIndex++) { channelMultipliers[channelIndex] = amplitudeMultiplier; } mixAudioSamples(lowLatencyStreamBuffer, streamStates[streamIndex].audioStream->channelCount, streamStates[streamIndex].audioStream->sampleRate, streamStates[streamIndex].audioStream->bytesPerSample, mixBuffer, transportFormat.channelCount, transportFormat.sampleRate, transportFormat.bytesPerSample, channelMultipliers, inFrameCountMax - inFrameIndex, mixFrameCount, &framesRead, NULL, &streamStates[streamIndex].resampleState); inFrameIndex += framesRead; } streamStates[streamIndex].frameIndex += inFrameIndex; } } else { AudioFrameIndex inFrameIndex = streamStates[streamIndex].frameIndex; AudioFrameIndex inFrameCountMax = streamStates[streamIndex].framesBuffered; AudioFrameIndex framesRead; float amplitudeMultiplier = categoryVolumes[streamStates[streamIndex].category]; if (amplitudeMultiplier == 0) { inFrameIndex += mixFrameCount > inFrameCountMax ? inFrameCountMax : mixFrameCount; } else { float channelMultipliers[transportFormat.channelCount]; for (unsigned int channelIndex = 0; channelIndex < transportFormat.channelCount; channelIndex++) { channelMultipliers[channelIndex] = amplitudeMultiplier; } AudioFrameIndex bufferFrameIndex = inFrameIndex % (AUDIO_STREAM_BUFFER_CHUNK_FRAME_COUNT * AUDIO_STREAM_BUFFER_CHUNK_COUNT); AudioFrameIndex bufferFrameCount = mixFrameCount; if (bufferFrameIndex + bufferFrameCount > AUDIO_STREAM_BUFFER_CHUNK_FRAME_COUNT * AUDIO_STREAM_BUFFER_CHUNK_COUNT) { mixAudioSamples(streamStates[streamIndex].buffer + bufferFrameIndex * streamStates[streamIndex].audioStream->channelCount * streamStates[streamIndex].audioStream->bytesPerSample, streamStates[streamIndex].audioStream->channelCount, streamStates[streamIndex].audioStream->sampleRate, streamStates[streamIndex].audioStream->bytesPerSample, mixBuffer, transportFormat.channelCount, transportFormat.sampleRate, transportFormat.bytesPerSample, channelMultipliers, AUDIO_STREAM_BUFFER_CHUNK_FRAME_COUNT * AUDIO_STREAM_BUFFER_CHUNK_COUNT - bufferFrameIndex, mixFrameCount, &framesRead, NULL, &streamStates[streamIndex].resampleState); inFrameIndex += framesRead; bufferFrameCount -= framesRead; bufferFrameIndex = 0; } mixAudioSamples(streamStates[streamIndex].buffer + bufferFrameIndex * streamStates[streamIndex].audioStream->channelCount * streamStates[streamIndex].audioStream->bytesPerSample, streamStates[streamIndex].audioStream->channelCount, streamStates[streamIndex].audioStream->sampleRate, streamStates[streamIndex].audioStream->bytesPerSample, mixBuffer, transportFormat.channelCount, transportFormat.sampleRate, transportFormat.bytesPerSample, channelMultipliers, inFrameCountMax - inFrameIndex, bufferFrameCount, &framesRead, NULL, &streamStates[streamIndex].resampleState); inFrameIndex += framesRead; } streamStates[streamIndex].frameIndex = inFrameIndex; if (inFrameIndex >= inFrameCountMax && streamStates[streamIndex].bufferEnded) { streamStates[streamIndex].ended = true; } } } memcpy(outSamples, mixBuffer, mixFrameCount * transportFormat.channelCount * transportFormat.bytesPerSample); outSamples += mixFrameCount * transportFormat.channelCount * transportFormat.bytesPerSample; frameCount -= mixFrameCount; } } static void audioOutCallback(void * outSamples, unsigned int frameCount, void * context) { if (paused) { return; } mixAllSources(outSamples, frameCount); } void AudioPlayer_init(const char * processName, unsigned int inSourceCountMax, unsigned int inStreamCountMax, unsigned int inCategoryCount, AudioOut_sampleFormat sampleFormat, AudioPlayer_referenceSoundEffectDataCallback inReferenceSoundEffectDataCallback, AudioPlayer_releaseSoundEffectDataCallback inReleaseSoundEffectDataCallback, void * inCallbackContext) { referenceSoundEffectDataCallback = inReferenceSoundEffectDataCallback; releaseSoundEffectDataCallback = inReleaseSoundEffectDataCallback; callbackContext = inCallbackContext; sourceCountMax = inSourceCountMax; streamCountMax = inStreamCountMax; categoryCount = inCategoryCount; sourceStates = calloc(sizeof(*sourceStates), sourceCountMax); streamStates = calloc(sizeof(*streamStates), streamCountMax); for (unsigned int streamIndex = 0; streamIndex < streamCountMax; streamIndex++) { streamStates[streamIndex].buffer = malloc(AUDIO_STREAM_BUFFER_CHUNK_SIZE * AUDIO_STREAM_BUFFER_CHUNK_COUNT); } categoryVolumes = malloc(sizeof(*categoryVolumes) * categoryCount); for (unsigned int category = 0; category < categoryCount; category++) { categoryVolumes[category] = 1.0f; } mixBuffer = malloc(sizeof(*mixBuffer) * sampleFormat.channelCount * MIX_BUFFER_FRAME_COUNT); lowLatencyStreamBuffer = malloc(sizeof(*lowLatencyStreamBuffer) * sampleFormat.channelCount * MIX_BUFFER_FRAME_COUNT); AudioOut_setHostFormat(sampleFormat); AudioOut_init(processName); transportFormat = AudioOut_getTransportFormat(); inited = true; } AudioOut_sampleFormat AudioPlayer_getSampleFormat(void) { return AudioOut_getTransportFormat(); } void AudioPlayer_setSampleFormat(AudioOut_sampleFormat sampleFormat) { AudioOut_setHostFormat(sampleFormat); AudioOut_setTransportFormat(sampleFormat); transportFormat = AudioOut_getTransportFormat(); } unsigned int AudioPlayer_getSampleRate(void) { return AudioOut_getTransportFormat().sampleRate; } void AudioPlayer_setSampleRate(unsigned int sampleRate) { AudioOut_sampleFormat sampleFormat = AudioOut_getTransportFormat(); sampleFormat.sampleRate = sampleRate; AudioOut_setTransportFormat(sampleFormat); } static AudioPlayer_soundInstanceID playSoundInternal(unsigned int sourceIndex, PCMAudio * audio, Atom identifier, AudioPlayer_category category, float amplitude, AudioFrameIndex startFrameIndex, AudioFrameIndex endFrameIndex, void (* endedCallback)(PCMAudio * audio, void * context), void * callbackContext) { if (audio == NULL) { return AUDIO_PLAYER_INSTANCE_ID_NONE; } sourceStates[sourceIndex].audioData = audio; sourceStates[sourceIndex].identifier = identifier; sourceStates[sourceIndex].frameIndex = startFrameIndex; if (endFrameIndex == 0 || endFrameIndex >= audio->frameCount) { sourceStates[sourceIndex].endFrameCount = audio->frameCount; } else { sourceStates[sourceIndex].endFrameCount = endFrameIndex + 1; } sourceStates[sourceIndex].instanceID = nextInstanceID++; sourceStates[sourceIndex].category = category; sourceStates[sourceIndex].amplitude = amplitude; sourceStates[sourceIndex].resampleState = initAudioResampleState(); sourceStates[sourceIndex].ended = false; sourceStates[sourceIndex].paused = false; sourceStates[sourceIndex].canceled = false; sourceStates[sourceIndex].endedCallback = endedCallback; sourceStates[sourceIndex].callbackContext = callbackContext; sourceStates[sourceIndex].playGroupID = lastPlayGroupID + (inPlayGroup > 0); sourceStates[sourceIndex].allocated = true; if (!audioOutputActive) { audioOutputActive = true; if (!paused && audioOutputEnabled) { AudioOut_startOutput(audioOutCallback, NULL); } } return sourceStates[sourceIndex].instanceID; } AudioPlayer_soundInstanceID AudioPlayer_playSoundEffect(Atom soundEffectIdentifier, AudioPlayer_category category) { if (!inited || soundEffectIdentifier == NULL || category >= categoryCount || referenceSoundEffectDataCallback == NULL) { return AUDIO_PLAYER_INSTANCE_ID_NONE; } for (unsigned int sourceIndex = 0; sourceIndex < sourceCountMax; sourceIndex++) { if (!sourceStates[sourceIndex].allocated) { PCMAudio * audio = referenceSoundEffectDataCallback(soundEffectIdentifier, callbackContext); return playSoundInternal(sourceIndex, audio, soundEffectIdentifier, category, 1.0f, 0, 0, NULL, NULL); } } return AUDIO_PLAYER_INSTANCE_ID_NONE; } AudioPlayer_soundInstanceID AudioPlayer_playAudioData(PCMAudio * audio, AudioPlayer_category category, float amplitude, AudioFrameIndex startFrameIndex, AudioFrameIndex endFrameIndex, void (* endedCallback)(PCMAudio * audio, void * context), void * callbackContext) { if (!inited || audio == NULL || category >= categoryCount) { return AUDIO_PLAYER_INSTANCE_ID_NONE; } for (unsigned int sourceIndex = 0; sourceIndex < sourceCountMax; sourceIndex++) { if (!sourceStates[sourceIndex].allocated) { return playSoundInternal(sourceIndex, audio, NULL, category, amplitude, startFrameIndex, endFrameIndex, endedCallback, callbackContext); } } return AUDIO_PLAYER_INSTANCE_ID_NONE; } void AudioPlayer_cancelSoundEffect(AudioPlayer_soundInstanceID soundInstanceID) { if (!inited) { return; } for (unsigned int sourceIndex = 0; sourceIndex < sourceCountMax; sourceIndex++) { if (sourceStates[sourceIndex].allocated && sourceStates[sourceIndex].instanceID == soundInstanceID) { sourceStates[sourceIndex].canceled = true; break; } } } void AudioPlayer_cancelSoundEffectsPlayingAudioData(PCMAudio * audioData) { if (!inited) { return; } for (unsigned int sourceIndex = 0; sourceIndex < sourceCountMax; sourceIndex++) { if (sourceStates[sourceIndex].allocated && sourceStates[sourceIndex].audioData == audioData) { sourceStates[sourceIndex].canceled = true; } } } void AudioPlayer_cancelAllSoundEffects(void) { if (!inited) { return; } for (unsigned int sourceIndex = 0; sourceIndex < sourceCountMax; sourceIndex++) { sourceStates[sourceIndex].canceled = true; } } bool AudioPlayer_anyPendingCanceledSoundEffects(void) { if (!inited) { return false; } for (unsigned int sourceIndex = 0; sourceIndex < sourceCountMax; sourceIndex++) { if (sourceStates[sourceIndex].allocated && sourceStates[sourceIndex].canceled && !sourceStates[sourceIndex].ended) { return true; } } for (unsigned int streamIndex = 0; streamIndex < streamCountMax; streamIndex++) { if (streamStates[streamIndex].allocated && streamStates[streamIndex].canceled && !streamStates[streamIndex].ended) { return true; } } return false; } void AudioPlayer_setSoundEffectPaused(AudioPlayer_soundInstanceID soundInstanceID, bool paused) { if (!inited) { return; } for (unsigned int sourceIndex = 0; sourceIndex < sourceCountMax; sourceIndex++) { if (sourceStates[sourceIndex].allocated && sourceStates[sourceIndex].instanceID == soundInstanceID) { sourceStates[sourceIndex].paused = paused; break; } } } AudioPlayer_soundInstanceID AudioPlayer_startAudioStream(PCMAudioStream * stream, bool loop, bool lowLatency, AudioPlayer_category category, void (* endedCallback)(PCMAudioStream * stream, bool canceled, void * context), void * callbackContext) { if (!inited || stream == NULL || category >= categoryCount) { return AUDIO_PLAYER_INSTANCE_ID_NONE; } for (unsigned int streamIndex = 0; streamIndex < streamCountMax; streamIndex++) { if (!streamStates[streamIndex].allocated) { streamStates[streamIndex].audioStream = stream; streamStates[streamIndex].instanceID = nextInstanceID++; streamStates[streamIndex].category = category; streamStates[streamIndex].resampleState = initAudioResampleState(); streamStates[streamIndex].loop = loop; streamStates[streamIndex].lowLatency = lowLatency; streamStates[streamIndex].frameIndex = 0; if (!streamStates[streamIndex].lowLatency) { streamStates[streamIndex].framesBuffered = call_virtual(read, stream, AUDIO_STREAM_BUFFER_CHUNK_FRAME_COUNT * AUDIO_STREAM_BUFFER_CHUNK_COUNT, streamStates[streamIndex].buffer, loop); streamStates[streamIndex].nextBufferThreshold = AUDIO_STREAM_BUFFER_CHUNK_FRAME_COUNT; } streamStates[streamIndex].bufferEnded = false; streamStates[streamIndex].ended = false; streamStates[streamIndex].paused = false; streamStates[streamIndex].canceled = false; streamStates[streamIndex].playGroupID = lastPlayGroupID + (inPlayGroup > 0); streamStates[streamIndex].endedCallback = endedCallback; streamStates[streamIndex].callbackContext = callbackContext; streamStates[streamIndex].allocated = true; if (!audioOutputActive) { audioOutputActive = true; if (!paused && audioOutputEnabled) { AudioOut_startOutput(audioOutCallback, NULL); } } return streamStates[streamIndex].instanceID; } } return AUDIO_PLAYER_INSTANCE_ID_NONE; } void AudioPlayer_stopAudioStream(AudioPlayer_soundInstanceID streamInstanceID) { if (!inited) { return; } for (unsigned int streamIndex = 0; streamIndex < streamCountMax; streamIndex++) { if (streamStates[streamIndex].allocated && streamStates[streamIndex].instanceID == streamInstanceID) { streamStates[streamIndex].canceled = true; break; } } } void AudioPlayer_stopAllAudioStreams(void) { if (!inited) { return; } for (unsigned int streamIndex = 0; streamIndex < streamCountMax; streamIndex++) { streamStates[streamIndex].canceled = true; } } void AudioPlayer_setAudioStreamPaused(AudioPlayer_soundInstanceID streamInstanceID, bool paused) { if (!inited) { return; } for (unsigned int streamIndex = 0; streamIndex < streamCountMax; streamIndex++) { if (streamStates[streamIndex].allocated && streamStates[streamIndex].instanceID == streamInstanceID) { streamStates[streamIndex].paused = paused; break; } } } void AudioPlayer_beginPlayGroup(void) { inPlayGroup++; } void AudioPlayer_endPlayGroup(void) { if (inPlayGroup == 0) { #ifdef DEBUG fprintf(stderr, "Warning: AudioPlayer_endPlayGroup() called without a corresponding call to AudioPlayer_beginPlayGroup()\n"); #endif return; } inPlayGroup--; if (inPlayGroup == 0) { lastPlayGroupID++; } } void AudioPlayer_run(void) { if (!inited) { return; } bool anyAudioPlaying = false; for (unsigned int sourceIndex = 0; sourceIndex < sourceCountMax; sourceIndex++) { if (sourceStates[sourceIndex].allocated) { if (sourceStates[sourceIndex].ended) { if (sourceStates[sourceIndex].endedCallback != NULL) { sourceStates[sourceIndex].endedCallback(sourceStates[sourceIndex].audioData, sourceStates[sourceIndex].callbackContext); } else if (sourceStates[sourceIndex].identifier != NULL && releaseSoundEffectDataCallback != NULL) { releaseSoundEffectDataCallback(sourceStates[sourceIndex].identifier, sourceStates[sourceIndex].audioData, callbackContext); } sourceStates[sourceIndex].audioData = NULL; sourceStates[sourceIndex].allocated = false; } else { anyAudioPlaying = true; } } } for (unsigned int streamIndex = 0; streamIndex < streamCountMax; streamIndex++) { if (streamStates[streamIndex].allocated) { if (streamStates[streamIndex].ended) { if (streamStates[streamIndex].endedCallback != NULL) { streamStates[streamIndex].endedCallback(streamStates[streamIndex].audioStream, streamStates[streamIndex].canceled, streamStates[streamIndex].callbackContext); } streamStates[streamIndex].audioStream = NULL; streamStates[streamIndex].allocated = false; } else { anyAudioPlaying = true; if (!streamStates[streamIndex].lowLatency && streamStates[streamIndex].frameIndex >= streamStates[streamIndex].nextBufferThreshold && !streamStates[streamIndex].bufferEnded) { AudioFrameIndex framesRead = call_virtual(read, streamStates[streamIndex].audioStream, AUDIO_STREAM_BUFFER_CHUNK_SIZE, streamStates[streamIndex].buffer + (streamStates[streamIndex].framesBuffered * AUDIO_STREAM_BUFFER_CHUNK_SIZE) % (AUDIO_STREAM_BUFFER_CHUNK_SIZE * AUDIO_STREAM_BUFFER_CHUNK_COUNT), streamStates[streamIndex].loop); if (framesRead == 0) { streamStates[streamIndex].bufferEnded = true; } else { streamStates[streamIndex].framesBuffered += framesRead; streamStates[streamIndex].nextBufferThreshold += AUDIO_STREAM_BUFFER_CHUNK_FRAME_COUNT; } } } } } if (!anyAudioPlaying && pauseWhenIdle) { audioOutputActive = false; if (!paused) { AudioOut_stopOutput(); } } } bool AudioPlayer_isSoundEffectPlaying(AudioPlayer_soundInstanceID soundInstanceID) { if (!inited) { return false; } for (unsigned int sourceIndex = 0; sourceIndex < sourceCountMax; sourceIndex++) { if (sourceStates[sourceIndex].instanceID == soundInstanceID && sourceStates[sourceIndex].allocated && !sourceStates[sourceIndex].canceled && !sourceStates[sourceIndex].ended) { return true; } } return false; } bool AudioPlayer_isAudioStreamPlaying(AudioPlayer_soundInstanceID streamInstanceID) { if (!inited) { return false; } for (unsigned int streamIndex = 0; streamIndex < streamCountMax; streamIndex++) { if (streamStates[streamIndex].instanceID == streamInstanceID && streamStates[streamIndex].allocated && !streamStates[streamIndex].canceled && !streamStates[streamIndex].ended) { return true; } } return false; } bool AudioPlayer_isAnyAudioPlaying(void) { return audioOutputActive; } bool AudioPlayer_isSoundEffectPaused(AudioPlayer_soundInstanceID soundInstanceID) { if (!inited) { return false; } for (unsigned int sourceIndex = 0; sourceIndex < sourceCountMax; sourceIndex++) { if (sourceStates[sourceIndex].instanceID == soundInstanceID && sourceStates[sourceIndex].allocated && !sourceStates[sourceIndex].canceled && !sourceStates[sourceIndex].ended) { return sourceStates[sourceIndex].paused; } } return false; } bool AudioPlayer_isAudioStreamPaused(AudioPlayer_soundInstanceID streamInstanceID) { if (!inited) { return false; } for (unsigned int streamIndex = 0; streamIndex < streamCountMax; streamIndex++) { if (streamStates[streamIndex].instanceID == streamInstanceID && streamStates[streamIndex].allocated && !streamStates[streamIndex].canceled && !streamStates[streamIndex].ended) { return streamStates[streamIndex].paused; } } return false; } AudioFrameIndex AudioPlayer_getSoundEffectApproximateFramesPlayed(AudioPlayer_soundInstanceID soundInstanceID) { if (!inited) { return 0; } for (unsigned int sourceIndex = 0; sourceIndex < sourceCountMax; sourceIndex++) { if (sourceStates[sourceIndex].instanceID == soundInstanceID && sourceStates[sourceIndex].allocated && !sourceStates[sourceIndex].canceled && !sourceStates[sourceIndex].ended) { return sourceStates[sourceIndex].frameIndex; } } return 0; } AudioFrameIndex AudioPlayer_getAudioStreamApproximateFramesPlayed(AudioPlayer_soundInstanceID streamInstanceID) { if (!inited) { return 0; } for (unsigned int streamIndex = 0; streamIndex < streamCountMax; streamIndex++) { if (streamStates[streamIndex].instanceID == streamInstanceID && streamStates[streamIndex].allocated && !streamStates[streamIndex].canceled && !streamStates[streamIndex].ended) { return streamStates[streamIndex].frameIndex; } } return 0; } void AudioPlayer_pause(void) { if (!inited) { return; } paused = true; if (audioOutputActive) { AudioOut_stopOutput(); } } bool AudioPlayer_isPaused(void) { return paused; } void AudioPlayer_unpause(void) { if (!inited) { return; } paused = false; if (audioOutputActive && audioOutputEnabled) { AudioOut_startOutput(audioOutCallback, NULL); } } void AudioPlayer_setCategoryVolume(AudioPlayer_category category, float volume) { if (!inited) { return; } if (category < categoryCount) { categoryVolumes[category] = volume; } } void AudioPlayer_setPausesAudioDeviceWhenIdle(bool inPauseWhenIdle) { pauseWhenIdle = inPauseWhenIdle; } void AudioPlayer_setSpeakerOutputEnabled(bool outputToSpeakers) { audioOutputEnabled = outputToSpeakers; } void AudioPlayer_setAdvanceStreamsAtAmplitudeZero(bool advanceAtZero) { advanceStreamsAtAmplitudeZero = advanceAtZero; } void AudioPlayer_readSamples(void * outSamples, unsigned int frameCount) { mixAllSources(outSamples, frameCount); }