wasapi2: Add support for exclusive mode device switching

Because of APO/OS mixer bypass in exclusive mode, we should
convert samples if new device has different format.
The conversion with additional buffering is implemented in this patch

Part-of: <https://gitlab.freedesktop.org/gstreamer/gstreamer/-/merge_requests/9586>
This commit is contained in:
Seungha Yang 2025-08-20 12:02:47 +09:00 committed by GStreamer Marge Bot
parent 4257cd8546
commit f2aabd6a71
3 changed files with 653 additions and 197 deletions

View File

@ -64,7 +64,6 @@
#include <memory>
#include <atomic>
#include <vector>
#include <endpointvolume.h>
#include <mutex>
#include <condition_variable>
#include <wrl.h>
@ -106,6 +105,9 @@ struct RbufCtx
CoTaskMemFree (mix_format);
gst_clear_caps (&caps);
if (conv)
gst_audio_converter_free (conv);
CloseHandle (capture_event);
CloseHandle (render_event);
}
@ -196,6 +198,13 @@ struct RbufCtx
std::vector<guint8> exclusive_staging;
size_t exclusive_staging_filled = 0;
size_t exclusive_period_bytes = 0;
GstAudioInfo device_info;
GstAudioInfo host_info;
std::vector<guint8> device_fifo;
std::vector<guint8> host_fifo;
size_t device_fifo_bytes = 0;
size_t host_fifo_bytes = 0;
GstAudioConverter *conv = nullptr;
UINT32 period = 0;
UINT32 client_buf_size = 0;
@ -563,6 +572,8 @@ initialize_audio_client_exclusive (IMMDevice * device,
return E_FAIL;
}
GST_DEBUG ("Configured exclusive mode period: %d frames", buffer_size);
if (period)
*period = buffer_size;
@ -804,118 +815,202 @@ gst_wasapi2_device_manager_create_ctx (IMMDeviceEnumerator * enumerator,
}
}
DWORD stream_flags = 0;
if (!desc->mix_format) {
if (desc->exclusive) {
g_assert (exclusive_formats->len > 0);
if (desc->exclusive) {
bool need_format_conv = false;
if (!desc->mix_format) {
auto format = (WAVEFORMATEX *) g_ptr_array_index (exclusive_formats, 0);
ctx->mix_format = gst_wasapi2_copy_wfx (format);
} else {
ctx->client->GetMixFormat (&ctx->mix_format);
if (!ctx->mix_format && gst_wasapi2_is_process_loopback_class (endpoint_class)) {
ctx->mix_format = gst_wasapi2_get_default_mix_format ();
}
}
if (!ctx->mix_format) {
GST_ERROR ("Couldn't get mix format");
return;
}
} else {
/* TODO: support exclusive mode device swich */
/* Check format support */
WAVEFORMATEX *closest = nullptr;
hr = ctx->client->IsFormatSupported (AUDCLNT_SHAREMODE_SHARED,
desc->mix_format, &closest);
if (hr == S_OK) {
ctx->mix_format = gst_wasapi2_copy_wfx (desc->mix_format);
/* format supported */
} else if (hr == S_FALSE) {
if (!closest) {
GST_ERROR ("Couldn't get closest format");
return;
}
GstCaps *old_caps = nullptr;
GstCaps *new_caps = nullptr;
gst_wasapi2_util_parse_waveformatex (desc->mix_format,
&old_caps, nullptr);
gst_wasapi2_util_parse_waveformatex (closest,
&new_caps, nullptr);
if (!new_caps || !old_caps) {
GST_ERROR ("Couldn't get caps from format");
gst_clear_caps (&new_caps);
gst_clear_caps (&old_caps);
CoTaskMemFree (closest);
return;
}
if (!gst_caps_is_equal (new_caps, old_caps)) {
GST_INFO ("Closest caps is different, old: %" GST_PTR_FORMAT
", new : %" GST_PTR_FORMAT, old_caps, new_caps);
/* Hope OS mixer can convert the format */
gst_caps_unref (new_caps);
gst_caps_unref (old_caps);
CoTaskMemFree (closest);
/* Try current format */
hr = ctx->client->IsFormatSupported (AUDCLNT_SHAREMODE_EXCLUSIVE,
desc->mix_format, nullptr);
if (hr == S_OK) {
ctx->mix_format = gst_wasapi2_copy_wfx (desc->mix_format);
stream_flags = AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM |
AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY;
} else {
/* Use closest format */
gst_wasapi2_sort_wfx (exclusive_formats, desc->mix_format);
auto format = (WAVEFORMATEX *) g_ptr_array_index (exclusive_formats, 0);
GstCaps *old_caps = nullptr;
GstCaps *new_caps = nullptr;
gst_wasapi2_util_parse_waveformatex (desc->mix_format,
&old_caps, nullptr);
gst_wasapi2_util_parse_waveformatex (format,
&new_caps, nullptr);
if (!new_caps || !old_caps) {
GST_ERROR ("Couldn't get caps from format");
gst_clear_caps (&new_caps);
gst_clear_caps (&old_caps);
return;
}
if (!gst_caps_is_equal (new_caps, old_caps)) {
GST_INFO ("Closest caps is different, old: %" GST_PTR_FORMAT
", new : %" GST_PTR_FORMAT, old_caps, new_caps);
need_format_conv = true;
gst_audio_info_from_caps (&ctx->host_info, old_caps);
}
gst_caps_unref (new_caps);
gst_caps_unref (old_caps);
ctx->mix_format = closest;
ctx->mix_format = gst_wasapi2_copy_wfx (format);
}
} else {
GST_ERROR ("Format not supported");
return;
}
}
gst_wasapi2_util_parse_waveformatex (ctx->mix_format, &ctx->caps, nullptr);
gst_wasapi2_util_parse_waveformatex (ctx->mix_format, &ctx->caps, nullptr);
gst_audio_info_from_caps (&ctx->device_info, ctx->caps);
if (!need_format_conv)
ctx->host_info = ctx->device_info;
hr = E_FAIL;
if (desc->exclusive) {
hr = initialize_audio_client_exclusive (device.Get (), ctx->client,
ctx->mix_format, &ctx->period, desc);
if (FAILED (hr)) {
desc->exclusive = false;
if (!ctx->client) {
ctx->client = nullptr;
gst_wasapi2_clear_wfx (&ctx->mix_format);
gst_clear_caps (&ctx->caps);
hr = device->Activate (__uuidof (IAudioClient), CLSCTX_ALL,
nullptr, &ctx->client);
if (!gst_wasapi2_result (hr)) {
GST_WARNING ("Couldn't get IAudioClient from IMMDevice");
return;
}
} else if (need_format_conv) {
GstAudioInfo *in_info, *out_info;
if (endpoint_class == GST_WASAPI2_ENDPOINT_CLASS_CAPTURE) {
in_info = &ctx->device_info;
out_info = &ctx->host_info;
} else {
in_info = &ctx->host_info;
out_info = &ctx->device_info;
}
auto config = gst_structure_new_static_str ("converter-config",
GST_AUDIO_CONVERTER_OPT_DITHER_METHOD, GST_TYPE_AUDIO_DITHER_METHOD,
GST_AUDIO_DITHER_TPDF,
GST_AUDIO_CONVERTER_OPT_RESAMPLER_METHOD,
GST_TYPE_AUDIO_RESAMPLER_METHOD, GST_AUDIO_RESAMPLER_METHOD_KAISER,
nullptr);
gst_audio_resampler_options_set_quality (GST_AUDIO_RESAMPLER_METHOD_KAISER,
GST_AUDIO_RESAMPLER_QUALITY_DEFAULT, GST_AUDIO_INFO_RATE (in_info),
GST_AUDIO_INFO_RATE (out_info), config);
ctx->conv = gst_audio_converter_new (GST_AUDIO_CONVERTER_FLAG_NONE,
in_info, out_info, config);
if (!ctx->conv) {
GST_ERROR ("Couldn't create converter");
desc->exclusive = false;
ctx->client = nullptr;
gst_wasapi2_clear_wfx (&ctx->mix_format);
gst_clear_caps (&ctx->caps);
hr = device->Activate (__uuidof (IAudioClient), CLSCTX_ALL,
nullptr, &ctx->client);
if (!gst_wasapi2_result (hr)) {
GST_WARNING ("Couldn't get IAudioClient from IMMDevice");
return;
}
} else {
GST_INFO ("converter configured");
}
}
}
/* Try IAudioClient3 if low-latency is requested */
if (FAILED (hr) && desc->low_latency &&
!gst_wasapi2_is_loopback_class (endpoint_class) &&
!gst_wasapi2_is_process_loopback_class (endpoint_class) &&
!desc->exclusive) {
hr = initialize_audio_client3 (ctx->client.Get (), ctx->mix_format,
&ctx->period, stream_flags);
if (!desc->exclusive) {
DWORD stream_flags = 0;
if (!desc->mix_format) {
ctx->client->GetMixFormat (&ctx->mix_format);
if (!ctx->mix_format && gst_wasapi2_is_process_loopback_class (endpoint_class)) {
ctx->mix_format = gst_wasapi2_get_default_mix_format ();
}
if (!ctx->mix_format) {
GST_ERROR ("Couldn't get mix format");
return;
}
} else {
/* Check format support */
WAVEFORMATEX *closest = nullptr;
hr = ctx->client->IsFormatSupported (AUDCLNT_SHAREMODE_SHARED,
desc->mix_format, &closest);
if (hr == S_OK) {
ctx->mix_format = gst_wasapi2_copy_wfx (desc->mix_format);
/* format supported */
} else if (hr == S_FALSE) {
if (!closest) {
GST_ERROR ("Couldn't get closest format");
return;
}
GstCaps *old_caps = nullptr;
GstCaps *new_caps = nullptr;
gst_wasapi2_util_parse_waveformatex (desc->mix_format,
&old_caps, nullptr);
gst_wasapi2_util_parse_waveformatex (closest,
&new_caps, nullptr);
if (!new_caps || !old_caps) {
GST_ERROR ("Couldn't get caps from format");
gst_clear_caps (&new_caps);
gst_clear_caps (&old_caps);
CoTaskMemFree (closest);
return;
}
if (!gst_caps_is_equal (new_caps, old_caps)) {
GST_INFO ("Closest caps is different, old: %" GST_PTR_FORMAT
", new : %" GST_PTR_FORMAT, old_caps, new_caps);
/* Hope OS mixer can convert the format */
gst_caps_unref (new_caps);
gst_caps_unref (old_caps);
CoTaskMemFree (closest);
ctx->mix_format = gst_wasapi2_copy_wfx (desc->mix_format);
stream_flags = AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM |
AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY;
} else {
gst_caps_unref (new_caps);
gst_caps_unref (old_caps);
ctx->mix_format = closest;
}
} else {
GST_ERROR ("Format not supported");
return;
}
}
gst_wasapi2_util_parse_waveformatex (ctx->mix_format, &ctx->caps, nullptr);
hr = E_FAIL;
/* Try IAudioClient3 if low-latency is requested */
if (desc->low_latency &&
!gst_wasapi2_is_loopback_class (endpoint_class) &&
!gst_wasapi2_is_process_loopback_class (endpoint_class) &&
!desc->exclusive) {
hr = initialize_audio_client3 (ctx->client.Get (), ctx->mix_format,
&ctx->period, stream_flags);
}
if (FAILED (hr)) {
DWORD extra_flags = stream_flags;
if (gst_wasapi2_is_loopback_class (endpoint_class))
extra_flags = AUDCLNT_STREAMFLAGS_LOOPBACK;
hr = initialize_audio_client (ctx->client.Get (), ctx->mix_format,
&ctx->period, extra_flags, endpoint_class, desc);
}
if (FAILED (hr))
return;
}
if (FAILED (hr)) {
DWORD extra_flags = stream_flags;
if (gst_wasapi2_is_loopback_class (endpoint_class))
extra_flags = AUDCLNT_STREAMFLAGS_LOOPBACK;
hr = initialize_audio_client (ctx->client.Get (), ctx->mix_format,
&ctx->period, extra_flags, endpoint_class, desc);
}
if (FAILED (hr))
return;
if (endpoint_class == GST_WASAPI2_ENDPOINT_CLASS_RENDER) {
hr = ctx->client->SetEventHandle (ctx->render_event);
if (!gst_wasapi2_result (hr)) {
@ -1248,7 +1343,7 @@ struct GstWasapi2RbufPrivate
gboolean exclusive = FALSE;
std::shared_ptr<RbufCtx> ctx;
std::atomic<bool> monitor_device_mute;
std::atomic<bool> monitor_device_mute = { false };
GThread *thread = nullptr;
HANDLE command_handle;
GstCaps *caps = nullptr;
@ -1280,6 +1375,8 @@ struct GstWasapi2RbufPrivate
HANDLE monitor_timer = nullptr;
bool monitor_timer_armed = false;
std::vector<guint8> temp_data;
GWeakRef parent;
GstWasapi2RbufCallback invalidated_cb;
};
@ -1611,18 +1708,9 @@ gst_wasapi2_rbuf_process_read (GstWasapi2Rbuf * self)
auto rb = GST_AUDIO_RING_BUFFER_CAST (self);
auto priv = self->priv;
BYTE *data = nullptr;
UINT32 to_read = 0;
guint32 to_read_bytes;
UINT32 to_read_frames = 0;
DWORD flags = 0;
HRESULT hr;
guint64 position = 0;
GstAudioInfo *info = &rb->spec.info;
guint gap_size = 0;
guint offset = 0;
gint segment;
guint8 *readptr;
gint len;
bool is_device_muted;
UINT64 qpc_pos = 0;
GstClockTime qpc_time;
@ -1631,108 +1719,228 @@ gst_wasapi2_rbuf_process_read (GstWasapi2Rbuf * self)
return E_FAIL;
}
auto & ctx = priv->ctx;
auto client = priv->ctx->capture_client;
hr = client->GetBuffer (&data, &to_read, &flags, &position, &qpc_pos);
auto hr =
client->GetBuffer (&data, &to_read_frames, &flags, &position, &qpc_pos);
/* 100 ns unit */
qpc_time = qpc_pos * 100;
GST_LOG_OBJECT (self, "Reading %d frames offset at %" G_GUINT64_FORMAT
", expected position %" G_GUINT64_FORMAT ", qpc-time %"
GST_TIME_FORMAT "(%" G_GUINT64_FORMAT "), flags 0x%x", to_read, position,
priv->expected_position, GST_TIME_ARGS (qpc_time), qpc_pos,
GST_TIME_FORMAT "(%" G_GUINT64_FORMAT "), flags 0x%x", to_read_frames,
position, priv->expected_position, GST_TIME_ARGS (qpc_time), qpc_pos,
(guint) flags);
if (hr == AUDCLNT_S_BUFFER_EMPTY || to_read == 0) {
if (hr == AUDCLNT_S_BUFFER_EMPTY || to_read_frames == 0) {
GST_LOG_OBJECT (self, "Empty buffer");
return S_OK;
}
is_device_muted =
priv->monitor_device_mute.load (std::memory_order_acquire) &&
priv->ctx->IsEndpointMuted ();
if (!gst_wasapi2_result (hr))
return hr;
to_read_bytes = to_read * GST_AUDIO_INFO_BPF (info);
/* XXX: position might not be increased in case of process loopback */
guint gap_dev_frames = 0;
if (!gst_wasapi2_is_process_loopback_class (priv->ctx->endpoint_class)) {
/* XXX: position might not be increased in case of process loopback */
if (priv->is_first) {
priv->expected_position = position + to_read;
priv->expected_position = position + to_read_frames;
priv->is_first = false;
} else {
if (position > priv->expected_position) {
guint gap_frames;
gap_frames = (guint) (position - priv->expected_position);
GST_WARNING_OBJECT (self, "Found %u frames gap", gap_frames);
gap_size = gap_frames * GST_AUDIO_INFO_BPF (info);
gap_dev_frames = (guint) (position - priv->expected_position);
GST_WARNING_OBJECT (self, "Found %u frames gap", gap_dev_frames);
}
priv->expected_position = position + to_read;
priv->expected_position = position + to_read_frames;
}
} else if (priv->mute) {
/* volume clinet might not be available in case of process loopback */
flags |= AUDCLNT_BUFFERFLAGS_SILENT;
}
gboolean device_muted =
priv->monitor_device_mute.load (std::memory_order_acquire) &&
priv->ctx->IsEndpointMuted ();
gboolean force_silence =
((flags & AUDCLNT_BUFFERFLAGS_SILENT) == AUDCLNT_BUFFERFLAGS_SILENT) ||
device_muted;
gsize host_bpf = (gsize) GST_AUDIO_INFO_BPF (&rb->spec.info);
gsize device_bpf = (ctx->conv)
? (gsize) GST_AUDIO_INFO_BPF (&ctx->device_info)
: (gsize) GST_AUDIO_INFO_BPF (&rb->spec.info);
/* Fill gap data if any */
while (gap_size > 0) {
if (!gst_audio_ring_buffer_prepare_read (rb, &segment, &readptr, &len)) {
GST_INFO_OBJECT (self, "No segment available");
hr = client->ReleaseBuffer (to_read);
gst_wasapi2_result (hr);
return S_OK;
}
g_assert (priv->segoffset >= 0);
len -= priv->segoffset;
if (len > (gint) gap_size)
len = gap_size;
gst_audio_format_info_fill_silence (info->finfo,
readptr + priv->segoffset, len);
priv->segoffset += len;
gap_size -= len;
if (priv->segoffset == rb->spec.segsize) {
gst_audio_ring_buffer_advance (rb, 1);
priv->segoffset = 0;
}
}
while (to_read_bytes) {
if (!gst_audio_ring_buffer_prepare_read (rb, &segment, &readptr, &len)) {
GST_INFO_OBJECT (self, "No segment available");
hr = client->ReleaseBuffer (to_read);
gst_wasapi2_result (hr);
return S_OK;
}
len -= priv->segoffset;
if (len > (gint) to_read_bytes)
len = to_read_bytes;
if (((flags & AUDCLNT_BUFFERFLAGS_SILENT) == AUDCLNT_BUFFERFLAGS_SILENT) ||
is_device_muted) {
gst_audio_format_info_fill_silence (info->finfo,
readptr + priv->segoffset, len);
if (gap_dev_frames > 0) {
if (ctx->conv) {
auto gap_bytes = (gsize) gap_dev_frames * device_bpf;
auto old = ctx->device_fifo_bytes;
ctx->device_fifo.resize (old + gap_bytes);
gst_audio_format_info_fill_silence (ctx->device_info.finfo,
ctx->device_fifo.data () + old, (gint) gap_bytes);
ctx->device_fifo_bytes += gap_bytes;
} else {
memcpy (readptr + priv->segoffset, data + offset, len);
}
auto gap_bytes = (gsize) gap_dev_frames * host_bpf;
while (gap_bytes > 0) {
gint segment;
guint8 *dstptr;
gint len;
priv->segoffset += len;
offset += len;
to_read_bytes -= len;
if (!gst_audio_ring_buffer_prepare_read (rb, &segment, &dstptr, &len))
break;
if (priv->segoffset == rb->spec.segsize) {
gst_audio_ring_buffer_advance (rb, 1);
priv->segoffset = 0;
len -= priv->segoffset;
if (len <= 0)
break;
gsize to_write = MIN ((gsize) len, gap_bytes);
gst_audio_format_info_fill_silence (rb->spec.info.finfo,
dstptr + priv->segoffset, (gint) to_write);
priv->segoffset += (gint) to_write;
gap_bytes -= to_write;
if (priv->segoffset == rb->spec.segsize) {
gst_audio_ring_buffer_advance (rb, 1);
priv->segoffset = 0;
}
}
}
}
hr = client->ReleaseBuffer (to_read);
if (ctx->conv) {
/* push device data to device_fifo */
const size_t in_bytes = (size_t) to_read_frames * device_bpf;
if (in_bytes > 0) {
const size_t old = ctx->device_fifo_bytes;
ctx->device_fifo.resize (old + in_bytes);
if (force_silence) {
gst_audio_format_info_fill_silence (ctx->device_info.finfo,
ctx->device_fifo.data () + old, (gint) in_bytes);
} else {
memcpy (ctx->device_fifo.data () + old, data, in_bytes);
}
ctx->device_fifo_bytes += in_bytes;
}
/* convert device_fifo -> host_fifo */
while (ctx->device_fifo_bytes >= device_bpf) {
auto in_frames_avail = (gsize) (ctx->device_fifo_bytes / device_bpf);
auto out_frames = gst_audio_converter_get_out_frames (ctx->conv,
(gint) in_frames_avail);
if (out_frames == 0)
break;
auto out_bytes = (size_t) (out_frames * host_bpf);
priv->temp_data.resize (out_bytes);
gpointer in_planes[1] = { ctx->device_fifo.data () };
gpointer out_planes[1] = { priv->temp_data.data () };
if (!gst_audio_converter_samples (ctx->conv,
GST_AUDIO_CONVERTER_FLAG_NONE,
in_planes, (gint) in_frames_avail,
out_planes, (gint) out_frames)) {
GST_ERROR_OBJECT (self, "Couldn't convert sample");
client->ReleaseBuffer (to_read_frames);
return E_FAIL;
}
auto consumed_in = (size_t) (in_frames_avail * device_bpf);
if (consumed_in < ctx->device_fifo_bytes) {
memmove (ctx->device_fifo.data (),
ctx->device_fifo.data () + consumed_in,
ctx->device_fifo_bytes - consumed_in);
}
ctx->device_fifo_bytes -= consumed_in;
ctx->device_fifo.resize (ctx->device_fifo_bytes);
/* Push converted data to host_fifo */
if (out_bytes > 0) {
auto hold = ctx->host_fifo_bytes;
ctx->host_fifo.resize (hold + out_bytes);
memcpy (ctx->host_fifo.data () + hold, priv->temp_data.data (),
out_bytes);
ctx->host_fifo_bytes += out_bytes;
}
if (ctx->device_fifo_bytes < device_bpf)
break;
}
/* host_fifo -> ringbuffer */
while (ctx->host_fifo_bytes > 0) {
gint segment;
guint8 *dstptr;
gint len;
if (!gst_audio_ring_buffer_prepare_read (rb, &segment, &dstptr, &len))
break;
len -= priv->segoffset;
if (len <= 0)
break;
auto to_copy = MIN ((size_t) len, ctx->host_fifo_bytes);
memcpy (dstptr + priv->segoffset, ctx->host_fifo.data (), to_copy);
priv->segoffset += (gint) to_copy;
if (to_copy < ctx->host_fifo_bytes) {
memmove (ctx->host_fifo.data (),
ctx->host_fifo.data () + to_copy, ctx->host_fifo_bytes - to_copy);
}
ctx->host_fifo_bytes -= to_copy;
ctx->host_fifo.resize (ctx->host_fifo_bytes);
if (priv->segoffset == rb->spec.segsize) {
gst_audio_ring_buffer_advance (rb, 1);
priv->segoffset = 0;
}
if (to_copy == 0)
break;
}
} else {
gsize remain = (gsize) to_read_frames * device_bpf;
gsize offset = 0;
while (remain > 0) {
gint segment;
guint8 *dstptr;
gint len;
if (!gst_audio_ring_buffer_prepare_read (rb, &segment, &dstptr, &len)) {
GST_INFO_OBJECT (self, "No segment available");
break;
}
len -= priv->segoffset;
if (len <= 0)
break;
auto to_write = MIN ((gsize) len, remain);
if (force_silence) {
gst_audio_format_info_fill_silence (rb->spec.info.finfo,
dstptr + priv->segoffset, (gint) to_write);
} else {
memcpy (dstptr + priv->segoffset, data + offset, to_write);
}
priv->segoffset += (gint) to_write;
offset += to_write;
remain -= to_write;
if (priv->segoffset == rb->spec.segsize) {
gst_audio_ring_buffer_advance (rb, 1);
priv->segoffset = 0;
}
}
}
hr = client->ReleaseBuffer (to_read_frames);
gst_wasapi2_result (hr);
return S_OK;
@ -1846,31 +2054,137 @@ gst_wasapi2_rbuf_process_write_exclusive (GstWasapi2Rbuf * self)
auto client = priv->ctx->client;
auto render_client = priv->ctx->render_client;
while (ctx->exclusive_staging_filled < ctx->exclusive_staging.size ()) {
gint segment;
guint8 *readptr;
gint len;
auto period_bytes = ctx->exclusive_period_bytes;
if (!gst_audio_ring_buffer_prepare_read (rb, &segment, &readptr, &len))
break;
if (ctx->conv) {
auto host_bpf = (gsize) GST_AUDIO_INFO_BPF (&ctx->host_info);
auto device_bpf = (gsize) GST_AUDIO_INFO_BPF (&ctx->device_info);
len -= priv->segoffset;
if (len <= 0)
break;
while (ctx->exclusive_staging_filled < period_bytes) {
bool processed_any = false;
gint segment;
guint8 *readptr;
gint len;
auto remain = ctx->exclusive_period_bytes - ctx->exclusive_staging_filled;
auto to_copy = (guint) MIN ((guint) len, (guint) remain);
/* read data from ringbuffer */
if (gst_audio_ring_buffer_prepare_read (rb, &segment, &readptr, &len)) {
len -= priv->segoffset;
if (len > 0) {
auto old = ctx->host_fifo_bytes;
ctx->host_fifo.resize (old + (size_t) len);
memcpy (ctx->host_fifo.data () + old, readptr + priv->segoffset,
(size_t) len);
ctx->host_fifo_bytes += (size_t) len;
processed_any = true;
memcpy (ctx->exclusive_staging.data () + ctx->exclusive_staging_filled,
readptr + priv->segoffset, to_copy);
priv->segoffset += len;
if (priv->segoffset == rb->spec.segsize) {
gst_audio_ring_buffer_clear (rb, segment);
gst_audio_ring_buffer_advance (rb, 1);
priv->segoffset = 0;
}
}
}
priv->segoffset += to_copy;
ctx->exclusive_staging_filled += to_copy;
/* do conversion */
{
auto host_frames_avail = (gsize) (ctx->host_fifo_bytes / host_bpf);
if (host_frames_avail > 0) {
auto out_frames =
gst_audio_converter_get_out_frames (ctx->conv, host_frames_avail);
if (out_frames > 0) {
auto out_bytes = (size_t) (out_frames * device_bpf);
priv->temp_data.resize (out_bytes);
if (priv->segoffset == rb->spec.segsize) {
gst_audio_ring_buffer_clear (rb, segment);
gst_audio_ring_buffer_advance (rb, 1);
priv->segoffset = 0;
gpointer in_planes[1] = { ctx->host_fifo.data () };
gpointer out_planes[1] = { priv->temp_data.data () };
if (!gst_audio_converter_samples (ctx->conv,
GST_AUDIO_CONVERTER_FLAG_NONE,
in_planes, host_frames_avail, out_planes, out_frames)) {
GST_ERROR_OBJECT (self, "gst_audio_converter_samples() failed");
return E_FAIL;
}
auto consumed_host = (size_t) (host_frames_avail * host_bpf);
if (consumed_host < ctx->host_fifo_bytes) {
memmove (ctx->host_fifo.data (),
ctx->host_fifo.data () + consumed_host,
ctx->host_fifo_bytes - consumed_host);
}
ctx->host_fifo_bytes -= consumed_host;
ctx->host_fifo.resize (ctx->host_fifo_bytes);
auto old_dev = ctx->device_fifo_bytes;
ctx->device_fifo.resize (old_dev + out_bytes);
memcpy (ctx->device_fifo.data () + old_dev, priv->temp_data.data (),
out_bytes);
ctx->device_fifo_bytes += out_bytes;
processed_any = true;
}
}
}
/* move device fifo to staging */
if (ctx->device_fifo_bytes > 0 &&
ctx->exclusive_staging_filled < period_bytes) {
auto need = period_bytes - ctx->exclusive_staging_filled;
auto to_copy = MIN (need, ctx->device_fifo_bytes);
memcpy (ctx->exclusive_staging.data () + ctx->exclusive_staging_filled,
ctx->device_fifo.data (), to_copy);
ctx->exclusive_staging_filled += to_copy;
if (to_copy < ctx->device_fifo_bytes) {
memmove (ctx->device_fifo.data (),
ctx->device_fifo.data () + to_copy,
ctx->device_fifo_bytes - to_copy);
}
ctx->device_fifo_bytes -= to_copy;
ctx->device_fifo.resize (ctx->device_fifo_bytes);
if (to_copy > 0)
processed_any = true;
}
if (!processed_any)
break;
if (ctx->exclusive_staging_filled >= period_bytes)
break;
}
} else {
while (ctx->exclusive_staging_filled < period_bytes) {
gint segment;
guint8 *readptr;
gint len;
if (!gst_audio_ring_buffer_prepare_read (rb, &segment, &readptr, &len))
break;
len -= priv->segoffset;
if (len <= 0)
break;
auto remain = period_bytes - ctx->exclusive_staging_filled;
auto to_copy = (size_t) MIN ((gsize) len, (gsize) remain);
memcpy (ctx->exclusive_staging.data () + ctx->exclusive_staging_filled,
readptr + priv->segoffset, to_copy);
priv->segoffset += (gint) to_copy;
ctx->exclusive_staging_filled += to_copy;
if (priv->segoffset == rb->spec.segsize) {
gst_audio_ring_buffer_clear (rb, segment);
gst_audio_ring_buffer_advance (rb, 1);
priv->segoffset = 0;
}
if (ctx->exclusive_staging_filled >= period_bytes)
break;
}
}
@ -2088,6 +2402,13 @@ gst_wasapi2_rbuf_process_start (GstWasapi2Rbuf * self, gboolean reset_offset)
if (priv->ctx) {
priv->ctx->exclusive_staging_filled = 0;
priv->ctx->device_fifo_bytes = 0;
priv->ctx->host_fifo_bytes = 0;
priv->ctx->device_fifo.clear ();
priv->ctx->host_fifo.clear ();
if (priv->ctx->conv)
gst_audio_converter_reset (priv->ctx->conv);
auto hr = priv->ctx->Start ();
@ -2109,15 +2430,6 @@ gst_wasapi2_rbuf_process_start (GstWasapi2Rbuf * self, gboolean reset_offset)
return S_OK;
}
static inline void
ClearMixFormat (WAVEFORMATEX ** mix_format)
{
if (*mix_format) {
CoTaskMemFree (*mix_format);
*mix_format = nullptr;
}
}
static HRESULT
gst_wasapi2_rbuf_process_stop (GstWasapi2Rbuf * self)
{
@ -2287,7 +2599,8 @@ gst_wasapi2_rbuf_loop_thread (GstWasapi2Rbuf * self)
LONGLONG elapsed = qpc_now.QuadPart - priv->fallback_qpc_base.QuadPart;
UINT64 elapsed_100ns = elapsed * 10000000ULL / priv->qpc_freq.QuadPart;
UINT32 rate = priv->mix_format->nSamplesPerSec;
auto rb = GST_AUDIO_RING_BUFFER_CAST (self);
UINT32 rate = GST_AUDIO_INFO_RATE (&rb->spec.info);
UINT64 expected_frames = (elapsed_100ns * rate) / 10000000ULL;
UINT64 delta = expected_frames - priv->fallback_frames_processed;
@ -2406,7 +2719,7 @@ gst_wasapi2_rbuf_loop_thread (GstWasapi2Rbuf * self)
}
case CommandType::Open:
priv->configured_allow_dummy = priv->allow_dummy;
ClearMixFormat (&priv->mix_format);
gst_wasapi2_clear_wfx (&priv->mix_format);
priv->ctx = gst_wasapi2_rbuf_create_ctx (self);
if (priv->ctx) {
@ -2450,7 +2763,7 @@ gst_wasapi2_rbuf_loop_thread (GstWasapi2Rbuf * self)
cmd->hr = S_OK;
SetEvent (cmd->event_handle);
priv->opened = false;
ClearMixFormat (&priv->mix_format);
gst_wasapi2_clear_wfx (&priv->mix_format);
gst_wasapi2_rbuf_stop_fallback_timer (self);
break;
case CommandType::Acquire:
@ -2483,7 +2796,7 @@ gst_wasapi2_rbuf_loop_thread (GstWasapi2Rbuf * self)
else
priv->ctx->SetVolume (priv->volume);
ClearMixFormat (&priv->mix_format);
gst_wasapi2_clear_wfx (&priv->mix_format);
priv->mix_format = gst_wasapi2_copy_wfx (priv->ctx->mix_format);
} else {
waitables[0] = dummy_render;
@ -2539,7 +2852,7 @@ gst_wasapi2_rbuf_loop_thread (GstWasapi2Rbuf * self)
priv->ctx = nullptr;
priv->cmd_queue = { };
ClearMixFormat (&priv->mix_format);
gst_wasapi2_clear_wfx (&priv->mix_format);
CoUninitialize ();

View File

@ -31,6 +31,7 @@
#include <string.h>
#include <wrl.h>
#include <vector>
#include <math.h>
GST_DEBUG_CATEGORY_EXTERN (gst_wasapi2_debug);
#define GST_CAT_DEFAULT gst_wasapi2_debug
@ -826,3 +827,142 @@ gst_wasapi2_wfx_list_to_caps (GPtrArray * list)
return caps;
}
/* *INDENT-ON* */
struct FormatView
{
WORD channels;
DWORD sample_rate;
GUID subformat;
WORD bits_per_sample;
WORD valid_bits_per_sample;
DWORD channel_mask;
WORD format_tag;
};
static inline gboolean
is_extensible_format (const WAVEFORMATEX * wfx)
{
return wfx->wFormatTag == WAVE_FORMAT_EXTENSIBLE &&
wfx->cbSize >= (sizeof (WAVEFORMATEXTENSIBLE) - sizeof (WAVEFORMATEX));
}
static FormatView
make_view (const WAVEFORMATEX * wfx)
{
FormatView view = { };
view.channels = wfx->nChannels;
view.sample_rate = wfx->nSamplesPerSec;
view.bits_per_sample = wfx->wBitsPerSample;
view.format_tag = wfx->wFormatTag;
if (is_extensible_format (wfx)) {
auto wfe = (const WAVEFORMATEXTENSIBLE *) wfx;
view.subformat = wfe->SubFormat;
view.valid_bits_per_sample = wfe->Samples.wValidBitsPerSample;
view.channel_mask = wfe->dwChannelMask;
if (view.valid_bits_per_sample == 0)
view.valid_bits_per_sample = view.bits_per_sample;
} else {
if (wfx->wFormatTag == WAVE_FORMAT_PCM) {
view.subformat = GST_KSDATAFORMAT_SUBTYPE_PCM;
} else if (wfx->wFormatTag == WAVE_FORMAT_IEEE_FLOAT) {
view.subformat = GST_KSDATAFORMAT_SUBTYPE_IEEE_FLOAT;
}
view.valid_bits_per_sample = view.bits_per_sample;
view.channel_mask = 0;
}
return view;
}
static gint
compare_format_similarity (const FormatView * a, const FormatView * b,
const FormatView * basis)
{
gboolean a_sub_eq = IsEqualGUID (a->subformat, basis->subformat);
gboolean b_sub_eq = IsEqualGUID (b->subformat, basis->subformat);
/* Check subformat (e.g., PCM vs FLOAT) */
if (a_sub_eq != b_sub_eq)
return a_sub_eq ? -1 : 1;
/* BPS diff */
gint da_bits =
abs ((gint) a->bits_per_sample - (gint) basis->bits_per_sample);
gint db_bits =
abs ((gint) b->bits_per_sample - (gint) basis->bits_per_sample);
if (da_bits != db_bits)
return (da_bits < db_bits) ? -1 : 1;
gint a_valid = a->valid_bits_per_sample ?
a->valid_bits_per_sample : a->bits_per_sample;
gint b_valid = b->valid_bits_per_sample ?
b->valid_bits_per_sample : b->bits_per_sample;
gint basis_valid = basis->valid_bits_per_sample ?
basis->valid_bits_per_sample : basis->bits_per_sample;
gint da_valid = abs (a_valid - basis_valid);
gint db_valid = abs (b_valid - basis_valid);
if (da_valid != db_valid)
return (da_valid < db_valid) ? -1 : 1;
/* Checks sample mask */
gboolean a_mask_eq = (a->channel_mask != 0 && basis->channel_mask != 0 &&
a->channel_mask == basis->channel_mask);
gboolean b_mask_eq = (b->channel_mask != 0 && basis->channel_mask != 0 &&
b->channel_mask == basis->channel_mask);
if (a_mask_eq != b_mask_eq)
return a_mask_eq ? -1 : 1;
/* Check format tag */
gint dtag_a = abs ((gint) a->format_tag - (gint) basis->format_tag);
gint dtag_b = abs ((gint) b->format_tag - (gint) basis->format_tag);
if (dtag_a != dtag_b)
return (dtag_a < dtag_b) ? -1 : 1;
return 0;
}
static gint
compare_wfx_func (gconstpointer pa, gconstpointer pb, gpointer user_data)
{
const WAVEFORMATEX *A = (const WAVEFORMATEX *) pa;
const WAVEFORMATEX *B = (const WAVEFORMATEX *) pb;
const WAVEFORMATEX *basis_wfx = (const WAVEFORMATEX *) user_data;
FormatView a = make_view (A);
FormatView b = make_view (B);
FormatView basis = make_view (basis_wfx);
/* Prefer same channel */
gint dch_a = abs ((gint) a.channels - (gint) basis.channels);
gint dch_b = abs ((gint) b.channels - (gint) basis.channels);
if (dch_a != dch_b)
return (dch_a < dch_b) ? -1 : 1;
/* Then sample rate */
gint64 dra = (gint64) a.sample_rate - (gint64) basis.sample_rate;
gint64 drb = (gint64) b.sample_rate - (gint64) basis.sample_rate;
dra = dra >= 0 ? dra : -dra;
drb = drb >= 0 ? drb : -drb;
if (dra != drb)
return (dra < drb) ? -1 : 1;
/* format compare */
gint fcmp = compare_format_similarity (&a, &b, &basis);
if (fcmp != 0)
return fcmp;
return 0;
}
void
gst_wasapi2_sort_wfx (GPtrArray * list, WAVEFORMATEX * wfx)
{
if (!list || list->len == 0 || !wfx)
return;
g_ptr_array_sort_with_data (list, compare_wfx_func, wfx);
}

View File

@ -132,6 +132,9 @@ gboolean gst_wasapi2_get_exclusive_formats (IAudioClient * client,
GstCaps * gst_wasapi2_wfx_list_to_caps (GPtrArray * list);
void gst_wasapi2_sort_wfx (GPtrArray * list,
WAVEFORMATEX * wfx);
G_END_DECLS
#ifdef __cplusplus