Add simulator audio support
This commit is contained in:
@@ -0,0 +1,325 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include "sdl_audio.h"
|
||||
#include "sdl_audio_buffer.h"
|
||||
|
||||
#include <tactility/device.h>
|
||||
#include <tactility/driver.h>
|
||||
#include <tactility/freertos/task.h>
|
||||
#include <tactility/log.h>
|
||||
#include <tactility/module.h>
|
||||
|
||||
#include <SDL2/SDL.h>
|
||||
#include <atomic>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <new>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr auto* TAG = "SdlAudio";
|
||||
constexpr uint32_t SAMPLE_RATE = 48000;
|
||||
|
||||
struct AudioData {
|
||||
SdlAudioBuffer buffer;
|
||||
SemaphoreHandle_t mutex = nullptr; // task-side only; never used by the SDL callback
|
||||
SDL_AudioDeviceID id = 0;
|
||||
AudioCodecDirection direction;
|
||||
std::atomic<float> volume { 100.0f };
|
||||
std::atomic<bool> muted { false };
|
||||
std::atomic<uint32_t> callbacks { 0 };
|
||||
};
|
||||
|
||||
AudioData* get_data(Device* device) {
|
||||
return static_cast<AudioData*>(device_get_driver_data(device));
|
||||
}
|
||||
|
||||
class Lock {
|
||||
SemaphoreHandle_t mutex;
|
||||
public:
|
||||
explicit Lock(AudioData* data) : mutex(data->mutex) { xSemaphoreTake(mutex, portMAX_DELAY); }
|
||||
~Lock() { xSemaphoreGive(mutex); }
|
||||
};
|
||||
|
||||
const char* device_name(const AudioData* data) {
|
||||
const char* name = std::getenv(data->direction == AUDIO_CODEC_DIR_INPUT ? "SIM_AUDIO_INPUT" : "SIM_AUDIO_OUTPUT");
|
||||
return name != nullptr && name[0] != '\0' ? name : nullptr;
|
||||
}
|
||||
|
||||
bool available(const AudioData* data) {
|
||||
const char* name = device_name(data);
|
||||
if (name != nullptr && std::strcmp(name, "none") == 0) return false;
|
||||
const int capture = data->direction == AUDIO_CODEC_DIR_INPUT;
|
||||
const int count = SDL_GetNumAudioDevices(capture);
|
||||
if (name == nullptr) return count > 0;
|
||||
for (int i = 0; i < count; ++i) {
|
||||
const char* candidate = SDL_GetAudioDeviceName(i, capture);
|
||||
if (candidate != nullptr && std::strcmp(candidate, name) == 0) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void apply_volume(AudioData* data, void* bytes, size_t size) {
|
||||
const float gain = data->muted.load() ? 0.0f : data->volume.load() / 100.0f;
|
||||
auto* output = static_cast<uint8_t*>(bytes);
|
||||
for (size_t i = 0; i < size; i += sizeof(int16_t)) {
|
||||
int16_t sample;
|
||||
std::memcpy(&sample, output + i, sizeof(sample));
|
||||
sample = static_cast<int16_t>(sample * gain);
|
||||
std::memcpy(output + i, &sample, sizeof(sample));
|
||||
}
|
||||
}
|
||||
|
||||
void audio_callback(void* context, Uint8* stream, int length) {
|
||||
auto* data = static_cast<AudioData*>(context);
|
||||
const size_t count = static_cast<size_t>(length) / sizeof(int16_t);
|
||||
if (data->direction == AUDIO_CODEC_DIR_INPUT) {
|
||||
// Drop incoming frames when the bounded capture buffer is full. Muted capture
|
||||
// must not leave real microphone samples queued for a later unmute.
|
||||
if (data->muted.load()) std::memset(stream, 0, length);
|
||||
data->buffer.write(stream, count);
|
||||
} else {
|
||||
const size_t copied = data->buffer.read(stream, count) * sizeof(int16_t);
|
||||
std::memset(stream + copied, 0, length - copied); // silence on underrun
|
||||
apply_volume(data, stream, copied);
|
||||
}
|
||||
data->callbacks.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
error_t start(Device* device) {
|
||||
const auto* config = static_cast<const SdlAudioConfig*>(device->config);
|
||||
if (config == nullptr || (config->direction != AUDIO_CODEC_DIR_INPUT && config->direction != AUDIO_CODEC_DIR_OUTPUT)) {
|
||||
return ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (SDL_InitSubSystem(SDL_INIT_AUDIO) != 0) {
|
||||
LOG_E(TAG, "Cannot initialize audio: %s", SDL_GetError());
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
auto* data = new (std::nothrow) AudioData;
|
||||
if (data == nullptr) {
|
||||
SDL_QuitSubSystem(SDL_INIT_AUDIO);
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
data->direction = config->direction;
|
||||
data->mutex = xSemaphoreCreateMutex();
|
||||
if (data->mutex == nullptr) {
|
||||
delete data;
|
||||
SDL_QuitSubSystem(SDL_INIT_AUDIO);
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
device_set_driver_data(device, data);
|
||||
const int capture = data->direction == AUDIO_CODEC_DIR_INPUT;
|
||||
for (int i = 0; i < SDL_GetNumAudioDevices(capture); ++i) {
|
||||
LOG_I(TAG, "%s device: %s", capture ? "Input" : "Output", SDL_GetAudioDeviceName(i, capture));
|
||||
}
|
||||
LOG_I(TAG, "%s: %s (%s)", device->name, device_name(data) != nullptr ? device_name(data) : "system default",
|
||||
available(data) ? "available" : "unavailable");
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t open(Device* device, const AudioCodecStreamConfig* config) {
|
||||
auto* data = get_data(device);
|
||||
if (config == nullptr) return ERROR_INVALID_ARGUMENT;
|
||||
if (config->direction != data->direction) return ERROR_NOT_SUPPORTED;
|
||||
// The shared stream module performs rate/channel conversion on S16 PCM.
|
||||
if (config->bits_per_sample != 16) return ERROR_NOT_SUPPORTED;
|
||||
const uint8_t channels = data->direction == AUDIO_CODEC_DIR_INPUT ? 1 : 2;
|
||||
if (config->sample_rate != SAMPLE_RATE || config->channels != channels) return ERROR_INVALID_ARGUMENT;
|
||||
Lock lock(data);
|
||||
if (data->id != 0) return ERROR_INVALID_STATE;
|
||||
if (!available(data)) {
|
||||
LOG_W(TAG, "No selected %s device available", data->direction == AUDIO_CODEC_DIR_INPUT ? "input" : "output");
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
#ifdef __APPLE__
|
||||
// Only ask when recording is requested, and only with real macOS audio (dummy
|
||||
// and disk backends are also useful for automated tests).
|
||||
if (data->direction == AUDIO_CODEC_DIR_INPUT && std::strcmp(SDL_GetCurrentAudioDriver(), "coreaudio") == 0) {
|
||||
error_t permission;
|
||||
while ((permission = sdl_audio_microphone_permission()) == ERROR_RESOURCE_BUSY) vTaskDelay(1);
|
||||
if (permission != ERROR_NONE) {
|
||||
LOG_W(TAG, "Microphone permission denied; enable access in macOS Privacy & Security > Microphone");
|
||||
return permission;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
SDL_AudioSpec wanted {};
|
||||
wanted.freq = SAMPLE_RATE;
|
||||
wanted.format = AUDIO_S16SYS;
|
||||
wanted.channels = channels;
|
||||
wanted.samples = 512;
|
||||
wanted.callback = audio_callback;
|
||||
wanted.userdata = data;
|
||||
data->buffer.reset();
|
||||
data->callbacks.store(0);
|
||||
// No allowed changes: SDL converts between our fixed PCM format and the host
|
||||
// device's format when necessary. No hardware-specific format leaks to apps.
|
||||
data->id = SDL_OpenAudioDevice(device_name(data), data->direction == AUDIO_CODEC_DIR_INPUT, &wanted, nullptr, 0);
|
||||
if (data->id == 0) {
|
||||
LOG_E(TAG, "Cannot open %s: %s", device->name, SDL_GetError());
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
SDL_PauseAudioDevice(data->id, 0);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t close(Device* device) {
|
||||
auto* data = get_data(device);
|
||||
Lock lock(data);
|
||||
if (data->id != 0) {
|
||||
// Preserve the end of short sounds. Drain is bounded even if the device has
|
||||
// disappeared; microphone close never waits for the capture buffer to empty.
|
||||
const TickType_t start = xTaskGetTickCount();
|
||||
while (data->direction == AUDIO_CODEC_DIR_OUTPUT && !data->buffer.empty()
|
||||
&& xTaskGetTickCount() - start < pdMS_TO_TICKS(250)
|
||||
&& SDL_GetAudioDeviceStatus(data->id) == SDL_AUDIO_PLAYING) {
|
||||
vTaskDelay(1);
|
||||
}
|
||||
SDL_CloseAudioDevice(data->id);
|
||||
data->id = 0;
|
||||
data->buffer.reset();
|
||||
}
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t stop(Device* device) {
|
||||
auto* data = get_data(device);
|
||||
close(device);
|
||||
vSemaphoreDelete(data->mutex);
|
||||
delete data;
|
||||
device_set_driver_data(device, nullptr);
|
||||
SDL_QuitSubSystem(SDL_INIT_AUDIO);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t transfer(Device* device, void* destination, const void* source, size_t size, size_t* transferred, TickType_t timeout, bool capture) {
|
||||
if (transferred != nullptr) *transferred = 0;
|
||||
auto* data = get_data(device);
|
||||
if ((data->direction == AUDIO_CODEC_DIR_INPUT) != capture) return ERROR_NOT_SUPPORTED;
|
||||
const size_t frame_size = sizeof(int16_t) * (capture ? 1 : 2);
|
||||
if (size % frame_size != 0 || (size != 0 && (capture ? destination == nullptr : source == nullptr))) return ERROR_INVALID_ARGUMENT;
|
||||
Lock lock(data);
|
||||
if (data->id == 0) return ERROR_INVALID_STATE;
|
||||
const TickType_t start = xTaskGetTickCount();
|
||||
TickType_t last_callback = start;
|
||||
uint32_t callbacks = data->callbacks.load();
|
||||
size_t done = 0;
|
||||
error_t result = ERROR_NONE;
|
||||
while (done < size) {
|
||||
if (SDL_GetAudioDeviceStatus(data->id) != SDL_AUDIO_PLAYING) {
|
||||
result = ERROR_RESOURCE;
|
||||
break;
|
||||
}
|
||||
const size_t count = (size - done) / sizeof(int16_t);
|
||||
const size_t copied = capture
|
||||
? data->buffer.read(static_cast<uint8_t*>(destination) + done, count)
|
||||
: data->buffer.write(static_cast<const uint8_t*>(source) + done, count);
|
||||
done += copied * sizeof(int16_t);
|
||||
if (done == size) break;
|
||||
const TickType_t now = xTaskGetTickCount();
|
||||
if (timeout != portMAX_DELAY && now - start >= timeout) {
|
||||
result = ERROR_TIMEOUT;
|
||||
break;
|
||||
}
|
||||
const uint32_t current_callbacks = data->callbacks.load();
|
||||
if (current_callbacks != callbacks) {
|
||||
callbacks = current_callbacks;
|
||||
last_callback = now;
|
||||
} else if (now - last_callback >= pdMS_TO_TICKS(2000)) {
|
||||
// A stopped backend must not strand an infinite-timeout caller or a
|
||||
// concurrent Settings disable waiting for that caller to finish.
|
||||
result = ERROR_RESOURCE;
|
||||
break;
|
||||
}
|
||||
vTaskDelay(1);
|
||||
}
|
||||
if (capture) apply_volume(data, destination, done);
|
||||
if (transferred != nullptr) *transferred = done;
|
||||
return result;
|
||||
}
|
||||
|
||||
error_t read(Device* device, void* destination, size_t size, size_t* count, TickType_t timeout) {
|
||||
return transfer(device, destination, nullptr, size, count, timeout, true);
|
||||
}
|
||||
|
||||
error_t write(Device* device, const void* source, size_t size, size_t* count, TickType_t timeout) {
|
||||
return transfer(device, nullptr, source, size, count, timeout, false);
|
||||
}
|
||||
|
||||
error_t set_volume(Device* device, AudioCodecDirection direction, float volume) {
|
||||
auto* data = get_data(device);
|
||||
if (direction != data->direction) return ERROR_NOT_SUPPORTED;
|
||||
if (!std::isfinite(volume) || volume < 0.0f || volume > 100.0f) return ERROR_INVALID_ARGUMENT;
|
||||
data->volume.store(volume);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t get_volume(Device* device, AudioCodecDirection direction, float* volume) {
|
||||
auto* data = get_data(device);
|
||||
if (direction != data->direction) return ERROR_NOT_SUPPORTED;
|
||||
*volume = data->volume.load();
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t set_mute(Device* device, AudioCodecDirection direction, bool muted) {
|
||||
auto* data = get_data(device);
|
||||
if (direction != data->direction) return ERROR_NOT_SUPPORTED;
|
||||
data->muted.store(muted);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t get_mute(Device* device, AudioCodecDirection direction, bool* muted) {
|
||||
auto* data = get_data(device);
|
||||
if (direction != data->direction) return ERROR_NOT_SUPPORTED;
|
||||
*muted = data->muted.load();
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t get_rate(Device* device, AudioCodecDirection direction, uint32_t* rate) {
|
||||
if (direction != get_data(device)->direction) return ERROR_NOT_SUPPORTED;
|
||||
*rate = SAMPLE_RATE;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t get_channels(Device* device, AudioCodecDirection direction, uint8_t* channels) {
|
||||
if (direction != get_data(device)->direction) return ERROR_NOT_SUPPORTED;
|
||||
*channels = direction == AUDIO_CODEC_DIR_INPUT ? 1 : 2;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t get_capabilities(Device* device, AudioCodecDirection* direction) {
|
||||
auto* data = get_data(device);
|
||||
if (!available(data)) return ERROR_NOT_SUPPORTED;
|
||||
*direction = data->direction;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
const AudioCodecApi api = {
|
||||
.open = open,
|
||||
.close = close,
|
||||
.read = read,
|
||||
.write = write,
|
||||
.set_volume = set_volume,
|
||||
.get_volume = get_volume,
|
||||
.set_mute = set_mute,
|
||||
.get_mute = get_mute,
|
||||
.get_native_sample_rate = get_rate,
|
||||
.get_native_channels = get_channels,
|
||||
.get_capabilities = get_capabilities,
|
||||
.get_input_gain_multiplier = nullptr,
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" Module simulator_module;
|
||||
|
||||
Driver sdl_audio_driver = {
|
||||
.name = "sdl-audio",
|
||||
.compatible = (const char*[]) { "tactility,sdl-audio", nullptr },
|
||||
.start_device = start,
|
||||
.stop_device = stop,
|
||||
.api = &api,
|
||||
.device_type = &AUDIO_CODEC_TYPE,
|
||||
.owner = &simulator_module,
|
||||
.internal = nullptr,
|
||||
};
|
||||
Reference in New Issue
Block a user