diff --git a/src/libs/libAudio2.cpp b/src/libs/libAudio2.cpp index a738ae5a..13b74f2c 100644 --- a/src/libs/libAudio2.cpp +++ b/src/libs/libAudio2.cpp @@ -188,7 +188,7 @@ struct AudioOut2PortStateEntry { uint32_t samples_num = 512; AudioInternal::Format audio_format = AudioInternal::Format::Unknown; int audio_handle = 0; - const void* pcm_data = nullptr; + std::vector pcm_data; }; struct AudioOut2SpeakerArrayState { @@ -341,11 +341,17 @@ static AudioOut2PortStateEntry* audioout2_find_port_locked(AudioOut2PortHandle p return nullptr; } +static size_t audioout2_pcm_size(const AudioOut2PortStateEntry& state) { + const auto bytes_per_sample = (state.data_format & 0x7fu) == 1 ? sizeof(int16_t) : sizeof(float); + return static_cast(state.samples_num) * + audioout2_data_format_channels(state.data_format) * bytes_per_sample; +} + 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)) { + !state.pcm_data.empty() && AudioInternal::AudioOutHasDevice(state.audio_handle)) { return true; } } @@ -353,24 +359,23 @@ static bool audioout2_context_has_queueable_device(AudioOut2ContextHandle ctx) { } static void audioout2_queue_context_audio(AudioOut2ContextHandle ctx, bool blocking) { + // The backend consumes the buffers synchronously, but can block while pacing SDL's queue. + // Keep both PCM storage and port handles alive until it returns. std::vector params; params.reserve(AudioInternal::OUT_PORTS_MAX); - g_audioout2_port_mutex.Lock(); + 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 && params.size() < AudioInternal::OUT_PORTS_MAX) { - params.push_back(AudioInternal::OutputParam {state.audio_handle, state.pcm_data}); + !state.pcm_data.empty() && params.size() < AudioInternal::OUT_PORTS_MAX) { + params.push_back(AudioInternal::OutputParam {state.audio_handle, state.pcm_data.data()}); } } - g_audioout2_port_mutex.Unlock(); - if (params.empty()) { - return; + if (!params.empty()) { + (void)AudioInternal::AudioOutOutputs(params.data(), static_cast(params.size()), + blocking); } - - (void)AudioInternal::AudioOutOutputs(params.data(), static_cast(params.size()), - blocking); } static void audioout2_close_audio_handle(int audio_handle) { @@ -679,7 +684,12 @@ int KYTY_SYSV_ABI AudioOut2PortSetAttributes(AudioOut2PortHandle port, if (has_pcm) { g_audioout2_port_mutex.Lock(); if (auto* state = audioout2_find_port_locked(port); state != nullptr) { - state->pcm_data = pcm_data; + if (pcm_data != nullptr && state->audio_format != AudioInternal::Format::Unknown) { + const auto* bytes = static_cast(pcm_data); + state->pcm_data.assign(bytes, bytes + audioout2_pcm_size(*state)); + } else { + state->pcm_data.clear(); + } } g_audioout2_port_mutex.Unlock(); } diff --git a/tests/AudioOut2PortTests.cpp b/tests/AudioOut2PortTests.cpp index 7665b634..d6f306f2 100644 --- a/tests/AudioOut2PortTests.cpp +++ b/tests/AudioOut2PortTests.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -16,14 +17,16 @@ namespace { 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; +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; +std::vector> g_output_pcm; +size_t g_capture_bytes = 0; +int g_next_device = 1; +int g_open_waiters = 0; +bool g_block_opens = false; void Check(bool value, const char* text) { if (!value) { @@ -146,6 +149,17 @@ std::vector OutputCalls() { return g_output_blocking; } +void CaptureOutputPcm(size_t bytes) { + std::lock_guard lock(g_device_mutex); + g_capture_bytes = bytes; + g_output_pcm.clear(); +} + +std::vector> OutputPcm() { + std::lock_guard lock(g_device_mutex); + return g_output_pcm; +} + void TestSlotReuse() { const auto context = CreateContext(); const auto param = MakeParam(); @@ -327,6 +341,41 @@ void TestHandleWithoutPcmDoesNotBypassQueue() { AudioOut2::AudioOut2ContextDestroy(context); } +void TestPcmCopiedBeforeScratchBufferReuse() { + const auto context = CreateContext(); + const auto param = MakeParam(); + AudioOut2::AudioOut2PortHandle first = 0; + AudioOut2::AudioOut2PortHandle second = 0; + Check(AudioOut2::AudioOut2PortCreate(context, AsParam(¶m), &first) == OK, + "first port create failed"); + Check(AudioOut2::AudioOut2PortCreate(context, AsParam(¶m), &second) == OK, + "second port create failed"); + + std::vector scratch(512 * 2); + std::vector first_pcm(scratch.size(), 0.25f); + std::vector second_pcm(scratch.size(), -0.5f); + std::copy(first_pcm.begin(), first_pcm.end(), scratch.begin()); + SetPcm(first, scratch.data()); + std::copy(second_pcm.begin(), second_pcm.end(), scratch.begin()); + SetPcm(second, scratch.data()); + std::fill(scratch.begin(), scratch.end(), 0.0f); + + const auto pcm_bytes = scratch.size() * sizeof(float); + CaptureOutputPcm(pcm_bytes); + Check(AudioOut2::AudioOut2ContextPush(context, 1) == OK, "shared-buffer push failed"); + const auto output = OutputPcm(); + Check(output.size() == 2, "shared-buffer push did not output both ports"); + Check(std::memcmp(output[0].data(), first_pcm.data(), pcm_bytes) == 0, + "first port lost PCM when scratch buffer was reused"); + Check(std::memcmp(output[1].data(), second_pcm.data(), pcm_bytes) == 0, + "second port lost PCM when scratch buffer was reused"); + + CaptureOutputPcm(0); + AudioOut2::AudioOut2PortDestroy(first); + AudioOut2::AudioOut2PortDestroy(second); + AudioOut2::AudioOut2ContextDestroy(context); +} + } // namespace namespace Libs::Audio::AudioInternal { @@ -363,9 +412,15 @@ bool AudioOutHasDevice(int handle) { g_device_backed_handles.end(); } -uint32_t AudioOutOutputs(const OutputParam* /*params*/, uint32_t /*num*/, bool blocking) { +uint32_t AudioOutOutputs(const OutputParam* params, uint32_t num, bool blocking) { std::lock_guard lock(g_device_mutex); g_output_blocking.push_back(blocking); + if (g_capture_bytes != 0) { + for (uint32_t i = 0; i < num; i++) { + const auto* bytes = static_cast(params[i].data); + g_output_pcm.emplace_back(bytes, bytes + g_capture_bytes); + } + } return 0; } @@ -389,6 +444,7 @@ int main() { TestFloat12ChannelPortOutputsPcm(); TestAsynchronousDevicePushKeepsQueueBounded(); TestHandleWithoutPcmDoesNotBypassQueue(); + TestPcmCopiedBeforeScratchBufferReuse(); std::printf("AudioOut2PortTests: all cases passed\n"); return 0; }