diff options
author | dec05eba <dec05eba@protonmail.com> | 2025-02-27 01:43:16 +0100 |
---|---|---|
committer | dec05eba <dec05eba@protonmail.com> | 2025-02-27 01:43:16 +0100 |
commit | ae2929d4f723cef7a148c671f0c71e5cb0f87592 (patch) | |
tree | 80f9ec5d58b69c1c35afbd9a21bdff9a5b729df1 | |
parent | d9eb44fae0953863b49c46666380dbffaec60b6b (diff) |
Pipewire: auto change default_output/default_input connected nodes when the default devices are changed in system audio settings
-rw-r--r-- | TODO | 2 | ||||
-rw-r--r-- | include/pipewire_audio.h | 32 | ||||
-rw-r--r-- | include/sound.hpp | 2 | ||||
-rw-r--r-- | src/pipewire_audio.c | 464 |
4 files changed, 400 insertions, 100 deletions
@@ -240,3 +240,5 @@ Detect if cached portal session token is no longer valid (this can happen if the Support reconnecting (and setting things up again) if the audio server is restarted (for both device recording and app recording). Find out how nvidia-smi fixes nvenc not working on opensuse and do that ourselves instead of relying on nvidia-smi that is not always installed. + +Pulseaudio code: add "running" variable to loops to allow stopping the running code when quitting.
\ No newline at end of file diff --git a/include/pipewire_audio.h b/include/pipewire_audio.h index e2ffd49..1cf6666 100644 --- a/include/pipewire_audio.h +++ b/include/pipewire_audio.h @@ -10,6 +10,7 @@ #define GSR_PIPEWIRE_AUDIO_MAX_STREAM_NODES 128 #define GSR_PIPEWIRE_AUDIO_MAX_PORTS 256 +#define GSR_PIPEWIRE_AUDIO_MAX_LINKS 256 #define GSR_PIPEWIRE_AUDIO_MAX_REQUESTED_LINKS 32 #define GSR_PIPEWIRE_AUDIO_MAX_VIRTUAL_SINKS 32 @@ -37,14 +38,31 @@ typedef struct { char *name; } gsr_pipewire_audio_port; +typedef struct { + uint32_t id; + uint32_t output_node_id; + uint32_t input_node_id; +} gsr_pipewire_audio_link; + typedef enum { GSR_PIPEWIRE_AUDIO_LINK_INPUT_TYPE_STREAM, /* Application */ GSR_PIPEWIRE_AUDIO_LINK_INPUT_TYPE_SINK /* Combined (virtual) sink */ } gsr_pipewire_audio_link_input_type; +typedef enum { + GSR_PIPEWIRE_AUDIO_REQUESTED_TYPE_STANDARD, + GSR_PIPEWIRE_AUDIO_REQUESTED_TYPE_DEFAULT_OUTPUT, + GSR_PIPEWIRE_AUDIO_REQUESTED_TYPE_DEFAULT_INPUT +} gsr_pipewire_audio_requested_type; + typedef struct { - char **output_names; - int num_output_names; + char *name; + gsr_pipewire_audio_requested_type type; +} gsr_pipewire_audio_requested_output; + +typedef struct { + gsr_pipewire_audio_requested_output *outputs; + int num_outputs; char *input_name; bool inverted; gsr_pipewire_audio_node_type output_type; @@ -60,12 +78,20 @@ typedef struct { struct spa_hook registry_listener; int server_version_sync; + struct pw_proxy *metadata_proxy; + struct spa_hook metadata_listener; + char default_output_device_name[128]; + char default_input_device_name[128]; + gsr_pipewire_audio_node stream_nodes[GSR_PIPEWIRE_AUDIO_MAX_STREAM_NODES]; int num_stream_nodes; gsr_pipewire_audio_port ports[GSR_PIPEWIRE_AUDIO_MAX_PORTS]; int num_ports; + gsr_pipewire_audio_link links[GSR_PIPEWIRE_AUDIO_MAX_LINKS]; + int num_links; + gsr_pipewire_audio_requested_link requested_links[GSR_PIPEWIRE_AUDIO_MAX_REQUESTED_LINKS]; int num_requested_links; @@ -118,6 +144,8 @@ bool gsr_pipewire_audio_add_link_from_apps_to_sink_inverted(gsr_pipewire_audio * If a device or a new device starts outputting audio after this function is called and the device name matches then it will automatically link the audio sources. |source_names| and |sink_name_input| are case-insensitive matches. + |source_names| can include "default_output" or "default_input" to use the default output/input + and it will automatically switch when the default output/input is changed in system audio settings. */ bool gsr_pipewire_audio_add_link_from_sources_to_sink(gsr_pipewire_audio *self, const char **source_names, int num_source_names, const char *sink_name_input); diff --git a/include/sound.hpp b/include/sound.hpp index 95bd6f2..87e2e2d 100644 --- a/include/sound.hpp +++ b/include/sound.hpp @@ -62,6 +62,8 @@ typedef enum { /* Get a sound device by name, returning the device into the |device| parameter. |device_name| can be a device name or "default_output" or "default_input". + If the device name is "default_output" or "default_input" then it will automatically switch which + device is records from when the default output/input is changed in the system audio settings. Returns 0 on success, or a negative value on failure. */ int sound_device_get_by_name(SoundDevice *device, const char *device_name, const char *description, unsigned int num_channels, unsigned int period_frame_size, AudioFormat audio_format); diff --git a/src/pipewire_audio.c b/src/pipewire_audio.c index 54f29bc..be91e76 100644 --- a/src/pipewire_audio.c +++ b/src/pipewire_audio.c @@ -1,6 +1,8 @@ #include "../include/pipewire_audio.h" #include <pipewire/pipewire.h> +#include <pipewire/extensions/metadata.h> +#include <pipewire/impl-module.h> static void on_core_info_cb(void *user_data, const struct pw_core_info *info) { gsr_pipewire_audio *self = user_data; @@ -44,35 +46,115 @@ static gsr_pipewire_audio_port* gsr_pipewire_audio_get_node_port_by_name(gsr_pip } static bool requested_link_matches_name_case_insensitive(const gsr_pipewire_audio_requested_link *requested_link, const char *name) { - for(int i = 0; i < requested_link->num_output_names; ++i) { - if(strcasecmp(requested_link->output_names[i], name) == 0) + for(int i = 0; i < requested_link->num_outputs; ++i) { + if(requested_link->outputs[i].type == GSR_PIPEWIRE_AUDIO_REQUESTED_TYPE_STANDARD && strcasecmp(requested_link->outputs[i].name, name) == 0) return true; } return false; } -static void gsr_pipewire_audio_create_link(gsr_pipewire_audio *self, const gsr_pipewire_audio_requested_link *requested_link) { - const gsr_pipewire_audio_node_type requested_link_node_type = requested_link->input_type == GSR_PIPEWIRE_AUDIO_LINK_INPUT_TYPE_STREAM ? GSR_PIPEWIRE_AUDIO_NODE_TYPE_STREAM_INPUT : GSR_PIPEWIRE_AUDIO_NODE_TYPE_SINK_OR_SOURCE; - const gsr_pipewire_audio_node *stream_input_node = gsr_pipewire_audio_get_node_by_name_case_insensitive(self, requested_link->input_name, requested_link_node_type); - if(!stream_input_node) - return; +static bool requested_link_has_type(const gsr_pipewire_audio_requested_link *requested_link, gsr_pipewire_audio_requested_type type) { + for(int i = 0; i < requested_link->num_outputs; ++i) { + if(requested_link->outputs[i].type == type) + return true; + } + return false; +} - const gsr_pipewire_audio_port *input_fl_port = NULL; - const gsr_pipewire_audio_port *input_fr_port = NULL; +static void gsr_pipewire_get_node_input_port_by_type(gsr_pipewire_audio *self, const gsr_pipewire_audio_node *input_node, gsr_pipewire_audio_link_input_type input_type, + const gsr_pipewire_audio_port **input_fl_port, const gsr_pipewire_audio_port **input_fr_port) +{ + *input_fl_port = NULL; + *input_fr_port = NULL; - switch(requested_link->input_type) { + switch(input_type) { case GSR_PIPEWIRE_AUDIO_LINK_INPUT_TYPE_STREAM: { - input_fl_port = gsr_pipewire_audio_get_node_port_by_name(self, stream_input_node->id, "input_FL"); - input_fr_port = gsr_pipewire_audio_get_node_port_by_name(self, stream_input_node->id, "input_FR"); + *input_fl_port = gsr_pipewire_audio_get_node_port_by_name(self, input_node->id, "input_FL"); + *input_fr_port = gsr_pipewire_audio_get_node_port_by_name(self, input_node->id, "input_FR"); break; } case GSR_PIPEWIRE_AUDIO_LINK_INPUT_TYPE_SINK: { - input_fl_port = gsr_pipewire_audio_get_node_port_by_name(self, stream_input_node->id, "playback_FL"); - input_fr_port = gsr_pipewire_audio_get_node_port_by_name(self, stream_input_node->id, "playback_FR"); + *input_fl_port = gsr_pipewire_audio_get_node_port_by_name(self, input_node->id, "playback_FL"); + *input_fr_port = gsr_pipewire_audio_get_node_port_by_name(self, input_node->id, "playback_FR"); + break; + } + } +} + +static void gsr_pipewire_get_node_output_port_by_type(gsr_pipewire_audio *self, const gsr_pipewire_audio_node *output_node, gsr_pipewire_audio_node_type output_type, + const gsr_pipewire_audio_port **output_fl_port, const gsr_pipewire_audio_port **output_fr_port) +{ + *output_fl_port = NULL; + *output_fr_port = NULL; + + switch(output_type) { + case GSR_PIPEWIRE_AUDIO_NODE_TYPE_STREAM_OUTPUT: + *output_fl_port = gsr_pipewire_audio_get_node_port_by_name(self, output_node->id, "output_FL"); + *output_fr_port = gsr_pipewire_audio_get_node_port_by_name(self, output_node->id, "output_FR"); + break; + case GSR_PIPEWIRE_AUDIO_NODE_TYPE_STREAM_INPUT: + *output_fl_port = gsr_pipewire_audio_get_node_port_by_name(self, output_node->id, "monitor_FL"); + *output_fr_port = gsr_pipewire_audio_get_node_port_by_name(self, output_node->id, "monitor_FR"); + break; + case GSR_PIPEWIRE_AUDIO_NODE_TYPE_SINK_OR_SOURCE: { + *output_fl_port = gsr_pipewire_audio_get_node_port_by_name(self, output_node->id, "monitor_FL"); + *output_fr_port = gsr_pipewire_audio_get_node_port_by_name(self, output_node->id, "monitor_FR"); + if(!*output_fl_port || !*output_fr_port) { + *output_fl_port = gsr_pipewire_audio_get_node_port_by_name(self, output_node->id, "capture_FL"); + *output_fr_port = gsr_pipewire_audio_get_node_port_by_name(self, output_node->id, "capture_FR"); + } + if(!*output_fl_port || !*output_fr_port) { + const gsr_pipewire_audio_port *output_mono_port = gsr_pipewire_audio_get_node_port_by_name(self, output_node->id, "monitor_MONO"); + if(!output_mono_port) + output_mono_port = gsr_pipewire_audio_get_node_port_by_name(self, output_node->id, "capture_MONO"); + + if(output_mono_port) { + *output_fl_port = output_mono_port; + *output_fr_port = output_mono_port; + } + } break; } } +} + +static void gsr_pipewire_audio_establish_link(gsr_pipewire_audio *self, const gsr_pipewire_audio_port *input_fl_port, const gsr_pipewire_audio_port *input_fr_port, + const gsr_pipewire_audio_port *output_fl_port, const gsr_pipewire_audio_port *output_fr_port) +{ + // TODO: Detect if link already exists before so we dont create these proxies when not needed + + //fprintf(stderr, "linking!\n"); + // TODO: error check and cleanup + { + struct pw_properties *props = pw_properties_new(NULL, NULL); + pw_properties_setf(props, PW_KEY_LINK_OUTPUT_PORT, "%u", output_fl_port->id); + pw_properties_setf(props, PW_KEY_LINK_INPUT_PORT, "%u", input_fl_port->id); + // TODO: Clean this up when removing node + struct pw_proxy *proxy = pw_core_create_object(self->core, "link-factory", PW_TYPE_INTERFACE_Link, PW_VERSION_LINK, &props->dict, 0); + //self->server_version_sync = pw_core_sync(self->core, PW_ID_CORE, self->server_version_sync); + pw_properties_free(props); + } + + { + struct pw_properties *props = pw_properties_new(NULL, NULL); + pw_properties_setf(props, PW_KEY_LINK_OUTPUT_PORT, "%u", output_fr_port->id); + pw_properties_setf(props, PW_KEY_LINK_INPUT_PORT, "%u", input_fr_port->id); + // TODO: Clean this up when removing node + struct pw_proxy *proxy = pw_core_create_object(self->core, "link-factory", PW_TYPE_INTERFACE_Link, PW_VERSION_LINK, &props->dict, 0); + //self->server_version_sync = pw_core_sync(self->core, PW_ID_CORE, self->server_version_sync); + pw_properties_free(props); + } +} + +static void gsr_pipewire_audio_create_link(gsr_pipewire_audio *self, const gsr_pipewire_audio_requested_link *requested_link) { + const gsr_pipewire_audio_node_type requested_link_node_type = requested_link->input_type == GSR_PIPEWIRE_AUDIO_LINK_INPUT_TYPE_STREAM ? GSR_PIPEWIRE_AUDIO_NODE_TYPE_STREAM_INPUT : GSR_PIPEWIRE_AUDIO_NODE_TYPE_SINK_OR_SOURCE; + const gsr_pipewire_audio_node *stream_input_node = gsr_pipewire_audio_get_node_by_name_case_insensitive(self, requested_link->input_name, requested_link_node_type); + if(!stream_input_node) + return; + const gsr_pipewire_audio_port *input_fl_port = NULL; + const gsr_pipewire_audio_port *input_fr_port = NULL; + gsr_pipewire_get_node_input_port_by_type(self, stream_input_node, requested_link->input_type, &input_fl_port, &input_fr_port); if(!input_fl_port || !input_fr_port) return; @@ -92,71 +174,203 @@ static void gsr_pipewire_audio_create_link(gsr_pipewire_audio *self, const gsr_p const gsr_pipewire_audio_port *output_fl_port = NULL; const gsr_pipewire_audio_port *output_fr_port = NULL; + gsr_pipewire_get_node_output_port_by_type(self, output_node, requested_link->output_type, &output_fl_port, &output_fr_port); + if(!output_fl_port || !output_fr_port) + continue; - switch(requested_link->output_type) { - case GSR_PIPEWIRE_AUDIO_NODE_TYPE_STREAM_OUTPUT: - output_fl_port = gsr_pipewire_audio_get_node_port_by_name(self, output_node->id, "output_FL"); - output_fr_port = gsr_pipewire_audio_get_node_port_by_name(self, output_node->id, "output_FR"); - break; - case GSR_PIPEWIRE_AUDIO_NODE_TYPE_STREAM_INPUT: - output_fl_port = gsr_pipewire_audio_get_node_port_by_name(self, output_node->id, "monitor_FL"); - output_fr_port = gsr_pipewire_audio_get_node_port_by_name(self, output_node->id, "monitor_FR"); - break; - case GSR_PIPEWIRE_AUDIO_NODE_TYPE_SINK_OR_SOURCE: { - output_fl_port = gsr_pipewire_audio_get_node_port_by_name(self, output_node->id, "monitor_FL"); - output_fr_port = gsr_pipewire_audio_get_node_port_by_name(self, output_node->id, "monitor_FR"); - if(!output_fl_port || !output_fr_port) { - output_fl_port = gsr_pipewire_audio_get_node_port_by_name(self, output_node->id, "capture_FL"); - output_fr_port = gsr_pipewire_audio_get_node_port_by_name(self, output_node->id, "capture_FR"); - } - if(!output_fl_port || !output_fr_port) { - const gsr_pipewire_audio_port *output_mono_port = NULL; - output_mono_port = gsr_pipewire_audio_get_node_port_by_name(self, output_node->id, "monitor_MONO"); - if(!output_mono_port) - output_mono_port = gsr_pipewire_audio_get_node_port_by_name(self, output_node->id, "capture_MONO"); - - if(output_mono_port) { - output_fl_port = output_mono_port; - output_fr_port = output_mono_port; - } - } - break; + gsr_pipewire_audio_establish_link(self, input_fl_port, input_fr_port, output_fl_port, output_fr_port); + } +} + +static void gsr_pipewire_audio_create_links(gsr_pipewire_audio *self) { + for(int i = 0; i < self->num_requested_links; ++i) { + gsr_pipewire_audio_create_link(self, &self->requested_links[i]); + } +} + +static void gsr_pipewire_audio_create_link_for_default_devices(gsr_pipewire_audio *self, const gsr_pipewire_audio_requested_link *requested_link, gsr_pipewire_audio_requested_type default_device_type) { + if(default_device_type == GSR_PIPEWIRE_AUDIO_REQUESTED_TYPE_STANDARD) + return; + + const char *device_name = default_device_type == GSR_PIPEWIRE_AUDIO_REQUESTED_TYPE_DEFAULT_OUTPUT ? self->default_output_device_name : self->default_input_device_name; + if(device_name[0] == '\0') + return; + + if(!requested_link_has_type(requested_link, default_device_type)) + return; + + const gsr_pipewire_audio_node_type requested_link_node_type = requested_link->input_type == GSR_PIPEWIRE_AUDIO_LINK_INPUT_TYPE_STREAM ? GSR_PIPEWIRE_AUDIO_NODE_TYPE_STREAM_INPUT : GSR_PIPEWIRE_AUDIO_NODE_TYPE_SINK_OR_SOURCE; + const gsr_pipewire_audio_node *stream_input_node = gsr_pipewire_audio_get_node_by_name_case_insensitive(self, requested_link->input_name, requested_link_node_type); + if(!stream_input_node) + return; + + const gsr_pipewire_audio_port *input_fl_port = NULL; + const gsr_pipewire_audio_port *input_fr_port = NULL; + gsr_pipewire_get_node_input_port_by_type(self, stream_input_node, requested_link->input_type, &input_fl_port, &input_fr_port); + if(!input_fl_port || !input_fr_port) + return; + + const gsr_pipewire_audio_node *stream_output_node = gsr_pipewire_audio_get_node_by_name_case_insensitive(self, device_name, GSR_PIPEWIRE_AUDIO_NODE_TYPE_SINK_OR_SOURCE); + if(!stream_output_node) + return; + + const gsr_pipewire_audio_port *output_fl_port = NULL; + const gsr_pipewire_audio_port *output_fr_port = NULL; + gsr_pipewire_get_node_output_port_by_type(self, stream_output_node, requested_link->output_type, &output_fl_port, &output_fr_port); + if(!output_fl_port || !output_fr_port) + return; + + gsr_pipewire_audio_establish_link(self, input_fl_port, input_fr_port, output_fl_port, output_fr_port); + //fprintf(stderr, "establishing a link from %u to %u\n", stream_output_node->id, stream_input_node->id); +} + +static void gsr_pipewire_audio_create_links_for_default_devices(gsr_pipewire_audio *self, gsr_pipewire_audio_requested_type default_device_type) { + for(int i = 0; i < self->num_requested_links; ++i) { + gsr_pipewire_audio_create_link_for_default_devices(self, &self->requested_links[i], default_device_type); + } +} + +static void gsr_pipewire_audio_destroy_links_by_output_to_input(gsr_pipewire_audio *self, uint32_t output_node_id, uint32_t input_node_id) { + for(int i = 0; i < self->num_links; ++i) { + if(self->links[i].output_node_id == output_node_id && self->links[i].input_node_id == input_node_id) + pw_registry_destroy(self->registry, self->links[i].id); + } +} + +static void gsr_pipewire_destroy_default_device_link(gsr_pipewire_audio *self, const gsr_pipewire_audio_requested_link *requested_link, gsr_pipewire_audio_requested_type default_device_type) { + if(default_device_type == GSR_PIPEWIRE_AUDIO_REQUESTED_TYPE_STANDARD) + return; + + const char *device_name = default_device_type == GSR_PIPEWIRE_AUDIO_REQUESTED_TYPE_DEFAULT_OUTPUT ? self->default_output_device_name : self->default_input_device_name; + if(device_name[0] == '\0') + return; + + if(!requested_link_has_type(requested_link, default_device_type)) + return; + + /* default_output and default_input can be the same device. In that case both are the same link and we dont want to remove the link */ + const gsr_pipewire_audio_requested_type opposite_device_type = default_device_type == GSR_PIPEWIRE_AUDIO_REQUESTED_TYPE_DEFAULT_OUTPUT ? GSR_PIPEWIRE_AUDIO_REQUESTED_TYPE_DEFAULT_INPUT : GSR_PIPEWIRE_AUDIO_REQUESTED_TYPE_DEFAULT_OUTPUT; + const char *opposite_device_name = opposite_device_type == GSR_PIPEWIRE_AUDIO_REQUESTED_TYPE_DEFAULT_OUTPUT ? self->default_output_device_name : self->default_input_device_name; + if(requested_link_has_type(requested_link, opposite_device_type) && strcmp(device_name, opposite_device_name) == 0) + return; + + const gsr_pipewire_audio_node_type requested_link_node_type = requested_link->input_type == GSR_PIPEWIRE_AUDIO_LINK_INPUT_TYPE_STREAM ? GSR_PIPEWIRE_AUDIO_NODE_TYPE_STREAM_INPUT : GSR_PIPEWIRE_AUDIO_NODE_TYPE_SINK_OR_SOURCE; + const gsr_pipewire_audio_node *stream_input_node = gsr_pipewire_audio_get_node_by_name_case_insensitive(self, requested_link->input_name, requested_link_node_type); + if(!stream_input_node) + return; + + const gsr_pipewire_audio_node *stream_output_node = gsr_pipewire_audio_get_node_by_name_case_insensitive(self, device_name, GSR_PIPEWIRE_AUDIO_NODE_TYPE_SINK_OR_SOURCE); + if(!stream_output_node) + return; + + if(requested_link_matches_name_case_insensitive(requested_link, stream_output_node->name)) + return; + + gsr_pipewire_audio_destroy_links_by_output_to_input(self, stream_output_node->id, stream_input_node->id); + //fprintf(stderr, "destroying a link from %u to %u\n", stream_output_node->id, stream_input_node->id); +} + +static void gsr_pipewire_destroy_default_device_links(gsr_pipewire_audio *self, gsr_pipewire_audio_requested_type default_device_type) { + for(int i = 0; i < self->num_requested_links; ++i) { + gsr_pipewire_destroy_default_device_link(self, &self->requested_links[i], default_device_type); + } +} + +static bool json_get_value(const char *json_str, const char *key, char *value, size_t value_size) { + char key_full[32]; + const int key_full_size = snprintf(key_full, sizeof(key_full), "\"%s\":", key); + const char *start = strstr(json_str, key_full); + if(!start) + return false; + + start += key_full_size; + const char *value_start = strchr(start, '"'); + if(!value_start) + return false; + + value_start += 1; + const char *value_end = strchr(value_start, '"'); + if(!value_end) + return false; + + snprintf(value, value_size, "%.*s", (int)(value_end - value_start), value_start); + return true; +} + +static int on_metadata_property_cb(void *data, uint32_t id, const char *key, const char *type, const char *value) { + (void)type; + gsr_pipewire_audio *self = data; + + if(id == PW_ID_CORE && key && value) { + char value_decoded[128]; + if(strcmp(key, "default.audio.sink") == 0) { + if(json_get_value(value, "name", value_decoded, sizeof(value_decoded)) && strcmp(value_decoded, self->default_output_device_name) != 0) { + gsr_pipewire_destroy_default_device_links(self, GSR_PIPEWIRE_AUDIO_REQUESTED_TYPE_DEFAULT_OUTPUT); + snprintf(self->default_output_device_name, sizeof(self->default_output_device_name), "%s", value_decoded); + gsr_pipewire_audio_create_links_for_default_devices(self, GSR_PIPEWIRE_AUDIO_REQUESTED_TYPE_DEFAULT_OUTPUT); + } + } else if(strcmp(key, "default.audio.source") == 0) { + if(json_get_value(value, "name", value_decoded, sizeof(value_decoded)) && strcmp(value_decoded, self->default_input_device_name) != 0) { + gsr_pipewire_destroy_default_device_links(self, GSR_PIPEWIRE_AUDIO_REQUESTED_TYPE_DEFAULT_INPUT); + snprintf(self->default_input_device_name, sizeof(self->default_input_device_name), "%s", value_decoded); + gsr_pipewire_audio_create_links_for_default_devices(self, GSR_PIPEWIRE_AUDIO_REQUESTED_TYPE_DEFAULT_INPUT); } } + } - if(!output_fl_port || !output_fr_port) - continue; + return 0; +} - // TODO: Detect if link already exists before so we dont create these proxies when not needed - - //fprintf(stderr, "linking!\n"); - // TODO: error check and cleanup - { - struct pw_properties *props = pw_properties_new(NULL, NULL); - pw_properties_setf(props, PW_KEY_LINK_OUTPUT_PORT, "%u", output_fl_port->id); - pw_properties_setf(props, PW_KEY_LINK_INPUT_PORT, "%u", input_fl_port->id); - // TODO: Clean this up when removing node - struct pw_proxy *proxy = pw_core_create_object(self->core, "link-factory", PW_TYPE_INTERFACE_Link, PW_VERSION_LINK, &props->dict, 0); - //self->server_version_sync = pw_core_sync(self->core, PW_ID_CORE, self->server_version_sync); - pw_properties_free(props); - } +static const struct pw_metadata_events metadata_events = { + PW_VERSION_METADATA_EVENTS, + .property = on_metadata_property_cb, +}; - { - struct pw_properties *props = pw_properties_new(NULL, NULL); - pw_properties_setf(props, PW_KEY_LINK_OUTPUT_PORT, "%u", output_fr_port->id); - pw_properties_setf(props, PW_KEY_LINK_INPUT_PORT, "%u", input_fr_port->id); - // TODO: Clean this up when removing node - struct pw_proxy *proxy = pw_core_create_object(self->core, "link-factory", PW_TYPE_INTERFACE_Link, PW_VERSION_LINK, &props->dict, 0); - //self->server_version_sync = pw_core_sync(self->core, PW_ID_CORE, self->server_version_sync); - pw_properties_free(props); - } +static void on_metadata_proxy_removed_cb(void *data) { + gsr_pipewire_audio *self = data; + if(self->metadata_proxy) { + // TODO: + //pw_proxy_destroy(self->metadata_proxy); + //self->metadata_proxy = NULL; } } -static void gsr_pipewire_audio_create_links(gsr_pipewire_audio *self) { - for(int j = 0; j < self->num_requested_links; ++j) { - gsr_pipewire_audio_create_link(self, &self->requested_links[j]); +static void on_metadata_proxy_destroy_cb(void *data) { + gsr_pipewire_audio *self = data; + + // TODO: + //spa_hook_remove(&metadata->metadata_listener); + //spa_hook_remove(&metadata->proxy_listener); + //spa_zero(metadata->metadata_listener); + //spa_zero(metadata->proxy_listener); + + //self->metadata_proxy = NULL; +} + +static const struct pw_proxy_events metadata_proxy_events = { + PW_VERSION_PROXY_EVENTS, + .removed = on_metadata_proxy_removed_cb, + .destroy = on_metadata_proxy_destroy_cb, +}; + +static bool gsr_pipewire_audio_listen_on_metadata(gsr_pipewire_audio *self, uint32_t id) { + if(self->metadata_proxy) { + pw_proxy_destroy(self->metadata_proxy); + self->metadata_proxy = NULL; + } + + self->metadata_proxy = pw_registry_bind(self->registry, id, PW_TYPE_INTERFACE_Metadata, PW_VERSION_METADATA, 0); + if(!self->metadata_proxy) { + fprintf(stderr, "gsr error: gsr_pipewire_audio_listen_on_metadata: failed to bind to registry\n"); + return false; } + + // TODO: + pw_proxy_add_object_listener(self->metadata_proxy, &self->metadata_listener, &metadata_events, self); + + //struct spa_hook proxy_listener; + //pw_proxy_add_listener(self->metadata_proxy, &proxy_listener, &metadata_proxy_events, self); + return true; } static void registry_event_global(void *data, uint32_t id, uint32_t permissions, @@ -164,7 +378,7 @@ static void registry_event_global(void *data, uint32_t id, uint32_t permissions, const struct spa_dict *props) { //fprintf(stderr, "add: id: %d, type: %s\n", (int)id, type); - if (props == NULL) + if(!props || !type) return; //pw_properties_new_dict(props); @@ -173,7 +387,7 @@ static void registry_event_global(void *data, uint32_t id, uint32_t permissions, if(strcmp(type, PW_TYPE_INTERFACE_Node) == 0) { const char *node_name = spa_dict_lookup(props, PW_KEY_NODE_NAME); const char *media_class = spa_dict_lookup(props, PW_KEY_MEDIA_CLASS); - //fprintf(stderr, " node name: %s, media class: %s\n", node_name, media_class); + //fprintf(stderr, " node id: %u, node name: %s, media class: %s\n", id, node_name, media_class); const bool is_stream_output = media_class && strcmp(media_class, "Stream/Output/Audio") == 0; const bool is_stream_input = media_class && strcmp(media_class, "Stream/Input/Audio") == 0; const bool is_sink = media_class && strcmp(media_class, "Audio/Sink") == 0; @@ -217,6 +431,7 @@ static void registry_event_global(void *data, uint32_t id, uint32_t permissions, //fprintf(stderr, " port name: %s, node id: %d, direction: %s\n", port_name, node_id_num, port_direction); char *port_name_copy = strdup(port_name); if(port_name_copy) { + //fprintf(stderr, " port id: %u, node id: %u, name: %s\n", id, node_id_num, port_name_copy); self->ports[self->num_ports].id = id; self->ports[self->num_ports].node_id = node_id_num; self->ports[self->num_ports].direction = direction; @@ -228,6 +443,25 @@ static void registry_event_global(void *data, uint32_t id, uint32_t permissions, } else if(self->num_ports >= GSR_PIPEWIRE_AUDIO_MAX_PORTS) { fprintf(stderr, "gsr error: reached the maximum amount of audio ports\n"); } + } else if(strcmp(type, PW_TYPE_INTERFACE_Link) == 0) { + const char *output_node = spa_dict_lookup(props, PW_KEY_LINK_OUTPUT_NODE); + const char *input_node = spa_dict_lookup(props, PW_KEY_LINK_INPUT_NODE); + + const uint32_t output_node_id_num = output_node ? atoi(output_node) : 0; + const uint32_t input_node_id_num = input_node ? atoi(input_node) : 0; + if(self->num_links < GSR_PIPEWIRE_AUDIO_MAX_LINKS && output_node_id_num > 0 && input_node_id_num > 0) { + //fprintf(stderr, " new link (%u): %u -> %u\n", id, output_node_id_num, input_node_id_num); + self->links[self->num_links].id = id; + self->links[self->num_links].output_node_id = output_node_id_num; + self->links[self->num_links].input_node_id = input_node_id_num; + ++self->num_links; + } else if(self->num_ports >= GSR_PIPEWIRE_AUDIO_MAX_LINKS) { + fprintf(stderr, "gsr error: reached the maximum amount of audio links\n"); + } + } else if(strcmp(type, PW_TYPE_INTERFACE_Metadata) == 0) { + const char *name = spa_dict_lookup(props, PW_KEY_METADATA_NAME); + if(name && strcmp(name, "default") == 0) + gsr_pipewire_audio_listen_on_metadata(self, id); } } @@ -237,9 +471,7 @@ static bool gsr_pipewire_audio_remove_node_by_id(gsr_pipewire_audio *self, uint3 continue; free(self->stream_nodes[i].name); - for(int j = i + 1; j < self->num_stream_nodes; ++j) { - self->stream_nodes[j - 1] = self->stream_nodes[j]; - } + self->stream_nodes[i] = self->stream_nodes[self->num_stream_nodes - 1]; --self->num_stream_nodes; return true; } @@ -252,15 +484,25 @@ static bool gsr_pipewire_audio_remove_port_by_id(gsr_pipewire_audio *self, uint3 continue; free(self->ports[i].name); - for(int j = i + 1; j < self->num_ports; ++j) { - self->ports[j - 1] = self->ports[j]; - } + self->ports[i] = self->ports[self->num_ports - 1]; --self->num_ports; return true; } return false; } +static bool gsr_pipewire_audio_remove_link_by_id(gsr_pipewire_audio *self, uint32_t link_id) { + for(int i = 0; i < self->num_links; ++i) { + if(self->links[i].id != link_id) + continue; + + self->links[i] = self->links[self->num_links - 1]; + --self->num_links; + return true; + } + return false; +} + static void registry_event_global_remove(void *data, uint32_t id) { //fprintf(stderr, "remove: %d\n", (int)id); gsr_pipewire_audio *self = (gsr_pipewire_audio*)data; @@ -273,6 +515,11 @@ static void registry_event_global_remove(void *data, uint32_t id) { //fprintf(stderr, "removed port\n"); return; } + + if(gsr_pipewire_audio_remove_link_by_id(self, id)) { + //fprintf(stderr, "removed link\n"); + return; + } } static const struct pw_registry_events registry_events = { @@ -300,14 +547,17 @@ bool gsr_pipewire_audio_init(gsr_pipewire_audio *self) { return false; } + pw_context_load_module(self->context, "libpipewire-module-link-factory", NULL, NULL); + + pw_thread_loop_lock(self->thread_loop); + if(pw_thread_loop_start(self->thread_loop) < 0) { fprintf(stderr, "gsr error: gsr_pipewire_audio_init: failed to start thread\n"); + pw_thread_loop_unlock(self->thread_loop); gsr_pipewire_audio_deinit(self); return false; } - pw_thread_loop_lock(self->thread_loop); - self->core = pw_context_connect(self->context, pw_properties_new(PW_KEY_REMOTE_NAME, NULL, NULL), 0); if(!self->core) { pw_thread_loop_unlock(self->thread_loop); @@ -341,6 +591,11 @@ void gsr_pipewire_audio_deinit(gsr_pipewire_audio *self) { } self->num_virtual_sink_proxies = 0; + if(self->metadata_proxy) { + pw_proxy_destroy(self->metadata_proxy); + self->metadata_proxy = NULL; + } + if(self->core) { pw_core_disconnect(self->core); self->core = NULL; @@ -366,11 +621,13 @@ void gsr_pipewire_audio_deinit(gsr_pipewire_audio *self) { } self->num_ports = 0; + self->num_links = 0; + for(int i = 0; i < self->num_requested_links; ++i) { - for(int j = 0; j < self->requested_links[i].num_output_names; ++j) { - free(self->requested_links[i].output_names[j]); + for(int j = 0; j < self->requested_links[i].num_outputs; ++j) { + free(self->requested_links[i].outputs[j].name); } - free(self->requested_links[i].output_names); + free(self->requested_links[i].outputs); free(self->requested_links[i].input_name); } self->num_requested_links = 0; @@ -439,14 +696,14 @@ static bool string_remove_suffix(char *str, const char *suffix) { } } -static bool gsr_pipewire_audio_add_link_from_apps_to_output(gsr_pipewire_audio *self, const char **output_names, int num_output_names, const char *input_name, gsr_pipewire_audio_node_type output_type, gsr_pipewire_audio_link_input_type input_type, bool inverted) { +static bool gsr_pipewire_audio_add_links_to_output(gsr_pipewire_audio *self, const char **output_names, int num_output_names, const char *input_name, gsr_pipewire_audio_node_type output_type, gsr_pipewire_audio_link_input_type input_type, bool inverted) { if(self->num_requested_links >= GSR_PIPEWIRE_AUDIO_MAX_REQUESTED_LINKS) { fprintf(stderr, "gsr error: reached the maximum amount of audio links\n"); return false; } - char **output_names_copy = calloc(num_output_names, sizeof(char*)); - if(!output_names_copy) + gsr_pipewire_audio_requested_output *outputs = calloc(num_output_names, sizeof(gsr_pipewire_audio_requested_output)); + if(!outputs) return false; char *input_name_copy = strdup(input_name); @@ -457,23 +714,34 @@ static bool gsr_pipewire_audio_add_link_from_apps_to_output(gsr_pipewire_audio * string_remove_suffix(input_name_copy, ".monitor"); for(int i = 0; i < num_output_names; ++i) { - output_names_copy[i] = strdup(output_names[i]); - if(!output_names_copy[i]) + outputs[i].name = strdup(output_names[i]); + if(!outputs[i].name) goto error; - if(output_type == GSR_PIPEWIRE_AUDIO_NODE_TYPE_SINK_OR_SOURCE) - string_remove_suffix(output_names_copy[i], ".monitor"); + outputs[i].type = GSR_PIPEWIRE_AUDIO_REQUESTED_TYPE_STANDARD; + if(output_type == GSR_PIPEWIRE_AUDIO_NODE_TYPE_SINK_OR_SOURCE) { + string_remove_suffix(outputs[i].name, ".monitor"); + + if(strcmp(outputs[i].name, "default_output") == 0) + outputs[i].type = GSR_PIPEWIRE_AUDIO_REQUESTED_TYPE_DEFAULT_OUTPUT; + else if(strcmp(outputs[i].name, "default_input") == 0) + outputs[i].type = GSR_PIPEWIRE_AUDIO_REQUESTED_TYPE_DEFAULT_INPUT; + else + outputs[i].type = GSR_PIPEWIRE_AUDIO_REQUESTED_TYPE_STANDARD; + } } pw_thread_loop_lock(self->thread_loop); - self->requested_links[self->num_requested_links].output_names = output_names_copy; - self->requested_links[self->num_requested_links].num_output_names = num_output_names; + self->requested_links[self->num_requested_links].outputs = outputs; + self->requested_links[self->num_requested_links].num_outputs = num_output_names; self->requested_links[self->num_requested_links].input_name = input_name_copy; self->requested_links[self->num_requested_links].output_type = output_type; self->requested_links[self->num_requested_links].input_type = input_type; self->requested_links[self->num_requested_links].inverted = inverted; ++self->num_requested_links; gsr_pipewire_audio_create_link(self, &self->requested_links[self->num_requested_links - 1]); + gsr_pipewire_audio_create_link_for_default_devices(self, &self->requested_links[self->num_requested_links - 1], GSR_PIPEWIRE_AUDIO_REQUESTED_TYPE_DEFAULT_OUTPUT); + gsr_pipewire_audio_create_link_for_default_devices(self, &self->requested_links[self->num_requested_links - 1], GSR_PIPEWIRE_AUDIO_REQUESTED_TYPE_DEFAULT_INPUT); pw_thread_loop_unlock(self->thread_loop); return true; @@ -481,30 +749,30 @@ static bool gsr_pipewire_audio_add_link_from_apps_to_output(gsr_pipewire_audio * error: free(input_name_copy); for(int i = 0; i < num_output_names; ++i) { - free(output_names_copy[i]); + free(outputs[i].name); } - free(output_names_copy); + free(outputs); return false; } bool gsr_pipewire_audio_add_link_from_apps_to_stream(gsr_pipewire_audio *self, const char **app_names, int num_app_names, const char *stream_name_input) { - return gsr_pipewire_audio_add_link_from_apps_to_output(self, app_names, num_app_names, stream_name_input, GSR_PIPEWIRE_AUDIO_NODE_TYPE_STREAM_OUTPUT, GSR_PIPEWIRE_AUDIO_LINK_INPUT_TYPE_STREAM, false); + return gsr_pipewire_audio_add_links_to_output(self, app_names, num_app_names, stream_name_input, GSR_PIPEWIRE_AUDIO_NODE_TYPE_STREAM_OUTPUT, GSR_PIPEWIRE_AUDIO_LINK_INPUT_TYPE_STREAM, false); } bool gsr_pipewire_audio_add_link_from_apps_to_stream_inverted(gsr_pipewire_audio *self, const char **app_names, int num_app_names, const char *stream_name_input) { - return gsr_pipewire_audio_add_link_from_apps_to_output(self, app_names, num_app_names, stream_name_input, GSR_PIPEWIRE_AUDIO_NODE_TYPE_STREAM_OUTPUT, GSR_PIPEWIRE_AUDIO_LINK_INPUT_TYPE_STREAM, true); + return gsr_pipewire_audio_add_links_to_output(self, app_names, num_app_names, stream_name_input, GSR_PIPEWIRE_AUDIO_NODE_TYPE_STREAM_OUTPUT, GSR_PIPEWIRE_AUDIO_LINK_INPUT_TYPE_STREAM, true); } bool gsr_pipewire_audio_add_link_from_apps_to_sink(gsr_pipewire_audio *self, const char **app_names, int num_app_names, const char *sink_name_input) { - return gsr_pipewire_audio_add_link_from_apps_to_output(self, app_names, num_app_names, sink_name_input, GSR_PIPEWIRE_AUDIO_NODE_TYPE_STREAM_OUTPUT, GSR_PIPEWIRE_AUDIO_LINK_INPUT_TYPE_SINK, false); + return gsr_pipewire_audio_add_links_to_output(self, app_names, num_app_names, sink_name_input, GSR_PIPEWIRE_AUDIO_NODE_TYPE_STREAM_OUTPUT, GSR_PIPEWIRE_AUDIO_LINK_INPUT_TYPE_SINK, false); } bool gsr_pipewire_audio_add_link_from_apps_to_sink_inverted(gsr_pipewire_audio *self, const char **app_names, int num_app_names, const char *sink_name_input) { - return gsr_pipewire_audio_add_link_from_apps_to_output(self, app_names, num_app_names, sink_name_input, GSR_PIPEWIRE_AUDIO_NODE_TYPE_STREAM_OUTPUT, GSR_PIPEWIRE_AUDIO_LINK_INPUT_TYPE_SINK, true); + return gsr_pipewire_audio_add_links_to_output(self, app_names, num_app_names, sink_name_input, GSR_PIPEWIRE_AUDIO_NODE_TYPE_STREAM_OUTPUT, GSR_PIPEWIRE_AUDIO_LINK_INPUT_TYPE_SINK, true); } bool gsr_pipewire_audio_add_link_from_sources_to_sink(gsr_pipewire_audio *self, const char **source_names, int num_source_names, const char *sink_name_input) { - return gsr_pipewire_audio_add_link_from_apps_to_output(self, source_names, num_source_names, sink_name_input, GSR_PIPEWIRE_AUDIO_NODE_TYPE_SINK_OR_SOURCE, GSR_PIPEWIRE_AUDIO_LINK_INPUT_TYPE_SINK, false); + return gsr_pipewire_audio_add_links_to_output(self, source_names, num_source_names, sink_name_input, GSR_PIPEWIRE_AUDIO_NODE_TYPE_SINK_OR_SOURCE, GSR_PIPEWIRE_AUDIO_LINK_INPUT_TYPE_SINK, false); } void gsr_pipewire_audio_for_each_app(gsr_pipewire_audio *self, gsr_pipewire_audio_app_query_callback callback, void *userdata) { |