diff --git a/src/audio.c b/src/audio.c index f159222d5..d20380d0f 100644 --- a/src/audio.c +++ b/src/audio.c @@ -72,7 +72,9 @@ #define SUPPORT_FILEFORMAT_MOD //------------------------------------------------- +#ifndef USE_MINI_AL #define USE_MINI_AL 1 // Set to 1 to use mini_al; 0 to use OpenAL. +#endif #if defined(AUDIO_STANDALONE) #include "audio.h" @@ -214,25 +216,24 @@ void TraceLog(int msgType, const char *text, ...); // Show trace lo typedef struct SoundData SoundData; struct SoundData { - mal_format format; - mal_uint32 channels; - mal_uint32 sampleRate; - mal_uint32 frameCount; - mal_uint32 frameCursorPos; // Keeps track of the next frame to read when mixing + mal_dsp dsp; // Necessary for pitch shift. This is an optimized passthrough when the pitch == 1. float volume; float pitch; bool playing; bool paused; bool looping; + unsigned int frameCursorPos; // Keeps track of the next frame to read when mixing + unsigned int bufferSizeInFrames; SoundData* next; SoundData* prev; - mal_uint8 data[1]; // Raw audio data. + unsigned char data[1]; // Raw audio data. }; // AudioStreamData typedef struct AudioStreamData AudioStreamData; -struct AudioStreamData { - mal_dsp dsp; // AudioStream data needs to flow through a persistent conversion pipeline. Not doing this will result in glitches between buffer updates. +struct AudioStreamData +{ + mal_dsp dsp; // AudioStream data needs to flow through a persistent conversion pipeline. Not doing this will result in glitches between buffer updates. float volume; float pitch; bool playing; @@ -250,7 +251,7 @@ static mal_device device; static mal_bool32 isAudioInitialized = MAL_FALSE; static float masterVolume = 1; static mal_mutex soundLock; -static SoundData* firstSound; // Sounds are tracked in a linked list. +static SoundData* firstSound; // Sounds are tracked in a linked list. static SoundData* lastSound; static AudioStreamData* firstAudioStream; static AudioStreamData* lastAudioStream; @@ -286,6 +287,9 @@ static void RemoveSound(SoundData* internalSound) } else { internalSound->next->prev = internalSound->prev; } + + internalSound->prev = NULL; + internalSound->next = NULL; } mal_mutex_unlock(&soundLock); } @@ -321,6 +325,9 @@ static void RemoveAudioStream(AudioStreamData* internalAudioStream) } else { internalAudioStream->next->prev = internalAudioStream->prev; } + + internalAudioStream->prev = NULL; + internalAudioStream->next = NULL; } mal_mutex_unlock(&soundLock); } @@ -333,6 +340,21 @@ static void OnLog_MAL(mal_context* pContext, mal_device* pDevice, const char* me TraceLog(LOG_ERROR, message); // All log messages from mini_al are errors. } +// This is the main mixing function. Mixing is pretty simple in this project - it's just an accumulation. +// +// framesOut is both an input and an output. It will be initially filled with zeros outside of this function. +static void MixFrames(float* framesOut, const float* framesIn, mal_uint32 frameCount, float localVolume) +{ + for (mal_uint32 iFrame = 0; iFrame < frameCount; ++iFrame) { + for (mal_uint32 iChannel = 0; iChannel < device.channels; ++iChannel) { + float* frameOut = framesOut + (iFrame * device.channels); + float* frameIn = framesIn + (iFrame * device.channels); + + frameOut[iChannel] += frameIn[iChannel] * masterVolume * localVolume; + } + } +} + static mal_uint32 OnSendAudioDataToDevice(mal_device* pDevice, mal_uint32 frameCount, void* pFramesOut) { // This is where all of the mixing takes place. @@ -345,7 +367,7 @@ static mal_uint32 OnSendAudioDataToDevice(mal_device* pDevice, mal_uint32 frameC // want to consider how you might want to avoid this. mal_mutex_lock(&soundLock); { - float* pFramesOutF = (float*)pFramesOut; // <-- Just for convenience. + float* framesOutF = (float*)pFramesOut; // <-- Just for convenience. // Sounds. for (SoundData* internalSound = firstSound; internalSound != NULL; internalSound = internalSound->next) @@ -365,33 +387,48 @@ static mal_uint32 OnSendAudioDataToDevice(mal_device* pDevice, mal_uint32 frameC break; } - // Keep reading until the end of the buffer, or we've already read as much as is allowed. + // Just read as much data we can from the stream. mal_uint32 framesToRead = (frameCount - framesRead); - mal_uint32 framesRemaining = (internalSound->frameCount - internalSound->frameCursorPos); - if (framesToRead > framesRemaining) { - framesToRead = framesRemaining; - } - - // This is where the real mixing takes place. This can be optimized. This assumes the device and sound are of the same format. - // - // TODO: Implement pitching. - for (mal_uint32 iFrame = 0; iFrame < framesToRead; ++iFrame) { - float* pFrameOut = pFramesOutF + ((framesRead+iFrame) * device.channels); - float* pFrameIn = ((float*)internalSound->data) + ((internalSound->frameCursorPos+iFrame) * device.channels); + while (framesToRead > 0) { + float tempBuffer[1024]; // 512 frames for stereo. - for (mal_uint32 iChannel = 0; iChannel < device.channels; ++iChannel) { - pFrameOut[iChannel] += pFrameIn[iChannel] * masterVolume * internalSound->volume; + mal_uint32 framesToReadRightNow = framesToRead; + if (framesToReadRightNow > sizeof(tempBuffer)/DEVICE_CHANNELS) { + framesToReadRightNow = sizeof(tempBuffer)/DEVICE_CHANNELS; } - } - framesRead += framesToRead; - internalSound->frameCursorPos += framesToRead; + // If we're not looping, we need to make sure we flush the internal buffers of the DSP pipeline to ensure we get the + // last few samples. + mal_bool32 flushDSP = !internalSound->looping; - // If we've reached the end of the sound's internal buffer we do one of two things: loop back to the start, or just stop. - if (framesToRead == framesRemaining) { - if (!internalSound->looping) { - break; + mal_uint32 framesJustRead = mal_dsp_read_frames_ex(&internalSound->dsp, framesToReadRightNow, tempBuffer, flushDSP); + if (framesJustRead > 0) { + float* framesOut = framesOutF + (framesRead * device.channels); + float* framesIn = tempBuffer; + MixFrames(framesOut, framesIn, framesJustRead, internalSound->volume); + + framesToRead -= framesJustRead; + framesRead += framesJustRead; } + + // If we weren't able to read all the frames we requested, break. + if (framesJustRead < framesToReadRightNow) { + if (!internalSound->looping) { + internalSound->playing = MAL_FALSE; + internalSound->frameCursorPos = 0; + break; + } else { + // Should never get here, but just for safety, move the cursor position back to the start and continue the loop. + internalSound->frameCursorPos = 0; + continue; + } + } + } + + // If for some reason we weren't able to read every frame we'll need to break from the loop. Not doing this could + // theoretically put us into an infinite loop. + if (framesToRead > 0) { + break; } } } @@ -427,17 +464,9 @@ static mal_uint32 OnSendAudioDataToDevice(mal_device* pDevice, mal_uint32 frameC mal_uint32 framesJustRead = mal_dsp_read_frames(&internalData->dsp, framesToReadRightNow, tempBuffer); if (framesJustRead > 0) { - // This is where the real mixing takes place. This can be optimized. This assumes the device and sound are of the same format. - // - // TODO: Implement pitching. - for (mal_uint32 iFrame = 0; iFrame < framesToRead; ++iFrame) { - float* pFrameOut = pFramesOutF + ((framesRead+iFrame) * device.channels); - float* pFrameIn = tempBuffer + (iFrame * device.channels); - - for (mal_uint32 iChannel = 0; iChannel < device.channels; ++iChannel) { - pFrameOut[iChannel] += pFrameIn[iChannel] * masterVolume * internalData->volume; - } - } + float* framesOut = framesOutF + (framesRead * device.channels); + float* framesIn = tempBuffer; + MixFrames(framesOut, framesIn, framesJustRead, internalData->volume); framesToRead -= framesJustRead; framesRead += framesJustRead; @@ -667,6 +696,39 @@ Sound LoadSound(const char *fileName) return sound; } +#if USE_MINI_AL +static mal_uint32 Sound_OnDSPRead(mal_dsp* pDSP, mal_uint32 frameCount, void* pFramesOut, void* pUserData) +{ + SoundData* internalData = (SoundData*)pUserData; + + mal_uint32 frameSizeInBytes = mal_get_sample_size_in_bytes(internalData->dsp.config.formatIn)*internalData->dsp.config.channelsIn; + + // Just keep reading as much as we can. Do not zero fill excess data in the output buffer. + mal_uint32 framesRead = 0; + while (framesRead < frameCount) + { + mal_uint32 framesRemaining = internalData->bufferSizeInFrames - internalData->frameCursorPos; + mal_uint32 framesToRead = (frameCount - framesRead); + if (framesToRead > framesRemaining) { + framesToRead = framesRemaining; + } + + memcpy((unsigned char*)pFramesOut + (framesRead*frameSizeInBytes), internalData->data + (internalData->frameCursorPos*frameSizeInBytes), framesToRead*frameSizeInBytes); + internalData->frameCursorPos += framesToRead; + framesRead += framesToRead; + + // If we've reached the end of the buffer but we're not looping, return. + if (framesToRead == framesRemaining) { + if (!internalData->looping) { + break; + } + } + } + + return framesRead; +} +#endif + // Load sound from wave data // NOTE: Wave data must be unallocated manually Sound LoadSoundFromWave(Wave wave) @@ -702,16 +764,28 @@ Sound LoadSoundFromWave(Wave wave) TraceLog(LOG_ERROR, "LoadSoundFromWave() : Format conversion failed."); } - internalSound->format = DEVICE_FORMAT; - internalSound->channels = DEVICE_CHANNELS; - internalSound->sampleRate = DEVICE_SAMPLE_RATE; - internalSound->frameCount = frameCount; - internalSound->frameCursorPos = 0; + // We run audio data through a sample rate converter in order to support pitch shift. By default this will use an optimized passthrough + // algorithm, but when the application changes the pitch it will change to a less optimal linear SRC. + mal_dsp_config dspConfig; + memset(&dspConfig, 0, sizeof(dspConfig)); + dspConfig.formatIn = DEVICE_FORMAT; + dspConfig.formatOut = DEVICE_FORMAT; + dspConfig.channelsIn = DEVICE_CHANNELS; + dspConfig.channelsOut = DEVICE_CHANNELS; + dspConfig.sampleRateIn = DEVICE_SAMPLE_RATE; + dspConfig.sampleRateOut = DEVICE_SAMPLE_RATE; + mal_result resultMAL = mal_dsp_init(&dspConfig, Sound_OnDSPRead, internalSound, &internalSound->dsp); + if (resultMAL != MAL_SUCCESS) { + TraceLog(LOG_ERROR, "LoadSoundFromWave() : Failed to create data conversion pipeline"); + } + internalSound->volume = 1; internalSound->pitch = 1; internalSound->playing = 0; internalSound->paused = 0; internalSound->looping = 0; + internalSound->bufferSizeInFrames = frameCount; + internalSound->frameCursorPos = 0; AppendSound(internalSound); sound.handle = (void*)internalSound; @@ -816,9 +890,8 @@ void UpdateSound(Sound sound, const void *data, int samplesCount) internalSound->paused = false; internalSound->frameCursorPos = 0; - // TODO: May want to lock/unlock this since this data buffer is read at mixing time. However, this puts a mutex in - // in the mixing code which makes it no longer real-time. This is likely not a critical issue for this project, though. - memcpy(internalSound->data, data, samplesCount*internalSound->channels*mal_get_sample_size_in_bytes(internalSound->format)); + // TODO: May want to lock/unlock this since this data buffer is read at mixing time. + memcpy(internalSound->data, data, samplesCount*internalSound->dsp.config.channelsIn*mal_get_sample_size_in_bytes(internalSound->dsp.config.formatIn)); #else ALint sampleRate, sampleSize, channels; alGetBufferi(sound.buffer, AL_FREQUENCY, &sampleRate); @@ -986,6 +1059,11 @@ void SetSoundPitch(Sound sound, float pitch) } internalSound->pitch = pitch; + + // Pitching is just an adjustment of the sample rate. Note that this changes the duration of the sound - higher pitches + // will make the sound faster; lower pitches make it slower. + mal_uint32 newOutputSampleRate = (mal_uint32)((((float)internalSound->dsp.config.sampleRateOut / (float)internalSound->dsp.config.sampleRateIn) / pitch) * internalSound->dsp.config.sampleRateIn); + mal_dsp_set_output_sample_rate(&internalSound->dsp, newOutputSampleRate); #else alSourcef(sound.source, AL_PITCH, pitch); #endif @@ -2013,7 +2091,18 @@ void SetAudioStreamPitch(AudioStream stream, float pitch) return; } + if (pitch == 0) + { + TraceLog(LOG_ERROR, "Attempting to set pitch to 0"); + return; + } + internalData->pitch = pitch; + + // Pitching is just an adjustment of the sample rate. Note that this changes the duration of the sound - higher pitches + // will make the sound faster; lower pitches make it slower. + mal_uint32 newOutputSampleRate = (mal_uint32)((((float)internalData->dsp.config.sampleRateOut / (float)internalData->dsp.config.sampleRateIn) / pitch) * internalData->dsp.config.sampleRateIn); + mal_dsp_set_output_sample_rate(&internalData->dsp, newOutputSampleRate); #else alSourcef(stream.source, AL_PITCH, pitch); #endif diff --git a/src/external/mini_al.h b/src/external/mini_al.h index 58f542cf7..fad9952f4 100644 --- a/src/external/mini_al.h +++ b/src/external/mini_al.h @@ -570,7 +570,6 @@ struct mal_src mal_src_config config; mal_src_read_proc onRead; void* pUserData; - float ratio; float bin[256]; mal_src_cache cache; // <-- For simplifying and optimizing client -> memory reading. @@ -1353,6 +1352,12 @@ static inline mal_device_config mal_device_config_init_playback(mal_format forma // Initializes a sample rate conversion object. mal_result mal_src_init(mal_src_config* pConfig, mal_src_read_proc onRead, void* pUserData, mal_src* pSRC); +// Dynamically adjusts the output sample rate. +// +// This is useful for dynamically adjust pitch. Keep in mind, however, that this will speed up or slow down the sound. If this +// is not acceptable you will need to use your own algorithm. +mal_result mal_src_set_output_sample_rate(mal_src* pSRC, mal_uint32 sampleRateOut); + // Reads a number of frames. // // Returns the number of frames actually read. @@ -1376,6 +1381,12 @@ mal_uint32 mal_src_read_frames_ex(mal_src* pSRC, mal_uint32 frameCount, void* pF // Initializes a DSP object. mal_result mal_dsp_init(mal_dsp_config* pConfig, mal_dsp_read_proc onRead, void* pUserData, mal_dsp* pDSP); +// Dynamically adjusts the output sample rate. +// +// This is useful for dynamically adjust pitch. Keep in mind, however, that this will speed up or slow down the sound. If this +// is not acceptable you will need to use your own algorithm. +mal_result mal_dsp_set_output_sample_rate(mal_dsp* pDSP, mal_uint32 sampleRateOut); + // Reads a number of frames and runs them through the DSP processor. // // This this _not_ flush the internal buffers which means you may end up with a few less frames than you may expect. Look at @@ -9313,21 +9324,27 @@ mal_result mal_src_init(mal_src_config* pConfig, mal_src_read_proc onRead, void* pSRC->onRead = onRead; pSRC->pUserData = pUserData; - // If the in and out sample rates are the same, fall back to the passthrough algorithm. - if (pSRC->config.sampleRateIn == pSRC->config.sampleRateOut) { - pSRC->config.algorithm = mal_src_algorithm_none; - } - if (pSRC->config.cacheSizeInFrames > MAL_SRC_CACHE_SIZE_IN_FRAMES || pSRC->config.cacheSizeInFrames == 0) { pSRC->config.cacheSizeInFrames = MAL_SRC_CACHE_SIZE_IN_FRAMES; } - pSRC->ratio = (float)pSRC->config.sampleRateIn / pSRC->config.sampleRateOut; - mal_src_cache_init(pSRC, &pSRC->cache); return MAL_SUCCESS; } +mal_result mal_src_set_output_sample_rate(mal_src* pSRC, mal_uint32 sampleRateOut) +{ + if (pSRC == NULL) return MAL_INVALID_ARGS; + + // Must have a sample rate of > 0. + if (sampleRateOut == 0) { + return MAL_INVALID_ARGS; + } + + pSRC->config.sampleRateOut = sampleRateOut; + return MAL_SUCCESS; +} + mal_uint32 mal_src_read_frames(mal_src* pSRC, mal_uint32 frameCount, void* pFramesOut) { return mal_src_read_frames_ex(pSRC, frameCount, pFramesOut, MAL_FALSE); @@ -9337,6 +9354,13 @@ mal_uint32 mal_src_read_frames_ex(mal_src* pSRC, mal_uint32 frameCount, void* pF { if (pSRC == NULL || frameCount == 0 || pFramesOut == NULL) return 0; + mal_src_algorithm algorithm = pSRC->config.algorithm; + + // Always use passthrough if the sample rates are the same. + if (pSRC->config.sampleRateIn == pSRC->config.sampleRateOut) { + algorithm = mal_src_algorithm_none; + } + // Could just use a function pointer instead of a switch for this... switch (pSRC->config.algorithm) { @@ -9408,7 +9432,7 @@ mal_uint32 mal_src_read_frames_linear(mal_src* pSRC, mal_uint32 frameCount, void pSRC->linear.isNextFramesLoaded = MAL_TRUE; } - float factor = pSRC->ratio; + float factor = (float)pSRC->config.sampleRateIn / pSRC->config.sampleRateOut; mal_uint32 totalFramesRead = 0; while (frameCount > 0) { @@ -9995,6 +10019,57 @@ mal_result mal_dsp_init(mal_dsp_config* pConfig, mal_dsp_read_proc onRead, void* return MAL_SUCCESS; } +mal_result mal_dsp_set_output_sample_rate(mal_dsp* pDSP, mal_uint32 sampleRateOut) +{ + if (pDSP == NULL) return MAL_INVALID_ARGS; + + // Must have a sample rate of > 0. + if (sampleRateOut == 0) { + return MAL_INVALID_ARGS; + } + + pDSP->config.sampleRateOut = sampleRateOut; + + // If we already have an SRC pipeline initialized we do _not_ want to re-create it. Instead we adjust it. If we didn't previously + // have an SRC pipeline in place we'll need to initialize it. + if (pDSP->isSRCRequired) { + if (pDSP->config.sampleRateIn != pDSP->config.sampleRateOut) { + mal_src_set_output_sample_rate(&pDSP->src, sampleRateOut); + } else { + pDSP->isSRCRequired = MAL_FALSE; + } + } else { + // We may need a new SRC pipeline. + if (pDSP->config.sampleRateIn != pDSP->config.sampleRateOut) { + pDSP->isSRCRequired = MAL_TRUE; + + mal_src_config srcConfig; + srcConfig.sampleRateIn = pDSP->config.sampleRateIn; + srcConfig.sampleRateOut = pDSP->config.sampleRateOut; + srcConfig.formatIn = pDSP->config.formatIn; + srcConfig.formatOut = mal_format_f32; + srcConfig.channels = pDSP->config.channelsIn; + srcConfig.algorithm = mal_src_algorithm_linear; + srcConfig.cacheSizeInFrames = pDSP->config.cacheSizeInFrames; + mal_result result = mal_src_init(&srcConfig, mal_dsp__src_on_read, pDSP, &pDSP->src); + if (result != MAL_SUCCESS) { + return result; + } + } else { + pDSP->isSRCRequired = MAL_FALSE; + } + } + + // Update whether or not the pipeline is a passthrough. + if (pDSP->config.formatIn == pDSP->config.formatOut && pDSP->config.channelsIn == pDSP->config.channelsOut && pDSP->config.sampleRateIn == pDSP->config.sampleRateOut && !pDSP->isChannelMappingRequired) { + pDSP->isPassthrough = MAL_TRUE; + } else { + pDSP->isPassthrough = MAL_FALSE; + } + + return MAL_SUCCESS; +} + mal_uint32 mal_dsp_read_frames(mal_dsp* pDSP, mal_uint32 frameCount, void* pFramesOut) { return mal_dsp_read_frames_ex(pDSP, frameCount, pFramesOut, MAL_FALSE);