| Message ID | 20260703064544.2301345-1-harsh.rai@oss.qualcomm.com |
|---|---|
| State | New |
| Headers | show |
| Series | [meta-multimedia] pipewire: add MP3 compressed offload support | expand |
On Fri, Jul 3, 2026 at 2:25 AM Harsh Rai via lists.openembedded.org <harsh.rai=oss.qualcomm.com@lists.openembedded.org> wrote: > 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 > > diff --git > a/meta-multimedia/recipes-multimedia/pipewire/pipewire/0001-pipewire-compress-offload.patch > b/meta-multimedia/recipes-multimedia/pipewire/pipewire/0001-pipewire-compress-offload.patch > new file mode 100644 > index 0000000000..9a60e5127a > --- /dev/null > +++ > b/meta-multimedia/recipes-multimedia/pipewire/pipewire/0001-pipewire-compress-offload.patch > @@ -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 > Submitted tag should accompany with reference to where it was submitted, perhaps a ML link or github PR link etc. > + > +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 > + > diff --git a/meta-multimedia/recipes-multimedia/pipewire/pipewire_1.6.7.bb > b/meta-multimedia/recipes-multimedia/pipewire/pipewire_1.6.7.bb > index 50feeaf843..90815ba012 100644 > --- a/meta-multimedia/recipes-multimedia/pipewire/pipewire_1.6.7.bb > +++ b/meta-multimedia/recipes-multimedia/pipewire/pipewire_1.6.7.bb > @@ -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} > <http://gitlab.freedesktop.org/pipewire/pipewire.git;branch=$%7BBRANCH%7D;protocol=https;tag=$%7BPV%7D> > " > +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 > -- > 2.43.0 > > > -=-=-=-=-=-=-=-=-=-=-=- > Links: You receive all messages sent to this group. > View/Reply Online (#128007): > https://lists.openembedded.org/g/openembedded-devel/message/128007 > Mute This Topic: https://lists.openembedded.org/mt/120097215/1997914 > Group Owner: openembedded-devel+owner@lists.openembedded.org > Unsubscribe: https://lists.openembedded.org/g/openembedded-devel/unsub [ > raj.khem@gmail.com] > -=-=-=-=-=-=-=-=-=-=-=- > >
diff --git a/meta-multimedia/recipes-multimedia/pipewire/pipewire/0001-pipewire-compress-offload.patch b/meta-multimedia/recipes-multimedia/pipewire/pipewire/0001-pipewire-compress-offload.patch new file mode 100644 index 0000000000..9a60e5127a --- /dev/null +++ b/meta-multimedia/recipes-multimedia/pipewire/pipewire/0001-pipewire-compress-offload.patch @@ -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 + +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 + diff --git a/meta-multimedia/recipes-multimedia/pipewire/pipewire_1.6.7.bb b/meta-multimedia/recipes-multimedia/pipewire/pipewire_1.6.7.bb index 50feeaf843..90815ba012 100644 --- a/meta-multimedia/recipes-multimedia/pipewire/pipewire_1.6.7.bb +++ b/meta-multimedia/recipes-multimedia/pipewire/pipewire_1.6.7.bb @@ -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