mirror of
https://github.com/mackron/miniaudio.git
synced 2026-04-21 15:56:58 +02:00
Add examples for the high level API.
This commit is contained in:
@@ -0,0 +1,230 @@
|
|||||||
|
/*
|
||||||
|
Demonstrates how to implement a custom decoder and use it with the high level API.
|
||||||
|
|
||||||
|
This is the same as the custom_decoder example, only it's used with the high level engine API
|
||||||
|
rather than the low level decoding API. You can use this to add support for Opus to your games, for
|
||||||
|
example (via libopus).
|
||||||
|
*/
|
||||||
|
#define MA_NO_VORBIS /* Disable the built-in Vorbis decoder to ensure the libvorbis decoder is picked. */
|
||||||
|
#define MA_NO_OPUS /* Disable the (not yet implemented) built-in Opus decoder to ensure the libopus decoder is picked. */
|
||||||
|
#define MINIAUDIO_IMPLEMENTATION
|
||||||
|
#include "../../miniaudio.h"
|
||||||
|
#include "../../extras/miniaudio_libvorbis.h"
|
||||||
|
#include "../../extras/miniaudio_libopus.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
static ma_result ma_decoding_backend_init__libvorbis(void* pUserData, ma_read_proc onRead, ma_seek_proc onSeek, ma_tell_proc onTell, void* pReadSeekTellUserData, const ma_decoding_backend_config* pConfig, const ma_allocation_callbacks* pAllocationCallbacks, ma_data_source** ppBackend)
|
||||||
|
{
|
||||||
|
ma_result result;
|
||||||
|
ma_libvorbis* pVorbis;
|
||||||
|
|
||||||
|
(void)pUserData;
|
||||||
|
|
||||||
|
pVorbis = (ma_libvorbis*)ma_malloc(sizeof(*pVorbis), pAllocationCallbacks);
|
||||||
|
if (pVorbis == NULL) {
|
||||||
|
return MA_OUT_OF_MEMORY;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = ma_libvorbis_init(onRead, onSeek, onTell, pReadSeekTellUserData, pConfig, pAllocationCallbacks, pVorbis);
|
||||||
|
if (result != MA_SUCCESS) {
|
||||||
|
ma_free(pVorbis, pAllocationCallbacks);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
*ppBackend = pVorbis;
|
||||||
|
|
||||||
|
return MA_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
static ma_result ma_decoding_backend_init_file__libvorbis(void* pUserData, const char* pFilePath, const ma_decoding_backend_config* pConfig, const ma_allocation_callbacks* pAllocationCallbacks, ma_data_source** ppBackend)
|
||||||
|
{
|
||||||
|
ma_result result;
|
||||||
|
ma_libvorbis* pVorbis;
|
||||||
|
|
||||||
|
(void)pUserData;
|
||||||
|
|
||||||
|
pVorbis = (ma_libvorbis*)ma_malloc(sizeof(*pVorbis), pAllocationCallbacks);
|
||||||
|
if (pVorbis == NULL) {
|
||||||
|
return MA_OUT_OF_MEMORY;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = ma_libvorbis_init_file(pFilePath, pConfig, pAllocationCallbacks, pVorbis);
|
||||||
|
if (result != MA_SUCCESS) {
|
||||||
|
ma_free(pVorbis, pAllocationCallbacks);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
*ppBackend = pVorbis;
|
||||||
|
|
||||||
|
return MA_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ma_decoding_backend_uninit__libvorbis(void* pUserData, ma_data_source* pBackend, const ma_allocation_callbacks* pAllocationCallbacks)
|
||||||
|
{
|
||||||
|
ma_libvorbis* pVorbis = (ma_libvorbis*)pBackend;
|
||||||
|
|
||||||
|
(void)pUserData;
|
||||||
|
|
||||||
|
ma_libvorbis_uninit(pVorbis, pAllocationCallbacks);
|
||||||
|
ma_free(pVorbis, pAllocationCallbacks);
|
||||||
|
}
|
||||||
|
|
||||||
|
static ma_result ma_decoding_backend_get_channel_map__libvorbis(void* pUserData, ma_data_source* pBackend, ma_channel* pChannelMap, size_t channelMapCap)
|
||||||
|
{
|
||||||
|
ma_libvorbis* pVorbis = (ma_libvorbis*)pBackend;
|
||||||
|
|
||||||
|
(void)pUserData;
|
||||||
|
|
||||||
|
return ma_libvorbis_get_data_format(pVorbis, NULL, NULL, NULL, pChannelMap, channelMapCap);
|
||||||
|
}
|
||||||
|
|
||||||
|
static ma_decoding_backend_vtable g_ma_decoding_backend_vtable_libvorbis =
|
||||||
|
{
|
||||||
|
ma_decoding_backend_init__libvorbis,
|
||||||
|
ma_decoding_backend_init_file__libvorbis,
|
||||||
|
NULL, /* onInitFileW() */
|
||||||
|
NULL, /* onInitMemory() */
|
||||||
|
ma_decoding_backend_uninit__libvorbis
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
static ma_result ma_decoding_backend_init__libopus(void* pUserData, ma_read_proc onRead, ma_seek_proc onSeek, ma_tell_proc onTell, void* pReadSeekTellUserData, const ma_decoding_backend_config* pConfig, const ma_allocation_callbacks* pAllocationCallbacks, ma_data_source** ppBackend)
|
||||||
|
{
|
||||||
|
ma_result result;
|
||||||
|
ma_libopus* pOpus;
|
||||||
|
|
||||||
|
(void)pUserData;
|
||||||
|
|
||||||
|
pOpus = (ma_libopus*)ma_malloc(sizeof(*pOpus), pAllocationCallbacks);
|
||||||
|
if (pOpus == NULL) {
|
||||||
|
return MA_OUT_OF_MEMORY;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = ma_libopus_init(onRead, onSeek, onTell, pReadSeekTellUserData, pConfig, pAllocationCallbacks, pOpus);
|
||||||
|
if (result != MA_SUCCESS) {
|
||||||
|
ma_free(pOpus, pAllocationCallbacks);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
*ppBackend = pOpus;
|
||||||
|
|
||||||
|
return MA_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
static ma_result ma_decoding_backend_init_file__libopus(void* pUserData, const char* pFilePath, const ma_decoding_backend_config* pConfig, const ma_allocation_callbacks* pAllocationCallbacks, ma_data_source** ppBackend)
|
||||||
|
{
|
||||||
|
ma_result result;
|
||||||
|
ma_libopus* pOpus;
|
||||||
|
|
||||||
|
(void)pUserData;
|
||||||
|
|
||||||
|
pOpus = (ma_libopus*)ma_malloc(sizeof(*pOpus), pAllocationCallbacks);
|
||||||
|
if (pOpus == NULL) {
|
||||||
|
return MA_OUT_OF_MEMORY;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = ma_libopus_init_file(pFilePath, pConfig, pAllocationCallbacks, pOpus);
|
||||||
|
if (result != MA_SUCCESS) {
|
||||||
|
ma_free(pOpus, pAllocationCallbacks);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
*ppBackend = pOpus;
|
||||||
|
|
||||||
|
return MA_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ma_decoding_backend_uninit__libopus(void* pUserData, ma_data_source* pBackend, const ma_allocation_callbacks* pAllocationCallbacks)
|
||||||
|
{
|
||||||
|
ma_libopus* pOpus = (ma_libopus*)pBackend;
|
||||||
|
|
||||||
|
(void)pUserData;
|
||||||
|
|
||||||
|
ma_libopus_uninit(pOpus, pAllocationCallbacks);
|
||||||
|
ma_free(pOpus, pAllocationCallbacks);
|
||||||
|
}
|
||||||
|
|
||||||
|
static ma_result ma_decoding_backend_get_channel_map__libopus(void* pUserData, ma_data_source* pBackend, ma_channel* pChannelMap, size_t channelMapCap)
|
||||||
|
{
|
||||||
|
ma_libopus* pOpus = (ma_libopus*)pBackend;
|
||||||
|
|
||||||
|
(void)pUserData;
|
||||||
|
|
||||||
|
return ma_libopus_get_data_format(pOpus, NULL, NULL, NULL, pChannelMap, channelMapCap);
|
||||||
|
}
|
||||||
|
|
||||||
|
static ma_decoding_backend_vtable g_ma_decoding_backend_vtable_libopus =
|
||||||
|
{
|
||||||
|
ma_decoding_backend_init__libopus,
|
||||||
|
ma_decoding_backend_init_file__libopus,
|
||||||
|
NULL, /* onInitFileW() */
|
||||||
|
NULL, /* onInitMemory() */
|
||||||
|
ma_decoding_backend_uninit__libopus
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
int main(int argc, char** argv)
|
||||||
|
{
|
||||||
|
ma_result result;
|
||||||
|
ma_resource_manager_config resourceManagerConfig;
|
||||||
|
ma_resource_manager resourceManager;
|
||||||
|
ma_engine_config engineConfig;
|
||||||
|
ma_engine engine;
|
||||||
|
|
||||||
|
/*
|
||||||
|
Add your custom backend vtables here. The order in the array defines the order of priority. The
|
||||||
|
vtables will be passed in to the resource manager config.
|
||||||
|
*/
|
||||||
|
ma_decoding_backend_vtable* pCustomBackendVTables[] =
|
||||||
|
{
|
||||||
|
&g_ma_decoding_backend_vtable_libvorbis,
|
||||||
|
&g_ma_decoding_backend_vtable_libopus
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
if (argc < 2) {
|
||||||
|
printf("No input file.\n");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Using custom decoding backends requires a resource manager. */
|
||||||
|
resourceManagerConfig = ma_resource_manager_config_init();
|
||||||
|
resourceManagerConfig.ppCustomDecodingBackendVTables = pCustomBackendVTables;
|
||||||
|
resourceManagerConfig.customDecodingBackendCount = sizeof(pCustomBackendVTables) / sizeof(pCustomBackendVTables[0]);
|
||||||
|
resourceManagerConfig.pCustomDecodingBackendUserData = NULL; /* <-- This will be passed in to the pUserData parameter of each function in the decoding backend vtables. */
|
||||||
|
|
||||||
|
result = ma_resource_manager_init(&resourceManagerConfig, &resourceManager);
|
||||||
|
if (result != MA_SUCCESS) {
|
||||||
|
printf("Failed to initialize resource manager.");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Once we have a resource manager we can create the engine. */
|
||||||
|
engineConfig = ma_engine_config_init();
|
||||||
|
engineConfig.pResourceManager = &resourceManager;
|
||||||
|
|
||||||
|
result = ma_engine_init(&engineConfig, &engine);
|
||||||
|
if (result != MA_SUCCESS) {
|
||||||
|
printf("Failed to initialize engine.");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Now we can play our sound. */
|
||||||
|
result = ma_engine_play_sound(&engine, argv[1], NULL);
|
||||||
|
if (result != MA_SUCCESS) {
|
||||||
|
printf("Failed to play sound.");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
printf("Press Enter to quit...");
|
||||||
|
getchar();
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
/*
|
||||||
|
Demonstrates how to apply an effect to a duplex stream using the node graph system.
|
||||||
|
|
||||||
|
This example applies a vocoder effect to the input stream before outputting it. A custom node
|
||||||
|
called `ma_vocoder_node` is used to achieve the effect which can be found in the extras folder in
|
||||||
|
the miniaudio repository. The vocoder node uses https://github.com/blastbay/voclib to achieve the
|
||||||
|
effect.
|
||||||
|
*/
|
||||||
|
#define MINIAUDIO_IMPLEMENTATION
|
||||||
|
#include "../../miniaudio.h"
|
||||||
|
#include "../miniaudio_engine.h"
|
||||||
|
#include "../_extras/nodes/ma_vocoder_node/ma_vocoder_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 1 /* For this example, always set to 1. */
|
||||||
|
|
||||||
|
static ma_waveform g_sourceData; /* The underlying data source of the excite node. */
|
||||||
|
static ma_audio_buffer_ref g_exciteData; /* The underlying data source of the source node. */
|
||||||
|
static ma_data_source_node g_sourceNode; /* A data source node containing the source data we'll be sending through to the vocoder. This will be routed into the first bus of the vocoder node. */
|
||||||
|
static ma_data_source_node g_exciteNode; /* A data source node containing the excite data we'll be sending through to the vocoder. This will be routed into the second bus of the vocoder node. */
|
||||||
|
static ma_vocoder_node g_vocoderNode; /* The vocoder node. */
|
||||||
|
static ma_node_graph g_nodeGraph;
|
||||||
|
|
||||||
|
void data_callback(ma_device* pDevice, void* pOutput, const void* pInput, ma_uint32 frameCount)
|
||||||
|
{
|
||||||
|
MA_ASSERT(pDevice->capture.format == pDevice->playback.format);
|
||||||
|
MA_ASSERT(pDevice->capture.channels == pDevice->playback.channels);
|
||||||
|
|
||||||
|
/*
|
||||||
|
The node graph system is a pulling style of API. At the lowest level of the chain will be a
|
||||||
|
node acting as a data source for the purpose of delivering the initial audio data. In our case,
|
||||||
|
the data source is our `pInput` buffer. We need to update the underlying data source so that it
|
||||||
|
read data from `pInput`.
|
||||||
|
*/
|
||||||
|
ma_audio_buffer_ref_set_data(&g_exciteData, pInput, frameCount);
|
||||||
|
|
||||||
|
/* With the source buffer configured we can now read directly 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_device_config deviceConfig;
|
||||||
|
ma_device device;
|
||||||
|
ma_node_graph_config nodeGraphConfig;
|
||||||
|
ma_vocoder_node_config vocoderNodeConfig;
|
||||||
|
ma_data_source_node_config sourceNodeConfig;
|
||||||
|
ma_data_source_node_config exciteNodeConfig;
|
||||||
|
ma_waveform_config waveformConfig;
|
||||||
|
|
||||||
|
deviceConfig = ma_device_config_init(ma_device_type_duplex);
|
||||||
|
deviceConfig.capture.pDeviceID = NULL;
|
||||||
|
deviceConfig.capture.format = DEVICE_FORMAT;
|
||||||
|
deviceConfig.capture.channels = DEVICE_CHANNELS;
|
||||||
|
deviceConfig.capture.shareMode = ma_share_mode_shared;
|
||||||
|
deviceConfig.playback.pDeviceID = NULL;
|
||||||
|
deviceConfig.playback.format = DEVICE_FORMAT;
|
||||||
|
deviceConfig.playback.channels = DEVICE_CHANNELS;
|
||||||
|
deviceConfig.dataCallback = data_callback;
|
||||||
|
result = ma_device_init(NULL, &deviceConfig, &device);
|
||||||
|
if (result != MA_SUCCESS) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Now we can setup our node graph. */
|
||||||
|
nodeGraphConfig = ma_node_graph_config_init(device.capture.channels);
|
||||||
|
|
||||||
|
result = ma_node_graph_init(&nodeGraphConfig, NULL, &g_nodeGraph);
|
||||||
|
if (result != MA_SUCCESS) {
|
||||||
|
printf("Failed to initialize node graph.");
|
||||||
|
goto done0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Vocoder. Attached straight to the endpoint. */
|
||||||
|
vocoderNodeConfig = ma_vocoder_node_config_init(device.capture.channels, device.sampleRate);
|
||||||
|
|
||||||
|
result = ma_vocoder_node_init(&g_nodeGraph, &vocoderNodeConfig, NULL, &g_vocoderNode);
|
||||||
|
if (result != MA_SUCCESS) {
|
||||||
|
printf("Failed to initialize vocoder node.");
|
||||||
|
goto done1;
|
||||||
|
}
|
||||||
|
|
||||||
|
ma_node_attach_output_bus(&g_vocoderNode, 0, ma_node_graph_get_endpoint(&g_nodeGraph), 0);
|
||||||
|
|
||||||
|
/* Amplify the volume of the vocoder output because in my testing it is a bit quiet. */
|
||||||
|
ma_node_set_output_bus_volume(&g_vocoderNode, 0, 4);
|
||||||
|
|
||||||
|
|
||||||
|
/* Source/carrier. Attached to input bus 0 of the vocoder node. */
|
||||||
|
waveformConfig = ma_waveform_config_init(device.capture.format, device.capture.channels, device.sampleRate, ma_waveform_type_sawtooth, 1.0, 50);
|
||||||
|
|
||||||
|
result = ma_waveform_init(&waveformConfig, &g_sourceData);
|
||||||
|
if (result != MA_SUCCESS) {
|
||||||
|
printf("Failed to initialize waveform for excite node.");
|
||||||
|
goto done3;
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceNodeConfig = ma_data_source_node_config_init(&g_sourceData, MA_FALSE);
|
||||||
|
|
||||||
|
result = ma_data_source_node_init(&g_nodeGraph, &sourceNodeConfig, NULL, &g_sourceNode);
|
||||||
|
if (result != MA_SUCCESS) {
|
||||||
|
printf("Failed to initialize excite node.");
|
||||||
|
goto done3;
|
||||||
|
}
|
||||||
|
|
||||||
|
ma_node_attach_output_bus(&g_sourceNode, 0, &g_vocoderNode, 0);
|
||||||
|
|
||||||
|
|
||||||
|
/* Excite/modulator. Attached to input bus 1 of the vocoder node. */
|
||||||
|
result = ma_audio_buffer_ref_init(device.capture.format, device.capture.channels, NULL, 0, &g_exciteData);
|
||||||
|
if (result != MA_SUCCESS) {
|
||||||
|
printf("Failed to initialize audio buffer for source.");
|
||||||
|
goto done2;
|
||||||
|
}
|
||||||
|
|
||||||
|
exciteNodeConfig = ma_data_source_node_config_init(&g_exciteData, MA_FALSE);
|
||||||
|
|
||||||
|
result = ma_data_source_node_init(&g_nodeGraph, &exciteNodeConfig, NULL, &g_exciteNode);
|
||||||
|
if (result != MA_SUCCESS) {
|
||||||
|
printf("Failed to initialize source node.");
|
||||||
|
goto done2;
|
||||||
|
}
|
||||||
|
|
||||||
|
ma_node_attach_output_bus(&g_exciteNode, 0, &g_vocoderNode, 1);
|
||||||
|
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
/*done4:*/ ma_data_source_node_uninit(&g_exciteNode, NULL);
|
||||||
|
done3: ma_data_source_node_uninit(&g_sourceNode, NULL);
|
||||||
|
done2: ma_vocoder_node_uninit(&g_vocoderNode, NULL);
|
||||||
|
done1: ma_node_graph_uninit(&g_nodeGraph, NULL);
|
||||||
|
done0: ma_device_uninit(&device);
|
||||||
|
|
||||||
|
(void)argc;
|
||||||
|
(void)argv;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,247 @@
|
|||||||
|
/*
|
||||||
|
This example demonstrates some of the advanced features of the high level engine API.
|
||||||
|
|
||||||
|
The following features are demonstrated:
|
||||||
|
|
||||||
|
* Initialization of the engine from a pre-initialized device.
|
||||||
|
* Self-managed resource managers.
|
||||||
|
* Multiple engines with a shared resource manager.
|
||||||
|
* Creation and management of `ma_sound` objects.
|
||||||
|
|
||||||
|
This example will play the sound that's passed in the command line.
|
||||||
|
|
||||||
|
If you were wanting to support multiple listeners, this example will show you how to do that. You achieve this by
|
||||||
|
initializing one `ma_engine` object for each listener, each of which share a single self-managed resource manager.
|
||||||
|
*/
|
||||||
|
#define MINIAUDIO_IMPLEMENTATION
|
||||||
|
#include "../../miniaudio.h"
|
||||||
|
#include "../miniaudio_engine.h"
|
||||||
|
|
||||||
|
#define MAX_DEVICES 2
|
||||||
|
#define MAX_SOUNDS 32
|
||||||
|
|
||||||
|
void data_callback(ma_device* pDevice, void* pOutput, const void* pInput, ma_uint32 frameCount)
|
||||||
|
{
|
||||||
|
(void)pInput;
|
||||||
|
|
||||||
|
/*
|
||||||
|
Since we're managing the underlying device ourselves, we need to manually call the engine's data handler. To do
|
||||||
|
this we need access to the `ma_engine` object which we passed in to the user data. The advantage of this is that
|
||||||
|
you could do your own audio processing in addition to the engine's standard processing.
|
||||||
|
*/
|
||||||
|
ma_engine_read_pcm_frames((ma_engine*)pDevice->pUserData, pOutput, frameCount, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char** argv)
|
||||||
|
{
|
||||||
|
ma_result result;
|
||||||
|
ma_context context;
|
||||||
|
ma_resource_manager_config resourceManagerConfig;
|
||||||
|
ma_resource_manager resourceManager;
|
||||||
|
ma_engine engines[MAX_DEVICES];
|
||||||
|
ma_device devices[MAX_DEVICES];
|
||||||
|
ma_uint32 engineCount = 0;
|
||||||
|
ma_uint32 iEngine;
|
||||||
|
ma_device_info* pPlaybackDeviceInfos;
|
||||||
|
ma_uint32 playbackDeviceCount;
|
||||||
|
ma_uint32 iAvailableDevice;
|
||||||
|
ma_uint32 iChosenDevice;
|
||||||
|
ma_sound sounds[MAX_SOUNDS];
|
||||||
|
ma_uint32 soundCount;
|
||||||
|
ma_uint32 iSound;
|
||||||
|
|
||||||
|
if (argc < 2) {
|
||||||
|
printf("No input file.");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
We are going to be initializing multiple engines. In order to save on memory usage we can use a self managed
|
||||||
|
resource manager so we can share a single resource manager across multiple engines.
|
||||||
|
*/
|
||||||
|
resourceManagerConfig = ma_resource_manager_config_init();
|
||||||
|
resourceManagerConfig.decodedFormat = ma_format_f32; /* ma_format_f32 should almost always be used as that's what the engine (and most everything else) uses for mixing. */
|
||||||
|
resourceManagerConfig.decodedChannels = 0; /* Setting the channel count to 0 will cause sounds to use their native channel count. */
|
||||||
|
resourceManagerConfig.decodedSampleRate = 48000; /* Using a consistent sample rate is useful for avoiding expensive resampling in the audio thread. This will result in resampling being performed by the loading thread(s). */
|
||||||
|
|
||||||
|
result = ma_resource_manager_init(&resourceManagerConfig, &resourceManager);
|
||||||
|
if (result != MA_SUCCESS) {
|
||||||
|
printf("Failed to initialize resource manager.");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* We're going to want a context so we can enumerate our playback devices. */
|
||||||
|
result = ma_context_init(NULL, 0, NULL, &context);
|
||||||
|
if (result != MA_SUCCESS) {
|
||||||
|
printf("Failed to initialize context.");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Now that we have a context we will want to enumerate over each device so we can display them to the user and give
|
||||||
|
them a chance to select the output devices they want to use.
|
||||||
|
*/
|
||||||
|
result = ma_context_get_devices(&context, &pPlaybackDeviceInfos, &playbackDeviceCount, NULL, NULL);
|
||||||
|
if (result != MA_SUCCESS) {
|
||||||
|
printf("Failed to enumerate playback devices.");
|
||||||
|
ma_context_uninit(&context);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* We have our devices, so now we want to get the user to select the devices they want to output to. */
|
||||||
|
engineCount = 0;
|
||||||
|
|
||||||
|
for (iChosenDevice = 0; iChosenDevice < MAX_DEVICES; iChosenDevice += 1) {
|
||||||
|
int c = 0;
|
||||||
|
for (;;) {
|
||||||
|
printf("Select playback device %d ([%d - %d], Q to quit):\n", iChosenDevice+1, 0, ma_min((int)playbackDeviceCount, 9));
|
||||||
|
|
||||||
|
for (iAvailableDevice = 0; iAvailableDevice < playbackDeviceCount; iAvailableDevice += 1) {
|
||||||
|
printf(" %d: %s\n", iAvailableDevice, pPlaybackDeviceInfos[iAvailableDevice].name);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
c = getchar();
|
||||||
|
if (c != '\n') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c == 'q' || c == 'Q') {
|
||||||
|
return 0; /* User aborted. */
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c >= '0' && c <= '9') {
|
||||||
|
c -= '0';
|
||||||
|
|
||||||
|
if (c < (int)playbackDeviceCount) {
|
||||||
|
ma_device_config deviceConfig;
|
||||||
|
ma_engine_config engineConfig;
|
||||||
|
|
||||||
|
/*
|
||||||
|
Create the device first before the engine. We'll specify the device in the engine's config. This is optional. When a device is
|
||||||
|
not pre-initialized the engine will create one for you internally. The device does not need to be started here - the engine will
|
||||||
|
do that for us in `ma_engine_start()`. The device's format is derived from the resource manager, but can be whatever you want.
|
||||||
|
It's useful to keep the format consistent with the resource manager to avoid data conversions costs in the audio callback. In
|
||||||
|
this example we're using the resource manager's sample format and sample rate, but leaving the channel count set to the device's
|
||||||
|
native channels. You can use whatever format/channels/rate you like.
|
||||||
|
*/
|
||||||
|
deviceConfig = ma_device_config_init(ma_device_type_playback);
|
||||||
|
deviceConfig.playback.pDeviceID = &pPlaybackDeviceInfos[c].id;
|
||||||
|
deviceConfig.playback.format = resourceManager.config.decodedFormat;
|
||||||
|
deviceConfig.playback.channels = 0;
|
||||||
|
deviceConfig.sampleRate = resourceManager.config.decodedSampleRate;
|
||||||
|
deviceConfig.dataCallback = data_callback;
|
||||||
|
deviceConfig.pUserData = &engines[engineCount];
|
||||||
|
|
||||||
|
result = ma_device_init(&context, &deviceConfig, &devices[engineCount]);
|
||||||
|
if (result != MA_SUCCESS) {
|
||||||
|
printf("Failed to initialize device for %s.\n", pPlaybackDeviceInfos[c].name);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Now that we have the device we can initialize the engine. The device is passed into the engine's config. */
|
||||||
|
engineConfig = ma_engine_config_init();
|
||||||
|
engineConfig.pDevice = &devices[engineCount];
|
||||||
|
engineConfig.pResourceManager = &resourceManager;
|
||||||
|
engineConfig.noAutoStart = MA_TRUE; /* Don't start the engine by default - we'll do that manually below. */
|
||||||
|
|
||||||
|
result = ma_engine_init(&engineConfig, &engines[engineCount]);
|
||||||
|
if (result != MA_SUCCESS) {
|
||||||
|
printf("Failed to initialize engine for %s.\n", pPlaybackDeviceInfos[c].name);
|
||||||
|
ma_device_uninit(&devices[engineCount]);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
engineCount += 1;
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
printf("Invalid device number.\n");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
printf("Invalid device number.\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("Device %d: %s\n", iChosenDevice+1, pPlaybackDeviceInfos[c].name);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* We should now have our engine's initialized. We can now start them. */
|
||||||
|
for (iEngine = 0; iEngine < engineCount; iEngine += 1) {
|
||||||
|
result = ma_engine_start(&engines[iEngine]);
|
||||||
|
if (result != MA_SUCCESS) {
|
||||||
|
printf("WARNING: Failed to start engine %d.\n", iEngine);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
At this point our engine's are running and outputting nothing but silence. To get them playing something we'll need
|
||||||
|
some sounds. In this example we're just using one sound per engine, but you can create as many as you like. Since
|
||||||
|
we're using a shared resource manager, the sound data will only be loaded once. This is how you would implement
|
||||||
|
multiple listeners.
|
||||||
|
*/
|
||||||
|
soundCount = 0;
|
||||||
|
for (iEngine = 0; iEngine < engineCount; iEngine += 1) {
|
||||||
|
/* Just one sound per engine in this example. We're going to be loading this asynchronously. */
|
||||||
|
result = ma_sound_init_from_file(&engines[iEngine], argv[1], MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_DECODE | MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_ASYNC | MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_STREAM, NULL, NULL, &sounds[iEngine]);
|
||||||
|
if (result != MA_SUCCESS) {
|
||||||
|
printf("WARNING: Failed to load sound \"%s\"", argv[1]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
The sound can be started as soon as ma_sound_init_from_file() returns, even for sounds that are initialized
|
||||||
|
with MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_ASYNC. The sound will start playing while it's being loaded. Note that if the
|
||||||
|
asynchronous loading process cannot keep up with the rate at which you try reading you'll end up glitching.
|
||||||
|
If this is an issue, you need to not load sounds asynchronously.
|
||||||
|
*/
|
||||||
|
result = ma_sound_start(&sounds[iEngine]);
|
||||||
|
if (result != MA_SUCCESS) {
|
||||||
|
printf("WARNING: Failed to start sound.");
|
||||||
|
}
|
||||||
|
|
||||||
|
soundCount += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
printf("Press Enter to quit...");
|
||||||
|
getchar();
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
int c = getchar();
|
||||||
|
if (c == '\n') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Teardown. */
|
||||||
|
|
||||||
|
/* The application owns the `ma_sound` object which means you're responsible for uninitializing them. */
|
||||||
|
for (iSound = 0; iSound < soundCount; iSound += 1) {
|
||||||
|
ma_sound_uninit(&sounds[iSound]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* We can now uninitialize each engine. */
|
||||||
|
for (iEngine = 0; iEngine < engineCount; iEngine += 1) {
|
||||||
|
ma_engine_uninit(&engines[iEngine]);
|
||||||
|
|
||||||
|
/*
|
||||||
|
The engine has been uninitialized so now lets uninitialize the device. Do this first to ensure we don't
|
||||||
|
uninitialize the resource manager from under the device while the data callback is running.
|
||||||
|
*/
|
||||||
|
ma_device_uninit(&devices[iEngine]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Do the resource manager last. This way we can guarantee the data callbacks of each device aren't trying to access
|
||||||
|
and data managed by the resource manager.
|
||||||
|
*/
|
||||||
|
ma_resource_manager_uninit(&resourceManager);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
This example demonstrates how to initialize an audio engine and play a sound.
|
||||||
|
|
||||||
|
This will play the sound specified on the command line.
|
||||||
|
*/
|
||||||
|
#define MINIAUDIO_IMPLEMENTATION
|
||||||
|
#include "../../miniaudio.h"
|
||||||
|
#include "../miniaudio_engine.h"
|
||||||
|
|
||||||
|
int main(int argc, char** argv)
|
||||||
|
{
|
||||||
|
ma_result result;
|
||||||
|
ma_engine engine;
|
||||||
|
|
||||||
|
if (argc < 2) {
|
||||||
|
printf("No input file.");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = ma_engine_init(NULL, &engine);
|
||||||
|
if (result != MA_SUCCESS) {
|
||||||
|
printf("Failed to initialize audio engine.");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
ma_engine_play_sound(&engine, argv[1], NULL);
|
||||||
|
|
||||||
|
printf("Press Enter to quit...");
|
||||||
|
getchar();
|
||||||
|
|
||||||
|
ma_engine_uninit(&engine);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
/*
|
||||||
|
Demonstrates how you can use the resource manager to manage loaded sounds.
|
||||||
|
|
||||||
|
This example loads the first sound specified on the command line via the resource manager and then plays it using the
|
||||||
|
low level API.
|
||||||
|
|
||||||
|
You can control whether or not you want to load the sound asynchronously and whether or not you want to store the data
|
||||||
|
in-memory or stream it. When storing the sound in-memory you can also control whether or not it is decoded. To do this,
|
||||||
|
specify a combination of the following options in `ma_resource_manager_data_source_init()`:
|
||||||
|
|
||||||
|
* MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_ASYNC - Load asynchronously.
|
||||||
|
* MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_DECODE - Store the sound in-memory in uncompressed/decoded format.
|
||||||
|
* MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_STREAM - Stream the sound from disk rather than storing entirely in memory. Useful for music.
|
||||||
|
|
||||||
|
The object returned by the resource manager is just a standard data source which means it can be plugged into any of
|
||||||
|
`ma_data_source_*()` APIs just like any other data source and it should just work.
|
||||||
|
|
||||||
|
Internally, there's a background thread that's used to process jobs and enable asynchronicity. By default there is only
|
||||||
|
a single job thread, but this can be configured in the resource manager config. You can also implement your own threads
|
||||||
|
for processing jobs. That is more advanced, and beyond the scope of this example.
|
||||||
|
|
||||||
|
When you initialize a resource manager you can specify the sample format, channels and sample rate to use when reading
|
||||||
|
data from the data source. This means the resource manager will ensure all sounds will have a standard format. When not
|
||||||
|
set, each sound will have their own formats and you'll need to do the necessary data conversion yourself.
|
||||||
|
*/
|
||||||
|
#define MA_NO_ENGINE /* We're intentionally not using the ma_engine API here. */
|
||||||
|
#define MINIAUDIO_IMPLEMENTATION
|
||||||
|
#include "../../miniaudio.h"
|
||||||
|
#include "../miniaudio_engine.h"
|
||||||
|
|
||||||
|
#ifdef __EMSCRIPTEN__
|
||||||
|
#include <emscripten.h>
|
||||||
|
|
||||||
|
void main_loop__em(void* pUserData)
|
||||||
|
{
|
||||||
|
ma_resource_manager* pResourceManager = (ma_resource_manager*)pUserData;
|
||||||
|
MA_ASSERT(pResourceManager != NULL);
|
||||||
|
|
||||||
|
/*
|
||||||
|
The Emscripten build does not support threading which means we need to process jobs manually. If
|
||||||
|
there are no jobs needing to be processed this will return immediately with MA_NO_DATA_AVAILABLE.
|
||||||
|
*/
|
||||||
|
ma_resource_manager_process_next_job(pResourceManager);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
void data_callback(ma_device* pDevice, void* pOutput, const void* pInput, ma_uint32 frameCount)
|
||||||
|
{
|
||||||
|
ma_data_source_read_pcm_frames((ma_data_source*)pDevice->pUserData, pOutput, frameCount, NULL, MA_TRUE);
|
||||||
|
|
||||||
|
(void)pInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char** argv)
|
||||||
|
{
|
||||||
|
ma_result result;
|
||||||
|
ma_device_config deviceConfig;
|
||||||
|
ma_device device;
|
||||||
|
ma_resource_manager_config resourceManagerConfig;
|
||||||
|
ma_resource_manager resourceManager;
|
||||||
|
ma_resource_manager_data_source dataSource;
|
||||||
|
|
||||||
|
if (argc < 2) {
|
||||||
|
printf("No input file.");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* We'll initialize the device first. */
|
||||||
|
deviceConfig = ma_device_config_init(ma_device_type_playback);
|
||||||
|
deviceConfig.dataCallback = data_callback;
|
||||||
|
deviceConfig.pUserData = &dataSource; /* <-- We'll be reading from this in the data callback. */
|
||||||
|
|
||||||
|
result = ma_device_init(NULL, &deviceConfig, &device);
|
||||||
|
if (result != MA_SUCCESS) {
|
||||||
|
printf("Failed to initialize device.");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
We have the device so now we want to initialize the resource manager. We'll use the resource manager to load a
|
||||||
|
sound based on the command line.
|
||||||
|
*/
|
||||||
|
resourceManagerConfig = ma_resource_manager_config_init();
|
||||||
|
resourceManagerConfig.decodedFormat = device.playback.format;
|
||||||
|
resourceManagerConfig.decodedChannels = device.playback.channels;
|
||||||
|
resourceManagerConfig.decodedSampleRate = device.sampleRate;
|
||||||
|
|
||||||
|
/*
|
||||||
|
We're not supporting threading with Emscripten so go ahead and disable threading. It's important
|
||||||
|
that we set the appropriate flag and also the job thread count to 0.
|
||||||
|
*/
|
||||||
|
#ifdef __EMSCRIPTEN__
|
||||||
|
resourceManagerConfig.flags |= MA_RESOURCE_MANAGER_FLAG_NO_THREADING;
|
||||||
|
resourceManagerConfig.jobThreadCount = 0;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
result = ma_resource_manager_init(&resourceManagerConfig, &resourceManager);
|
||||||
|
if (result != MA_SUCCESS) {
|
||||||
|
ma_device_uninit(&device);
|
||||||
|
printf("Failed to initialize the resource manager.");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Now that we have a resource manager we can load a sound. */
|
||||||
|
result = ma_resource_manager_data_source_init(
|
||||||
|
&resourceManager,
|
||||||
|
argv[1],
|
||||||
|
MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_DECODE | MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_ASYNC | MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_STREAM,
|
||||||
|
NULL, /* Async notification. */
|
||||||
|
&dataSource);
|
||||||
|
if (result != MA_SUCCESS) {
|
||||||
|
printf("Failed to load sound \"%s\".", argv[1]);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Now that we have a sound we can start the device. */
|
||||||
|
result = ma_device_start(&device);
|
||||||
|
if (result != MA_SUCCESS) {
|
||||||
|
ma_device_uninit(&device);
|
||||||
|
printf("Failed to start device.");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef __EMSCRIPTEN__
|
||||||
|
emscripten_set_main_loop_arg(main_loop__em, &resourceManager, 0, 1);
|
||||||
|
#else
|
||||||
|
printf("Press Enter to quit...\n");
|
||||||
|
getchar();
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* Teardown. */
|
||||||
|
|
||||||
|
/* Uninitialize the device first to ensure the data callback is stopped and doesn't try to access any data. */
|
||||||
|
ma_device_uninit(&device);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Before uninitializing the resource manager we need to uninitialize every data source. The data source is owned by
|
||||||
|
the caller which means you're responsible for uninitializing it.
|
||||||
|
*/
|
||||||
|
ma_resource_manager_data_source_uninit(&dataSource);
|
||||||
|
|
||||||
|
/* Uninitialize the resource manager after each data source. */
|
||||||
|
ma_resource_manager_uninit(&resourceManager);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,327 @@
|
|||||||
|
/*
|
||||||
|
Demonstrates how you can use the resource manager to manage loaded sounds.
|
||||||
|
|
||||||
|
The resource manager can be used to create a data source whose resources are managed internally by miniaudio. The data
|
||||||
|
sources can then be read just like any other data source such as decoders and audio buffers.
|
||||||
|
|
||||||
|
In this example we use the resource manager independently of the `ma_engine` API so that we can demonstrate how it can
|
||||||
|
be used by itself without getting it confused with `ma_engine`.
|
||||||
|
|
||||||
|
The main feature of the resource manager is the ability to decode and stream audio data asynchronously. Asynchronicity
|
||||||
|
is achieved with a job system. The resource manager will issue jobs which are processed by a configurable number of job
|
||||||
|
threads. You can also implement your own custom job threads which this example also demonstrates.
|
||||||
|
|
||||||
|
In this example we show how you can create a data source, mix them with other data sources, configure the number of job
|
||||||
|
threads to manage internally and how to implement your own custom job thread.
|
||||||
|
*/
|
||||||
|
#define MA_NO_ENGINE /* We're intentionally not using the ma_engine API here. */
|
||||||
|
#define MINIAUDIO_IMPLEMENTATION
|
||||||
|
#include "../../miniaudio.h"
|
||||||
|
#include "../miniaudio_engine.h"
|
||||||
|
|
||||||
|
static ma_resource_manager_data_source g_dataSources[16];
|
||||||
|
static ma_uint32 g_dataSourceCount;
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
TODO: Consider putting these public functions in miniaudio.h. Will depend on ma_mix_pcm_frames_f32()
|
||||||
|
being merged into miniaudio.h (it's currently in miniaudio_engine.h).
|
||||||
|
*/
|
||||||
|
static ma_result ma_data_source_read_pcm_frames_f32_ex(ma_data_source* pDataSource, float* pFramesOut, ma_uint64 frameCount, ma_uint64* pFramesRead, ma_bool32 loop, ma_format dataSourceFormat, ma_uint32 dataSourceChannels)
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
This function is intended to be used when the format and channel count of the data source is
|
||||||
|
known beforehand. The idea is to avoid overhead due to redundant calls to ma_data_source_get_data_format().
|
||||||
|
*/
|
||||||
|
MA_ASSERT(pDataSource != NULL);
|
||||||
|
|
||||||
|
if (dataSourceFormat == ma_format_f32) {
|
||||||
|
/* Fast path. No conversion necessary. */
|
||||||
|
return ma_data_source_read_pcm_frames(pDataSource, pFramesOut, frameCount, pFramesRead, loop);
|
||||||
|
} else {
|
||||||
|
/* Slow path. Conversion necessary. */
|
||||||
|
ma_result result;
|
||||||
|
ma_uint64 totalFramesRead;
|
||||||
|
ma_uint8 temp[MA_DATA_CONVERTER_STACK_BUFFER_SIZE];
|
||||||
|
ma_uint64 tempCapInFrames = sizeof(temp) / ma_get_bytes_per_frame(dataSourceFormat, dataSourceChannels);
|
||||||
|
|
||||||
|
totalFramesRead = 0;
|
||||||
|
while (totalFramesRead < frameCount) {
|
||||||
|
ma_uint64 framesJustRead;
|
||||||
|
ma_uint64 framesToRead = frameCount - totalFramesRead;
|
||||||
|
if (framesToRead > tempCapInFrames) {
|
||||||
|
framesToRead = tempCapInFrames;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = ma_data_source_read_pcm_frames(pDataSource, pFramesOut, framesToRead, &framesJustRead, loop);
|
||||||
|
|
||||||
|
ma_convert_pcm_frames_format(ma_offset_pcm_frames_ptr_f32(pFramesOut, totalFramesRead, dataSourceChannels), ma_format_f32, temp, dataSourceFormat, framesJustRead, dataSourceChannels, ma_dither_mode_none);
|
||||||
|
totalFramesRead += framesJustRead;
|
||||||
|
|
||||||
|
if (result != MA_SUCCESS) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return MA_SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MA_API ma_result ma_data_source_read_pcm_frames_f32(ma_data_source* pDataSource, float* pFramesOut, ma_uint64 frameCount, ma_uint64* pFramesRead, ma_bool32 loop)
|
||||||
|
{
|
||||||
|
ma_result result;
|
||||||
|
ma_format format;
|
||||||
|
ma_uint32 channels;
|
||||||
|
|
||||||
|
result = ma_data_source_get_data_format(pDataSource, &format, &channels, NULL, NULL, 0);
|
||||||
|
if (result != MA_SUCCESS) {
|
||||||
|
return result; /* Failed to retrieve the data format of the data source. */
|
||||||
|
}
|
||||||
|
|
||||||
|
return ma_data_source_read_pcm_frames_f32_ex(pDataSource, pFramesOut, frameCount, pFramesRead, loop, format, channels);
|
||||||
|
}
|
||||||
|
|
||||||
|
MA_API ma_result ma_data_source_read_pcm_frames_and_mix_f32(ma_data_source* pDataSource, float* pFramesOut, ma_uint64 frameCount, ma_uint64* pFramesRead, ma_bool32 loop, float volume)
|
||||||
|
{
|
||||||
|
ma_result result;
|
||||||
|
ma_format format;
|
||||||
|
ma_uint32 channels;
|
||||||
|
ma_uint64 totalFramesRead;
|
||||||
|
|
||||||
|
if (pFramesRead != NULL) {
|
||||||
|
*pFramesRead = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pDataSource == NULL) {
|
||||||
|
return MA_INVALID_ARGS;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = ma_data_source_get_data_format(pDataSource, &format, &channels, NULL, NULL, 0);
|
||||||
|
if (result != MA_SUCCESS) {
|
||||||
|
return result; /* Failed to retrieve the data format of the data source. */
|
||||||
|
}
|
||||||
|
|
||||||
|
totalFramesRead = 0;
|
||||||
|
while (totalFramesRead < frameCount) {
|
||||||
|
float temp[MA_DATA_CONVERTER_STACK_BUFFER_SIZE/sizeof(float)];
|
||||||
|
ma_uint64 tempCapInFrames = ma_countof(temp) / channels;
|
||||||
|
ma_uint64 framesJustRead;
|
||||||
|
ma_uint64 framesToRead = frameCount - totalFramesRead;
|
||||||
|
if (framesToRead > tempCapInFrames) {
|
||||||
|
framesToRead = tempCapInFrames;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = ma_data_source_read_pcm_frames_f32_ex(pDataSource, temp, framesToRead, &framesJustRead, loop, format, channels);
|
||||||
|
|
||||||
|
ma_mix_pcm_frames_f32(ma_offset_pcm_frames_ptr(pFramesOut, totalFramesRead, ma_format_f32, channels), temp, framesJustRead, channels, volume);
|
||||||
|
totalFramesRead += framesJustRead;
|
||||||
|
|
||||||
|
if (result != MA_SUCCESS) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pFramesRead != NULL) {
|
||||||
|
*pFramesRead = totalFramesRead;
|
||||||
|
}
|
||||||
|
|
||||||
|
return MA_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void data_callback(ma_device* pDevice, void* pOutput, const void* pInput, ma_uint32 frameCount)
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
In this example we're just going to play our data sources layered on top of each other. This
|
||||||
|
assumes the device's format is f32 and that the buffer is not pre-silenced.
|
||||||
|
*/
|
||||||
|
ma_uint32 iDataSource;
|
||||||
|
|
||||||
|
MA_ASSERT(pDevice->playback.format == ma_format_f32);
|
||||||
|
|
||||||
|
(void)pInput; /* Unused. */
|
||||||
|
|
||||||
|
/*
|
||||||
|
If the device was configured with noPreSilencedOutputBuffer then you would need to silence the
|
||||||
|
buffer here, or make sure the first data source to be mixed is copied rather than mixed.
|
||||||
|
*/
|
||||||
|
/*ma_silence_pcm_frames(pOutput, frameCount, ma_format_f32, pDevice->playback.channels);*/
|
||||||
|
|
||||||
|
/* For each sound, mix as much data as we can. */
|
||||||
|
for (iDataSource = 0; iDataSource < g_dataSourceCount; iDataSource += 1) {
|
||||||
|
ma_data_source_read_pcm_frames_and_mix_f32(&g_dataSources[iDataSource], (float*)pOutput, frameCount, NULL, MA_TRUE, /* volume = */1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static ma_thread_result MA_THREADCALL custom_job_thread(void* pUserData)
|
||||||
|
{
|
||||||
|
ma_resource_manager* pResourceManager = (ma_resource_manager*)pUserData;
|
||||||
|
MA_ASSERT(pResourceManager != NULL);
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
ma_result result;
|
||||||
|
ma_resource_manager_job job;
|
||||||
|
|
||||||
|
/*
|
||||||
|
Retrieve a job from the queue first. This defines what it is you're about to do. By default this will be
|
||||||
|
blocking. You can initialize the resource manager with MA_RESOURCE_MANAGER_FLAG_NON_BLOCKING to not block in
|
||||||
|
which case MA_NO_DATA_AVAILABLE will be returned if no jobs are available.
|
||||||
|
|
||||||
|
When the quit job is returned (MA_RESOURCE_MANAGER_JOB_QUIT), the return value will always be MA_CANCELLED. If you don't want
|
||||||
|
to check the return value (you should), you can instead check if the job code is MA_RESOURCE_MANAGER_JOB_QUIT and use that
|
||||||
|
instead.
|
||||||
|
*/
|
||||||
|
result = ma_resource_manager_next_job(pResourceManager, &job);
|
||||||
|
if (result != MA_SUCCESS) {
|
||||||
|
if (result == MA_CANCELLED) {
|
||||||
|
printf("CUSTOM JOB THREAD TERMINATING VIA MA_CANCELLED... ");
|
||||||
|
} else {
|
||||||
|
printf("CUSTOM JOB THREAD ERROR: %s. TERMINATING... ", ma_result_description(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Terminate if we got a quit message. You don't need to terminate like this, but's a bit more robust. You can
|
||||||
|
just use a global variable or something similar if it's easier for your particular situation. The quit job
|
||||||
|
remains in the queue and will continue to be returned by future calls to ma_resource_manager_next_job(). The
|
||||||
|
reason for this is to give every job thread visibility to the quit job so they have a chance to exit.
|
||||||
|
|
||||||
|
We won't actually be hitting this code because the call above will return MA_CANCELLED when the MA_RESOURCE_MANAGER_JOB_QUIT
|
||||||
|
event is received which means the `result != MA_SUCCESS` logic above will catch it. If you do not check the
|
||||||
|
return value of ma_resource_manager_next_job() you will want to check for MA_RESOURCE_MANAGER_JOB_QUIT like the code below.
|
||||||
|
*/
|
||||||
|
if (job.toc.breakup.code == MA_RESOURCE_MANAGER_JOB_QUIT) {
|
||||||
|
printf("CUSTOM JOB THREAD TERMINATING VIA MA_RESOURCE_MANAGER_JOB_QUIT... ");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Call ma_resource_manager_process_job() to actually do the work to process the job. */
|
||||||
|
printf("PROCESSING IN CUSTOM JOB THREAD: %d\n", job.toc.breakup.code);
|
||||||
|
ma_resource_manager_process_job(pResourceManager, &job);
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("TERMINATED\n");
|
||||||
|
return (ma_thread_result)0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char** argv)
|
||||||
|
{
|
||||||
|
ma_result result;
|
||||||
|
ma_device_config deviceConfig;
|
||||||
|
ma_device device;
|
||||||
|
ma_resource_manager_config resourceManagerConfig;
|
||||||
|
ma_resource_manager resourceManager;
|
||||||
|
ma_thread jobThread;
|
||||||
|
int iFile;
|
||||||
|
|
||||||
|
deviceConfig = ma_device_config_init(ma_device_type_playback);
|
||||||
|
deviceConfig.playback.format = ma_format_f32;
|
||||||
|
deviceConfig.dataCallback = data_callback;
|
||||||
|
deviceConfig.pUserData = NULL;
|
||||||
|
|
||||||
|
result = ma_device_init(NULL, &deviceConfig, &device);
|
||||||
|
if (result != MA_SUCCESS) {
|
||||||
|
printf("Failed to initialize device.");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* We can start the device before loading any sounds. We'll just end up outputting silence. */
|
||||||
|
result = ma_device_start(&device);
|
||||||
|
if (result != MA_SUCCESS) {
|
||||||
|
ma_device_uninit(&device);
|
||||||
|
printf("Failed to start device.");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
We have the device so now we want to initialize the resource manager. We'll use the resource manager to load some
|
||||||
|
sounds based on the command line.
|
||||||
|
*/
|
||||||
|
resourceManagerConfig = ma_resource_manager_config_init();
|
||||||
|
|
||||||
|
/*
|
||||||
|
We'll set a standard decoding format to save us to processing time at mixing time. If you're wanting to use
|
||||||
|
spatialization with your decoded sounds, you may want to consider leaving this as 0 to ensure the file's native
|
||||||
|
channel count is used so you can do proper spatialization.
|
||||||
|
*/
|
||||||
|
resourceManagerConfig.decodedFormat = device.playback.format;
|
||||||
|
resourceManagerConfig.decodedChannels = device.playback.channels;
|
||||||
|
resourceManagerConfig.decodedSampleRate = device.sampleRate;
|
||||||
|
|
||||||
|
/* The number of job threads to be managed internally. Set this to 0 if you want to self-manage your job threads */
|
||||||
|
resourceManagerConfig.jobThreadCount = 4;
|
||||||
|
|
||||||
|
result = ma_resource_manager_init(&resourceManagerConfig, &resourceManager);
|
||||||
|
if (result != MA_SUCCESS) {
|
||||||
|
ma_device_uninit(&device);
|
||||||
|
printf("Failed to initialize the resource manager.");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Now that we have a resource manager we can set up our custom job thread. This is optional. Normally when doing
|
||||||
|
self-managed job threads you would set the internal job thread count to zero. We're doing both internal and
|
||||||
|
self-managed job threads in this example just for demonstration purposes.
|
||||||
|
*/
|
||||||
|
ma_thread_create(&jobThread, ma_thread_priority_default, 0, custom_job_thread, &resourceManager, NULL);
|
||||||
|
|
||||||
|
/* Create each data source from the resource manager. Note that the caller is the owner. */
|
||||||
|
for (iFile = 0; iFile < ma_countof(g_dataSources) && iFile < argc-1; iFile += 1) {
|
||||||
|
result = ma_resource_manager_data_source_init(
|
||||||
|
&resourceManager,
|
||||||
|
argv[iFile+1],
|
||||||
|
MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_DECODE | MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_ASYNC /*| MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_STREAM*/,
|
||||||
|
NULL, /* Async notification. */
|
||||||
|
&g_dataSources[iFile]);
|
||||||
|
|
||||||
|
if (result != MA_SUCCESS) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
g_dataSourceCount += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("Press Enter to quit...");
|
||||||
|
getchar();
|
||||||
|
|
||||||
|
|
||||||
|
/* Teardown. */
|
||||||
|
|
||||||
|
/*
|
||||||
|
Uninitialize the device first to ensure the data callback is stopped and doesn't try to access
|
||||||
|
any data.
|
||||||
|
*/
|
||||||
|
ma_device_uninit(&device);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Our data sources need to be explicitly uninitialized. ma_resource_manager_uninit() will not do
|
||||||
|
it for us. This needs to be done before posting the quit event and uninitializing the resource
|
||||||
|
manager or else we'll get stuck in a deadlock because ma_resource_manager_data_source_uninit()
|
||||||
|
will be waiting for the job thread(s) to finish work, which will never happen because they were
|
||||||
|
just terminated.
|
||||||
|
*/
|
||||||
|
for (iFile = 0; (size_t)iFile < g_dataSourceCount; iFile += 1) {
|
||||||
|
ma_resource_manager_data_source_uninit(&g_dataSources[iFile]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Before uninitializing the resource manager we need to make sure a quit event has been posted to
|
||||||
|
ensure we can get out of our custom thread. The call to ma_resource_manager_uninit() will also
|
||||||
|
do this, but we need to call it explicitly so that our self-managed thread can exit naturally.
|
||||||
|
You only need to post a quit job if you're using that as the exit indicator. You can instead
|
||||||
|
use whatever variable you want to terminate your job thread, but since this example is using a
|
||||||
|
quit job we need to post one. Note that you don't need to do this if you're not managing your
|
||||||
|
own threads - ma_resource_manager_uninit() alone will suffice in that case.
|
||||||
|
*/
|
||||||
|
ma_resource_manager_post_job_quit(&resourceManager);
|
||||||
|
ma_thread_wait(&jobThread); /* Wait for the custom job thread to finish so it doesn't try to access any data. */
|
||||||
|
|
||||||
|
/* Uninitialize the resource manager after each data source. */
|
||||||
|
ma_resource_manager_uninit(&resourceManager);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -1,254 +1 @@
|
|||||||
/*
|
#include "../../examples/custom_decoder_engine.c"
|
||||||
Demonstrates how to implement a custom decoder.
|
|
||||||
|
|
||||||
This example implements two custom decoders:
|
|
||||||
|
|
||||||
* Vorbis via libvorbis
|
|
||||||
* Opus via libopus
|
|
||||||
|
|
||||||
A custom decoder must implement a data source. In this example, the libvorbis data source is called
|
|
||||||
`ma_libvorbis` and the Opus data source is called `ma_libopus`. These two objects are compatible
|
|
||||||
with the `ma_data_source` APIs and can be taken straight from this example and used in real code.
|
|
||||||
|
|
||||||
The custom decoding data sources (`ma_libvorbis` and `ma_libopus` in this example) are connected to
|
|
||||||
the decoder via the decoder config (`ma_decoder_config`). You need to implement a vtable for each
|
|
||||||
of your custom decoders. See `ma_decoding_backend_vtable` for the functions you need to implement.
|
|
||||||
The `onInitFile`, `onInitFileW` and `onInitMemory` functions are optional.
|
|
||||||
*/
|
|
||||||
#define MA_NO_VORBIS /* Disable the built-in Vorbis decoder to ensure the libvorbis decoder is picked. */
|
|
||||||
#define MA_NO_OPUS /* Disable the (not yet implemented) built-in Opus decoder to ensure the libopus decoder is picked. */
|
|
||||||
#define MINIAUDIO_IMPLEMENTATION
|
|
||||||
#include "../../miniaudio.h"
|
|
||||||
#include "../../extras/miniaudio_libvorbis.h"
|
|
||||||
#include "../../extras/miniaudio_libopus.h"
|
|
||||||
#include "../miniaudio_engine.h"
|
|
||||||
|
|
||||||
#include <stdio.h>
|
|
||||||
|
|
||||||
static ma_result ma_decoding_backend_init__libvorbis(void* pUserData, ma_read_proc onRead, ma_seek_proc onSeek, ma_tell_proc onTell, void* pReadSeekTellUserData, const ma_decoding_backend_config* pConfig, const ma_allocation_callbacks* pAllocationCallbacks, ma_data_source** ppBackend)
|
|
||||||
{
|
|
||||||
ma_result result;
|
|
||||||
ma_libvorbis* pVorbis;
|
|
||||||
|
|
||||||
(void)pUserData;
|
|
||||||
|
|
||||||
pVorbis = (ma_libvorbis*)ma_malloc(sizeof(*pVorbis), pAllocationCallbacks);
|
|
||||||
if (pVorbis == NULL) {
|
|
||||||
return MA_OUT_OF_MEMORY;
|
|
||||||
}
|
|
||||||
|
|
||||||
result = ma_libvorbis_init(onRead, onSeek, onTell, pReadSeekTellUserData, pConfig, pAllocationCallbacks, pVorbis);
|
|
||||||
if (result != MA_SUCCESS) {
|
|
||||||
ma_free(pVorbis, pAllocationCallbacks);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
*ppBackend = pVorbis;
|
|
||||||
|
|
||||||
return MA_SUCCESS;
|
|
||||||
}
|
|
||||||
|
|
||||||
static ma_result ma_decoding_backend_init_file__libvorbis(void* pUserData, const char* pFilePath, const ma_decoding_backend_config* pConfig, const ma_allocation_callbacks* pAllocationCallbacks, ma_data_source** ppBackend)
|
|
||||||
{
|
|
||||||
ma_result result;
|
|
||||||
ma_libvorbis* pVorbis;
|
|
||||||
|
|
||||||
(void)pUserData;
|
|
||||||
|
|
||||||
pVorbis = (ma_libvorbis*)ma_malloc(sizeof(*pVorbis), pAllocationCallbacks);
|
|
||||||
if (pVorbis == NULL) {
|
|
||||||
return MA_OUT_OF_MEMORY;
|
|
||||||
}
|
|
||||||
|
|
||||||
result = ma_libvorbis_init_file(pFilePath, pConfig, pAllocationCallbacks, pVorbis);
|
|
||||||
if (result != MA_SUCCESS) {
|
|
||||||
ma_free(pVorbis, pAllocationCallbacks);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
*ppBackend = pVorbis;
|
|
||||||
|
|
||||||
return MA_SUCCESS;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void ma_decoding_backend_uninit__libvorbis(void* pUserData, ma_data_source* pBackend, const ma_allocation_callbacks* pAllocationCallbacks)
|
|
||||||
{
|
|
||||||
ma_libvorbis* pVorbis = (ma_libvorbis*)pBackend;
|
|
||||||
|
|
||||||
(void)pUserData;
|
|
||||||
|
|
||||||
ma_libvorbis_uninit(pVorbis, pAllocationCallbacks);
|
|
||||||
ma_free(pVorbis, pAllocationCallbacks);
|
|
||||||
}
|
|
||||||
|
|
||||||
static ma_result ma_decoding_backend_get_channel_map__libvorbis(void* pUserData, ma_data_source* pBackend, ma_channel* pChannelMap, size_t channelMapCap)
|
|
||||||
{
|
|
||||||
ma_libvorbis* pVorbis = (ma_libvorbis*)pBackend;
|
|
||||||
|
|
||||||
(void)pUserData;
|
|
||||||
|
|
||||||
return ma_libvorbis_get_data_format(pVorbis, NULL, NULL, NULL, pChannelMap, channelMapCap);
|
|
||||||
}
|
|
||||||
|
|
||||||
static ma_decoding_backend_vtable g_ma_decoding_backend_vtable_libvorbis =
|
|
||||||
{
|
|
||||||
ma_decoding_backend_init__libvorbis,
|
|
||||||
ma_decoding_backend_init_file__libvorbis,
|
|
||||||
NULL, /* onInitFileW() */
|
|
||||||
NULL, /* onInitMemory() */
|
|
||||||
ma_decoding_backend_uninit__libvorbis
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
static ma_result ma_decoding_backend_init__libopus(void* pUserData, ma_read_proc onRead, ma_seek_proc onSeek, ma_tell_proc onTell, void* pReadSeekTellUserData, const ma_decoding_backend_config* pConfig, const ma_allocation_callbacks* pAllocationCallbacks, ma_data_source** ppBackend)
|
|
||||||
{
|
|
||||||
ma_result result;
|
|
||||||
ma_libopus* pOpus;
|
|
||||||
|
|
||||||
(void)pUserData;
|
|
||||||
|
|
||||||
pOpus = (ma_libopus*)ma_malloc(sizeof(*pOpus), pAllocationCallbacks);
|
|
||||||
if (pOpus == NULL) {
|
|
||||||
return MA_OUT_OF_MEMORY;
|
|
||||||
}
|
|
||||||
|
|
||||||
result = ma_libopus_init(onRead, onSeek, onTell, pReadSeekTellUserData, pConfig, pAllocationCallbacks, pOpus);
|
|
||||||
if (result != MA_SUCCESS) {
|
|
||||||
ma_free(pOpus, pAllocationCallbacks);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
*ppBackend = pOpus;
|
|
||||||
|
|
||||||
return MA_SUCCESS;
|
|
||||||
}
|
|
||||||
|
|
||||||
static ma_result ma_decoding_backend_init_file__libopus(void* pUserData, const char* pFilePath, const ma_decoding_backend_config* pConfig, const ma_allocation_callbacks* pAllocationCallbacks, ma_data_source** ppBackend)
|
|
||||||
{
|
|
||||||
ma_result result;
|
|
||||||
ma_libopus* pOpus;
|
|
||||||
|
|
||||||
(void)pUserData;
|
|
||||||
|
|
||||||
pOpus = (ma_libopus*)ma_malloc(sizeof(*pOpus), pAllocationCallbacks);
|
|
||||||
if (pOpus == NULL) {
|
|
||||||
return MA_OUT_OF_MEMORY;
|
|
||||||
}
|
|
||||||
|
|
||||||
result = ma_libopus_init_file(pFilePath, pConfig, pAllocationCallbacks, pOpus);
|
|
||||||
if (result != MA_SUCCESS) {
|
|
||||||
ma_free(pOpus, pAllocationCallbacks);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
*ppBackend = pOpus;
|
|
||||||
|
|
||||||
return MA_SUCCESS;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void ma_decoding_backend_uninit__libopus(void* pUserData, ma_data_source* pBackend, const ma_allocation_callbacks* pAllocationCallbacks)
|
|
||||||
{
|
|
||||||
ma_libopus* pOpus = (ma_libopus*)pBackend;
|
|
||||||
|
|
||||||
(void)pUserData;
|
|
||||||
|
|
||||||
ma_libopus_uninit(pOpus, pAllocationCallbacks);
|
|
||||||
ma_free(pOpus, pAllocationCallbacks);
|
|
||||||
}
|
|
||||||
|
|
||||||
static ma_result ma_decoding_backend_get_channel_map__libopus(void* pUserData, ma_data_source* pBackend, ma_channel* pChannelMap, size_t channelMapCap)
|
|
||||||
{
|
|
||||||
ma_libopus* pOpus = (ma_libopus*)pBackend;
|
|
||||||
|
|
||||||
(void)pUserData;
|
|
||||||
|
|
||||||
return ma_libopus_get_data_format(pOpus, NULL, NULL, NULL, pChannelMap, channelMapCap);
|
|
||||||
}
|
|
||||||
|
|
||||||
static ma_decoding_backend_vtable g_ma_decoding_backend_vtable_libopus =
|
|
||||||
{
|
|
||||||
ma_decoding_backend_init__libopus,
|
|
||||||
ma_decoding_backend_init_file__libopus,
|
|
||||||
NULL, /* onInitFileW() */
|
|
||||||
NULL, /* onInitMemory() */
|
|
||||||
ma_decoding_backend_uninit__libopus
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
void data_callback(ma_device* pDevice, void* pOutput, const void* pInput, ma_uint32 frameCount)
|
|
||||||
{
|
|
||||||
ma_data_source* pDataSource = (ma_data_source*)pDevice->pUserData;
|
|
||||||
if (pDataSource == NULL) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ma_data_source_read_pcm_frames(pDataSource, pOutput, frameCount, NULL, MA_TRUE);
|
|
||||||
|
|
||||||
(void)pInput;
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(int argc, char** argv)
|
|
||||||
{
|
|
||||||
ma_result result;
|
|
||||||
ma_resource_manager_config resourceManagerConfig;
|
|
||||||
ma_resource_manager resourceManager;
|
|
||||||
ma_engine_config engineConfig;
|
|
||||||
ma_engine engine;
|
|
||||||
|
|
||||||
/*
|
|
||||||
Add your custom backend vtables here. The order in the array defines the order of priority. The
|
|
||||||
vtables will be passed in to the resource manager config.
|
|
||||||
*/
|
|
||||||
ma_decoding_backend_vtable* pCustomBackendVTables[] =
|
|
||||||
{
|
|
||||||
&g_ma_decoding_backend_vtable_libvorbis,
|
|
||||||
&g_ma_decoding_backend_vtable_libopus
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
if (argc < 2) {
|
|
||||||
printf("No input file.\n");
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Using custom decoding backends requires a resource manager. */
|
|
||||||
resourceManagerConfig = ma_resource_manager_config_init();
|
|
||||||
resourceManagerConfig.ppCustomDecodingBackendVTables = pCustomBackendVTables;
|
|
||||||
resourceManagerConfig.customDecodingBackendCount = sizeof(pCustomBackendVTables) / sizeof(pCustomBackendVTables[0]);
|
|
||||||
resourceManagerConfig.pCustomDecodingBackendUserData = NULL; /* <-- This will be passed in to the pUserData parameter of each function in the decoding backend vtables. */
|
|
||||||
|
|
||||||
result = ma_resource_manager_init(&resourceManagerConfig, &resourceManager);
|
|
||||||
if (result != MA_SUCCESS) {
|
|
||||||
printf("Failed to initialize resource manager.");
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Once we have a resource manager we can create the engine. */
|
|
||||||
engineConfig = ma_engine_config_init();
|
|
||||||
engineConfig.pResourceManager = &resourceManager;
|
|
||||||
|
|
||||||
result = ma_engine_init(&engineConfig, &engine);
|
|
||||||
if (result != MA_SUCCESS) {
|
|
||||||
printf("Failed to initialize engine.");
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Now we can play our sound. */
|
|
||||||
result = ma_engine_play_sound(&engine, argv[1], NULL);
|
|
||||||
if (result != MA_SUCCESS) {
|
|
||||||
printf("Failed to play sound.");
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
printf("Press Enter to quit...");
|
|
||||||
getchar();
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
@@ -1,149 +1 @@
|
|||||||
/*
|
#include "../../examples/duplex_effect.c"
|
||||||
Demonstrates how to apply an effect to a duplex stream using the node graph system.
|
|
||||||
|
|
||||||
This example applies a vocoder effect to the input stream before outputting it. A custom node
|
|
||||||
called `ma_vocoder_node` is used to achieve the effect which can be found in the extras folder in
|
|
||||||
the miniaudio repository. The vocoder node uses https://github.com/blastbay/voclib to achieve the
|
|
||||||
effect.
|
|
||||||
*/
|
|
||||||
#define MINIAUDIO_IMPLEMENTATION
|
|
||||||
#include "../../miniaudio.h"
|
|
||||||
#include "../miniaudio_engine.h"
|
|
||||||
#include "../_extras/nodes/ma_vocoder_node/ma_vocoder_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 1 /* For this example, always set to 1. */
|
|
||||||
|
|
||||||
static ma_waveform g_sourceData; /* The underlying data source of the excite node. */
|
|
||||||
static ma_audio_buffer_ref g_exciteData; /* The underlying data source of the source node. */
|
|
||||||
static ma_data_source_node g_sourceNode; /* A data source node containing the source data we'll be sending through to the vocoder. This will be routed into the first bus of the vocoder node. */
|
|
||||||
static ma_data_source_node g_exciteNode; /* A data source node containing the excite data we'll be sending through to the vocoder. This will be routed into the second bus of the vocoder node. */
|
|
||||||
static ma_vocoder_node g_vocoderNode; /* The vocoder node. */
|
|
||||||
static ma_node_graph g_nodeGraph;
|
|
||||||
|
|
||||||
void data_callback(ma_device* pDevice, void* pOutput, const void* pInput, ma_uint32 frameCount)
|
|
||||||
{
|
|
||||||
MA_ASSERT(pDevice->capture.format == pDevice->playback.format);
|
|
||||||
MA_ASSERT(pDevice->capture.channels == pDevice->playback.channels);
|
|
||||||
|
|
||||||
/*
|
|
||||||
The node graph system is a pulling style of API. At the lowest level of the chain will be a
|
|
||||||
node acting as a data source for the purpose of delivering the initial audio data. In our case,
|
|
||||||
the data source is our `pInput` buffer. We need to update the underlying data source so that it
|
|
||||||
read data from `pInput`.
|
|
||||||
*/
|
|
||||||
ma_audio_buffer_ref_set_data(&g_exciteData, pInput, frameCount);
|
|
||||||
|
|
||||||
/* With the source buffer configured we can now read directly 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_device_config deviceConfig;
|
|
||||||
ma_device device;
|
|
||||||
ma_node_graph_config nodeGraphConfig;
|
|
||||||
ma_vocoder_node_config vocoderNodeConfig;
|
|
||||||
ma_data_source_node_config sourceNodeConfig;
|
|
||||||
ma_data_source_node_config exciteNodeConfig;
|
|
||||||
ma_waveform_config waveformConfig;
|
|
||||||
|
|
||||||
deviceConfig = ma_device_config_init(ma_device_type_duplex);
|
|
||||||
deviceConfig.capture.pDeviceID = NULL;
|
|
||||||
deviceConfig.capture.format = DEVICE_FORMAT;
|
|
||||||
deviceConfig.capture.channels = DEVICE_CHANNELS;
|
|
||||||
deviceConfig.capture.shareMode = ma_share_mode_shared;
|
|
||||||
deviceConfig.playback.pDeviceID = NULL;
|
|
||||||
deviceConfig.playback.format = DEVICE_FORMAT;
|
|
||||||
deviceConfig.playback.channels = DEVICE_CHANNELS;
|
|
||||||
deviceConfig.dataCallback = data_callback;
|
|
||||||
result = ma_device_init(NULL, &deviceConfig, &device);
|
|
||||||
if (result != MA_SUCCESS) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Now we can setup our node graph. */
|
|
||||||
nodeGraphConfig = ma_node_graph_config_init(device.capture.channels);
|
|
||||||
|
|
||||||
result = ma_node_graph_init(&nodeGraphConfig, NULL, &g_nodeGraph);
|
|
||||||
if (result != MA_SUCCESS) {
|
|
||||||
printf("Failed to initialize node graph.");
|
|
||||||
goto done0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Vocoder. Attached straight to the endpoint. */
|
|
||||||
vocoderNodeConfig = ma_vocoder_node_config_init(device.capture.channels, device.sampleRate);
|
|
||||||
|
|
||||||
result = ma_vocoder_node_init(&g_nodeGraph, &vocoderNodeConfig, NULL, &g_vocoderNode);
|
|
||||||
if (result != MA_SUCCESS) {
|
|
||||||
printf("Failed to initialize vocoder node.");
|
|
||||||
goto done1;
|
|
||||||
}
|
|
||||||
|
|
||||||
ma_node_attach_output_bus(&g_vocoderNode, 0, ma_node_graph_get_endpoint(&g_nodeGraph), 0);
|
|
||||||
|
|
||||||
/* Amplify the volume of the vocoder output because in my testing it is a bit quiet. */
|
|
||||||
ma_node_set_output_bus_volume(&g_vocoderNode, 0, 4);
|
|
||||||
|
|
||||||
|
|
||||||
/* Source/carrier. Attached to input bus 0 of the vocoder node. */
|
|
||||||
waveformConfig = ma_waveform_config_init(device.capture.format, device.capture.channels, device.sampleRate, ma_waveform_type_sawtooth, 1.0, 50);
|
|
||||||
|
|
||||||
result = ma_waveform_init(&waveformConfig, &g_sourceData);
|
|
||||||
if (result != MA_SUCCESS) {
|
|
||||||
printf("Failed to initialize waveform for excite node.");
|
|
||||||
goto done3;
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceNodeConfig = ma_data_source_node_config_init(&g_sourceData, MA_FALSE);
|
|
||||||
|
|
||||||
result = ma_data_source_node_init(&g_nodeGraph, &sourceNodeConfig, NULL, &g_sourceNode);
|
|
||||||
if (result != MA_SUCCESS) {
|
|
||||||
printf("Failed to initialize excite node.");
|
|
||||||
goto done3;
|
|
||||||
}
|
|
||||||
|
|
||||||
ma_node_attach_output_bus(&g_sourceNode, 0, &g_vocoderNode, 0);
|
|
||||||
|
|
||||||
|
|
||||||
/* Excite/modulator. Attached to input bus 1 of the vocoder node. */
|
|
||||||
result = ma_audio_buffer_ref_init(device.capture.format, device.capture.channels, NULL, 0, &g_exciteData);
|
|
||||||
if (result != MA_SUCCESS) {
|
|
||||||
printf("Failed to initialize audio buffer for source.");
|
|
||||||
goto done2;
|
|
||||||
}
|
|
||||||
|
|
||||||
exciteNodeConfig = ma_data_source_node_config_init(&g_exciteData, MA_FALSE);
|
|
||||||
|
|
||||||
result = ma_data_source_node_init(&g_nodeGraph, &exciteNodeConfig, NULL, &g_exciteNode);
|
|
||||||
if (result != MA_SUCCESS) {
|
|
||||||
printf("Failed to initialize source node.");
|
|
||||||
goto done2;
|
|
||||||
}
|
|
||||||
|
|
||||||
ma_node_attach_output_bus(&g_exciteNode, 0, &g_vocoderNode, 1);
|
|
||||||
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
/*done4:*/ ma_data_source_node_uninit(&g_exciteNode, NULL);
|
|
||||||
done3: ma_data_source_node_uninit(&g_sourceNode, NULL);
|
|
||||||
done2: ma_vocoder_node_uninit(&g_vocoderNode, NULL);
|
|
||||||
done1: ma_node_graph_uninit(&g_nodeGraph, NULL);
|
|
||||||
done0: ma_device_uninit(&device);
|
|
||||||
|
|
||||||
(void)argc;
|
|
||||||
(void)argv;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
@@ -1,34 +1 @@
|
|||||||
/*
|
#include "../../examples/engine_hello_world.c"
|
||||||
This example demonstrates how to initialize an audio engine and play a sound.
|
|
||||||
|
|
||||||
This will play the sound specified on the command line.
|
|
||||||
*/
|
|
||||||
#define MINIAUDIO_IMPLEMENTATION
|
|
||||||
#include "../../miniaudio.h"
|
|
||||||
#include "../miniaudio_engine.h"
|
|
||||||
|
|
||||||
int main(int argc, char** argv)
|
|
||||||
{
|
|
||||||
ma_result result;
|
|
||||||
ma_engine engine;
|
|
||||||
|
|
||||||
if (argc < 2) {
|
|
||||||
printf("No input file.");
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
result = ma_engine_init(NULL, &engine);
|
|
||||||
if (result != MA_SUCCESS) {
|
|
||||||
printf("Failed to initialize audio engine.");
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
ma_engine_play_sound(&engine, argv[1], NULL);
|
|
||||||
|
|
||||||
printf("Press Enter to quit...");
|
|
||||||
getchar();
|
|
||||||
|
|
||||||
ma_engine_uninit(&engine);
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
@@ -1,149 +1 @@
|
|||||||
/*
|
#include "../../examples/resource_manager.c"
|
||||||
Demonstrates how you can use the resource manager to manage loaded sounds.
|
|
||||||
|
|
||||||
This example loads the first sound specified on the command line via the resource manager and then plays it using the
|
|
||||||
low level API.
|
|
||||||
|
|
||||||
You can control whether or not you want to load the sound asynchronously and whether or not you want to store the data
|
|
||||||
in-memory or stream it. When storing the sound in-memory you can also control whether or not it is decoded. To do this,
|
|
||||||
specify a combination of the following options in `ma_resource_manager_data_source_init()`:
|
|
||||||
|
|
||||||
* MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_ASYNC - Load asynchronously.
|
|
||||||
* MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_DECODE - Store the sound in-memory in uncompressed/decoded format.
|
|
||||||
* MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_STREAM - Stream the sound from disk rather than storing entirely in memory. Useful for music.
|
|
||||||
|
|
||||||
The object returned by the resource manager is just a standard data source which means it can be plugged into any of
|
|
||||||
`ma_data_source_*()` APIs just like any other data source and it should just work.
|
|
||||||
|
|
||||||
Internally, there's a background thread that's used to process jobs and enable asynchronicity. By default there is only
|
|
||||||
a single job thread, but this can be configured in the resource manager config. You can also implement your own threads
|
|
||||||
for processing jobs. That is more advanced, and beyond the scope of this example.
|
|
||||||
|
|
||||||
When you initialize a resource manager you can specify the sample format, channels and sample rate to use when reading
|
|
||||||
data from the data source. This means the resource manager will ensure all sounds will have a standard format. When not
|
|
||||||
set, each sound will have their own formats and you'll need to do the necessary data conversion yourself.
|
|
||||||
*/
|
|
||||||
#define MA_NO_ENGINE /* We're intentionally not using the ma_engine API here. */
|
|
||||||
#define MINIAUDIO_IMPLEMENTATION
|
|
||||||
#include "../../miniaudio.h"
|
|
||||||
#include "../miniaudio_engine.h"
|
|
||||||
|
|
||||||
#ifdef __EMSCRIPTEN__
|
|
||||||
#include <emscripten.h>
|
|
||||||
|
|
||||||
void main_loop__em(void* pUserData)
|
|
||||||
{
|
|
||||||
ma_resource_manager* pResourceManager = (ma_resource_manager*)pUserData;
|
|
||||||
MA_ASSERT(pResourceManager != NULL);
|
|
||||||
|
|
||||||
/*
|
|
||||||
The Emscripten build does not support threading which means we need to process jobs manually. If
|
|
||||||
there are no jobs needing to be processed this will return immediately with MA_NO_DATA_AVAILABLE.
|
|
||||||
*/
|
|
||||||
ma_resource_manager_process_next_job(pResourceManager);
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
void data_callback(ma_device* pDevice, void* pOutput, const void* pInput, ma_uint32 frameCount)
|
|
||||||
{
|
|
||||||
ma_data_source_read_pcm_frames((ma_data_source*)pDevice->pUserData, pOutput, frameCount, NULL, MA_TRUE);
|
|
||||||
|
|
||||||
(void)pInput;
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(int argc, char** argv)
|
|
||||||
{
|
|
||||||
ma_result result;
|
|
||||||
ma_device_config deviceConfig;
|
|
||||||
ma_device device;
|
|
||||||
ma_resource_manager_config resourceManagerConfig;
|
|
||||||
ma_resource_manager resourceManager;
|
|
||||||
ma_resource_manager_data_source dataSource;
|
|
||||||
|
|
||||||
if (argc < 2) {
|
|
||||||
printf("No input file.");
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* We'll initialize the device first. */
|
|
||||||
deviceConfig = ma_device_config_init(ma_device_type_playback);
|
|
||||||
deviceConfig.dataCallback = data_callback;
|
|
||||||
deviceConfig.pUserData = &dataSource; /* <-- We'll be reading from this in the data callback. */
|
|
||||||
|
|
||||||
result = ma_device_init(NULL, &deviceConfig, &device);
|
|
||||||
if (result != MA_SUCCESS) {
|
|
||||||
printf("Failed to initialize device.");
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
We have the device so now we want to initialize the resource manager. We'll use the resource manager to load a
|
|
||||||
sound based on the command line.
|
|
||||||
*/
|
|
||||||
resourceManagerConfig = ma_resource_manager_config_init();
|
|
||||||
resourceManagerConfig.decodedFormat = device.playback.format;
|
|
||||||
resourceManagerConfig.decodedChannels = device.playback.channels;
|
|
||||||
resourceManagerConfig.decodedSampleRate = device.sampleRate;
|
|
||||||
|
|
||||||
/*
|
|
||||||
We're not supporting threading with Emscripten so go ahead and disable threading. It's important
|
|
||||||
that we set the appropriate flag and also the job thread count to 0.
|
|
||||||
*/
|
|
||||||
#ifdef __EMSCRIPTEN__
|
|
||||||
resourceManagerConfig.flags |= MA_RESOURCE_MANAGER_FLAG_NO_THREADING;
|
|
||||||
resourceManagerConfig.jobThreadCount = 0;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
result = ma_resource_manager_init(&resourceManagerConfig, &resourceManager);
|
|
||||||
if (result != MA_SUCCESS) {
|
|
||||||
ma_device_uninit(&device);
|
|
||||||
printf("Failed to initialize the resource manager.");
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Now that we have a resource manager we can load a sound. */
|
|
||||||
result = ma_resource_manager_data_source_init(
|
|
||||||
&resourceManager,
|
|
||||||
argv[1],
|
|
||||||
MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_DECODE | MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_ASYNC | MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_STREAM,
|
|
||||||
NULL, /* Async notification. */
|
|
||||||
&dataSource);
|
|
||||||
if (result != MA_SUCCESS) {
|
|
||||||
printf("Failed to load sound \"%s\".", argv[1]);
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Now that we have a sound we can start the device. */
|
|
||||||
result = ma_device_start(&device);
|
|
||||||
if (result != MA_SUCCESS) {
|
|
||||||
ma_device_uninit(&device);
|
|
||||||
printf("Failed to start device.");
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
#ifdef __EMSCRIPTEN__
|
|
||||||
emscripten_set_main_loop_arg(main_loop__em, &resourceManager, 0, 1);
|
|
||||||
#else
|
|
||||||
printf("Press Enter to quit...\n");
|
|
||||||
getchar();
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/* Teardown. */
|
|
||||||
|
|
||||||
/* Uninitialize the device first to ensure the data callback is stopped and doesn't try to access any data. */
|
|
||||||
ma_device_uninit(&device);
|
|
||||||
|
|
||||||
/*
|
|
||||||
Before uninitializing the resource manager we need to uninitialize every data source. The data source is owned by
|
|
||||||
the caller which means you're responsible for uninitializing it.
|
|
||||||
*/
|
|
||||||
ma_resource_manager_data_source_uninit(&dataSource);
|
|
||||||
|
|
||||||
/* Uninitialize the resource manager after each data source. */
|
|
||||||
ma_resource_manager_uninit(&resourceManager);
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
@@ -1,327 +1 @@
|
|||||||
/*
|
#include "../../examples/resource_manager_advanced.c"
|
||||||
Demonstrates how you can use the resource manager to manage loaded sounds.
|
|
||||||
|
|
||||||
The resource manager can be used to create a data source whose resources are managed internally by miniaudio. The data
|
|
||||||
sources can then be read just like any other data source such as decoders and audio buffers.
|
|
||||||
|
|
||||||
In this example we use the resource manager independently of the `ma_engine` API so that we can demonstrate how it can
|
|
||||||
be used by itself without getting it confused with `ma_engine`.
|
|
||||||
|
|
||||||
The main feature of the resource manager is the ability to decode and stream audio data asynchronously. Asynchronicity
|
|
||||||
is achieved with a job system. The resource manager will issue jobs which are processed by a configurable number of job
|
|
||||||
threads. You can also implement your own custom job threads which this example also demonstrates.
|
|
||||||
|
|
||||||
In this example we show how you can create a data source, mix them with other data sources, configure the number of job
|
|
||||||
threads to manage internally and how to implement your own custom job thread.
|
|
||||||
*/
|
|
||||||
#define MA_NO_ENGINE /* We're intentionally not using the ma_engine API here. */
|
|
||||||
#define MINIAUDIO_IMPLEMENTATION
|
|
||||||
#include "../../miniaudio.h"
|
|
||||||
#include "../miniaudio_engine.h"
|
|
||||||
|
|
||||||
static ma_resource_manager_data_source g_dataSources[16];
|
|
||||||
static ma_uint32 g_dataSourceCount;
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
TODO: Consider putting these public functions in miniaudio.h. Will depend on ma_mix_pcm_frames_f32()
|
|
||||||
being merged into miniaudio.h (it's currently in miniaudio_engine.h).
|
|
||||||
*/
|
|
||||||
static ma_result ma_data_source_read_pcm_frames_f32_ex(ma_data_source* pDataSource, float* pFramesOut, ma_uint64 frameCount, ma_uint64* pFramesRead, ma_bool32 loop, ma_format dataSourceFormat, ma_uint32 dataSourceChannels)
|
|
||||||
{
|
|
||||||
/*
|
|
||||||
This function is intended to be used when the format and channel count of the data source is
|
|
||||||
known beforehand. The idea is to avoid overhead due to redundant calls to ma_data_source_get_data_format().
|
|
||||||
*/
|
|
||||||
MA_ASSERT(pDataSource != NULL);
|
|
||||||
|
|
||||||
if (dataSourceFormat == ma_format_f32) {
|
|
||||||
/* Fast path. No conversion necessary. */
|
|
||||||
return ma_data_source_read_pcm_frames(pDataSource, pFramesOut, frameCount, pFramesRead, loop);
|
|
||||||
} else {
|
|
||||||
/* Slow path. Conversion necessary. */
|
|
||||||
ma_result result;
|
|
||||||
ma_uint64 totalFramesRead;
|
|
||||||
ma_uint8 temp[MA_DATA_CONVERTER_STACK_BUFFER_SIZE];
|
|
||||||
ma_uint64 tempCapInFrames = sizeof(temp) / ma_get_bytes_per_frame(dataSourceFormat, dataSourceChannels);
|
|
||||||
|
|
||||||
totalFramesRead = 0;
|
|
||||||
while (totalFramesRead < frameCount) {
|
|
||||||
ma_uint64 framesJustRead;
|
|
||||||
ma_uint64 framesToRead = frameCount - totalFramesRead;
|
|
||||||
if (framesToRead > tempCapInFrames) {
|
|
||||||
framesToRead = tempCapInFrames;
|
|
||||||
}
|
|
||||||
|
|
||||||
result = ma_data_source_read_pcm_frames(pDataSource, pFramesOut, framesToRead, &framesJustRead, loop);
|
|
||||||
|
|
||||||
ma_convert_pcm_frames_format(ma_offset_pcm_frames_ptr_f32(pFramesOut, totalFramesRead, dataSourceChannels), ma_format_f32, temp, dataSourceFormat, framesJustRead, dataSourceChannels, ma_dither_mode_none);
|
|
||||||
totalFramesRead += framesJustRead;
|
|
||||||
|
|
||||||
if (result != MA_SUCCESS) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return MA_SUCCESS;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MA_API ma_result ma_data_source_read_pcm_frames_f32(ma_data_source* pDataSource, float* pFramesOut, ma_uint64 frameCount, ma_uint64* pFramesRead, ma_bool32 loop)
|
|
||||||
{
|
|
||||||
ma_result result;
|
|
||||||
ma_format format;
|
|
||||||
ma_uint32 channels;
|
|
||||||
|
|
||||||
result = ma_data_source_get_data_format(pDataSource, &format, &channels, NULL, NULL, 0);
|
|
||||||
if (result != MA_SUCCESS) {
|
|
||||||
return result; /* Failed to retrieve the data format of the data source. */
|
|
||||||
}
|
|
||||||
|
|
||||||
return ma_data_source_read_pcm_frames_f32_ex(pDataSource, pFramesOut, frameCount, pFramesRead, loop, format, channels);
|
|
||||||
}
|
|
||||||
|
|
||||||
MA_API ma_result ma_data_source_read_pcm_frames_and_mix_f32(ma_data_source* pDataSource, float* pFramesOut, ma_uint64 frameCount, ma_uint64* pFramesRead, ma_bool32 loop, float volume)
|
|
||||||
{
|
|
||||||
ma_result result;
|
|
||||||
ma_format format;
|
|
||||||
ma_uint32 channels;
|
|
||||||
ma_uint64 totalFramesRead;
|
|
||||||
|
|
||||||
if (pFramesRead != NULL) {
|
|
||||||
*pFramesRead = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pDataSource == NULL) {
|
|
||||||
return MA_INVALID_ARGS;
|
|
||||||
}
|
|
||||||
|
|
||||||
result = ma_data_source_get_data_format(pDataSource, &format, &channels, NULL, NULL, 0);
|
|
||||||
if (result != MA_SUCCESS) {
|
|
||||||
return result; /* Failed to retrieve the data format of the data source. */
|
|
||||||
}
|
|
||||||
|
|
||||||
totalFramesRead = 0;
|
|
||||||
while (totalFramesRead < frameCount) {
|
|
||||||
float temp[MA_DATA_CONVERTER_STACK_BUFFER_SIZE/sizeof(float)];
|
|
||||||
ma_uint64 tempCapInFrames = ma_countof(temp) / channels;
|
|
||||||
ma_uint64 framesJustRead;
|
|
||||||
ma_uint64 framesToRead = frameCount - totalFramesRead;
|
|
||||||
if (framesToRead > tempCapInFrames) {
|
|
||||||
framesToRead = tempCapInFrames;
|
|
||||||
}
|
|
||||||
|
|
||||||
result = ma_data_source_read_pcm_frames_f32_ex(pDataSource, temp, framesToRead, &framesJustRead, loop, format, channels);
|
|
||||||
|
|
||||||
ma_mix_pcm_frames_f32(ma_offset_pcm_frames_ptr(pFramesOut, totalFramesRead, ma_format_f32, channels), temp, framesJustRead, channels, volume);
|
|
||||||
totalFramesRead += framesJustRead;
|
|
||||||
|
|
||||||
if (result != MA_SUCCESS) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pFramesRead != NULL) {
|
|
||||||
*pFramesRead = totalFramesRead;
|
|
||||||
}
|
|
||||||
|
|
||||||
return MA_SUCCESS;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void data_callback(ma_device* pDevice, void* pOutput, const void* pInput, ma_uint32 frameCount)
|
|
||||||
{
|
|
||||||
/*
|
|
||||||
In this example we're just going to play our data sources layered on top of each other. This
|
|
||||||
assumes the device's format is f32 and that the buffer is not pre-silenced.
|
|
||||||
*/
|
|
||||||
ma_uint32 iDataSource;
|
|
||||||
|
|
||||||
MA_ASSERT(pDevice->playback.format == ma_format_f32);
|
|
||||||
|
|
||||||
(void)pInput; /* Unused. */
|
|
||||||
|
|
||||||
/*
|
|
||||||
If the device was configured with noPreSilencedOutputBuffer then you would need to silence the
|
|
||||||
buffer here, or make sure the first data source to be mixed is copied rather than mixed.
|
|
||||||
*/
|
|
||||||
/*ma_silence_pcm_frames(pOutput, frameCount, ma_format_f32, pDevice->playback.channels);*/
|
|
||||||
|
|
||||||
/* For each sound, mix as much data as we can. */
|
|
||||||
for (iDataSource = 0; iDataSource < g_dataSourceCount; iDataSource += 1) {
|
|
||||||
ma_data_source_read_pcm_frames_and_mix_f32(&g_dataSources[iDataSource], (float*)pOutput, frameCount, NULL, MA_TRUE, /* volume = */1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static ma_thread_result MA_THREADCALL custom_job_thread(void* pUserData)
|
|
||||||
{
|
|
||||||
ma_resource_manager* pResourceManager = (ma_resource_manager*)pUserData;
|
|
||||||
MA_ASSERT(pResourceManager != NULL);
|
|
||||||
|
|
||||||
for (;;) {
|
|
||||||
ma_result result;
|
|
||||||
ma_resource_manager_job job;
|
|
||||||
|
|
||||||
/*
|
|
||||||
Retrieve a job from the queue first. This defines what it is you're about to do. By default this will be
|
|
||||||
blocking. You can initialize the resource manager with MA_RESOURCE_MANAGER_FLAG_NON_BLOCKING to not block in
|
|
||||||
which case MA_NO_DATA_AVAILABLE will be returned if no jobs are available.
|
|
||||||
|
|
||||||
When the quit job is returned (MA_RESOURCE_MANAGER_JOB_QUIT), the return value will always be MA_CANCELLED. If you don't want
|
|
||||||
to check the return value (you should), you can instead check if the job code is MA_RESOURCE_MANAGER_JOB_QUIT and use that
|
|
||||||
instead.
|
|
||||||
*/
|
|
||||||
result = ma_resource_manager_next_job(pResourceManager, &job);
|
|
||||||
if (result != MA_SUCCESS) {
|
|
||||||
if (result == MA_CANCELLED) {
|
|
||||||
printf("CUSTOM JOB THREAD TERMINATING VIA MA_CANCELLED... ");
|
|
||||||
} else {
|
|
||||||
printf("CUSTOM JOB THREAD ERROR: %s. TERMINATING... ", ma_result_description(result));
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Terminate if we got a quit message. You don't need to terminate like this, but's a bit more robust. You can
|
|
||||||
just use a global variable or something similar if it's easier for your particular situation. The quit job
|
|
||||||
remains in the queue and will continue to be returned by future calls to ma_resource_manager_next_job(). The
|
|
||||||
reason for this is to give every job thread visibility to the quit job so they have a chance to exit.
|
|
||||||
|
|
||||||
We won't actually be hitting this code because the call above will return MA_CANCELLED when the MA_RESOURCE_MANAGER_JOB_QUIT
|
|
||||||
event is received which means the `result != MA_SUCCESS` logic above will catch it. If you do not check the
|
|
||||||
return value of ma_resource_manager_next_job() you will want to check for MA_RESOURCE_MANAGER_JOB_QUIT like the code below.
|
|
||||||
*/
|
|
||||||
if (job.toc.breakup.code == MA_RESOURCE_MANAGER_JOB_QUIT) {
|
|
||||||
printf("CUSTOM JOB THREAD TERMINATING VIA MA_RESOURCE_MANAGER_JOB_QUIT... ");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Call ma_resource_manager_process_job() to actually do the work to process the job. */
|
|
||||||
printf("PROCESSING IN CUSTOM JOB THREAD: %d\n", job.toc.breakup.code);
|
|
||||||
ma_resource_manager_process_job(pResourceManager, &job);
|
|
||||||
}
|
|
||||||
|
|
||||||
printf("TERMINATED\n");
|
|
||||||
return (ma_thread_result)0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(int argc, char** argv)
|
|
||||||
{
|
|
||||||
ma_result result;
|
|
||||||
ma_device_config deviceConfig;
|
|
||||||
ma_device device;
|
|
||||||
ma_resource_manager_config resourceManagerConfig;
|
|
||||||
ma_resource_manager resourceManager;
|
|
||||||
ma_thread jobThread;
|
|
||||||
int iFile;
|
|
||||||
|
|
||||||
deviceConfig = ma_device_config_init(ma_device_type_playback);
|
|
||||||
deviceConfig.playback.format = ma_format_f32;
|
|
||||||
deviceConfig.dataCallback = data_callback;
|
|
||||||
deviceConfig.pUserData = NULL;
|
|
||||||
|
|
||||||
result = ma_device_init(NULL, &deviceConfig, &device);
|
|
||||||
if (result != MA_SUCCESS) {
|
|
||||||
printf("Failed to initialize device.");
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* We can start the device before loading any sounds. We'll just end up outputting silence. */
|
|
||||||
result = ma_device_start(&device);
|
|
||||||
if (result != MA_SUCCESS) {
|
|
||||||
ma_device_uninit(&device);
|
|
||||||
printf("Failed to start device.");
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
We have the device so now we want to initialize the resource manager. We'll use the resource manager to load some
|
|
||||||
sounds based on the command line.
|
|
||||||
*/
|
|
||||||
resourceManagerConfig = ma_resource_manager_config_init();
|
|
||||||
|
|
||||||
/*
|
|
||||||
We'll set a standard decoding format to save us to processing time at mixing time. If you're wanting to use
|
|
||||||
spatialization with your decoded sounds, you may want to consider leaving this as 0 to ensure the file's native
|
|
||||||
channel count is used so you can do proper spatialization.
|
|
||||||
*/
|
|
||||||
resourceManagerConfig.decodedFormat = device.playback.format;
|
|
||||||
resourceManagerConfig.decodedChannels = device.playback.channels;
|
|
||||||
resourceManagerConfig.decodedSampleRate = device.sampleRate;
|
|
||||||
|
|
||||||
/* The number of job threads to be managed internally. Set this to 0 if you want to self-manage your job threads */
|
|
||||||
resourceManagerConfig.jobThreadCount = 4;
|
|
||||||
|
|
||||||
result = ma_resource_manager_init(&resourceManagerConfig, &resourceManager);
|
|
||||||
if (result != MA_SUCCESS) {
|
|
||||||
ma_device_uninit(&device);
|
|
||||||
printf("Failed to initialize the resource manager.");
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Now that we have a resource manager we can set up our custom job thread. This is optional. Normally when doing
|
|
||||||
self-managed job threads you would set the internal job thread count to zero. We're doing both internal and
|
|
||||||
self-managed job threads in this example just for demonstration purposes.
|
|
||||||
*/
|
|
||||||
ma_thread_create(&jobThread, ma_thread_priority_default, 0, custom_job_thread, &resourceManager, NULL);
|
|
||||||
|
|
||||||
/* Create each data source from the resource manager. Note that the caller is the owner. */
|
|
||||||
for (iFile = 0; iFile < ma_countof(g_dataSources) && iFile < argc-1; iFile += 1) {
|
|
||||||
result = ma_resource_manager_data_source_init(
|
|
||||||
&resourceManager,
|
|
||||||
argv[iFile+1],
|
|
||||||
MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_DECODE | MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_ASYNC /*| MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_STREAM*/,
|
|
||||||
NULL, /* Async notification. */
|
|
||||||
&g_dataSources[iFile]);
|
|
||||||
|
|
||||||
if (result != MA_SUCCESS) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
g_dataSourceCount += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
printf("Press Enter to quit...");
|
|
||||||
getchar();
|
|
||||||
|
|
||||||
|
|
||||||
/* Teardown. */
|
|
||||||
|
|
||||||
/*
|
|
||||||
Uninitialize the device first to ensure the data callback is stopped and doesn't try to access
|
|
||||||
any data.
|
|
||||||
*/
|
|
||||||
ma_device_uninit(&device);
|
|
||||||
|
|
||||||
/*
|
|
||||||
Our data sources need to be explicitly uninitialized. ma_resource_manager_uninit() will not do
|
|
||||||
it for us. This needs to be done before posting the quit event and uninitializing the resource
|
|
||||||
manager or else we'll get stuck in a deadlock because ma_resource_manager_data_source_uninit()
|
|
||||||
will be waiting for the job thread(s) to finish work, which will never happen because they were
|
|
||||||
just terminated.
|
|
||||||
*/
|
|
||||||
for (iFile = 0; (size_t)iFile < g_dataSourceCount; iFile += 1) {
|
|
||||||
ma_resource_manager_data_source_uninit(&g_dataSources[iFile]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Before uninitializing the resource manager we need to make sure a quit event has been posted to
|
|
||||||
ensure we can get out of our custom thread. The call to ma_resource_manager_uninit() will also
|
|
||||||
do this, but we need to call it explicitly so that our self-managed thread can exit naturally.
|
|
||||||
You only need to post a quit job if you're using that as the exit indicator. You can instead
|
|
||||||
use whatever variable you want to terminate your job thread, but since this example is using a
|
|
||||||
quit job we need to post one. Note that you don't need to do this if you're not managing your
|
|
||||||
own threads - ma_resource_manager_uninit() alone will suffice in that case.
|
|
||||||
*/
|
|
||||||
ma_resource_manager_post_job_quit(&resourceManager);
|
|
||||||
ma_thread_wait(&jobThread); /* Wait for the custom job thread to finish so it doesn't try to access any data. */
|
|
||||||
|
|
||||||
/* Uninitialize the resource manager after each data source. */
|
|
||||||
ma_resource_manager_uninit(&resourceManager);
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user