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
@@ -45,6 +45,8 @@ cat > "$contents_path/Info.plist" <<'EOF'
<string>0.8.0-dev</string> <string>0.8.0-dev</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>1</string> <string>1</string>
<key>NSMicrophoneUsageDescription</key>
<string>Tactility uses the microphone when a simulator app records audio.</string>
</dict> </dict>
</plist> </plist>
EOF EOF
+4
View File
@@ -110,6 +110,9 @@ endif ()
# Defined as regular project for PC and component for ESP # Defined as regular project for PC and component for ESP
if (NOT DEFINED ENV{ESP_IDF_VERSION}) if (NOT DEFINED ENV{ESP_IDF_VERSION})
if (APPLE)
enable_language(OBJC OBJCXX)
endif ()
add_subdirectory(Tactility) add_subdirectory(Tactility)
add_subdirectory(TactilityFreeRtos) add_subdirectory(TactilityFreeRtos)
add_subdirectory(TactilityKernel) add_subdirectory(TactilityKernel)
@@ -137,6 +140,7 @@ if (NOT DEFINED ENV{ESP_IDF_VERSION})
add_subdirectory(Modules/lvgl-window-manager-module) add_subdirectory(Modules/lvgl-window-manager-module)
add_subdirectory(Drivers/gps-generic-module) add_subdirectory(Drivers/gps-generic-module)
add_subdirectory(Drivers/gps-meshtastic-module) add_subdirectory(Drivers/gps-meshtastic-module)
add_subdirectory(Drivers/audio-stream-module)
# FreeRTOS # FreeRTOS
set(FREERTOS_CONFIG_FILE_DIRECTORY ${PROJECT_SOURCE_DIR}/Devices/simulator/Source CACHE STRING "") set(FREERTOS_CONFIG_FILE_DIRECTORY ${PROJECT_SOURCE_DIR}/Devices/simulator/Source CACHE STRING "")
+9
View File
@@ -21,4 +21,13 @@ if (NOT DEFINED ENV{ESP_IDF_VERSION})
target_link_libraries(simulator PRIVATE ${SDL2_LIBRARIES}) target_link_libraries(simulator PRIVATE ${SDL2_LIBRARIES})
if (APPLE)
target_sources(simulator PRIVATE Source/drivers/sdl_audio_permission.mm)
target_link_libraries(simulator PRIVATE "-framework AVFoundation" "-framework Foundation")
# The developer executable can also request recording outside an .app bundle.
target_link_options(simulator INTERFACE
"LINKER:-sectcreate,__TEXT,__info_plist,${CMAKE_CURRENT_SOURCE_DIR}/macos-info.plist")
set_property(TARGET Tactility APPEND PROPERTY LINK_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/macos-info.plist")
endif ()
endif() endif()
@@ -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,
};
@@ -0,0 +1,13 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <tactility/drivers/audio_codec.h>
struct SdlAudioConfig {
AudioCodecDirection direction;
};
#ifdef __APPLE__
// Nonblocking: ERROR_RESOURCE_BUSY means that the user has not answered the prompt yet.
error_t sdl_audio_microphone_permission();
#endif
@@ -0,0 +1,52 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <algorithm>
#include <array>
#include <atomic>
#include <cstdint>
#include <cstring>
// Single producer / single consumer. The SDL callback is a native thread: it must never
// allocate, block, log, or call FreeRTOS. Unsigned counters also work across wraparound.
class SdlAudioBuffer {
static constexpr uint32_t CAPACITY = 16384; // samples; ~171 ms of 48 kHz stereo
std::array<int16_t, CAPACITY> samples {};
std::atomic<uint32_t> read_position { 0 };
std::atomic<uint32_t> write_position { 0 };
public:
size_t write(const void* source, size_t count) {
const uint32_t write = write_position.load(std::memory_order_relaxed);
const uint32_t read = read_position.load(std::memory_order_acquire);
count = std::min(count, static_cast<size_t>(CAPACITY - (write - read)));
const size_t first = std::min(count, static_cast<size_t>(CAPACITY - write % CAPACITY));
const auto* bytes = static_cast<const uint8_t*>(source);
std::memcpy(samples.data() + write % CAPACITY, bytes, first * sizeof(int16_t));
std::memcpy(samples.data(), bytes + first * sizeof(int16_t), (count - first) * sizeof(int16_t));
write_position.store(write + count, std::memory_order_release);
return count;
}
size_t read(void* destination, size_t count) {
const uint32_t read = read_position.load(std::memory_order_relaxed);
const uint32_t write = write_position.load(std::memory_order_acquire);
count = std::min(count, static_cast<size_t>(write - read));
const size_t first = std::min(count, static_cast<size_t>(CAPACITY - read % CAPACITY));
auto* bytes = static_cast<uint8_t*>(destination);
std::memcpy(bytes, samples.data() + read % CAPACITY, first * sizeof(int16_t));
std::memcpy(bytes + first * sizeof(int16_t), samples.data(), (count - first) * sizeof(int16_t));
read_position.store(read + count, std::memory_order_release);
return count;
}
bool empty() const {
return read_position.load(std::memory_order_acquire) == write_position.load(std::memory_order_acquire);
}
// Only while the SDL device is closed and no caller is doing I/O.
void reset() {
read_position.store(0);
write_position.store(0);
}
};
@@ -0,0 +1,27 @@
// SPDX-License-Identifier: Apache-2.0
#include "sdl_audio.h"
#import <AVFoundation/AVFoundation.h>
#include <atomic>
error_t sdl_audio_microphone_permission() {
@autoreleasepool {
switch ([AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeAudio]) {
case AVAuthorizationStatusAuthorized:
return ERROR_NONE;
case AVAuthorizationStatusDenied:
case AVAuthorizationStatusRestricted:
return ERROR_NOT_ALLOWED;
case AVAuthorizationStatusNotDetermined: {
static std::atomic<bool> requested { false };
if (!requested.exchange(true)) {
[AVCaptureDevice requestAccessForMediaType:AVMediaTypeAudio completionHandler:^(BOOL granted) {
requested.store(false);
}];
}
return ERROR_RESOURCE_BUSY;
}
}
return ERROR_NOT_ALLOWED;
}
}
+9
View File
@@ -1,4 +1,5 @@
#include "drivers/sdl_display.h" #include "drivers/sdl_display.h"
#include "drivers/sdl_audio.h"
#include <tactility/device.h> #include <tactility/device.h>
#include <tactility/device_listener.h> #include <tactility/device_listener.h>
@@ -16,11 +17,13 @@ extern "C" {
extern Driver sdl_display_driver; extern Driver sdl_display_driver;
extern Driver sdl_pointer_driver; extern Driver sdl_pointer_driver;
extern Driver sdl_keyboard_driver; extern Driver sdl_keyboard_driver;
extern Driver sdl_audio_driver;
static Driver* const simulator_drivers[] = { static Driver* const simulator_drivers[] = {
&sdl_display_driver, &sdl_display_driver,
&sdl_pointer_driver, &sdl_pointer_driver,
&sdl_keyboard_driver, &sdl_keyboard_driver,
&sdl_audio_driver,
nullptr nullptr
}; };
@@ -54,6 +57,10 @@ static SdlDisplayConfig sdl_display_config = { 480, 320 };
static Device sdl_display_device {}; static Device sdl_display_device {};
static Device sdl_pointer_device {}; static Device sdl_pointer_device {};
static Device sdl_keyboard_device {}; static Device sdl_keyboard_device {};
static Device sdl_speaker_device {};
static Device sdl_microphone_device {};
static const SdlAudioConfig sdl_speaker_config { AUDIO_CODEC_DIR_OUTPUT };
static const SdlAudioConfig sdl_microphone_config { AUDIO_CODEC_DIR_INPUT };
static bool construct_add_start(Device* device, Device* parent, const char* name, const void* config, const char* compatible) { static bool construct_add_start(Device* device, Device* parent, const char* name, const void* config, const char* compatible) {
device->address = 0; device->address = 0;
@@ -109,6 +116,8 @@ static void on_root_started(Device* device, DeviceEvent event, void* context) {
construct_add_start(&sdl_display_device, device, "display0", &sdl_display_config, "tactility,sdl-display"); construct_add_start(&sdl_display_device, device, "display0", &sdl_display_config, "tactility,sdl-display");
construct_add_start(&sdl_pointer_device, device, "pointer0", nullptr, "tactility,sdl-pointer"); construct_add_start(&sdl_pointer_device, device, "pointer0", nullptr, "tactility,sdl-pointer");
construct_add_start(&sdl_keyboard_device, device, "keyboard0", nullptr, "tactility,sdl-keyboard"); construct_add_start(&sdl_keyboard_device, device, "keyboard0", nullptr, "tactility,sdl-keyboard");
construct_add_start(&sdl_speaker_device, device, "speaker0", &sdl_speaker_config, "tactility,sdl-audio");
construct_add_start(&sdl_microphone_device, device, "microphone0", &sdl_microphone_config, "tactility,sdl-audio");
} }
extern "C" { extern "C" {
+1
View File
@@ -1,3 +1,4 @@
dependencies: dependencies:
- Platforms/platform-posix - Platforms/platform-posix
- Drivers/audio-stream-module
dts: simulator.dts dts: simulator.dts
+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleIdentifier</key>
<string>org.tactilityproject.simulator</string>
<key>CFBundleName</key>
<string>Tactility</string>
<key>NSMicrophoneUsageDescription</key>
<string>Tactility uses the microphone when a simulator app records audio.</string>
</dict>
</plist>
+14
View File
@@ -0,0 +1,14 @@
add_executable(SimulatorAudioTests EXCLUDE_FROM_ALL
audio.cpp
../Source/drivers/sdl_audio.cpp
)
target_include_directories(SimulatorAudioTests PRIVATE ${DOCTESTINC} ../Source/drivers)
target_link_libraries(SimulatorAudioTests PRIVATE TactilityKernel platform-posix audio-stream-module SDL2-static)
if (APPLE)
target_sources(SimulatorAudioTests PRIVATE ../Source/drivers/sdl_audio_permission.mm)
target_link_libraries(SimulatorAudioTests PRIVATE "-framework AVFoundation" "-framework Foundation")
endif ()
add_test(NAME SimulatorAudioTests COMMAND SimulatorAudioTests)
set_tests_properties(SimulatorAudioTests PROPERTIES TIMEOUT 30 ENVIRONMENT "SDL_AUDIODRIVER=dummy;SIM_AUDIO_INPUT=;SIM_AUDIO_OUTPUT=")
add_test(NAME SimulatorAudioPcmTests COMMAND SimulatorAudioTests --no-skip "--test-case=disk PCM*")
set_tests_properties(SimulatorAudioPcmTests PROPERTIES TIMEOUT 30 ENVIRONMENT "SDL_AUDIODRIVER=disk;SIM_AUDIO_INPUT=;SIM_AUDIO_OUTPUT=")
+301
View File
@@ -0,0 +1,301 @@
// SPDX-License-Identifier: Apache-2.0
#define DOCTEST_CONFIG_IMPLEMENT
#include "doctest.h"
#include "sdl_audio.h"
#include "sdl_audio_buffer.h"
#include <tactility/device.h>
#include <tactility/driver.h>
#include <tactility/drivers/audio_stream.h>
#include <tactility/freertos/task.h>
#include <tactility/kernel_init.h>
#include <SDL2/SDL.h>
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <cstdio>
#include <fstream>
#include <unistd.h>
#include <vector>
extern Driver sdl_audio_driver;
extern "C" {
extern Module platform_posix_module;
extern Module audio_stream_module;
extern Device audio_stream_device;
static Driver* const drivers[] = { &sdl_audio_driver, nullptr };
Module simulator_module = { .name = "simulator-audio-test", .drivers = drivers };
}
static const SdlAudioConfig output_config { AUDIO_CODEC_DIR_OUTPUT };
static const SdlAudioConfig input_config { AUDIO_CODEC_DIR_INPUT };
static Device speaker { .name = "speaker-test", .config = &output_config };
static Device microphone { .name = "microphone-test", .config = &input_config };
struct Stream {
AudioStreamHandle handle = nullptr;
~Stream() { if (handle != nullptr) audio_stream_close(handle); }
};
TEST_CASE("bounded PCM buffer preserves data across wrap and overflow") {
SdlAudioBuffer buffer;
std::vector<int16_t> input(20000);
for (size_t i = 0; i < input.size(); ++i) input[i] = static_cast<int16_t>(i);
std::vector<int16_t> output(20000, -1);
CHECK(buffer.read(output.data(), output.size()) == 0);
REQUIRE(buffer.write(input.data(), input.size()) == 16384);
CHECK(buffer.write(input.data(), 2) == 0);
REQUIRE(buffer.read(output.data(), 10000) == 10000);
CHECK(std::equal(output.begin(), output.begin() + 10000, input.begin()));
REQUIRE(buffer.write(input.data(), 10000) == 10000);
REQUIRE(buffer.read(output.data(), output.size()) == 16384);
CHECK(std::equal(output.begin(), output.begin() + 6384, input.begin() + 10000));
CHECK(std::equal(output.begin() + 6384, output.begin() + 16384, input.begin()));
CHECK(buffer.empty());
}
TEST_CASE("simulator streams support independent full duplex and common PCM rates") {
for (uint32_t rate : { 16000u, 44100u, 48000u }) {
CAPTURE(rate);
const AudioStreamConfig config { rate, 16, 1 };
Stream input;
Stream output;
REQUIRE(audio_stream_open_input(&audio_stream_device, &config, &input.handle) == ERROR_NONE);
REQUIRE(audio_stream_open_output(&audio_stream_device, &config, &output.handle) == ERROR_NONE);
AudioStreamHandle duplicate = nullptr;
CHECK(audio_stream_open_output(&audio_stream_device, &config, &duplicate) == ERROR_INVALID_STATE);
std::vector<int16_t> samples(rate / 20, 1234);
size_t count = 0;
REQUIRE(audio_stream_write(output.handle, samples.data(), samples.size() * 2, &count, pdMS_TO_TICKS(1000)) == ERROR_NONE);
CHECK(count == samples.size() * 2);
REQUIRE(audio_stream_read(input.handle, samples.data(), samples.size() * 2, &count, pdMS_TO_TICKS(1000)) == ERROR_NONE);
CHECK(count == samples.size() * 2);
CHECK(std::all_of(samples.begin(), samples.end(), [](int16_t sample) { return sample == 0; }));
REQUIRE(audio_stream_close(output.handle) == ERROR_NONE);
output.handle = nullptr;
// Closing the speaker must not stop the microphone.
REQUIRE(audio_stream_read(input.handle, samples.data(), samples.size() * 2, &count, pdMS_TO_TICKS(1000)) == ERROR_NONE);
}
}
TEST_CASE("simulator audio controls persist across opens and disabling closes output") {
auto* device = &audio_stream_device;
REQUIRE(audio_stream_set_volume(device, AUDIO_CODEC_DIR_OUTPUT, 25.0f) == ERROR_NONE);
REQUIRE(audio_stream_set_mute(device, AUDIO_CODEC_DIR_OUTPUT, true) == ERROR_NONE);
const AudioStreamConfig config { 48000, 16, 2 };
Stream output;
REQUIRE(audio_stream_open_output(device, &config, &output.handle) == ERROR_NONE);
float volume = 0;
bool muted = false;
REQUIRE(audio_codec_get_volume(&speaker, AUDIO_CODEC_DIR_OUTPUT, &volume) == ERROR_NONE);
REQUIRE(audio_codec_get_mute(&speaker, AUDIO_CODEC_DIR_OUTPUT, &muted) == ERROR_NONE);
CHECK(volume == 25.0f);
CHECK(muted);
REQUIRE(audio_stream_set_enabled(device, AUDIO_CODEC_DIR_OUTPUT, false) == ERROR_NONE);
output.handle = nullptr; // set_enabled closes and owns destruction of the handle
CHECK(audio_stream_open_output(device, &config, &output.handle) == ERROR_NOT_ALLOWED);
REQUIRE(audio_stream_set_enabled(device, AUDIO_CODEC_DIR_OUTPUT, true) == ERROR_NONE);
REQUIRE(audio_stream_set_mute(device, AUDIO_CODEC_DIR_OUTPUT, false) == ERROR_NONE);
REQUIRE(audio_stream_open_output(device, &config, &output.handle) == ERROR_NONE);
}
TEST_CASE("bounded playback reports partial progress on a nonblocking timeout") {
const AudioStreamConfig config { 48000, 16, 2 };
Stream output;
REQUIRE(audio_stream_open_output(&audio_stream_device, &config, &output.handle) == ERROR_NONE);
std::vector<int16_t> samples(48000 * 2, 0);
size_t count = 999;
CHECK(audio_stream_write(output.handle, samples.data(), samples.size() * 2, &count, 0) == ERROR_TIMEOUT);
CHECK(count > 0);
CHECK(count < samples.size() * 2);
CHECK(count % 4 == 0);
}
TEST_CASE("converted streams report partial progress on timeout") {
const AudioStreamConfig config { 16000, 16, 1 };
Stream output;
Stream input;
REQUIRE(audio_stream_open_output(&audio_stream_device, &config, &output.handle) == ERROR_NONE);
REQUIRE(audio_stream_open_input(&audio_stream_device, &config, &input.handle) == ERROR_NONE);
std::vector<int16_t> samples(16000, 0);
size_t count = 999;
CHECK(audio_stream_write(output.handle, samples.data(), samples.size() * 2, &count, 0) == ERROR_TIMEOUT);
CHECK(count > 0);
CHECK(count < samples.size() * 2);
CHECK(count % 2 == 0);
vTaskDelay(pdMS_TO_TICKS(30)); // allow dummy capture to produce some samples
count = 999;
CHECK(audio_stream_read(input.handle, samples.data(), samples.size() * 2, &count, 0) == ERROR_TIMEOUT);
CHECK(count > 0);
CHECK(count < samples.size() * 2);
CHECK(count % 2 == 0);
}
TEST_CASE("missing selected microphone does not prevent speaker playback") {
REQUIRE(SDL_setenv("SIM_AUDIO_INPUT", "tactility-nonexistent-microphone", 1) == 0);
AudioCodecDirection capability;
CHECK(audio_codec_get_capabilities(&microphone, &capability) == ERROR_NOT_SUPPORTED);
const AudioCodecStreamConfig config { 48000, 16, 1, AUDIO_CODEC_DIR_INPUT };
CHECK(audio_codec_open(&microphone, &config) == ERROR_NOT_SUPPORTED);
CHECK(audio_codec_get_capabilities(&speaker, &capability) == ERROR_NONE);
CHECK(capability == AUDIO_CODEC_DIR_OUTPUT);
REQUIRE(SDL_setenv("SIM_AUDIO_INPUT", "", 1) == 0);
}
TEST_CASE("unsupported sample widths fail without breaking a later open") {
AudioStreamConfig config { 16000, 24, 1 };
Stream output;
CHECK(audio_stream_open_output(&audio_stream_device, &config, &output.handle) != ERROR_NONE);
CHECK(output.handle == nullptr);
config.bits_per_sample = 16;
REQUIRE(audio_stream_open_output(&audio_stream_device, &config, &output.handle) == ERROR_NONE);
}
TEST_CASE("zero sample rate is rejected before conversion") {
const AudioStreamConfig config { 0, 16, 1 };
Stream output;
CHECK(audio_stream_open_output(&audio_stream_device, &config, &output.handle) == ERROR_INVALID_ARGUMENT);
}
TEST_CASE("disabling while a codec is opening cancels the pending stream") {
const auto* original_api = static_cast<const AudioCodecApi*>(sdl_audio_driver.api);
static const AudioCodecApi* wrapped_api;
wrapped_api = original_api;
AudioCodecApi delayed_api = *original_api;
delayed_api.open = [](Device* device, const AudioCodecStreamConfig* config) {
vTaskDelay(pdMS_TO_TICKS(50)); // models waiting for microphone permission
return wrapped_api->open(device, config);
};
sdl_audio_driver.api = &delayed_api;
struct RestoreApi {
const AudioCodecApi* api;
~RestoreApi() { sdl_audio_driver.api = api; }
} restore { original_api };
REQUIRE(xTaskCreate([](void*) {
vTaskDelay(pdMS_TO_TICKS(5));
audio_stream_set_enabled(&audio_stream_device, AUDIO_CODEC_DIR_OUTPUT, false);
// Re-enabling does not resurrect the cancelled attempt.
audio_stream_set_enabled(&audio_stream_device, AUDIO_CODEC_DIR_OUTPUT, true);
vTaskDelete(nullptr);
}, "disable-audio", 8192, nullptr, 1, nullptr) == pdPASS);
const AudioStreamConfig config { 48000, 16, 2 };
Stream output;
CHECK(audio_stream_open_output(&audio_stream_device, &config, &output.handle) == ERROR_NOT_ALLOWED);
CHECK(output.handle == nullptr);
sdl_audio_driver.api = original_api;
REQUIRE(audio_stream_open_output(&audio_stream_device, &config, &output.handle) == ERROR_NONE);
}
TEST_CASE("hardware output smoke test" * doctest::skip()) {
// Explicit opt-in only: SDL_AUDIODRIVER=coreaudio ... --no-skip --test-case='hardware output smoke test'
const AudioStreamConfig config { 48000, 16, 2 };
Stream output;
REQUIRE(audio_stream_set_volume(&audio_stream_device, AUDIO_CODEC_DIR_OUTPUT, 20) == ERROR_NONE);
REQUIRE(audio_stream_open_output(&audio_stream_device, &config, &output.handle) == ERROR_NONE);
std::vector<int16_t> samples(48000); // half a second, stereo, quiet 440 Hz tone
for (size_t frame = 0; frame < samples.size() / 2; ++frame) {
samples[frame * 2] = samples[frame * 2 + 1] = static_cast<int16_t>(3000 * std::sin(frame * 440.0 * 6.283185307 / 48000));
}
size_t count = 0;
REQUIRE(audio_stream_write(output.handle, samples.data(), samples.size() * 2, &count, pdMS_TO_TICKS(2000)) == ERROR_NONE);
CHECK(count == samples.size() * 2);
}
TEST_CASE("disk PCM capture and playback apply gain and mute" * doctest::skip()) {
REQUIRE(std::strcmp(SDL_GetCurrentAudioDriver(), "disk") == 0);
auto* device = &audio_stream_device;
const AudioStreamConfig config { 48000, 16, 1 };
Stream input;
Stream output;
REQUIRE(audio_stream_set_volume(device, AUDIO_CODEC_DIR_INPUT, 50) == ERROR_NONE);
REQUIRE(audio_stream_set_volume(device, AUDIO_CODEC_DIR_OUTPUT, 25) == ERROR_NONE);
REQUIRE(audio_stream_open_input(device, &config, &input.handle) == ERROR_NONE);
REQUIRE(audio_stream_open_output(device, &config, &output.handle) == ERROR_NONE);
std::vector<int16_t> samples(960, -1);
size_t count = 0;
REQUIRE(audio_stream_read(input.handle, samples.data(), samples.size() * 2, &count, pdMS_TO_TICKS(1000)) == ERROR_NONE);
CHECK(count == samples.size() * 2);
// Fixture contains 10000; microphone gain is 50%.
CHECK(std::all_of(samples.begin(), samples.end(), [](int16_t sample) { return sample == 5000; }));
REQUIRE(audio_stream_write(output.handle, samples.data(), samples.size() * 2, &count, pdMS_TO_TICKS(1000)) == ERROR_NONE);
REQUIRE(audio_stream_close(output.handle) == ERROR_NONE);
output.handle = nullptr;
{
std::ifstream file(std::getenv("SDL_DISKAUDIOFILE"), std::ios::binary);
REQUIRE(file.good());
size_t nonzero = 0;
int16_t sample;
while (file.read(reinterpret_cast<char*>(&sample), sizeof(sample))) {
CHECK((sample == 0 || sample == 1250)); // 25% output volume
if (sample != 0) nonzero++;
}
CHECK(nonzero == samples.size() * 2); // mono was duplicated to stereo
}
REQUIRE(audio_stream_set_mute(device, AUDIO_CODEC_DIR_INPUT, true) == ERROR_NONE);
REQUIRE(audio_stream_read(input.handle, samples.data(), samples.size() * 2, &count, pdMS_TO_TICKS(1000)) == ERROR_NONE);
CHECK(std::all_of(samples.begin(), samples.end(), [](int16_t sample) { return sample == 0; }));
REQUIRE(audio_stream_set_mute(device, AUDIO_CODEC_DIR_OUTPUT, true) == ERROR_NONE);
REQUIRE(audio_stream_open_output(device, &config, &output.handle) == ERROR_NONE);
std::fill(samples.begin(), samples.end(), 5000);
REQUIRE(audio_stream_write(output.handle, samples.data(), samples.size() * 2, &count, pdMS_TO_TICKS(1000)) == ERROR_NONE);
REQUIRE(audio_stream_close(output.handle) == ERROR_NONE);
output.handle = nullptr;
std::ifstream file(std::getenv("SDL_DISKAUDIOFILE"), std::ios::binary);
REQUIRE(file.good());
size_t total = 0;
int16_t sample;
while (file.read(reinterpret_cast<char*>(&sample), sizeof(sample))) {
CHECK(sample == 0);
total++;
}
CHECK(total >= samples.size() * 2);
}
struct TestContext { int argc; char** argv; int result = 1; };
static void run_tests(void* argument) {
auto* data = static_cast<TestContext*>(argument);
Module* modules[] = { &platform_posix_module, &simulator_module, &audio_stream_module, nullptr };
DtsDevice devices[] = { DTS_DEVICE_TERMINATOR };
if (kernel_init(modules, devices) == ERROR_NONE
&& device_construct_add_start(&speaker, "tactility,sdl-audio") == ERROR_NONE
&& device_construct_add_start(&microphone, "tactility,sdl-audio") == ERROR_NONE) {
doctest::Context context(data->argc, data->argv);
context.setOption("no-breaks", true);
data->result = context.run();
device_stop(&microphone);
device_stop(&speaker);
}
vTaskEndScheduler();
vTaskDelete(nullptr);
}
int main(int argc, char** argv) {
if (std::getenv("SDL_AUDIODRIVER") == nullptr) SDL_setenv("SDL_AUDIODRIVER", "dummy", 1);
char input_path[] = "sim-audio-input-XXXXXX";
char output_path[] = "sim-audio-output-XXXXXX";
const bool disk = std::strcmp(std::getenv("SDL_AUDIODRIVER"), "disk") == 0;
if (disk) {
const int input_fd = mkstemp(input_path);
const int output_fd = mkstemp(output_path);
if (input_fd < 0 || output_fd < 0) return 1;
FILE* file = fdopen(input_fd, "wb");
if (file == nullptr) return 1;
const std::vector<int16_t> fixture(48000, 10000);
const size_t written = std::fwrite(fixture.data(), sizeof(int16_t), fixture.size(), file);
std::fclose(file);
::close(output_fd);
if (written != fixture.size()) return 1;
SDL_setenv("SDL_DISKAUDIOFILEIN", input_path, 1);
SDL_setenv("SDL_DISKAUDIOFILE", output_path, 1);
}
TestContext data { argc, argv };
if (xTaskCreate(run_tests, "audio-test", 32768, &data, 1, nullptr) != pdPASS) return 1;
vTaskStartScheduler();
if (disk) {
std::remove(input_path);
std::remove(output_path);
}
return data.result;
}
+93
View File
@@ -0,0 +1,93 @@
# Simulator audio
The desktop simulator exposes an SDL speaker and microphone through Tactility's
standard `audio_stream_*` API. On macOS, SDL uses CoreAudio. Audio Settings controls
the simulator's input/output volume, mute, and enabled state; these controls do not
change macOS's system volume.
## Running on macOS
Build the simulator in the usual host build environment (with `python`, `lark`,
and `pyyaml` available, and without `ESP_IDF_VERSION`):
```sh
cmake -S . -B buildsim
cmake --build buildsim --target Tactility -j 8
```
Create a fresh application bundle (the script deliberately refuses to overwrite
an existing bundle):
```sh
sh Buildscripts/release-simulator-macos-app.sh buildsim release/Tactility-audio.app
open release/Tactility-audio.app
```
Alternatively, run `../buildsim/Tactility/Tactility` with `Data/` as the working
directory. Both the executable and the application bundle include a microphone
usage description. macOS asks for microphone access on the first actual recording
request, not at simulator startup. If denied, enable access in **System Settings
→ Privacy & Security → Microphone** and relaunch. For command-line launches, macOS
may attribute the permission to the terminal or launching application.
A Mac mini needs an external input device, such as a USB mic or headset. Without
an input device, speaker output still works and input is reported unavailable.
Connect the input before launch for predictable discovery/UI behavior.
## Selecting devices
By default, each stream opens the system's default device. Startup logs list SDL's
device names. Optional environment variables select an exact name:
```sh
SIM_AUDIO_OUTPUT="Mac mini Speakers" SIM_AUDIO_INPUT="USB Microphone" \
release/Tactility-audio.app/Contents/MacOS/Tactility
```
- `SIM_AUDIO_OUTPUT`: exact output name, or `none` to disable output.
- `SIM_AUDIO_INPUT`: exact input name, or `none` to disable input.
- Unset or empty values use the system default.
Selection is applied when opening a stream. An already-open stream does not
automatically switch when the system default changes; close/reopen it or relaunch.
A missing selected device causes an open failure rather than silently selecting a
different device. Permission/device errors are logged under `SdlAudio`.
## Formats and behavior
- Signed 16-bit PCM; the shared stream module converts app sample rates and channel
counts to 48 kHz mono capture / stereo playback. SDL handles host format conversion.
- One input and one output stream can be open simultaneously.
- Read/write from a worker task, never the LVGL thread.
- Audio callbacks use bounded lock-free buffers, with silence on playback underrun
and dropped incoming frames on capture overflow.
- Reads/writes report partial byte counts on timeout, including converted streams.
- Closing output drains its bounded buffer for up to 250 ms. Closing input does not
affect output, and vice versa.
- No audio mixing or acoustic echo cancellation is added by this backend. Use
headphones when testing simultaneous microphone capture and playback.
## Verification
```sh
cmake --build buildsim --target SimulatorAudioTests -j 8
ctest --test-dir buildsim/Tests -R SimulatorAudio --output-on-failure
```
These tests use SDL's dummy and disk backends without requiring microphone access.
They cover full duplex, 16/44.1/48 kHz app formats, bounded buffering, partial
timeouts, enable/disable, unavailable inputs, and sample-level input/output gain
and mute. They also cover disabling audio while a slow codec open is pending,
as can happen during a microphone permission prompt. Disk fixtures and output
files are temporary and removed after the run.
To explicitly play a quiet half-second 440 Hz tone through real Mac audio:
```sh
SDL_AUDIODRIVER=coreaudio SIM_AUDIO_OUTPUT= \
buildsim/Tests/simulator/SimulatorAudioTests \
--no-skip --test-case="hardware output smoke test"
```
Physical microphone capture, the first-use permission prompt, and live device
unplugging require a separate manual check with an input device attached.
@@ -113,6 +113,10 @@ struct AudioStreamHandleImpl : AudioStreamHandleData {
SemaphoreHandle_t drain_semaphore = nullptr; 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 { struct AudioStreamData {
Device* input_codec = nullptr; Device* input_codec = nullptr;
Device* output_codec = nullptr; Device* output_codec = nullptr;
@@ -130,6 +134,8 @@ struct AudioStreamData {
bool output_muted = false; bool output_muted = false;
AudioStreamHandleImpl* open_input = nullptr; AudioStreamHandleImpl* open_input = nullptr;
AudioStreamHandleImpl* open_output = 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 // 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. // through them, so close (possibly forced by set_enabled) can't race with read/write.
SemaphoreHandle_t mutex = nullptr; SemaphoreHandle_t mutex = nullptr;
@@ -261,7 +267,7 @@ error_t open_stream(Device* device, const struct AudioStreamConfig* config, Audi
return ERROR_INVALID_ARGUMENT; return ERROR_INVALID_ARGUMENT;
} }
if (config->channels == 0) { if (config->channels == 0 || config->sample_rate == 0) {
return ERROR_INVALID_ARGUMENT; 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 // 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. // 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; *slot = reservation;
xSemaphoreGive(data->mutex); xSemaphoreGive(data->mutex);
@@ -361,6 +369,18 @@ error_t open_stream(Device* device, const struct AudioStreamConfig* config, Audi
} }
xSemaphoreTake(data->mutex, portMAX_DELAY); 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; *slot = handle;
xSemaphoreGive(data->mutex); 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) { 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); auto* handle = static_cast<AudioStreamHandleImpl*>(handle_base);
if (handle->direction != AUDIO_CODEC_DIR_INPUT || handle->bytes_per_frame == 0) { if (handle->direction != AUDIO_CODEC_DIR_INPUT || handle->bytes_per_frame == 0) {
return ERROR_INVALID_STATE; 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; size_t codec_bytes_read = 0;
result = audio_codec_read(data->input_codec, handle->codec_buffer.data(), codec_bytes_needed, &codec_bytes_read, timeout); 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; 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()); const int16_t* rate_input = reinterpret_cast<const int16_t*>(handle->codec_buffer.data());
uint8_t rate_input_channels = handle->codec_channels; 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); auto* samples = reinterpret_cast<int16_t*>(out_data);
size_t sample_count = *bytes_read / sizeof(int16_t); size_t sample_count = *bytes_read / sizeof(int16_t);
for (size_t i = 0; i < sample_count; i++) { 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) { 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); auto* handle = static_cast<AudioStreamHandleImpl*>(handle_base);
if (handle->direction != AUDIO_CODEC_DIR_OUTPUT || handle->bytes_per_frame == 0) { if (handle->direction != AUDIO_CODEC_DIR_OUTPUT || handle->bytes_per_frame == 0) {
return ERROR_INVALID_STATE; 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_to_write = codec_frames * handle->codec_bytes_per_frame;
size_t codec_bytes_written = 0; 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); 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) { if (bytes_written != nullptr && codec_frames > 0) {
// The caller provided `data_size` worth of input; we consumed all of it (resampled/converted). // A bounded host/hardware queue can accept only part of a converted write,
*bytes_written = data_size; // 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; data->output_enabled = enabled;
} }
// Capture and clear the slot under the lock so we hand close_stream() a pointer that // A pending open has no handle to close yet. Let its owning task clean up when
// can't simultaneously be torn down by a racing close from the owning app (close_stream // the codec returns, even if the user re-enables the direction in the meantime.
// re-checks `*slot == handle` and no-ops if it's already been cleared/replaced).
AudioStreamHandleImpl* to_close = nullptr; AudioStreamHandleImpl* to_close = nullptr;
if (!enabled) { if (!enabled) {
AudioStreamHandleImpl** slot = is_input ? &data->open_input : &data->open_output; 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); xSemaphoreGive(data->mutex);
+2
View File
@@ -11,6 +11,7 @@ add_subdirectory(${CMAKE_SOURCE_DIR}/Modules/crypt-module/tests ${CMAKE_CURRENT_
add_subdirectory(${CMAKE_SOURCE_DIR}/Modules/app-module/tests ${CMAKE_CURRENT_BINARY_DIR}/app-module) add_subdirectory(${CMAKE_SOURCE_DIR}/Modules/app-module/tests ${CMAKE_CURRENT_BINARY_DIR}/app-module)
add_subdirectory(${CMAKE_SOURCE_DIR}/Modules/app-posix-module/tests ${CMAKE_CURRENT_BINARY_DIR}/app-posix-module) add_subdirectory(${CMAKE_SOURCE_DIR}/Modules/app-posix-module/tests ${CMAKE_CURRENT_BINARY_DIR}/app-posix-module)
add_subdirectory(${CMAKE_SOURCE_DIR}/Modules/http-module/tests ${CMAKE_CURRENT_BINARY_DIR}/http-module) add_subdirectory(${CMAKE_SOURCE_DIR}/Modules/http-module/tests ${CMAKE_CURRENT_BINARY_DIR}/http-module)
add_subdirectory(${CMAKE_SOURCE_DIR}/Devices/simulator/tests ${CMAKE_CURRENT_BINARY_DIR}/simulator)
add_custom_target(build-tests) add_custom_target(build-tests)
add_dependencies(build-tests ServiceModuleTests) add_dependencies(build-tests ServiceModuleTests)
@@ -21,3 +22,4 @@ add_dependencies(build-tests CryptModuleTests)
add_dependencies(build-tests AppModuleTests) add_dependencies(build-tests AppModuleTests)
add_dependencies(build-tests AppPosixModuleTests) add_dependencies(build-tests AppPosixModuleTests)
add_dependencies(build-tests HttpModuleTests) add_dependencies(build-tests HttpModuleTests)
add_dependencies(build-tests SimulatorAudioTests)