Add simulator audio support

This commit is contained in:
Adolfo Reyna
2026-09-23 19:13:39 -04:00
parent 982bba70b2
commit dcfd4e9bcc
15 changed files with 905 additions and 11 deletions
@@ -113,6 +113,10 @@ struct AudioStreamHandleImpl : AudioStreamHandleData {
SemaphoreHandle_t drain_semaphore = nullptr;
};
// A slow codec open (notably the macOS microphone permission prompt) reserves a
// direction before a real handle exists. It must never be passed to close_stream().
AudioStreamHandleImpl* const OPENING_STREAM = reinterpret_cast<AudioStreamHandleImpl*>(1);
struct AudioStreamData {
Device* input_codec = nullptr;
Device* output_codec = nullptr;
@@ -130,6 +134,8 @@ struct AudioStreamData {
bool output_muted = false;
AudioStreamHandleImpl* open_input = nullptr;
AudioStreamHandleImpl* open_output = nullptr;
bool input_open_cancelled = false;
bool output_open_cancelled = false;
// Guards open_input/open_output and the closing/busy_count fields of any handle reachable
// through them, so close (possibly forced by set_enabled) can't race with read/write.
SemaphoreHandle_t mutex = nullptr;
@@ -261,7 +267,7 @@ error_t open_stream(Device* device, const struct AudioStreamConfig* config, Audi
return ERROR_INVALID_ARGUMENT;
}
if (config->channels == 0) {
if (config->channels == 0 || config->sample_rate == 0) {
return ERROR_INVALID_ARGUMENT;
}
@@ -291,7 +297,9 @@ error_t open_stream(Device* device, const struct AudioStreamConfig* config, Audi
// Reserve the slot with a placeholder so concurrent opens can't race past the check
// above while we do the (potentially slow) codec open below outside the lock.
auto* reservation = reinterpret_cast<AudioStreamHandleImpl*>(1);
auto* reservation = OPENING_STREAM;
bool* open_cancelled = is_input ? &data->input_open_cancelled : &data->output_open_cancelled;
*open_cancelled = false;
*slot = reservation;
xSemaphoreGive(data->mutex);
@@ -361,6 +369,18 @@ error_t open_stream(Device* device, const struct AudioStreamConfig* config, Audi
}
xSemaphoreTake(data->mutex, portMAX_DELAY);
if (*open_cancelled) {
// Keep the reservation until the codec is closed, so re-enabling cannot
// open a second stream while this cancelled open is still cleaning up.
xSemaphoreGive(data->mutex);
vSemaphoreDelete(handle->drain_semaphore);
delete handle;
audio_codec_close(codec);
xSemaphoreTake(data->mutex, portMAX_DELAY);
*slot = nullptr;
xSemaphoreGive(data->mutex);
return ERROR_NOT_ALLOWED;
}
*slot = handle;
xSemaphoreGive(data->mutex);
@@ -377,6 +397,7 @@ error_t open_output(Device* device, const struct AudioStreamConfig* config, Audi
}
error_t read_stream(AudioStreamHandle handle_base, void* out_data, size_t data_size, size_t* bytes_read, TickType_t timeout) {
if (bytes_read != nullptr) *bytes_read = 0;
auto* handle = static_cast<AudioStreamHandleImpl*>(handle_base);
if (handle->direction != AUDIO_CODEC_DIR_INPUT || handle->bytes_per_frame == 0) {
return ERROR_INVALID_STATE;
@@ -419,7 +440,7 @@ error_t read_stream(AudioStreamHandle handle_base, void* out_data, size_t data_s
size_t codec_bytes_read = 0;
result = audio_codec_read(data->input_codec, handle->codec_buffer.data(), codec_bytes_needed, &codec_bytes_read, timeout);
if (result == ERROR_NONE) {
if (codec_bytes_read > 0) {
size_t codec_frames_read = codec_bytes_read / handle->codec_bytes_per_frame;
const int16_t* rate_input = reinterpret_cast<const int16_t*>(handle->codec_buffer.data());
uint8_t rate_input_channels = handle->codec_channels;
@@ -448,7 +469,7 @@ error_t read_stream(AudioStreamHandle handle_base, void* out_data, size_t data_s
}
}
if (result == ERROR_NONE && handle->input_gain != 1.0f && bytes_read != nullptr && *bytes_read > 0) {
if (handle->input_gain != 1.0f && bytes_read != nullptr && *bytes_read > 0) {
auto* samples = reinterpret_cast<int16_t*>(out_data);
size_t sample_count = *bytes_read / sizeof(int16_t);
for (size_t i = 0; i < sample_count; i++) {
@@ -462,6 +483,7 @@ error_t read_stream(AudioStreamHandle handle_base, void* out_data, size_t data_s
}
error_t write_stream(AudioStreamHandle handle_base, const void* in_data, size_t data_size, size_t* bytes_written, TickType_t timeout) {
if (bytes_written != nullptr) *bytes_written = 0;
auto* handle = static_cast<AudioStreamHandleImpl*>(handle_base);
if (handle->direction != AUDIO_CODEC_DIR_OUTPUT || handle->bytes_per_frame == 0) {
return ERROR_INVALID_STATE;
@@ -532,9 +554,13 @@ error_t write_stream(AudioStreamHandle handle_base, const void* in_data, size_t
size_t codec_bytes_to_write = codec_frames * handle->codec_bytes_per_frame;
size_t codec_bytes_written = 0;
result = audio_codec_write(data->output_codec, handle->codec_buffer.data(), codec_bytes_to_write, &codec_bytes_written, timeout);
if (result == ERROR_NONE && bytes_written != nullptr) {
// The caller provided `data_size` worth of input; we consumed all of it (resampled/converted).
*bytes_written = data_size;
if (bytes_written != nullptr && codec_frames > 0) {
// A bounded host/hardware queue can accept only part of a converted write,
// including when returning ERROR_TIMEOUT. Report progress in app-side whole
// frames rather than leaving the count untouched or claiming the entire write.
size_t written_frames = codec_bytes_written / handle->codec_bytes_per_frame;
if (written_frames > codec_frames) written_frames = codec_frames;
*bytes_written = (in_frames * written_frames / codec_frames) * handle->bytes_per_frame;
}
}
@@ -686,13 +712,17 @@ error_t set_enabled(Device* device, AudioCodecDirection direction, bool enabled)
data->output_enabled = enabled;
}
// Capture and clear the slot under the lock so we hand close_stream() a pointer that
// can't simultaneously be torn down by a racing close from the owning app (close_stream
// re-checks `*slot == handle` and no-ops if it's already been cleared/replaced).
// A pending open has no handle to close yet. Let its owning task clean up when
// the codec returns, even if the user re-enables the direction in the meantime.
AudioStreamHandleImpl* to_close = nullptr;
if (!enabled) {
AudioStreamHandleImpl** slot = is_input ? &data->open_input : &data->open_output;
to_close = *slot;
if (*slot == OPENING_STREAM) {
if (is_input) data->input_open_cancelled = true;
else data->output_open_cancelled = true;
} else {
to_close = *slot;
}
}
xSemaphoreGive(data->mutex);