// SPDX-License-Identifier: Apache-2.0 #define DOCTEST_CONFIG_IMPLEMENT #include "doctest.h" #include "sdl_audio.h" #include "sdl_audio_buffer.h" #include #include #include #include #include #include #include #include #include #include #include #include #include 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 input(20000); for (size_t i = 0; i < input.size(); ++i) input[i] = static_cast(i); std::vector 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 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 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 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(µphone, &capability) == ERROR_NOT_SUPPORTED); const AudioCodecStreamConfig config { 48000, 16, 1, AUDIO_CODEC_DIR_INPUT }; CHECK(audio_codec_open(µphone, &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(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 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(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 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(&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(&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(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(µphone, "tactility,sdl-audio") == ERROR_NONE) { doctest::Context context(data->argc, data->argv); context.setOption("no-breaks", true); data->result = context.run(); device_stop(µphone); 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 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; }