// SPDX-License-Identifier: Apache-2.0 #include "sdl_audio.h" #include "sdl_audio_buffer.h" #include #include #include #include #include #include #include #include #include #include #include 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 volume { 100.0f }; std::atomic muted { false }; std::atomic callbacks { 0 }; }; AudioData* get_data(Device* device) { return static_cast(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(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(sample * gain); std::memcpy(output + i, &sample, sizeof(sample)); } } void audio_callback(void* context, Uint8* stream, int length) { auto* data = static_cast(context); const size_t count = static_cast(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(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(destination) + done, count) : data->buffer.write(static_cast(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, };