cosmopolitan/dsp/audio/cosmoaudio/cosmoaudio.c
2024-09-18 17:17:02 -07:00

519 lines
18 KiB
C

// Copyright 2024 Justine Alexandra Roberts Tunney
//
// Permission to use, copy, modify, and/or distribute this software for
// any purpose with or without fee is hereby granted, provided that the
// above copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
// WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
// WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
// AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
// DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR
// PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
#define COSMOAUDIO_BUILD
#include "cosmoaudio.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MA_DEBUG_OUTPUT
#define MA_DR_MP3_NO_STDIO
#define MA_NO_DECODING
#define MA_NO_ENCODING
#define MA_NO_ENGINE
#define MA_NO_GENERATION
#define MA_NO_NODE_GRAPH
#define MA_NO_RESOURCE_MANAGER
#define MA_STATIC
#define MINIAUDIO_IMPLEMENTATION
#include "miniaudio.h"
struct CosmoAudio {
enum CosmoAudioDeviceType deviceType;
ma_uint32 outputBufferFrames;
ma_uint32 inputBufferFrames;
int sampleRate;
int channels;
int isLeft;
ma_context context;
ma_device device;
ma_pcm_rb output;
ma_pcm_rb input;
ma_event event;
ma_log log;
};
static int read_ring_buffer(ma_log* log, ma_pcm_rb* rb, float* pOutput,
ma_uint32 frameCount, ma_uint32 channels) {
ma_result result;
ma_uint32 framesRead;
ma_uint32 framesToRead;
for (framesRead = 0; framesRead < frameCount; framesRead += framesToRead) {
framesToRead = frameCount - framesRead;
void* pMappedBuffer;
result = ma_pcm_rb_acquire_read(rb, &framesToRead, &pMappedBuffer);
if (result != MA_SUCCESS) {
ma_log_postf(log, MA_LOG_LEVEL_WARNING,
"ma_pcm_rb_acquire_read failed: %s\n",
ma_result_description(result));
return COSMOAUDIO_ERROR;
}
if (!framesToRead)
break;
memcpy(pOutput + framesRead * channels, pMappedBuffer,
framesToRead * channels * sizeof(float));
result = ma_pcm_rb_commit_read(rb, framesToRead);
if (result != MA_SUCCESS) {
if (result == MA_AT_END) {
framesRead += framesToRead;
break;
}
ma_log_postf(log, MA_LOG_LEVEL_WARNING,
"ma_pcm_rb_commit_read failed: %s\n",
ma_result_description(result));
return COSMOAUDIO_ERROR;
}
}
return framesRead;
}
static int write_ring_buffer(ma_log* log, ma_pcm_rb* rb, const float* pInput,
ma_uint32 frameCount, ma_uint32 channels) {
ma_result result;
ma_uint32 framesWritten;
ma_uint32 framesToWrite;
for (framesWritten = 0; framesWritten < frameCount;
framesWritten += framesToWrite) {
framesToWrite = frameCount - framesWritten;
void* pMappedBuffer;
result = ma_pcm_rb_acquire_write(rb, &framesToWrite, &pMappedBuffer);
if (result != MA_SUCCESS) {
ma_log_postf(log, MA_LOG_LEVEL_WARNING,
"ma_pcm_rb_acquire_write failed: %s\n",
ma_result_description(result));
return COSMOAUDIO_ERROR;
}
if (!framesToWrite)
break;
memcpy(pMappedBuffer, pInput + framesWritten * channels,
framesToWrite * channels * sizeof(float));
result = ma_pcm_rb_commit_write(rb, framesToWrite);
if (result != MA_SUCCESS) {
if (result == MA_AT_END) {
framesWritten += framesToWrite;
break;
}
ma_log_postf(log, MA_LOG_LEVEL_WARNING,
"ma_pcm_rb_commit_write failed: %s\n",
ma_result_description(result));
return COSMOAUDIO_ERROR;
}
}
return framesWritten;
}
static void data_callback_f32(ma_device* pDevice, float* pOutput,
const float* pInput, ma_uint32 frameCount) {
struct CosmoAudio* ca = (struct CosmoAudio*)pDevice->pUserData;
if (ca->deviceType & kCosmoAudioDeviceTypePlayback) {
//
// "By default, miniaudio will pre-silence the data callback's
// output buffer. If you know that you will always write valid data
// to the output buffer you can disable pre-silencing by setting
// the noPreSilence config option in the device config to true."
//
// —Quoth miniaudio documentation § 16.1. Low Level API
//
if (ca->isLeft) {
int framesCopied = read_ring_buffer(&ca->log, &ca->output, pOutput,
frameCount, ca->channels);
if (framesCopied < (int)frameCount)
ca->isLeft = 0;
} else {
// TODO(jart): Maybe we should stretch the audio too short?
int frameOffset;
int availableFrames = ma_pcm_rb_available_read(&ca->output);
if (availableFrames >= (int)frameCount) {
frameOffset = 0;
} else {
frameOffset = frameCount - availableFrames;
frameCount = availableFrames;
}
read_ring_buffer(&ca->log, &ca->output,
pOutput + frameOffset * ca->channels, frameCount,
ca->channels);
ca->isLeft = 1;
}
}
if (ca->deviceType & kCosmoAudioDeviceTypeCapture)
write_ring_buffer(&ca->log, &ca->input, pInput, frameCount, ca->channels);
ma_event_signal(&ca->event);
}
static void data_callback(ma_device* pDevice, void* pOutput, const void* pInput,
ma_uint32 frameCount) {
data_callback_f32(pDevice, (float*)pOutput, (const float*)pInput, frameCount);
}
/**
* Returns current version of cosmo audio library.
*/
COSMOAUDIO_ABI int cosmoaudio_version(void) {
return 1;
}
/**
* Opens access to speaker and microphone.
*
* @param out_ca will receive pointer to allocated CosmoAudio object,
* which must be freed by caller with cosmoaudio_close(); if this
* function fails, then this will receive a NULL pointer value so
* that cosmoaudio_close(), cosmoaudio_write() etc. can be called
* without crashing if no error checking is performed
* @return 0 on success, or negative error code on failure
*/
COSMOAUDIO_ABI int cosmoaudio_open( //
struct CosmoAudio** out_ca, //
const struct CosmoAudioOpenOptions* options) {
// Validate arguments.
if (!out_ca)
return COSMOAUDIO_EINVAL;
*out_ca = NULL;
if (!options)
return COSMOAUDIO_EINVAL;
if (options->sizeofThis < (int)sizeof(struct CosmoAudioOpenOptions))
return COSMOAUDIO_EINVAL;
if (options->bufferFrames < 0)
return COSMOAUDIO_EINVAL;
if (options->sampleRate < 8000)
return COSMOAUDIO_EINVAL;
if (options->channels < 1)
return COSMOAUDIO_EINVAL;
if (!options->deviceType)
return COSMOAUDIO_EINVAL;
if (options->deviceType &
~(kCosmoAudioDeviceTypePlayback | kCosmoAudioDeviceTypeCapture))
return COSMOAUDIO_EINVAL;
// Allocate cosmo audio object.
struct CosmoAudio* ca;
ca = (struct CosmoAudio*)calloc(1, sizeof(struct CosmoAudio));
if (!ca)
return COSMOAUDIO_ERROR;
ca->channels = options->channels;
ca->sampleRate = options->sampleRate;
ca->deviceType = options->deviceType;
// Create win32-style condition variable.
if (ma_event_init(&ca->event) != MA_SUCCESS) {
free(ca);
return COSMOAUDIO_ERROR;
}
// Create audio log.
if (ma_log_init(NULL, &ca->log) != MA_SUCCESS) {
ma_event_uninit(&ca->event);
free(ca);
return COSMOAUDIO_ERROR;
}
if (!options->debugLog)
ca->log.callbackCount = 0;
// Create audio context.
ma_context_config contextConfig = ma_context_config_init();
contextConfig.pLog = &ca->log;
if (ma_context_init(NULL, 0, &contextConfig, &ca->context) != MA_SUCCESS) {
ma_event_uninit(&ca->event);
ma_log_uninit(&ca->log);
free(ca);
return COSMOAUDIO_ERROR;
}
// Initialize device.
ma_result result;
ma_device_config deviceConfig;
deviceConfig = ma_device_config_init(ca->deviceType);
deviceConfig.sampleRate = ca->sampleRate;
if (ca->deviceType & kCosmoAudioDeviceTypeCapture) {
deviceConfig.capture.channels = ca->channels;
deviceConfig.capture.format = ma_format_f32;
deviceConfig.capture.shareMode = ma_share_mode_shared;
}
if (ca->deviceType & kCosmoAudioDeviceTypePlayback) {
deviceConfig.playback.channels = ca->channels;
deviceConfig.playback.format = ma_format_f32;
}
deviceConfig.dataCallback = data_callback;
deviceConfig.pUserData = ca;
result = ma_device_init(&ca->context, &deviceConfig, &ca->device);
if (result != MA_SUCCESS) {
ma_context_uninit(&ca->context);
ma_event_uninit(&ca->event);
ma_log_uninit(&ca->log);
free(ca);
return COSMOAUDIO_ERROR;
}
// Initialize the speaker ring buffer.
int period = ca->device.playback.internalPeriodSizeInFrames;
if (!options->bufferFrames) {
ca->outputBufferFrames = period * 10;
} else if (options->bufferFrames < period * 2) {
ca->outputBufferFrames = period * 2;
} else {
ca->outputBufferFrames = options->bufferFrames;
}
if (ca->deviceType & kCosmoAudioDeviceTypePlayback) {
result = ma_pcm_rb_init(ma_format_f32, ca->channels, ca->outputBufferFrames,
NULL, NULL, &ca->output);
if (result != MA_SUCCESS) {
ma_device_uninit(&ca->device);
ma_context_uninit(&ca->context);
ma_event_uninit(&ca->event);
ma_log_uninit(&ca->log);
free(ca);
return COSMOAUDIO_ERROR;
}
ma_pcm_rb_set_sample_rate(&ca->output, ca->sampleRate);
}
// Initialize the microphone ring buffer.
period = ca->device.capture.internalPeriodSizeInFrames;
if (!options->bufferFrames) {
ca->inputBufferFrames = period * 10;
} else if (options->bufferFrames < period * 2) {
ca->inputBufferFrames = period * 2;
} else {
ca->inputBufferFrames = options->bufferFrames;
}
if (ca->deviceType & kCosmoAudioDeviceTypeCapture) {
result = ma_pcm_rb_init(ma_format_f32, ca->channels, ca->inputBufferFrames,
NULL, NULL, &ca->input);
if (result != MA_SUCCESS) {
ma_device_uninit(&ca->device);
if (ca->deviceType & kCosmoAudioDeviceTypePlayback)
ma_pcm_rb_uninit(&ca->output);
ma_context_uninit(&ca->context);
ma_event_uninit(&ca->event);
ma_log_uninit(&ca->log);
free(ca);
return COSMOAUDIO_ERROR;
}
ma_pcm_rb_set_sample_rate(&ca->output, ca->sampleRate);
}
// Start audio playback.
if (ma_device_start(&ca->device) != MA_SUCCESS) {
ma_device_uninit(&ca->device);
if (ca->deviceType & kCosmoAudioDeviceTypePlayback)
ma_pcm_rb_uninit(&ca->output);
if (ca->deviceType & kCosmoAudioDeviceTypeCapture)
ma_pcm_rb_uninit(&ca->input);
ma_context_uninit(&ca->context);
ma_event_uninit(&ca->event);
ma_log_uninit(&ca->log);
free(ca);
return COSMOAUDIO_ERROR;
}
*out_ca = ca;
return COSMOAUDIO_SUCCESS;
}
/**
* Closes audio device and frees all associated resources.
*
* This function is non-blocking and will drop buffered audio. In
* playback mode, you need to call cosmoaudio_flush() to ensure data
* supplied by cosmoaudio_write() gets played on your speaker.
*
* Calling this function twice on the same object will result in
* undefined behavior. Even if this function fails, the `ca` will be
* freed to the greatest extent possible.
*
* @param ca is CosmoAudio object returned earlier by cosmoaudio_open()
* @return 0 on success, or negative error code on failure
*/
COSMOAUDIO_ABI int cosmoaudio_close(struct CosmoAudio* ca) {
if (!ca)
return COSMOAUDIO_EINVAL;
ma_device_uninit(&ca->device); // do this first
if (ca->deviceType & kCosmoAudioDeviceTypePlayback)
ma_pcm_rb_uninit(&ca->output);
if (ca->deviceType & kCosmoAudioDeviceTypeCapture)
ma_pcm_rb_uninit(&ca->input);
ma_context_uninit(&ca->context);
ma_event_uninit(&ca->event);
ma_log_uninit(&ca->log);
free(ca);
return COSMOAUDIO_SUCCESS;
}
/**
* Writes raw audio data to speaker.
*
* The data is written to a ring buffer in real-time, which is then
* played back very soon on the audio device. This has tolerence for
* a certain amount of buffering, but expects that this function is
* repeatedly called at a regular time interval. The caller should
* have its own sleep loop for this purpose.
*
* This function never blocks. Programs that don't have their own timer
* can use cosmoaudio_poll() to wait until audio may be written.
*
* For any given CosmoAudio object, it's assumed that only a single
* thread will call this function.
*
* @param ca is CosmoAudio object returned earlier by cosmoaudio_open()
* @param data is pointer to raw audio samples, expected to be in the range
* -1.0 to 1.0, where channels are interleaved
* @param frames is the number of frames (i.e. number of samples divided by
* number of channels) from `data` to write to audio device
* @return number of frames written, or negative error code on failure
*/
COSMOAUDIO_ABI int cosmoaudio_write(struct CosmoAudio* ca, const float* data,
int frames) {
if (!ca)
return COSMOAUDIO_EINVAL;
if (frames < 0)
return COSMOAUDIO_EINVAL;
if (!(ca->deviceType & kCosmoAudioDeviceTypePlayback))
return COSMOAUDIO_EINVAL;
if (1u + frames > ca->outputBufferFrames)
return COSMOAUDIO_ENOBUF;
if (!frames)
return 0;
if (!data)
return COSMOAUDIO_EINVAL;
return write_ring_buffer(&ca->log, &ca->output, data, frames, ca->channels);
}
/**
* Reads raw audio data from microphone.
*
* The data is read from a ring buffer in real-time, which is then
* played back on the audio device. This has tolerence for a certain
* amount of buffering (based on the `bufferFrames` parameter passed to
* cosmoaudio_open(), which by default assumes this function will be
* called at at a regular time interval.
*
* This function never blocks. Programs that don't have their own timer
* can use cosmoaudio_poll() to wait until audio may be read.
*
* For any given CosmoAudio object, it's assumed that only a single
* thread will call this function.
*
* @param ca is CosmoAudio object returned earlier by cosmoaudio_open()
* @param data is pointer to raw audio samples, expected to be in the range
* -1.0 to 1.0, where channels are interleaved
* @param frames is the number of frames (i.e. number of samples divided by
* number of channels) from `data` to read from microphone
* @return number of frames read, or negative error code on failure
*/
COSMOAUDIO_ABI int cosmoaudio_read(struct CosmoAudio* ca, float* data,
int frames) {
if (!ca)
return COSMOAUDIO_EINVAL;
if (frames < 0)
return COSMOAUDIO_EINVAL;
if (!(ca->deviceType & kCosmoAudioDeviceTypeCapture))
return COSMOAUDIO_EINVAL;
if (!frames)
return 0;
if (!data)
return COSMOAUDIO_EINVAL;
return read_ring_buffer(&ca->log, &ca->input, data, frames, ca->channels);
}
/**
* Waits until it's possible to read/write audio.
*
* This function is uninterruptible. All signals are masked throughout
* the duration of time this function may block, including cancelation
* signals, because this is not a cancelation point. Cosmopolitan Libc
* applies this masking in its dlopen wrapper.
*
* @param ca is CosmoAudio object returned earlier by cosmoaudio_open()
* @param in_out_readFrames if non-NULL specifies how many frames of
* capture data be immediately readable by cosmoaudio_read() before
* this can return; it must not exceed the buffer size; on return
* this will be set to the actual number of frames in the buffer;
* if the caller supplies a zero then this call is a non-blocking
* way to query buffer sizes
* @param in_out_writeFrames if non-NULL specifies how many frames of
* capture data be immediately writable by cosmoaudio_write() before
* this can return; it must not exceed the buffer size; on return
* this will be set to the actual number of frames in the buffer;
* if the caller supplies a zero then this call is a non-blocking
* way to query buffer sizes
* @return 0 on success, or negative error code on error
*/
COSMOAUDIO_ABI int cosmoaudio_poll(struct CosmoAudio* ca,
int* in_out_readFrames,
int* in_out_writeFrames) {
if (!ca)
return COSMOAUDIO_EINVAL;
if (!in_out_readFrames && !in_out_writeFrames)
return COSMOAUDIO_EINVAL;
if (in_out_readFrames && !(ca->deviceType & kCosmoAudioDeviceTypeCapture))
return COSMOAUDIO_EINVAL;
if (in_out_writeFrames && !(ca->deviceType & kCosmoAudioDeviceTypePlayback))
return COSMOAUDIO_EINVAL;
if (in_out_readFrames && 1u + *in_out_readFrames > ca->inputBufferFrames)
return COSMOAUDIO_ENOBUF;
if (in_out_writeFrames && 1u + *in_out_writeFrames > ca->outputBufferFrames)
return COSMOAUDIO_ENOBUF;
for (;;) {
int done = 1;
ma_uint32 readable = 0;
ma_uint32 writable = 0;
if (in_out_readFrames) {
readable = ma_pcm_rb_available_read(&ca->input);
done &= readable >= (ma_uint32)*in_out_readFrames;
}
if (in_out_writeFrames) {
writable = ma_pcm_rb_available_write(&ca->output);
done &= writable >= (ma_uint32)*in_out_writeFrames;
}
if (done) {
if (in_out_readFrames)
*in_out_readFrames = readable;
if (in_out_writeFrames)
*in_out_writeFrames = writable;
return COSMOAUDIO_SUCCESS;
}
if (ma_event_wait(&ca->event) != MA_SUCCESS)
return COSMOAUDIO_ERROR;
}
}
/**
* Waits for written samples to be sent to device.
*
* This function is only valid to call in playback or duplex mode.
*
* This function is uninterruptible. All signals are masked throughout
* the duration of time this function may block, including cancelation
* signals, because this is not a cancelation point. Cosmopolitan Libc
* applies this masking in its dlopen wrapper.
*
* @param ca is CosmoAudio object returned earlier by cosmoaudio_open()
* @return 0 on success, or negative error code on failure
*/
COSMOAUDIO_ABI int cosmoaudio_flush(struct CosmoAudio* ca) {
if (!ca)
return COSMOAUDIO_EINVAL;
if (!(ca->deviceType & kCosmoAudioDeviceTypePlayback))
return COSMOAUDIO_EINVAL;
for (;;) {
if (!ma_pcm_rb_available_read(&ca->output))
return COSMOAUDIO_SUCCESS;
if (ma_event_wait(&ca->event) != MA_SUCCESS)
return COSMOAUDIO_ERROR;
}
}