From babd7fb00f74d174b70ec20095c486590420a2ed Mon Sep 17 00:00:00 2001 From: David Reid Date: Sun, 25 Feb 2024 09:50:42 +1000 Subject: [PATCH 01/12] Forward declare ma_semaphore API. --- miniaudio.h | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/miniaudio.h b/miniaudio.h index 9ed870e3..0e4cf0e1 100644 --- a/miniaudio.h +++ b/miniaudio.h @@ -6182,6 +6182,12 @@ MA_API ma_result ma_event_wait(ma_event* pEvent); Signals the specified auto-reset event. */ MA_API ma_result ma_event_signal(ma_event* pEvent); + + +MA_API ma_result ma_semaphore_init(int initialValue, ma_semaphore* pSemaphore); +MA_API void ma_semaphore_uninit(ma_semaphore* pSemaphore); +MA_API ma_result ma_semaphore_wait(ma_semaphore* pSemaphore); +MA_API ma_result ma_semaphore_release(ma_semaphore* pSemaphore); #endif /* MA_NO_THREADING */ From e32cc9ff83fe90593158b23aebf6adf55dd629df Mon Sep 17 00:00:00 2001 From: David Reid Date: Tue, 27 Feb 2024 08:27:05 +1000 Subject: [PATCH 02/12] Update dr_wav and dr_mp3. --- miniaudio.h | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/miniaudio.h b/miniaudio.h index 0e4cf0e1..c0f89242 100644 --- a/miniaudio.h +++ b/miniaudio.h @@ -59830,7 +59830,7 @@ extern "C" { #define MA_DR_WAV_XSTRINGIFY(x) MA_DR_WAV_STRINGIFY(x) #define MA_DR_WAV_VERSION_MAJOR 0 #define MA_DR_WAV_VERSION_MINOR 13 -#define MA_DR_WAV_VERSION_REVISION 14 +#define MA_DR_WAV_VERSION_REVISION 16 #define MA_DR_WAV_VERSION_STRING MA_DR_WAV_XSTRINGIFY(MA_DR_WAV_VERSION_MAJOR) "." MA_DR_WAV_XSTRINGIFY(MA_DR_WAV_VERSION_MINOR) "." MA_DR_WAV_XSTRINGIFY(MA_DR_WAV_VERSION_REVISION) #include #define MA_DR_WAVE_FORMAT_PCM 0x1 @@ -60537,7 +60537,7 @@ extern "C" { #define MA_DR_MP3_XSTRINGIFY(x) MA_DR_MP3_STRINGIFY(x) #define MA_DR_MP3_VERSION_MAJOR 0 #define MA_DR_MP3_VERSION_MINOR 6 -#define MA_DR_MP3_VERSION_REVISION 38 +#define MA_DR_MP3_VERSION_REVISION 39 #define MA_DR_MP3_VERSION_STRING MA_DR_MP3_XSTRINGIFY(MA_DR_MP3_VERSION_MAJOR) "." MA_DR_MP3_XSTRINGIFY(MA_DR_MP3_VERSION_MINOR) "." MA_DR_MP3_XSTRINGIFY(MA_DR_MP3_VERSION_REVISION) #include #define MA_DR_MP3_MAX_PCM_FRAMES_PER_MP3_FRAME 1152 @@ -81716,7 +81716,7 @@ MA_API void ma_dr_wav_f32_to_s32(ma_int32* pOut, const float* pIn, size_t sample return; } for (i = 0; i < sampleCount; ++i) { - *pOut++ = (ma_int32)(2147483648.0 * pIn[i]); + *pOut++ = (ma_int32)(2147483648.0f * pIn[i]); } } MA_API void ma_dr_wav_f64_to_s32(ma_int32* pOut, const double* pIn, size_t sampleCount) @@ -91204,8 +91204,8 @@ static ma_int16 ma_dr_mp3d_scale_pcm(float sample) s32 -= (s32 < 0); s = (ma_int16)ma_dr_mp3_clip_int16_arm(s32); #else - if (sample >= 32766.5) return (ma_int16) 32767; - if (sample <= -32767.5) return (ma_int16)-32768; + if (sample >= 32766.5f) return (ma_int16) 32767; + if (sample <= -32767.5f) return (ma_int16)-32768; s = (ma_int16)(sample + .5f); s -= (s < 0); #endif @@ -91591,9 +91591,9 @@ MA_API void ma_dr_mp3dec_f32_to_s16(const float *in, ma_int16 *out, size_t num_s for(; i < num_samples; i++) { float sample = in[i] * 32768.0f; - if (sample >= 32766.5) + if (sample >= 32766.5f) out[i] = (ma_int16) 32767; - else if (sample <= -32767.5) + else if (sample <= -32767.5f) out[i] = (ma_int16)-32768; else { From 7a8ebd7f4d3a1bb08e056fba3ddb7e857485ccec Mon Sep 17 00:00:00 2001 From: David Reid Date: Tue, 27 Feb 2024 15:31:17 +1000 Subject: [PATCH 03/12] Update to node processing. Previously, processing a node would involve a temporary buffer allocated on the stack. Because this was fixed size, it would result in processing being sub-divided into chunks in order to avoid overflowing that buffer. This becomes an issue when a node needs to have a known processing size. An example might be some kind of effect that requires processing be in powers of two. With this commit, the `processingSizeInFrames` variable in `ma_node_graph_config` can be used to make it so processing always happens in fixed sized chunks. In this situations, it's recommended you always call `ma_node_graph_read_pcm_frames()` with a frame count of a multiple of `processingSizeInFrames`. The allocation strategy used here is not optimal and will be improved in future commits. It currently allocates a buffer per-node, but since the data contained within it is transient in nature, it should be possible to use a global fixed sized stack that supports allocating a variable amount of space within the stack buffer. --- miniaudio.h | 191 ++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 156 insertions(+), 35 deletions(-) diff --git a/miniaudio.h b/miniaudio.h index c0f89242..5db2f74f 100644 --- a/miniaudio.h +++ b/miniaudio.h @@ -10628,6 +10628,7 @@ typedef struct { const ma_node_vtable* vtable; /* Should never be null. Initialization of the node will fail if so. */ ma_node_state initialState; /* Defaults to ma_node_state_started. */ + ma_uint32 processingSizeInFrames; /* The preferred processing size in frames per instantiation of the processing callback. You should leave this set to 0 unless your node needs to process in chunks of a specific size. */ ma_uint32 inputBusCount; /* Only used if the vtable specifies an input bus count of `MA_NODE_BUS_COUNT_UNKNOWN`, otherwise must be set to `MA_NODE_BUS_COUNT_UNKNOWN` (default). */ ma_uint32 outputBusCount; /* Only used if the vtable specifies an output bus count of `MA_NODE_BUS_COUNT_UNKNOWN`, otherwise be set to `MA_NODE_BUS_COUNT_UNKNOWN` (default). */ const ma_uint32* pInputChannels; /* The number of elements are determined by the input bus count as determined by the vtable, or `inputBusCount` if the vtable specifies `MA_NODE_BUS_COUNT_UNKNOWN`. */ @@ -10684,6 +10685,9 @@ struct ma_node_base /* These variables are set once at startup. */ ma_node_graph* pNodeGraph; /* The graph this node belongs to. */ const ma_node_vtable* vtable; + float* pPreMixBuffer; /* When data is read from an input bus, it's read into this buffer which will then be mixed into another buffer. Must be allocated on the heap in order to support processingSizeInFrames. If allocated on the heap, it would need to be broken down into smaller parts. */ + ma_uint32 preMixBufferCapInFrames; /* The capacity of the pre-mix buffer. */ + ma_uint32 processingSizeInFrames; float* pCachedData; /* Allocated on the heap. Fixed size. Needs to be stored on the heap because reading from output buses is done in separate function calls. */ ma_uint16 cachedDataCapInFramesPerBus; /* The capacity of the input data cache in frames, per bus. */ @@ -10735,7 +10739,7 @@ MA_API ma_result ma_node_set_time(ma_node* pNode, ma_uint64 localTime); typedef struct { ma_uint32 channels; - ma_uint16 nodeCacheCapInFrames; + ma_uint32 processingSizeInFrames; /* This is the preferred processing size for node processing callbacks unless overridden by a node itself. Can be 0 in which case it will be based on the frame count passed into ma_node_graph_read_pcm_frames(), but will not be well defined. */ } ma_node_graph_config; MA_API ma_node_graph_config ma_node_graph_config_init(ma_uint32 channels); @@ -10746,7 +10750,9 @@ struct ma_node_graph /* Immutable. */ ma_node_base base; /* The node graph itself is a node so it can be connected as an input to different node graph. This has zero inputs and calls ma_node_graph_read_pcm_frames() to generate it's output. */ ma_node_base endpoint; /* Special node that all nodes eventually connect to. Data is read from this node in ma_node_graph_read_pcm_frames(). */ - ma_uint16 nodeCacheCapInFrames; + float* pProcessingCache; /* This will be allocated when processingSizeInFrames is non-zero. This is needed because ma_node_graph_read_pcm_frames() can be called with a variable number of frames, and we may need to do some buffering in situations where the caller requests a frame count that's not a multiple of processingSizeInFrames. */ + ma_uint32 processingCacheFramesRemaining; + ma_uint32 processingSizeInFrames; /* Read and written by multiple threads. */ MA_ATOMIC(4, ma_bool32) isReading; @@ -71152,7 +71158,6 @@ static ma_result ma_job_process__resource_manager__seek_data_stream(ma_job* pJob #define MA_DEFAULT_NODE_CACHE_CAP_IN_FRAMES_PER_BUS 480 #endif - static ma_result ma_node_read_pcm_frames(ma_node* pNode, ma_uint32 outputBusIndex, float* pFramesOut, ma_uint32 frameCount, ma_uint32* pFramesRead, ma_uint64 globalTime); MA_API void ma_debug_fill_pcm_frames_with_sine_wave(float* pFramesOut, ma_uint32 frameCount, ma_format format, ma_uint32 channels, ma_uint32 sampleRate) @@ -71191,8 +71196,8 @@ MA_API ma_node_graph_config ma_node_graph_config_init(ma_uint32 channels) ma_node_graph_config config; MA_ZERO_OBJECT(&config); - config.channels = channels; - config.nodeCacheCapInFrames = MA_DEFAULT_NODE_CACHE_CAP_IN_FRAMES_PER_BUS; + config.channels = channels; + config.processingSizeInFrames = 0; return config; } @@ -71279,11 +71284,7 @@ MA_API ma_result ma_node_graph_init(const ma_node_graph_config* pConfig, const m } MA_ZERO_OBJECT(pNodeGraph); - pNodeGraph->nodeCacheCapInFrames = pConfig->nodeCacheCapInFrames; - if (pNodeGraph->nodeCacheCapInFrames == 0) { - pNodeGraph->nodeCacheCapInFrames = MA_DEFAULT_NODE_CACHE_CAP_IN_FRAMES_PER_BUS; - } - + pNodeGraph->processingSizeInFrames = pConfig->processingSizeInFrames; /* Base node so we can use the node graph as a node into another graph. */ baseConfig = ma_node_config_init(); @@ -71308,6 +71309,18 @@ MA_API ma_result ma_node_graph_init(const ma_node_graph_config* pConfig, const m return result; } + + /* Processing cache. */ + if (pConfig->processingSizeInFrames > 0) { + pNodeGraph->pProcessingCache = (float*)ma_malloc(pConfig->processingSizeInFrames * pConfig->channels * sizeof(float), pAllocationCallbacks); + if (pNodeGraph->pProcessingCache == NULL) { + ma_node_uninit(&pNodeGraph->endpoint, pAllocationCallbacks); + ma_node_uninit(&pNodeGraph->base, pAllocationCallbacks); + return MA_OUT_OF_MEMORY; + } + } + + return MA_SUCCESS; } @@ -71318,6 +71331,12 @@ MA_API void ma_node_graph_uninit(ma_node_graph* pNodeGraph, const ma_allocation_ } ma_node_uninit(&pNodeGraph->endpoint, pAllocationCallbacks); + ma_node_uninit(&pNodeGraph->base, pAllocationCallbacks); + + if (pNodeGraph->pProcessingCache != NULL) { + ma_free(pNodeGraph->pProcessingCache, pAllocationCallbacks); + pNodeGraph->pProcessingCache = NULL; + } } MA_API ma_node* ma_node_graph_get_endpoint(ma_node_graph* pNodeGraph) @@ -71350,27 +71369,72 @@ MA_API ma_result ma_node_graph_read_pcm_frames(ma_node_graph* pNodeGraph, void* totalFramesRead = 0; while (totalFramesRead < frameCount) { ma_uint32 framesJustRead; - ma_uint64 framesToRead = frameCount - totalFramesRead; + ma_uint64 framesToRead; + float* pRunningFramesOut; + framesToRead = frameCount - totalFramesRead; if (framesToRead > 0xFFFFFFFF) { framesToRead = 0xFFFFFFFF; } - ma_node_graph_set_is_reading(pNodeGraph, MA_TRUE); - { - result = ma_node_read_pcm_frames(&pNodeGraph->endpoint, 0, (float*)ma_offset_pcm_frames_ptr(pFramesOut, totalFramesRead, ma_format_f32, channels), (ma_uint32)framesToRead, &framesJustRead, ma_node_get_time(&pNodeGraph->endpoint)); - } - ma_node_graph_set_is_reading(pNodeGraph, MA_FALSE); + pRunningFramesOut = (float*)ma_offset_pcm_frames_ptr(pFramesOut, totalFramesRead, ma_format_f32, channels); - totalFramesRead += framesJustRead; + /* If there's anything in the cache, consume that first. */ + if (pNodeGraph->processingCacheFramesRemaining > 0) { + ma_uint32 framesToReadFromCache; - if (result != MA_SUCCESS) { - break; - } + framesToReadFromCache = (ma_uint32)framesToRead; + if (framesToReadFromCache > pNodeGraph->processingCacheFramesRemaining) { + framesToReadFromCache = pNodeGraph->processingCacheFramesRemaining; + } - /* Abort if we weren't able to read any frames or else we risk getting stuck in a loop. */ - if (framesJustRead == 0) { - break; + MA_COPY_MEMORY(pRunningFramesOut, pNodeGraph->pProcessingCache, framesToReadFromCache * channels * sizeof(float)); + MA_MOVE_MEMORY(pNodeGraph->pProcessingCache, pNodeGraph->pProcessingCache + (framesToReadFromCache * channels), (pNodeGraph->processingCacheFramesRemaining - framesToReadFromCache) * channels * sizeof(float)); + pNodeGraph->processingCacheFramesRemaining -= framesToReadFromCache; + + totalFramesRead += framesToReadFromCache; + continue; + } else { + /* + If processingSizeInFrames is non-zero, we need to make sure we always read in chunks of that size. If the frame count is less than + that, we need to read into the cache and then continue on. + */ + float* pReadDst = pRunningFramesOut; + + if (pNodeGraph->processingSizeInFrames > 0) { + if (framesToRead < pNodeGraph->processingSizeInFrames) { + pReadDst = pNodeGraph->pProcessingCache; /* We need to read into the cache because otherwise we'll overflow the output buffer. */ + } + + framesToRead = pNodeGraph->processingSizeInFrames; + } + + ma_node_graph_set_is_reading(pNodeGraph, MA_TRUE); + { + result = ma_node_read_pcm_frames(&pNodeGraph->endpoint, 0, pReadDst, (ma_uint32)framesToRead, &framesJustRead, ma_node_get_time(&pNodeGraph->endpoint)); + } + ma_node_graph_set_is_reading(pNodeGraph, MA_FALSE); + + /* + Do not increment the total frames read counter if we read into the cache. We use this to determine how many frames have + been written to the final output buffer. + */ + if (pReadDst == pNodeGraph->pProcessingCache) { + /* We read into the cache. */ + pNodeGraph->processingCacheFramesRemaining = framesJustRead; + } else { + /* We read straight into the output buffer. */ + totalFramesRead += framesJustRead; + } + + if (result != MA_SUCCESS) { + break; + } + + /* Abort if we weren't able to read any frames or else we risk getting stuck in a loop. */ + if (framesJustRead == 0) { + break; + } } } @@ -71807,8 +71871,8 @@ static ma_result ma_node_input_bus_read_pcm_frames(ma_node* pInputNode, ma_node_ if (pFramesOut != NULL) { /* Read. */ - float temp[MA_DATA_CONVERTER_STACK_BUFFER_SIZE / sizeof(float)]; - ma_uint32 tempCapInFrames = ma_countof(temp) / inputChannels; + float* temp = ((ma_node_base*)pOutputBus->pNode)->pPreMixBuffer; + ma_uint32 tempCapInFrames = ((ma_node_base*)pOutputBus->pNode)->preMixBufferCapInFrames; while (framesProcessed < frameCount) { float* pRunningFramesOut; @@ -71886,6 +71950,25 @@ MA_API ma_node_config ma_node_config_init(void) return config; } +static ma_uint16 ma_node_config_get_cache_size_in_frames(const ma_node_config* pConfig, const ma_node_graph* pNodeGraph) +{ + ma_uint32 cacheSizeInFrames; + + if (pConfig->processingSizeInFrames > 0) { + cacheSizeInFrames = pConfig->processingSizeInFrames; + } else if (pNodeGraph->processingSizeInFrames > 0) { + cacheSizeInFrames = pNodeGraph->processingSizeInFrames; + } else { + cacheSizeInFrames = MA_DEFAULT_NODE_CACHE_CAP_IN_FRAMES_PER_BUS; + } + + if (cacheSizeInFrames > 0xFFFF) { + cacheSizeInFrames = 0xFFFF; + } + + return (ma_uint16)cacheSizeInFrames; +} + static ma_result ma_node_detach_full(ma_node* pNode); @@ -71936,8 +72019,10 @@ typedef struct size_t inputBusOffset; size_t outputBusOffset; size_t cachedDataOffset; + size_t preMixBufferDataOffset; ma_uint32 inputBusCount; /* So it doesn't have to be calculated twice. */ ma_uint32 outputBusCount; /* So it doesn't have to be calculated twice. */ + ma_uint32 preMixBufferCapInFrames; } ma_node_heap_layout; static ma_result ma_node_translate_bus_counts(const ma_node_config* pConfig, ma_uint32* pInputBusCount, ma_uint32* pOutputBusCount) @@ -72005,6 +72090,7 @@ static ma_result ma_node_get_heap_layout(ma_node_graph* pNodeGraph, const ma_nod ma_result result; ma_uint32 inputBusCount; ma_uint32 outputBusCount; + ma_uint32 preMixBufferCapInFrames; MA_ASSERT(pHeapLayout != NULL); @@ -72040,7 +72126,7 @@ static ma_result ma_node_get_heap_layout(ma_node_graph* pNodeGraph, const ma_nod /* Cached audio data. - We need to allocate memory for a caching both input and output data. We have an optimization + We need to allocate memory for caching both input and output data. We have an optimization where no caching is necessary for specific conditions: - The node has 0 inputs and 1 output. @@ -72059,14 +72145,18 @@ static ma_result ma_node_get_heap_layout(ma_node_graph* pNodeGraph, const ma_nod } else { /* Slow path. Cache needed. */ size_t cachedDataSizeInBytes = 0; + ma_uint32 cacheCapInFrames; ma_uint32 iBus; + /* The capacity of the cache is based on our callback processing size. */ + cacheCapInFrames = ma_node_config_get_cache_size_in_frames(pConfig, pNodeGraph); + for (iBus = 0; iBus < inputBusCount; iBus += 1) { - cachedDataSizeInBytes += pNodeGraph->nodeCacheCapInFrames * ma_get_bytes_per_frame(ma_format_f32, pConfig->pInputChannels[iBus]); + cachedDataSizeInBytes += cacheCapInFrames * ma_get_bytes_per_frame(ma_format_f32, pConfig->pInputChannels[iBus]); } for (iBus = 0; iBus < outputBusCount; iBus += 1) { - cachedDataSizeInBytes += pNodeGraph->nodeCacheCapInFrames * ma_get_bytes_per_frame(ma_format_f32, pConfig->pOutputChannels[iBus]); + cachedDataSizeInBytes += cacheCapInFrames * ma_get_bytes_per_frame(ma_format_f32, pConfig->pOutputChannels[iBus]); } pHeapLayout->cachedDataOffset = pHeapLayout->sizeInBytes; @@ -72074,12 +72164,39 @@ static ma_result ma_node_get_heap_layout(ma_node_graph* pNodeGraph, const ma_nod } + /* + We need a buffer for storing the input data just before it's mixed. The size of this buffer needs + to be based on the preferred processing size. If that's unset, we default to a constant value. We + only ever process a single input bus at a time so we can size this based on the input bus that has + the most channels. + */ + { + size_t preMixBufferSizeInBytes; + ma_uint32 maxChannelCount = 0; + ma_uint32 iBus; + + for (iBus = 0; iBus < inputBusCount; iBus += 1) { + if (maxChannelCount < pConfig->pInputChannels[iBus]) { + maxChannelCount = pConfig->pInputChannels[iBus]; + } + } + + preMixBufferCapInFrames = ma_node_config_get_cache_size_in_frames(pConfig, pNodeGraph); + preMixBufferSizeInBytes = maxChannelCount * sizeof(float) * preMixBufferCapInFrames; + + pHeapLayout->preMixBufferDataOffset = pHeapLayout->sizeInBytes; + pHeapLayout->sizeInBytes += ma_align_64(preMixBufferSizeInBytes); + } + + + /* Not technically part of the heap, but we can output the input and output bus counts so we can avoid a redundant call to ma_node_translate_bus_counts(). */ pHeapLayout->inputBusCount = inputBusCount; pHeapLayout->outputBusCount = outputBusCount; + pHeapLayout->preMixBufferCapInFrames = preMixBufferCapInFrames; /* Make sure allocation size is aligned. */ pHeapLayout->sizeInBytes = ma_align_64(pHeapLayout->sizeInBytes); @@ -72130,13 +72247,15 @@ MA_API ma_result ma_node_init_preallocated(ma_node_graph* pNodeGraph, const ma_n pNodeBase->_pHeap = pHeap; MA_ZERO_MEMORY(pHeap, heapLayout.sizeInBytes); - pNodeBase->pNodeGraph = pNodeGraph; - pNodeBase->vtable = pConfig->vtable; - pNodeBase->state = pConfig->initialState; + pNodeBase->pNodeGraph = pNodeGraph; + pNodeBase->vtable = pConfig->vtable; + pNodeBase->processingSizeInFrames = pConfig->processingSizeInFrames; + pNodeBase->state = pConfig->initialState; pNodeBase->stateTimes[ma_node_state_started] = 0; pNodeBase->stateTimes[ma_node_state_stopped] = (ma_uint64)(ma_int64)-1; /* Weird casting for VC6 compatibility. */ - pNodeBase->inputBusCount = heapLayout.inputBusCount; - pNodeBase->outputBusCount = heapLayout.outputBusCount; + pNodeBase->inputBusCount = heapLayout.inputBusCount; + pNodeBase->outputBusCount = heapLayout.outputBusCount; + pNodeBase->preMixBufferCapInFrames = heapLayout.preMixBufferCapInFrames; if (heapLayout.inputBusOffset != MA_SIZE_MAX) { pNodeBase->pInputBuses = (ma_node_input_bus*)ma_offset_ptr(pHeap, heapLayout.inputBusOffset); @@ -72152,11 +72271,13 @@ MA_API ma_result ma_node_init_preallocated(ma_node_graph* pNodeGraph, const ma_n if (heapLayout.cachedDataOffset != MA_SIZE_MAX) { pNodeBase->pCachedData = (float*)ma_offset_ptr(pHeap, heapLayout.cachedDataOffset); - pNodeBase->cachedDataCapInFramesPerBus = pNodeGraph->nodeCacheCapInFrames; + pNodeBase->cachedDataCapInFramesPerBus = ma_node_config_get_cache_size_in_frames(pConfig, pNodeGraph); } else { pNodeBase->pCachedData = NULL; } + pNodeBase->pPreMixBuffer = (float*)ma_offset_ptr(pHeap, heapLayout.preMixBufferDataOffset); + /* We need to run an initialization step for each input and output bus. */ @@ -75122,7 +75243,7 @@ MA_API ma_result ma_engine_init(const ma_engine_config* pConfig, ma_engine* pEng /* The engine is a node graph. This needs to be initialized after we have the device so we can can determine the channel count. */ nodeGraphConfig = ma_node_graph_config_init(engineConfig.channels); - nodeGraphConfig.nodeCacheCapInFrames = (engineConfig.periodSizeInFrames > 0xFFFF) ? 0xFFFF : (ma_uint16)engineConfig.periodSizeInFrames; + nodeGraphConfig.processingSizeInFrames = engineConfig.periodSizeInFrames; result = ma_node_graph_init(&nodeGraphConfig, &pEngine->allocationCallbacks, &pEngine->nodeGraph); if (result != MA_SUCCESS) { From 9aa6e035bbb21aa08dd5d55eb13400ee33f02fc4 Mon Sep 17 00:00:00 2001 From: David Reid Date: Tue, 27 Feb 2024 17:05:56 +1000 Subject: [PATCH 04/12] Memory improvements to node processing. When processing a node, miniaudio will read into a temporary buffer before mixing input attachments. This commit removes the per-node heap allocation and replaces it with a per-graph stack. This should result in less memory usage at larger scales, but at the expense of slightly more usage at smaller scales. The size of the stack can be configured via ma_node_graph_config. If ma_engine is being used, it can be done via ma_engine_config. --- miniaudio.h | 183 ++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 133 insertions(+), 50 deletions(-) diff --git a/miniaudio.h b/miniaudio.h index 5db2f74f..d6e89078 100644 --- a/miniaudio.h +++ b/miniaudio.h @@ -10558,6 +10558,16 @@ Node Graph /* Use this when the bus count is determined by the node instance rather than the vtable. */ #define MA_NODE_BUS_COUNT_UNKNOWN 255 + +/* For some internal memory management of ma_node_graph. */ +typedef struct +{ + size_t offset; + size_t sizeInBytes; + unsigned char _data[1]; +} ma_stack; + + typedef struct ma_node_graph ma_node_graph; typedef void ma_node; @@ -10685,8 +10695,6 @@ struct ma_node_base /* These variables are set once at startup. */ ma_node_graph* pNodeGraph; /* The graph this node belongs to. */ const ma_node_vtable* vtable; - float* pPreMixBuffer; /* When data is read from an input bus, it's read into this buffer which will then be mixed into another buffer. Must be allocated on the heap in order to support processingSizeInFrames. If allocated on the heap, it would need to be broken down into smaller parts. */ - ma_uint32 preMixBufferCapInFrames; /* The capacity of the pre-mix buffer. */ ma_uint32 processingSizeInFrames; float* pCachedData; /* Allocated on the heap. Fixed size. Needs to be stored on the heap because reading from output buses is done in separate function calls. */ ma_uint16 cachedDataCapInFramesPerBus; /* The capacity of the input data cache in frames, per bus. */ @@ -10740,6 +10748,7 @@ typedef struct { ma_uint32 channels; ma_uint32 processingSizeInFrames; /* This is the preferred processing size for node processing callbacks unless overridden by a node itself. Can be 0 in which case it will be based on the frame count passed into ma_node_graph_read_pcm_frames(), but will not be well defined. */ + size_t preMixStackSizeInBytes; /* Defaults to 512KB per channel. Reducing this will save memory, but the depth of your node graph will be more restricted. */ } ma_node_graph_config; MA_API ma_node_graph_config ma_node_graph_config_init(ma_uint32 channels); @@ -10756,6 +10765,9 @@ struct ma_node_graph /* Read and written by multiple threads. */ MA_ATOMIC(4, ma_bool32) isReading; + + /* Modified only by the audio thread. */ + ma_stack* pPreMixStack; }; MA_API ma_result ma_node_graph_init(const ma_node_graph_config* pConfig, const ma_allocation_callbacks* pAllocationCallbacks, ma_node_graph* pNodeGraph); @@ -11209,6 +11221,7 @@ typedef struct ma_uint32 gainSmoothTimeInFrames; /* The number of frames to interpolate the gain of spatialized sounds across. If set to 0, will use gainSmoothTimeInMilliseconds. */ ma_uint32 gainSmoothTimeInMilliseconds; /* When set to 0, gainSmoothTimeInFrames will be used. If both are set to 0, a default value will be used. */ ma_uint32 defaultVolumeSmoothTimeInPCMFrames; /* Defaults to 0. Controls the default amount of smoothing to apply to volume changes to sounds. High values means more smoothing at the expense of high latency (will take longer to reach the new volume). */ + ma_uint32 preMixStackSizeInBytes; /* A stack is used for internal processing in the node graph. This allows you to configure the size of this stack. Smaller values will reduce the maximum depth of your node graph. You should rarely need to modify this. */ ma_allocation_callbacks allocationCallbacks; ma_bool32 noAutoStart; /* When set to true, requires an explicit call to ma_engine_start(). This is false by default, meaning the engine will be started automatically in ma_engine_init(). */ ma_bool32 noDevice; /* When set to true, don't create a default device. ma_engine_read_pcm_frames() can be called manually to read data. */ @@ -71153,11 +71166,75 @@ static ma_result ma_job_process__resource_manager__seek_data_stream(ma_job* pJob #ifndef MA_NO_NODE_GRAPH + +static ma_stack* ma_stack_init(size_t sizeInBytes, const ma_allocation_callbacks* pAllocationCallbacks) +{ + ma_stack* pStack; + + if (sizeInBytes == 0) { + return NULL; + } + + pStack = (ma_stack*)ma_malloc(sizeof(*pStack) - sizeof(pStack->_data) + sizeInBytes, pAllocationCallbacks); + if (pStack == NULL) { + return NULL; + } + + pStack->offset = 0; + pStack->sizeInBytes = sizeInBytes; + + return pStack; +} + +static void ma_stack_uninit(ma_stack* pStack, const ma_allocation_callbacks* pAllocationCallbacks) +{ + if (pStack == NULL) { + return; + } + + ma_free(pStack, pAllocationCallbacks); +} + +static void* ma_stack_alloc(ma_stack* pStack, size_t sz) +{ + /* The size of the allocation is stored in the memory directly before the pointer. This needs to include padding to keep it aligned to ma_uintptr */ + void* p = (void*)((char*)pStack->_data + pStack->offset); + size_t* pSize = (size_t*)p; + + sz = (sz + (sizeof(ma_uintptr) - 1)) & ~(sizeof(ma_uintptr) - 1); /* Padding. */ + if (pStack->offset + sz + sizeof(size_t) > pStack->sizeInBytes) { + return NULL; /* Out of memory. */ + } + + pStack->offset += sz + sizeof(size_t); + + *pSize = sz; + return (void*)((char*)p + sizeof(size_t)); +} + +static void ma_stack_free(ma_stack* pStack, void* p) +{ + size_t* pSize; + + if (p == NULL) { + return; + } + + pSize = (size_t*)p - 1; + pStack->offset -= *pSize + sizeof(size_t); +} + + + /* 10ms @ 48K = 480. Must never exceed 65535. */ #ifndef MA_DEFAULT_NODE_CACHE_CAP_IN_FRAMES_PER_BUS #define MA_DEFAULT_NODE_CACHE_CAP_IN_FRAMES_PER_BUS 480 #endif +#ifndef MA_DEFAULT_PREMIX_STACK_SIZE_PER_CHANNEL +#define MA_DEFAULT_PREMIX_STACK_SIZE_PER_CHANNEL 524288 +#endif + static ma_result ma_node_read_pcm_frames(ma_node* pNode, ma_uint32 outputBusIndex, float* pFramesOut, ma_uint32 frameCount, ma_uint32* pFramesRead, ma_uint64 globalTime); MA_API void ma_debug_fill_pcm_frames_with_sine_wave(float* pFramesOut, ma_uint32 frameCount, ma_format format, ma_uint32 channels, ma_uint32 sampleRate) @@ -71321,6 +71398,28 @@ MA_API ma_result ma_node_graph_init(const ma_node_graph_config* pConfig, const m } + /* + We need a pre-mix stack. The size of this stack is configurable via the config. The default value depends on the channel count. + */ + { + size_t preMixStackSizeInBytes = pConfig->preMixStackSizeInBytes; + if (preMixStackSizeInBytes == 0) { + preMixStackSizeInBytes = pConfig->channels * MA_DEFAULT_PREMIX_STACK_SIZE_PER_CHANNEL; + } + + pNodeGraph->pPreMixStack = ma_stack_init(preMixStackSizeInBytes, pAllocationCallbacks); + if (pNodeGraph->pPreMixStack == NULL) { + ma_node_uninit(&pNodeGraph->endpoint, pAllocationCallbacks); + ma_node_uninit(&pNodeGraph->base, pAllocationCallbacks); + if (pNodeGraph->pProcessingCache != NULL) { + ma_free(pNodeGraph->pProcessingCache, pAllocationCallbacks); + } + + return MA_OUT_OF_MEMORY; + } + } + + return MA_SUCCESS; } @@ -71337,6 +71436,11 @@ MA_API void ma_node_graph_uninit(ma_node_graph* pNodeGraph, const ma_allocation_ ma_free(pNodeGraph->pProcessingCache, pAllocationCallbacks); pNodeGraph->pProcessingCache = NULL; } + + if (pNodeGraph->pPreMixStack != NULL) { + ma_stack_uninit(pNodeGraph->pPreMixStack, pAllocationCallbacks); + pNodeGraph->pPreMixStack = NULL; + } } MA_API ma_node* ma_node_graph_get_endpoint(ma_node_graph* pNodeGraph) @@ -71821,8 +71925,6 @@ static ma_result ma_node_input_bus_read_pcm_frames(ma_node* pInputNode, ma_node_ ma_uint32 inputChannels; ma_bool32 doesOutputBufferHaveContent = MA_FALSE; - (void)pInputNode; /* Not currently used. */ - /* This will be called from the audio thread which means we can't be doing any locking. Basically, this function will not perfom any locking, whereas attaching and detaching will, but crafted in @@ -71871,19 +71973,12 @@ static ma_result ma_node_input_bus_read_pcm_frames(ma_node* pInputNode, ma_node_ if (pFramesOut != NULL) { /* Read. */ - float* temp = ((ma_node_base*)pOutputBus->pNode)->pPreMixBuffer; - ma_uint32 tempCapInFrames = ((ma_node_base*)pOutputBus->pNode)->preMixBufferCapInFrames; - while (framesProcessed < frameCount) { float* pRunningFramesOut; ma_uint32 framesToRead; - ma_uint32 framesJustRead; + ma_uint32 framesJustRead = 0; framesToRead = frameCount - framesProcessed; - if (framesToRead > tempCapInFrames) { - framesToRead = tempCapInFrames; - } - pRunningFramesOut = ma_offset_pcm_frames_ptr_f32(pFramesOut, framesProcessed, inputChannels); if (doesOutputBufferHaveContent == MA_FALSE) { @@ -71891,11 +71986,32 @@ static ma_result ma_node_input_bus_read_pcm_frames(ma_node* pInputNode, ma_node_ result = ma_node_read_pcm_frames(pOutputBus->pNode, pOutputBus->outputBusIndex, pRunningFramesOut, framesToRead, &framesJustRead, globalTime + framesProcessed); } else { /* Slow path. Not the first attachment. Mixing required. */ - result = ma_node_read_pcm_frames(pOutputBus->pNode, pOutputBus->outputBusIndex, temp, framesToRead, &framesJustRead, globalTime + framesProcessed); - if (result == MA_SUCCESS || result == MA_AT_END) { - if (isSilentOutput == MA_FALSE) { /* Don't mix if the node outputs silence. */ - ma_mix_pcm_frames_f32(pRunningFramesOut, temp, framesJustRead, inputChannels, /*volume*/1); + ma_uint32 preMixBufferCapInFrames = ((ma_node_base*)pInputNode)->cachedDataCapInFramesPerBus; + float* pPreMixBuffer = (float*)ma_stack_alloc(((ma_node_base*)pInputNode)->pNodeGraph->pPreMixStack, preMixBufferCapInFrames * inputChannels * sizeof(float)); + + if (pPreMixBuffer == NULL) { + /* + If you're hitting this assert it means you've got an unusually deep chain of nodes, you've got an excessively large processing + size, or you have a combination of both, and as a result have run out of stack space. You can increase this using the + preMixStackSizeInBytes variable in ma_node_graph_config. If you're using ma_engine, you can do it via the preMixStackSizeInBytes + variable in ma_engine_config. It defaults to 512KB per output channel. + */ + MA_ASSERT(MA_FALSE); + } else { + if (framesToRead > preMixBufferCapInFrames) { + framesToRead = preMixBufferCapInFrames; } + + result = ma_node_read_pcm_frames(pOutputBus->pNode, pOutputBus->outputBusIndex, pPreMixBuffer, framesToRead, &framesJustRead, globalTime + framesProcessed); + if (result == MA_SUCCESS || result == MA_AT_END) { + if (isSilentOutput == MA_FALSE) { /* Don't mix if the node outputs silence. */ + ma_mix_pcm_frames_f32(pRunningFramesOut, pPreMixBuffer, framesJustRead, inputChannels, /*volume*/1); + } + } + + /* The pre-mix buffer is no longer required. */ + ma_stack_free(((ma_node_base*)pInputNode)->pNodeGraph->pPreMixStack, pPreMixBuffer); + pPreMixBuffer = NULL; } } @@ -72019,10 +72135,8 @@ typedef struct size_t inputBusOffset; size_t outputBusOffset; size_t cachedDataOffset; - size_t preMixBufferDataOffset; ma_uint32 inputBusCount; /* So it doesn't have to be calculated twice. */ ma_uint32 outputBusCount; /* So it doesn't have to be calculated twice. */ - ma_uint32 preMixBufferCapInFrames; } ma_node_heap_layout; static ma_result ma_node_translate_bus_counts(const ma_node_config* pConfig, ma_uint32* pInputBusCount, ma_uint32* pOutputBusCount) @@ -72090,7 +72204,6 @@ static ma_result ma_node_get_heap_layout(ma_node_graph* pNodeGraph, const ma_nod ma_result result; ma_uint32 inputBusCount; ma_uint32 outputBusCount; - ma_uint32 preMixBufferCapInFrames; MA_ASSERT(pHeapLayout != NULL); @@ -72164,39 +72277,12 @@ static ma_result ma_node_get_heap_layout(ma_node_graph* pNodeGraph, const ma_nod } - /* - We need a buffer for storing the input data just before it's mixed. The size of this buffer needs - to be based on the preferred processing size. If that's unset, we default to a constant value. We - only ever process a single input bus at a time so we can size this based on the input bus that has - the most channels. - */ - { - size_t preMixBufferSizeInBytes; - ma_uint32 maxChannelCount = 0; - ma_uint32 iBus; - - for (iBus = 0; iBus < inputBusCount; iBus += 1) { - if (maxChannelCount < pConfig->pInputChannels[iBus]) { - maxChannelCount = pConfig->pInputChannels[iBus]; - } - } - - preMixBufferCapInFrames = ma_node_config_get_cache_size_in_frames(pConfig, pNodeGraph); - preMixBufferSizeInBytes = maxChannelCount * sizeof(float) * preMixBufferCapInFrames; - - pHeapLayout->preMixBufferDataOffset = pHeapLayout->sizeInBytes; - pHeapLayout->sizeInBytes += ma_align_64(preMixBufferSizeInBytes); - } - - - /* Not technically part of the heap, but we can output the input and output bus counts so we can avoid a redundant call to ma_node_translate_bus_counts(). */ pHeapLayout->inputBusCount = inputBusCount; pHeapLayout->outputBusCount = outputBusCount; - pHeapLayout->preMixBufferCapInFrames = preMixBufferCapInFrames; /* Make sure allocation size is aligned. */ pHeapLayout->sizeInBytes = ma_align_64(pHeapLayout->sizeInBytes); @@ -72255,7 +72341,6 @@ MA_API ma_result ma_node_init_preallocated(ma_node_graph* pNodeGraph, const ma_n pNodeBase->stateTimes[ma_node_state_stopped] = (ma_uint64)(ma_int64)-1; /* Weird casting for VC6 compatibility. */ pNodeBase->inputBusCount = heapLayout.inputBusCount; pNodeBase->outputBusCount = heapLayout.outputBusCount; - pNodeBase->preMixBufferCapInFrames = heapLayout.preMixBufferCapInFrames; if (heapLayout.inputBusOffset != MA_SIZE_MAX) { pNodeBase->pInputBuses = (ma_node_input_bus*)ma_offset_ptr(pHeap, heapLayout.inputBusOffset); @@ -72276,9 +72361,6 @@ MA_API ma_result ma_node_init_preallocated(ma_node_graph* pNodeGraph, const ma_n pNodeBase->pCachedData = NULL; } - pNodeBase->pPreMixBuffer = (float*)ma_offset_ptr(pHeap, heapLayout.preMixBufferDataOffset); - - /* We need to run an initialization step for each input and output bus. */ for (iInputBus = 0; iInputBus < ma_node_get_input_bus_count(pNodeBase); iInputBus += 1) { @@ -75244,6 +75326,7 @@ MA_API ma_result ma_engine_init(const ma_engine_config* pConfig, ma_engine* pEng /* The engine is a node graph. This needs to be initialized after we have the device so we can can determine the channel count. */ nodeGraphConfig = ma_node_graph_config_init(engineConfig.channels); nodeGraphConfig.processingSizeInFrames = engineConfig.periodSizeInFrames; + nodeGraphConfig.preMixStackSizeInBytes = engineConfig.preMixStackSizeInBytes; result = ma_node_graph_init(&nodeGraphConfig, &pEngine->allocationCallbacks, &pEngine->nodeGraph); if (result != MA_SUCCESS) { From 5cb0c0567565e515db210938158222cbdccdca1e Mon Sep 17 00:00:00 2001 From: David Reid Date: Wed, 28 Feb 2024 08:10:34 +1000 Subject: [PATCH 05/12] Update Steam Audio example to work with latest version. --- examples/engine_steamaudio.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/engine_steamaudio.c b/examples/engine_steamaudio.c index d64cc2fd..e9610ab4 100644 --- a/examples/engine_steamaudio.c +++ b/examples/engine_steamaudio.c @@ -23,8 +23,8 @@ would support variable sized updates which would avoid this whole mess entirely. #define MINIAUDIO_IMPLEMENTATION #include "../miniaudio.h" +#include /* Required for uint32_t which is used by STEAMAUDIO_VERSION, and a random use of uint8_t. If there's a Steam Audio maintainer reading this, that needs to be fixed to use IPLuint32 and IPLuint8. */ #include /* Steam Audio */ -#include /* Required for uint32_t which is used by STEAMAUDIO_VERSION. That dependency needs to be removed from Steam Audio - use IPLuint32 or "unsigned int" instead! */ #define FORMAT ma_format_f32 /* Must be floating point. */ #define CHANNELS 2 /* Must be stereo for this example. */ @@ -98,6 +98,7 @@ static void ma_steamaudio_binaural_node_process_pcm_frames(ma_node* pNode, const ma_uint32 totalFramesToProcess = *pFrameCountOut; ma_uint32 totalFramesProcessed = 0; + MA_ZERO_OBJECT(&binauralParams); binauralParams.direction.x = pBinauralNode->direction.x; binauralParams.direction.y = pBinauralNode->direction.y; binauralParams.direction.z = pBinauralNode->direction.z; @@ -322,7 +323,8 @@ int main(int argc, char** argv) /* IPLHRTF */ MA_ZERO_OBJECT(&iplHRTFSettings); - iplHRTFSettings.type = IPL_HRTFTYPE_DEFAULT; + iplHRTFSettings.type = IPL_HRTFTYPE_DEFAULT; + iplHRTFSettings.volume = 1; result = ma_result_from_IPLerror(iplHRTFCreate(iplContext, &iplAudioSettings, &iplHRTFSettings, &iplHRTF)); if (result != MA_SUCCESS) { From e3af234720ffbf6438791ee749825b3a17c25118 Mon Sep 17 00:00:00 2001 From: David Reid Date: Wed, 28 Feb 2024 08:15:13 +1000 Subject: [PATCH 06/12] Silence a warning in the node graph example. --- examples/node_graph.c | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/node_graph.c b/examples/node_graph.c index 83aa8945..dd1b9374 100644 --- a/examples/node_graph.c +++ b/examples/node_graph.c @@ -88,6 +88,7 @@ void data_callback(ma_device* pDevice, void* pOutput, const void* pInput, ma_uin ma_node_graph_read_pcm_frames(&g_nodeGraph, pOutput, frameCount, NULL); (void)pInput; /* Unused. */ + (void)pDevice; /* Unused. */ } int main(int argc, char** argv) From d36a2ef651c5fd109c9c4e20369400ac6e0233e1 Mon Sep 17 00:00:00 2001 From: David Reid Date: Wed, 28 Feb 2024 17:16:50 +1000 Subject: [PATCH 07/12] Minor cleanup. --- miniaudio.h | 41 ++++++++++++++++++----------------------- 1 file changed, 18 insertions(+), 23 deletions(-) diff --git a/miniaudio.h b/miniaudio.h index d6e89078..99939872 100644 --- a/miniaudio.h +++ b/miniaudio.h @@ -10638,7 +10638,6 @@ typedef struct { const ma_node_vtable* vtable; /* Should never be null. Initialization of the node will fail if so. */ ma_node_state initialState; /* Defaults to ma_node_state_started. */ - ma_uint32 processingSizeInFrames; /* The preferred processing size in frames per instantiation of the processing callback. You should leave this set to 0 unless your node needs to process in chunks of a specific size. */ ma_uint32 inputBusCount; /* Only used if the vtable specifies an input bus count of `MA_NODE_BUS_COUNT_UNKNOWN`, otherwise must be set to `MA_NODE_BUS_COUNT_UNKNOWN` (default). */ ma_uint32 outputBusCount; /* Only used if the vtable specifies an output bus count of `MA_NODE_BUS_COUNT_UNKNOWN`, otherwise be set to `MA_NODE_BUS_COUNT_UNKNOWN` (default). */ const ma_uint32* pInputChannels; /* The number of elements are determined by the input bus count as determined by the vtable, or `inputBusCount` if the vtable specifies `MA_NODE_BUS_COUNT_UNKNOWN`. */ @@ -10693,11 +10692,14 @@ typedef struct ma_node_base ma_node_base; struct ma_node_base { /* These variables are set once at startup. */ - ma_node_graph* pNodeGraph; /* The graph this node belongs to. */ + ma_node_graph* pNodeGraph; /* The graph this node belongs to. */ const ma_node_vtable* vtable; - ma_uint32 processingSizeInFrames; - float* pCachedData; /* Allocated on the heap. Fixed size. Needs to be stored on the heap because reading from output buses is done in separate function calls. */ - ma_uint16 cachedDataCapInFramesPerBus; /* The capacity of the input data cache in frames, per bus. */ + ma_uint32 inputBusCount; + ma_uint32 outputBusCount; + ma_node_input_bus* pInputBuses; + ma_node_output_bus* pOutputBuses; + float* pCachedData; /* Allocated on the heap. Fixed size. Needs to be stored on the heap because reading from output buses is done in separate function calls. */ + ma_uint16 cachedDataCapInFramesPerBus; /* The capacity of the input data cache in frames, per bus. */ /* These variables are read and written only from the audio thread. */ ma_uint16 cachedFrameCountOut; @@ -10705,13 +10707,9 @@ struct ma_node_base ma_uint16 consumedFrameCountIn; /* These variables are read and written between different threads. */ - MA_ATOMIC(4, ma_node_state) state; /* When set to stopped, nothing will be read, regardless of the times in stateTimes. */ - MA_ATOMIC(8, ma_uint64) stateTimes[2]; /* Indexed by ma_node_state. Specifies the time based on the global clock that a node should be considered to be in the relevant state. */ - MA_ATOMIC(8, ma_uint64) localTime; /* The node's local clock. This is just a running sum of the number of output frames that have been processed. Can be modified by any thread with `ma_node_set_time()`. */ - ma_uint32 inputBusCount; - ma_uint32 outputBusCount; - ma_node_input_bus* pInputBuses; - ma_node_output_bus* pOutputBuses; + MA_ATOMIC(4, ma_node_state) state; /* When set to stopped, nothing will be read, regardless of the times in stateTimes. */ + MA_ATOMIC(8, ma_uint64) stateTimes[2]; /* Indexed by ma_node_state. Specifies the time based on the global clock that a node should be considered to be in the relevant state. */ + MA_ATOMIC(8, ma_uint64) localTime; /* The node's local clock. This is just a running sum of the number of output frames that have been processed. Can be modified by any thread with `ma_node_set_time()`. */ /* Memory management. */ ma_node_input_bus _inputBuses[MA_MAX_NODE_LOCAL_BUS_COUNT]; @@ -11250,7 +11248,7 @@ struct ma_engine ma_allocation_callbacks allocationCallbacks; ma_bool8 ownsResourceManager; ma_bool8 ownsDevice; - ma_spinlock inlinedSoundLock; /* For synchronizing access so the inlined sound list. */ + ma_spinlock inlinedSoundLock; /* For synchronizing access to the inlined sound list. */ ma_sound_inlined* pInlinedSoundHead; /* The first inlined sound. Inlined sounds are tracked in a linked list. */ MA_ATOMIC(4, ma_uint32) inlinedSoundCount; /* The total number of allocated inlined sound objects. Used for debugging. */ ma_uint32 gainSmoothTimeInFrames; /* The number of frames to interpolate the gain of spatialized sounds across. */ @@ -72070,9 +72068,7 @@ static ma_uint16 ma_node_config_get_cache_size_in_frames(const ma_node_config* p { ma_uint32 cacheSizeInFrames; - if (pConfig->processingSizeInFrames > 0) { - cacheSizeInFrames = pConfig->processingSizeInFrames; - } else if (pNodeGraph->processingSizeInFrames > 0) { + if (pNodeGraph->processingSizeInFrames > 0) { cacheSizeInFrames = pNodeGraph->processingSizeInFrames; } else { cacheSizeInFrames = MA_DEFAULT_NODE_CACHE_CAP_IN_FRAMES_PER_BUS; @@ -72333,14 +72329,13 @@ MA_API ma_result ma_node_init_preallocated(ma_node_graph* pNodeGraph, const ma_n pNodeBase->_pHeap = pHeap; MA_ZERO_MEMORY(pHeap, heapLayout.sizeInBytes); - pNodeBase->pNodeGraph = pNodeGraph; - pNodeBase->vtable = pConfig->vtable; - pNodeBase->processingSizeInFrames = pConfig->processingSizeInFrames; - pNodeBase->state = pConfig->initialState; + pNodeBase->pNodeGraph = pNodeGraph; + pNodeBase->vtable = pConfig->vtable; + pNodeBase->state = pConfig->initialState; pNodeBase->stateTimes[ma_node_state_started] = 0; pNodeBase->stateTimes[ma_node_state_stopped] = (ma_uint64)(ma_int64)-1; /* Weird casting for VC6 compatibility. */ - pNodeBase->inputBusCount = heapLayout.inputBusCount; - pNodeBase->outputBusCount = heapLayout.outputBusCount; + pNodeBase->inputBusCount = heapLayout.inputBusCount; + pNodeBase->outputBusCount = heapLayout.outputBusCount; if (heapLayout.inputBusOffset != MA_SIZE_MAX) { pNodeBase->pInputBuses = (ma_node_input_bus*)ma_offset_ptr(pHeap, heapLayout.inputBusOffset); @@ -72916,7 +72911,7 @@ static ma_result ma_node_read_pcm_frames(ma_node* pNode, ma_uint32 outputBusInde /* A passthrough should never have modified the input and output frame counts. If you're - triggering these assers you need to fix your processing callback. + triggering these asserts you need to fix your processing callback. */ MA_ASSERT(frameCountIn == totalFramesRead); MA_ASSERT(frameCountOut == totalFramesRead); From feea26496cbbd64efe3c5db937e39ec4502e6e65 Mon Sep 17 00:00:00 2001 From: David Reid Date: Wed, 28 Feb 2024 17:17:13 +1000 Subject: [PATCH 08/12] Update Steam Audio example. --- examples/engine_steamaudio.c | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/examples/engine_steamaudio.c b/examples/engine_steamaudio.c index e9610ab4..71fdad6a 100644 --- a/examples/engine_steamaudio.c +++ b/examples/engine_steamaudio.c @@ -8,17 +8,11 @@ By implementing this as a node, it can be plugged into any position within the g channel count of this node is always stereo. Steam Audio requires fixed sized processing, the size of which must be specified at initialization -time of the IPLBinauralEffect and IPLHRTF objects. This creates a problem because the node graph -will at times need to break down processing into smaller chunks for it's internal processing. The -node graph internally will read into a temporary buffer which is then mixed into the final output -buffer. This temporary buffer is allocated on the stack and is a fixed size. However, variability -comes into play because the channel count of the node is variable. It's not safe to just blindly -process the effect with the frame count specified in miniaudio's node processing callback. Doing so -results in glitching. To work around this, this example is just setting the update size to a known -value that works (256). If it's set to something too big it'll exceed miniaudio's processing size -used by the node graph. Alternatively you could use some kind of intermediary cache which -accumulates input data until enough is available and then do the processing. Ideally, Steam Audio -would support variable sized updates which would avoid this whole mess entirely. +time of the IPLBinauralEffect and IPLHRTF objects. To ensure miniaudio and Steam Audio are +consistent, you must set the period size in the engine config to be consistent with the frame size +you specify in your IPLAudioSettings object. If for some reason you want the period size of the +engine to be different to that of your Steam Audio configuration, you'll need to implement a sort +of buffering solution to your node. */ #define MINIAUDIO_IMPLEMENTATION #include "../miniaudio.h" @@ -285,8 +279,16 @@ int main(int argc, char** argv) /* The engine needs to be initialized first. */ engineConfig = ma_engine_config_init(); - engineConfig.channels = CHANNELS; - engineConfig.sampleRate = SAMPLE_RATE; + engineConfig.channels = CHANNELS; + engineConfig.sampleRate = SAMPLE_RATE; + + /* + Steam Audio requires processing in fixed sized chunks. Setting the period size in the engine config will + ensure our updates happen in predicably sized chunks as required by Steam Audio. + + Note that the configuration of Steam Audio below (IPLAudioSettings) will use this variable to specify the + update size to ensure it remains consistent. + */ engineConfig.periodSizeInFrames = 256; result = ma_engine_init(&engineConfig, &g_engine); @@ -306,6 +308,9 @@ int main(int argc, char** argv) be documented. If this is for some kind of buffer management with FFT or something, then this need not be exposed to the public API. There should be no need for the public API to require a fixed sized update. + + It's important that this be set to the periodSizeInFrames specified in the engine config above. + This ensures updates on both the miniaudio side and the Steam Audio side are consistent. */ iplAudioSettings.frameSize = engineConfig.periodSizeInFrames; From b454e7f14bc5004ba29078d84410a99695ef29b7 Mon Sep 17 00:00:00 2001 From: David Reid Date: Wed, 28 Feb 2024 18:40:35 +1000 Subject: [PATCH 09/12] PulseAudio: Try fixing a channel mapping bug. Public issue https://github.com/mackron/miniaudio/issues/811 --- miniaudio.h | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/miniaudio.h b/miniaudio.h index 99939872..b477f1e5 100644 --- a/miniaudio.h +++ b/miniaudio.h @@ -30388,6 +30388,7 @@ static ma_result ma_device_init__pulse(ma_device* pDevice, const ma_device_confi ma_pa_buffer_attr attr; const ma_pa_sample_spec* pActualSS = NULL; const ma_pa_buffer_attr* pActualAttr = NULL; + const ma_pa_channel_map* pActualChannelMap = NULL; ma_uint32 iChannel; ma_pa_stream_flags_t streamFlags; @@ -30537,7 +30538,12 @@ static ma_result ma_device_init__pulse(ma_device* pDevice, const ma_device_confi goto on_error4; } + /* Internal channel map. */ + pActualChannelMap = ((ma_pa_stream_get_channel_map_proc)pDevice->pContext->pulse.pa_stream_get_channel_map)((ma_pa_stream*)pDevice->pulse.pStreamCapture); + if (pActualChannelMap == NULL) { + pActualChannelMap = &cmap; /* Fallback just in case. */ + } /* Bug in PipeWire. There have been reports that PipeWire is returning AUX channels when reporting @@ -30547,8 +30553,8 @@ static ma_result ma_device_init__pulse(ma_device* pDevice, const ma_device_confi fixed sooner than later. I might remove this hack later. */ if (pDescriptorCapture->channels > 2) { - for (iChannel = 0; iChannel < pDescriptorCapture->channels; ++iChannel) { - pDescriptorCapture->channelMap[iChannel] = ma_channel_position_from_pulse(cmap.map[iChannel]); + for (iChannel = 0; iChannel < pDescriptorCapture->channels; iChannel += 1) { + pDescriptorCapture->channelMap[iChannel] = ma_channel_position_from_pulse(pActualChannelMap->map[iChannel]); } } else { /* Hack for mono and stereo. */ @@ -30689,7 +30695,12 @@ static ma_result ma_device_init__pulse(ma_device* pDevice, const ma_device_confi goto on_error4; } + /* Internal channel map. */ + pActualChannelMap = ((ma_pa_stream_get_channel_map_proc)pDevice->pContext->pulse.pa_stream_get_channel_map)((ma_pa_stream*)pDevice->pulse.pStreamPlayback); + if (pActualChannelMap == NULL) { + pActualChannelMap = &cmap; /* Fallback just in case. */ + } /* Bug in PipeWire. There have been reports that PipeWire is returning AUX channels when reporting @@ -30699,8 +30710,8 @@ static ma_result ma_device_init__pulse(ma_device* pDevice, const ma_device_confi fixed sooner than later. I might remove this hack later. */ if (pDescriptorPlayback->channels > 2) { - for (iChannel = 0; iChannel < pDescriptorPlayback->channels; ++iChannel) { - pDescriptorPlayback->channelMap[iChannel] = ma_channel_position_from_pulse(cmap.map[iChannel]); + for (iChannel = 0; iChannel < pDescriptorPlayback->channels; iChannel += 1) { + pDescriptorPlayback->channelMap[iChannel] = ma_channel_position_from_pulse(pActualChannelMap->map[iChannel]); } } else { /* Hack for mono and stereo. */ @@ -72068,6 +72079,8 @@ static ma_uint16 ma_node_config_get_cache_size_in_frames(const ma_node_config* p { ma_uint32 cacheSizeInFrames; + (void)pConfig; + if (pNodeGraph->processingSizeInFrames > 0) { cacheSizeInFrames = pNodeGraph->processingSizeInFrames; } else { From 29da9b789cbcf07cdedfc6867b9d7a4fefa1fabc Mon Sep 17 00:00:00 2001 From: David Reid Date: Thu, 29 Feb 2024 10:25:02 +1000 Subject: [PATCH 10/12] Add new init flags for sounds and resource managed data sources. This adds the following flags: * MA_SOUND_FLAG_LOOPING * MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_LOOPING These can be used to initialize sounds and resource managed data sources to loop by default. This is the recommended way to enable looping for streams. The `isLooping` config option in `ma_sound_config` and `ma_resource_manager_data_source_config` has been deprecated. If you are using those, you should switch to the new flag or else you'll get compiler errors when upgrading to a future version. --- CHANGES.md | 1 + miniaudio.h | 38 ++++++++++++++++++++++++++++---------- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 90fc1a42..776cf594 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,6 @@ v0.11.22 - TBD ===================== +* Add `MA_SOUND_FLAG_LOOPING` and `MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_LOOPING` flags. These can be used to initialize sounds and resource managed data sources to loop by default. This is the recommended way to enable looping for streams. The `isLooping` config option in `ma_sound_config` and `ma_resource_manager_data_source_config` has been deprecated. If you are using those, you should switch to the new flag or else you'll get compiler errors when upgrading to a future version. * Fix a bug relating to node detachment. * Fix a bug where amplification with `ma_device_set_master_volume()` does not work. * ALSA: Fix some warnings relating to unhandled return value of `read()`. diff --git a/miniaudio.h b/miniaudio.h index b477f1e5..e67031da 100644 --- a/miniaudio.h +++ b/miniaudio.h @@ -10246,7 +10246,8 @@ typedef enum MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_DECODE = 0x00000002, /* Decode data before storing in memory. When set, decoding is done at the resource manager level rather than the mixing thread. Results in faster mixing, but higher memory usage. */ MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_ASYNC = 0x00000004, /* When set, the resource manager will load the data source asynchronously. */ MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_WAIT_INIT = 0x00000008, /* When set, waits for initialization of the underlying data source before returning from ma_resource_manager_data_source_init(). */ - MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_UNKNOWN_LENGTH = 0x00000010 /* Gives the resource manager a hint that the length of the data source is unknown and calling `ma_data_source_get_length_in_pcm_frames()` should be avoided. */ + MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_UNKNOWN_LENGTH = 0x00000010, /* Gives the resource manager a hint that the length of the data source is unknown and calling `ma_data_source_get_length_in_pcm_frames()` should be avoided. */ + MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_LOOPING = 0x00000020 /* When set, configures the data source to loop by default. */ } ma_resource_manager_data_source_flags; @@ -10314,8 +10315,8 @@ typedef struct ma_uint64 rangeEndInPCMFrames; ma_uint64 loopPointBegInPCMFrames; ma_uint64 loopPointEndInPCMFrames; - ma_bool32 isLooping; ma_uint32 flags; + ma_bool32 isLooping; /* Deprecated. Use the MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_LOOPING flag in `flags` instead. */ } ma_resource_manager_data_source_config; MA_API ma_resource_manager_data_source_config ma_resource_manager_data_source_config_init(void); @@ -11050,6 +11051,7 @@ typedef enum MA_SOUND_FLAG_ASYNC = 0x00000004, /* MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_ASYNC */ MA_SOUND_FLAG_WAIT_INIT = 0x00000008, /* MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_WAIT_INIT */ MA_SOUND_FLAG_UNKNOWN_LENGTH = 0x00000010, /* MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_UNKNOWN_LENGTH */ + MA_SOUND_FLAG_LOOPING = 0x00000020, /* MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_LOOPING */ /* ma_sound specific flags. */ MA_SOUND_FLAG_NO_DEFAULT_ATTACHMENT = 0x00001000, /* Do not attach to the endpoint by default. Useful for when setting up nodes in a complex graph system. */ @@ -11149,13 +11151,13 @@ typedef struct ma_uint64 rangeEndInPCMFrames; ma_uint64 loopPointBegInPCMFrames; ma_uint64 loopPointEndInPCMFrames; - ma_bool32 isLooping; ma_sound_end_proc endCallback; /* Fired when the sound reaches the end. Will be fired from the audio thread. Do not restart, uninitialize or otherwise change the state of the sound from here. Instead fire an event or set a variable to indicate to a different thread to change the start of the sound. Will not be fired in response to a scheduled stop with ma_sound_set_stop_time_*(). */ void* pEndCallbackUserData; #ifndef MA_NO_RESOURCE_MANAGER ma_resource_manager_pipeline_notifications initNotifications; #endif ma_fence* pDoneFence; /* Deprecated. Use initNotifications instead. Released when the resource manager has finished decoding the entire sound. Not used with streams. */ + ma_bool32 isLooping; /* Deprecated. Use the MA_SOUND_FLAG_LOOPING flag in `flags` instead. */ } ma_sound_config; MA_API ma_sound_config ma_sound_config_init(void); /* Deprecated. Will be removed in version 0.12. Use ma_sound_config_2() instead. */ @@ -68898,6 +68900,10 @@ static ma_result ma_resource_manager_data_buffer_init_ex_internal(ma_resource_ma flags &= ~MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_ASYNC; } + if (pConfig->isLooping) { + flags |= MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_LOOPING; + } + async = (flags & MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_ASYNC) != 0; /* @@ -68974,7 +68980,7 @@ static ma_result ma_resource_manager_data_buffer_init_ex_internal(ma_resource_ma job.data.resourceManager.loadDataBuffer.rangeEndInPCMFrames = pConfig->rangeEndInPCMFrames; job.data.resourceManager.loadDataBuffer.loopPointBegInPCMFrames = pConfig->loopPointBegInPCMFrames; job.data.resourceManager.loadDataBuffer.loopPointEndInPCMFrames = pConfig->loopPointEndInPCMFrames; - job.data.resourceManager.loadDataBuffer.isLooping = pConfig->isLooping; + job.data.resourceManager.loadDataBuffer.isLooping = (flags & MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_LOOPING) != 0; /* If we need to wait for initialization to complete we can just process the job in place. */ if ((flags & MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_WAIT_INIT) != 0) { @@ -69610,6 +69616,7 @@ MA_API ma_result ma_resource_manager_data_stream_init_ex(ma_resource_manager* pR ma_bool32 waitBeforeReturning = MA_FALSE; ma_resource_manager_inline_notification waitNotification; ma_resource_manager_pipeline_notifications notifications; + ma_uint32 flags; if (pDataStream == NULL) { if (pConfig != NULL && pConfig->pNotifications != NULL) { @@ -69640,13 +69647,18 @@ MA_API ma_result ma_resource_manager_data_stream_init_ex(ma_resource_manager* pR return result; } + flags = pConfig->flags; + if (pConfig->isLooping) { + flags |= MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_LOOPING; + } + pDataStream->pResourceManager = pResourceManager; pDataStream->flags = pConfig->flags; pDataStream->result = MA_BUSY; ma_data_source_set_range_in_pcm_frames(pDataStream, pConfig->rangeBegInPCMFrames, pConfig->rangeEndInPCMFrames); ma_data_source_set_loop_point_in_pcm_frames(pDataStream, pConfig->loopPointBegInPCMFrames, pConfig->loopPointEndInPCMFrames); - ma_data_source_set_looping(pDataStream, pConfig->isLooping); + ma_data_source_set_looping(pDataStream, (flags & MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_LOOPING) != 0); if (pResourceManager == NULL || (pConfig->pFilePath == NULL && pConfig->pFilePathW == NULL)) { ma_resource_manager_pipeline_notifications_signal_all_notifications(¬ifications); @@ -69834,7 +69846,7 @@ static void ma_resource_manager_data_stream_fill_page(ma_resource_manager_data_s ma_atomic_exchange_32(&pDataStream->isPageValid[pageIndex], MA_TRUE); } -static void ma_resource_manager_data_stream_fill_pages(ma_resource_manager_data_stream* pDataStream) +static void ma_resource_manager_data_stream_fill_pages(ma_resource_manager_data_stream* pDataStream, ma_bool32 isInitialFill) { ma_uint32 iPage; @@ -70268,6 +70280,9 @@ static ma_result ma_resource_manager_data_source_preinit(ma_resource_manager* pR } pDataSource->flags = pConfig->flags; + if (pConfig->isLooping) { + pDataSource->flags |= MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_LOOPING; + } return MA_SUCCESS; } @@ -70998,7 +71013,7 @@ static ma_result ma_job_process__resource_manager__load_data_stream(ma_job* pJob ma_decoder_seek_to_pcm_frame(&pDataStream->decoder, pJob->data.resourceManager.loadDataStream.initialSeekPoint); /* We have our decoder and our page buffer, so now we need to fill our pages. */ - ma_resource_manager_data_stream_fill_pages(pDataStream); + ma_resource_manager_data_stream_fill_pages(pDataStream, MA_TRUE); /* And now we're done. We want to make sure the result is MA_SUCCESS. */ result = MA_SUCCESS; @@ -71124,7 +71139,7 @@ static ma_result ma_job_process__resource_manager__seek_data_stream(ma_job* pJob ma_decoder_seek_to_pcm_frame(&pDataStream->decoder, pJob->data.resourceManager.seekDataStream.frameIndex); /* After seeking we'll need to reload the pages. */ - ma_resource_manager_data_stream_fill_pages(pDataStream); + ma_resource_manager_data_stream_fill_pages(pDataStream, MA_FALSE); /* We need to let the public API know that we're done seeking. */ ma_atomic_fetch_sub_32(&pDataStream->seekCounter, 1); @@ -76147,7 +76162,7 @@ static ma_result ma_sound_init_from_data_source_internal(ma_engine* pEngine, con ma_data_source_set_range_in_pcm_frames(ma_sound_get_data_source(pSound), pConfig->loopPointBegInPCMFrames, pConfig->loopPointEndInPCMFrames); } - ma_sound_set_looping(pSound, pConfig->isLooping); + ma_sound_set_looping(pSound, pConfig->isLooping || ((pConfig->flags & MA_SOUND_FLAG_LOOPING) != 0)); return MA_SUCCESS; } @@ -76171,6 +76186,9 @@ MA_API ma_result ma_sound_init_from_file_internal(ma_engine* pEngine, const ma_s it and can avoid accessing the sound from within the notification. */ flags = pConfig->flags | MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_WAIT_INIT; + if (pConfig->isLooping) { + flags |= MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_LOOPING; + } pSound->pResourceManagerDataSource = (ma_resource_manager_data_source*)ma_malloc(sizeof(*pSound->pResourceManagerDataSource), &pEngine->allocationCallbacks); if (pSound->pResourceManagerDataSource == NULL) { @@ -76199,7 +76217,7 @@ MA_API ma_result ma_sound_init_from_file_internal(ma_engine* pEngine, const ma_s resourceManagerDataSourceConfig.rangeEndInPCMFrames = pConfig->rangeEndInPCMFrames; resourceManagerDataSourceConfig.loopPointBegInPCMFrames = pConfig->loopPointBegInPCMFrames; resourceManagerDataSourceConfig.loopPointEndInPCMFrames = pConfig->loopPointEndInPCMFrames; - resourceManagerDataSourceConfig.isLooping = pConfig->isLooping; + resourceManagerDataSourceConfig.isLooping = (flags & MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_LOOPING) != 0; result = ma_resource_manager_data_source_init_ex(pEngine->pResourceManager, &resourceManagerDataSourceConfig, pSound->pResourceManagerDataSource); if (result != MA_SUCCESS) { From 63e1900db8e8be4080f6868717ae41ab3973661c Mon Sep 17 00:00:00 2001 From: David Reid Date: Thu, 29 Feb 2024 10:50:06 +1000 Subject: [PATCH 11/12] Update documentation. --- miniaudio.h | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/miniaudio.h b/miniaudio.h index e67031da..37201fb5 100644 --- a/miniaudio.h +++ b/miniaudio.h @@ -1686,6 +1686,7 @@ combination of the following flags: MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_DECODE MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_ASYNC MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_WAIT_INIT + MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_LOOPING ``` When no flags are specified (set to 0), the sound will be fully loaded into memory, but not @@ -1706,6 +1707,14 @@ can instead stream audio data which you can do by specifying the second pages. When a new page needs to be decoded, a job will be posted to the job queue and then subsequently processed in a job thread. +The `MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_LOOPING` flag can be used so that the sound will loop +when it reaches the end by default. It's recommended you use this flag when you want to have a +looping streaming sound. If you try loading a very short sound as a stream, you will get a glitch. +This is because the resource manager needs to pre-fill the initial buffer at initialization time, +and if you don't specify the `MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_LOOPING` flag, the resource +manager will assume the sound is not looping and will stop filling the buffer when it reaches the +end, therefore resulting in a discontinuous buffer. + For in-memory sounds, reference counting is used to ensure the data is loaded only once. This means multiple calls to `ma_resource_manager_data_source_init()` with the same file path will result in the file data only being loaded once. Each call to `ma_resource_manager_data_source_init()` must be From 3bdf611768411e354ab91735d35a37814b4cb593 Mon Sep 17 00:00:00 2001 From: David Reid Date: Thu, 29 Feb 2024 14:30:48 +1000 Subject: [PATCH 12/12] Fix an invalid comment. --- examples/custom_decoder.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/custom_decoder.c b/examples/custom_decoder.c index b8384989..7021e84c 100644 --- a/examples/custom_decoder.c +++ b/examples/custom_decoder.c @@ -176,7 +176,6 @@ static ma_decoding_backend_vtable g_ma_decoding_backend_vtable_libopus = - void data_callback(ma_device* pDevice, void* pOutput, const void* pInput, ma_uint32 frameCount) { ma_data_source* pDataSource = (ma_data_source*)pDevice->pUserData; @@ -219,7 +218,7 @@ int main(int argc, char** argv) /* Initialize the decoder. */ decoderConfig = ma_decoder_config_init_default(); - decoderConfig.pCustomBackendUserData = NULL; /* In this example our backend objects are contained within a ma_decoder_ex object to avoid a malloc. Our vtables need to know about this. */ + decoderConfig.pCustomBackendUserData = NULL; /* None of our decoders require user data, so this can be set to null. */ decoderConfig.ppCustomBackendVTables = pCustomBackendVTables; decoderConfig.customBackendCount = sizeof(pCustomBackendVTables) / sizeof(pCustomBackendVTables[0]);