From 1f26e50e08e702f1a27cee1c613372f497a3ce20 Mon Sep 17 00:00:00 2001 From: Harsh Rai Date: Tue, 16 Jun 2026 19:21:37 +0530 Subject: [PATCH] pulse: enable MP3 compressed offload via Pulse Add PipeWire Pulse virtual sink and forwarder for offload. Route compressed Pulse streams to PAL compressed sink. Signed-off-by: Harsh Rai --- src/modules/meson.build | 2 + .../module-compress-offload-forwarder.c | 649 ++++++++++++++++++ .../modules/module-compress-offload-sink.c | 293 ++++++++ .../module-protocol-pulse/pulse-server.c | 254 ++++++- 4 files changed, 1187 insertions(+), 11 deletions(-) create mode 100644 src/modules/module-protocol-pulse/modules/module-compress-offload-forwarder.c create mode 100644 src/modules/module-protocol-pulse/modules/module-compress-offload-sink.c diff --git a/src/modules/meson.build b/src/modules/meson.build index 6bd108e95..510dbb6b3 100644 --- a/src/modules/meson.build +++ b/src/modules/meson.build @@ -336,6 +336,8 @@ pipewire_module_protocol_pulse_sources = [ 'module-protocol-pulse/modules/module-alsa-source.c', 'module-protocol-pulse/modules/module-always-sink.c', 'module-protocol-pulse/modules/module-combine-sink.c', + 'module-protocol-pulse/modules/module-compress-offload-sink.c', + 'module-protocol-pulse/modules/module-compress-offload-forwarder.c', 'module-protocol-pulse/modules/module-device-manager.c', 'module-protocol-pulse/modules/module-device-restore.c', 'module-protocol-pulse/modules/module-echo-cancel.c', diff --git a/src/modules/module-protocol-pulse/modules/module-compress-offload-forwarder.c b/src/modules/module-protocol-pulse/modules/module-compress-offload-forwarder.c new file mode 100644 index 000000000..33054e4ba --- /dev/null +++ b/src/modules/module-protocol-pulse/modules/module-compress-offload-forwarder.c @@ -0,0 +1,649 @@ +/* PipeWire */ +/* Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. */ +/* SPDX-License-Identifier: BSD-3-Clause-Clear */ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "../module.h" + +#define NAME "compress-offload-forwarder" + +#define DEFAULT_CODEC "mp3" +#define DEFAULT_RATE 48000u +#define DEFAULT_CHANNELS 2u +#define DEFAULT_BITRATE 128000u +#define DEFAULT_BLOCK_SIZE (16u * 1024u) + +static const char *const forwarder_options = + "source_sink= " + "target_sink= " + "codec= " + "rate= " + "channels= " + "bitrate= " + "block_size="; + +PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME); +#define PW_LOG_TOPIC_DEFAULT mod_topic + +struct forwarder_data { + struct pw_core *core; + struct pw_loop *loop; + struct spa_source *activate_event; + struct spa_hook core_listener; + struct pw_registry *registry; + struct spa_hook registry_listener; + + struct pw_stream *capture; + struct spa_hook capture_listener; + struct pw_stream *playback; + struct spa_hook playback_listener; + + char source_name[256]; + char monitor_name[280]; + char target_name[256]; + char codec[32]; + uint32_t rate; + uint32_t channels; + uint32_t bitrate; + uint32_t block_size; + uint32_t target_id; + uint8_t *pending_data; + uint32_t pending_size; + uint32_t pending_offset; + uint8_t capture_streaming:1; + uint8_t logged_first_buffer:1; + uint8_t logged_first_output_buffer:1; + uint8_t logged_silence_buffer:1; + uint8_t activate_pending:1; + uint8_t playback_active:1; + uint8_t playback_failed:1; +}; + +static int create_playback_stream(struct forwarder_data *d); +static void set_playback_active(struct forwarder_data *d, bool active); +static void maybe_create_playback_stream(struct forwarder_data *d, const char *reason); + +static void request_playback_activation(struct forwarder_data *d) +{ + if (d->activate_event == NULL || d->activate_pending) + return; + d->activate_pending = true; + pw_loop_signal_event(d->loop, d->activate_event); +} + +static void playback_activate_event(void *data, uint64_t count) +{ + struct forwarder_data *d = data; + + d->activate_pending = false; + maybe_create_playback_stream(d, "first-audio-buffer"); +} + +static bool buffer_is_zeroed(const void *data, uint32_t size) +{ + const uint8_t *bytes = data; + uint32_t index; + + for (index = 0; index < size; index++) { + if (bytes[index] != 0) + return false; + } + return true; +} + +static void clear_pending_data(struct forwarder_data *d) +{ + free(d->pending_data); + d->pending_data = NULL; + d->pending_size = 0; + d->pending_offset = 0; +} + +static void compact_pending_data(struct forwarder_data *d) +{ + uint32_t remaining; + + if (d->pending_offset == 0) + return; + if (d->pending_offset >= d->pending_size) { + clear_pending_data(d); + return; + } + remaining = d->pending_size - d->pending_offset; + memmove(d->pending_data, d->pending_data + d->pending_offset, remaining); + d->pending_size = remaining; + d->pending_offset = 0; +} + +static int append_pending_data(struct forwarder_data *d, const void *data, uint32_t size) +{ + uint8_t *pending_data; + + if (size == 0) + return 0; + compact_pending_data(d); + if (d->pending_size > UINT32_MAX - size) + return -EOVERFLOW; + pending_data = realloc(d->pending_data, d->pending_size + size); + if (pending_data == NULL) + return -errno; + d->pending_data = pending_data; + memcpy(d->pending_data + d->pending_size, data, size); + d->pending_size += size; + return 0; +} + +static void maybe_create_playback_stream(struct forwarder_data *d, const char *reason) +{ + int res; + + if (!d->capture_streaming) + return; + if (d->playback != NULL) { + set_playback_active(d, true); + return; + } + if (d->target_id == PW_ID_ANY) { + pw_log_debug("compress-forwarder target:%s not resolved yet, defer output create (%s)", + d->target_name, reason); + return; + } + res = create_playback_stream(d); + if (res < 0) + pw_log_error("compress-forwarder target:%s create failed (%s): %s", + d->target_name, reason, spa_strerror(res)); + else + set_playback_active(d, true); +} + +static void destroy_playback_stream(struct forwarder_data *d) +{ + clear_pending_data(d); + if (d->playback == NULL) + return; + spa_hook_remove(&d->playback_listener); + pw_stream_destroy(d->playback); + d->playback = NULL; + d->playback_active = false; + d->playback_failed = false; +} + +static const struct spa_pod *build_raw_format(struct forwarder_data *d, + struct spa_pod_builder *builder, uint32_t id) +{ + struct spa_audio_info_raw info; + + spa_zero(info); + info.format = SPA_AUDIO_FORMAT_S16_LE; + info.rate = d->rate; + info.channels = d->channels; + if (info.channels == 1) { + info.position[0] = SPA_AUDIO_CHANNEL_MONO; + } else { + info.channels = 2; + info.position[0] = SPA_AUDIO_CHANNEL_FL; + info.position[1] = SPA_AUDIO_CHANNEL_FR; + } + return spa_format_audio_raw_build(builder, id, &info); +} + +static const struct spa_pod *build_encoded_format(struct forwarder_data *d, + struct spa_pod_builder *builder, uint32_t id) +{ + struct spa_audio_info info; + + spa_zero(info); + info.media_type = SPA_MEDIA_TYPE_audio; + info.media_subtype = SPA_MEDIA_SUBTYPE_mp3; + info.info.mp3.rate = d->rate; + info.info.mp3.channels = d->channels; + info.info.mp3.channel_mode = d->channels == 1 ? + SPA_AUDIO_MP3_CHANNEL_MODE_MONO : SPA_AUDIO_MP3_CHANNEL_MODE_STEREO; + + return spa_format_audio_build(builder, id, &info); +} + +static void playback_state_changed(void *data, enum pw_stream_state old, + enum pw_stream_state state, const char *error) +{ + struct forwarder_data *d = data; + + pw_log_debug("compress-forwarder target:%s state %d -> %d%s%s", + d->target_name, old, state, error ? ": " : "", error ? error : ""); + if (state == PW_STREAM_STATE_ERROR) + d->playback_failed = true; +} + +static void set_playback_active(struct forwarder_data *d, bool active) +{ + if (d->playback == NULL || d->playback_active == active) + return; + pw_log_debug("compress-forwarder target:%s active=%d", d->target_name, active); + pw_stream_set_active(d->playback, active); + d->playback_active = active; +} + +static void capture_state_changed(void *data, enum pw_stream_state old, + enum pw_stream_state state, const char *error) +{ + struct forwarder_data *d = data; + + pw_log_debug("compress-forwarder source:%s state %d -> %d%s%s", + d->monitor_name, old, state, error ? ": " : "", error ? error : ""); + if (d->playback_failed) + destroy_playback_stream(d); + d->capture_streaming = state == PW_STREAM_STATE_STREAMING; + if (state == PW_STREAM_STATE_PAUSED || state == PW_STREAM_STATE_ERROR) + set_playback_active(d, false); +} + +static void capture_process(void *data) +{ + struct forwarder_data *d = data; + struct pw_buffer *in_buf, *out_buf; + struct spa_data *in_data, *out_data; + uint32_t in_offset, in_size, out_size, copy_size; + const void *input_data; + const void *copy_data; + uint32_t copy_available; + int res; + + in_buf = pw_stream_dequeue_buffer(d->capture); + if (in_buf == NULL) + return; + + in_data = &in_buf->buffer->datas[0]; + if (in_data->data == NULL || in_data->chunk == NULL) { + pw_stream_queue_buffer(d->capture, in_buf); + return; + } + in_offset = SPA_MIN(in_data->chunk->offset, in_data->maxsize); + in_size = SPA_MIN(in_data->chunk->size, in_data->maxsize - in_offset); + input_data = SPA_PTROFF(in_data->data, in_offset, void); + if (in_size == 0) { + pw_stream_queue_buffer(d->capture, in_buf); + return; + } + if (buffer_is_zeroed(input_data, in_size)) { + if (!d->logged_silence_buffer) { + pw_log_debug("compress-forwarder source:%s ignoring zeroed startup buffer size=%u flags=0x%x", + d->monitor_name, in_size, in_data->chunk->flags); + d->logged_silence_buffer = true; + } + pw_stream_queue_buffer(d->capture, in_buf); + return; + } + if (!d->logged_first_buffer) { + const uint8_t *bytes = SPA_PTROFF(in_data->data, in_offset, const uint8_t); + pw_log_debug("compress-forwarder source:%s first non-silence buffer size=%u offset=%u flags=0x%x", + d->monitor_name, in_size, in_offset, in_data->chunk->flags); + pw_log_debug("compress-forwarder source:%s first non-silence bytes=%02x %02x %02x %02x %02x %02x %02x %02x", + d->monitor_name, + in_size > 0 ? bytes[0] : 0, + in_size > 1 ? bytes[1] : 0, + in_size > 2 ? bytes[2] : 0, + in_size > 3 ? bytes[3] : 0, + in_size > 4 ? bytes[4] : 0, + in_size > 5 ? bytes[5] : 0, + in_size > 6 ? bytes[6] : 0, + in_size > 7 ? bytes[7] : 0); + d->logged_first_buffer = true; + } + + if (d->playback_failed) + destroy_playback_stream(d); + + res = append_pending_data(d, input_data, in_size); + if (res < 0) { + pw_log_error("compress-forwarder source:%s failed to preserve input before target write: %s", + d->monitor_name, spa_strerror(res)); + pw_stream_queue_buffer(d->capture, in_buf); + return; + } + + if (d->playback == NULL) { + request_playback_activation(d); + if (d->playback == NULL) { + pw_log_debug("compress-forwarder source:%s preserved %u bytes pending target create total=%u", + d->monitor_name, in_size, d->pending_size - d->pending_offset); + pw_stream_queue_buffer(d->capture, in_buf); + return; + } + } + if (!d->playback_active) + set_playback_active(d, true); + if (d->playback_failed || !d->playback_active) { + pw_log_debug("compress-forwarder source:%s preserved %u bytes pending inactive target total=%u", + d->monitor_name, in_size, d->pending_size - d->pending_offset); + pw_stream_queue_buffer(d->capture, in_buf); + return; + } + + out_buf = pw_stream_dequeue_buffer(d->playback); + if (out_buf == NULL) { + pw_log_debug("compress-forwarder source:%s preserved %u bytes pending output buffer total=%u", + d->monitor_name, in_size, d->pending_size - d->pending_offset); + pw_stream_queue_buffer(d->capture, in_buf); + return; + } + + out_data = &out_buf->buffer->datas[0]; + if (out_data->data == NULL || out_data->chunk == NULL) { + pw_stream_queue_buffer(d->playback, out_buf); + pw_stream_queue_buffer(d->capture, in_buf); + return; + } + + out_size = out_data->maxsize; + if (out_size == 0) { + pw_stream_queue_buffer(d->playback, out_buf); + pw_stream_queue_buffer(d->capture, in_buf); + return; + } + copy_data = d->pending_data + d->pending_offset; + copy_available = d->pending_size - d->pending_offset; + copy_size = SPA_MIN(copy_available, out_size); + memcpy(out_data->data, copy_data, copy_size); + d->pending_offset += copy_size; + if (d->pending_offset >= d->pending_size) + clear_pending_data(d); + + out_data->chunk->offset = 0; + out_data->chunk->size = copy_size; + out_data->chunk->stride = 1; + out_buf->size = copy_size; + + if (!d->logged_first_output_buffer) { + const uint8_t *out_bytes = out_data->data; + pw_log_debug("compress-forwarder target:%s queue buffer copy_size=%u out_max=%u chunk=%u/%u/%d bytes=%02x %02x %02x %02x %02x %02x %02x %02x", + d->target_name, copy_size, out_data->maxsize, + out_data->chunk->offset, out_data->chunk->size, + out_data->chunk->stride, + copy_size > 0 ? out_bytes[0] : 0, + copy_size > 1 ? out_bytes[1] : 0, + copy_size > 2 ? out_bytes[2] : 0, + copy_size > 3 ? out_bytes[3] : 0, + copy_size > 4 ? out_bytes[4] : 0, + copy_size > 5 ? out_bytes[5] : 0, + copy_size > 6 ? out_bytes[6] : 0, + copy_size > 7 ? out_bytes[7] : 0); + d->logged_first_output_buffer = true; + } + + pw_stream_queue_buffer(d->playback, out_buf); + pw_stream_queue_buffer(d->capture, in_buf); +} + +static const struct pw_stream_events capture_events = { + PW_VERSION_STREAM_EVENTS, + .state_changed = capture_state_changed, + .process = capture_process, +}; + +static const struct pw_stream_events playback_events = { + PW_VERSION_STREAM_EVENTS, + .state_changed = playback_state_changed, +}; + +static void registry_global(void *data, uint32_t id, uint32_t permissions, + const char *type, uint32_t version, const struct spa_dict *props) +{ + struct forwarder_data *d = data; + const char *name; + + if (!spa_streq(type, PW_TYPE_INTERFACE_Node) || props == NULL) + return; + name = spa_dict_lookup(props, PW_KEY_NODE_NAME); + if (!spa_streq(name, d->target_name)) + return; + d->target_id = id; + pw_log_info("compress-forwarder target:%s resolved id=%u", d->target_name, id); +} + +static void registry_global_remove(void *data, uint32_t id) +{ + struct forwarder_data *d = data; + + if (id != d->target_id) + return; + pw_log_info("compress-forwarder target:%s removed id=%u", d->target_name, id); + d->target_id = PW_ID_ANY; + destroy_playback_stream(d); +} + +static const struct pw_registry_events registry_events = { + PW_VERSION_REGISTRY_EVENTS, + .global = registry_global, + .global_remove = registry_global_remove, +}; + +static int create_playback_stream(struct forwarder_data *d) +{ + struct pw_properties *props; + const struct spa_pod *params[2]; + uint8_t buffer[1024]; + struct spa_pod_builder builder; + uint32_t n_params = 0; + int res; + + props = pw_properties_new(PW_KEY_MEDIA_CLASS, "Stream/Output/Audio", + PW_KEY_NODE_NAME, "compress-offload-forwarder-output", + PW_KEY_MEDIA_TYPE, "Audio", + PW_KEY_MEDIA_CATEGORY, "Playback", + PW_KEY_MEDIA_ROLE, "Music", + PW_KEY_TARGET_OBJECT, d->target_name, + PW_KEY_NODE_AUTOCONNECT, "true", + PW_KEY_STREAM_DONT_REMIX, "true", + "node.dont-reconnect", "true", + PW_KEY_NODE_RATE, "1/48000", + "compress.offload", "true", + "codec.type", d->codec, + NULL); + if (props == NULL) + return -errno; + pw_properties_setf(props, "codec.sample_rate", "%u", d->rate); + pw_properties_setf(props, "codec.channels", "%u", d->channels); + pw_properties_setf(props, "codec.bit_rate", "%u", d->bitrate); + pw_properties_setf(props, PW_KEY_NODE_RATE, "1/%u", d->rate); + + d->playback = pw_stream_new(d->core, "compress offload forwarder output", props); + if (d->playback == NULL) + return -errno; + pw_stream_add_listener(d->playback, &d->playback_listener, &playback_events, d); + + spa_pod_builder_init(&builder, buffer, sizeof(buffer)); + params[n_params++] = build_encoded_format(d, &builder, SPA_PARAM_EnumFormat); + pw_log_debug("compress-forwarder target:%s connect target-id=%u", d->target_name, d->target_id); + res = pw_stream_connect(d->playback, PW_DIRECTION_OUTPUT, d->target_id, + PW_STREAM_FLAG_AUTOCONNECT | + PW_STREAM_FLAG_NO_CONVERT | + PW_STREAM_FLAG_MAP_BUFFERS | + PW_STREAM_FLAG_RT_PROCESS, + params, n_params); + if (res < 0) { + destroy_playback_stream(d); + return res; + } + pw_stream_set_active(d->playback, false); + d->playback_active = false; + d->playback_failed = false; + return 0; +} + +static int create_capture_stream(struct forwarder_data *d) +{ + struct pw_properties *props; + const struct spa_pod *params[2]; + uint8_t buffer[1024]; + struct spa_pod_builder builder; + uint32_t n_params = 0; + int res; + + props = pw_properties_new(PW_KEY_MEDIA_CLASS, "Stream/Input/Audio", + PW_KEY_NODE_NAME, "compress-offload-forwarder-capture", + PW_KEY_TARGET_OBJECT, d->source_name, + PW_KEY_NODE_AUTOCONNECT, "true", + PW_KEY_STREAM_CAPTURE_SINK, "true", + PW_KEY_STREAM_DONT_REMIX, "true", + "node.dont-reconnect", "true", + NULL); + if (props == NULL) + return -errno; + pw_properties_setf(props, PW_KEY_NODE_RATE, "1/%u", d->rate); + + d->capture = pw_stream_new(d->core, "compress offload forwarder capture", props); + if (d->capture == NULL) + return -errno; + pw_stream_add_listener(d->capture, &d->capture_listener, &capture_events, d); + + spa_pod_builder_init(&builder, buffer, sizeof(buffer)); + params[n_params++] = build_raw_format(d, &builder, SPA_PARAM_EnumFormat); + res = pw_stream_connect(d->capture, PW_DIRECTION_INPUT, PW_ID_ANY, + PW_STREAM_FLAG_AUTOCONNECT | + PW_STREAM_FLAG_MAP_BUFFERS | + PW_STREAM_FLAG_RT_PROCESS, + params, n_params); + if (res < 0) + return res; + pw_stream_set_active(d->capture, true); + return 0; +} + +static void core_error(void *data, uint32_t id, int seq, int res, const char *message) +{ + struct module *module = data; + + pw_log_error("compress-forwarder error id:%u seq:%d res:%d (%s): %s", + id, seq, res, spa_strerror(res), message); + if (id == PW_ID_CORE && res == -EPIPE) + module_schedule_unload(module); +} + +static const struct pw_core_events core_events = { + PW_VERSION_CORE_EVENTS, + .error = core_error, +}; + +static int module_compress_offload_forwarder_load(struct module *module) +{ + struct forwarder_data *d = module->user_data; + int res; + + PW_LOG_TOPIC_INIT(mod_topic); + d->loop = pw_context_get_main_loop(module->impl->context); + if (d->loop == NULL) + return -EINVAL; + d->activate_event = pw_loop_add_event(d->loop, playback_activate_event, d); + if (d->activate_event == NULL) + return -errno; + d->core = pw_context_connect(module->impl->context, NULL, 0); + if (d->core == NULL) + return -errno; + pw_core_add_listener(d->core, &d->core_listener, &core_events, module); + d->target_id = PW_ID_ANY; + d->registry = pw_core_get_registry(d->core, PW_VERSION_REGISTRY, 0); + if (d->registry == NULL) + return -errno; + pw_registry_add_listener(d->registry, &d->registry_listener, ®istry_events, d); + + res = create_capture_stream(d); + if (res < 0) + return res; + + pw_log_info("compress-forwarder loaded: source=%s monitor=%s target=%s codec=%s rate=%u channels=%u", + d->source_name, d->monitor_name, d->target_name, + d->codec, d->rate, d->channels); + module_emit_loaded(module, 0); + return 0; +} + +static int module_compress_offload_forwarder_unload(struct module *module) +{ + struct forwarder_data *d = module->user_data; + + if (d->capture != NULL) { + spa_hook_remove(&d->capture_listener); + pw_stream_destroy(d->capture); + d->capture = NULL; + } + destroy_playback_stream(d); + if (d->registry != NULL) { + spa_hook_remove(&d->registry_listener); + pw_proxy_destroy((struct pw_proxy*)d->registry); + d->registry = NULL; + } + if (d->core != NULL) { + spa_hook_remove(&d->core_listener); + pw_core_disconnect(d->core); + d->core = NULL; + } + if (d->activate_event != NULL) { + pw_loop_destroy_source(d->loop, d->activate_event); + d->activate_event = NULL; + } + d->loop = NULL; + return 0; +} + +static int module_compress_offload_forwarder_prepare(struct module *module) +{ + struct forwarder_data *d = module->user_data; + struct pw_properties *props = module->props; + const char *str; + + PW_LOG_TOPIC_INIT(mod_topic); + str = pw_properties_get(props, "source_sink"); + spa_scnprintf(d->source_name, sizeof(d->source_name), "%s", + str ? str : "pal_speaker_compress"); + str = pw_properties_get(props, "target_sink"); + spa_scnprintf(d->target_name, sizeof(d->target_name), "%s", + str ? str : "pal_sink_speaker_compress"); + str = pw_properties_get(props, "codec"); + spa_scnprintf(d->codec, sizeof(d->codec), "%s", str ? str : DEFAULT_CODEC); + d->rate = pw_properties_get_uint32(props, "rate", DEFAULT_RATE); + d->channels = pw_properties_get_uint32(props, "channels", DEFAULT_CHANNELS); + d->bitrate = pw_properties_get_uint32(props, "bitrate", DEFAULT_BITRATE); + d->block_size = pw_properties_get_uint32(props, "block_size", DEFAULT_BLOCK_SIZE); + spa_scnprintf(d->monitor_name, sizeof(d->monitor_name), "%s.monitor", d->source_name); + + if (!spa_streq(d->codec, "mp3")) { + pw_log_warn("compress-forwarder only supports mp3 right now, requested codec=%s", d->codec); + return -ENOTSUP; + } + if (d->channels == 0) + d->channels = DEFAULT_CHANNELS; + if (d->rate == 0) + d->rate = DEFAULT_RATE; + if (d->block_size == 0) + d->block_size = DEFAULT_BLOCK_SIZE; + + return 0; +} + +static const struct spa_dict_item module_compress_offload_forwarder_info[] = { + { PW_KEY_MODULE_AUTHOR, "Qualcomm Technologies, Inc." }, + { PW_KEY_MODULE_DESCRIPTION, "Forward Pulse compress-offload sink data to PAL compress sink" }, + { PW_KEY_MODULE_USAGE, forwarder_options }, + { PW_KEY_MODULE_VERSION, PACKAGE_VERSION }, +}; + +DEFINE_MODULE_INFO(module_compress_offload_forwarder) = { + .name = "module-compress-offload-forwarder", + .prepare = module_compress_offload_forwarder_prepare, + .load = module_compress_offload_forwarder_load, + .unload = module_compress_offload_forwarder_unload, + .properties = &SPA_DICT_INIT_ARRAY(module_compress_offload_forwarder_info), + .data_size = sizeof(struct forwarder_data), +}; diff --git a/src/modules/module-protocol-pulse/modules/module-compress-offload-sink.c b/src/modules/module-protocol-pulse/modules/module-compress-offload-sink.c new file mode 100644 index 000000000..243cc8e8b --- /dev/null +++ b/src/modules/module-protocol-pulse/modules/module-compress-offload-sink.c @@ -0,0 +1,293 @@ +/* PipeWire */ +/* Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. */ +/* SPDX-License-Identifier: BSD-3-Clause-Clear */ + +#include +#include + +#include +#include + +#include "../module.h" + +static const char *const pulse_module_options = + "sink_name= " + "sink_properties= " + "target_sink= " + "codec= " + "rate= " + "channels= " + "bitrate="; + +#define NAME "compress-offload-sink" + +PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME); +#define PW_LOG_TOPIC_DEFAULT mod_topic + +struct module_compress_offload_sink_data { + struct pw_core *core; + struct spa_hook core_listener; + + struct pw_proxy *proxy; + struct spa_hook proxy_listener; +}; + +static void module_proxy_removed(void *data) +{ + struct module *module = data; + struct module_compress_offload_sink_data *d = module->user_data; + + pw_log_debug("compress-forwarder proxy removed: proxy=%p", d->proxy); + pw_proxy_destroy(d->proxy); +} + +static void module_proxy_destroy(void *data) +{ + struct module *module = data; + struct module_compress_offload_sink_data *d = module->user_data; + + pw_log_debug("compress-forwarder proxy destroy: proxy=%p - scheduling module unload", d->proxy); + spa_hook_remove(&d->proxy_listener); + d->proxy = NULL; + module_schedule_unload(module); +} + +static void module_proxy_bound_props(void *data, uint32_t global_id, const struct spa_dict *props) +{ + struct module *module = data; + struct module_compress_offload_sink_data *d = module->user_data; + + pw_log_debug("proxy %p bound id:%u module_index=%u", d->proxy, global_id, module->index); + pw_log_debug("compress-forwarder node successfully created and bound, emitting loaded"); + module_emit_loaded(module, 0); +} + +static void module_proxy_error(void *data, int seq, int res, const char *message) +{ + struct module *module = data; + struct module_compress_offload_sink_data *d = module->user_data; + + pw_log_error("proxy %p error %d: %s", d->proxy, res, message); + pw_proxy_destroy(d->proxy); +} + +static const struct pw_proxy_events proxy_events = { + PW_VERSION_PROXY_EVENTS, + .removed = module_proxy_removed, + .bound_props = module_proxy_bound_props, + .error = module_proxy_error, + .destroy = module_proxy_destroy, +}; + +static void module_core_error(void *data, uint32_t id, int seq, int res, const char *message) +{ + struct module *module = data; + + pw_log_error("error id:%u seq:%d res:%d (%s): %s", + id, seq, res, spa_strerror(res), message); + + if (id == PW_ID_CORE && res == -EPIPE) + module_schedule_unload(module); +} + +static const struct pw_core_events core_events = { + PW_VERSION_CORE_EVENTS, + .error = module_core_error, +}; + +static int module_compress_offload_sink_load(struct module *module) +{ + struct module_compress_offload_sink_data *d = module->user_data; + + pw_log_info("compress-forwarder load: node=%s target=%s codec=%s rate=%s channels=%s bitrate=%s", + pw_properties_get(module->props, PW_KEY_NODE_NAME), + pw_properties_get(module->props, "compress.target.object"), + pw_properties_get(module->props, "codec.type"), + pw_properties_get(module->props, "codec.sample_rate"), + pw_properties_get(module->props, "codec.channels"), + pw_properties_get(module->props, "codec.bit_rate")); + + d->core = pw_context_connect(module->impl->context, NULL, 0); + if (d->core == NULL) { + pw_log_error("compress-forwarder core connect failed: errno=%d (%s)", + errno, spa_strerror(-errno)); + return -errno; + } + + pw_log_debug("compress-forwarder core connected: core=%p", d->core); + pw_core_add_listener(d->core, &d->core_listener, &core_events, module); + pw_properties_setf(module->props, "pulse.module.id", "%u", module->index); + + d->proxy = pw_core_create_object(d->core, + "adapter", PW_TYPE_INTERFACE_Node, PW_VERSION_NODE, + module->props ? &module->props->dict : NULL, 0); + if (d->proxy == NULL) { + pw_log_error("compress-forwarder adapter create failed: errno=%d (%s)", + errno, spa_strerror(-errno)); + return -errno; + } + + pw_log_debug("compress-forwarder adapter create requested: proxy=%p - waiting for bound_props", d->proxy); + pw_proxy_add_listener(d->proxy, &d->proxy_listener, &proxy_events, module); + return SPA_RESULT_RETURN_ASYNC(0); +} + +static int module_compress_offload_sink_unload(struct module *module) +{ + struct module_compress_offload_sink_data *d = module->user_data; + + pw_log_debug("compress-forwarder unload: proxy=%p core=%p - cleaning up", d->proxy, d->core); + if (d->proxy != NULL) { + spa_hook_remove(&d->proxy_listener); + pw_proxy_destroy(d->proxy); + d->proxy = NULL; + } + if (d->core != NULL) { + spa_hook_remove(&d->core_listener); + pw_core_disconnect(d->core); + d->core = NULL; + } + return 0; +} + +static int module_compress_offload_sink_prepare(struct module * const module) +{ + struct pw_properties * const props = module->props; + const char *str; + uint32_t rate = 44100; + uint32_t channels = 2; + uint32_t bitrate = 128000; + + PW_LOG_TOPIC_INIT(mod_topic); + + if ((str = pw_properties_get(props, "sink_name")) != NULL) { + pw_log_debug("compress-forwarder option: sink_name=%s", str); + pw_properties_set(props, PW_KEY_NODE_NAME, str); + pw_properties_set(props, "sink_name", NULL); + } else { + pw_log_debug("compress-forwarder option: sink_name missing, using default"); + pw_properties_set(props, PW_KEY_NODE_NAME, "compress-offload-sink"); + } + + if ((str = pw_properties_get(props, "sink_properties")) != NULL) { + pw_log_debug("compress-forwarder option: sink_properties=%s", str); + module_args_add_props(props, str); + pw_properties_set(props, "sink_properties", NULL); + } + + if ((str = pw_properties_get(props, "target_sink")) != NULL) { + pw_log_debug("compress-forwarder option: target_sink=%s", str); + pw_properties_set(props, "compress.target.object", str); + pw_properties_set(props, "target_sink", NULL); + } else { + pw_log_warn("compress-forwarder option: target_sink missing; stream will not have explicit compress target"); + } + + if ((str = pw_properties_get(props, "rate")) != NULL) { + uint32_t v = (uint32_t)atoi(str); + if (v > 0) + rate = v; + pw_properties_set(props, "rate", NULL); + } + if ((str = pw_properties_get(props, "channels")) != NULL) { + uint32_t v = (uint32_t)atoi(str); + if (v > 0) + channels = v; + pw_properties_set(props, "channels", NULL); + } + if ((str = pw_properties_get(props, "bitrate")) != NULL) { + uint32_t v = (uint32_t)atoi(str); + if (v > 0) + bitrate = v; + pw_properties_set(props, "bitrate", NULL); + } + if ((str = pw_properties_get(props, "channel_map")) != NULL) { + pw_properties_set(props, "audio.position", str); + pw_properties_set(props, "channel_map", NULL); + } else { + switch (channels) { + case 1: + pw_properties_set(props, "audio.position", "[ MONO ]"); + break; + case 2: + pw_properties_set(props, "audio.position", "[ FL FR ]"); + break; + case 3: + pw_properties_set(props, "audio.position", "[ FL FR LFE ]"); + break; + case 4: + pw_properties_set(props, "audio.position", "[ FL FR RL RR ]"); + break; + case 5: + pw_properties_set(props, "audio.position", "[ FL FR FC RL RR ]"); + break; + case 6: + pw_properties_set(props, "audio.position", "[ FL FR FC LFE RL RR ]"); + break; + case 8: + pw_properties_set(props, "audio.position", "[ FL FR FC LFE RL RR SL SR ]"); + break; + default: + break; + } + } + + str = pw_properties_get(props, "codec"); + pw_properties_set(props, "codec.type", str ? str : "mp3"); + pw_properties_setf(props, "codec.sample_rate", "%u", rate); + pw_properties_setf(props, "codec.channels", "%u", channels); + pw_properties_setf(props, "codec.bit_rate", "%u", bitrate); + pw_properties_set(props, "codec", NULL); + + if (pw_properties_get(props, PW_KEY_MEDIA_CLASS) == NULL) + pw_properties_set(props, PW_KEY_MEDIA_CLASS, "Audio/Sink"); + if (pw_properties_get(props, PW_KEY_NODE_DESCRIPTION) == NULL) + pw_properties_setf(props, PW_KEY_NODE_DESCRIPTION, "%s (compress offload)", + pw_properties_get(props, PW_KEY_NODE_NAME)); + + /* + * Advertise this node as an encoded-only sink so WirePlumber's + * canPassthrough() allows linking compress streams to it. + * audio.format=S16LE is kept as the synthetic PCM sample_spec that + * Pulse clients see in pactl; no PCM conversion ever occurs. + */ + pw_properties_set(props, PW_KEY_AUDIO_FORMAT, "S16LE"); + pw_properties_setf(props, PW_KEY_AUDIO_RATE, "%u", rate); + pw_properties_setf(props, PW_KEY_AUDIO_CHANNELS, "%u", channels); + pw_properties_set(props, "item.node.supports-encoded-fmts", "true"); + pw_properties_set(props, PW_KEY_NODE_VIRTUAL, "true"); + pw_properties_set(props, PW_KEY_PRIORITY_SESSION, "1"); + pw_properties_set(props, PW_KEY_PRIORITY_DRIVER, "1"); + pw_properties_set(props, "monitor.channel-volumes", "true"); + pw_properties_set(props, "monitor.passthrough", "true"); + pw_properties_set(props, "compress.offload", "true"); + pw_properties_set(props, PW_KEY_FACTORY_NAME, "support.null-audio-sink"); + + pw_log_info("prepared compress-forwarder alias: name=%s target=%s codec=%s rate=%u channels=%u bitrate=%u media.class=%s audio.format=%s virtual=%s factory=%s", + pw_properties_get(props, PW_KEY_NODE_NAME), + pw_properties_get(props, "compress.target.object"), + pw_properties_get(props, "codec.type"), + rate, channels, bitrate, + pw_properties_get(props, PW_KEY_MEDIA_CLASS), + pw_properties_get(props, PW_KEY_AUDIO_FORMAT), + pw_properties_get(props, PW_KEY_NODE_VIRTUAL), + pw_properties_get(props, PW_KEY_FACTORY_NAME)); + + return 0; +} + +static const struct spa_dict_item module_compress_offload_sink_info[] = { + { PW_KEY_MODULE_AUTHOR, "Qualcomm Technologies, Inc." }, + { PW_KEY_MODULE_DESCRIPTION, "Pulse-visible alias sink for compressed offload playback" }, + { PW_KEY_MODULE_USAGE, pulse_module_options }, + { PW_KEY_MODULE_VERSION, PACKAGE_VERSION }, +}; + +DEFINE_MODULE_INFO(module_compress_offload_sink) = { + .name = "module-compress-offload-sink", + .prepare = module_compress_offload_sink_prepare, + .load = module_compress_offload_sink_load, + .unload = module_compress_offload_sink_unload, + .properties = &SPA_DICT_INIT_ARRAY(module_compress_offload_sink_info), + .data_size = sizeof(struct module_compress_offload_sink_data), +}; diff --git a/src/modules/module-protocol-pulse/pulse-server.c b/src/modules/module-protocol-pulse/pulse-server.c index 564f64ad8..67ed24a3c 100644 --- a/src/modules/module-protocol-pulse/pulse-server.c +++ b/src/modules/module-protocol-pulse/pulse-server.c @@ -89,6 +89,9 @@ struct temporary_move_data { uint8_t used:1; }; +/* Buffer size used for compress-offload streams (frame_size == 1). */ +#define COMPRESS_OFFLOAD_BUF_SIZE 16484u + static struct sample *find_sample(struct impl *impl, uint32_t index, const char *name) { union pw_map_item *item; @@ -1232,10 +1235,18 @@ static const struct spa_pod *get_buffers_param(struct stream *s, blocks = 1; stride = s->frame_size; + /* compressed offload uses byte granularity */ + if (s->frame_size == 1) { + size = COMPRESS_OFFLOAD_BUF_SIZE; + pw_log_debug("[%s] get_buffers_param: compress offload path size=%u", s->client->name, size); + goto build_buffers_param; + } + size = defs->quantum_limit * s->frame_size; pw_log_info("[%s] stride %d size %u", s->client->name, stride, size); +build_buffers_param: param = spa_pod_builder_add_object(b, SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers, SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(MIN_BUFFERS, @@ -1261,6 +1272,23 @@ static void stream_param_changed(void *data, uint32_t id, const struct spa_pod * if (id != SPA_PARAM_Format || param == NULL) return; + /* For compress-offload streams, force byte-granularity regardless + * of negotiated format (we connect with S16LE but data is compressed). */ + { + const char *co = stream->props ? + pw_properties_get(stream->props, "compress.offload") : NULL; + if (co && pw_properties_parse_bool(co)) { + uint32_t media_type = 0, media_subtype = 0; + spa_format_parse(param, &media_type, &media_subtype); + stream->frame_size = 1; + stream->rate = stream->ss.rate ? stream->ss.rate : + pw_properties_get_uint32(stream->props, "codec.sample_rate", 44100); + pw_log_debug("[%s] compress-offload stream_param_changed: forcing frame_size=1 rate=%u subtype=%u", + stream->client->name, stream->rate, media_subtype); + goto stream_param_format_done; + } + } + if ((res = format_parse_param(param, false, &stream->ss, &stream->map, NULL, NULL)) < 0) { pw_stream_set_error(stream->stream, res, "format not supported"); return; @@ -1276,6 +1304,9 @@ static void stream_param_changed(void *data, uint32_t id, const struct spa_pod * return; } stream->rate = stream->ss.rate; +stream_param_format_done: + pw_log_debug("[%s] stream param done: frame_size=%u rate=%u ss.format=%u", + stream->client->name, stream->frame_size, stream->rate, stream->ss.format); if (stream->create_tag != SPA_ID_INVALID) { struct pw_manager_object *peer; @@ -1614,6 +1645,98 @@ static void log_format_info(struct impl *impl, enum spa_log_level level, struct impl, it->key, it->value); } +static const struct spa_pod *build_compress_offload_format(struct spa_pod_builder *b, + uint32_t id, const struct pw_properties *props, uint32_t *rate) +{ + const char *codec; + struct spa_audio_info info; + uint32_t sample_rate, channels, bitrate; + + spa_zero(info); + info.media_type = SPA_MEDIA_TYPE_audio; + codec = pw_properties_get(props, "codec.type"); + sample_rate = pw_properties_get_uint32(props, "codec.sample_rate", 44100); + channels = pw_properties_get_uint32(props, "codec.channels", 2); + bitrate = pw_properties_get_uint32(props, "codec.bit_rate", 128000); + + if (codec == NULL || spa_streq(codec, "mp3")) { + info.media_subtype = SPA_MEDIA_SUBTYPE_mp3; + info.info.mp3.rate = sample_rate; + info.info.mp3.channels = channels; + info.info.mp3.channel_mode = channels == 1 ? + SPA_AUDIO_MP3_CHANNEL_MODE_MONO : SPA_AUDIO_MP3_CHANNEL_MODE_STEREO; + } else if (spa_streq(codec, "aac") || spa_streq(codec, "aac_adts")) { + info.media_subtype = SPA_MEDIA_SUBTYPE_aac; + info.info.aac.rate = sample_rate; + info.info.aac.channels = channels; + info.info.aac.bitrate = bitrate; + info.info.aac.stream_format = SPA_AUDIO_AAC_STREAM_FORMAT_MP4ADTS; + } else if (spa_streq(codec, "aac_adif")) { + info.media_subtype = SPA_MEDIA_SUBTYPE_aac; + info.info.aac.rate = sample_rate; + info.info.aac.channels = channels; + info.info.aac.bitrate = bitrate; + info.info.aac.stream_format = SPA_AUDIO_AAC_STREAM_FORMAT_ADIF; + } else if (spa_streq(codec, "aac_latm")) { + info.media_subtype = SPA_MEDIA_SUBTYPE_aac; + info.info.aac.rate = sample_rate; + info.info.aac.channels = channels; + info.info.aac.bitrate = bitrate; + info.info.aac.stream_format = SPA_AUDIO_AAC_STREAM_FORMAT_MP4LATM; + } else if (spa_streq(codec, "flac")) { + info.media_subtype = SPA_MEDIA_SUBTYPE_flac; + info.info.flac.rate = sample_rate; + info.info.flac.channels = channels; + } else if (spa_streq(codec, "vorbis")) { + info.media_subtype = SPA_MEDIA_SUBTYPE_vorbis; + info.info.vorbis.rate = sample_rate; + info.info.vorbis.channels = channels; + } else if (spa_streq(codec, "opus")) { + info.media_subtype = SPA_MEDIA_SUBTYPE_opus; + info.info.opus.rate = sample_rate; + info.info.opus.channels = channels; + } else if (spa_streq(codec, "wma") || spa_streq(codec, "wma_std")) { + info.media_subtype = SPA_MEDIA_SUBTYPE_wma; + info.info.wma.rate = sample_rate; + info.info.wma.channels = channels; + info.info.wma.bitrate = bitrate; + info.info.wma.profile = SPA_AUDIO_WMA_PROFILE_WMA9; + } else if (spa_streq(codec, "wma_pro")) { + info.media_subtype = SPA_MEDIA_SUBTYPE_wma; + info.info.wma.rate = sample_rate; + info.info.wma.channels = channels; + info.info.wma.bitrate = bitrate; + info.info.wma.profile = SPA_AUDIO_WMA_PROFILE_WMA9_PRO; + } else { + errno = ENOTSUP; + return NULL; + } + if (rate != NULL) + *rate = sample_rate; + return spa_format_audio_build(b, id, &info); +} + +static void synthesize_compress_offload_spec(struct sample_spec *ss, + struct channel_map *map, const struct pw_properties *props) +{ + uint32_t channels; + ss->format = SPA_AUDIO_FORMAT_S16_LE; + ss->rate = pw_properties_get_uint32(props, "codec.sample_rate", 44100); + channels = pw_properties_get_uint32(props, "codec.channels", 2); + ss->channels = (uint8_t)(channels > 0 ? channels : 2); + spa_zero(*map); + switch (ss->channels) { + case 1: channel_map_parse("mono", map); break; + case 2: channel_map_parse("stereo", map); break; + case 3: channel_map_parse("surround-21", map); break; + case 4: channel_map_parse("surround-40", map); break; + case 5: channel_map_parse("surround-50", map); break; + case 6: channel_map_parse("surround-51", map); break; + case 8: channel_map_parse("surround-71", map); break; + default: channel_map_parse("stereo", map); ss->channels = 2; break; + } +} + static int do_create_playback_stream(struct client *client, uint32_t command, uint32_t tag, struct message *m) { struct impl *impl = client->impl; @@ -1639,7 +1762,8 @@ static int do_create_playback_stream(struct client *client, uint32_t command, ui muted_set = false, fail_on_suspend = false, relative_volume = false, - passthrough = false; + passthrough = false, + compress_offload = false; struct volume volume; struct pw_properties *props = NULL; uint8_t n_formats = 0; @@ -1698,6 +1822,68 @@ static int do_create_playback_stream(struct client *client, uint32_t command, ui } o = find_device(client, sink_index, sink_name, true, &is_monitor); + if (o != NULL) { + const struct pw_node_info *ninfo = o->info; + const struct spa_dict *sp = (ninfo && ninfo->props) ? ninfo->props : NULL; + char node_name_buf[256] = {0}; + const char *node_name; + const char *co; + const char *_nn = pw_properties_get(o->props, PW_KEY_NODE_NAME); + + if (_nn) + snprintf(node_name_buf, sizeof(node_name_buf), "%s", _nn); + node_name = node_name_buf[0] ? node_name_buf : NULL; + co = sp ? spa_dict_lookup(sp, "compress.offload") : NULL; + pw_log_debug("[%s] compress-offload check: node=%s ninfo=%p co=%s", + client->name, node_name ? node_name : "(null)", ninfo, co ? co : "(null)"); + if (co && spa_atob(co)) { + char target_buf[256] = {0}; + char codec_buf[64] = {0}; + char rate_buf[32] = {0}; + char channels_buf[16] = {0}; + char bitrate_buf[32] = {0}; + const char *target; + const char *codec; + const char *rate_s; + const char *channels; + const char *bitrate; + const char *_s; + + if ((_s = spa_dict_lookup(sp, "compress.target.object")) != NULL) + snprintf(target_buf, sizeof(target_buf), "%s", _s); + if ((_s = spa_dict_lookup(sp, "codec.type")) != NULL) + snprintf(codec_buf, sizeof(codec_buf), "%s", _s); + if ((_s = spa_dict_lookup(sp, "codec.sample_rate")) != NULL) + snprintf(rate_buf, sizeof(rate_buf), "%s", _s); + if ((_s = spa_dict_lookup(sp, "codec.channels")) != NULL) + snprintf(channels_buf, sizeof(channels_buf), "%s", _s); + if ((_s = spa_dict_lookup(sp, "codec.bit_rate")) != NULL) + snprintf(bitrate_buf, sizeof(bitrate_buf), "%s", _s); + + target = target_buf[0] ? target_buf : NULL; + codec = codec_buf[0] ? codec_buf : NULL; + rate_s = rate_buf[0] ? rate_buf : NULL; + channels = channels_buf[0] ? channels_buf : NULL; + bitrate = bitrate_buf[0] ? bitrate_buf : NULL; + + pw_properties_set(props, "compress.offload", "true"); + pw_properties_set(props, "item.node.supports-encoded-fmts", "true"); + pw_properties_set(props, PW_KEY_MEDIA_TYPE, "Audio"); + pw_properties_set(props, PW_KEY_MEDIA_CATEGORY, "Playback"); + pw_properties_set(props, PW_KEY_MEDIA_ROLE, "Music"); + if (codec) pw_properties_set(props, "codec.type", codec); + if (rate_s) pw_properties_set(props, "codec.sample_rate", rate_s); + if (channels) pw_properties_set(props, "codec.channels", channels); + if (bitrate) pw_properties_set(props, "codec.bit_rate", bitrate); + + pw_log_info("[%s] detected compress-offload sink=%s target=%s codec=%s", + client->name, node_name ? node_name : "(null)", + target ? target : "(null)", codec ? codec : "(null)"); + pw_log_debug("[%s] compress target=%s - leaving sink_name=%s for virtual sink routing", + client->name, target ? target : "(null)", sink_name ? sink_name : "(null)"); + } + } + spa_zero(fix_ss); spa_zero(fix_map); if ((fix_format || fix_rate || fix_channels) && o != NULL) { @@ -1745,6 +1931,9 @@ static int do_create_playback_stream(struct client *client, uint32_t command, ui goto error_protocol; } + compress_offload = pw_properties_parse_bool( + pw_properties_get(props, "compress.offload")); + if (client->version >= 21) { if (message_get(m, TAG_U8, &n_formats, @@ -1762,7 +1951,9 @@ static int do_create_playback_stream(struct client *client, uint32_t command, ui TAG_INVALID) < 0) goto error_protocol; - if (n_params < MAX_FORMATS && + if (compress_offload) { + log_format_info(impl, SPA_LOG_LEVEL_DEBUG, &format); + } else if (n_params < MAX_FORMATS && (params[n_params] = format_info_build_param(&b, SPA_PARAM_EnumFormat, &format, &r)) != NULL) { n_params++; @@ -1776,7 +1967,23 @@ static int do_create_playback_stream(struct client *client, uint32_t command, ui } } } - if (sample_spec_valid(&ss)) { + if (compress_offload) { + uint32_t compress_rate = 0; + if (n_params < MAX_FORMATS && + (params[n_params] = build_compress_offload_format(&b, + SPA_PARAM_EnumFormat, props, &compress_rate)) != NULL) { + n_params++; + n_valid_formats++; + ss_rate = rate = compress_rate; + synthesize_compress_offload_spec(&ss, &map, props); + pw_log_debug("[%s] synthesized compress-offload format codec=%s rate=%u channels=%u", + client->name, pw_properties_get(props, "codec.type"), compress_rate, + pw_properties_get_uint32(props, "codec.channels", 2)); + } else { + pw_log_warn("%p: unsupported compress codec:%s", + impl, pw_properties_get(props, "codec.type")); + } + } else if (sample_spec_valid(&ss)) { struct sample_spec sfix = ss; struct channel_map mfix = map; @@ -1800,6 +2007,8 @@ static int do_create_playback_stream(struct client *client, uint32_t command, ui if (m->offset != m->length) goto error_protocol; + pw_log_debug("[%s] format check: n_valid=%u n_params=%u compress=%d passthrough=%d", + client->name, n_valid_formats, n_params, compress_offload, passthrough); if (n_valid_formats == 0) goto error_no_formats; @@ -1837,6 +2046,8 @@ static int do_create_playback_stream(struct client *client, uint32_t command, ui flags = 0; if (no_move) flags |= PW_STREAM_FLAG_DONT_RECONNECT; + if (compress_offload) + flags |= PW_STREAM_FLAG_NO_CONVERT; if (corked) flags |= PW_STREAM_FLAG_INACTIVE; @@ -1868,14 +2079,35 @@ static int do_create_playback_stream(struct client *client, uint32_t command, ui &stream->stream_listener, &stream_events, stream); - pw_stream_connect(stream->stream, - PW_DIRECTION_OUTPUT, - SPA_ID_INVALID, - flags | - PW_STREAM_FLAG_AUTOCONNECT | - PW_STREAM_FLAG_RT_PROCESS | - PW_STREAM_FLAG_MAP_BUFFERS, - params, n_params); + if (compress_offload) { + uint8_t raw_buf[256]; + struct spa_pod_builder rb = SPA_POD_BUILDER_INIT(raw_buf, sizeof(raw_buf)); + struct spa_audio_info_raw raw_info = { + .format = SPA_AUDIO_FORMAT_S16_LE, + .rate = ss.rate ? ss.rate : 44100, + .channels = ss.channels ? ss.channels : 2, + }; + const struct spa_pod *raw_params[1]; + raw_params[0] = spa_format_audio_raw_build(&rb, SPA_PARAM_EnumFormat, &raw_info); + pw_log_debug("[%s] pw_stream_connect: using raw S16LE param for compress sink", client->name); + pw_stream_connect(stream->stream, + PW_DIRECTION_OUTPUT, + SPA_ID_INVALID, + flags | + PW_STREAM_FLAG_AUTOCONNECT | + PW_STREAM_FLAG_RT_PROCESS | + PW_STREAM_FLAG_MAP_BUFFERS, + raw_params, 1); + } else { + pw_stream_connect(stream->stream, + PW_DIRECTION_OUTPUT, + SPA_ID_INVALID, + flags | + PW_STREAM_FLAG_AUTOCONNECT | + PW_STREAM_FLAG_RT_PROCESS | + PW_STREAM_FLAG_MAP_BUFFERS, + params, n_params); + } stream_update_tag_param(stream);