From ba3080d07d8e1ba3f97ffc2c03fb81b709629c65 Mon Sep 17 00:00:00 2001 From: David Reid Date: Fri, 10 Dec 2021 16:13:16 +1000 Subject: [PATCH] Add support for partial processing to nodes. This includes a leading trimming node to act as a test. --- extras/nodes/ma_ltrim_node/ma_ltrim_node.c | 102 +++++++ extras/nodes/ma_ltrim_node/ma_ltrim_node.h | 35 +++ .../ma_ltrim_node/ma_ltrim_node_example.c | 115 ++++++++ miniaudio.h | 257 +++++++++--------- 4 files changed, 385 insertions(+), 124 deletions(-) create mode 100644 extras/nodes/ma_ltrim_node/ma_ltrim_node.c create mode 100644 extras/nodes/ma_ltrim_node/ma_ltrim_node.h create mode 100644 extras/nodes/ma_ltrim_node/ma_ltrim_node_example.c diff --git a/extras/nodes/ma_ltrim_node/ma_ltrim_node.c b/extras/nodes/ma_ltrim_node/ma_ltrim_node.c new file mode 100644 index 00000000..64aae373 --- /dev/null +++ b/extras/nodes/ma_ltrim_node/ma_ltrim_node.c @@ -0,0 +1,102 @@ + +#include "ma_ltrim_node.h" + +MA_API ma_ltrim_node_config ma_ltrim_node_config_init(ma_uint32 channels, float threshold) +{ + ma_ltrim_node_config config; + + MA_ZERO_OBJECT(&config); + config.nodeConfig = ma_node_config_init(); /* Input and output channels will be set in ma_ltrim_node_init(). */ + config.channels = channels; + config.threshold = threshold; + + return config; +} + + +static void ma_ltrim_node_process_pcm_frames(ma_node* pNode, const float** ppFramesIn, ma_uint32* pFrameCountIn, float** ppFramesOut, ma_uint32* pFrameCountOut) +{ + ma_ltrim_node* pTrimNode = (ma_ltrim_node*)pNode; + ma_uint32 framesProcessedIn = 0; + ma_uint32 framesProcessedOut = 0; + ma_uint32 channelCount = ma_node_get_input_channels(pNode, 0); + + /* + If we haven't yet found the start, skip over every input sample until we find a frame outside + of the threshold. + */ + if (pTrimNode->foundStart == MA_FALSE) { + while (framesProcessedIn < *pFrameCountIn) { + ma_uint32 iChannel = 0; + for (iChannel = 0; iChannel < channelCount; iChannel += 1) { + float sample = ppFramesIn[0][framesProcessedIn*channelCount + iChannel]; + if (sample < -pTrimNode->threshold || sample > pTrimNode->threshold) { + pTrimNode->foundStart = MA_TRUE; + break; + } + } + + if (pTrimNode->foundStart) { + break; /* The start has been found. Get out of this loop and finish off processing. */ + } else { + framesProcessedIn += 1; + } + } + } + + /* If there's anything left, just copy it over. */ + framesProcessedOut = ma_min(*pFrameCountOut, *pFrameCountIn - framesProcessedIn); + ma_copy_pcm_frames(ppFramesOut[0], &ppFramesIn[0][framesProcessedIn], framesProcessedOut, ma_format_f32, channelCount); + + framesProcessedIn += framesProcessedOut; + + /* We always "process" every input frame, but we may only done a partial output. */ + *pFrameCountIn = framesProcessedIn; + *pFrameCountOut = framesProcessedOut; +} + +static ma_node_vtable g_ma_ltrim_node_vtable = +{ + ma_ltrim_node_process_pcm_frames, + NULL, + 1, /* 1 input channel. */ + 1, /* 1 output channel. */ + MA_NODE_FLAG_DIFFERENT_PROCESSING_RATES +}; + +MA_API ma_result ma_ltrim_node_init(ma_node_graph* pNodeGraph, const ma_ltrim_node_config* pConfig, const ma_allocation_callbacks* pAllocationCallbacks, ma_ltrim_node* pTrimNode) +{ + ma_result result; + ma_node_config baseConfig; + + if (pTrimNode == NULL) { + return MA_INVALID_ARGS; + } + + MA_ZERO_OBJECT(pTrimNode); + + if (pConfig == NULL) { + return MA_INVALID_ARGS; + } + + pTrimNode->threshold = pConfig->threshold; + pTrimNode->foundStart = MA_FALSE; + + baseConfig = pConfig->nodeConfig; + baseConfig.vtable = &g_ma_ltrim_node_vtable; + baseConfig.pInputChannels = &pConfig->channels; + baseConfig.pOutputChannels = &pConfig->channels; + + result = ma_node_init(pNodeGraph, &baseConfig, pAllocationCallbacks, &pTrimNode->baseNode); + if (result != MA_SUCCESS) { + return result; + } + + return MA_SUCCESS; +} + +MA_API void ma_ltrim_node_uninit(ma_ltrim_node* pTrimNode, const ma_allocation_callbacks* pAllocationCallbacks) +{ + /* The base node is always uninitialized first. */ + ma_node_uninit(pTrimNode, pAllocationCallbacks); +} diff --git a/extras/nodes/ma_ltrim_node/ma_ltrim_node.h b/extras/nodes/ma_ltrim_node/ma_ltrim_node.h new file mode 100644 index 00000000..2dddd3a7 --- /dev/null +++ b/extras/nodes/ma_ltrim_node/ma_ltrim_node.h @@ -0,0 +1,35 @@ +/* Include ma_ltrim_node.h after miniaudio.h */ +#ifndef ma_ltrim_node_h +#define ma_ltrim_node_h + +#ifdef __cplusplus +extern "C" { +#endif + +/* +The trim node has one input and one output. +*/ +typedef struct +{ + ma_node_config nodeConfig; + ma_uint32 channels; + float threshold; +} ma_ltrim_node_config; + +MA_API ma_ltrim_node_config ma_ltrim_node_config_init(ma_uint32 channels, float threshold); + + +typedef struct +{ + ma_node_base baseNode; + float threshold; + ma_bool32 foundStart; +} ma_ltrim_node; + +MA_API ma_result ma_ltrim_node_init(ma_node_graph* pNodeGraph, const ma_ltrim_node_config* pConfig, const ma_allocation_callbacks* pAllocationCallbacks, ma_ltrim_node* pTrimNode); +MA_API void ma_ltrim_node_uninit(ma_ltrim_node* pTrimNode, const ma_allocation_callbacks* pAllocationCallbacks); + +#ifdef __cplusplus +} +#endif +#endif /* ma_ltrim_node_h */ diff --git a/extras/nodes/ma_ltrim_node/ma_ltrim_node_example.c b/extras/nodes/ma_ltrim_node/ma_ltrim_node_example.c new file mode 100644 index 00000000..0bae166d --- /dev/null +++ b/extras/nodes/ma_ltrim_node/ma_ltrim_node_example.c @@ -0,0 +1,115 @@ +#define MINIAUDIO_IMPLEMENTATION +#include "../../../miniaudio.h" +#include "ma_ltrim_node.c" + +#include + +#define DEVICE_FORMAT ma_format_f32 /* Must always be f32 for this example because the node graph system only works with this. */ +#define DEVICE_CHANNELS 0 /* The input file will determine the channel count. */ +#define DEVICE_SAMPLE_RATE 0 /* The input file will determine the sample rate. */ + +static ma_decoder g_decoder; /* The decoder that we'll read data from. */ +static ma_data_source_node g_dataSupplyNode; /* The node that will sit at the root level. Will be reading data from g_dataSupply. */ +static ma_ltrim_node g_trimNode; /* The trim node. */ +static ma_node_graph g_nodeGraph; /* The main node graph that we'll be feeding data through. */ + +void data_callback(ma_device* pDevice, void* pOutput, const void* pInput, ma_uint32 frameCount) +{ + (void)pInput; + (void)pDevice; + + /* All we need to do is read from the node graph. */ + ma_node_graph_read_pcm_frames(&g_nodeGraph, pOutput, frameCount, NULL); +} + +int main(int argc, char** argv) +{ + ma_result result; + ma_decoder_config decoderConfig; + ma_device_config deviceConfig; + ma_device device; + ma_node_graph_config nodeGraphConfig; + ma_ltrim_node_config trimNodeConfig; + ma_data_source_node_config dataSupplyNodeConfig; + + if (argc < 1) { + printf("No input file.\n"); + return -1; + } + + + /* Decoder. */ + decoderConfig = ma_decoder_config_init(DEVICE_FORMAT, DEVICE_CHANNELS, DEVICE_SAMPLE_RATE); + + result = ma_decoder_init_file(argv[1], &decoderConfig, &g_decoder); + if (result != MA_SUCCESS) { + printf("Failed to load decoder.\n"); + return -1; + } + + + /* Device. */ + deviceConfig = ma_device_config_init(ma_device_type_playback); + deviceConfig.playback.pDeviceID = NULL; + deviceConfig.playback.format = g_decoder.outputFormat; + deviceConfig.playback.channels = g_decoder.outputChannels; + deviceConfig.sampleRate = g_decoder.outputSampleRate; + deviceConfig.dataCallback = data_callback; + result = ma_device_init(NULL, &deviceConfig, &device); + if (result != MA_SUCCESS) { + return result; + } + + + /* Node graph. */ + nodeGraphConfig = ma_node_graph_config_init(device.playback.channels); + + result = ma_node_graph_init(&nodeGraphConfig, NULL, &g_nodeGraph); + if (result != MA_SUCCESS) { + printf("Failed to initialize node graph."); + goto done0; + } + + + /* Trimmer. Attached straight to the endpoint. Input will be the data source node. */ + trimNodeConfig = ma_ltrim_node_config_init(device.playback.channels, 0); + + result = ma_ltrim_node_init(&g_nodeGraph, &trimNodeConfig, NULL, &g_trimNode); + if (result != MA_SUCCESS) { + printf("Failed to initialize ltrim node."); + goto done1; + } + + ma_node_attach_output_bus(&g_trimNode, 0, ma_node_graph_get_endpoint(&g_nodeGraph), 0); + + + /* Data supply. */ + dataSupplyNodeConfig = ma_data_source_node_config_init(&g_decoder); + + result = ma_data_source_node_init(&g_nodeGraph, &dataSupplyNodeConfig, NULL, &g_dataSupplyNode); + if (result != MA_SUCCESS) { + printf("Failed to initialize data source node."); + goto done2; + } + + ma_node_attach_output_bus(&g_dataSupplyNode, 0, &g_trimNode, 0); + + + + /* Now we just start the device and wait for the user to terminate the program. */ + ma_device_start(&device); + + printf("Press Enter to quit...\n"); + getchar(); + + /* It's important that we stop the device first or else we'll uninitialize the graph from under the device. */ + ma_device_stop(&device); + + +/*done3:*/ ma_data_source_node_uninit(&g_dataSupplyNode, NULL); +done2: ma_ltrim_node_uninit(&g_trimNode, NULL); +done1: ma_node_graph_uninit(&g_nodeGraph, NULL); +done0: ma_device_uninit(&device); + + return 0; +} \ No newline at end of file diff --git a/miniaudio.h b/miniaudio.h index befe516d..66a85c98 100644 --- a/miniaudio.h +++ b/miniaudio.h @@ -28,7 +28,7 @@ audio device. The high level API is good for those who have complex mixing and e 1.1. Low Level API ------------------ +------------------ The low level API gives you access to the raw audio data of an audio device. It supports playback, capture, full-duplex and loopback (WASAPI only). You can enumerate over devices to determine which physical device(s) you want to connect to. @@ -67466,149 +67466,158 @@ static ma_result ma_node_read_pcm_frames(ma_node* pNode, ma_uint32 outputBusInde /* Getting here means we need to do another round of processing. */ pNodeBase->cachedFrameCountOut = 0; - /* - We need to prepare our output frame pointers for processing. In the same iteration we need - to mark every output bus as unread so that future calls to this function for different buses - for the current time period don't pull in data when they should instead be reading from cache. - */ - for (iOutputBus = 0; iOutputBus < outputBusCount; iOutputBus += 1) { - ma_node_output_bus_set_has_read(&pNodeBase->pOutputBuses[iOutputBus], MA_FALSE); /* <-- This is what tells the next calls to this function for other output buses for this time period to read from cache instead of pulling in more data. */ - ppFramesOut[iOutputBus] = ma_node_get_cached_output_ptr(pNode, iOutputBus); - } - - /* We only need to read from input buses if there isn't already some data in the cache. */ - if (pNodeBase->cachedFrameCountIn == 0) { - ma_uint32 maxFramesReadIn = 0; - - /* Here is where we pull in data from the input buses. This is what will trigger an advance in time. */ - for (iInputBus = 0; iInputBus < inputBusCount; iInputBus += 1) { - ma_uint32 framesRead; - - /* The first thing to do is get the offset within our bulk allocation to store this input data. */ - ppFramesIn[iInputBus] = ma_node_get_cached_input_ptr(pNode, iInputBus); - - /* Once we've determined our destination pointer we can read. Note that we must inspect the number of frames read and fill any leftovers with silence for safety. */ - result = ma_node_input_bus_read_pcm_frames(pNodeBase, &pNodeBase->pInputBuses[iInputBus], ppFramesIn[iInputBus], framesToProcessIn, &framesRead, globalTime); - if (result != MA_SUCCESS) { - /* It doesn't really matter if we fail because we'll just fill with silence. */ - framesRead = 0; /* Just for safety, but I don't think it's really needed. */ - } - - /* TODO: Minor optimization opportunity here. If no frames were read and the buffer is already filled with silence, no need to re-silence it. */ - /* Any leftover frames need to silenced for safety. */ - if (framesRead < framesToProcessIn) { - ma_silence_pcm_frames(ppFramesIn[iInputBus] + (framesRead * ma_node_get_input_channels(pNodeBase, iInputBus)), (framesToProcessIn - framesRead), ma_format_f32, ma_node_get_input_channels(pNodeBase, iInputBus)); - } - - maxFramesReadIn = ma_max(maxFramesReadIn, framesRead); - } - - /* This was a fresh load of input data so reset our consumption counter. */ - pNodeBase->consumedFrameCountIn = 0; + for (;;) { + frameCountOut = 0; /* - We don't want to keep processing if there's nothing to process, so set the number of cached - input frames to the maximum number we read from each attachment (the lesser will be padded - with silence). If we didn't read anything, this will be set to 0 and the entire buffer will - have been assigned to silence. This being equal to 0 is an important property for us because - it allows us to detect when NULL can be passed into the processing callback for the input - buffer for the purpose of continuous processing. + We need to prepare our output frame pointers for processing. In the same iteration we need + to mark every output bus as unread so that future calls to this function for different buses + for the current time period don't pull in data when they should instead be reading from cache. */ - pNodeBase->cachedFrameCountIn = (ma_uint16)maxFramesReadIn; - } else { - /* We don't need to read anything, but we do need to prepare our input frame pointers. */ - for (iInputBus = 0; iInputBus < inputBusCount; iInputBus += 1) { - ppFramesIn[iInputBus] = ma_node_get_cached_input_ptr(pNode, iInputBus) + (pNodeBase->consumedFrameCountIn * ma_node_get_input_channels(pNodeBase, iInputBus)); + for (iOutputBus = 0; iOutputBus < outputBusCount; iOutputBus += 1) { + ma_node_output_bus_set_has_read(&pNodeBase->pOutputBuses[iOutputBus], MA_FALSE); /* <-- This is what tells the next calls to this function for other output buses for this time period to read from cache instead of pulling in more data. */ + ppFramesOut[iOutputBus] = ma_node_get_cached_output_ptr(pNode, iOutputBus); } - } - /* - At this point we have our input data so now we need to do some processing. Sneaky little - optimization here - we can set the pointer to the output buffer for this output bus so - that the final copy into the output buffer is done directly by onProcess(). - */ - if (pFramesOut != NULL) { - ppFramesOut[outputBusIndex] = pFramesOut; - } + /* We only need to read from input buses if there isn't already some data in the cache. */ + if (pNodeBase->cachedFrameCountIn == 0) { + ma_uint32 maxFramesReadIn = 0; + /* Here is where we pull in data from the input buses. This is what will trigger an advance in time. */ + for (iInputBus = 0; iInputBus < inputBusCount; iInputBus += 1) { + ma_uint32 framesRead; - /* Give the processing function the entire capacity of the output buffer. */ - frameCountOut = framesToProcessOut; + /* The first thing to do is get the offset within our bulk allocation to store this input data. */ + ppFramesIn[iInputBus] = ma_node_get_cached_input_ptr(pNode, iInputBus); - /* - We need to treat nodes with continuous processing a little differently. For these ones, - we always want to fire the callback with the requested number of frames, regardless of - pNodeBase->cachedFrameCountIn, which could be 0. Also, we want to check if we can pass - in NULL for the input buffer to the callback. - */ - if ((pNodeBase->vtable->flags & MA_NODE_FLAG_CONTINUOUS_PROCESSING) != 0) { - /* We're using continuous processing. Make sure we specify the whole frame count at all times. */ - frameCountIn = framesToProcessIn; /* Give the processing function as much input data as we've got in the buffer, including any silenced padding from short reads. */ + /* Once we've determined our destination pointer we can read. Note that we must inspect the number of frames read and fill any leftovers with silence for safety. */ + result = ma_node_input_bus_read_pcm_frames(pNodeBase, &pNodeBase->pInputBuses[iInputBus], ppFramesIn[iInputBus], framesToProcessIn, &framesRead, globalTime); + if (result != MA_SUCCESS) { + /* It doesn't really matter if we fail because we'll just fill with silence. */ + framesRead = 0; /* Just for safety, but I don't think it's really needed. */ + } - if ((pNodeBase->vtable->flags & MA_NODE_FLAG_ALLOW_NULL_INPUT) != 0 && pNodeBase->consumedFrameCountIn == 0 && pNodeBase->cachedFrameCountIn == 0) { - consumeNullInput = MA_TRUE; + /* TODO: Minor optimization opportunity here. If no frames were read and the buffer is already filled with silence, no need to re-silence it. */ + /* Any leftover frames need to silenced for safety. */ + if (framesRead < framesToProcessIn) { + ma_silence_pcm_frames(ppFramesIn[iInputBus] + (framesRead * ma_node_get_input_channels(pNodeBase, iInputBus)), (framesToProcessIn - framesRead), ma_format_f32, ma_node_get_input_channels(pNodeBase, iInputBus)); + } + + maxFramesReadIn = ma_max(maxFramesReadIn, framesRead); + } + + /* This was a fresh load of input data so reset our consumption counter. */ + pNodeBase->consumedFrameCountIn = 0; + + /* + We don't want to keep processing if there's nothing to process, so set the number of cached + input frames to the maximum number we read from each attachment (the lesser will be padded + with silence). If we didn't read anything, this will be set to 0 and the entire buffer will + have been assigned to silence. This being equal to 0 is an important property for us because + it allows us to detect when NULL can be passed into the processing callback for the input + buffer for the purpose of continuous processing. + */ + pNodeBase->cachedFrameCountIn = (ma_uint16)maxFramesReadIn; } else { + /* We don't need to read anything, but we do need to prepare our input frame pointers. */ + for (iInputBus = 0; iInputBus < inputBusCount; iInputBus += 1) { + ppFramesIn[iInputBus] = ma_node_get_cached_input_ptr(pNode, iInputBus) + (pNodeBase->consumedFrameCountIn * ma_node_get_input_channels(pNodeBase, iInputBus)); + } + } + + /* + At this point we have our input data so now we need to do some processing. Sneaky little + optimization here - we can set the pointer to the output buffer for this output bus so + that the final copy into the output buffer is done directly by onProcess(). + */ + if (pFramesOut != NULL) { + ppFramesOut[outputBusIndex] = ma_offset_pcm_frames_ptr_f32(pFramesOut, pNodeBase->cachedFrameCountOut, ma_node_get_output_channels(pNode, outputBusIndex)); + } + + + /* Give the processing function the entire capacity of the output buffer. */ + frameCountOut = (framesToProcessOut - pNodeBase->cachedFrameCountOut); + + /* + We need to treat nodes with continuous processing a little differently. For these ones, + we always want to fire the callback with the requested number of frames, regardless of + pNodeBase->cachedFrameCountIn, which could be 0. Also, we want to check if we can pass + in NULL for the input buffer to the callback. + */ + if ((pNodeBase->vtable->flags & MA_NODE_FLAG_CONTINUOUS_PROCESSING) != 0) { + /* We're using continuous processing. Make sure we specify the whole frame count at all times. */ + frameCountIn = framesToProcessIn; /* Give the processing function as much input data as we've got in the buffer, including any silenced padding from short reads. */ + + if ((pNodeBase->vtable->flags & MA_NODE_FLAG_ALLOW_NULL_INPUT) != 0 && pNodeBase->consumedFrameCountIn == 0 && pNodeBase->cachedFrameCountIn == 0) { + consumeNullInput = MA_TRUE; + } else { + consumeNullInput = MA_FALSE; + } + + /* + Since we're using continuous processing we're always passing in a full frame count + regardless of how much input data was read. If this is greater than what we read as + input, we'll end up with an underflow. We instead need to make sure our cached frame + count is set to the number of frames we'll be passing to the data callback. Not + doing this will result in an underflow when we "consume" the cached data later on. + + Note that this check needs to be done after the "consumeNullInput" check above because + we use the property of cachedFrameCountIn being 0 to determine whether or not we + should be passing in a null pointer to the processing callback for when the node is + configured with MA_NODE_FLAG_ALLOW_NULL_INPUT. + */ + if (pNodeBase->cachedFrameCountIn < (ma_uint16)frameCountIn) { + pNodeBase->cachedFrameCountIn = (ma_uint16)frameCountIn; + } + } else { + frameCountIn = pNodeBase->cachedFrameCountIn; /* Give the processing function as much valid input data as we've got. */ consumeNullInput = MA_FALSE; } /* - Since we're using continuous processing we're always passing in a full frame count - regardless of how much input data was read. If this is greater than what we read as - input, we'll end up with an underflow. We instead need to make sure our cached frame - count is set to the number of frames we'll be passing to the data callback. Not - doing this will result in an underflow when we "consume" the cached data later on. - - Note that this check needs to be done after the "consumeNullInput" check above because - we use the property of cachedFrameCountIn being 0 to determine whether or not we - should be passing in a null pointer to the processing callback for when the node is - configured with MA_NODE_FLAG_ALLOW_NULL_INPUT. + Process data slightly differently depending on whether or not we're consuming NULL + input (checked just above). */ - if (pNodeBase->cachedFrameCountIn < (ma_uint16)frameCountIn) { - pNodeBase->cachedFrameCountIn = (ma_uint16)frameCountIn; + if (consumeNullInput) { + ma_node_process_pcm_frames_internal(pNode, NULL, &frameCountIn, ppFramesOut, &frameCountOut); + } else { + /* + We want to skip processing if there's no input data, but we can only do that safely if + we know that there is no chance of any output frames being produced. If continuous + processing is being used, this won't be a problem because the input frame count will + always be non-0. However, if continuous processing is *not* enabled and input and output + data is processed at different rates, we still need to process that last input frame + because there could be a few excess output frames needing to be produced from cached + data. The `MA_NODE_FLAG_DIFFERENT_PROCESSING_RATES` flag is used as the indicator for + determining whether or not we need to process the node even when there are no input + frames available right now. + */ + if (frameCountIn > 0 || (pNodeBase->vtable->flags & MA_NODE_FLAG_DIFFERENT_PROCESSING_RATES) != 0) { + ma_node_process_pcm_frames_internal(pNode, (const float**)ppFramesIn, &frameCountIn, ppFramesOut, &frameCountOut); /* From GCC: expected 'const float **' but argument is of type 'float **'. Shouldn't this be implicit? Excplicit cast to silence the warning. */ + } } - } else { - frameCountIn = pNodeBase->cachedFrameCountIn; /* Give the processing function as much valid input data as we've got. */ - consumeNullInput = MA_FALSE; - } - /* - Process data slightly differently depending on whether or not we're consuming NULL - input (checked just above). - */ - if (consumeNullInput) { - ma_node_process_pcm_frames_internal(pNode, NULL, &frameCountIn, ppFramesOut, &frameCountOut); - } else { /* - We want to skip processing if there's no input data, but we can only do that safely if - we know that there is no chance of any output frames being produced. If continuous - processing is being used, this won't be a problem because the input frame count will - always be non-0. However, if continuous processing is *not* enabled and input and output - data is processed at different rates, we still need to process that last input frame - because there could be a few excess output frames needing to be produced from cached - data. The `MA_NODE_FLAG_DIFFERENT_PROCESSING_RATES` flag is used as the indicator for - determining whether or not we need to process the node even when there are no input - frames available right now. + Thanks to our sneaky optimization above we don't need to do any data copying directly into + the output buffer - the onProcess() callback just did that for us. We do, however, need to + apply the number of input and output frames that were processed. Note that due to continuous + processing above, we need to do explicit checks here. If we just consumed a NULL input + buffer it means that no actual input data was processed from the internal buffers and we + don't want to be modifying any counters. */ - if (frameCountIn > 0 || (pNodeBase->vtable->flags & MA_NODE_FLAG_DIFFERENT_PROCESSING_RATES) != 0) { - ma_node_process_pcm_frames_internal(pNode, (const float**)ppFramesIn, &frameCountIn, ppFramesOut, &frameCountOut); /* From GCC: expected 'const float **' but argument is of type 'float **'. Shouldn't this be implicit? Excplicit cast to silence the warning. */ + if (consumeNullInput == MA_FALSE) { + pNodeBase->consumedFrameCountIn += (ma_uint16)frameCountIn; + pNodeBase->cachedFrameCountIn -= (ma_uint16)frameCountIn; + } + + /* The cached output frame count is always equal to what we just read. */ + pNodeBase->cachedFrameCountOut += (ma_uint16)frameCountOut; + + /* If we couldn't process any data, we're done. The loop needs to be terminated here or else we'll get stuck in a loop. */ + if (pNodeBase->cachedFrameCountOut == framesToProcessOut || (frameCountOut == 0 && frameCountIn == 0)) { + break; } } - - /* - Thanks to our sneaky optimization above we don't need to do any data copying directly into - the output buffer - the onProcess() callback just did that for us. We do, however, need to - apply the number of input and output frames that were processed. Note that due to continuous - processing above, we need to do explicit checks here. If we just consumed a NULL input - buffer it means that no actual input data was processed from the internal buffers and we - don't want to be modifying any counters. - */ - if (consumeNullInput == MA_FALSE) { - pNodeBase->consumedFrameCountIn += (ma_uint16)frameCountIn; - pNodeBase->cachedFrameCountIn -= (ma_uint16)frameCountIn; - } - - /* The cached output frame count is always equal to what we just read. */ - pNodeBase->cachedFrameCountOut = (ma_uint16)frameCountOut; } else { /* We're not needing to read anything from the input buffer so just read directly from our @@ -87790,7 +87799,7 @@ There are many breaking API changes in this release. have been removed. Instead you should set the encodingFormat variable in the decoder config. - ma_decoder_get_length_in_pcm_frames() has been updated to return a result code and output the length via an output parameter. This makes it consistent with data sources. - - ma_decoder_read_pcm_frames() has been update to return a result code and output the number of + - ma_decoder_read_pcm_frames() has been updated to return a result code and output the number of frames read via an output parameter. - Allocation callbacks must now implement the onRealloc() callback. Previously miniaudio would emulate this in terms of malloc and free, but for simplicity it is now required that allocation