Commit a81f09d9 authored by David Reid's avatar David Reid

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.
parent f2ea6562
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.
/*
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 */
/*
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, &notification);
}
}
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 */
#include "../osaudio.h"
/* This example uses miniaudio for decoding audio files. */
#define MINIAUDIO_IMPLEMENTATION
#include "../../../miniaudio.h"
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#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
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment