diff --git a/src/libs/audio.cpp b/src/libs/audio.cpp index 32cb751..2011023 100644 --- a/src/libs/audio.cpp +++ b/src/libs/audio.cpp @@ -82,6 +82,7 @@ public: Id AudioOutOpen(int type, uint32_t samples_num, uint32_t freq, Format format); bool AudioOutClose(Id handle); bool AudioOutValid(Id handle); + bool AudioOutHasDevice(Id handle); bool AudioOutSetVolume(Id handle, uint32_t bitflag, const int* volume); uint32_t AudioOutOutputs(OutputParam* params, uint32_t num, bool blocking = true); bool AudioOutGetStatus(Id handle, int* type, int* channels_num); @@ -152,6 +153,10 @@ void AudioOutClose(int handle) { } } +bool AudioOutHasDevice(int handle) { + return g_audio != nullptr && handle > 0 && g_audio->AudioOutHasDevice(Audio::Id(handle)); +} + uint32_t AudioOutOutputs(const OutputParam* params, uint32_t num, bool blocking) { if (g_audio == nullptr || params == nullptr || num == 0) { return 0; @@ -451,6 +456,13 @@ bool Audio::AudioOutValid(Id handle) { m_out_ports[handle.GetId()].used); } +bool Audio::AudioOutHasDevice(Id handle) { + Common::LockGuard lock(m_mutex); + + return (handle.GetId() >= 0 && handle.GetId() < OUT_PORTS_MAX && + m_out_ports[handle.GetId()].used && m_out_ports[handle.GetId()].audio_device != 0); +} + bool Audio::AudioOutGetStatus(Id handle, int* type, int* channels_num) { Common::LockGuard lock(m_mutex); @@ -516,16 +528,18 @@ uint32_t Audio::AudioOutOutputs(OutputParam* params, uint32_t num, bool blocking max_wait_time = (wait_time > max_wait_time ? wait_time : max_wait_time); } - bool all_ports_have_device = true; + bool any_port_has_device = false; for (uint32_t i = 0; i < num; i++) { - if (m_out_ports[params[i].handle.GetId()].audio_device == 0) { - all_ports_have_device = false; + if (m_out_ports[params[i].handle.GetId()].audio_device != 0) { + any_port_has_device = true; break; } } - // Device-backed ports are paced by the SDL queue above. - if (blocking && max_wait_time != 0 && !all_ports_have_device) { + // One real output device is enough to pace the whole synchronized batch. Applying the fallback + // when a vibration port is present would rate-limit the device-backed ports to exactly 1x and + // prevent their SDL queues from building an underrun cushion. + if (blocking && max_wait_time != 0 && !any_port_has_device) { Common::Thread::SleepMicro(max_wait_time); } diff --git a/src/libs/audio_internal.h b/src/libs/audio_internal.h index 9bf58ce..82be73c 100644 --- a/src/libs/audio_internal.h +++ b/src/libs/audio_internal.h @@ -26,6 +26,7 @@ static constexpr int OUT_PORTS_MAX = 32; int AudioOutOpen(int type, uint32_t samples_num, uint32_t freq, Format format); void AudioOutClose(int handle); +bool AudioOutHasDevice(int handle); uint32_t AudioOutOutputs(const OutputParam* params, uint32_t num, bool blocking = true); } // namespace Libs::Audio::AudioInternal diff --git a/src/libs/libAudio2.cpp b/src/libs/libAudio2.cpp index 054859f..c0133d4 100644 --- a/src/libs/libAudio2.cpp +++ b/src/libs/libAudio2.cpp @@ -336,6 +336,17 @@ static AudioOut2PortStateEntry* audioout2_find_port_locked(AudioOut2PortHandle p return nullptr; } +static bool audioout2_context_has_queueable_device(AudioOut2ContextHandle ctx) { + Common::LockGuard lock(g_audioout2_port_mutex); + for (const auto& state: g_audioout2_ports) { + if (state.used && state.context == ctx && state.audio_handle > 0 && + state.pcm_data != nullptr && AudioInternal::AudioOutHasDevice(state.audio_handle)) { + return true; + } + } + return false; +} + static void audioout2_queue_context_audio(AudioOut2ContextHandle ctx, bool blocking) { std::vector params; params.reserve(AudioInternal::OUT_PORTS_MAX); @@ -493,15 +504,23 @@ int KYTY_SYSV_ABI AudioOut2ContextPush(AudioOut2ContextHandle ctx, uint32_t bloc uint32_t sleep_micros = audioout2_grain_micros(512); for (;;) { + // Only a synchronous submission carrying PCM to a real device can rely on the SDL queue for + // pacing. Async pushes must retain queue-depth backpressure, and a handle without PCM (or a + // vibration/failed-open handle) has no downstream operation that can block this call. + const bool use_device_clock = + blocking != 0 && audioout2_context_has_queueable_device(ctx); + g_audioout2_context_mutex.Lock(); if (auto* state = audioout2_find_context_locked(ctx); state != nullptr) { audioout2_update_context_locked(state); sleep_micros = audioout2_grain_micros(state->num_grains); - if (state->queued < state->queue_depth) { + if (state->queued < state->queue_depth || use_device_clock) { if (state->queued == 0) { state->last_update = LibKernel::KernelGetProcessTime(); } - state->queued++; + if (state->queued < state->queue_depth) { + state->queued++; + } g_audioout2_context_mutex.Unlock(); audioout2_queue_context_audio(ctx, blocking != 0); return OK; diff --git a/tests/AudioOut2PortTests.cpp b/tests/AudioOut2PortTests.cpp index 67baee8..85deba4 100644 --- a/tests/AudioOut2PortTests.cpp +++ b/tests/AudioOut2PortTests.cpp @@ -19,6 +19,8 @@ namespace AudioOut2 = Libs::Audio::AudioOut2; std::mutex g_device_mutex; std::condition_variable g_device_cv; std::vector g_live_devices; +std::vector g_device_backed_handles; +std::vector g_output_blocking; int g_next_device = 1; int g_open_waiters = 0; bool g_block_opens = false; @@ -61,6 +63,17 @@ struct PortState { uint64_t reserved[6]; }; +struct Attribute { + uint32_t attribute_id; + int32_t reserved; + const void* value; + size_t value_size; +}; + +struct Pcm { + const void* data; +}; + const auto* AsParam(const PortParam* param) { return reinterpret_cast(param); } @@ -73,6 +86,10 @@ auto* AsState(PortState* state) { return reinterpret_cast(state); } +const auto* AsAttribute(const Attribute* attribute) { + return reinterpret_cast(attribute); +} + PortParam MakeParam(uint32_t data_format = 0x200) { PortParam param {}; param.data_format = data_format; @@ -80,9 +97,9 @@ PortParam MakeParam(uint32_t data_format = 0x200) { return param; } -AudioOut2::AudioOut2ContextHandle CreateContext() { +AudioOut2::AudioOut2ContextHandle CreateContext(uint32_t queue_depth = 4) { ContextParam param {}; - param.queue_depth = 4; + param.queue_depth = queue_depth; param.num_grains = 512; AudioOut2::AudioOut2ContextHandle context = 0; Check(AudioOut2::AudioOut2ContextCreate(AsParam(¶m), nullptr, 0, &context) == OK, @@ -112,6 +129,23 @@ int LiveDeviceCount() { return static_cast(g_live_devices.size()); } +void SetPcm(AudioOut2::AudioOut2PortHandle port, const void* data) { + const Pcm pcm {data}; + const Attribute attribute {0, 0, &pcm, sizeof(pcm)}; + Check(AudioOut2::AudioOut2PortSetAttributes(port, AsAttribute(&attribute), 1) == OK, + "setting PCM failed"); +} + +void ResetOutputCalls() { + std::lock_guard lock(g_device_mutex); + g_output_blocking.clear(); +} + +std::vector OutputCalls() { + std::lock_guard lock(g_device_mutex); + return g_output_blocking; +} + void TestSlotReuse() { const auto context = CreateContext(); const auto param = MakeParam(); @@ -202,14 +236,83 @@ void TestContextDestroyCancelsPendingCreate() { Check(LiveDeviceCount() == 0, "cancelled port create leaked a device"); } +void TestSynchronousDevicePushBypassesModelledQueue() { + const auto context = CreateContext(1); + const auto param = MakeParam(); + AudioOut2::AudioOut2PortHandle port = 0; + Check(AudioOut2::AudioOut2PortCreate(context, AsParam(¶m), &port) == OK, + "device port create failed"); + + uint32_t pcm[512] {}; + SetPcm(port, pcm); + ResetOutputCalls(); + + Check(AudioOut2::AudioOut2ContextPush(context, 1) == OK, "first sync push failed"); + Check(AudioOut2::AudioOut2ContextPush(context, 1) == OK, + "device-paced sync push was blocked by modelled queue"); + const auto calls = OutputCalls(); + Check(calls.size() == 2, "sync pushes did not reach the device backend"); + Check(calls[0] && calls[1], "sync pushes lost their blocking mode"); + + AudioOut2::AudioOut2PortDestroy(port); + AudioOut2::AudioOut2ContextDestroy(context); +} + +void TestAsynchronousDevicePushKeepsQueueBounded() { + const auto context = CreateContext(1); + const auto param = MakeParam(); + AudioOut2::AudioOut2PortHandle port = 0; + Check(AudioOut2::AudioOut2PortCreate(context, AsParam(¶m), &port) == OK, + "device port create failed"); + + uint32_t pcm[512] {}; + SetPcm(port, pcm); + ResetOutputCalls(); + + Check(AudioOut2::AudioOut2ContextPush(context, 0) == OK, "first async push failed"); + Check(AudioOut2::AudioOut2ContextPush(context, 0) != OK, + "full async queue accepted another buffer"); + const auto calls = OutputCalls(); + Check(calls.size() == 1 && !calls[0], "rejected async push reached the device backend"); + + uint32_t queued = 0; + uint32_t available = 0; + Check(AudioOut2::AudioOut2ContextGetQueueLevel(context, &queued, &available) == OK, + "queue-level query failed"); + Check(queued == 1 && available == 0, "async queue level does not match accepted pushes"); + + AudioOut2::AudioOut2PortDestroy(port); + AudioOut2::AudioOut2ContextDestroy(context); +} + +void TestHandleWithoutPcmDoesNotBypassQueue() { + const auto context = CreateContext(1); + const auto param = MakeParam(); + AudioOut2::AudioOut2PortHandle port = 0; + Check(AudioOut2::AudioOut2PortCreate(context, AsParam(¶m), &port) == OK, + "device port create failed"); + ResetOutputCalls(); + + Check(AudioOut2::AudioOut2ContextPush(context, 1) == OK, "empty sync push failed"); + Check(AudioOut2::AudioOut2ContextPush(context, 0) != OK, + "handle without PCM bypassed queue backpressure"); + Check(OutputCalls().empty(), "empty push reached the device backend"); + + AudioOut2::AudioOut2PortDestroy(port); + AudioOut2::AudioOut2ContextDestroy(context); +} + } // namespace namespace Libs::Audio::AudioInternal { -int AudioOutOpen(int /*type*/, uint32_t /*samples_num*/, uint32_t /*freq*/, Format /*format*/) { +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); + if (type != 10) { + g_device_backed_handles.push_back(handle); + } g_open_waiters++; g_device_cv.notify_all(); g_device_cv.wait(lock, []() { return !g_block_opens; }); @@ -222,9 +325,22 @@ void AudioOutClose(int handle) { if (it != g_live_devices.end()) { g_live_devices.erase(it); } + const auto device_it = + std::find(g_device_backed_handles.begin(), g_device_backed_handles.end(), handle); + if (device_it != g_device_backed_handles.end()) { + g_device_backed_handles.erase(device_it); + } } -uint32_t AudioOutOutputs(const OutputParam* /*params*/, uint32_t /*num*/, bool /*blocking*/) { +bool AudioOutHasDevice(int handle) { + std::lock_guard lock(g_device_mutex); + return std::find(g_device_backed_handles.begin(), g_device_backed_handles.end(), handle) != + g_device_backed_handles.end(); +} + +uint32_t AudioOutOutputs(const OutputParam* /*params*/, uint32_t /*num*/, bool blocking) { + std::lock_guard lock(g_device_mutex); + g_output_blocking.push_back(blocking); return 0; } @@ -244,6 +360,9 @@ int main() { TestFullTableRecovers(); TestConcurrentCreates(); TestContextDestroyCancelsPendingCreate(); + TestSynchronousDevicePushBypassesModelledQueue(); + TestAsynchronousDevicePushKeepsQueueBounded(); + TestHandleWithoutPcmDoesNotBypassQueue(); std::printf("AudioOut2PortTests: all cases passed\n"); return 0; }