mirror of
https://github.com/mackron/miniaudio.git
synced 2026-04-23 00:34:03 +02:00
834 lines
49 KiB
C
834 lines
49 KiB
C
/* !!! THIS FILE WILL BE MERGED INTO miniaudio.h WHEN COMPLETE !!! */
|
|
|
|
/*
|
|
EXPERIMENTAL
|
|
============
|
|
Everything in this file is experimental and subject to change. Some stuff isn't yet implemented, in particular spatialization. I've noted some ideas that are
|
|
basically straight off the top of my head - many of these are probably outright wrong or just generally bad ideas.
|
|
|
|
Very simple APIs for spatialization are declared by not yet implemented. They're just placeholders to give myself an idea on some of the API design.
|
|
|
|
The idea is that you have an `ma_engine` object - one per listener. Decoupled from that is the `ma_resource_manager` object. You can have one `ma_resource_manager`
|
|
object to many `ma_engine` objects. This will allow you to share resources between each listener. The `ma_engine` is responsible for the playback of audio from a
|
|
list of data sources. The `ma_resource_manager` is responsible for the actual loading, caching and unloading of those data sources. This decoupling is
|
|
something that I'm really liking right now and will likely stay in place for the final version.
|
|
|
|
You create "sounds" from the engine which represent a sound/voice in the world. You first need to create a sound, and then you need to start it. Sounds do not
|
|
start by default. You can use `ma_engine_play_sound()` to "fire and forget" sounds.
|
|
|
|
Sounds can be allocated to groups called `ma_sound_group`. This is how you can support submixing and is one way you could achieve the kinds of groupings you see
|
|
in games for things like SFX, Music and Voices. Unlike sounds, groups are started by default. When you stop a group, all sounds within that group will be
|
|
stopped atomically. When the group is started again, all sounds attached to the group will also be started, so long as the sound is also marked as started.
|
|
|
|
The creation and deletion of sounds and groups should be thread safe.
|
|
|
|
The engine runs on top of a node graph, and sounds and groups are just nodes within that graph. The output of a sound can be attached to the input of any node
|
|
on the graph. To apply an effect to a sound or group, attach it's output to the input of an effect node. See the Routing Infrastructure section below for
|
|
details on this.
|
|
|
|
The best resource to use when understanding the API is the function declarations for `ma_engine`. I expect you should be able to figure it out! :)
|
|
*/
|
|
#ifndef miniaudio_engine_h
|
|
#define miniaudio_engine_h
|
|
|
|
#ifdef __cplusplus
|
|
extern "C" {
|
|
#endif
|
|
|
|
/*
|
|
Resource Management
|
|
===================
|
|
Many programs will want to manage sound resources for things such as reference counting and
|
|
streaming. This is supported by miniaudio via the `ma_resource_manager` API.
|
|
|
|
The resource manager is mainly responsible for the following:
|
|
|
|
1) Loading of sound files into memory with reference counting.
|
|
2) Streaming of sound data
|
|
|
|
When loading a sound file, the resource manager will give you back a `ma_data_source` compatible
|
|
object called `ma_resource_manager_data_source`. This object can be passed into any
|
|
`ma_data_source` API which is how you can read and seek audio data. When loading a sound file, you
|
|
specify whether or not you want the sound to be fully loaded into memory (and optionally
|
|
pre-decoded) or streamed. When loading into memory, you can also specify whether or not you want
|
|
the data to be loaded asynchronously.
|
|
|
|
The example below is how you can initialize a resource manager using it's default configuration:
|
|
|
|
```c
|
|
ma_resource_manager_config config;
|
|
ma_resource_manager resourceManager;
|
|
|
|
config = ma_resource_manager_config_init();
|
|
result = ma_resource_manager_init(&config, &resourceManager);
|
|
if (result != MA_SUCCESS) {
|
|
ma_device_uninit(&device);
|
|
printf("Failed to initialize the resource manager.");
|
|
return -1;
|
|
}
|
|
```
|
|
|
|
You can configure the format, channels and sample rate of the decoded audio data. By default it
|
|
will use the file's native data format, but you can configure it to use a consistent format. This
|
|
is useful for offloading the cost of data conversion to load time rather than dynamically
|
|
converting a mixing time. To do this, you configure the decoded format, channels and sample rate
|
|
like the code below:
|
|
|
|
```c
|
|
config = ma_resource_manager_config_init();
|
|
config.decodedFormat = device.playback.format;
|
|
config.decodedChannels = device.playback.channels;
|
|
config.decodedSampleRate = device.sampleRate;
|
|
```
|
|
|
|
In the code above, the resource manager will be configured so that any decoded audio data will be
|
|
pre-converted at load time to the device's native data format. If instead you used defaults and
|
|
the data format of the file did not match the device's data format, you would need to convert the
|
|
data at mixing time which may be prohibitive in high-performance and large scale scenarios like
|
|
games.
|
|
|
|
Asynchronicity is achieved via a job system. When an operation needs to be performed, such as the
|
|
decoding of a page, a job will be posted to a queue which will then be processed by a job thread.
|
|
By default there will be only one job thread running, but this can be configured, like so:
|
|
|
|
```c
|
|
config = ma_resource_manager_config_init();
|
|
config.jobThreadCount = MY_JOB_THREAD_COUNT;
|
|
```
|
|
|
|
By default job threads are managed internally by the resource manager, however you can also self
|
|
manage your job threads if, for example, you want to integrate the job processing into your
|
|
existing job infrastructure, or if you simply don't like the way the resource manager does it. To
|
|
do this, just set the job thread count to 0 and process jobs manually. To process jobs, you first
|
|
need to retrieve a job using `ma_resource_manager_next_job()` and then process it using
|
|
`ma_resource_manager_process_job()`:
|
|
|
|
```c
|
|
config = ma_resource_manager_config_init();
|
|
config.jobThreadCount = 0; // Don't manage any job threads internally.
|
|
config.flags = MA_RESOURCE_MANAGER_FLAG_NON_BLOCKING; // Optional. Makes `ma_resource_manager_next_job()` non-blocking.
|
|
|
|
// ... Initialize your custom job threads ...
|
|
|
|
void my_custom_job_thread(...)
|
|
{
|
|
for (;;) {
|
|
ma_resource_manager_job job;
|
|
ma_result result = ma_resource_manager_next_job(pMyResourceManager, &job);
|
|
if (result != MA_SUCCESS) {
|
|
if (result == MA_NOT_DATA_AVAILABLE) {
|
|
// No jobs are available. Keep going. Will only get this if the resource manager was initialized
|
|
// with MA_RESOURCE_MANAGER_FLAG_NON_BLOCKING.
|
|
continue;
|
|
} else if (result == MA_CANCELLED) {
|
|
// MA_RESOURCE_MANAGER_JOB_QUIT was posted. Exit.
|
|
break;
|
|
} else {
|
|
// Some other error occurred.
|
|
break;
|
|
}
|
|
}
|
|
|
|
ma_resource_manager_process_job(pMyResourceManager, &job);
|
|
}
|
|
}
|
|
```
|
|
|
|
In the example above, the `MA_RESOURCE_MANAGER_JOB_QUIT` event is the used as the termination indicator, but you can
|
|
use whatever you would like to terminate the thread. The call to `ma_resource_manager_next_job()`
|
|
is blocking by default, by can be configured to be non-blocking by initializing the resource
|
|
manager with the `MA_RESOURCE_MANAGER_FLAG_NON_BLOCKING` configuration flag.
|
|
|
|
When loading a file, it's sometimes convenient to be able to customize how files are opened and
|
|
read. This can be done by setting `pVFS` member of the resource manager's config:
|
|
|
|
```c
|
|
// Initialize your custom VFS object. See documentation for VFS for information on how to do this.
|
|
my_custom_vfs vfs = my_custom_vfs_init();
|
|
|
|
config = ma_resource_manager_config_init();
|
|
config.pVFS = &vfs;
|
|
```
|
|
|
|
If you do not specify a custom VFS, the resource manager will use the operating system's normal
|
|
file operations. This is default.
|
|
|
|
To load a sound file and create a data source, call `ma_resource_manager_data_source_init()`. When
|
|
loading a sound you need to specify the file path and options for how the sounds should be loaded.
|
|
By default a sound will be loaded synchronously. The returned data source is owned by the caller
|
|
which means the caller is responsible for the allocation and freeing of the data source. Below is
|
|
an example for initializing a data source:
|
|
|
|
```c
|
|
ma_resource_manager_data_source dataSource;
|
|
ma_result result = ma_resource_manager_data_source_init(pResourceManager, pFilePath, flags, &dataSource);
|
|
if (result != MA_SUCCESS) {
|
|
// Error.
|
|
}
|
|
|
|
// ...
|
|
|
|
// A ma_resource_manager_data_source object is compatible with the `ma_data_source` API. To read data, just call
|
|
// the `ma_data_source_read_pcm_frames()` like you would with any normal data source.
|
|
result = ma_data_source_read_pcm_frames(&dataSource, pDecodedData, frameCount, &framesRead);
|
|
if (result != MA_SUCCESS) {
|
|
// Failed to read PCM frames.
|
|
}
|
|
|
|
// ...
|
|
|
|
ma_resource_manager_data_source_uninit(pResourceManager, &dataSource);
|
|
```
|
|
|
|
The `flags` parameter specifies how you want to perform loading of the sound file. It can be a
|
|
combination of the following flags:
|
|
|
|
```
|
|
MA_DATA_SOURCE_STREAM
|
|
MA_DATA_SOURCE_DECODE
|
|
MA_DATA_SOURCE_ASYNC
|
|
```
|
|
|
|
When no flags are specified (set to 0), the sound will be fully loaded into memory, but not
|
|
decoded, meaning the raw file data will be stored in memory, and then dynamically decoded when
|
|
`ma_data_source_read_pcm_frames()` is called. To instead decode the audio data before storing it in
|
|
memory, use the `MA_DATA_SOURCE_DECODE` flag. By default, the sound file will be loaded
|
|
synchronously, meaning `ma_resource_manager_data_source_init()` will only return after the entire
|
|
file has been loaded. This is good for simplicity, but can be prohibitively slow. You can instead
|
|
load the sound asynchronously using the `MA_DATA_SOURCE_ASYNC` flag. This will result in
|
|
`ma_resource_manager_data_source_init()` returning quickly, but no data will be returned by
|
|
`ma_data_source_read_pcm_frames()` until some data is available. When no data is available because
|
|
the asynchronous decoding hasn't caught up, `MA_BUSY` will be returned by
|
|
`ma_data_source_read_pcm_frames()`.
|
|
|
|
For large sounds, it's often prohibitive to store the entire file in memory. To mitigate this, you
|
|
can instead stream audio data which you can do by specifying the `MA_DATA_SOURCE_STREAM` flag. When
|
|
streaming, data will be decoded in 1 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.
|
|
|
|
When loading asynchronously, it can be useful to poll whether or not loading has finished. Use
|
|
`ma_resource_manager_data_source_result()` to determine this. For in-memory sounds, this will
|
|
return `MA_SUCCESS` when the file has been *entirely* decoded. If the sound is still being decoded,
|
|
`MA_BUSY` will be returned. Otherwise, some other error code will be returned if the sound failed
|
|
to load. For streaming data sources, `MA_SUCCESS` will be returned when the first page has been
|
|
decoded and the sound is ready to be played. If the first page is still being decoded, `MA_BUSY`
|
|
will be returned. Otherwise, some other error code will be returned if the sound failed to load.
|
|
|
|
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
|
|
matched up with a call to `ma_resource_manager_data_source_uninit()`. Sometimes it can be useful
|
|
for a program to register self-managed raw audio data and associate it with a file path. Use the
|
|
`ma_resource_manager_register_*()` and `ma_resource_manager_unregister_*()` APIs to do this.
|
|
`ma_resource_manager_register_decoded_data()` is used to associate a pointer to raw, self-managed
|
|
decoded audio data in the specified data format with the specified name. Likewise,
|
|
`ma_resource_manager_register_encoded_data()` is used to associate a pointer to raw self-managed
|
|
encoded audio data (the raw file data) with the specified name. Note that these names need not be
|
|
actual file paths. When `ma_resource_manager_data_source_init()` is called (without the
|
|
`MA_DATA_SOURCE_STREAM` flag), the resource manager will look for these explicitly registered data
|
|
buffers and, if found, will use it as the backing data for the data source. Note that the resource
|
|
manager does *not* make a copy of this data so it is up to the caller to ensure the pointer stays
|
|
valid for it's lifetime. Use `ma_resource_manager_unregister_data()` to unregister the self-managed
|
|
data. It does not make sense to use the `MA_DATA_SOURCE_STREAM` flag with a self-managed data
|
|
pointer. When `MA_DATA_SOURCE_STREAM` is specified, it will try loading the file data through the
|
|
VFS.
|
|
|
|
|
|
Resource Manager Implementation Details
|
|
---------------------------------------
|
|
Resources are managed in two main ways:
|
|
|
|
1) By storing the entire sound inside an in-memory buffer (referred to as a data buffer)
|
|
2) By streaming audio data on the fly (referred to as a data stream)
|
|
|
|
A resource managed data source (`ma_resource_manager_data_source`) encapsulates a data buffer or
|
|
data stream, depending on whether or not the data source was initialized with the
|
|
`MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_STREAM` flag. If so, it will make use of a
|
|
`ma_resource_manager_data_stream` object. Otherwise it will use a `ma_resource_manager_data_buffer`
|
|
object. Both of these objects are data sources which means they can be used with any
|
|
`ma_data_source_*()` API.
|
|
|
|
Another major feature of the resource manager is the ability to asynchronously decode audio files.
|
|
This relieves the audio thread of time-consuming decoding which can negatively affect scalability
|
|
due to the audio thread needing to complete it's work extremely quickly to avoid glitching.
|
|
Asynchronous decoding is achieved through a job system. There is a central multi-producer,
|
|
multi-consumer, lock-free, fixed-capacity job queue. When some asynchronous work needs to be done,
|
|
a job is posted to the queue which is then read by a job thread. The number of job threads can be
|
|
configured for improved scalability, and job threads can all run in parallel without needing to
|
|
worry about the order of execution (how this is achieved is explained below).
|
|
|
|
When a sound is being loaded asynchronously, playback can begin before the sound has been fully
|
|
decoded. This enables the application to start playback of the sound quickly, while at the same
|
|
time allowing to resource manager to keep loading in the background. Since there may be less
|
|
threads than the number of sounds being loaded at a given time, a simple scheduling system is used
|
|
to keep decoding time balanced and fair. The resource manager solves this by splitting decoding
|
|
into chunks called pages. By default, each page is 1 second long. When a page has been decoded, a
|
|
new job will be posted to start decoding the next page. By dividing up decoding into pages, an
|
|
individual sound shouldn't ever delay every other sound from having their first page decoded. Of
|
|
course, when loading many sounds at the same time, there will always be an amount of time required
|
|
to process jobs in the queue so in heavy load situations there will still be some delay. To
|
|
determine if a data source is ready to have some frames read, use
|
|
`ma_resource_manager_data_source_get_available_frames()`. This will return the number of frames
|
|
available starting from the current position.
|
|
|
|
|
|
Data Buffers
|
|
------------
|
|
When the `MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_STREAM` flag is excluded at initialization time, the
|
|
resource manager will try to load the data into an in-memory data buffer. Before doing so, however,
|
|
it will first check if the specified file has already been loaded. If so, it will increment a
|
|
reference counter and just use the already loaded data. This saves both time and memory. A binary
|
|
search tree (BST) is used for storing data buffers as it has good balance between efficiency and
|
|
simplicity. The key of the BST is a 64-bit hash of the file path that was passed into
|
|
`ma_resource_manager_data_source_init()`. The advantage of using a hash is that it saves memory
|
|
over storing the entire path, has faster comparisons, and results in a mostly balanced BST due to
|
|
the random nature of the hash. The disadvantage is that file names are case-sensitive. If this is
|
|
an issue, you should normalize your file names to upper- or lower-case before initializing your
|
|
data sources.
|
|
|
|
When a sound file has not already been loaded and the `MMA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_ASYNC`
|
|
is excluded, the file will be decoded synchronously by the calling thread. There are two options
|
|
for controlling how the audio is stored in the data buffer - encoded or decoded. When the
|
|
`MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_DECODE` option is excluded, the raw file data will be stored
|
|
in memory. Otherwise the sound will be decoded before storing it in memory. Synchronous loading is
|
|
a very simple and standard process of simply adding an item to the BST, allocating a block of
|
|
memory and then decoding (if `MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_DECODE` is specified).
|
|
|
|
When the `MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_ASYNC` flag is specified, loading of the data buffer
|
|
is done asynchronously. In this case, a job is posted to the queue to start loading and then the
|
|
function immediately returns, setting an internal result code to `MA_BUSY`. This result code is
|
|
returned when the program calls `ma_resource_manager_data_source_result()`. When decoding has fully
|
|
completed `MA_RESULT` will be returned. This can be used to know if loading has fully completed.
|
|
|
|
When loading asynchronously, a single job is posted to the queue of the type
|
|
`MA_RESOURCE_MANAGER_JOB_LOAD_DATA_BUFFER_NODE`. This involves making a copy of the file path and
|
|
associating it with job. When the job is processed by the job thread, it will first load the file
|
|
using the VFS associated with the resource manager. When using a custom VFS, it's important that it
|
|
be completely thread-safe because it will be used from one or more job threads at the same time.
|
|
Individual files should only ever be accessed by one thread at a time, however. After opening the
|
|
file via the VFS, the job will determine whether or not the file is being decoded. If not, it
|
|
simply allocates a block of memory and loads the raw file contents into it and returns. On the
|
|
other hand, when the file is being decoded, it will first allocate a decoder on the heap and
|
|
initialize it. Then it will check if the length of the file is known. If so it will allocate a
|
|
block of memory to store the decoded output and initialize it to silence. If the size is unknown,
|
|
it will allocate room for one page. After memory has been allocated, the first page will be
|
|
decoded. If the sound is shorter than a page, the result code will be set to `MA_SUCCESS` and the
|
|
completion event will be signalled and loading is now complete. If, however, there is more to
|
|
decode, a job with the code `MA_RESOURCE_MANAGER_JOB_PAGE_DATA_BUFFER_NODE` is posted. This job
|
|
will decode the next page and perform the same process if it reaches the end. If there is more to
|
|
decode, the job will post another `MA_RESOURCE_MANAGER_JOB_PAGE_DATA_BUFFER_NODE` job which will
|
|
keep on happening until the sound has been fully decoded. For sounds of an unknown length, the
|
|
buffer will be dynamically expanded as necessary, and then shrunk with a final realloc() when the
|
|
end of the file has been reached.
|
|
|
|
|
|
Data Streams
|
|
------------
|
|
Data streams only ever store two pages worth of data for each instance. They are most useful for
|
|
large sounds like music tracks in games that would consume too much memory if fully decoded in
|
|
memory. After every frame from a page has been read, a job will be posted to load the next page
|
|
which is done from the VFS.
|
|
|
|
For data streams, the `MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_ASYNC` flag will determine whether or
|
|
not initialization of the data source waits until the two pages have been decoded. When unset,
|
|
`ma_resource_manager_data_source_init()` will wait until the two pages have been loaded, otherwise
|
|
it will return immediately.
|
|
|
|
When frames are read from a data stream using `ma_resource_manager_data_source_read_pcm_frames()`,
|
|
`MA_BUSY` will be returned if there are no frames available. If there are some frames available,
|
|
but less than the number requested, `MA_SUCCESS` will be returned, but the actual number of frames
|
|
read will be less than the number requested. Due to the asymchronous nature of data streams,
|
|
seeking is also asynchronous. If the data stream is in the middle of a seek, `MA_BUSY` will be
|
|
returned when trying to read frames.
|
|
|
|
When `ma_resource_manager_data_source_read_pcm_frames()` results in a page getting fully consumed
|
|
a job is posted to load the next page. This will be posted from the same thread that called
|
|
`ma_resource_manager_data_source_read_pcm_frames()` which should be lock-free.
|
|
|
|
Data streams are uninitialized by posting a job to the queue, but the function won't return until
|
|
that job has been processed. The reason for this is that the caller owns the data stream object and
|
|
therefore miniaudio needs to ensure everything completes before handing back control to the caller.
|
|
Also, if the data stream is uninitialized while pages are in the middle of decoding, they must
|
|
complete before destroying any underlying object and the job system handles this cleanly.
|
|
|
|
|
|
Job Queue
|
|
---------
|
|
The resource manager uses a job queue which is multi-producer, multi-consumer, lock-free and
|
|
fixed-capacity. The lock-free property of the queue is achieved using the algorithm described by
|
|
Michael and Scott: Nonblocking Algorithms and Preemption-Safe Locking on Multiprogrammed Shared
|
|
Memory Multiprocessors. In order for this to work, only a fixed number of jobs can be allocated and
|
|
inserted into the queue which is done through a lock-free data structure for allocating an index
|
|
into a fixed sized array, with reference counting for mitigation of the ABA problem. The reference
|
|
count is 32-bit.
|
|
|
|
For many types of jobs it's important that they execute in a specific order. In these cases, jobs
|
|
are executed serially. For the resource manager, serial execution of jobs is only required on a
|
|
per-object basis (per data buffer or per data stream). Each of these objects stores an execution
|
|
counter. When a job is posted it is associated with an execution counter. When the job is
|
|
processed, it checks if the execution counter of the job equals the execution counter of the
|
|
owning object and if so, processes the job. If the counters are not equal, the job will be posted
|
|
back onto the job queue for later processing. When the job finishes processing the execution order
|
|
of the main object is incremented. This system means the no matter how many job threads are
|
|
executing, decoding of an individual sound will always get processed serially. The advantage to
|
|
having multiple threads comes into play when loading multiple sounds at the time time.
|
|
*/
|
|
|
|
|
|
/*
|
|
Routing Infrastructure
|
|
======================
|
|
miniaudio's routing infrastructure follows a node graph paradigm. The idea is that you create a
|
|
node whose outputs are attached to inputs of another node, thereby creating a graph. There are
|
|
different types of nodes, with each node in the graph processing input data to produce output,
|
|
which is then fed through the chain. Each node in the graph can apply their own custom effects. At
|
|
the end of the graph is an endpoint which represents the end of the chain and is where the final
|
|
output is ultimately extracted from.
|
|
|
|
Each node has a number of input buses and a number of output buses. An output bus from a node is
|
|
attached to an input bus of another. Multiple nodes can connect their output buses to another
|
|
node's input bus, in which case their outputs will be mixed before processing by the node.
|
|
|
|
Each input bus must be configured to accept the same number of channels, but input buses and output
|
|
buses can each have different channel counts, in which case miniaudio will automatically convert
|
|
the input data to the output channel count before processing. The number of channels of an output
|
|
bus of one node must match the channel count of the input bus it's attached to. The channel counts
|
|
cannot be changed after the node has been initialized. If you attempt to attach an output bus to
|
|
an input bus with a different channel count, attachment will fail.
|
|
|
|
To use a node graph, you first need to initialize a `ma_node_graph` object. This is essentially a
|
|
container around the entire graph. The `ma_node_graph` object is required for some thread-safety
|
|
issues which will be explained later. A `ma_node_graph` object is initialized using miniaudio's
|
|
standard config/init system:
|
|
|
|
```c
|
|
ma_node_graph_config nodeGraphConfig = ma_node_graph_config_init(myChannelCount);
|
|
|
|
result = ma_node_graph_init(&nodeGraphConfig, NULL, &nodeGraph); // Second parameter is a pointer to allocation callbacks.
|
|
if (result != MA_SUCCESS) {
|
|
// Failed to initialize node graph.
|
|
}
|
|
```
|
|
|
|
When you initialize the node graph, you're specifying the channel count of the endpoint. The
|
|
endpoint is a special node which has one input bus and one output bus, both of which have the
|
|
same channel count, which is specified in the config. Any nodes that connect directly to the
|
|
endpoint must be configured such that their output buses have the same channel count. When you read
|
|
audio data from the node graph, it'll have the channel count you specified in the config. To read
|
|
data from the graph:
|
|
|
|
```c
|
|
ma_uint32 framesRead;
|
|
result = ma_node_graph_read_pcm_frames(&nodeGraph, pFramesOut, frameCount, &framesRead);
|
|
if (result != MA_SUCCESS) {
|
|
// Failed to read data from the node graph.
|
|
}
|
|
```
|
|
|
|
When you read audio data, miniaudio starts at the node graph's endpoint node which then pulls in
|
|
data from it's input attachments, which in turn recusively pull in data from their inputs, and so
|
|
on. At the very base of the graph there will be some kind of data source node which will have zero
|
|
inputs and will instead read directly from a data source. The base nodes don't literally need to
|
|
read from a `ma_data_source` object, but they will always have some kind of underlying object that
|
|
sources some kind of audio. The `ma_data_source_node` node can be used to read from a
|
|
`ma_data_source`. Data is always in floating-point format and in the number of channels you
|
|
specified when the graph was initialized. The sample rate is defined by the underlying data sources.
|
|
It's up to you to ensure they use a consistent and appropraite sample rate.
|
|
|
|
The `ma_node` API is designed to allow custom nodes to be implemented with relative ease, but
|
|
miniaudio includes a few stock nodes for common functionality. This is how you would initialize a
|
|
node which reads directly from a data source (`ma_data_source_node`) which is an example of one
|
|
of the stock nodes that comes with miniaudio:
|
|
|
|
```c
|
|
ma_data_source_node_config config = ma_data_source_node_config_init(pMyDataSource, isLooping);
|
|
|
|
ma_data_source_node dataSourceNode;
|
|
result = ma_data_source_node_init(&nodeGraph, &config, NULL, &dataSourceNode);
|
|
if (result != MA_SUCCESS) {
|
|
// Failed to create data source node.
|
|
}
|
|
```
|
|
|
|
The data source node will use the output channel count to determine the channel count of the output
|
|
bus. There will be 1 output bus and 0 input buses (data will be drawn directly from the data
|
|
source). The data source must output to floating-point (ma_format_f32) or else an error will be
|
|
returned from `ma_data_source_node_init()`.
|
|
|
|
By default the node will not be attached to the graph. To do so, use `ma_node_attach_output_bus()`:
|
|
|
|
```c
|
|
result = ma_node_attach_output_bus(&dataSourceNode, 0, ma_node_graph_get_endpoint(&nodeGraph), 0);
|
|
if (result != MA_SUCCESS) {
|
|
// Failed to attach node.
|
|
}
|
|
```
|
|
|
|
The code above connects the data source node directly to the endpoint. Since the data source node
|
|
has only a single output bus, the index will always be 0. Likewise, the endpoint only has a single
|
|
input bus which means the input bus index will also always be 0.
|
|
|
|
To detach a specific output bus, use `ma_node_detach_output_bus()`. To detach all output buses, use
|
|
`ma_node_detach_all_output_buses()`. If you want to just move the output bus from one attachment to
|
|
another, you do not need to detach first. You can just call `ma_node_attach_output_bus()` and it'll
|
|
deal with it for you.
|
|
|
|
Less frequently you may want to create a specialized node. This will be a node where you implement
|
|
your own processing callback to apply a custom effect of some kind. This is similar to initalizing
|
|
one of the stock node types, only this time you need to specify a pointer to a vtable containing a
|
|
pointer to the processing function and the number of input and output buses. Example:
|
|
|
|
```c
|
|
static void my_custom_node_process_pcm_frames(ma_node* pNode, const float** ppFramesIn, ma_uint32* pFrameCountIn, float** ppFramesOut, ma_uint32* pFrameCountOut)
|
|
{
|
|
// Do some processing of ppFramesIn (one stream of audio data per input bus)
|
|
const float* pFramesIn_0 = ppFramesIn[0]; // Input bus @ index 0.
|
|
const float* pFramesIn_1 = ppFramesIn[1]; // Input bus @ index 1.
|
|
float* pFramesOut_0 = ppFramesOut[0]; // Output bus @ index 0.
|
|
|
|
// Do some processing. On input, `pFrameCountIn` will be the number of input frames in each
|
|
// buffer in `ppFramesIn` and `pFrameCountOut` will be the capacity of each of the buffers
|
|
// in `ppFramesOut`. On output, `pFrameCountIn` should be set to the number of input frames
|
|
// your node consumed and `pFrameCountOut` should be set the number of output frames that
|
|
// were produced.
|
|
//
|
|
// You should process as many frames as you can. If your effect consumes input frames at the
|
|
// same rate as output frames (always the case, unless you're doing resampling), you need
|
|
// only look at `ppFramesOut` and process that exact number of frames. If you're doing
|
|
// resampling, you'll need to be sure to set both `pFrameCountIn` and `pFrameCountOut`
|
|
// properly.
|
|
}
|
|
|
|
static ma_node_vtable my_custom_node_vtable =
|
|
{
|
|
my_custom_node_process_pcm_frames, // The function that will be called process your custom node. This is where you'd implement your effect processing.
|
|
NULL, // Optional. A callback for calculating the number of input frames that are required to process a specified number of output frames.
|
|
2, // 2 input buses.
|
|
1, // 1 output bus.
|
|
0 // Default flags.
|
|
};
|
|
|
|
...
|
|
|
|
// Each bus needs to have a channel count specified. To do this you need to specify the channel
|
|
// counts in an array and then pass that into the node config.
|
|
ma_uint32 inputChannels[2]; // Equal in size to the number of input channels specified in the vtable.
|
|
ma_uint32 outputChannels[1]; // Equal in size to the number of output channels specicied in the vtable.
|
|
|
|
inputChannels[0] = channelsIn;
|
|
inputChannels[1] = channelsIn;
|
|
outputChannels[0] = channelsOut;
|
|
|
|
ma_node_config nodeConfig = ma_node_config_init();
|
|
nodeConfig.vtable = &my_custom_node_vtable;
|
|
nodeConfig.pInputChannels = inputChannels;
|
|
nodeConfig.pOutputChannels = outputChannels;
|
|
|
|
ma_node_base node;
|
|
result = ma_node_init(&nodeGraph, &nodeConfig, NULL, &node);
|
|
if (result != MA_SUCCESS) {
|
|
// Failed to initialize node.
|
|
}
|
|
```
|
|
|
|
When initializing a custom node, as in the code above, you'll normally just place your vtable in
|
|
static space. The number of input and output buses are specified as part of the vtable. If you need
|
|
a variable number of buses on a per-node bases, the vtable should have the relevant bus count set
|
|
to `MA_NODE_BUS_COUNT_UNKNOWN`. In this case, the bus count should be set in the node config:
|
|
|
|
```c
|
|
static ma_node_vtable my_custom_node_vtable =
|
|
{
|
|
my_custom_node_process_pcm_frames, // The function that will be called process your custom node. This is where you'd implement your effect processing.
|
|
NULL, // Optional. A callback for calculating the number of input frames that are required to process a specified number of output frames.
|
|
MA_NODE_BUS_COUNT_UNKNOWN, // The number of input buses is determined on a per-node basis.
|
|
1, // 1 output bus.
|
|
0 // Default flags.
|
|
};
|
|
|
|
...
|
|
|
|
ma_node_config nodeConfig = ma_node_config_init();
|
|
nodeConfig.vtable = &my_custom_node_vtable;
|
|
nodeConfig.inputBusCount = myBusCount; // <-- Since the vtable specifies MA_NODE_BUS_COUNT_UNKNOWN, the input bus count should be set here.
|
|
nodeConfig.pInputChannels = inputChannels; // <-- Make sure there are nodeConfig.inputBusCount elements in this array.
|
|
nodeConfig.pOutputChannels = outputChannels; // <-- The vtable specifies 1 output bus, so there must be 1 element in this array.
|
|
```
|
|
|
|
In the above example it's important to never set the `inputBusCount` and `outputBusCount` members
|
|
to anything other than their defaults if the vtable specifies an explicit count. They can only be
|
|
set if the vtable specifies MA_NODE_BUS_COUNT_UNKNOWN in the relevant bus count.
|
|
|
|
Most often you'll want to create a structure to encapsulate your node with some extra data. You
|
|
need to make sure the `ma_node_base` object is your first member of the structure:
|
|
|
|
```c
|
|
typedef struct
|
|
{
|
|
ma_node_base base; // <-- Make sure this is always the first member.
|
|
float someCustomData;
|
|
} my_custom_node;
|
|
```
|
|
|
|
By doing this, your object will be compatible with all `ma_node` APIs and you can attach it to the
|
|
graph just like any other node.
|
|
|
|
In the custom processing callback (`my_custom_node_process_pcm_frames()` in the example above), the
|
|
number of channels for each bus is what was specified by the config when the node was initialized
|
|
with `ma_node_init()`. In addition, all attachments to each of the input buses will have been
|
|
pre-mixed by miniaudio. The config allows you to specify different channel counts for each
|
|
individual input and output bus. It's up to the effect to handle it appropriate, and if it can't,
|
|
return an error in it's initialization routine.
|
|
|
|
Custom nodes can be assigned some flags to describe their behaviour. These are set via the vtable
|
|
and include the following:
|
|
|
|
+-----------------------------------------+---------------------------------------------------+
|
|
| Flag Name | Description |
|
|
+-----------------------------------------+---------------------------------------------------+
|
|
| MA_NODE_FLAG_PASSTHROUGH | Useful for nodes that do not do any kind of audio |
|
|
| | processing, but are instead used for tracking |
|
|
| | time, handling events, etc. Also used by the |
|
|
| | internal endpoint node. It reads directly from |
|
|
| | the input bus to the output bus. Nodes with this |
|
|
| | flag must have exactly 1 input bus and 1 output |
|
|
| | bus, and both buses must have the same channel |
|
|
| | counts. |
|
|
+-----------------------------------------+---------------------------------------------------+
|
|
| MA_NODE_FLAG_CONTINUOUS_PROCESSING | Causes the processing callback to be called even |
|
|
| | when no data is available to be read from input |
|
|
| | attachments. This is useful for effects like |
|
|
| | echos where there will be a tail of audio data |
|
|
| | that still needs to be processed even when the |
|
|
| | original data sources have reached their ends. |
|
|
+-----------------------------------------+---------------------------------------------------+
|
|
| MA_NODE_FLAG_ALLOW_NULL_INPUT | Used in conjunction with |
|
|
| | `MA_NODE_FLAG_CONTINUOUS_PROCESSING`. When this |
|
|
| | is set, the `ppFramesIn` parameter of the |
|
|
| | processing callback will be set to NULL when |
|
|
| | there are no input frames are available. When |
|
|
| | this is unset, silence will be posted to the |
|
|
| | processing callback. |
|
|
+-----------------------------------------+---------------------------------------------------+
|
|
| MA_NODE_FLAG_DIFFERENT_PROCESSING_RATES | Used to tell miniaudio that input and output |
|
|
| | frames are processed at different rates. You |
|
|
| | should set this for any nodes that perform |
|
|
| | resampling. |
|
|
+-----------------------------------------+---------------------------------------------------+
|
|
|
|
|
|
If you need to make a copy of an audio stream for effect processing you can use a splitter node
|
|
called `ma_splitter_node`. This takes has 1 input bus and splits the stream into 2 output buses.
|
|
You can use it like this:
|
|
|
|
```c
|
|
ma_splitter_node_config splitterNodeConfig = ma_splitter_node_config_init(channelsIn, channelsOut);
|
|
|
|
ma_splitter_node splitterNode;
|
|
result = ma_splitter_node_init(&nodeGraph, &splitterNodeConfig, NULL, &splitterNode);
|
|
if (result != MA_SUCCESS) {
|
|
// Failed to create node.
|
|
}
|
|
|
|
// Attach your output buses to two different input buses (can be on two different nodes).
|
|
ma_node_attach_output_bus(&splitterNode, 0, ma_node_graph_get_endpoint(&nodeGraph), 0); // Attach directly to the endpoint.
|
|
ma_node_attach_output_bus(&splitterNode, 1, &myEffectNode, 0); // Attach to input bus 0 of some effect node.
|
|
```
|
|
|
|
The volume of an output bus can be configured on a per-bus basis:
|
|
|
|
```c
|
|
ma_node_set_output_bus_volume(&splitterNode, 0, 0.5f);
|
|
ma_node_set_output_bus_volume(&splitterNode, 1, 0.5f);
|
|
```
|
|
|
|
In the code above we're using the splitter node from before and changing the volume of each of the
|
|
copied streams.
|
|
|
|
You can start, stop and mute a node with the following:
|
|
|
|
```c
|
|
ma_node_set_state(&splitterNode, ma_node_state_started); // The default state.
|
|
ma_node_set_state(&splitterNode, ma_node_state_stopped);
|
|
ma_node_set_state(&splitterNode, ma_node_state_muted);
|
|
```
|
|
|
|
By default the node is in a started state, but since it won't be connected to anything won't
|
|
actually be invoked by the node graph until it's connected. When you stop a node, data will not be
|
|
read from any of it's input connections. You can use this property to stop a group of sounds
|
|
atomically.
|
|
|
|
You can configure the initial state of a node in it's config:
|
|
|
|
```c
|
|
nodeConfig.initialState = ma_node_state_stopped;
|
|
```
|
|
|
|
Note that for the stock specialized nodes, all of their configs will have a `nodeConfig` member
|
|
which is the config to use with the base node. This is where the initial state can be configured
|
|
for specialized nodes:
|
|
|
|
```c
|
|
dataSourceNodeConfig.nodeConfig.initialState = ma_node_state_stopped;
|
|
```
|
|
|
|
When using a specialized node like `ma_data_source_node` or `ma_splitter_node`, be sure to not
|
|
modify the `vtable` member of the `nodeConfig` object.
|
|
|
|
|
|
Timing
|
|
------
|
|
The node graph supports starting and stopping nodes at scheduled times. This is especially useful
|
|
for data source nodes where you want to get the node set up, but only start playback at a specific
|
|
time. There are two clocks: local and global.
|
|
|
|
A local clock is per-node, whereas the global clock is per graph. Scheduling starts and stops can
|
|
only be done based on the global clock because the local clock will not be running while the node
|
|
is stopped. The global clocks advances whenever `ma_node_graph_read_pcm_frames()` is called. On the
|
|
other hand, the local clock only advances when the node's processing callback is fired, and is
|
|
advanced based on the output frame count.
|
|
|
|
To retrieve the global time, use `ma_node_graph_get_time()`. The global time can be set with
|
|
`ma_node_graph_set_time()` which might be useful if you want to do seeking on a global timeline.
|
|
Getting and setting the local time is similar. Use `ma_node_get_time()` to retrieve the local time,
|
|
and `ma_node_set_time()` to set the local time. The global and local times will be advanced by the
|
|
audio thread, so care should be taken to avoid data races. Ideally you should avoid calling these
|
|
outside of the node processing callbacks which are always run on the audio thread.
|
|
|
|
There is basic support for scheduling the starting and stopping of nodes. You can only schedule one
|
|
start and one stop at a time. This is mainly intended for putting nodes into a started or stopped
|
|
state in a frame-exact manner. Without this mechanism, starting and stopping of a node is limited
|
|
to the resolution of a call to `ma_node_graph_read_pcm_frames()` which would typically be in blocks
|
|
of several milliseconds. The following APIs can be used for scheduling node states:
|
|
|
|
```c
|
|
ma_node_set_state_time()
|
|
ma_node_get_state_time()
|
|
```
|
|
|
|
The time is absolute and must be based on the global clock. An example is below:
|
|
|
|
```c
|
|
ma_node_set_state_time(&myNode, ma_node_state_started, sampleRate*1); // Delay starting to 1 second.
|
|
ma_node_set_state_time(&myNode, ma_node_state_stopped, sampleRate*5); // Delay stopping to 5 seconds.
|
|
```
|
|
|
|
An example for changing the state using a relative time.
|
|
|
|
```c
|
|
ma_node_set_state_time(&myNode, ma_node_state_started, sampleRate*1 + ma_node_graph_get_time(&myNodeGraph));
|
|
ma_node_set_state_time(&myNode, ma_node_state_stopped, sampleRate*5 + ma_node_graph_get_time(&myNodeGraph));
|
|
```
|
|
|
|
Note that due to the nature of multi-threading the times may not be 100% exact. If this is an
|
|
issue, consider scheduling state changes from within a processing callback. An idea might be to
|
|
have some kind of passthrough trigger node that is used specifically for tracking time and handling
|
|
events.
|
|
|
|
|
|
|
|
Thread Safety and Locking
|
|
-------------------------
|
|
When processing audio, it's ideal not to have any kind of locking in the audio thread. Since it's
|
|
expected that `ma_node_graph_read_pcm_frames()` would be run on the audio thread, it does so
|
|
without the use of any locks. This section discusses the implementation used by miniaudio and goes
|
|
over some of the compromises employed by miniaudio to achieve this goal. Note that the current
|
|
implementation may not be ideal - feedback and critiques are most welcome.
|
|
|
|
The node graph API is not *entirely* lock-free. Only `ma_node_graph_read_pcm_frames()` is expected
|
|
to be lock-free. Attachment, detachment and uninitialization of nodes use locks to simplify the
|
|
implementation, but are crafted in a way such that such locking is not required when reading audio
|
|
data from the graph. Locking in these areas are achieved by means of spinlocks.
|
|
|
|
The main complication with keeping `ma_node_graph_read_pcm_frames()` lock-free stems from the fact
|
|
that a node can be uninitialized, and it's memory potentially freed, while in the middle of being
|
|
processed on the audio thread. There are times when the audio thread will be referencing a node,
|
|
which means the uninitialization process of a node needs to make sure it delays returning until the
|
|
audio thread is finished so that control is not handed back to the caller thereby giving them a
|
|
chance to free the node's memory.
|
|
|
|
When the audio thread is processing a node, it does so by reading from each of the output buses of
|
|
the node. In order for a node to process data for one of it's output buses, it needs to read from
|
|
each of it's input buses, and so on an so forth. It follows that once all output buses of a node
|
|
are detached, the node as a whole will be disconnected and no further processing will occur unless
|
|
it's output buses are reattached, which won't be happening when the node is being uninitialized.
|
|
By having `ma_node_detach_output_bus()` wait until the audio thread is finished with it, we can
|
|
simplify a few things, at the expense of making `ma_node_detach_output_bus()` a bit slower. By
|
|
doing this, the implementation of `ma_node_uninit()` becomes trivial - just detach all output
|
|
nodes, followed by each of the attachments to each of it's input nodes, and then do any final clean
|
|
up.
|
|
|
|
With the above design, the worst-case scenario is `ma_node_detach_output_bus()` taking as long as
|
|
it takes to process the output bus being detached. This will happen if it's called at just the
|
|
wrong moment where the audio thread has just iterated it and has just started processing. The
|
|
caller of `ma_node_detach_output_bus()` will stall until the audio thread is finished, which
|
|
includes the cost of recursively processing it's inputs. This is the biggest compromise made with
|
|
the approach taken by miniaudio for it's lock-free processing system. The cost of detaching nodes
|
|
earlier in the pipeline (data sources, for example) will be cheaper than the cost of detaching
|
|
higher level nodes, such as some kind of final post-processing endpoint. If you need to do mass
|
|
detachments, detach starting from the lowest level nodes and work your way towards the final
|
|
endpoint node (but don't try detaching the node graph's endpoint). If the audio thread is not
|
|
running, detachment will be fast and detachment in any order will be the same. The reason nodes
|
|
need to wait for their input attachments to complete is due to the potential for desyncs between
|
|
data sources. If the node was to terminate processing mid way through processing it's inputs,
|
|
there's a chance that some of the underlying data sources will have been read, but then others not.
|
|
That will then result in a potential desynchronization when detaching and reattaching higher-level
|
|
nodes. A possible solution to this is to have an option when detaching to terminate processing
|
|
before processing all input attachments which should be fairly simple.
|
|
|
|
Another compromise, albeit less significant, is locking when attaching and detaching nodes. This
|
|
locking is achieved by means of a spinlock in order to reduce memory overhead. A lock is present
|
|
for each input bus and output bus. When an output bus is connected to an input bus, both the output
|
|
bus and input bus is locked. This locking is specifically for attaching and detaching across
|
|
different threads and does not affect `ma_node_graph_read_pcm_frames()` in any way. The locking and
|
|
unlocking is mostly self-explanatory, but a slightly less intuitive aspect comes into it when
|
|
considering that iterating over attachments must not break as a result of attaching or detaching a
|
|
node while iteration is occuring.
|
|
|
|
Attaching and detaching are both quite simple. When an output bus of a node is attached to an input
|
|
bus of another node, it's added to a linked list. Basically, an input bus is a linked list, where
|
|
each item in the list is and output bus. We have some intentional (and convenient) restrictions on
|
|
what can done with the linked list in order to simplify the implementation. First of all, whenever
|
|
something needs to iterate over the list, it must do so in a forward direction. Backwards iteration
|
|
is not supported. Also, items can only be added to the start of the list.
|
|
|
|
The linked list is a doubly-linked list where each item in the list (an output bus) holds a pointer
|
|
to the next item in the list, and another to the previous item. A pointer to the previous item is
|
|
only required for fast detachment of the node - it is never used in iteration. This is an
|
|
important property because it means from the perspective of iteration, attaching and detaching of
|
|
an item can be done with a single atomic assignment. This is exploited by both the attachment and
|
|
detachment process. When attaching the node, the first thing that is done is the setting of the
|
|
local "next" and "previous" pointers of the node. After that, the item is "attached" to the list
|
|
by simply performing an atomic exchange with the head pointer. After that, the node is "attached"
|
|
to the list from the perspective of iteration. Even though the "previous" pointer of the next item
|
|
hasn't yet been set, from the perspective of iteration it's been attached because iteration will
|
|
only be happening in a forward direction which means the "previous" pointer won't actually ever get
|
|
used. The same general process applies to detachment. See `ma_node_attach_output_bus()` and
|
|
`ma_node_detach_output_bus()` for the implementation of this mechanism.
|
|
*/
|
|
|
|
|
|
|
|
|
|
/*
|
|
Engine
|
|
======
|
|
The `ma_engine` API is a high-level API for audio playback. Internally it contains sounds (`ma_sound`) with resources managed via a resource manager
|
|
(`ma_resource_manager`).
|
|
|
|
Within the world there is the concept of a "listener". Each `ma_engine` instances has a single listener, but you can instantiate multiple `ma_engine` instances
|
|
if you need more than one listener. In this case you will want to share a resource manager which you can do by initializing one manually and passing it into
|
|
`ma_engine_config`. Using this method will require your application to manage groups and sounds on a per `ma_engine` basis.
|
|
*/
|
|
|
|
|
|
#ifdef __cplusplus
|
|
}
|
|
#endif
|
|
#endif /* miniaudio_engine_h */
|
|
|
|
|
|
#if defined(MA_IMPLEMENTATION) || defined(MINIAUDIO_IMPLEMENTATION)
|
|
|
|
#endif
|