From a81f09d93c0a9aacda3511e69df32d60ea205f2f Mon Sep 17 00:00:00 2001 From: David Reid Date: Thu, 17 Aug 2023 19:45:15 +1000 Subject: [PATCH] Add osaudio to the extras folder. This is just a small project to experiment with a few API ideas. This is not a replacement for miniaudio or anything so don't panic. --- extras/osaudio/README.md | 49 ++ extras/osaudio/osaudio.h | 594 +++++++++++++++ extras/osaudio/osaudio_miniaudio.c | 946 ++++++++++++++++++++++++ extras/osaudio/tests/osaudio_deviceio.c | 196 +++++ 4 files changed, 1785 insertions(+) create mode 100644 extras/osaudio/README.md create mode 100644 extras/osaudio/osaudio.h create mode 100644 extras/osaudio/osaudio_miniaudio.c create mode 100644 extras/osaudio/tests/osaudio_deviceio.c diff --git a/extras/osaudio/README.md b/extras/osaudio/README.md new file mode 100644 index 00000000..71328810 --- /dev/null +++ b/extras/osaudio/README.md @@ -0,0 +1,49 @@ +This is just a little experiment to explore some ideas for the kind of API that I would build if I +was building my own operation system. The name "osaudio" means Operating System Audio. Or maybe you +can think of it as Open Source Audio. It's whatever you want it to be. + +The idea behind this project came about after considering the absurd complexity of audio APIs on +various platforms after years of working on miniaudio. This project aims to disprove the idea that +complete and flexible audio solutions and simple APIs are mutually exclusive and that it's possible +to have both. I challenge anybody to prove me wrong. + +In addition to the above, I also wanted to explore some ideas for a different API design to +miniaudio. miniaudio uses a callback model for data transfer, whereas osaudio uses a blocking +read/write model. + +This project is essentially just a header file with a reference implementation that uses miniaudio +under the hood. You can compile this very easily - just compile osaudio_miniaudio.c, and use +osaudio.h just like any other header. There are no dependencies for the header, and the miniaudio +implementation obviously requires miniaudio. Adjust the include path in osaudio_miniaudio.c if need +be. + +See osaudio.h for full documentation. Below is an example to get you started: + +```c +#include "osaudio.h" + +... + +osaudio_t audio; +osaudio_config_t config; + +osaudio_config_init(&config, OSAUDIO_OUTPUT); +config.format = OSAUDIO_FORMAT_F32; +config.channels = 2; +config.rate = 48000; + +osaudio_open(&audio, &config); + +osaudio_write(audio, myAudioData, frameCount); // <-- This will block until all of the data has been sent to the device. + +osaudio_close(audio); +``` + +Compare the code above with the likes of other APIs like Core Audio and PipeWire. I challenge +anybody to argue their APIs are cleaner and easier to use than this when it comes to simple audio +playback. + +If you have any feedback on this I'd be interested to hear it. In particular, I'd really like to +hear from people who believe the likes of Core Audio (Apple), PipeWire, PulseAudio or any other +audio API actually have good APIs (they don't!) and what makes their's better and/or worse than +this project. diff --git a/extras/osaudio/osaudio.h b/extras/osaudio/osaudio.h new file mode 100644 index 00000000..13030111 --- /dev/null +++ b/extras/osaudio/osaudio.h @@ -0,0 +1,594 @@ +/* +This is a simple API for low-level audio playback and capture. A reference implementation using +miniaudio is provided in osaudio.c which can be found alongside this file. Consider all code +public domain. + +The idea behind this project came about after considering the absurd complexity of audio APIs on +various platforms after years of working on miniaudio. This project aims to disprove the idea that +complete and flexible audio solutions and simple APIs are mutually exclusive and that it's possible +to have both. The idea of reliability through simplicity is the first and foremost goal of this +project. The difference between this project and miniaudio is that this project is designed around +the idea of what I would build if I was building an audio API for an operating system, such as at +the level of WASAPI or ALSA. A cross-platform and cross-backend library like miniaudio is +necessarily different in design, but there are indeed things that I would have done differently if +given my time again, some of those ideas of which I'm expressing in this project. + +--- + +The concept of low-level audio is simple - you have a device, such as a speaker system or a +micrphone system, and then you write or read audio data to/from it. So in the case of playback, you +need only write your raw audio data to the device which then emits it from the speakers when it's +ready. Likewise, for capture you simply read audio data from the device which is filled with data +by the microphone. + +A complete low-level audio solution requires the following: + + 1) The ability to enumerate devices that are connected to the system. + 2) The ability to open and close a connection to a device. + 3) The ability to start and stop the device. + 4) The ability to write and read audio data to/from the device. + 5) The ability to query the device for it's data configuration. + 6) The ability to notify the application when certain events occur, such as the device being + stopped, or rerouted. + +The API presented here aims to meet all of the above requirements. It uses a single-threaded +blocking read/write model for data delivery instead of a callback model. This makes it a bit more +flexible since it gives the application full control over the audio thread. It might also make it +more feasible to use this API on single-threaded systems. + +Device enumeration is achieved with a single function: osaudio_enumerate(). This function returns +an array of osaudio_info_t structures which contain information about each device. The array is +allocated must be freed with free(). Contained within the osaudio_info_t struct is, most +importantly, the device ID, which is used to open a connection to the device, and the name of the +device which can be used to display to the user. For advanced users, it also includes information +about the device's native data configuration. + +Opening and closing a connection to a device is achieved with osaudio_open() and osaudio_close(). +An important concept is that of the ability to configure the device. This is achieved with the +osaudio_config_t structure which is passed to osaudio_open(). In addition to the ID of the device, +this structure includes information about the desired format, channel count and sample rate. You +can also configure the latency of the device, or the buffer size, which is specified in frames. A +flags member is used for specifying additional options, such as whether or not to disable automatic +rerouting. Finally a callback can be specified for notifications. When osaudio_open() returns, the +config structure will be filled with the device's actual configuration. You can inspect the channel +map from this structure to know how to arrange the channels in your audio data. + +This API uses a blocking write/read model for pushing and pulling data to/from the device. This +is done with the osaudio_write() and osaudio_read() functions. These functions will block until +the requested number of frames have been processed or the device is drained or flushed with +osaudio_drain() or osaudio_flush() respectively. It is from these functions that the device is +started. As soon as you start writing data with osaudio_write() or reading data with +osaudio_read(), the device will start. When the device is drained of flushed with osaudio_drain() +or osaudio_flush(), the device will be stopped. osaudio_drain() will block until the device has +been drained, whereas osaudio_flush() will stop playback immediately and return. You can also pause +and resume the device with osaudio_pause() and osaudio_resume(). Since reading and writing is +blocking, it can be useful to know how many frames can be written/read without blocking. This is +achieved with osaudio_get_avail(). + +Querying the device's configuration is achieved with osaudio_get_info(). This function will return +a pointer to a osaudio_info_t structure which contains information about the device, most +importantly it's name and data configuration. The name is important for displaying on a UI, and +the data configuration is important for knowing how to format your audio data. The osaudio_info_t +structure will contain an array of osaudio_config_t structures. This will contain one entry, which +will contain the exact information that was returned in the config structure that was passed to +osaudio_open(). + +A common requirement is to open a device that represents the operating system's default device. +This is done easily by simply passing in NULL for the device ID. Below is an example for opening a +default device: + + int result; + osaudio_t audio; + osaudio_config_t config; + + osaudio_config_init(&config, OSAUDIO_OUTPUT); + config.format = OSAUDIO_FORMAT_F32; + config.channels = 2; + config.rate = 48000; + + result = osaudio_open(&audio, &config); + if (result != OSAUDIO_SUCCESS) { + printf("Failed to open device."); + return -1; + } + + ... + + osaudio_close(audio); + +In the above example, the default device is opened for playback (OSAUDIO_OUTPUT). The format is +set to 32-bit floating point (OSAUDIO_FORMAT_F32), the channel count is set to stereo (2), and the +sample rate is set to 48kHz. The device is then closed when we're done with it. + +If instead we wanted to open a specific device, we can do that by passing in the device ID. Below +is an example for how to do this: + + int result; + osaudio_t audio; + osaudio_config_t config; + unsigned int infoCount; + osaudio_info_t* info; + + result = osaudio_enumerate(&infoCount, &info); + if (result != OSAUDIO_SUCCESS) { + printf("Failed to enumerate devices.\n"); + return -1; + } + + // ... Iterate over the `info` array and find the device you want to open. Use the `direction` member to discriminate between input and output ... + + osaudio_config_init(&config, OSAUDIO_OUTPUT); + config.id = &info[indexOfYourChosenDevice].id; + config.format = OSAUDIO_FORMAT_F32; + config.channels = 2; + config.rate = 48000; + + osaudio_open(&audio, &config); + + ... + + osaudio_close(audio); + free(info); // The pointer returned by osaudio_enumerate() must be freed with free(). + +The id structure is just a 256 byte array that uniquely identifies the device. Implementations may +have different representations for device IDs, and A 256 byte array should accomodates all +device ID representations. Implementations are required to zero-fill unused bytes. The osaudio_id_t +structure can be copied which makes it suitable for serialization and deserialization in situations +where you may want to save the device ID to permanent storage so it can be stored in a config file. + +Implementations need to do their own data conversion between the device's native data configuration +and the requested configuration. In this case, when the format, channels and rate are specified in +the config, they should be unchanged when osaudio_open() returns. If this is not possible, the +osaudio_open() will return OSAUDIO_FORMAT_NOT_SUPPORTED. However, there are cases where it's useful +for a program to use the device's native configuration instead of some fixed configuration. This is +achieved by setting the format, channels and rate to 0. Below is an example: + + int result; + osaudio_t audio; + osaudio_config_t config; + + memset(&config, 0, sizeof(config)); + config.direction = OSAUDIO_OUTPUT; + + result = osaudio_open(&audio, &config); + if (result != OSAUDIO_SUCCESS) { + printf("Failed to open device."); + return -1; + } + + // ... `config` will have been updated by osaudio_open() to contain the *actual* format/channels/rate ... + + osaudio_close(audio); + +In addition to the code above, you can explicitly call `osaudio_get_info()` to retrieve the format +configuration. If you need to know the native configuration before opening the device, you can use +enumeration. The format, channels and rate will be contined in the first item in the configs array. + +The examples above all use playback, but the same applies for capture. The only difference is that +the direction is set to OSAUDIO_INPUT instead of OSAUDIO_OUTPUT. + +To output audio from the speakers you need to call osaudio_write(). Likewise, to capture audio from +a microphone you need to call osaudio_read(). These functions will block until the requested number +of frames have been written or read. The device will start automatically. Below is an example for +writing some data to a device: + + int result = osaudio_write(audio, myAudioData, myAudioDataFrameCount); + if (result == OSAUDIO_SUCCESS) { + printf("Successfully wrote %d frames of audio data.\n", myAudioDataFrameCount); + } else { + printf("Failed to write audio data.\n"); + } + +osaudio_write() and osaudio_read() will return OSAUDIO_SUCCESS if the requested number of frames +were written or read. You cannot call osaudio_close() while a write or read operation is in +progress. + +If you want to write or read audio data without blocking, you can use osaudio_get_avail() to +determine how many frames are available for writing or reading. Below is an example: + + unsigned int framesAvailable = osaudio_get_avail(audio); + if (result > 0) { + printf("There are %d frames available for writing.\n", framesAvailable); + } else { + printf("There are no frames available for writing.\n"); + } + +If you want to abort a blocking write or read, you can use osaudio_flush(). This will result in any +pending write or read operation being aborted. + +There are several ways of pausing a device. The first is to just drain or flush the device and +simply don't do any more read/write operations. A drain and flush will put the device into a +stopped state until the next call to either read or write, depending on the device's direction. +If, however, this does not suit your requirements, you can use osaudio_pause() and +osaudio_resume(). Take note, however, that these functions will result in osaudio_drain() never +returning because it'll result in the device being in a stopped state which in turn results in the +buffer never being read and therefore never drained. + +Everything is thread safe with a few minor exceptions which has no practical issues for the client: + + * You cannot call any function while osaudio_open() is still in progress. + * You cannot call osaudio_close() while any other function is still in progress. + * You can only call osaudio_write() and osaudio_read() from one thread at a time. + +None of these issues should be a problem for the client in practice. You won't have a valid +osaudio_t object until osaudio_open() has returned. For osaudio_close(), it makes no sense to +destroy the object while it's still in use, and doing so would mean the client is using very poor +form. For osaudio_write() and osaudio_read(), you wouldn't ever want to call this simultaneously +across multiple threads anyway because otherwise you'd end up with garbage audio. + +The rules above only apply when working with a single osaudio_t object. You can have multiple +osaudio_t objects open at the same time, and you can call any function on different osaudio_t +objects simultaneously from different threads. + +--- + +# Feedback + +I'm looking for feedback on the following: + + * Are the supported formats enough? If not, what other formats are needed, and what is the + justification for including it? Just because it's the native format on one particular + piece of hardware is not enough. Big-endian and little-endian will never be supported. All + formats are native-endian. + * Are the available channel positions enough? What other positions are needed? + * Just some general criticism would be appreciated. + +*/ +#ifndef osaudio_h +#define osaudio_h + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct _osaudio_t* osaudio_t; +typedef struct osaudio_config_t osaudio_config_t; +typedef struct osaudio_id_t osaudio_id_t; +typedef struct osaudio_info_t osaudio_info_t; +typedef struct osaudio_notification_t osaudio_notification_t; + +/* Results codes. */ +typedef int osaudio_result_t; +#define OSAUDIO_SUCCESS 0 +#define OSAUDIO_ERROR -1 +#define OSAUDIO_INVALID_ARGS -2 +#define OSAUDIO_INVALID_OPERATION -3 +#define OSAUDIO_OUT_OF_MEMORY -4 +#define OSAUDIO_FORMAT_NOT_SUPPORTED -101 /* The requested format is not supported. */ +#define OSAUDIO_XRUN -102 /* An underrun or overrun occurred. Can be returned by osaudio_read() or osaudio_write(). */ +#define OSAUDIO_DEVICE_STOPPED -103 /* The device is stopped. Can be returned by osaudio_drain(). It is invalid to call osaudio_drain() on a device that is not running because otherwise it'll get stuck. */ + +/* Directions. Cannot be combined. Use separate osaudio_t objects for birectional setups. */ +typedef int osaudio_direction_t; +#define OSAUDIO_INPUT 1 +#define OSAUDIO_OUTPUT 2 + +/* All formats are native endian and interleaved. */ +typedef int osaudio_format_t; +#define OSAUDIO_FORMAT_UNKNOWN 0 +#define OSAUDIO_FORMAT_F32 1 +#define OSAUDIO_FORMAT_U8 2 +#define OSAUDIO_FORMAT_S16 3 +#define OSAUDIO_FORMAT_S24 4 /* Tightly packed. */ +#define OSAUDIO_FORMAT_S32 5 + +/* Channel positions. */ +typedef unsigned char osaudio_channel_t; +#define OSAUDIO_CHANNEL_NONE 0 +#define OSAUDIO_CHANNEL_MONO 1 +#define OSAUDIO_CHANNEL_FL 2 +#define OSAUDIO_CHANNEL_FR 3 +#define OSAUDIO_CHANNEL_FC 4 +#define OSAUDIO_CHANNEL_LFE 5 +#define OSAUDIO_CHANNEL_BL 6 +#define OSAUDIO_CHANNEL_BR 7 +#define OSAUDIO_CHANNEL_FLC 8 +#define OSAUDIO_CHANNEL_FRC 9 +#define OSAUDIO_CHANNEL_BC 10 +#define OSAUDIO_CHANNEL_SL 11 +#define OSAUDIO_CHANNEL_SR 12 +#define OSAUDIO_CHANNEL_TC 13 +#define OSAUDIO_CHANNEL_TFL 14 +#define OSAUDIO_CHANNEL_TFC 15 +#define OSAUDIO_CHANNEL_TFR 16 +#define OSAUDIO_CHANNEL_TBL 17 +#define OSAUDIO_CHANNEL_TBC 18 +#define OSAUDIO_CHANNEL_TBR 19 +#define OSAUDIO_CHANNEL_AUX0 20 +#define OSAUDIO_CHANNEL_AUX1 21 +#define OSAUDIO_CHANNEL_AUX2 22 +#define OSAUDIO_CHANNEL_AUX3 23 +#define OSAUDIO_CHANNEL_AUX4 24 +#define OSAUDIO_CHANNEL_AUX5 25 +#define OSAUDIO_CHANNEL_AUX6 26 +#define OSAUDIO_CHANNEL_AUX7 27 +#define OSAUDIO_CHANNEL_AUX8 28 +#define OSAUDIO_CHANNEL_AUX9 29 +#define OSAUDIO_CHANNEL_AUX10 30 +#define OSAUDIO_CHANNEL_AUX11 31 +#define OSAUDIO_CHANNEL_AUX12 32 +#define OSAUDIO_CHANNEL_AUX13 33 +#define OSAUDIO_CHANNEL_AUX14 34 +#define OSAUDIO_CHANNEL_AUX15 35 +#define OSAUDIO_CHANNEL_AUX16 36 +#define OSAUDIO_CHANNEL_AUX17 37 +#define OSAUDIO_CHANNEL_AUX18 38 +#define OSAUDIO_CHANNEL_AUX19 39 +#define OSAUDIO_CHANNEL_AUX20 40 +#define OSAUDIO_CHANNEL_AUX21 41 +#define OSAUDIO_CHANNEL_AUX22 42 +#define OSAUDIO_CHANNEL_AUX23 43 +#define OSAUDIO_CHANNEL_AUX24 44 +#define OSAUDIO_CHANNEL_AUX25 45 +#define OSAUDIO_CHANNEL_AUX26 46 +#define OSAUDIO_CHANNEL_AUX27 47 +#define OSAUDIO_CHANNEL_AUX28 48 +#define OSAUDIO_CHANNEL_AUX29 49 +#define OSAUDIO_CHANNEL_AUX30 50 +#define OSAUDIO_CHANNEL_AUX31 51 + +/* The maximum number of channels supported. */ +#define OSAUDIO_MAX_CHANNELS 64 + +/* Notification types. */ +typedef int osaudio_notification_type_t; +#define OSAUDIO_NOTIFICATION_STARTED 0 /* The device was started in response to a call to osaudio_write() or osaudio_read(). */ +#define OSAUDIO_NOTIFICATION_STOPPED 1 /* The device was stopped in response to a call to osaudio_drain() or osaudio_flush(). */ +#define OSAUDIO_NOTIFICATION_REROUTED 2 /* The device was rerouted. Not all implementations need to support rerouting. */ +#define OSAUDIO_NOTIFICATION_INTERRUPTION_BEGIN 3 /* The device was interrupted due to something like a phone call. */ +#define OSAUDIO_NOTIFICATION_INTERRUPTION_END 4 /* The interruption has been ended. */ + +/* Flags. */ +#define OSAUDIO_FLAG_NO_REROUTING 1 /* When set, will tell the implementation to disable automatic rerouting if possible. This is a hint and may be ignored by the implementation. */ +#define OSAUDIO_FLAG_REPORT_XRUN 2 /* When set, will tell the implementation to report underruns and overruns via osaudio_write() and osaudio_read() by aborting and returning OSAUDIO_XRUN. */ + +struct osaudio_notification_t +{ + osaudio_notification_type_t type; /* OSAUDIO_NOTIFICATION_* */ + union + { + struct + { + int _unused; + } started; + struct + { + int _unused; + } stopped; + struct + { + int _unused; + } rerouted; + struct + { + int _unused; + } interruption; + } data; +}; + +struct osaudio_id_t +{ + char data[256]; +}; + +struct osaudio_config_t +{ + osaudio_id_t* device_id; /* Set to NULL to use default device. When non-null, automatic routing will be disabled. */ + osaudio_direction_t direction; /* OSAUDIO_INPUT or OSAUDIO_OUTPUT. Cannot be combined. Use separate osaudio_t objects for bidirectional setups. */ + osaudio_format_t format; /* OSAUDIO_FORMAT_* */ + unsigned int channels; /* Number of channels. */ + unsigned int rate; /* Sample rate in seconds. */ + osaudio_channel_t channel_map[OSAUDIO_MAX_CHANNELS]; /* Leave all items set to 0 for defaults. */ + unsigned int buffer_size; /* In frames. Set to 0 to use the system default. */ + unsigned int flags; /* A combination of OSAUDIO_FLAG_* */ + void (* notification)(void* user_data, const osaudio_notification_t* notification); /* Called when some kind of event occurs, such as a device being closed. Never called from the audio thread. */ + void* user_data; /* Passed to notification(). */ +}; + +struct osaudio_info_t +{ + osaudio_id_t id; + char name[256]; + osaudio_direction_t direction; /* OSAUDIO_INPUT or OSAUDIO_OUTPUT. */ + unsigned int config_count; + osaudio_config_t* configs; +}; + + +/* +Enumerates the available devices. + +On output, `count` will contain the number of items in the `info` array. The array must be freed +with free() when it's no longer needed. + +Use the `direction` member to discriminate between input and output devices. Below is an example: + + unsigned int count; + osaudio_info_t* info; + osaudio_enumerate(&count, &info); + + for (int i = 0; i < count; ++i) { + if (info[i].direction == OSAUDIO_OUTPUT) { + printf("Output device: %s\n", info[i].name); + } else { + printf("Input device: %s\n", info[i].name); + } + } + +You can use the `id` member to open a specific device with osaudio_open(). You do not need to do +device enumeration if you only want to open the default device. +*/ +osaudio_result_t osaudio_enumerate(unsigned int* count, osaudio_info_t** info); + +/* +Initializes a default config. + +The config object will be cleared to zero, with the direction set to `direction`. This will result +in a configuration that uses the device's native format, channels and rate. + +osaudio_config_t is a transparent struct. Just set the relevant fields to the desired values after +calling this function. Example: + + osaudio_config_t config; + osaudio_config_init(&config, OSAUDIO_OUTPUT); + config.format = OSAUDIO_FORMAT_F32; + config.channels = 2; + config.rate = 48000; +*/ +void osaudio_config_init(osaudio_config_t* config, osaudio_direction_t direction); + +/* +Opens a connection to a device. + +On input, config must be filled with the desired configuration. On output, it will be filled with +the actual configuration. + +Initialize the config with osaudio_config_init() and then fill in the desired configuration. Below +is an example: + + osaudio_config_t config; + osaudio_config_init(&config, OSAUDIO_OUTPUT); + config.format = OSAUDIO_FORMAT_F32; + config.channels = 2; + config.rate = 48000; + +When the format, channels or rate are left at their default values, or set to 0 (or +OSAUDIO_FORMAT_UNKNOWN for format), the native format, channels or rate will use the device's +native configuration: + + osaudio_config_t config; + osaudio_config_init(&config, OSAUDIO_OUTPUT); + config.format = OSAUDIO_FORMAT_UNKNOWN; + config.channels = 0; + config.rate = 0; + +The code above is equivalent to this: + + osaudio_config_t config; + osaudio_config_init(&config, OSAUDIO_OUTPUT); + +On output the config will be filled with the actual configuration. The implementation will perform +any necessary data conversion between the requested data configuration and the device's native +configuration. If it cannot, the function will return a OSAUDIO_FORMAT_NOT_SUPPORTED error. In this +case the caller can decide to reinitialize the device to use it's native configuration and do it's +own data conversion, or abort if it cannot do so. Use the channel map to determine the ordering of +your channels. Automatic channel map conversion is not performed - that must be done manually by +the caller when transfering data to/from the device. + +Close the device with osaudio_close(). + +Returns 0 on success, any other error code on failure. +*/ +osaudio_result_t osaudio_open(osaudio_t* audio, osaudio_config_t* config); + +/* +Closes a connection to a device. + +As soon as this function is called, the device should be considered invalid and unsuable. Do not +attempt to use the audio object once this function has been called. + +It's invalid to call this while any other function is still running. You can use osaudio_flush() to +quickly abort any pending writes or reads. You can also use osaudio_drain() to wait for all pending +writes or reads to complete. + +Returns 0 on success, < 0 on failure. +*/ +osaudio_result_t osaudio_close(osaudio_t audio); + +/* +Writes audio data to the device. + +This will block until all data has been written or the device is closed. + +You can only write from a single thread at any given time. If you want to write from multiple +threads, you need to use your own synchronization mechanism. + +This will automatically start the device if frame_count is > 0 and it's not in a paused state. + +Use osaudio_get_avail() to determine how much data can be written without blocking. + +Returns 0 on success, < 0 on failure. +*/ +osaudio_result_t osaudio_write(osaudio_t audio, const void* data, unsigned int frame_count); + +/* +Reads audio data from the device. + +This will block until the requested number of frames has been read or the device is closed. + +You can only read from a single thread at any given time. If you want to read from multiple +threads, you need to use your own synchronization mechanism. + +This will automatically start the device if frame_count is > 0 and it's not in a paused state. + +Use osaudio_get_avail() to determine how much data can be read without blocking. + +Returns 0 on success, < 0 on failure. +*/ +osaudio_result_t osaudio_read(osaudio_t audio, void* data, unsigned int frame_count); + +/* +Drains the device. + +This will block until all pending reads or writes have completed. + +If after calling this function another call to osaudio_write() or osaudio_read() is made, the +device will be resumed like normal. + +It is invalid to call this while the device is paused. + +Returns 0 on success, < 0 on failure. +*/ +osaudio_result_t osaudio_drain(osaudio_t audio); + +/* +Flushes the device. + +This will immediately flush any pending reads or writes. It will not block. Any in-progress reads +or writes will return immediately. + +If after calling this function another thread starts reading or writing, the device will be resumed +like normal. + +Returns 0 on success, < 0 on failure. +*/ +osaudio_result_t osaudio_flush(osaudio_t audio); + +/* +Pauses or resumes the device. + +Pausing a device will trigger a OSAUDIO_NOTIFICATION_STOPPED notification. Resuming a device will +trigger a OSAUDIO_NOTIFICATION_STARTED notification. + +Returns 0 on success, < 0 on failure. +*/ +osaudio_result_t osaudio_pause(osaudio_t audio); + +/* +Resumes the device. + +Returns 0 on success, < 0 on failure. +*/ +osaudio_result_t osaudio_resume(osaudio_t audio); + +/* +Returns the number of frames that can be read or written without blocking. +*/ +unsigned int osaudio_get_avail(osaudio_t audio); + +/* +Gets information about the device. + +There will be one item in the configs array which will contain the device's current configuration, +the contents of which will match that of the config that was returned by osaudio_open(). + +Returns NULL on failure. Do not free the returned pointer. It's up to the implementation to manage +the meory of this object. +*/ +const osaudio_info_t* osaudio_get_info(osaudio_t audio); + + +#ifdef __cplusplus +} +#endif +#endif /* osaudio_h */ diff --git a/extras/osaudio/osaudio_miniaudio.c b/extras/osaudio/osaudio_miniaudio.c new file mode 100644 index 00000000..ae609688 --- /dev/null +++ b/extras/osaudio/osaudio_miniaudio.c @@ -0,0 +1,946 @@ +/* +Consider this a reference implementation of osaudio. It uses miniaudio under the hood. You can add +this file directly to your source tree, but you may need to update the miniaudio path. + +This will use a mutex in osaudio_read() and osaudio_write(). It's a low-contention lock that's only +used for the purpose of osaudio_drain(), but it's still a lock nonetheless. I'm not worrying about +this too much right now because this is just an example implementation, but I might improve on this +at a later date. +*/ +#ifndef osaudio_miniaudio_c +#define osaudio_miniaudio_c + +#include "osaudio.h" + +/* +If you would rather define your own implementation of miniaudio, define OSAUDIO_NO_MINIAUDIO_IMPLEMENTATION. If you do this, +you need to make sure you include the implmeentation before osaudio.c. This would only really be useful if you are wanting +to do a unity build which uses other parts of miniaudio that this file is currently excluding. +*/ +#ifndef OSAUDIO_NO_MINIAUDIO_IMPLEMENTATION +#define MA_API static +#define MA_NO_DECODING +#define MA_NO_ENCODING +#define MA_NO_RESOURCE_MANAGER +#define MA_NO_NODE_GRAPH +#define MA_NO_ENGINE +#define MA_NO_GENERATION +#define MINIAUDIO_IMPLEMENTATION +#include "../../miniaudio.h" +#endif + +struct _osaudio_t +{ + ma_device device; + osaudio_info_t info; + osaudio_config_t config; /* info.configs will point to this. */ + ma_pcm_rb buffer; + ma_semaphore bufferSemaphore; /* The semaphore for controlling access to the buffer. The audio thread will release the semaphore. The read and write functions will wait on it. */ + ma_atomic_bool32 isActive; /* Starts off as false. Set to true when config.buffer_size data has been written in the case of playback, or as soon as osaudio_read() is called in the case of capture. */ + ma_atomic_bool32 isPaused; + ma_atomic_bool32 isFlushed; /* When set, activation of the device will flush any data that's currently in the buffer. Defaults to false, and will be set to true in osaudio_drain() and osaudio_flush(). */ + ma_atomic_bool32 xrunDetected; /* Used for detecting when an xrun has occurred and returning from osaudio_read/write() when OSAUDIO_FLAG_REPORT_XRUN is enabled. */ + ma_spinlock activateLock; /* Used for starting and stopping the device. Needed because two variables control this - isActive and isPaused. */ + ma_mutex drainLock; /* Used for osaudio_drain(). For mutal exclusion between drain() and read()/write(). Technically results in a lock in read()/write(), but not overthinking that since this is just a reference for now. */ +}; + + +static ma_bool32 osaudio_g_is_backend_known = MA_FALSE; +static ma_backend osaudio_g_backend = ma_backend_wasapi; +static ma_context osaudio_g_context; +static ma_mutex osaudio_g_context_lock; /* Only used for device enumeration. Created and destroyed with our context. */ +static ma_uint32 osaudio_g_refcount = 0; +static ma_spinlock osaudio_g_lock = 0; + + +static osaudio_result_t osaudio_result_from_miniaudio(ma_result result) +{ + switch (result) + { + case MA_SUCCESS: return OSAUDIO_SUCCESS; + case MA_INVALID_ARGS: return OSAUDIO_INVALID_ARGS; + case MA_INVALID_OPERATION: return OSAUDIO_INVALID_OPERATION; + case MA_OUT_OF_MEMORY: return OSAUDIO_OUT_OF_MEMORY; + default: return OSAUDIO_ERROR; + } +} + +static ma_format osaudio_format_to_miniaudio(osaudio_format_t format) +{ + switch (format) + { + case OSAUDIO_FORMAT_F32: return ma_format_f32; + case OSAUDIO_FORMAT_S16: return ma_format_s16; + case OSAUDIO_FORMAT_S24: return ma_format_s24; + case OSAUDIO_FORMAT_S32: return ma_format_s32; + default: return ma_format_unknown; + } +} + +static osaudio_format_t osaudio_format_from_miniaudio(ma_format format) +{ + switch (format) + { + case ma_format_f32: return OSAUDIO_FORMAT_F32; + case ma_format_s16: return OSAUDIO_FORMAT_S16; + case ma_format_s24: return OSAUDIO_FORMAT_S24; + case ma_format_s32: return OSAUDIO_FORMAT_S32; + default: return OSAUDIO_FORMAT_UNKNOWN; + } +} + + +static osaudio_channel_t osaudio_channel_from_miniaudio(ma_channel channel) +{ + /* Channel positions between here and miniaudio will remain in sync. */ + return (osaudio_channel_t)channel; +} + +static ma_channel osaudio_channel_to_miniaudio(osaudio_channel_t channel) +{ + /* Channel positions between here and miniaudio will remain in sync. */ + return (ma_channel)channel; +} + + +static void osaudio_dummy_data_callback(ma_device* pDevice, void* pOutput, const void* pInput, ma_uint32 frameCount) +{ + (void)pDevice; + (void)pOutput; + (void)pInput; + (void)frameCount; +} + +static osaudio_result_t osaudio_determine_miniaudio_backend(ma_backend* pBackend, ma_device* pDummyDevice) +{ + ma_device dummyDevice; + ma_device_config dummyDeviceConfig; + ma_result result; + + /* + To do this we initialize a dummy device. We allow the caller to make use of this device as an optimization. This is + only used by osaudio_enumerate_devices() because that can make use of the context from the dummy device rather than + having to create it's own. pDummyDevice can be null. + */ + if (pDummyDevice == NULL) { + pDummyDevice = &dummyDevice; + } + + dummyDeviceConfig = ma_device_config_init(ma_device_type_playback); + dummyDeviceConfig.dataCallback = osaudio_dummy_data_callback; + + result = ma_device_init(NULL, &dummyDeviceConfig, pDummyDevice); + if (result != MA_SUCCESS || pDummyDevice->pContext->backend == ma_backend_null) { + /* Failed to open a default playback device. Try capture. */ + if (result == MA_SUCCESS) { + /* This means we successfully initialize a device, but it's backend is null. It could be that there's no playback devices attached. Try capture. */ + ma_device_uninit(pDummyDevice); + } + + dummyDeviceConfig = ma_device_config_init(ma_device_type_capture); + result = ma_device_init(NULL, &dummyDeviceConfig, pDummyDevice); + } + + if (result != MA_SUCCESS) { + return osaudio_result_from_miniaudio(result); + } + + *pBackend = pDummyDevice->pContext->backend; + + /* We're done. */ + if (pDummyDevice == &dummyDevice) { + ma_device_uninit(&dummyDevice); + } + + return OSAUDIO_SUCCESS; +} + +static osaudio_result_t osaudio_ref_context_nolock() +{ + /* Initialize the global context if necessary. */ + if (osaudio_g_refcount == 0) { + osaudio_result_t result; + + /* If we haven't got a known context, we'll need to determine it here. */ + if (osaudio_g_is_backend_known == MA_FALSE) { + result = osaudio_determine_miniaudio_backend(&osaudio_g_backend, NULL); + if (result != OSAUDIO_SUCCESS) { + return result; + } + } + + result = osaudio_result_from_miniaudio(ma_context_init(&osaudio_g_backend, 1, NULL, &osaudio_g_context)); + if (result != OSAUDIO_SUCCESS) { + return result; + } + + /* Need a mutex for device enumeration. */ + ma_mutex_init(&osaudio_g_context_lock); + } + + osaudio_g_refcount += 1; + + return OSAUDIO_SUCCESS; +} + +static osaudio_result_t osaudio_unref_context_nolock() +{ + if (osaudio_g_refcount == 0) { + return OSAUDIO_INVALID_OPERATION; + } + + osaudio_g_refcount -= 1; + + /* Uninitialize the context if we don't have any more references. */ + if (osaudio_g_refcount == 0) { + ma_context_uninit(&osaudio_g_context); + ma_mutex_uninit(&osaudio_g_context_lock); + } + + return OSAUDIO_SUCCESS; +} + +static ma_context* osaudio_ref_context() +{ + osaudio_result_t result; + + ma_spinlock_lock(&osaudio_g_lock); + { + result = osaudio_ref_context_nolock(); + } + ma_spinlock_unlock(&osaudio_g_lock); + + if (result != OSAUDIO_SUCCESS) { + return NULL; + } + + return &osaudio_g_context; +} + +static osaudio_result_t osaudio_unref_context() +{ + osaudio_result_t result; + + ma_spinlock_lock(&osaudio_g_lock); + { + result = osaudio_unref_context_nolock(); + } + ma_spinlock_unlock(&osaudio_g_lock); + + return result; +} + + +static void osaudio_info_from_miniaudio(osaudio_info_t* info, const ma_device_info* infoMA) +{ + unsigned int iNativeConfig; + + /* It just so happens, by absolutely total coincidence, that the size of the ID and name are the same between here and miniaudio. What are the odds?! */ + memcpy(info->id.data, &infoMA->id, sizeof(info->id.data)); + memcpy(info->name, infoMA->name, sizeof(info->name)); + + info->config_count = (unsigned int)infoMA->nativeDataFormatCount; + for (iNativeConfig = 0; iNativeConfig < info->config_count; iNativeConfig += 1) { + unsigned int iChannel; + + info->configs[iNativeConfig].device_id = &info->id; + info->configs[iNativeConfig].direction = info->direction; + info->configs[iNativeConfig].format = osaudio_format_from_miniaudio(infoMA->nativeDataFormats[iNativeConfig].format); + info->configs[iNativeConfig].channels = (unsigned int)infoMA->nativeDataFormats[iNativeConfig].channels; + info->configs[iNativeConfig].rate = (unsigned int)infoMA->nativeDataFormats[iNativeConfig].sampleRate; + + /* Apparently miniaudio does not report channel positions. I don't know why I'm not doing that. */ + for (iChannel = 0; iChannel < info->configs[iNativeConfig].channels; iChannel += 1) { + info->configs[iNativeConfig].channel_map[iChannel] = OSAUDIO_CHANNEL_NONE; + } + } +} + +static osaudio_result_t osaudio_enumerate_nolock(unsigned int* count, osaudio_info_t** info, ma_context* pContext) +{ + osaudio_result_t result; + ma_device_info* pPlaybackInfos; + ma_uint32 playbackCount; + ma_device_info* pCaptureInfos; + ma_uint32 captureCount; + ma_uint32 iInfo; + size_t allocSize; + osaudio_info_t* pRunningInfo; + osaudio_config_t* pRunningConfig; + + /* We now need to retrieve the device information from miniaudio. */ + result = osaudio_result_from_miniaudio(ma_context_get_devices(pContext, &pPlaybackInfos, &playbackCount, &pCaptureInfos, &captureCount)); + if (result != OSAUDIO_SUCCESS) { + osaudio_unref_context(); + return result; + } + + /* + Because the caller needs to free the returned pointer it's important that we keep it all in one allocation. Because there can be + a variable number of native configs we'll have to compute the size of the allocation first, and then do a second pass to fill + out the data. + */ + allocSize = ((size_t)playbackCount + (size_t)captureCount) * sizeof(osaudio_info_t); + + /* Now we need to iterate over each playback and capture device and add up the number of native configs. */ + for (iInfo = 0; iInfo < playbackCount; iInfo += 1) { + ma_context_get_device_info(pContext, ma_device_type_playback, &pPlaybackInfos[iInfo].id, &pPlaybackInfos[iInfo]); + allocSize += pPlaybackInfos[iInfo].nativeDataFormatCount * sizeof(osaudio_config_t); + } + for (iInfo = 0; iInfo < captureCount; iInfo += 1) { + ma_context_get_device_info(pContext, ma_device_type_capture, &pCaptureInfos[iInfo].id, &pCaptureInfos[iInfo]); + allocSize += pCaptureInfos[iInfo].nativeDataFormatCount * sizeof(osaudio_config_t); + } + + /* Now that we know the size of the allocation we can allocate it. */ + *info = (osaudio_info_t*)calloc(1, allocSize); + if (*info == NULL) { + osaudio_unref_context(); + return OSAUDIO_OUT_OF_MEMORY; + } + + pRunningInfo = *info; + pRunningConfig = (osaudio_config_t*)(((unsigned char*)*info) + (((size_t)playbackCount + (size_t)captureCount) * sizeof(osaudio_info_t))); + + for (iInfo = 0; iInfo < playbackCount; iInfo += 1) { + pRunningInfo->direction = OSAUDIO_OUTPUT; + pRunningInfo->configs = pRunningConfig; + osaudio_info_from_miniaudio(pRunningInfo, &pPlaybackInfos[iInfo]); + + pRunningConfig += pRunningInfo->config_count; + pRunningInfo += 1; + } + + for (iInfo = 0; iInfo < captureCount; iInfo += 1) { + pRunningInfo->direction = OSAUDIO_INPUT; + pRunningInfo->configs = pRunningConfig; + osaudio_info_from_miniaudio(pRunningInfo, &pPlaybackInfos[iInfo]); + + pRunningConfig += pRunningInfo->config_count; + pRunningInfo += 1; + } + + *count = (unsigned int)(playbackCount + captureCount); + + return OSAUDIO_SUCCESS; +} + +osaudio_result_t osaudio_enumerate(unsigned int* count, osaudio_info_t** info) +{ + osaudio_result_t result; + ma_context* pContext = NULL; + + if (count != NULL) { + *count = 0; + } + if (info != NULL) { + *info = NULL; + } + + if (count == NULL || info == NULL) { + return OSAUDIO_INVALID_ARGS; + } + + pContext = osaudio_ref_context(); + if (pContext == NULL) { + return OSAUDIO_ERROR; + } + + ma_mutex_lock(&osaudio_g_context_lock); + { + result = osaudio_enumerate_nolock(count, info, pContext); + } + ma_mutex_unlock(&osaudio_g_context_lock); + + /* We're done. We can now return. */ + osaudio_unref_context(); + return result; +} + + +void osaudio_config_init(osaudio_config_t* config, osaudio_direction_t direction) +{ + if (config == NULL) { + return; + } + + memset(config, 0, sizeof(*config)); + config->direction = direction; +} + + +static void osaudio_data_callback_playback(osaudio_t audio, void* pOutput, ma_uint32 frameCount) +{ + /* + If there's content in the buffer, read from it and release the semaphore. There needs to be a whole frameCount chunk + in the buffer so we can keep everything in nice clean chunks. When we read from the buffer, we release a semaphore + which will allow the main thread to write more data to the buffer. + */ + ma_uint32 framesToRead; + ma_uint32 framesProcessed; + void* pBuffer; + + framesToRead = ma_pcm_rb_available_read(&audio->buffer); + if (framesToRead > frameCount) { + framesToRead = frameCount; + } + + framesProcessed = framesToRead; + + /* For robustness we should run this in a loop in case the buffer wraps around. */ + while (frameCount > 0) { + framesToRead = frameCount; + + ma_pcm_rb_acquire_read(&audio->buffer, &framesToRead, &pBuffer); + if (framesToRead == 0) { + break; + } + + memcpy(pOutput, pBuffer, framesToRead * ma_get_bytes_per_frame(audio->device.playback.format, audio->device.playback.channels)); + ma_pcm_rb_commit_read(&audio->buffer, framesToRead); + + frameCount -= framesToRead; + pOutput = ((unsigned char*)pOutput) + (framesToRead * ma_get_bytes_per_frame(audio->device.playback.format, audio->device.playback.channels)); + } + + /* Make sure we release the semaphore if we ended up reading anything. */ + if (framesProcessed > 0) { + ma_semaphore_release(&audio->bufferSemaphore); + } + + if (frameCount > 0) { + /* Underrun. Pad with silence. */ + ma_silence_pcm_frames(pOutput, frameCount, audio->device.playback.format, audio->device.playback.channels); + ma_atomic_bool32_set(&audio->xrunDetected, MA_TRUE); + } +} + +static void osaudio_data_callback_capture(osaudio_t audio, const void* pInput, ma_uint32 frameCount) +{ + /* If there's space in the buffer, write to it and release the semaphore. The semaphore is only released on full-chunk boundaries. */ + ma_uint32 framesToWrite; + ma_uint32 framesProcessed; + void* pBuffer; + + framesToWrite = ma_pcm_rb_available_write(&audio->buffer); + if (framesToWrite > frameCount) { + framesToWrite = frameCount; + } + + framesProcessed = framesToWrite; + + while (frameCount > 0) { + framesToWrite = frameCount; + + ma_pcm_rb_acquire_write(&audio->buffer, &framesToWrite, &pBuffer); + if (framesToWrite == 0) { + break; + } + + memcpy(pBuffer, pInput, framesToWrite * ma_get_bytes_per_frame(audio->device.capture.format, audio->device.capture.channels)); + ma_pcm_rb_commit_write(&audio->buffer, framesToWrite); + + frameCount -= framesToWrite; + pInput = ((unsigned char*)pInput) + (framesToWrite * ma_get_bytes_per_frame(audio->device.capture.format, audio->device.capture.channels)); + } + + /* Make sure we release the semaphore if we ended up reading anything. */ + if (framesProcessed > 0) { + ma_semaphore_release(&audio->bufferSemaphore); + } + + if (frameCount > 0) { + /* Overrun. Not enough room to move our input data into the buffer. */ + ma_atomic_bool32_set(&audio->xrunDetected, MA_TRUE); + } +} + +static void osaudio_nofication_callback(const ma_device_notification* pNotification) +{ + osaudio_t audio = (osaudio_t)pNotification->pDevice->pUserData; + + if (audio->config.notification != NULL) { + osaudio_notification_t notification; + + switch (pNotification->type) + { + case ma_device_notification_type_started: + { + notification.type = OSAUDIO_NOTIFICATION_STARTED; + } break; + case ma_device_notification_type_stopped: + { + notification.type = OSAUDIO_NOTIFICATION_STOPPED; + } break; + case ma_device_notification_type_rerouted: + { + notification.type = OSAUDIO_NOTIFICATION_REROUTED; + } break; + case ma_device_notification_type_interruption_began: + { + notification.type = OSAUDIO_NOTIFICATION_INTERRUPTION_BEGIN; + } break; + case ma_device_notification_type_interruption_ended: + { + notification.type = OSAUDIO_NOTIFICATION_INTERRUPTION_END; + } break; + } + + audio->config.notification(audio->config.user_data, ¬ification); + } +} + +static void osaudio_data_callback(ma_device* pDevice, void* pOutput, const void* pInput, ma_uint32 frameCount) +{ + osaudio_t audio = (osaudio_t)pDevice->pUserData; + + if (audio->info.direction == OSAUDIO_OUTPUT) { + osaudio_data_callback_playback(audio, pOutput, frameCount); + } else { + osaudio_data_callback_capture(audio, pInput, frameCount); + } +} + +osaudio_result_t osaudio_open(osaudio_t* audio, osaudio_config_t* config) +{ + osaudio_result_t result; + ma_context* pContext = NULL; + ma_device_config deviceConfig; + ma_device_info deviceInfo; + int periodCount = 2; + unsigned int iChannel; + + if (audio != NULL) { + *audio = NULL; /* Safety. */ + } + + if (audio == NULL || config == NULL) { + return OSAUDIO_INVALID_ARGS; + } + + pContext = osaudio_ref_context(); /* Will be unreferenced in osaudio_close(). */ + if (pContext == NULL) { + return OSAUDIO_ERROR; + } + + *audio = (osaudio_t)calloc(1, sizeof(**audio)); + if (*audio == NULL) { + osaudio_unref_context(); + return OSAUDIO_OUT_OF_MEMORY; + } + + if (config->direction == OSAUDIO_OUTPUT) { + deviceConfig = ma_device_config_init(ma_device_type_playback); + deviceConfig.playback.format = osaudio_format_to_miniaudio(config->format); + deviceConfig.playback.channels = (ma_uint32)config->channels; + + if (config->channel_map[0] != OSAUDIO_CHANNEL_NONE) { + for (iChannel = 0; iChannel < config->channels; iChannel += 1) { + deviceConfig.playback.pChannelMap[iChannel] = osaudio_channel_to_miniaudio(config->channel_map[iChannel]); + } + } + } else { + deviceConfig = ma_device_config_init(ma_device_type_capture); + deviceConfig.capture.format = osaudio_format_to_miniaudio(config->format); + deviceConfig.capture.channels = (ma_uint32)config->channels; + + if (config->channel_map[0] != OSAUDIO_CHANNEL_NONE) { + for (iChannel = 0; iChannel < config->channels; iChannel += 1) { + deviceConfig.capture.pChannelMap[iChannel] = osaudio_channel_to_miniaudio(config->channel_map[iChannel]); + } + } + } + + deviceConfig.sampleRate = (ma_uint32)config->rate; + + /* If the buffer size is 0, we'll default to 10ms. */ + deviceConfig.periodSizeInFrames = (ma_uint32)config->buffer_size; + if (deviceConfig.periodSizeInFrames == 0) { + deviceConfig.periodSizeInMilliseconds = 10; + } + + deviceConfig.dataCallback = osaudio_data_callback; + deviceConfig.pUserData = *audio; + + if ((config->flags & OSAUDIO_FLAG_NO_REROUTING) != 0) { + deviceConfig.wasapi.noAutoStreamRouting = MA_TRUE; + } + + if (config->notification != NULL) { + deviceConfig.notificationCallback = osaudio_nofication_callback; + } + + result = osaudio_result_from_miniaudio(ma_device_init(pContext, &deviceConfig, &((*audio)->device))); + if (result != OSAUDIO_SUCCESS) { + free(*audio); + osaudio_unref_context(); + return result; + } + + /* The input config needs to be updated with actual values. */ + if (config->direction == OSAUDIO_OUTPUT) { + config->format = osaudio_format_from_miniaudio((*audio)->device.playback.format); + config->channels = (unsigned int)(*audio)->device.playback.channels; + + for (iChannel = 0; iChannel < config->channels; iChannel += 1) { + config->channel_map[iChannel] = osaudio_channel_from_miniaudio((*audio)->device.playback.channelMap[iChannel]); + } + } else { + config->format = osaudio_format_from_miniaudio((*audio)->device.capture.format); + config->channels = (unsigned int)(*audio)->device.capture.channels; + + for (iChannel = 0; iChannel < config->channels; iChannel += 1) { + config->channel_map[iChannel] = osaudio_channel_from_miniaudio((*audio)->device.capture.channelMap[iChannel]); + } + } + + config->rate = (unsigned int)(*audio)->device.sampleRate; + + if (deviceConfig.periodSizeInFrames == 0) { + if (config->direction == OSAUDIO_OUTPUT) { + config->buffer_size = (int)(*audio)->device.playback.internalPeriodSizeInFrames; + } else { + config->buffer_size = (int)(*audio)->device.capture.internalPeriodSizeInFrames; + } + } + + + /* The device object needs to have a it's local info built. We can get the ID and name from miniaudio. */ + result = osaudio_result_from_miniaudio(ma_device_get_info(&(*audio)->device, (*audio)->device.type, &deviceInfo)); + if (result == MA_SUCCESS) { + memcpy((*audio)->info.id.data, &deviceInfo.id, sizeof((*audio)->info.id.data)); + memcpy((*audio)->info.name, deviceInfo.name, sizeof((*audio)->info.name)); + } + + (*audio)->info.direction = config->direction; + (*audio)->info.config_count = 1; + (*audio)->info.configs = &(*audio)->config; + (*audio)->config = *config; + (*audio)->config.device_id = &(*audio)->info.id; + + + /* We need a ring buffer. */ + result = osaudio_result_from_miniaudio(ma_pcm_rb_init(osaudio_format_to_miniaudio(config->format), (ma_uint32)config->channels, (ma_uint32)config->buffer_size * periodCount, NULL, NULL, &(*audio)->buffer)); + if (result != OSAUDIO_SUCCESS) { + ma_device_uninit(&(*audio)->device); + free(*audio); + osaudio_unref_context(); + return result; + } + + /* Now we need a semaphore to control access to the ring buffer to to block read/write when necessary. */ + result = osaudio_result_from_miniaudio(ma_semaphore_init((config->direction == OSAUDIO_OUTPUT) ? periodCount : 0, &(*audio)->bufferSemaphore)); + if (result != OSAUDIO_SUCCESS) { + ma_pcm_rb_uninit(&(*audio)->buffer); + ma_device_uninit(&(*audio)->device); + free(*audio); + osaudio_unref_context(); + return result; + } + + return OSAUDIO_SUCCESS; +} + +osaudio_result_t osaudio_close(osaudio_t audio) +{ + if (audio == NULL) { + return OSAUDIO_INVALID_ARGS; + } + + ma_device_uninit(&audio->device); + osaudio_unref_context(); + + return OSAUDIO_SUCCESS; +} + +static void osaudio_activate(osaudio_t audio) +{ + ma_spinlock_lock(&audio->activateLock); + { + if (ma_atomic_bool32_get(&audio->isActive) == MA_FALSE) { + ma_atomic_bool32_set(&audio->isActive, MA_TRUE); + + /* If we need to flush, do so now before starting the device. */ + if (ma_atomic_bool32_get(&audio->isFlushed) == MA_TRUE) { + ma_pcm_rb_reset(&audio->buffer); + ma_atomic_bool32_set(&audio->isFlushed, MA_FALSE); + } + + /* If we're not paused, start the device. */ + if (ma_atomic_bool32_get(&audio->isPaused) == MA_FALSE) { + ma_device_start(&audio->device); + } + } + } + ma_spinlock_unlock(&audio->activateLock); +} + +osaudio_result_t osaudio_write(osaudio_t audio, const void* data, unsigned int frame_count) +{ + if (audio == NULL) { + return OSAUDIO_INVALID_ARGS; + } + + ma_mutex_lock(&audio->drainLock); + { + /* Don't return until everything has been written. */ + while (frame_count > 0) { + ma_uint32 framesToWrite = frame_count; + ma_uint32 framesAvailableInBuffer; + + /* There should be enough data available in the buffer now, but check anyway. */ + framesAvailableInBuffer = ma_pcm_rb_available_write(&audio->buffer); + if (framesAvailableInBuffer > 0) { + void* pBuffer; + + if (framesToWrite > framesAvailableInBuffer) { + framesToWrite = framesAvailableInBuffer; + } + + ma_pcm_rb_acquire_write(&audio->buffer, &framesToWrite, &pBuffer); + { + ma_copy_pcm_frames(pBuffer, data, framesToWrite, audio->device.playback.format, audio->device.playback.channels); + } + ma_pcm_rb_commit_write(&audio->buffer, framesToWrite); + + frame_count -= (unsigned int)framesToWrite; + data = (const void*)((const unsigned char*)data + (framesToWrite * ma_get_bytes_per_frame(audio->device.playback.format, audio->device.playback.channels))); + + if (framesToWrite > 0) { + osaudio_activate(audio); + } + } else { + /* If we get here it means there's not enough data available in the buffer. We need to wait for more. */ + ma_semaphore_wait(&audio->bufferSemaphore); + + /* If we're not active it probably means we've flushed. This write needs to be aborted. */ + if (ma_atomic_bool32_get(&audio->isActive) == MA_FALSE) { + break; + } + } + } + } + ma_mutex_unlock(&audio->drainLock); + + if ((audio->config.flags & OSAUDIO_FLAG_REPORT_XRUN) != 0) { + if (ma_atomic_bool32_get(&audio->xrunDetected)) { + ma_atomic_bool32_set(&audio->xrunDetected, MA_FALSE); + return OSAUDIO_XRUN; + } + } + + return OSAUDIO_SUCCESS; +} + +osaudio_result_t osaudio_read(osaudio_t audio, void* data, unsigned int frame_count) +{ + if (audio == NULL) { + return OSAUDIO_INVALID_ARGS; + } + + ma_mutex_lock(&audio->drainLock); + { + while (frame_count > 0) { + ma_uint32 framesToRead = frame_count; + ma_uint32 framesAvailableInBuffer; + + /* There should be enough data available in the buffer now, but check anyway. */ + framesAvailableInBuffer = ma_pcm_rb_available_read(&audio->buffer); + if (framesAvailableInBuffer > 0) { + void* pBuffer; + + if (framesToRead > framesAvailableInBuffer) { + framesToRead = framesAvailableInBuffer; + } + + ma_pcm_rb_acquire_read(&audio->buffer, &framesToRead, &pBuffer); + { + ma_copy_pcm_frames(data, pBuffer, framesToRead, audio->device.capture.format, audio->device.capture.channels); + } + ma_pcm_rb_commit_read(&audio->buffer, framesToRead); + + frame_count -= (unsigned int)framesToRead; + data = (void*)((unsigned char*)data + (framesToRead * ma_get_bytes_per_frame(audio->device.capture.format, audio->device.capture.channels))); + } else { + /* Activate the device from the get go or else we'll never end up capturing anything. */ + osaudio_activate(audio); + + /* If we get here it means there's not enough data available in the buffer. We need to wait for more. */ + ma_semaphore_wait(&audio->bufferSemaphore); + + /* If we're not active it probably means we've flushed. This read needs to be aborted. */ + if (ma_atomic_bool32_get(&audio->isActive) == MA_FALSE) { + break; + } + } + } + } + ma_mutex_unlock(&audio->drainLock); + + if ((audio->config.flags & OSAUDIO_FLAG_REPORT_XRUN) != 0) { + if (ma_atomic_bool32_get(&audio->xrunDetected)) { + ma_atomic_bool32_set(&audio->xrunDetected, MA_FALSE); + return OSAUDIO_XRUN; + } + } + + return OSAUDIO_SUCCESS; +} + +osaudio_result_t osaudio_drain(osaudio_t audio) +{ + if (audio == NULL) { + return OSAUDIO_INVALID_ARGS; + } + + /* This cannot be called while the device is in a paused state. */ + if (ma_atomic_bool32_get(&audio->isPaused)) { + return OSAUDIO_DEVICE_STOPPED; + } + + /* For capture we want to stop the device immediately or else we won't ever drain the buffer because miniaudio will be constantly filling it. */ + if (audio->info.direction == OSAUDIO_INPUT) { + ma_device_stop(&audio->device); + } + + /* + Mark the device as inactive *before* releasing the semaphore. When read/write completes waiting + on the semaphore, they'll check this flag and abort. + */ + ma_atomic_bool32_set(&audio->isActive, MA_FALSE); + + /* + Again in capture mode, we need to release the semaphore before waiting for the drain lock because + there's a chance read() will be waiting on the semaphore and will need to be woken up in order for + it to be given to chance to return. + */ + if (audio->info.direction == OSAUDIO_INPUT) { + ma_semaphore_release(&audio->bufferSemaphore); + } + + /* Now we need to wait for any pending reads or writes to complete. */ + ma_mutex_lock(&audio->drainLock); + { + /* No processing should be happening on the buffer at this point. Wait for miniaudio to consume the buffer. */ + while (ma_pcm_rb_available_read(&audio->buffer) > 0) { + ma_sleep(1); + } + + /* + At this point the buffer should be empty, and we shouldn't be in any read or write calls. If + it's a playback device, we'll want to stop the device. There's no need to release the semaphore. + */ + if (audio->info.direction == OSAUDIO_OUTPUT) { + ma_device_stop(&audio->device); + } + } + ma_mutex_unlock(&audio->drainLock); + + return OSAUDIO_SUCCESS; +} + +osaudio_result_t osaudio_flush(osaudio_t audio) +{ + if (audio == NULL) { + return OSAUDIO_INVALID_ARGS; + } + + /* + First stop the device. This ensures the miniaudio background thread doesn't try modifying the + buffer from under us while we're trying to flush it. + */ + ma_device_stop(&audio->device); + + /* + Mark the device as inactive *before* releasing the semaphore. When read/write completes waiting + on the semaphore, they'll check this flag and abort. + */ + ma_atomic_bool32_set(&audio->isActive, MA_FALSE); + + /* + Release the semaphore after marking the device as inactive. This needs to be released in order + to wakeup osaudio_read() and osaudio_write(). + */ + ma_semaphore_release(&audio->bufferSemaphore); + + /* + The buffer should only be modified by osaudio_read() or osaudio_write(), or the miniaudio + background thread. Therefore, we don't actually clear the buffer here. Instead we'll clear it + in osaudio_activate(), depending on whether or not the below flag is set. + */ + ma_atomic_bool32_set(&audio->isFlushed, MA_TRUE); + + return OSAUDIO_SUCCESS; +} + +osaudio_result_t osaudio_pause(osaudio_t audio) +{ + osaudio_result_t result = OSAUDIO_SUCCESS; + + if (audio == NULL) { + return OSAUDIO_INVALID_ARGS; + } + + ma_spinlock_lock(&audio->activateLock); + { + if (ma_atomic_bool32_get(&audio->isPaused) == MA_FALSE) { + ma_atomic_bool32_set(&audio->isPaused, MA_TRUE); + + /* No need to stop the device if it's not active. */ + if (ma_atomic_bool32_get(&audio->isActive) == MA_FALSE) { + result = osaudio_result_from_miniaudio(ma_device_stop(&audio->device)); + } + } + } + ma_spinlock_unlock(&audio->activateLock); + + return result; +} + +osaudio_result_t osaudio_resume(osaudio_t audio) +{ + osaudio_result_t result = OSAUDIO_SUCCESS; + + if (audio == NULL) { + return OSAUDIO_INVALID_ARGS; + } + + ma_spinlock_lock(&audio->activateLock); + { + if (ma_atomic_bool32_get(&audio->isPaused)) { + ma_atomic_bool32_set(&audio->isPaused, MA_FALSE); + + /* Don't start the device unless it's active. */ + if (ma_atomic_bool32_get(&audio->isActive)) { + result = osaudio_result_from_miniaudio(ma_device_start(&audio->device)); + } + } + } + ma_spinlock_unlock(&audio->activateLock); + + return result; +} + +unsigned int osaudio_get_avail(osaudio_t audio) +{ + if (audio == NULL) { + return 0; + } + + if (audio->info.direction == OSAUDIO_OUTPUT) { + return ma_pcm_rb_available_write(&audio->buffer); + } else { + return ma_pcm_rb_available_read(&audio->buffer); + } +} + +const osaudio_info_t* osaudio_get_info(osaudio_t audio) +{ + if (audio == NULL) { + return NULL; + } + + return &audio->info; +} + +#endif /* osaudio_miniaudio_c */ diff --git a/extras/osaudio/tests/osaudio_deviceio.c b/extras/osaudio/tests/osaudio_deviceio.c new file mode 100644 index 00000000..a08aaf05 --- /dev/null +++ b/extras/osaudio/tests/osaudio_deviceio.c @@ -0,0 +1,196 @@ +#include "../osaudio.h" + +/* This example uses miniaudio for decoding audio files. */ +#define MINIAUDIO_IMPLEMENTATION +#include "../../../miniaudio.h" + +#include +#include +#include + +#define MODE_PLAYBACK 0 +#define MODE_CAPTURE 1 +#define MODE_DUPLEX 2 + +void enumerate_devices() +{ + int result; + unsigned int iDevice; + unsigned int count; + osaudio_info_t* pDeviceInfos; + + result = osaudio_enumerate(&count, &pDeviceInfos); + if (result != OSAUDIO_SUCCESS) { + printf("Failed to enumerate audio devices.\n"); + return; + } + + for (iDevice = 0; iDevice < count; iDevice += 1) { + printf("(%s) %s\n", (pDeviceInfos[iDevice].direction == OSAUDIO_OUTPUT) ? "Playback" : "Capture", pDeviceInfos[iDevice].name); + } + + free(pDeviceInfos); +} + +osaudio_t open_device(int direction) +{ + int result; + osaudio_t audio; + osaudio_config_t config; + + osaudio_config_init(&config, direction); + config.format = OSAUDIO_FORMAT_F32; + config.channels = 2; + config.rate = 48000; + config.flags = OSAUDIO_FLAG_REPORT_XRUN; + + result = osaudio_open(&audio, &config); + if (result != OSAUDIO_SUCCESS) { + printf("Failed to open audio device.\n"); + return NULL; + } + + return audio; +} + +void do_playback(int argc, char** argv) +{ + int result; + osaudio_t audio; + const osaudio_config_t* config; + const char* pFilePath = NULL; + ma_result resultMA; + ma_decoder_config decoderConfig; + ma_decoder decoder; + + audio = open_device(OSAUDIO_OUTPUT); + if (audio == NULL) { + printf("Failed to open audio device.\n"); + return; + } + + config = &osaudio_get_info(audio)->configs[0]; + + /* We want to always use f32. */ + if (config->format == OSAUDIO_FORMAT_F32) { + if (argc > 1) { + pFilePath = argv[1]; + + decoderConfig = ma_decoder_config_init(ma_format_f32, (ma_uint32)config->channels, (ma_uint32)config->rate); + + resultMA = ma_decoder_init_file(pFilePath, &decoderConfig, &decoder); + if (resultMA == MA_SUCCESS) { + /* Now just keep looping over each sample until we get to the end. */ + for (;;) { + float frames[1024]; + ma_uint64 frameCount; + + resultMA = ma_decoder_read_pcm_frames(&decoder, frames, ma_countof(frames) / config->channels, &frameCount); + if (resultMA != MA_SUCCESS) { + break; + } + + result = osaudio_write(audio, frames, (unsigned int)frameCount); /* Safe cast. */ + if (result != OSAUDIO_SUCCESS && result != OSAUDIO_XRUN) { + printf("Error writing to audio device."); + break; + } + + if (result == OSAUDIO_XRUN) { + printf("WARNING: An xrun occurred while writing to the playback device.\n"); + } + } + } else { + printf("Failed to open file: %s\n", pFilePath); + } + } else { + printf("No input file.\n"); + } + } else { + printf("Unsupported device format.\n"); + } + + /* Getting here means we're done and we can tear down. */ + osaudio_close(audio); +} + +void do_duplex() +{ + int result; + osaudio_t capture; + osaudio_t playback; + + capture = open_device(OSAUDIO_INPUT); + if (capture == NULL) { + printf("Failed to open capture device.\n"); + return; + } + + playback = open_device(OSAUDIO_OUTPUT); + if (playback == NULL) { + osaudio_close(capture); + printf("Failed to open playback device.\n"); + return; + } + + for (;;) { + float frames[1024]; + unsigned int frameCount; + + frameCount = ma_countof(frames) / osaudio_get_info(capture)->configs[0].channels; + + /* Capture. */ + result = osaudio_read(capture, frames, frameCount); + if (result != OSAUDIO_SUCCESS && result != OSAUDIO_XRUN) { + printf("Error reading from capture device.\n"); + break; + } + + if (result == OSAUDIO_XRUN) { + printf("WARNING: An xrun occurred while reading from the capture device.\n"); + } + + + /* Playback. */ + result = osaudio_write(playback, frames, frameCount); + if (result != OSAUDIO_SUCCESS && result != OSAUDIO_XRUN) { + printf("Error writing to playback device.\n"); + break; + } + + if (result == OSAUDIO_XRUN) { + printf("WARNING: An xrun occurred while writing to the playback device.\n"); + } + } + + osaudio_close(capture); + osaudio_close(playback); +} + +int main(int argc, char** argv) +{ + int mode = MODE_PLAYBACK; + int iarg; + + enumerate_devices(); + + for (iarg = 0; iarg < argc; iarg += 1) { + if (strcmp(argv[iarg], "capture") == 0) { + mode = MODE_CAPTURE; + } else if (strcmp(argv[iarg], "duplex") == 0) { + mode = MODE_DUPLEX; + } + } + + switch (mode) + { + case MODE_PLAYBACK: do_playback(argc, argv); break; + case MODE_CAPTURE: break; + case MODE_DUPLEX: do_duplex(); break; + } + + (void)argc; + (void)argv; + + return 0; +} \ No newline at end of file