new file mode 100644
@@ -0,0 +1,1343 @@
+From 345e4b033c2ff69ab7aa02a123f6041b878f7fb9 Mon Sep 17 00:00:00 2001
+From: Harsh Rai <harsh.rai@oss.qualcomm.com>
+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.
+
+Upstream-Status: Submitted [https://gitlab.freedesktop.org/pipewire/pipewire/-/merge_requests/2878]
+
+Signed-off-by: Harsh Rai <harsh.rai@oss.qualcomm.com>
+---
+ 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 59f46ae..900871d 100644
+--- a/src/modules/meson.build
++++ b/src/modules/meson.build
+@@ -324,6 +324,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 0000000..33054e4
+--- /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 <errno.h>
++#include <limits.h>
++#include <stdint.h>
++#include <stdlib.h>
++#include <string.h>
++
++#include <spa/param/audio/compressed.h>
++#include <spa/param/audio/format-utils.h>
++#include <spa/param/audio/raw.h>
++#include <spa/utils/result.h>
++#include <spa/utils/string.h>
++#include <pipewire/pipewire.h>
++
++#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=<Pulse-visible compress sink node.name> "
++ "target_sink=<PAL compress sink node.name> "
++ "codec=<codec name, default mp3> "
++ "rate=<sample rate, default 48000> "
++ "channels=<number of channels, default 2> "
++ "bitrate=<bit rate, default 128000> "
++ "block_size=<buffer size in bytes, default 16384>";
++
++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 0000000..243cc8e
+--- /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 <errno.h>
++#include <stdlib.h>
++
++#include <spa/utils/result.h>
++#include <pipewire/pipewire.h>
++
++#include "../module.h"
++
++static const char *const pulse_module_options =
++ "sink_name=<name of sink> "
++ "sink_properties=<properties for the sink> "
++ "target_sink=<node.name of the downstream compress-offload sink> "
++ "codec=<codec name, default mp3> "
++ "rate=<sample rate, default 44100> "
++ "channels=<number of channels, default 2> "
++ "bitrate=<bit rate, default 128000>";
++
++#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 eb3233b..61f0dfb 100644
+--- a/src/modules/module-protocol-pulse/pulse-server.c
++++ b/src/modules/module-protocol-pulse/pulse-server.c
+@@ -84,6 +84,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;
+@@ -1199,10 +1202,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,
+@@ -1228,6 +1239,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;
+@@ -1243,6 +1271,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;
+@@ -1583,6 +1614,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;
+@@ -1609,7 +1732,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;
+@@ -1668,6 +1792,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) {
+@@ -1715,6 +1901,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,
+@@ -1732,7 +1921,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++;
+@@ -1746,7 +1937,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;
+
+@@ -1770,6 +1977,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;
+
+@@ -1806,6 +2015,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 (sink_name != NULL) {
+ if (o != NULL)
+@@ -1833,14 +2044,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);
+
+--
+2.43.0
+
@@ -15,6 +15,7 @@ DEPENDS = "dbus"
SRCREV = "3b2cb4fb037bf6033b87d3c87ee917b2f686d309"
BRANCH = "${@oe.utils.trim_version('${PV}', 2)}"
SRC_URI = "git://gitlab.freedesktop.org/pipewire/pipewire.git;branch=${BRANCH};protocol=https;tag=${PV}"
+SRC_URI += "file://0001-pipewire-compress-offload.patch"
SRC_URI += "file://0002-spa-plugins-alsa-acp-compat.h-p-is-already-const-do-.patch"
inherit meson pkgconfig systemd gettext useradd
Add a PipeWire Pulse virtual sink and forwarder for compressed offload, enabling MP3 compressed Pulse streams to be routed to a PAL compressed sink. Signed-off-by: Harsh Rai <harsh.rai@oss.qualcomm.com> --- .../0001-pipewire-compress-offload.patch | 1343 +++++++++++++++++ .../pipewire/pipewire_1.6.7.bb | 1 + 2 files changed, 1344 insertions(+) create mode 100644 meta-multimedia/recipes-multimedia/pipewire/pipewire/0001-pipewire-compress-offload.patch