summaryrefslogtreecommitdiff
path: root/src/audio-impl-pw.cpp
diff options
context:
space:
mode:
authorJonathan Bradley <jcb@pikum.xyz>2025-06-25 17:50:44 -0400
committerJonathan Bradley <jcb@pikum.xyz>2025-06-25 17:50:44 -0400
commit9e791d26560b566bb21b5cd39d9042a41f29714c (patch)
tree7e95ca82423feb1009b6916bca82142d1326a94a /src/audio-impl-pw.cpp
parent3c73b503330eb67ad9489da6941ae3b28a686780 (diff)
audio: first-pass, pipewire
Diffstat (limited to 'src/audio-impl-pw.cpp')
-rw-r--r--src/audio-impl-pw.cpp364
1 files changed, 364 insertions, 0 deletions
diff --git a/src/audio-impl-pw.cpp b/src/audio-impl-pw.cpp
new file mode 100644
index 0000000..61e27a2
--- /dev/null
+++ b/src/audio-impl-pw.cpp
@@ -0,0 +1,364 @@
+
+#include "audio-impl-pw.hpp"
+#include "audio-types.hpp"
+#include "pipewire/keys.h"
+#include "pipewire/thread-loop.h"
+#include "pk.h"
+
+// see header file
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wpedantic"
+#include "spa/utils/json.h"
+#pragma GCC diagnostic pop
+
+#define PKE_AUDIO_PW_NAME_RESERVE_LEN 256
+
+pke_audio_implementation_pipewire pke_audio_impl_pw{};
+
+int metadata_event_property(void *data, uint32_t subject, const char *key, const char *type, const char *value);
+
+static const struct pw_stream_events stream_events = {
+ .version = PW_VERSION_STREAM_EVENTS,
+ .destroy = nullptr,
+ .state_changed = nullptr,
+ .control_info = nullptr,
+ .io_changed = nullptr,
+ .param_changed = on_pipewire_stream_param_changed,
+ .add_buffer = nullptr,
+ .remove_buffer = nullptr,
+ .process = on_pipewire_process,
+ .drained = nullptr,
+ .command = nullptr,
+ .trigger_done = nullptr,
+};
+static const struct pw_registry_events registry_events = {
+ .version = PW_VERSION_REGISTRY_EVENTS,
+ .global = on_registry_event_global,
+ .global_remove = on_registry_event_global_removal,
+};
+static const struct pw_metadata_events metadata_events {
+ .version = PW_VERSION_METADATA_EVENTS,
+ .property = metadata_event_property,
+};
+
+bool pw_objects_find_pw_object_by_id(void *removed_id, void *item)
+{
+ return reinterpret_cast<pke_audio_pw_object *>(item)->id == *(uint32_t *)removed_id;
+}
+
+void pke_audio_pw_init() {
+ int i;
+
+ pke_audio_impl_pw.bkt = pk_mem_bucket_create("pipewire bucket", PK_MEM_DEFAULT_BUCKET_SIZE, PK_MEMBUCKET_FLAG_NONE);
+ pke_audio_impl_pw.loop = nullptr;
+ pke_audio_impl_pw.pod_buffer = nullptr;
+ pke_audio_impl_pw.stream = nullptr;
+ pke_audio_impl_pw.core = nullptr;
+ pke_audio_impl_pw.registry = nullptr;
+ pke_audio_impl_pw.registry_listener = {};
+ pke_audio_impl_pw.metadata = nullptr;
+ pke_audio_impl_pw.metadata_listener = {};
+ pke_audio_impl_pw.pw_objects.bkt = pke_audio_impl_pw.bkt;
+ pke_audio_impl_pw.created_objects.bkt = pke_audio_impl_pw.bkt;
+ pke_audio_impl_pw.default_sink_name = pk_new<char>(PKE_AUDIO_PW_NAME_RESERVE_LEN, pke_audio_impl_pw.bkt);
+ pke_audio_impl_pw.default_sink_id = 0;
+ pke_audio_impl_pw.is_needing_output_remapped = true;
+
+ pke_audio_impl_pw.pod_buffer = pk_new<uint8_t>(pke_audio_mstr.channel_count * PKE_AUDIO_IMPL_POD_BUFFER_LEN, pke_audio_impl_pw.bkt);
+
+ pw_init(NULL, NULL);
+
+ pke_audio_impl_pw.loop = pw_thread_loop_new("pke audio thread", NULL);
+ if (pke_audio_impl_pw.loop == NULL) {
+ fprintf(stderr, "[" __FILE__ "][pke_audio_pw_init] failed to create pw_loop");
+ return;
+ }
+
+ pw_properties *props = pw_properties_new(
+ PW_KEY_MEDIA_NAME, "pke-audio-pw",
+ PW_KEY_APP_NAME, "pke",
+ PW_KEY_MEDIA_TYPE, "Audio",
+ PW_KEY_MEDIA_CATEGORY, "Playback",
+ PW_KEY_MEDIA_ROLE, "Music",
+ nullptr);
+ assert(props != NULL);
+ pke_audio_impl_pw.stream = pw_stream_new_simple(
+ pw_thread_loop_get_loop(pke_audio_impl_pw.loop),
+ "pke-audio-pw",
+ props,
+ &stream_events,
+ NULL /* user-data */);
+ if (pke_audio_impl_pw.stream == NULL) {
+ fprintf(stderr, "[" __FILE__ "][pke_audio_pw_init] failed to create pw_stream");
+ return;
+ }
+
+ spa_pod_builder b{};
+ b.data = pke_audio_impl_pw.pod_buffer;
+ b.size = PKE_AUDIO_IMPL_POD_BUFFER_LEN * pke_audio_mstr.channel_count;
+ b._padding = 0;
+ b.callbacks.data = NULL;
+ b.callbacks.funcs = NULL;
+ b.state.flags = 0;
+ b.state.offset = 0;
+ b.state.frame = NULL;
+
+ spa_audio_info_raw spa_audio_info_raw_init{};
+ spa_audio_info_raw_init.flags = 0;
+ spa_audio_info_raw_init.format = SPA_AUDIO_FORMAT_F32;
+ spa_audio_info_raw_init.rate = 48000;
+ spa_audio_info_raw_init.channels = pke_audio_mstr.channel_count;
+ spa_audio_info_raw_init.position[0] = SPA_AUDIO_CHANNEL_FL;
+ spa_audio_info_raw_init.position[1] = SPA_AUDIO_CHANNEL_FR;
+
+ const struct spa_pod *params[1];
+ params[0] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat,
+ &spa_audio_info_raw_init);
+
+ if (i = pw_stream_connect(pke_audio_impl_pw.stream,
+ PW_DIRECTION_OUTPUT,
+ PW_ID_ANY,
+ pw_stream_flags(PW_STREAM_FLAG_MAP_BUFFERS | PW_STREAM_FLAG_RT_PROCESS),
+ params, 1), i != 0) {
+ fprintf(stderr, "[" __FILE__ "][pke_audio_pw_init] failed to connect to pw_stream");
+ return;
+ }
+
+ pke_audio_impl_pw.core = pw_stream_get_core(pke_audio_impl_pw.stream);
+ if (pke_audio_impl_pw.core == NULL) {
+ fprintf(stderr, "[" __FILE__ "][pke_audio_pw_init] failed to get core");
+ return;
+ }
+
+ pke_audio_impl_pw.registry = pw_core_get_registry(pke_audio_impl_pw.core, PW_VERSION_REGISTRY, 0);
+ if (pke_audio_impl_pw.registry == NULL) {
+ fprintf(stderr, "[" __FILE__ "][pke_audio_pw_init] failed to get registry");
+ return;
+ }
+
+ memset(&pke_audio_impl_pw.registry_listener, 0, sizeof(pke_audio_impl_pw.registry_listener));
+ pw_registry_add_listener(pke_audio_impl_pw.registry, &pke_audio_impl_pw.registry_listener, &registry_events, NULL);
+
+ pw_thread_loop_start(pke_audio_impl_pw.loop);
+ return;
+}
+
+void pke_audio_pw_teardown() {
+ uint32_t i;
+ pke_audio_mstr.mtx_buffer.lock();
+ pw_thread_loop_stop(pke_audio_impl_pw.loop);
+ for (i = pke_audio_impl_pw.created_objects.next; i > 0; --i) {
+ pw_core_destroy(pke_audio_impl_pw.core, ((void **)pke_audio_impl_pw.created_objects.data)[i - 1]);
+ }
+ for (i = pke_audio_impl_pw.pw_objects.next; i > 0; --i) {
+ pw_properties_free(pke_audio_impl_pw.pw_objects[i - 1].props);
+ }
+ if (pke_audio_impl_pw.metadata != NULL) {
+ pw_proxy_destroy((struct pw_proxy*)pke_audio_impl_pw.metadata);
+ }
+ if (pke_audio_impl_pw.registry != NULL) {
+ pw_proxy_destroy((struct pw_proxy*)pke_audio_impl_pw.registry);
+ }
+ if (pke_audio_impl_pw.core != NULL) {
+ pw_core_disconnect(pke_audio_impl_pw.core);
+ }
+ if (pke_audio_impl_pw.stream != NULL) {
+ pw_stream_destroy(pke_audio_impl_pw.stream);
+ }
+ if (pke_audio_impl_pw.loop != NULL) {
+ pw_thread_loop_destroy(pke_audio_impl_pw.loop);
+ }
+ pw_deinit();
+ pk_delete<char>(pke_audio_impl_pw.default_sink_name, PKE_AUDIO_PW_NAME_RESERVE_LEN, pke_audio_impl_pw.bkt);
+ pk_delete<uint8_t>(pke_audio_impl_pw.pod_buffer, pke_audio_mstr.channel_count * PKE_AUDIO_IMPL_POD_BUFFER_LEN, pke_audio_impl_pw.bkt);
+ pk_mem_bucket_destroy(pke_audio_impl_pw.bkt);
+ pke_audio_impl_pw.bkt = CAFE_BABE(pk_membucket);
+ pke_audio_mstr.mtx_buffer.unlock();
+}
+
+void on_pipewire_process(void *user_data) {
+ (void)user_data;
+ pw_buffer *b;
+ spa_buffer *buf;
+ int stride;
+ uint64_t n_frames;
+ float *dst;
+
+ if ((b = pw_stream_dequeue_buffer(pke_audio_impl_pw.stream)) == NULL) {
+ fprintf(stderr, "[" __FILE__ "][on_pipewire_process] out of buffers");
+ return;
+ }
+
+ buf = b->buffer;
+ if ((dst = (float*)buf->datas[0].data) == NULL) {
+ fprintf(stderr, "[" __FILE__ "][on_pipewire_process] given buffer was null");
+ return;
+ }
+
+ stride = sizeof(float) * pke_audio_mstr.channel_count;
+ n_frames = buf->datas[0].maxsize / stride;
+ if (b->requested) {
+ n_frames = PK_MIN(b->requested, n_frames);
+ }
+
+ pke_audio_mstr.mtx_buffer.lock();
+ memset(dst, 0, sizeof(float) * n_frames * pke_audio_mstr.channel_count);
+ buf->datas[0].chunk->offset = 0;
+ buf->datas[0].chunk->stride = stride;
+ buf->datas[0].chunk->size = n_frames * stride;
+ pw_stream_queue_buffer(pke_audio_impl_pw.stream, b);
+ pke_audio_mstr.mtx_buffer.unlock();
+ return;
+}
+
+void on_pipewire_stream_param_changed(void *, uint32_t id, const struct spa_pod *param) {
+ /* NULL means to clear the format */
+ if (param == NULL || id != SPA_PARAM_Format) {
+ // memset(&pke_audio_impl_pw.format, 0, sizeof(struct spa_audio_info));
+ return;
+ }
+
+ if (spa_format_parse(param, &pke_audio_impl_pw.format.media_type, &pke_audio_impl_pw.format.media_subtype) < 0) {
+ return;
+ }
+
+ /* only accept raw audio */
+ if (pke_audio_impl_pw.format.media_type != SPA_MEDIA_TYPE_audio || pke_audio_impl_pw.format.media_subtype != SPA_MEDIA_SUBTYPE_raw) {
+ return;
+ }
+
+ /* call a helper function to parse the format for us. */
+ spa_format_audio_raw_parse(param, &pke_audio_impl_pw.format.info.raw);
+
+ fprintf(stdout, "\r\ncapturing rate changed:%d channels:%d\n", pke_audio_impl_pw.format.info.raw.rate, pke_audio_impl_pw.format.info.raw.channels);
+ return;
+}
+
+int metadata_event_property(void *data, uint32_t subject, const char *key, const char *type, const char *value)
+{
+ if (subject == PW_ID_CORE) {
+ if (key == NULL || spa_streq(key, "default.audio.sink")) {
+ if (pke_audio_impl_pw.default_sink_id != 0) {
+ // the default just changed??
+ pke_audio_impl_pw.is_needing_output_remapped = true;
+ }
+ if (value == NULL || spa_json_str_object_find(value, strlen(value), "name", pke_audio_impl_pw.default_sink_name, sizeof(pke_audio_impl_pw.default_sink_name)) < 0) {
+ pke_audio_impl_pw.default_sink_name[0] = '\0';
+ }
+ }
+ }
+ fprintf(stdout, "\r\nPW_METADATA_CB: data: %p, subject: %d, key: '%s', type: '%s', value: '%s'\n", data, subject, key, type, value);
+ return 0;
+}
+
+void on_registry_event_global(void *data, uint32_t id, uint32_t permissions, const char *type, uint32_t version, const struct spa_dict *props) {
+ (void)data;
+ (void)permissions;
+ const char *str;
+ pke_audio_pw_object obj{};
+ enum PK_STN_RES res;
+
+ // if (props == NULL) return; // why
+ if (props == NULL) {
+ printf("\r\nPW CALLBACK: NULL PROPS: object: id:%u type:%s/%d\n", id, type, version);
+ return; // why - in what scenario does this happen?
+ }
+
+ if (spa_streq(type, PW_TYPE_INTERFACE_Node)) {
+ printf("\r\nPW CALLBACK NODE: object: id:%u type:%s/%d\n", id, type, version);
+ obj.type = PKE_AUDIO_PW_OBJECT_TYPE_NODE;
+ } else if (spa_streq(type, PW_TYPE_INTERFACE_Port)) {
+ obj.type = PKE_AUDIO_PW_OBJECT_TYPE_PORT;
+ if ((str = spa_dict_lookup(props, PW_KEY_PORT_DIRECTION)) == NULL) {
+ return;
+ }
+ if (spa_streq(str, "in")) {
+ obj.data.port.direction = PW_DIRECTION_INPUT;
+ } else if (spa_streq(str, "out")) {
+ obj.data.port.direction = PW_DIRECTION_OUTPUT;
+ } else {
+ return;
+ }
+ if ((str = spa_dict_lookup(props, PW_KEY_NODE_ID)) == NULL) {
+ return;
+ }
+ if (res = pk_stn(&obj.data.port.node, str, 0), res != PK_STN_RES_SUCCESS) {
+ fprintf(stderr, "\r\nPW_CALLBACK: failed to parse port node from str: '%s', PK_STN_RES: %i\n", str, res);
+ return;
+ }
+ if ((str = spa_dict_lookup(props, PW_KEY_PORT_ID)) == NULL) {
+ return;
+ }
+ if (res = pk_stn(&obj.data.port.id, str, 0), res != PK_STN_RES_SUCCESS) {
+ fprintf(stderr, "\r\nPW_CALLBACK: failed to parse port id from str: '%s', PK_STN_RES: %i\n", str, res);
+ return;
+ }
+ } else if (spa_streq(type, PW_TYPE_INTERFACE_Link)) {
+ obj.type = PKE_AUDIO_PW_OBJECT_TYPE_LINK;
+ if ((str = spa_dict_lookup(props, PW_KEY_LINK_OUTPUT_PORT)) == NULL) {
+ return;
+ }
+ if (res = pk_stn(&obj.data.link.output_port, str, 0), res != PK_STN_RES_SUCCESS) {
+ fprintf(stderr, "\r\nPW_CALLBACK: failed to parse link output port from str: '%s', PK_STN_RES: %i\n", str, res);
+ return;
+ }
+ if ((str = spa_dict_lookup(props, PW_KEY_LINK_INPUT_PORT)) == NULL) {
+ return;
+ }
+ if (res = pk_stn(&obj.data.link.input_port, str, 0), res != PK_STN_RES_SUCCESS) {
+ fprintf(stderr, "\r\nPW_CALLBACK: failed to parse link input port from str: '%s', PK_STN_RES: %i\n", str, res);
+ return;
+ }
+ } else if (spa_streq(type, PW_TYPE_INTERFACE_Metadata)) {
+ obj.type = PKE_AUDIO_PW_OBJECT_TYPE_METADATA;
+ if ((str = spa_dict_lookup(props, PW_KEY_METADATA_NAME)) == NULL) {
+ return;
+ }
+ if (!spa_streq(str, "default")) {
+ return;
+ }
+ pke_audio_impl_pw.metadata = (pw_metadata*)pw_registry_bind(pke_audio_impl_pw.registry, id, PW_TYPE_INTERFACE_Metadata, PW_VERSION_METADATA, 0);
+
+ spa_zero(pke_audio_impl_pw.metadata_listener);
+
+ pw_metadata_add_listener(pke_audio_impl_pw.metadata, &pke_audio_impl_pw.metadata_listener, &metadata_events, NULL);
+ } else {
+ return;
+ }
+
+#if 0
+ int i;
+ for (i = 0; i < props->n_items; ++i) {
+ printf("\r\n\t'%s': '%s'", props->items[i].key, props->items[i].value);
+ }
+ printf("\r\n");
+#endif
+
+ obj.id = id;
+ obj.props = pw_properties_new_dict(props);
+
+ printf("\r\nPW CALLBACK: object: id:%u type:%s/%d\n", id, type, version);
+
+ pk_arr_append_t(&pke_audio_impl_pw.pw_objects, obj);
+ return;
+}
+
+void on_registry_event_global_removal(void *data, uint32_t id) {
+ (void)data;
+ uint32_t i;
+ // printf("\r\nPW CALLBACK: object remove: id:%u ...", id);
+ if (i = pk_arr_find_first_index(&pke_audio_impl_pw.pw_objects, &id, pw_objects_find_pw_object_by_id), i != 0xFFFFFFFF)
+ {
+ if (pke_audio_impl_pw.pw_objects[i].type != PKE_AUDIO_PW_OBJECT_TYPE_LINK) {
+ pke_audio_impl_pw.is_needing_output_remapped = true;
+ }
+ if (pke_audio_impl_pw.pw_objects[i].id == pke_audio_impl_pw.default_sink_id) {
+ // we just lost our target audio device
+ pke_audio_impl_pw.default_sink_id = 0;
+ pke_audio_impl_pw.default_sink_name[0] = '\0';
+ }
+ pk_arr_remove_at(&pke_audio_impl_pw.pw_objects, i);
+ // printf(" removed.\n");
+ }
+ return;
+}