Commit ba3080d0 authored by David Reid's avatar David Reid

Add support for partial processing to nodes.

This includes a leading trimming node to act as a test.
parent b8d04bdd
#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);
}
/* 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 */
#define MINIAUDIO_IMPLEMENTATION
#include "../../../miniaudio.h"
#include "ma_ltrim_node.c"
#include <stdio.h>
#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
......@@ -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);
}
for (;;) {
frameCountOut = 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;
/* 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;
/* 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);
/* 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. */
}
/* 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));
/* 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);
}
maxFramesReadIn = ma_max(maxFramesReadIn, framesRead);
}
/* This was a fresh load of input data so reset our consumption counter. */
pNodeBase->consumedFrameCountIn = 0;
/* 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));
}
}
/*
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.
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().
*/
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));
if (pFramesOut != NULL) {
ppFramesOut[outputBusIndex] = ma_offset_pcm_frames_ptr_f32(pFramesOut, pNodeBase->cachedFrameCountOut, ma_node_get_output_channels(pNode, outputBusIndex));
}
}
/*
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;
}
/* Give the processing function the entire capacity of the output buffer. */
frameCountOut = (framesToProcessOut - pNodeBase->cachedFrameCountOut);
/* Give the processing function the entire capacity of the output buffer. */
frameCountOut = framesToProcessOut;
/*
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. */
/*
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;
}
if ((pNodeBase->vtable->flags & MA_NODE_FLAG_ALLOW_NULL_INPUT) != 0 && pNodeBase->consumedFrameCountIn == 0 && pNodeBase->cachedFrameCountIn == 0) {
consumeNullInput = MA_TRUE;
/*
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;
}
}
/*
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;
/* 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;
}
}
} 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
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment