/* Currently an explicit start is required. Perhaps make it so this is started by default, or maybe start it when the first sound is started? Maybe make it an option? */
result=ma_engine_start(&engine);/* Do we want the engine to be started by default? */
if(result!=MA_SUCCESS){
ma_engine_uninit(&engine);
return(int)result;
}
/* We can load our resource after starting the engine - the engine will deal with loading everything properly. */
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
caching system outlined in the resource manager are just ideas off the top of my head. This will almost certainly change. The resource manager currently just
naively allocates ma_decoder objects on the heap and streams them from the disk. No caching or background loading is going on here - I just want to get some
API ideas written and a prototype up and and running.
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 for 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 play it. Sounds do not
play by default. A placeholder helper API called ma_engine_play_sound() exists, but is not yet implemented. This will just play a sound in-place which will be
memory managed by the `ma_engine` object. Sounds can have an effect (`ma_effect`) applied to it which can be set with `ma_engine_sound_set_effect()`.
Sounds can be allocated to groups called `ma_sound_group`. The creation and deletion of groups is not thread safe and should usually happen at initialization
time. Groups are how you handle submixing. In many games you will see settings to control the master volume in addition to groups, usually called SFX, Music
and Voices. The `ma_sound_group` object is how you would achieve this via the `ma_engine` API. When a sound is created you need to specify the group it should
be associated with. The sound's group cannot be changed after it has been created.
The creation and deletion of sounds should, hopefully, be thread safe. I have not yet done thorough testing on this, so there's a good chance there may be some
subtle bugs there.
Some things haven't yet been fully decided on. The following things in particular are some of the things I'm considering. If you have any opinions, feel free
to send me a message and give me your opinions/advice:
- You need to explicitly start playback with `ma_engine_start()`. I'm considering making the default behaviour cause it to auto-start when the first sound
is started. The question then is do we automatically stop it when the last sound is stopped? If so, would we still auto-stop it if the user explicitly
called `ma_engine_start()`?
- I haven't yet got spatialization working. I'm expecting it may be required to use an acceleration structure for querying audible sounds and only mixing
those which can be heard by the listener, but then that will cause problems in the mixing thread because that should, ideally, not have any locking.
- No caching or background loading is implemented in the resource manager. This is planned.
- Sound groups can have an effect applied to them before being mixed with the parent group, but I'm considering making it so the effect is not allowed to
have resampling enabled thereby simplifying memory management between parent and child groups.
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! :)
*/
/*
Resource Management
===================
Resources are managed via the `ma_resource_manager` API.
At it's core, the resource manager is responsible for the loading and caching of audio data. There are two types of audio data: encoded and decoded. Encoded
audio data is the raw contents of an audio file on disk. Decoded audio data is raw, uncompressed PCM audio data. Both encoded and decoded audio data are
associated with a name for purpose of instancing. The idea is that you have one chunk of encoded or decoded audio data to many `ma_data_source` objects. In
this case, the `ma_data_source` object is the instance.
There are three levels of storage, in order of speed:
1) Decoded/Uncompressed Cache
2) Encoded/Compressed Cache
3) Disk (accessed via a VFS)
Whenever a sound is played, it should usually be loaded into one of the in-memory caches (level 1 or 2).
*/
#ifndef miniaudio_engine_h
#define miniaudio_engine_h
#include "ma_mixing.h"
#ifdef __cplusplus
extern"C"{
#endif
typedefstruct
{
ma_uint64sizeInFrames;
void*pData;
}ma_decoded_data;
typedefstruct
{
size_tsizeInBytes;
void*pData;
}ma_encoded_data;
typedefstruct
{
size_tlevel1CacheSizeInBytes;/* Set to 0 to disable level 1 cache. Set to (size_t)-1 for unlimited. */
size_tlevel2CacheSizeInBytes;/* Set to 0 to disable level 2 cache. Set to (size_t)-1 for unlimited. */
The `ma_engine` API is a high-level API for audio playback. Internally it contains a world of 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.
*/
typedefstructma_enginema_engine;
typedefstructma_soundma_sound;
typedefstructma_sound_groupma_sound_group;
typedefstruct
{
floatx;
floaty;
floatz;
}ma_vec3;
typedefstruct
{
floatx;
floaty;
floatz;
floatw;
}ma_quat;
structma_sound
{
ma_data_source*pDataSource;
floatvolume;
ma_effect*pEffect;
ma_vec3position;
ma_quatrotation;/* For directional audio. */
ma_sound_group*pGroup;/* The group the sound is attached to. */
volatilema_sound*pPrevSoundInGroup;/* Marked as volatile because we need to be very explicit with when we make copies of this and we can't have the compiler optimize it out. */
volatilema_sound*pNextSoundInGroup;/* Marked as volatile because we need to be very explicit with when we make copies of this and we can't have the compiler optimize it out. */
volatilema_bool32isPlaying;/* False by default. Sounds need to be explicitly started with ma_engine_sound_start() and stopped with ma_engine_sound_stop(). */
volatilema_bool32isMixing;
ma_bool32ownsDataSource;
ma_bool32isSpatial;/* Set the false by default. When set to false, with not have spatialisation applied. */
ma_bool32isLooping;/* False by default. */
};
structma_sound_group
{
ma_sound_group*pParent;
ma_sound_group*pFirstChild;
ma_sound_group*pPrevSibling;
ma_sound_group*pNextSibling;
volatilema_sound*pFirstSoundInGroup;/* Marked as volatile because we need to be very explicit with when we make copies of this and we can't have the compiler optimize it out. */
ma_mixermixer;
ma_mutexlock;/* Only used by ma_engine_create_sound_*() and ma_engine_delete_sound(). Not used in the mixing thread. */
ma_bool32isPlaying;/* True by default. Sound groups can be stopped with ma_engine_sound_stop() and resumed with ma_engine_sound_start(). Also affects children. */
};
typedefstructma_listenerma_listener;
structma_listener
{
ma_devicedevice;/* The playback device associated with this listener. */
ma_pcm_rbfixedRB;/* The intermediary ring buffer for helping with fixed sized updates. */
ma_vec3position;
ma_quatrotation;
};
typedefstruct
{
ma_resource_manager*pResourceManager;/* Can be null in which case a resource manager will be created for you. */
ma_formatformat;/* The format to use when mixing and spatializing. When set to 0 will use the native format of the device. */
ma_uint32channels;/* The number of channels to use when mixing and spatializing. When set to 0, will use the native channel count of the device. */
ma_uint32sampleRate;/* The sample rate. When set to 0 will use the native channel count of the device. */
ma_uint32periodSizeInFrames;
ma_uint32periodSizeInMilliseconds;/* Updates will always be exactly this size. The underlying device may be a different size, but from the perspective of the mixer that won't matter. */
ma_device_id*pPlaybackDeviceID;/* The ID of the playback device to use with the default listener. */
MA_APIma_resultma_engine_play_sound(ma_engine*pEngine,constchar*pFilePath,ma_sound_group*pGroup);/* Not yet implemented. */
MA_APIma_resultma_engine_sound_group_init(ma_engine*pEngine,ma_sound_group*pParentGroup,ma_sound_group*pGroup);/* Parent must be set at initialization time and cannot be changed. Not thread-safe. */
MA_APIvoidma_engine_sound_group_uninit(ma_engine*pEngine,ma_sound_group*pGroup);/* Not thread-safe. */
deviceConfig.noPreZeroedOutputBuffer=MA_TRUE;/* We'll always be outputting to every frame in the callback so there's no need for a pre-silenced buffer. */
deviceConfig.noClip=MA_TRUE;/* The mixing engine here will do clipping for us. */
/* With the device initialized we need an intermediary buffer for handling fixed sized updates. Currently using a ring buffer for this, but can probably use something a bit more optimal. */
/* With the context create we can now create the default listener. After we have the listener we can set the format, channels and sample rate appropriately. */
returnresult;/* Failed to initialize default listener. */
}
/* Now that have the default listener we can ensure we have the format, channels and sample rate set to proper values to ensure future listeners are configured consistently. */
/* We need to tell the engine that we own the data source so that it knows to delete it when deleting the sound. This needs to be done after creating the sound with ma_engine_create_sound_from_data_source(). */
The sound should never be in a playing state when this is called. It *can*, however, but in the middle of mixing in the mixing thread. It needs to finish
mixing before being uninitialized completely, but that is done at a higher level to this function.
*/
MA_ASSERT(pSound->isPlaying==MA_FALSE);
/*
We want the creation and deletion of sounds to be supported across multiple threads. An application may have any thread that want's to call
ma_engine_play_sound(), for example. The application would expect this to just work. The problem, however, is that the mixing thread will be iterating over
the list at the same time. We need to be careful with how we remove a sound from the list because we'll essentially be taking the sound out from under the
mixing thread and the mixing thread must continue to work. Normally you would wrap the iteration in a lock as well, however an added complication is that
the mixing thread cannot be locked as it's running on the audio thread, and locking in the audio thread is a no-no).
To start with, ma_engine_sound_detach() (this function) and ma_engine_sound_attach() need to be wrapped in a lock. This lock will *not* be used by the
mixing thread. We therefore need to craft this in a very particular way so as to ensure the mixing thread does not lose track of it's iteration state. What
we don't want to do is clear the pNextSoundInGroup variable to NULL. This need to be maintained to ensure the mixing thread can continue iteration even
after the sound has been removed from the group. This is acceptable because sounds are fixed to their group for the entire life, and this function will
only ever be called when the sound is being uninitialized which therefore means it'll never be iterated again.
*/
ma_mutex_lock(&pGroup->lock);
{
if(pSound->pPrevSoundInGroup==NULL){
/* The sound is the head of the list. All we need to do is change the pPrevSoundInGroup member of the next sound to NULL and make it the new head. */
/* Make sure the sound is stopped as soon as possible to reduce the chance that it gets locked by the mixer. We also need to stop it before detaching from the group. */
result=ma_engine_sound_stop(pEngine,pSound);
if(result!=MA_SUCCESS){
return;
}
/* The sound needs to removed from the group to ensure it doesn't get iterated again and cause things to break again. This is thread-safe. */
result=ma_engine_sound_detach(pEngine,pSound);
if(result!=MA_SUCCESS){
return;
}
/*
The sound is detached from the group, but it may still be in the middle of mixing which means our data source is locked. We need to wait for
this to finish before deleting from the resource manager.
We could define this so that we don't wait if the sound does not own the underlying data source, but this might end up being dangerous because
the application may think it's safe to destroy the data source when it actually isn't. It just feels untidy doing it like that.
*/
ma_engine_sound_mix_wait(pSound);
/* Once the sound is detached from the group we can guarantee that it won't be referenced by the mixer thread which means it's safe for us to destroy the data source. */
/* TODO: Implement me. Need our own temporary sound pool which can use an allocator for, but I'm also thinking of allowing the use of a different allocator to the default one. */
/* TODO: Look at the possibility of allowing groups to use a different format to the primary data format. Makes mixing and group management much more complicated. */
MA_ASSERT(pGroup==&pEngine->masterSoundGroup);/* The master group is the only one allowed to not have a parent group. */
}
/*
We need to initialize the lock that'll be used to synchronize adding and removing of sounds to the group. This lock is _not_ used by the mixing thread. The mixing
thread is written in a way where a lock should not be required.
MA_ASSERT(MA_FALSE);/* Should never happen. Trigger an assert for debugging, but don't stop uninitializing in production to ensure we free memory down below. */