#include "libs/audio.h" #include "libs/audio_internal.h" #include "libs/errno.h" #include #include #include #include #include #include #include #include #include namespace { namespace AudioOut2 = Libs::Audio::AudioOut2; std::mutex g_device_mutex; std::condition_variable g_device_cv; std::vector g_live_devices; int g_next_device = 1; int g_open_waiters = 0; bool g_block_opens = false; void Check(bool value, const char* text) { if (!value) { std::fprintf(stderr, "AudioOut2PortTests: failed: %s\n", text); std::abort(); } } struct PortParam { uint16_t port_type; uint16_t pad; uint32_t data_format; uint32_t sampling_freq; uint32_t flags; uint64_t user_handle; uint32_t reserved[10]; }; struct ContextParam { uint32_t max_ports; uint32_t max_object_ports; uint32_t guarantee_object_ports; uint32_t queue_depth; uint32_t num_grains; uint32_t flags; uint32_t reserved[10]; }; struct PortState { uint16_t output; uint8_t num_channels; uint8_t pad1; int16_t volume; uint16_t reroute_counter; uint32_t flags; uint32_t pad2; uint64_t reserved[6]; }; const auto* AsParam(const PortParam* param) { return reinterpret_cast(param); } const auto* AsParam(const ContextParam* param) { return reinterpret_cast(param); } auto* AsState(PortState* state) { return reinterpret_cast(state); } PortParam MakeParam(uint32_t data_format = 0x200) { PortParam param {}; param.data_format = data_format; param.sampling_freq = 48000; return param; } AudioOut2::AudioOut2ContextHandle CreateContext() { ContextParam param {}; param.queue_depth = 4; param.num_grains = 512; AudioOut2::AudioOut2ContextHandle context = 0; Check(AudioOut2::AudioOut2ContextCreate(AsParam(¶m), nullptr, 0, &context) == OK, "context create failed"); return context; } void BlockDeviceOpens() { std::lock_guard lock(g_device_mutex); g_open_waiters = 0; g_block_opens = true; } void WaitForDeviceOpens(int count) { std::unique_lock lock(g_device_mutex); g_device_cv.wait(lock, [count]() { return g_open_waiters >= count; }); } void ReleaseDeviceOpens() { std::lock_guard lock(g_device_mutex); g_block_opens = false; g_device_cv.notify_all(); } int LiveDeviceCount() { std::lock_guard lock(g_device_mutex); return static_cast(g_live_devices.size()); } void TestSlotReuse() { const auto context = CreateContext(); const auto param = MakeParam(); for (int i = 0; i < 300; i++) { AudioOut2::AudioOut2PortHandle port = 0; Check(AudioOut2::AudioOut2PortCreate(context, AsParam(¶m), &port) == OK, "port slot was not reusable"); Check(port != 0, "port handle is zero"); AudioOut2::AudioOut2PortDestroy(port); } AudioOut2::AudioOut2ContextDestroy(context); } void TestFullTableRecovers() { const auto context = CreateContext(); const auto param = MakeParam(); std::vector ports; ports.reserve(256); for (int i = 0; i < 256; i++) { AudioOut2::AudioOut2PortHandle port = 0; Check(AudioOut2::AudioOut2PortCreate(context, AsParam(¶m), &port) == OK, "port table filled early"); ports.push_back(port); } AudioOut2::AudioOut2PortHandle overflow = 0; Check(AudioOut2::AudioOut2PortCreate(context, AsParam(¶m), &overflow) != OK, "full port table accepted another port"); for (auto port: ports) { AudioOut2::AudioOut2PortDestroy(port); } AudioOut2::AudioOut2PortHandle port = 0; Check(AudioOut2::AudioOut2PortCreate(context, AsParam(¶m), &port) == OK, "port table did not recover"); AudioOut2::AudioOut2PortDestroy(port); AudioOut2::AudioOut2ContextDestroy(context); } void TestConcurrentCreates() { constexpr int thread_count = 8; const auto context = CreateContext(); const auto param = MakeParam(0x800); std::vector ports(thread_count); std::vector results(thread_count); std::vector threads; BlockDeviceOpens(); for (int i = 0; i < thread_count; i++) { threads.emplace_back([&, i]() { results[i] = AudioOut2::AudioOut2PortCreate(context, AsParam(¶m), &ports[i]); }); } WaitForDeviceOpens(thread_count); ReleaseDeviceOpens(); for (auto& thread: threads) { thread.join(); } for (int i = 0; i < thread_count; i++) { Check(results[i] == OK, "concurrent port create failed"); PortState state {}; AudioOut2::AudioOut2PortGetState(ports[i], AsState(&state)); Check(state.num_channels == 8, "concurrent create lost its reserved slot"); AudioOut2::AudioOut2PortDestroy(ports[i]); } Check(LiveDeviceCount() == 0, "concurrent create leaked a device"); AudioOut2::AudioOut2ContextDestroy(context); } void TestContextDestroyCancelsPendingCreate() { const auto context = CreateContext(); const auto param = MakeParam(); AudioOut2::AudioOut2PortHandle port = 0; int result = OK; BlockDeviceOpens(); std::thread creator( [&]() { result = AudioOut2::AudioOut2PortCreate(context, AsParam(¶m), &port); }); WaitForDeviceOpens(1); AudioOut2::AudioOut2ContextDestroy(context); ReleaseDeviceOpens(); creator.join(); Check(result != OK, "destroyed context retained a pending port create"); Check(LiveDeviceCount() == 0, "cancelled port create leaked a device"); } } // namespace namespace Libs::Audio::AudioInternal { int AudioOutOpen(int /*type*/, uint32_t /*samples_num*/, uint32_t /*freq*/, Format /*format*/) { std::unique_lock lock(g_device_mutex); const int handle = g_next_device++; g_live_devices.push_back(handle); g_open_waiters++; g_device_cv.notify_all(); g_device_cv.wait(lock, []() { return !g_block_opens; }); return handle; } void AudioOutClose(int handle) { std::lock_guard lock(g_device_mutex); const auto it = std::find(g_live_devices.begin(), g_live_devices.end(), handle); if (it != g_live_devices.end()) { g_live_devices.erase(it); } } uint32_t AudioOutOutputs(const OutputParam* /*params*/, uint32_t /*num*/, bool /*blocking*/) { return 0; } } // namespace Libs::Audio::AudioInternal namespace Libs::LibKernel { uint64_t KYTY_SYSV_ABI KernelGetProcessTime() { static std::atomic_uint64_t now {0}; return now.fetch_add(1000); } } // namespace Libs::LibKernel int main() { TestSlotReuse(); TestFullTableRecovers(); TestConcurrentCreates(); TestContextDestroyCancelsPendingCreate(); std::printf("AudioOut2PortTests: all cases passed\n"); return 0; }