;;; pw-lib.el --- PipeWire library -*- lexical-binding: t -*- ;; Copyright (C) 2022 Milan Zamazal ;; 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 . ;; 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 '()) (defvar pw-lib--bindings nil) (defun pw-lib-refresh () "Clear cache of objects retrieved from PipeWire." (setq pw-lib--objects (pw-access-objects pw-lib--accessor) pw-lib--bindings nil)) (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")))) (defun pw-lib--node-parameters (object-or-id &optional refresh) (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))) (when (or refresh (not parameters)) (setq parameters (pw-access-properties pw-lib--accessor (pw-lib-object-id node))) (setcdr node (cons (cons 'parameters parameters) (assq-delete-all 'parameters (cdr node))))) 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." (or pw-lib--bindings (setq pw-lib--bindings (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 &optional type) "Return child objects of the object identified by numeric PipeWire ID. 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)) (pw-lib-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-default-playback-ports () "Return list of PipeWire objects that are default playback ports." (if-let ((sink (pw-lib-default-audio-sink))) (cl-remove-if-not #'(lambda (o) (if-let ((name (pw-lib-object-value o "port.name"))) (string-match "^playback" name))) (pw-lib-children (pw-lib-object-id sink) "Port")))) (defun pw-lib--volume-% (volume) (when volume (round (* 100 volume)))) (defun pw-lib--volume-float (volume) (/ (float volume) 100)) (defun pw-lib--object-parameters (object &optional refresh) (let* ((node-p (equal (pw-lib-object-type object) "Node")) (parameters (pw-lib--node-parameters object refresh)) (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))) (defun pw-lib-muted-p (object &optional refresh) "Return whether the given PipeWire object is muted. Applicable only to Nodes and Ports. If REFRESH is non-nil then retrive fresh information from PipeWire rather than using cached data to obtain the result." (cl-destructuring-bind (node-p parameters monitor-p node-id port-id) (pw-lib--object-parameters object refresh) (eq (cdr (assoc (if monitor-p "monitorMute" "mute") parameters)) 'true))) (defun pw-lib-toggle-mute (object &optional refresh) "Toggle mute status of the given PipeWire OBJECT. Return the new boolean mute status of OBJECT. Applicable only to Nodes and Ports. If REFRESH is non-nil then retrive fresh information from PipeWire rather than using cached data to obtain the result." (cl-destructuring-bind (node-p parameters monitor-p node-id port-id) (pw-lib--object-parameters object refresh) (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))) (defun pw-lib-volume (object &optional refresh) "Return volume of the given PipeWire object. The returned value is an integer in the range 0-100. Applicable only to Nodes and Ports. If REFRESH is non-nil then retrive fresh information from PipeWire rather than using cached data to obtain the result." (cl-destructuring-bind (node-p parameters monitor-p node-id port-id) (pw-lib--object-parameters object refresh) (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 &optional single-p) "Set the volume of PipeWire OBJECT to VOLUME. VOLUME must be an integer in the range 0-100. If SINGLE-P is non-nil, set the volume only for a single channel, otherwise set the volume to the same value for all the related channels." (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)))) (if single-p (cl-substitute float-volume nil orig-value :test #'always :start port-id :count 1) (make-list (length orig-value) float-volume)))))) (pw-access-set-properties pw-lib--accessor node-id (list (cons property value)))))) (defun pw-lib--set-default-node (object stored-p) (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))) (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." (pcase (pw-lib-object-type object) ("Device" (dolist (node (pw-lib-children (pw-lib-object-id object) "Node")) (pw-lib--set-default-node node stored-p))) ("Node" (pw-lib--set-default-node object stored-p)) (_ (error "Cannot set this kind of object as default.")))) (provide 'pw-lib)