From 2c421bb990ecaced36a4dd2f0b85fcdffbf5aff9 Mon Sep 17 00:00:00 2001 From: Milan Zamazal Date: Wed, 15 Jun 2022 21:31:24 +0200 Subject: [PATCH] Add support for device profiles This is especially useful with Bluetooth devices. --- pw-access.el | 48 +++++++++++++++++++++++++++++++++++++++++++++--- pw-lib.el | 30 ++++++++++++++++++++++++++++++ pw-ui.el | 18 ++++++++++++++++++ 3 files changed, 93 insertions(+), 3 deletions(-) diff --git a/pw-access.el b/pw-access.el index 3d23175..732cfa2 100644 --- a/pw-access.el +++ b/pw-access.el @@ -73,6 +73,30 @@ PROPERTIES is an association list in the same format as in `pw-access-properties'. It needn't contain all the properties, just the properties to be changed.") +(cl-defgeneric pw-access-current-profile (class device-id) + "Return current profile of the given device. +DEVICE-ID is a numeric PipeWire Device id (other kinds of PipeWire +objects are not supported in this method). + +The profile is an association list with elements of the form +(PROPERTY . VALUE), in the same format as properties in +`pw-access-properties'.") + +(cl-defgeneric pw-access-profiles (class device-id) + "Return available profiles of the given device. +DEVICE-ID is a numeric PipeWire Device id (other kinds of PipeWire +objects are not supported in this method). + +Return a list of profiles, which are in the same format as in +`pw-access-current-profile'.") + +(cl-defgeneric pw-access-set-profile (class device-id profile-index) + "Set the profile of the given device. +DEVICE-ID is a numeric PipeWire Device id (other kinds of PipeWire +objects are not supported in this method). +PROFILE-INDEX is a numeric index of the profile to set, as returned +from PipeWire.") + (cl-defgeneric pw-access-defaults (class) "Return default sinks and sources. An association lists is returned. Each list element is of the form @@ -183,11 +207,29 @@ Note this interface may not work with all PipeWire versions.") (defun pw-cli--format-property (property) (format "%s: %s" (car property) (pw-cli--format-property-value (cdr property)))) - (let* ((formatted (mapconcat #'pw-cli--format-property properties ", ")) - (props (concat "{ " formatted " }"))) +(defun pw-cli--set-parameter (object-id parameter value) + (let* ((formatted (mapconcat #'pw-cli--format-property value ", ")) + (param-value (concat "{ " formatted " }"))) (call-process pw-cli-command nil pw-cli-command nil - "set-param" (number-to-string node-id) "Props" props))) + "set-param" (number-to-string object-id) parameter param-value))) + (cl-defmethod pw-access-set-properties ((_class pw-cli-accessor) node-id properties) + (pw-cli--set-parameter node-id "Props" properties)) + +(cl-defmethod pw-access-current-profile ((_class pw-cli-accessor) device-id) + (with-temp-buffer + (pw-cli--command pw-cli-command `("enum-params" ,(number-to-string device-id) "Profile")) + (pw-cli--parse-properties))) + +(cl-defmethod pw-access-profiles ((_class pw-cli-accessor) device-id) + (with-temp-buffer + (pw-cli--command pw-cli-command `("enum-params" ,(number-to-string device-id) "EnumProfile")) + (cl-loop for profile = (pw-cli--parse-properties) then (pw-cli--parse-properties) + while profile + collect profile))) + +(cl-defmethod pw-access-set-profile ((_class pw-cli-accessor) device-id profile-index) + (pw-cli--set-parameter device-id "Profile" `(("index" . ,profile-index) ("save" . "true")))) (defun pw-cli--parse-metadata () (let ((metadata '())) diff --git a/pw-lib.el b/pw-lib.el index ddbbc54..51b7e77 100644 --- a/pw-lib.el +++ b/pw-lib.el @@ -81,6 +81,36 @@ If the given KEY doesn't exist in OBJECT, return DEFAULT." E.g. \"Device\", \"Node\", \"Port\", \"Client\", ..." (pw-lib-object-value object 'type)) +(defun pw-lib--profile-name (profile) + (cdr (or (assoc "description" profile) + (assoc "name" profile)))) + +(defun pw-lib-current-profile (device-id) + "Return the current profile name of the given device. +DEVICE-ID is the numeric id of the device. +The returned profile name is a string, or nil if it cannot be found." + (pw-lib--profile-name (pw-access-current-profile pw-lib--accessor device-id))) + +(defun pw-lib-profiles (device-id) + "Return list of available profiles of the given device. +DEVICE-ID is the numeric id of the device. +A list of strings (possibly empty) is returned." + (mapcar #'pw-lib--profile-name (pw-access-profiles pw-lib--accessor device-id))) + +(defun pw-lib-set-profile (device-id profile) + "Set the profile of the given device. +DEVICE-ID is the numeric id of the device. +PROFILE is a string name of the profile, it must be one of the values +returned from `pw-lib-profiles'. " + (let* ((all-profiles (pw-access-profiles pw-lib--accessor device-id)) + (properties (cl-find profile all-profiles :key #'pw-lib--profile-name :test #'equal))) + (unless properties + (error "Profile %s of device %s not found" profile device-id)) + (let ((index (cdr (assoc "index" properties)))) + (unless index + (error "Index of %s profile of device %s not found" profile device-id)) + (pw-access-set-profile pw-lib--accessor device-id index)))) + (defun pw-lib--node (object) (if (equal (pw-lib-object-type object) "Node") object diff --git a/pw-ui.el b/pw-ui.el index 09a27a2..50238ba 100644 --- a/pw-ui.el +++ b/pw-ui.el @@ -110,10 +110,14 @@ The indicator is displayed only on graphical terminals." (let* ((id (pw-lib-object-id object)) (type (pw-lib-object-type object)) (text (format "%4s: %s" id (pw-ui--object-name object))) + (profile (when (equal type "Device") + (pw-lib-current-profile (pw-lib-object-id object)))) (face (if (member id default-ids) 'pipewire-default-object-face 'default)) (media-class (pw-lib-object-value object "media.class"))) (when media-class (setq text (format "%s (%s)" text media-class))) + (when profile + (setq text (format "%s: %s" text profile))) (let ((volume-p (member type '("Node" "Port")))) (when (and volume-p (pw-lib-muted-p object)) (setq face `(:inherit (pipewire-muted-face ,face)))) @@ -337,10 +341,24 @@ Otherwise ask for the Node to set as the default Node." (pw-lib-set-default object t) (pw-ui--update))) +(defun pipewire-set-profile () + "Set profile of the device at the current point." + (interactive) + (if-let ((device (pw-ui--current-object nil '("Device"))) + (device-id (pw-lib-object-id device)) + (profiles (pw-lib-profiles device-id))) + (progn + (pw-lib-set-profile device-id (completing-read "Select profile: " profiles nil t)) + ;; Without this, ports of the device may not be displayed on the update: + (sit-for 0) + (pw-ui--update)) + (error "Nothing to set a profile for here"))) + (defvar pipewire-mode-map (let ((map (make-sparse-keymap))) (define-key map "d" 'pipewire-set-default) (define-key map "m" 'pipewire-toggle-muted) + (define-key map "p" 'pipewire-set-profile) (define-key map "v" 'pipewire-set-volume) (define-key map "=" 'pipewire-increase-volume) (define-key map "-" 'pipewire-decrease-volume)