Capture PCM at set time when multiple AudioOut2 ports share a buffer (#826)

* capture PCM at set time when ports share a buffer

* Snapshot size limit and open device check

* Fall back to PCM pointer on stale snapshot

* audio: own AudioOut2 PCM at attribute set time

---------

Co-authored-by: nmzik <Nmzik@mail.ru>
This commit is contained in:
Emil
2026-09-26 00:19:49 +02:00
committed by GitHub
parent cda236dbb3
commit 313b60e922
2 changed files with 87 additions and 21 deletions
+22 -12
View File
@@ -188,7 +188,7 @@ struct AudioOut2PortStateEntry {
uint32_t samples_num = 512; uint32_t samples_num = 512;
AudioInternal::Format audio_format = AudioInternal::Format::Unknown; AudioInternal::Format audio_format = AudioInternal::Format::Unknown;
int audio_handle = 0; int audio_handle = 0;
const void* pcm_data = nullptr; std::vector<uint8_t> pcm_data;
}; };
struct AudioOut2SpeakerArrayState { struct AudioOut2SpeakerArrayState {
@@ -341,11 +341,17 @@ static AudioOut2PortStateEntry* audioout2_find_port_locked(AudioOut2PortHandle p
return nullptr; 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<size_t>(state.samples_num) *
audioout2_data_format_channels(state.data_format) * bytes_per_sample;
}
static bool audioout2_context_has_queueable_device(AudioOut2ContextHandle ctx) { static bool audioout2_context_has_queueable_device(AudioOut2ContextHandle ctx) {
Common::LockGuard lock(g_audioout2_port_mutex); Common::LockGuard lock(g_audioout2_port_mutex);
for (const auto& state: g_audioout2_ports) { for (const auto& state: g_audioout2_ports) {
if (state.used && state.context == ctx && state.audio_handle > 0 && 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; 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) { 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<AudioInternal::OutputParam> params; std::vector<AudioInternal::OutputParam> params;
params.reserve(AudioInternal::OUT_PORTS_MAX); 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) { for (const auto& state: g_audioout2_ports) {
if (state.used && state.context == ctx && state.audio_handle > 0 && if (state.used && state.context == ctx && state.audio_handle > 0 &&
state.pcm_data != nullptr && params.size() < AudioInternal::OUT_PORTS_MAX) { !state.pcm_data.empty() && params.size() < AudioInternal::OUT_PORTS_MAX) {
params.push_back(AudioInternal::OutputParam {state.audio_handle, state.pcm_data}); params.push_back(AudioInternal::OutputParam {state.audio_handle, state.pcm_data.data()});
} }
} }
g_audioout2_port_mutex.Unlock();
if (params.empty()) { if (!params.empty()) {
return; (void)AudioInternal::AudioOutOutputs(params.data(), static_cast<uint32_t>(params.size()),
blocking);
} }
(void)AudioInternal::AudioOutOutputs(params.data(), static_cast<uint32_t>(params.size()),
blocking);
} }
static void audioout2_close_audio_handle(int audio_handle) { static void audioout2_close_audio_handle(int audio_handle) {
@@ -679,7 +684,12 @@ int KYTY_SYSV_ABI AudioOut2PortSetAttributes(AudioOut2PortHandle port,
if (has_pcm) { if (has_pcm) {
g_audioout2_port_mutex.Lock(); g_audioout2_port_mutex.Lock();
if (auto* state = audioout2_find_port_locked(port); state != nullptr) { 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<const uint8_t*>(pcm_data);
state->pcm_data.assign(bytes, bytes + audioout2_pcm_size(*state));
} else {
state->pcm_data.clear();
}
} }
g_audioout2_port_mutex.Unlock(); g_audioout2_port_mutex.Unlock();
} }
+65 -9
View File
@@ -8,6 +8,7 @@
#include <cstdint> #include <cstdint>
#include <cstdio> #include <cstdio>
#include <cstdlib> #include <cstdlib>
#include <cstring>
#include <mutex> #include <mutex>
#include <thread> #include <thread>
#include <vector> #include <vector>
@@ -16,14 +17,16 @@ namespace {
namespace AudioOut2 = Libs::Audio::AudioOut2; namespace AudioOut2 = Libs::Audio::AudioOut2;
std::mutex g_device_mutex; std::mutex g_device_mutex;
std::condition_variable g_device_cv; std::condition_variable g_device_cv;
std::vector<int> g_live_devices; std::vector<int> g_live_devices;
std::vector<int> g_device_backed_handles; std::vector<int> g_device_backed_handles;
std::vector<bool> g_output_blocking; std::vector<bool> g_output_blocking;
int g_next_device = 1; std::vector<std::vector<uint8_t>> g_output_pcm;
int g_open_waiters = 0; size_t g_capture_bytes = 0;
bool g_block_opens = false; int g_next_device = 1;
int g_open_waiters = 0;
bool g_block_opens = false;
void Check(bool value, const char* text) { void Check(bool value, const char* text) {
if (!value) { if (!value) {
@@ -146,6 +149,17 @@ std::vector<bool> OutputCalls() {
return g_output_blocking; 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<std::vector<uint8_t>> OutputPcm() {
std::lock_guard lock(g_device_mutex);
return g_output_pcm;
}
void TestSlotReuse() { void TestSlotReuse() {
const auto context = CreateContext(); const auto context = CreateContext();
const auto param = MakeParam(); const auto param = MakeParam();
@@ -327,6 +341,41 @@ void TestHandleWithoutPcmDoesNotBypassQueue() {
AudioOut2::AudioOut2ContextDestroy(context); 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(&param), &first) == OK,
"first port create failed");
Check(AudioOut2::AudioOut2PortCreate(context, AsParam(&param), &second) == OK,
"second port create failed");
std::vector<float> scratch(512 * 2);
std::vector<float> first_pcm(scratch.size(), 0.25f);
std::vector<float> 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
namespace Libs::Audio::AudioInternal { namespace Libs::Audio::AudioInternal {
@@ -363,9 +412,15 @@ bool AudioOutHasDevice(int handle) {
g_device_backed_handles.end(); 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); std::lock_guard lock(g_device_mutex);
g_output_blocking.push_back(blocking); g_output_blocking.push_back(blocking);
if (g_capture_bytes != 0) {
for (uint32_t i = 0; i < num; i++) {
const auto* bytes = static_cast<const uint8_t*>(params[i].data);
g_output_pcm.emplace_back(bytes, bytes + g_capture_bytes);
}
}
return 0; return 0;
} }
@@ -389,6 +444,7 @@ int main() {
TestFloat12ChannelPortOutputsPcm(); TestFloat12ChannelPortOutputsPcm();
TestAsynchronousDevicePushKeepsQueueBounded(); TestAsynchronousDevicePushKeepsQueueBounded();
TestHandleWithoutPcmDoesNotBypassQueue(); TestHandleWithoutPcmDoesNotBypassQueue();
TestPcmCopiedBeforeScratchBufferReuse();
std::printf("AudioOut2PortTests: all cases passed\n"); std::printf("AudioOut2PortTests: all cases passed\n");
return 0; return 0;
} }