2022-06-04 21:51:45 +02:00
|
|
|
;;; pw-lib.el --- PipeWire library -*- lexical-binding: t -*-
|
|
|
|
|
|
|
|
;; Copyright (C) 2022 Milan Zamazal <pdm@zamazal.org>
|
|
|
|
|
|
|
|
;; COPYRIGHT NOTICE
|
|
|
|
;;
|
|
|
|
;; This program is free software: you can redistribute it and/or modify
|
|
|
|
;; it under the terms of the GNU General Public License as published by
|
|
|
|
;; the Free Software Foundation, either version 3 of the License, or
|
|
|
|
;; (at your option) any later version.
|
|
|
|
;;
|
|
|
|
;; This program is distributed in the hope that it will be useful,
|
|
|
|
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
;; GNU General Public License for more details.
|
|
|
|
;;
|
|
|
|
;; You should have received a copy of the GNU General Public License
|
|
|
|
;; along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
|
|
|
;; Backend-independent library to access PipeWire functionality.
|
|
|
|
;; It abstracts data returned from `pw-access' methods and provides
|
|
|
|
;; functions to work with them.
|
|
|
|
;;
|
|
|
|
;; pw-lib caches data retrieved from PipeWire and uses the cached
|
|
|
|
;; data. If The cache can be invalidated by calling `pw-lib-refresh'.
|
|
|
|
|
|
|
|
(require 'cl-lib)
|
|
|
|
(require 'pw-access)
|
|
|
|
|
|
|
|
(defvar pw-lib--accessor (pw-cli-accessor))
|
|
|
|
|
|
|
|
(defvar pw-lib--objects '())
|
|
|
|
|
|
|
|
(defun pw-lib-refresh ()
|
|
|
|
"Clear cache of objects retrieved from PipeWire."
|
|
|
|
(setq pw-lib--objects (pw-access-objects pw-lib--accessor)))
|
|
|
|
|
|
|
|
(defun pw-lib-objects (&optional type)
|
|
|
|
"Return a list of PipeWire objects.
|
|
|
|
TYPE is a string identifying PipeWire objects types (e.g. \"Device\",
|
|
|
|
\"Node\", \"Port\", \"Client\", ...). If specified, return only
|
|
|
|
objects of the given type.
|
|
|
|
The format of the list elements is unspecified, use pw-lib functions
|
|
|
|
to access their data.
|
|
|
|
Note that PipeWire data is cached, if you need its up-to-date
|
|
|
|
version, call `pw-lib-refresh' first."
|
|
|
|
(unless pw-lib--objects
|
|
|
|
(pw-lib-refresh))
|
|
|
|
(let ((objects pw-lib--objects))
|
|
|
|
(when type
|
|
|
|
(setq objects (cl-remove-if-not
|
|
|
|
#'(lambda (o) (string= (cdr (assq 'type (cdr o))) type))
|
|
|
|
objects)))
|
|
|
|
objects))
|
|
|
|
|
|
|
|
(defun pw-lib-get-object (id)
|
|
|
|
"Return PipeWire object identified by ID.
|
|
|
|
If such an object doesn't exist, return nil.
|
|
|
|
Note that PipeWire data is cached, if you need its up-to-date
|
|
|
|
version, call `pw-lib-refresh' first."
|
|
|
|
(assoc id pw-lib--objects))
|
|
|
|
|
|
|
|
(defun pw-lib-object-id (object)
|
|
|
|
"Return id of the given PipeWire OBJECT."
|
|
|
|
(car object))
|
|
|
|
|
|
|
|
(defun pw-lib--object-info (object)
|
|
|
|
(cdr object))
|
|
|
|
|
|
|
|
(defun pw-lib-object-value (object key &optional default)
|
|
|
|
"Return PipeWire OBJECT value identified by KEY.
|
|
|
|
KEY is a string corresponding to a PipeWire value identifier.
|
|
|
|
If the given KEY doesn't exist in OBJECT, return DEFAULT."
|
|
|
|
(or (cdr (assoc key (pw-lib--object-info object)))
|
|
|
|
default))
|
|
|
|
|
|
|
|
(defun pw-lib-object-type (object)
|
|
|
|
"Return PipeWire type of OBJECT as a string.
|
|
|
|
E.g. \"Device\", \"Node\", \"Port\", \"Client\", ..."
|
|
|
|
(pw-lib-object-value object 'type))
|
|
|
|
|
|
|
|
(defun pw-lib--node (object)
|
|
|
|
(if (equal (pw-lib-object-type object) "Node")
|
|
|
|
object
|
|
|
|
(pw-lib-get-object (pw-lib-object-value object "node.id"))))
|
|
|
|
|
2022-06-05 11:42:10 +02:00
|
|
|
(defun pw-lib--node-parameters (object-or-id &optional refresh)
|
2022-06-04 21:51:45 +02:00
|
|
|
(let* ((object (if (numberp object-or-id)
|
|
|
|
(pw-lib-get-object object-or-id)
|
|
|
|
object-or-id))
|
|
|
|
(node (pw-lib--node object))
|
|
|
|
(parameters (pw-lib-object-value node 'parameters)))
|
2022-06-05 11:42:10 +02:00
|
|
|
(when (or refresh (not parameters))
|
2022-06-04 21:51:45 +02:00
|
|
|
(setq parameters (pw-access-properties pw-lib--accessor (pw-lib-object-id node)))
|
|
|
|
(setcdr node (cons (cons 'parameters parameters)
|
2022-06-05 11:42:10 +02:00
|
|
|
(assq-delete-all 'parameters (cdr node)))))
|
2022-06-04 21:51:45 +02:00
|
|
|
parameters))
|
|
|
|
|
|
|
|
(defun pw-lib-default-nodes ()
|
|
|
|
"Return assignments of PipeWire Nodes to default sinks and sources.
|
|
|
|
An association lists with elements of the form (KEY . ID) is
|
|
|
|
returned, where KEY is a string identifying the given kind of
|
|
|
|
default sink or source as reported by PipeWire and ID is the
|
|
|
|
corresponding PipeWire node numeric id.
|
|
|
|
Note that PipeWire data is cached, if you need its up-to-date
|
|
|
|
version, call `pw-lib-refresh' first."
|
|
|
|
(let ((defaults (pw-access-defaults pw-lib--accessor))
|
|
|
|
(nodes (mapcar #'(lambda (o)
|
|
|
|
(cons (pw-lib-object-value o "node.name") (pw-lib-object-id o)))
|
|
|
|
(pw-lib-objects "Node"))))
|
|
|
|
(cl-remove-if-not #'cdr
|
|
|
|
(mapcar #'(lambda (d)
|
|
|
|
(cons (car d) (cdr (assoc (cdr d) nodes))))
|
|
|
|
defaults))))
|
|
|
|
|
|
|
|
(defun pw-lib-bindings ()
|
|
|
|
"Return bindings between PipeWire objects.
|
|
|
|
An association lists with elements of the form (PARENT . CHILD) is
|
|
|
|
returned where PARENT and CHILD are numeric ids of PipeWire objects.
|
|
|
|
Note that PipeWire data is cached, if you need its up-to-date
|
|
|
|
version, call `pw-lib-refresh' first."
|
|
|
|
(apply #'nconc (mapcar #'(lambda (o)
|
|
|
|
(let ((o-id (pw-lib-object-id o)))
|
|
|
|
(mapcar #'(lambda (p)
|
|
|
|
(cons o-id (cdr p)))
|
|
|
|
(cl-remove-if-not #'numberp (pw-lib--object-info o)
|
|
|
|
:key #'cdr))))
|
|
|
|
(pw-lib-objects))))
|
|
|
|
|
|
|
|
(defun pw-lib-children (id bindings &optional type)
|
|
|
|
"Return child objects of the object identified by numeric PipeWire ID.
|
|
|
|
BINDINGS are object bindings as returned from `pw-lib-bindings'.
|
|
|
|
If a string TYPE is specified then only children of the given PipeWire
|
|
|
|
type are returned.
|
|
|
|
Note that PipeWire data is cached, if you need its up-to-date
|
|
|
|
version, call `pw-lib-refresh' first."
|
|
|
|
(let ((children (mapcar #'pw-lib-get-object
|
|
|
|
(mapcar #'car (cl-remove-if #'(lambda (b) (/= (cdr b) id)) bindings)))))
|
|
|
|
(when type
|
|
|
|
(setq children (cl-remove-if-not #'(lambda (o) (equal (pw-lib-object-type o) type))
|
|
|
|
children)))
|
|
|
|
children))
|
|
|
|
|
|
|
|
(defun pw-lib-default-audio-sink ()
|
|
|
|
"Return a PipeWire object that is the current default audio sink."
|
|
|
|
(pw-lib-get-object (cdr (assoc "default.audio.sink" (pw-lib-default-nodes)))))
|
|
|
|
|
|
|
|
(defun pw-lib--volume-% (volume)
|
|
|
|
(when volume
|
|
|
|
(round (* 100 volume))))
|
|
|
|
|
|
|
|
(defun pw-lib--volume-float (volume)
|
|
|
|
(/ (float volume) 100))
|
|
|
|
|
2022-06-05 11:42:10 +02:00
|
|
|
(defun pw-lib--object-parameters (object &optional refresh)
|
2022-06-04 21:51:45 +02:00
|
|
|
(let* ((node-p (equal (pw-lib-object-type object) "Node"))
|
2022-06-05 11:42:10 +02:00
|
|
|
(parameters (pw-lib--node-parameters object refresh))
|
2022-06-04 21:51:45 +02:00
|
|
|
(monitor-p (unless node-p
|
|
|
|
(equal (pw-lib-object-value object "port.monitor") "true")))
|
|
|
|
(node-id (pw-lib-object-id (pw-lib--node object)))
|
|
|
|
(port-id (unless node-p
|
|
|
|
(pw-lib-object-value object "port.id"))))
|
|
|
|
(list node-p parameters monitor-p node-id port-id)))
|
|
|
|
|
2022-06-05 11:42:10 +02:00
|
|
|
(defun pw-lib-muted-p (object &optional refresh)
|
2022-06-04 21:51:45 +02:00
|
|
|
"Return whether the given PipeWire object is muted.
|
|
|
|
Applicable only to Nodes and Ports.
|
2022-06-05 11:42:10 +02:00
|
|
|
If REFRESH is non-nil then retrive fresh information from PipeWire
|
|
|
|
rather than using cached data to obtain the result."
|
2022-06-04 21:51:45 +02:00
|
|
|
(cl-destructuring-bind (node-p parameters monitor-p node-id port-id)
|
2022-06-05 11:42:10 +02:00
|
|
|
(pw-lib--object-parameters object refresh)
|
2022-06-04 21:51:45 +02:00
|
|
|
(eq (cdr (assoc (if monitor-p "monitorMute" "mute") parameters)) 'true)))
|
|
|
|
|
2022-06-05 11:42:10 +02:00
|
|
|
(defun pw-lib-toggle-mute (object &optional refresh)
|
2022-06-04 21:51:45 +02:00
|
|
|
"Toggle mute status of the given PipeWire OBJECT.
|
|
|
|
Return the new boolean mute status of OBJECT.
|
|
|
|
Applicable only to Nodes and Ports.
|
2022-06-05 11:42:10 +02:00
|
|
|
If REFRESH is non-nil then retrive fresh information from PipeWire
|
|
|
|
rather than using cached data to obtain the result."
|
2022-06-04 21:51:45 +02:00
|
|
|
(cl-destructuring-bind (node-p parameters monitor-p node-id port-id)
|
2022-06-05 11:42:10 +02:00
|
|
|
(pw-lib--object-parameters object refresh)
|
2022-06-04 21:51:45 +02:00
|
|
|
(let* ((mute (not (pw-lib-muted-p object)))
|
|
|
|
(property (if monitor-p "monitorMute" "mute"))
|
|
|
|
(value (if mute "true" "false")))
|
|
|
|
(pw-access-set-properties pw-lib--accessor node-id (list (cons property value)))
|
|
|
|
mute)))
|
|
|
|
|
2022-06-05 11:42:10 +02:00
|
|
|
(defun pw-lib-volume (object &optional refresh)
|
2022-06-04 21:51:45 +02:00
|
|
|
"Return volume of the given PipeWire object.
|
|
|
|
The returned value is an integer in the range 0-100.
|
|
|
|
Applicable only to Nodes and Ports.
|
2022-06-05 11:42:10 +02:00
|
|
|
If REFRESH is non-nil then retrive fresh information from PipeWire
|
|
|
|
rather than using cached data to obtain the result."
|
2022-06-04 21:51:45 +02:00
|
|
|
(cl-destructuring-bind (node-p parameters monitor-p node-id port-id)
|
2022-06-05 11:42:10 +02:00
|
|
|
(pw-lib--object-parameters object refresh)
|
2022-06-04 21:51:45 +02:00
|
|
|
(pw-lib--volume-%
|
|
|
|
(if node-p
|
|
|
|
(cdr (assoc "volume" parameters))
|
|
|
|
(nth port-id (cdr (assoc (if monitor-p "monitorVolumes" "channelVolumes") parameters)))))))
|
|
|
|
|
|
|
|
(defun pw-lib-set-volume (volume object)
|
|
|
|
"Set the volume of PipeWire OBJECT to VOLUME.
|
|
|
|
VOLUME must be an integer in the range 0-100."
|
|
|
|
(cl-destructuring-bind (node-p parameters monitor-p node-id port-id)
|
|
|
|
(pw-lib--object-parameters object)
|
|
|
|
(let* ((property (cond
|
|
|
|
(node-p "volume")
|
|
|
|
(monitor-p "monitorVolumes")
|
|
|
|
(t "channelVolumes")))
|
|
|
|
(float-volume (pw-lib--volume-float volume))
|
|
|
|
(value (if node-p
|
|
|
|
float-volume
|
|
|
|
(let ((orig-value (cdr (assoc property parameters))))
|
|
|
|
(cl-substitute float-volume nil orig-value
|
|
|
|
:test #'always :start port-id :count 1)))))
|
|
|
|
(pw-access-set-properties pw-lib--accessor node-id (list (cons property value))))))
|
|
|
|
|
|
|
|
(defun pw-lib-set-default (object stored-p)
|
|
|
|
"Set PipeWire OBJECT as the default sink or source.
|
|
|
|
If STORED-P is non-nil, set the stored default sink or source,
|
|
|
|
otherwise set the current default sink or source."
|
|
|
|
(let ((suffix (mapconcat #'downcase
|
|
|
|
(split-string (pw-lib-object-value object "media.class") "/")
|
|
|
|
"."))
|
|
|
|
(prefix (if stored-p "default.configured." "default."))
|
|
|
|
(node-name (pw-lib-object-value object "node.name")))
|
|
|
|
(pw-access-set-default pw-lib--accessor (concat prefix suffix) node-name)))
|
|
|
|
|
|
|
|
(provide 'pw-lib)
|