From 9535551f3c38bf55eb864009f93513ba8f212000 Mon Sep 17 00:00:00 2001 From: David Reid Date: Sat, 5 Sep 2020 10:49:57 +1000 Subject: [PATCH] Add a new resource manager example and simplify the old one. --- research/_examples/resource_manager.c | 207 ++++------------ .../_examples/resource_manager_advanced.c | 229 ++++++++++++++++++ 2 files changed, 279 insertions(+), 157 deletions(-) create mode 100644 research/_examples/resource_manager_advanced.c diff --git a/research/_examples/resource_manager.c b/research/_examples/resource_manager.c index d7e9f676..6ad5b3a5 100644 --- a/research/_examples/resource_manager.c +++ b/research/_examples/resource_manager.c @@ -1,106 +1,40 @@ /* 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. +This example loads the first sound specified on the command line via the resource manager and then plays it using the +low level API. -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`. Audio data is mixed using the `ma_mixer` API, but you -can also use data sources with `ma_data_source_read_pcm_frames()` in the same way we do in the simple_looping example. +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()`: -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. + * MA_DATA_SOURCE_FLAG_ASYNC - Load asynchronously. + * MA_DATA_SOURCE_FLAG_DECODE - Store the sound in-memory in uncompressed/decoded format. + * MA_DATA_SOURCE_FLAG_STREAM - Stream the sound from disk rather than storing entirely in memory. Useful for music. -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. +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" -static ma_mixer g_mixer; -static ma_resource_manager_data_source g_dataSources[16]; -static ma_uint32 g_dataSourceCount; - 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. */ - ma_uint32 bpf = ma_get_bytes_per_frame(pDevice->playback.format, pDevice->playback.channels); - ma_uint32 framesProcessed = 0; - while (framesProcessed < frameCount) { - ma_uint64 frameCountIn; - ma_uint64 frameCountOut = (frameCount - framesProcessed); - - ma_mixer_begin(&g_mixer, NULL, &frameCountOut, &frameCountIn); - { - size_t iDataSource; - for (iDataSource = 0; iDataSource < g_dataSourceCount; iDataSource += 1) { - ma_mixer_mix_data_source(&g_mixer, &g_dataSources[iDataSource], 0, frameCountIn, NULL, 1, NULL, MA_TRUE); - } - } - ma_mixer_end(&g_mixer, NULL, ma_offset_ptr(pOutput, framesProcessed * bpf), 0); - - framesProcessed += (ma_uint32)frameCountOut; /* Safe cast. */ - } + ma_data_source_read_pcm_frames((ma_data_source*)pDevice->pUserData, pOutput, frameCount, NULL, MA_TRUE); (void)pInput; } -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_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_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_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 you 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_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_JOB_QUIT like the code below. - */ - if (job.toc.code == MA_JOB_QUIT) { - printf("CUSTOM JOB THREAD TERMINATING VIA MA_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.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; @@ -108,13 +42,18 @@ int main(int argc, char** argv) ma_device device; ma_resource_manager_config resourceManagerConfig; ma_resource_manager resourceManager; - ma_mixer_config mixerConfig; - ma_thread jobThread; - int iFile; + 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 = NULL; + deviceConfig.pUserData = &dataSource; /* <-- We'll be reading from this in the data callback. */ result = ma_device_init(NULL, &deviceConfig, &device); if (result != MA_SUCCESS) { @@ -122,46 +61,16 @@ int main(int argc, char** argv) return -1; } - /* - Before starting the device we'll need to initialize the mixer. If we don't do this first, the data callback will be - fired and will try to use the mixer without it being initialized. - */ - mixerConfig = ma_mixer_config_init(device.playback.format, device.playback.channels, 1024, NULL, NULL); - - result = ma_mixer_init(&mixerConfig, &g_mixer); - if (result != MA_SUCCESS) { - ma_device_uninit(&device); - printf("Failed to initialize mixer."); - 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. + 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(); - - /* - 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); @@ -169,27 +78,26 @@ int main(int argc, char** argv) 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); + /* Now that we have a resource manager we can load a sound. */ + result = ma_resource_manager_data_source_init( + &resourceManager, + argv[1], + MA_DATA_SOURCE_FLAG_DECODE | MA_DATA_SOURCE_FLAG_ASYNC /*| MA_DATA_SOURCE_FLAG_STREAM*/, + NULL, /* Async notification. */ + &dataSource); - /* 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_DATA_SOURCE_FLAG_DECODE | MA_DATA_SOURCE_FLAG_ASYNC /*| MA_DATA_SOURCE_FLAG_STREAM*/, - NULL, /* Async notification. */ - &g_dataSources[iFile]); + if (result != MA_SUCCESS) { + printf("Failed to load sound \"%s\".", argv[1]); + return -1; + } - if (result != MA_SUCCESS) { - break; - } - g_dataSourceCount += 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; } printf("Press Enter to quit..."); @@ -202,28 +110,13 @@ int main(int argc, char** argv) ma_device_uninit(&device); /* - 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 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. + 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_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. */ - - /* Our data sources need to be explicitly uninitialized. ma_resource_manager_uninit() will not do it for us. */ - for (iFile = 0; (size_t)iFile < g_dataSourceCount; iFile += 1) { - ma_resource_manager_data_source_uninit(&g_dataSources[iFile]); - } + ma_resource_manager_data_source_uninit(&dataSource); /* Uninitialize the resource manager after each data source. */ ma_resource_manager_uninit(&resourceManager); - /* - We're uninitializing the mixer last, but it doesn't matter when it's done, so long as it's after the device has - been stopped/uninitialized. - */ - ma_mixer_uninit(&g_mixer); - return 0; } diff --git a/research/_examples/resource_manager_advanced.c b/research/_examples/resource_manager_advanced.c new file mode 100644 index 00000000..d7e9f676 --- /dev/null +++ b/research/_examples/resource_manager_advanced.c @@ -0,0 +1,229 @@ +/* +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`. Audio data is mixed using the `ma_mixer` API, but you +can also use data sources with `ma_data_source_read_pcm_frames()` in the same way we do in the simple_looping example. + +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_mixer g_mixer; +static ma_resource_manager_data_source g_dataSources[16]; +static ma_uint32 g_dataSourceCount; + +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. */ + ma_uint32 bpf = ma_get_bytes_per_frame(pDevice->playback.format, pDevice->playback.channels); + ma_uint32 framesProcessed = 0; + while (framesProcessed < frameCount) { + ma_uint64 frameCountIn; + ma_uint64 frameCountOut = (frameCount - framesProcessed); + + ma_mixer_begin(&g_mixer, NULL, &frameCountOut, &frameCountIn); + { + size_t iDataSource; + for (iDataSource = 0; iDataSource < g_dataSourceCount; iDataSource += 1) { + ma_mixer_mix_data_source(&g_mixer, &g_dataSources[iDataSource], 0, frameCountIn, NULL, 1, NULL, MA_TRUE); + } + } + ma_mixer_end(&g_mixer, NULL, ma_offset_ptr(pOutput, framesProcessed * bpf), 0); + + framesProcessed += (ma_uint32)frameCountOut; /* Safe cast. */ + } + + (void)pInput; +} + +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_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_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_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 you 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_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_JOB_QUIT like the code below. + */ + if (job.toc.code == MA_JOB_QUIT) { + printf("CUSTOM JOB THREAD TERMINATING VIA MA_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.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_mixer_config mixerConfig; + ma_thread jobThread; + int iFile; + + deviceConfig = ma_device_config_init(ma_device_type_playback); + 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; + } + + /* + Before starting the device we'll need to initialize the mixer. If we don't do this first, the data callback will be + fired and will try to use the mixer without it being initialized. + */ + mixerConfig = ma_mixer_config_init(device.playback.format, device.playback.channels, 1024, NULL, NULL); + + result = ma_mixer_init(&mixerConfig, &g_mixer); + if (result != MA_SUCCESS) { + ma_device_uninit(&device); + printf("Failed to initialize mixer."); + 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); + + /* 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_DATA_SOURCE_FLAG_DECODE | MA_DATA_SOURCE_FLAG_ASYNC /*| MA_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); + + /* + 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 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. + */ + 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. */ + + /* Our data sources need to be explicitly uninitialized. ma_resource_manager_uninit() will not do it for us. */ + for (iFile = 0; (size_t)iFile < g_dataSourceCount; iFile += 1) { + ma_resource_manager_data_source_uninit(&g_dataSources[iFile]); + } + + /* Uninitialize the resource manager after each data source. */ + ma_resource_manager_uninit(&resourceManager); + + /* + We're uninitializing the mixer last, but it doesn't matter when it's done, so long as it's after the device has + been stopped/uninitialized. + */ + ma_mixer_uninit(&g_mixer); + + return 0; +}