[raudio] Remove usage of ma_data_converter_get_required_input_frame_count() (#5568)

* Audio: Remove use of ma_data_converter_get_required_input_frame_count().

This function is being removed from miniaudio. To make this work with
the current architecture of raylib it requires the use of a cache.

This commit implements a generic solution that works across all
AudioBuffer types (static, streams and callback based), but the static
case could be optimized to avoid the cache by incorporating the
functionality of ReadAudioBufferFramesInInternalFormat() into
ReadAudioBufferFramesInMixingFormat(). It would be unpractical to avoid
the cache with streams and callback-based AudioBuffers however so this
commit sticks with a generic solution.

* Audio: Correct usage of miniaudio's dynamic rate adjustment.

This affects pitch shifting. The output rate is being modified with
ma_data_converter_set_rate(), but then that value is being used in the
computation of the output rate the next time SetAudioBufferPitch() which
results in a cascade. The correct way to do this is to use an anchored
output rate as the basis for the calculation after pitch shifting. In
this case, it's the device's sample rate that acts as the anchor.

* Audio: Optimize memory usage for data conversion.

This reduces the per-AudioBuffer conversion cache from 256 PCM frames
down to 8.
This commit is contained in:
David Reid
2026-02-20 22:46:41 +10:00
committed by GitHub
parent ce617cd814
commit 0aacd330d4

View File

@ -295,6 +295,10 @@ typedef struct tagBITMAPINFOHEADER {
#define MAX_AUDIO_BUFFER_POOL_CHANNELS 16 // Audio pool channels #define MAX_AUDIO_BUFFER_POOL_CHANNELS 16 // Audio pool channels
#endif #endif
#ifndef AUDIO_BUFFER_RESIDUAL_CAPACITY
#define AUDIO_BUFFER_RESIDUAL_CAPACITY 8 // In PCM frames. For resampling and pitch shifting.
#endif
//---------------------------------------------------------------------------------- //----------------------------------------------------------------------------------
// Types and Structures Definition // Types and Structures Definition
//---------------------------------------------------------------------------------- //----------------------------------------------------------------------------------
@ -337,6 +341,8 @@ typedef enum {
// Audio buffer struct // Audio buffer struct
struct rAudioBuffer { struct rAudioBuffer {
ma_data_converter converter; // Audio data converter ma_data_converter converter; // Audio data converter
unsigned char* converterResidual; // Cached residual input frames for use by the converter
unsigned int converterResidualCount; // The number of valid frames sitting in converterResidual
AudioCallback callback; // Audio buffer callback for buffer filling on audio threads AudioCallback callback; // Audio buffer callback for buffer filling on audio threads
rAudioProcessor *processor; // Audio processor rAudioProcessor *processor; // Audio processor
@ -586,6 +592,15 @@ AudioBuffer *LoadAudioBuffer(ma_format format, ma_uint32 channels, ma_uint32 sam
return NULL; return NULL;
} }
// A cache for use by the converter is necessary when resampling because
// when generating output frames a different number of input frames will
// be consumed. Any residual input frames need to be kept track of to
// ensure there are no discontinuities. Since raylib supports pitch
// shifting, which is done through resampling, a cache will always be
// required. This will be kept relatively small to avoid too much wastage.
audioBuffer->converterResidualCount = 0;
audioBuffer->converterResidual = (unsigned char*)RL_CALLOC(AUDIO_BUFFER_RESIDUAL_CAPACITY*ma_get_bytes_per_frame(format, channels), 1);
// Init audio buffer values // Init audio buffer values
audioBuffer->volume = 1.0f; audioBuffer->volume = 1.0f;
audioBuffer->pitch = 1.0f; audioBuffer->pitch = 1.0f;
@ -621,6 +636,7 @@ void UnloadAudioBuffer(AudioBuffer *buffer)
{ {
UntrackAudioBuffer(buffer); UntrackAudioBuffer(buffer);
ma_data_converter_uninit(&buffer->converter, NULL); ma_data_converter_uninit(&buffer->converter, NULL);
RL_FREE(buffer->converterResidual);
RL_FREE(buffer->data); RL_FREE(buffer->data);
RL_FREE(buffer); RL_FREE(buffer);
} }
@ -705,7 +721,7 @@ void SetAudioBufferPitch(AudioBuffer *buffer, float pitch)
// Note that this changes the duration of the sound: // Note that this changes the duration of the sound:
// - higher pitches will make the sound faster // - higher pitches will make the sound faster
// - lower pitches make it slower // - lower pitches make it slower
ma_uint32 outputSampleRate = (ma_uint32)((float)buffer->converter.sampleRateOut/pitch); ma_uint32 outputSampleRate = (ma_uint32)((float)AUDIO.System.device.sampleRate/pitch);
ma_data_converter_set_rate(&buffer->converter, buffer->converter.sampleRateIn, outputSampleRate); ma_data_converter_set_rate(&buffer->converter, buffer->converter.sampleRateIn, outputSampleRate);
buffer->pitch = pitch; buffer->pitch = pitch;
@ -2456,38 +2472,78 @@ static ma_uint32 ReadAudioBufferFramesInMixingFormat(AudioBuffer *audioBuffer, f
// NOTE: Continuously converting data from the AudioBuffer's internal format to the mixing format, // NOTE: Continuously converting data from the AudioBuffer's internal format to the mixing format,
// which should be defined by the output format of the data converter. // which should be defined by the output format of the data converter.
// This is done until frameCount frames have been output. // This is done until frameCount frames have been output.
// The important detail to remember is that more data than required should neeveer be read, ma_uint32 bpf = ma_get_bytes_per_frame(audioBuffer->converter.formatIn, audioBuffer->converter.channelsIn);
// for the specified number of output frames.
// This can be achieved with ma_data_converter_get_required_input_frame_count()
ma_uint8 inputBuffer[4096] = { 0 }; ma_uint8 inputBuffer[4096] = { 0 };
ma_uint32 inputBufferFrameCap = sizeof(inputBuffer)/ma_get_bytes_per_frame(audioBuffer->converter.formatIn, audioBuffer->converter.channelsIn); ma_uint32 inputBufferFrameCap = sizeof(inputBuffer)/bpf;
ma_uint32 totalOutputFramesProcessed = 0; ma_uint32 totalOutputFramesProcessed = 0;
while (totalOutputFramesProcessed < frameCount) while (totalOutputFramesProcessed < frameCount)
{ {
float *runningFramesOut = framesOut + (totalOutputFramesProcessed*audioBuffer->converter.channelsOut);
ma_uint64 outputFramesToProcessThisIteration = frameCount - totalOutputFramesProcessed; ma_uint64 outputFramesToProcessThisIteration = frameCount - totalOutputFramesProcessed;
ma_uint64 inputFramesToProcessThisIteration = 0; ma_uint64 inputFramesToProcessThisIteration = 0;
(void)ma_data_converter_get_required_input_frame_count(&audioBuffer->converter, outputFramesToProcessThisIteration, &inputFramesToProcessThisIteration); // Process any residual input frames from the previous read first.
if (inputFramesToProcessThisIteration > inputBufferFrameCap) if (audioBuffer->converterResidualCount > 0)
{ {
inputFramesToProcessThisIteration = inputBufferFrameCap; ma_uint64 inputFramesProcessedThisIteration = audioBuffer->converterResidualCount;
ma_uint64 outputFramesProcessedThisIteration = outputFramesToProcessThisIteration;
ma_data_converter_process_pcm_frames(&audioBuffer->converter, audioBuffer->converterResidual, &inputFramesProcessedThisIteration, runningFramesOut, &outputFramesProcessedThisIteration);
// Make sure the data in the cache is consumed. This can be optimized to use a cursor instead of a memmove().
memmove(audioBuffer->converterResidual, audioBuffer->converterResidual + inputFramesProcessedThisIteration*bpf, (size_t)(AUDIO_BUFFER_RESIDUAL_CAPACITY - inputFramesProcessedThisIteration) * bpf);
audioBuffer->converterResidualCount -= (ma_uint32)inputFramesProcessedThisIteration; // Safe cast
totalOutputFramesProcessed += (ma_uint32)outputFramesProcessedThisIteration; // Safe cast
}
else
{
// Getting here means there are no residual frames from the previous read. Fresh data can now be
// pulled from the AudioBuffer and processed.
//
// A best guess needs to be used made to determine how many input frames to pull from the
// buffer. There are three possible outcomes: 1) exact; 2) underestimated; 3) overestimated.
//
// When the guess is exactly correct or underestimated there is nothing special to handle - it'll be
// handled naturally by the loop.
//
// When the guess is overestimated, that's when it gets more complicated. In this case, any overflow
// needs to be stored in a buffer for later processing by the next read.
ma_uint32 estimatedInputFrameCount = (ma_uint32)(((float)audioBuffer->converter.resampler.sampleRateIn / audioBuffer->converter.resampler.sampleRateOut) * outputFramesToProcessThisIteration);
if (estimatedInputFrameCount == 0)
{
estimatedInputFrameCount = 1; // Make sure at least one input frame is read.
} }
float *runningFramesOut = framesOut + (totalOutputFramesProcessed*audioBuffer->converter.channelsOut); if (estimatedInputFrameCount > inputBufferFrameCap)
{
estimatedInputFrameCount = inputBufferFrameCap;
}
// At this point we can convert the data to our mixing format estimatedInputFrameCount = ReadAudioBufferFramesInInternalFormat(audioBuffer, inputBuffer, estimatedInputFrameCount);
ma_uint64 inputFramesProcessedThisIteration = ReadAudioBufferFramesInInternalFormat(audioBuffer, inputBuffer, (ma_uint32)inputFramesToProcessThisIteration);
ma_uint64 inputFramesProcessedThisIteration = estimatedInputFrameCount;
ma_uint64 outputFramesProcessedThisIteration = outputFramesToProcessThisIteration; ma_uint64 outputFramesProcessedThisIteration = outputFramesToProcessThisIteration;
ma_data_converter_process_pcm_frames(&audioBuffer->converter, inputBuffer, &inputFramesProcessedThisIteration, runningFramesOut, &outputFramesProcessedThisIteration); ma_data_converter_process_pcm_frames(&audioBuffer->converter, inputBuffer, &inputFramesProcessedThisIteration, runningFramesOut, &outputFramesProcessedThisIteration);
totalOutputFramesProcessed += (ma_uint32)outputFramesProcessedThisIteration; // Safe cast if (estimatedInputFrameCount > inputFramesProcessedThisIteration)
{
// Getting here means the estimated input frame count was overestimated. The residual needs
// be stored for later use.
ma_uint64 residualFrameCount = estimatedInputFrameCount - inputFramesProcessedThisIteration;
if (inputFramesProcessedThisIteration < inputFramesToProcessThisIteration) break; // Ran out of input data // A safety check to make sure the capacity of the residual cache is not exceeded.
if (residualFrameCount > AUDIO_BUFFER_RESIDUAL_CAPACITY)
{
residualFrameCount = AUDIO_BUFFER_RESIDUAL_CAPACITY;
}
// This should never be hit, but added here for safety memcpy(audioBuffer->converterResidual, inputBuffer + inputFramesProcessedThisIteration*bpf, (size_t)(residualFrameCount * bpf));
// Ensures we get out of the loop when no input nor output frames are processed audioBuffer->converterResidualCount = residualFrameCount;
if ((inputFramesProcessedThisIteration == 0) && (outputFramesProcessedThisIteration == 0)) break; }
totalOutputFramesProcessed += (ma_uint32)outputFramesProcessedThisIteration;
}
} }
return totalOutputFramesProcessed; return totalOutputFramesProcessed;