PipeWire: Make native data format detection more specific.

This commit is contained in:
David Reid
2026-01-16 15:08:57 +10:00
parent b29541068c
commit 72ed924fb5
+213 -84
View File
@@ -1869,6 +1869,135 @@ static struct ma_pw_metadata_events ma_gMetadataEventsPipeWire =
};
typedef struct
{
enum ma_spa_audio_format format;
ma_uint32 channels;
ma_uint32 sampleRate;
const ma_uint32* pChannelPositions;
} ma_pipewire_audio_info;
static void ma_pipewire_audio_info_parse(const struct ma_spa_pod* pParam, ma_log* pLog, ma_pipewire_audio_info* pAudioInfo)
{
struct ma_spa_pod_object* pObject;
struct ma_spa_pod_prop* pNextProp;
ma_uint8* ptr = (ma_uint8*)pParam;
ma_uint32 cursor = 0;
ma_uint32 size;
MA_PIPEWIRE_ZERO_OBJECT(pAudioInfo);
if (pParam == NULL) {
return;
}
if (pParam->type != MA_SPA_TYPE_Object || pParam->size < sizeof(struct ma_spa_pod_object_body)) {
ma_log_postf(pLog, MA_LOG_LEVEL_ERROR, "Failed to parse PipeWire format parameter (invalid pod type).");
return;
}
pObject = (struct ma_spa_pod_object*)pParam;
/* The size of the pod does not include the header which makes parsing kind of annoying for us. We'll add it here. */
size = pParam->size + sizeof(struct ma_spa_pod);
cursor += sizeof(struct ma_spa_pod);
if (pObject->body.type != MA_SPA_TYPE_OBJECT_Format) {
ma_log_postf(pLog, MA_LOG_LEVEL_ERROR, "Failed to parse PipeWire format parameter (invalid body type).");
return;
}
cursor += sizeof(struct ma_spa_pod_object_body);
/* At this point we have parsed the header part of the object, and what follows now should be a list of properties. */
while (cursor < size) {
struct ma_spa_pod* pPropValue;
pNextProp = (struct ma_spa_pod_prop*)(ptr + cursor);
if (cursor + sizeof(struct ma_spa_pod_prop) > size) {
ma_log_postf(pLog, MA_LOG_LEVEL_ERROR, "Failed to parse PipeWire format parameter (invalid size for property).");
return;
}
/* Place the cursor on the prop value, which is a spa_pod. */
cursor += 8; /* Size of the key and flags. */
pPropValue = (struct ma_spa_pod*)(ptr + cursor);
switch (pNextProp->key)
{
case MA_SPA_FORMAT_AUDIO_format:
{
pAudioInfo->format = (enum ma_spa_audio_format)ma_spa_pod_get_id_value(pPropValue);
} break;
case MA_SPA_FORMAT_AUDIO_channels:
{
pAudioInfo->channels = ma_spa_pod_get_int_value(pPropValue);
} break;
case MA_SPA_FORMAT_AUDIO_rate:
{
pAudioInfo->sampleRate = ma_spa_pod_get_int_value(pPropValue);
} break;
case MA_SPA_FORMAT_AUDIO_position:
{
/* I'm assuming we can only get an array back for this. */
if (pPropValue->type != MA_SPA_TYPE_Array) {
ma_log_postf(pLog, MA_LOG_LEVEL_ERROR, "Failed to parse PipeWire format parameter (invalid type for position property).");
return;
}
pAudioInfo->pChannelPositions = (const ma_uint32*)ma_spa_pod_array_get_values((const struct ma_spa_pod_array*)pPropValue);
} break;
}
cursor += ma_align_64(8 + pPropValue->size); /* Size of the value pod header + the size of the pod itself. */
}
}
typedef struct
{
ma_log* pLog;
ma_uint32 channels;
ma_uint32 sampleRate;
ma_bool32 done;
} ma_stream_event_param_changed_enumeration_data__pipewire;
static void ma_stream_event_param_changed_enumeration__pipewire(void* pUserData, ma_uint32 id, const struct ma_spa_pod* pParam)
{
ma_stream_event_param_changed_enumeration_data__pipewire* pData = (ma_stream_event_param_changed_enumeration_data__pipewire*)pUserData;
if (id == MA_SPA_PARAM_Format) {
ma_pipewire_audio_info audioInfo;
ma_pipewire_audio_info_parse(pParam, pData->pLog, &audioInfo);
pData->channels = audioInfo.channels;
pData->sampleRate = audioInfo.sampleRate;
pData->done = MA_TRUE;
}
}
static const struct ma_pw_stream_events ma_gStreamEventsPipeWire_Enumeration =
{
MA_PW_VERSION_STREAM_EVENTS,
NULL, /* destroy */
NULL, /* state_changed */
NULL, /* control_info */
NULL, /* io_changed */
ma_stream_event_param_changed_enumeration__pipewire,
NULL, /* add_buffer */
NULL, /* remove_buffer */
NULL, /* process */
NULL, /* drained */
NULL, /* command */
NULL, /* trigger_done */
};
static void ma_registry_event_global_add_enumeration_by_type__pipewire(ma_enumerate_devices_data_pipewire* pEnumData, ma_uint32 id, ma_uint32 permissions, const char* type, ma_uint32 version, const struct ma_spa_dict* props, ma_device_type deviceType)
{
ma_device_info deviceInfo;
@@ -1901,13 +2030,85 @@ static void ma_registry_event_global_add_enumeration_by_type__pipewire(ma_enumer
/* Name. */
ma_strncpy_s(deviceInfo.name, sizeof(deviceInfo.name), pNiceName, (size_t)-1);
/* Data Format. Just support everything for now. */
/* TODO: See if there's a reasonable way to query the true "native" format. Maybe just initialize a stream and handle the SPA_PARAM_Format parameter in param_changed()? */
/* Data Format. Create a stream so we can identify the native channels and sample rate. */
{
struct ma_pw_properties* pProperties;
struct ma_pw_stream* pStream;
pProperties = pEnumData->pContextStatePipeWire->pw_properties_new(
MA_PW_KEY_MEDIA_TYPE, "Audio",
MA_PW_KEY_MEDIA_CATEGORY, (deviceType == ma_device_type_playback) ? "Playback" : "Capture",
MA_PW_KEY_MEDIA_ROLE, "Game",
MA_PW_KEY_TARGET_OBJECT, pNodeName,
NULL);
pStream = pEnumData->pContextStatePipeWire->pw_stream_new(pEnumData->pCore, "miniaudio-enumeration", pProperties);
if (pStream != NULL) {
ma_stream_event_param_changed_enumeration_data__pipewire eventsData;
struct ma_spa_hook eventListener;
enum ma_spa_audio_format formatPA;
struct ma_spa_pod_audio_info_raw podAudioInfo;
const struct ma_spa_pod* pConnectionParameters[1];
enum ma_pw_stream_flags streamFlags;
int connectResult;
/* The way to determine the "native" channel count and sample rate is by handling the SPA_PARAM_Format event in the params_changed callback. */
MA_PIPEWIRE_ZERO_OBJECT(&eventsData);
eventsData.pLog = pEnumData->pContextStatePipeWire->pLog;
pEnumData->pContextStatePipeWire->pw_stream_add_listener(pStream, &eventListener, &ma_gStreamEventsPipeWire_Enumeration, &eventsData);
/* Now we can connect the stream. */
if (ma_is_little_endian()) {
formatPA = MA_SPA_AUDIO_FORMAT_F32_LE;
} else {
formatPA = MA_SPA_AUDIO_FORMAT_F32_BE;
}
podAudioInfo = ma_spa_pod_audio_info_raw_init(formatPA, 0, 0);
pConnectionParameters[0] = (struct ma_spa_pod*)&podAudioInfo;
streamFlags = (enum ma_pw_stream_flags)(MA_PW_STREAM_FLAG_AUTOCONNECT);
connectResult = pEnumData->pContextStatePipeWire->pw_stream_connect(pStream, (deviceType == ma_device_type_playback) ? MA_SPA_DIRECTION_OUTPUT : MA_SPA_DIRECTION_INPUT, MA_PW_ID_ANY, streamFlags, pConnectionParameters, ma_countof(pConnectionParameters));
if (connectResult >= 0) {
/* Keep looping until we've processed our channel count and sample rate. */
while (!eventsData.done) {
pEnumData->pContextStatePipeWire->pw_loop_iterate(pEnumData->pLoop, 1);
}
if (eventsData.channels != 0 && eventsData.sampleRate != 0) {
ma_device_info_add_native_data_format(&deviceInfo, ma_format_f32, eventsData.channels, eventsData.channels, eventsData.sampleRate, eventsData.sampleRate);
}
} else {
ma_log_postf(pEnumData->pContextStatePipeWire->pLog, MA_LOG_LEVEL_ERROR, "Failed to connect PipeWire stream for enumeration. Device ID: \"%s\".", pNodeName);
}
/* We're done with the stream. */
pEnumData->pContextStatePipeWire->pw_stream_destroy(pStream);
} else {
ma_log_postf(pEnumData->pContextStatePipeWire->pLog, MA_LOG_LEVEL_ERROR, "Failed to create PipeWire stream for enumeration. Device ID: \"%s\".", pNodeName);
}
}
/*
In the logic above we chose the "native" format that PipeWire would pick if no channel count or sample rate is
specified during device initialization. However, PipeWire will actually do it's own data conversion, so from the
perspective of miniaudio, it also "natively" supports any format, channel count and sample rate. I'm not sure if
it's better to include these entries of if we should just leave it set to the one format.
*/
#if 0
ma_device_info_add_native_data_format(&deviceInfo, ma_format_f32, 1, 64, ma_standard_sample_rate_min, ma_standard_sample_rate_max);
ma_device_info_add_native_data_format(&deviceInfo, ma_format_s16, 1, 64, ma_standard_sample_rate_min, ma_standard_sample_rate_max);
ma_device_info_add_native_data_format(&deviceInfo, ma_format_s32, 1, 64, ma_standard_sample_rate_min, ma_standard_sample_rate_max);
ma_device_info_add_native_data_format(&deviceInfo, ma_format_s24, 1, 64, ma_standard_sample_rate_min, ma_standard_sample_rate_max);
ma_device_info_add_native_data_format(&deviceInfo, ma_format_u8, 1, 64, ma_standard_sample_rate_min, ma_standard_sample_rate_max);
#endif
/* Just in case something went wrong in the logic above, if we never extracted a native format we'll add a fallback. */
if (deviceInfo.nativeDataFormatCount == 0) {
ma_device_info_add_native_data_format(&deviceInfo, ma_format_f32, 1, 64, ma_standard_sample_rate_min, ma_standard_sample_rate_max);
}
ma_enumerate_devices_data_pipewire_add(pEnumData, deviceType, &deviceInfo);
@@ -2118,10 +2319,7 @@ static void ma_stream_event_param_changed__pipewire(void* pUserData, ma_uint32 i
const struct ma_spa_pod* pBufferParameters[1];
ma_uint32 bytesPerFrame;
ma_uint32 iChannel;
enum ma_spa_audio_format formatPA = MA_SPA_AUDIO_FORMAT_UNKNOWN;
ma_uint32 channels = 0;
ma_uint32 sampleRate = 0;
const ma_uint32* pChannelPositionsPA = NULL;
ma_pipewire_audio_info audioInfo;
/* It's possible for PipeWire to fire this callback with pParam set to NULL. I noticed it when tearing down a stream. Why does it do this? */
if (pParam == NULL) {
@@ -2150,94 +2348,25 @@ static void ma_stream_event_param_changed__pipewire(void* pUserData, ma_uint32 i
}
#endif
{
struct ma_spa_pod_object* pObject;
struct ma_spa_pod_prop* pNextProp;
ma_uint8* ptr = (ma_uint8*)pParam;
ma_uint32 cursor = 0;
ma_uint32 size;
if (pParam->type != MA_SPA_TYPE_Object || pParam->size < sizeof(struct ma_spa_pod_object_body)) {
ma_log_postf(pContextStatePipeWire->pLog, MA_LOG_LEVEL_ERROR, "Failed to parse PipeWire format parameter (invalid pod type).");
return;
ma_pipewire_audio_info_parse(pParam, pContextStatePipeWire->pLog, &audioInfo);
}
pObject = (struct ma_spa_pod_object*)pParam;
/* The size of the pod does not include the header which makes parsing kind of annoying for us. We'll add it here. */
size = pParam->size + sizeof(struct ma_spa_pod);
cursor += sizeof(struct ma_spa_pod);
if (pObject->body.type != MA_SPA_TYPE_OBJECT_Format) {
ma_log_postf(pContextStatePipeWire->pLog, MA_LOG_LEVEL_ERROR, "Failed to parse PipeWire format parameter (invalid body type).");
return;
}
cursor += sizeof(struct ma_spa_pod_object_body);
/* At this point we have parsed the header part of the object, and what follows now should be a list of properties. */
while (cursor < size) {
struct ma_spa_pod* pPropValue;
pNextProp = (struct ma_spa_pod_prop*)(ptr + cursor);
if (cursor + sizeof(struct ma_spa_pod_prop) > size) {
ma_log_postf(pContextStatePipeWire->pLog, MA_LOG_LEVEL_ERROR, "Failed to parse PipeWire format parameter (invalid size for property).");
return;
}
/* Place the cursor on the prop value, which is a spa_pod. */
cursor += 8; /* Size of the key and flags. */
pPropValue = (struct ma_spa_pod*)(ptr + cursor);
switch (pNextProp->key)
{
case MA_SPA_FORMAT_AUDIO_format:
{
formatPA = (enum ma_spa_audio_format)ma_spa_pod_get_id_value(pPropValue);
} break;
case MA_SPA_FORMAT_AUDIO_channels:
{
channels = ma_spa_pod_get_int_value(pPropValue);
} break;
case MA_SPA_FORMAT_AUDIO_rate:
{
sampleRate = ma_spa_pod_get_int_value(pPropValue);
} break;
case MA_SPA_FORMAT_AUDIO_position:
{
/* I'm assuming we can only get an array back for this. */
if (pPropValue->type != MA_SPA_TYPE_Array) {
ma_log_postf(pContextStatePipeWire->pLog, MA_LOG_LEVEL_ERROR, "Failed to parse PipeWire format parameter (invalid type for position property).");
return;
}
pChannelPositionsPA = (const ma_uint32*)ma_spa_pod_array_get_values((const struct ma_spa_pod_array*)pPropValue);
} break;
}
cursor += ma_align_64(8 + pPropValue->size); /* Size of the value pod header + the size of the pod itself. */
}
}
/* Now that we definitely know the sample rate, we can reliably configure the size of the buffer. */
if (pStreamState->bufferSizeInFrames == 0) {
pStreamState->bufferSizeInFrames = ma_calculate_buffer_size_in_frames_from_descriptor(pStreamState->pDescriptor, sampleRate);
pStreamState->bufferSizeInFrames = ma_calculate_buffer_size_in_frames_from_descriptor(pStreamState->pDescriptor, audioInfo.sampleRate);
}
pStreamState->format = ma_format_from_pipewire(formatPA);
pStreamState->channels = channels;
pStreamState->sampleRate = sampleRate;
pStreamState->format = ma_format_from_pipewire(audioInfo.format);
pStreamState->channels = audioInfo.channels;
pStreamState->sampleRate = audioInfo.sampleRate;
/* We should always get a channel map, but just to be safe we'll check for it, and if we don't get one back we'll use a default. */
if (pChannelPositionsPA != NULL) {
for (iChannel = 0; iChannel < channels; iChannel += 1) {
pStreamState->channelMap[iChannel] = ma_channel_from_pipewire(pChannelPositionsPA[iChannel]);
if (audioInfo.pChannelPositions != NULL) {
for (iChannel = 0; iChannel < audioInfo.channels; iChannel += 1) {
pStreamState->channelMap[iChannel] = ma_channel_from_pipewire(audioInfo.pChannelPositions[iChannel]);
}
} else {
ma_channel_map_init_standard(ma_standard_channel_map_alsa, pStreamState->channelMap, ma_countof(pStreamState->channelMap), channels);
ma_channel_map_init_standard(ma_standard_channel_map_alsa, pStreamState->channelMap, ma_countof(pStreamState->channelMap), audioInfo.channels);
}