Compare commits

...

35 Commits

Author SHA1 Message Date
ae7a95230f README: Add info about MELPA 2022-07-26 20:00:57 +02:00
115a8a89a3 Use user-error rather than ‘error’ in interactive functions 2022-07-25 20:58:07 +02:00
8109f43855 Toggle microphone: Better error message if there is no default input
Instead of an Elisp type error.
2022-07-16 21:33:36 +02:00
a81c61ab0a Expand commentary in pipewire.el
This is what would be displayed in MELPA package description.
2022-07-16 21:25:20 +02:00
c23524cd30 Take pipewire-cli out of pipewire-access.el
This should resolve the remaining issues with name prefixes.
2022-07-16 21:03:29 +02:00
e032234c63 Fix typo in pipewire-lib commentary 2022-07-16 20:53:02 +02:00
78c8c6e083 Use ‘pipewire-’ prefix everywhere
This is necessary for MELPA.
2022-07-16 20:53:00 +02:00
33fbb2ba1a Rephrase docstrings starting with lower cases to stop checkdoc complaints 2022-07-16 20:12:05 +02:00
07e8456ce4 Rename pipewire-zero-pkg.el to pipewire-zero.el
And replace the package definition with a simple ‘require’.
This should be enough for MELPA.
2022-07-12 21:38:07 +02:00
4ce4bbda10 Simplify a bit a condition in pipewire--current-object 2022-07-12 21:32:56 +02:00
cc8fa0336e Document CLASS cl-defgeneric arguments
This is needed to make checkdoc happy.  The wording is not very good
because checkdoc insists on using one of the predefined words before
`pw-accessor’.
2022-07-12 21:27:19 +02:00
7ec78e2caf Rename pw-ui.el to pipewire.el
This makes all the prefixes in the file the same and checkdoc is
happy about it.
2022-07-12 21:15:58 +02:00
562b910aa0 Silence checkdoc messages about docstrings of internal objects
Internal objects here don’t have and shouldn’t have documentation
strings because:

- There is nothing useful to specify in them.
- They are not a stable API to be used.
- They would pollute the source file and make it larger.
2022-07-06 10:20:27 +02:00
119aef1b08 Move Code: labels before the beginning of code
From the places where they were inserted by checkdoc.
2022-07-05 13:30:30 +02:00
91888abb91 pw-lib-set-default: Remove ending dot in the error message 2022-07-04 19:17:02 +02:00
22d02e5b7f Document single-p argument in docstrings 2022-07-04 19:16:33 +02:00
dd4c2728a5 Mistakes in docstrings corrected 2022-07-04 19:16:12 +02:00
2b53b19c05 Add useless “Code” headers to make checkdoc happier 2022-07-04 19:15:13 +02:00
3f56c8fc0d Use Version instead of Package-Version in package headers 2022-07-04 19:04:42 +02:00
e9273dcf09 New command pipewire-properties 2022-07-02 09:43:54 +02:00
5047d562c4 Remove trailing spaces 2022-07-02 09:30:44 +02:00
b54e3e4af6 Display nick as the object name if available 2022-07-02 09:18:09 +02:00
55977bbc70 Use pipewire-zero as Elisp package name
To avoid the problems with a numeric suffix.
2022-06-29 22:09:39 +02:00
4c19a7b130 Rename the pkg file to pipewire-pkg.el
This is what package-install looks for, it apparently strips the
numeric suffix.
2022-06-29 22:03:44 +02:00
be0e9ce3a8 Add file headers
To make package-lint happy.
2022-06-29 22:00:24 +02:00
cb40da6d96 Improve the pkg file 2022-06-29 22:00:23 +02:00
271f9d5ed2 Add “ends here” footers
They are useless but package-lint requires them.
2022-06-29 21:55:53 +02:00
bac34b4e8f Specify a parent customization group 2022-06-28 22:24:20 +02:00
bff6a566e2 Add a package file 2022-06-28 22:24:20 +02:00
7695f21b18 Add debug declaration to pw-ui--osd macro 2022-06-28 22:24:20 +02:00
9425f7f359 Don’t prefix lambdas with #' 2022-06-28 22:24:20 +02:00
c31950cb30 Don’t use -face suffix for faces 2022-06-28 22:24:20 +02:00
bcf5c5e3d4 Add “;;; Commentary:” labels 2022-06-28 22:24:20 +02:00
57fe580df2 Escape initial parentheses in docstrings 2022-06-28 22:24:20 +02:00
66b49c4e37 Don’t err in pw-ui--update-muted when called on a node 2022-06-28 22:24:20 +02:00
8 changed files with 1120 additions and 983 deletions

View File

@ -18,14 +18,9 @@ discuss anything.
pipewire-0 currently requires presence of PipeWire command line tools,
namely [[https://docs.pipewire.org/page_man_pw_cli_1.html][pw-cli]] and [[https://docs.pipewire.org/page_man_pw_metadata_1.html][pw-metadata]].
To install pipewire-0, put the *.el files to a site-lisp directory and
add the following line to your Emacs configuration:
#+begin_src elisp
(require pw-ui)
#+end_src
Or if you use [[https://github.com/radian-software/straight.el][straight.el]], you can install pipewire-0 as follows:
You can install pipewire-0 from [[https://melpa.org/#/pipewire][MELPA]]. If you use [[https://github.com/radian-software/straight.el][straight.el]] and
prefer using source repos directly, you can install pipewire-0 as
follows:
#+begin_src elisp
(straight-use-package
@ -34,6 +29,13 @@ Or if you use [[https://github.com/radian-software/straight.el][straight.el]], y
:local-repo "pipewire-0"))
#+end_src
To install pipewire-0 manually, put the *.el files to a site-lisp
directory and add the following line to your Emacs configuration:
#+begin_src elisp
(require pipewire)
#+end_src
** User interface
=M-x pipewire= enters a buffer with PipeWire objects:
@ -68,20 +70,20 @@ They can be bound to multimedia keys:
pipewire-0 consists of the following source files:
- [[file:pw-lib.el][pw-lib.el]] :: PipeWire library to be used in Elisp programs.
- [[file:pw-access.el][pw-access.el]] :: PipeWire communication interface, not supposed to be
used outside =pw-lib=.
- [[file:pw-ui.el][pw-ui.el]] :: User commands and interface.
- [[file:pipewire-lib.el][pipewire-lib.el]] :: PipeWire library to be used in Elisp programs.
- [[file:pipewire-access.el][pipewire-access.el]] :: PipeWire communication interface, not supposed to be
used outside =pipewire-lib=.
- [[file:pipewire.el][pipewire.el]] :: User commands and interface.
Look into [[file:pw-lib.el][pw-lib.el]] to see what public =pw-lib-*= functions are
Look into [[file:pipewire-lib.el][pipewire-lib.el]] to see what public =pipewire-lib-*= functions are
available there. For example, the following snippet can be used to
display current volume level of the default audio sink:
#+begin_src elisp
(let ((object (pw-lib-default-audio-sink)))
(let ((object (pipewire-lib-default-audio-sink)))
(format "%s%s"
(pw-lib-volume object t)
(if (pw-lib-muted-p object) "(M)" "")))
(pipewire-lib-volume object t)
(if (pipewire-lib-muted-p object) "(M)" "")))
#+end_src
* Notes
@ -90,7 +92,7 @@ PipeWire is currently accessed using [[https://docs.pipewire.org/page_man_pw_cli
pw-cli output is apparently undocumented and changes between versions
so this is not a reliable way to communicate with PipeWire. But I
dont know about anything better currently. Nevertheless, its easy
to replace pw-cli with something else in [[file:pw-access.el][pw-access.el]].
to replace pw-cli with something else in [[file:pipewire-access.el][pipewire-access.el]].
** Why is it named pipewire-0?

132
pipewire-access.el Normal file
View File

@ -0,0 +1,132 @@
;;; pipewire-access.el --- PipeWire generic access -*- lexical-binding: t -*-
;; Copyright (C) 2022 Milan Zamazal <pdm@zamazal.org>
;; Author: Milan Zamazal <pdm@zamazal.org>
;; Version: 1
;; Package-Requires: ((emacs "25.1"))
;; Keywords: multimedia
;; URL: https://git.zamazal.org/pdm/pipewire-0
;; 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/>.
;;; Commentary:
;;
;; A generic interface for communication with PipeWire (https://pipewire.org).
;; It abstracts communication with PipeWire to be backend independent.
;; Only functions from this module may communicate with PipeWire.
;;; Code:
(require 'eieio)
(defclass pipewire-accessor ()
()
:documentation
"Base PipeWire interface class.
All PipeWire interfaces should derive from this class.")
(cl-defgeneric pipewire-access-objects (class)
"Return all the objects currently reported by PipeWire.
It is a list of object data. Each of the elements has a form
\(OBJECT-ID . INFO) where OBJECT-ID is a numeric OBJECT-ID as
reported by PipeWire and INFO is an association list of items
\(NAME . VALUE) where NAME is a string item name as reported by
PipeWire and VALUE is the corresponding value. VALUE is a number for
object ids, a string otherwise.
A special entry with `type' symbol as its name contains the PipeWire
type of the objects, as a string (e.g. \"Device\", \"Node\", \"Port\",
\"Client\", ...).
CLASS is a PipeWire interface, see symbol `pipewire-accessor'.")
(cl-defgeneric pipewire-access-properties (class node-id)
"Return properties of the given node.
NODE-ID is a numeric PipeWire Node id (other kinds of PipeWire objects
are not supported in this method).
Object properties may be, unlike object info items, settable.
An assocation list is returned. Each list element is of the form
\(PROPERTY . VALUE) where PROPERTY is a string name of the given
property. VALUE can be:
- \"true\" or \"false\" for boolean values (t and nil are not used to
avoid confusion with nil representing invalid or unavailable value).
- A number for numeric values (ids, integers, floats).
- A string for string values.
- A list of elements of any of these types for arrays and structs.
CLASS is a PipeWire interface, see symbol `pipewire-accessor'.")
(cl-defgeneric pipewire-access-set-properties (class node-id properties)
"Set PROPERTIES of the given node.
NODE-ID is a numeric PipeWire Node id (other kinds of PipeWire objects
are not supported in this method).
PROPERTIES is an association list in the same format as in
`pipewire-access-properties'. It needn't contain all the properties, just
the properties to be changed.
CLASS is a PipeWire interface, see symbol `pipewire-accessor'.")
(cl-defgeneric pipewire-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
`pipewire-access-properties'.
CLASS is a PipeWire interface, see symbol `pipewire-accessor'.")
(cl-defgeneric pipewire-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
`pipewire-access-current-profile'.
CLASS is a PipeWire interface, see symbol `pipewire-accessor'.")
(cl-defgeneric pipewire-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.
CLASS is a PipeWire interface, see symbol `pipewire-accessor'.")
(cl-defgeneric pipewire-access-defaults (class)
"Return default sinks and sources.
An association lists is returned. Each list element is of the form
\(KEY . NAME) where KEY is a string identifying the given kind of
default sink or source as reported by PipeWire and NAME is a string
name of the node assigned to the default.
CLASS is a PipeWire interface, see symbol `pipewire-accessor'.")
(cl-defgeneric pipewire-access-set-default (class key node-name)
"Set default sink or source.
KEY is a string identifying the given kind of default sink or source
as reported in `pipewire-access-defaults' and NODE-NAME is a string name of
the node that should be assigned to KEY.
CLASS is a PipeWire interface, see symbol `pipewire-accessor'.")
(provide 'pipewire-access)
;; Local Variables:
;; checkdoc-force-docstrings-flag: nil
;; End:
;;; pipewire-access.el ends here

185
pipewire-cli.el Normal file
View File

@ -0,0 +1,185 @@
;;; pipewire-cli.el --- PipeWire command line access -*- lexical-binding: t -*-
;; Copyright (C) 2022 Milan Zamazal <pdm@zamazal.org>
;; Author: Milan Zamazal <pdm@zamazal.org>
;; Version: 1
;; Package-Requires: ((emacs "25.1"))
;; Keywords: multimedia
;; URL: https://git.zamazal.org/pdm/pipewire-0
;; 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/>.
;;; Commentary:
;;
;; Currently, pw-cli is used to talk to PipeWire. This is not optimal
;; because pw-cli doesn't seem to have documented output format and
;; the format changes accross PipeWire versions. But there seem to be
;; no better options currently. This module should allow switching to
;; other communication means easily, without any changes needed
;; outside this module, except for using a different communication
;; class.
;;; Code:
(require 'eieio)
(require 'pipewire-access)
(defvar pipewire-cli-command "pw-cli"
"Command to invoke pw-cli.")
(defvar pipewire-cli-metadata-command "pw-metadata"
"Command to invoke pw-metadata.")
(defclass pipewire-cli-accessor (pipewire-accessor)
()
:documentation
"Command line based interface to PipeWire.
Note this interface may not work with all PipeWire versions.")
(defun pipewire-cli--command (command args)
(apply #'call-process command nil t nil args)
(goto-char (point-min)))
(defun pipewire-cli--next-line ()
(goto-char (line-beginning-position 2)))
(defun pipewire-cli--parse-list ()
(let ((objects '()))
(while (re-search-forward "^[\t]id \\([0-9]+\\), type PipeWire:Interface:\\(.*\\)/.*$" nil t)
(let ((id (string-to-number (match-string 1)))
(properties `((type . ,(match-string 2)))))
(pipewire-cli--next-line)
(while (looking-at "^ [\t][\t]\\([a-z.]+\\) = \"\\(.*\\)\"")
(let ((property (match-string 1))
(value (match-string 2)))
(when (string-suffix-p ".id" property)
(setq value (string-to-number value)))
(push (cons property value) properties))
(pipewire-cli--next-line))
(push (cons id properties) objects)))
(nreverse objects)))
(cl-defmethod pipewire-access-objects ((_class pipewire-cli-accessor))
(with-temp-buffer
(pipewire-cli--command pipewire-cli-command '("list-objects"))
(pipewire-cli--parse-list)))
(defun pipewire-cli--read-property (&optional nesting)
(unless nesting
(setq nesting 0))
(when (looking-at (concat (make-string (+ 6 (* 2 nesting)) ? )
"\\([A-Za-z:]+\\) \\(.*\\)"))
(let ((type (match-string 1))
(value (match-string 2)))
(pcase type
("Bool"
(if (equal value "true") 'true 'false))
((or "Float" "Id" "Int")
(string-to-number value))
("String"
(substring value 1 -1))
((or "Array:" "Struct:")
(let ((array '())
item)
(pipewire-cli--next-line)
(while (setq item (pipewire-cli--read-property (1+ nesting)))
(push item array)
(pipewire-cli--next-line))
(nreverse array)))))))
(defun pipewire-cli--parse-properties ()
(pipewire-cli--next-line)
(let ((end (or (save-excursion (re-search-forward "^ Object:" nil t))
(point-max)))
(properties '()))
(while (and (< (point) end)
(re-search-forward "^ Prop: key \\([A-Za-z:]+\\)" end t))
(pipewire-cli--next-line)
(let ((property (car (last (split-string (match-string 1) ":"))))
(value (pipewire-cli--read-property)))
(when value
(push (cons property value) properties))))
(goto-char end)
properties))
(cl-defmethod pipewire-access-properties ((_class pipewire-cli-accessor) node-id)
(with-temp-buffer
(pipewire-cli--command pipewire-cli-command `("enum-params" ,(number-to-string node-id) "Props"))
(pipewire-cli--parse-properties)))
(defun pipewire-cli--format-property-value (value)
(cond
((consp value)
(concat "[ " (mapconcat #'pipewire-cli--format-property-value value ", ") " ]"))
((numberp value)
(number-to-string value))
(t
value)))
(defun pipewire-cli--format-property (property)
(format "%s: %s" (car property) (pipewire-cli--format-property-value (cdr property))))
(defun pipewire-cli--format-properties (properties)
(concat "{ " (mapconcat #'pipewire-cli--format-property properties ", ") " }"))
(defun pipewire-cli--set-parameter (object-id parameter value)
(let* ((formatted (pipewire-cli--format-properties value)))
(call-process pipewire-cli-command nil pipewire-cli-command nil
"set-param" (number-to-string object-id) parameter formatted)))
(cl-defmethod pipewire-access-set-properties ((_class pipewire-cli-accessor) node-id properties)
(pipewire-cli--set-parameter node-id "Props" properties))
(cl-defmethod pipewire-access-current-profile ((_class pipewire-cli-accessor) device-id)
(with-temp-buffer
(pipewire-cli--command pipewire-cli-command `("enum-params" ,(number-to-string device-id) "Profile"))
(pipewire-cli--parse-properties)))
(cl-defmethod pipewire-access-profiles ((_class pipewire-cli-accessor) device-id)
(with-temp-buffer
(pipewire-cli--command pipewire-cli-command `("enum-params" ,(number-to-string device-id) "EnumProfile"))
(cl-loop for profile = (pipewire-cli--parse-properties) then (pipewire-cli--parse-properties)
while profile
collect profile)))
(cl-defmethod pipewire-access-set-profile ((_class pipewire-cli-accessor) device-id profile-index)
(pipewire-cli--set-parameter device-id "Profile" `(("index" . ,profile-index) ("save" . "true"))))
(defun pipewire-cli--parse-metadata ()
(let ((metadata '()))
(while (re-search-forward
"key:'\\([a-z.]+\\)'.*\\(value\\|\"name\"\\): ?['\"]\\([^'\"]+\\)['\"]"
nil t)
(push (cons (match-string 1) (match-string 3)) metadata))
metadata))
(cl-defmethod pipewire-access-defaults ((_class pipewire-cli-accessor))
(with-temp-buffer
(pipewire-cli--command pipewire-cli-metadata-command '("0"))
(pipewire-cli--parse-metadata)))
(cl-defmethod pipewire-access-set-default ((_class pipewire-cli-accessor) property node-name)
(call-process pipewire-cli-metadata-command nil pipewire-cli-metadata-command nil
"0" property (format "{ \"name\": \"%s\" }" node-name)))
(provide 'pipewire-cli)
;; Local Variables:
;; checkdoc-force-docstrings-flag: nil
;; End:
;;; pipewire-cli.el ends here

338
pipewire-lib.el Normal file
View File

@ -0,0 +1,338 @@
;;; pipewire-lib.el --- PipeWire library -*- lexical-binding: t -*-
;; Copyright (C) 2022 Milan Zamazal <pdm@zamazal.org>
;; Author: Milan Zamazal <pdm@zamazal.org>
;; Version: 1
;; Package-Requires: ((emacs "28.1"))
;; Keywords: multimedia
;; URL: https://git.zamazal.org/pdm/pipewire-0
;; 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/>.
;;; Commentary:
;;
;; Backend-independent library to access PipeWire functionality.
;; It abstracts data returned from `pipewire-access' methods and provides
;; functions to work with them.
;;
;; pipewire-lib caches data retrieved from PipeWire and uses the cached
;; data. The cache can be invalidated by calling `pipewire-lib-refresh'.
;;; Code:
(require 'cl-lib)
(require 'pipewire-access)
(require 'pipewire-cli)
(defvar pipewire-lib--accessor (pipewire-cli-accessor))
(defvar pipewire-lib--objects '())
(defvar pipewire-lib--bindings nil)
(defvar pipewire-lib--defaults nil)
(defun pipewire-lib-refresh ()
"Clear cache of objects retrieved from PipeWire."
(setq pipewire-lib--objects (pipewire-access-objects pipewire-lib--accessor)
pipewire-lib--bindings nil
pipewire-lib--defaults nil))
(defun pipewire-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 pipewire-lib functions
to access their data.
Note that PipeWire data is cached, if you need its up-to-date
version, call `pipewire-lib-refresh' first."
(unless pipewire-lib--objects
(pipewire-lib-refresh))
(let ((objects pipewire-lib--objects))
(when type
(setq objects (cl-remove-if-not
(lambda (o) (string= (cdr (assq 'type (cdr o))) type))
objects)))
objects))
(defun pipewire-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 `pipewire-lib-refresh' first."
(assoc id pipewire-lib--objects))
(defun pipewire-lib-object-id (object)
"Return id of the given PipeWire OBJECT."
(car object))
(defun pipewire-lib--object-info (object)
(cdr object))
(defun pipewire-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 (pipewire-lib--object-info object)))
default))
(defun pipewire-lib-properties (object)
"Return names of PipeWire OBJECT properties.
The returned value is a list of strings.
The corresponding values can be retrieved using `pipewire-lib-object-value'
function."
(cl-remove-if-not #'stringp (mapcar #'car (pipewire-lib--object-info object))))
(defun pipewire-lib-object-type (object)
"Return PipeWire type of OBJECT as a string.
E.g. \"Device\", \"Node\", \"Port\", \"Client\", ..."
(pipewire-lib-object-value object 'type))
(defun pipewire-lib--profile-name (profile)
(cdr (or (assoc "description" profile)
(assoc "name" profile))))
(defun pipewire-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."
(pipewire-lib--profile-name (pipewire-access-current-profile pipewire-lib--accessor device-id)))
(defun pipewire-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 #'pipewire-lib--profile-name (pipewire-access-profiles pipewire-lib--accessor device-id)))
(defun pipewire-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 `pipewire-lib-profiles'."
(let* ((all-profiles (pipewire-access-profiles pipewire-lib--accessor device-id))
(properties (cl-find profile all-profiles :key #'pipewire-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))
(pipewire-access-set-profile pipewire-lib--accessor device-id index))))
(defun pipewire-lib-parent-node (object)
"Return parent node of OBJECT.
This is typically used for ports.
Behavior is undefined if OBJECT has no parent node."
(pipewire-lib-get-object (pipewire-lib-object-value object "node.id")))
(defun pipewire-lib--node (object)
(if (equal (pipewire-lib-object-type object) "Node")
object
(pipewire-lib-parent-node object)))
(defun pipewire-lib--node-parameters (object-or-id &optional refresh)
(let* ((object (if (numberp object-or-id)
(pipewire-lib-get-object object-or-id)
object-or-id))
(node (pipewire-lib--node object))
(parameters (pipewire-lib-object-value node 'parameters)))
(when (or refresh (not parameters))
(setq parameters (pipewire-access-properties pipewire-lib--accessor (pipewire-lib-object-id node)))
(setcdr node (cons (cons 'parameters parameters)
(assq-delete-all 'parameters (cdr node)))))
parameters))
(defun pipewire-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 `pipewire-lib-refresh' first."
(unless pipewire-lib--defaults
(let ((defaults (pipewire-access-defaults pipewire-lib--accessor))
(nodes (mapcar (lambda (o)
(cons (pipewire-lib-object-value o "node.name") (pipewire-lib-object-id o)))
(pipewire-lib-objects "Node"))))
(setq pipewire-lib--defaults
(cl-remove-if-not #'cdr
(mapcar (lambda (d)
(cons (car d) (cdr (assoc (cdr d) nodes))))
defaults)))))
pipewire-lib--defaults)
(defun pipewire-lib--default-node (key)
(pipewire-lib-get-object (cdr (assoc key (pipewire-lib-default-nodes)))))
(defun pipewire-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 `pipewire-lib-refresh' first."
(or pipewire-lib--bindings
(setq pipewire-lib--bindings
(apply #'nconc
(mapcar (lambda (o)
(let ((o-id (pipewire-lib-object-id o)))
(mapcar (lambda (p)
(cons o-id (cdr p)))
(cl-remove-if-not #'numberp (pipewire-lib--object-info o)
:key #'cdr))))
(pipewire-lib-objects))))))
(defun pipewire-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 `pipewire-lib-refresh' first."
(let ((children (mapcar #'pipewire-lib-get-object
(mapcar #'car (cl-remove-if (lambda (b) (/= (cdr b) id))
(pipewire-lib-bindings))))))
(when type
(setq children (cl-remove-if-not (lambda (o) (equal (pipewire-lib-object-type o) type))
children)))
children))
(defun pipewire-lib--node-ports (node &optional regexp)
(when node
(let ((ports (pipewire-lib-children (pipewire-lib-object-id node) "Port")))
(if regexp
(cl-delete-if-not (lambda (o)
(if-let ((name (pipewire-lib-object-value o "port.name")))
(string-match regexp name)))
ports)
ports))))
(defun pipewire-lib-default-audio-sink ()
"Return a PipeWire object that is the current default audio sink."
(pipewire-lib--default-node "default.audio.sink"))
(defun pipewire-lib-default-audio-source ()
"Return a PipeWire object that is the current default audio source."
(pipewire-lib--default-node "default.audio.source"))
(defun pipewire-lib-default-playback-ports ()
"Return list of PipeWire objects that are default playback ports."
(pipewire-lib--node-ports (pipewire-lib-default-audio-sink) "^playback"))
(defun pipewire-lib-default-capture-ports ()
"Return list of PipeWire objects that are default capture ports."
(pipewire-lib--node-ports (pipewire-lib-default-audio-source) "^capture"))
(defun pipewire-lib--volume-% (volume)
(when volume
(round (* 100 volume))))
(defun pipewire-lib--volume-float (volume)
(/ (float volume) 100))
(defun pipewire-lib--object-parameters (object &optional refresh)
(let* ((node-p (equal (pipewire-lib-object-type object) "Node"))
(parameters (pipewire-lib--node-parameters object refresh))
(monitor-p (unless node-p
(equal (pipewire-lib-object-value object "port.monitor") "true")))
(node-id (pipewire-lib-object-id (pipewire-lib--node object)))
(port-id (unless node-p
(pipewire-lib-object-value object "port.id"))))
(list node-p parameters monitor-p node-id port-id)))
(defun pipewire-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)
(pipewire-lib--object-parameters object refresh)
(eq (cdr (assoc (if monitor-p "monitorMute" "mute") parameters)) 'true)))
(defun pipewire-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)
(pipewire-lib--object-parameters object refresh)
(let* ((mute (not (pipewire-lib-muted-p object)))
(property (if monitor-p "monitorMute" "mute"))
(value (if mute "true" "false")))
(pipewire-access-set-properties pipewire-lib--accessor node-id (list (cons property value)))
mute)))
(defun pipewire-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)
(pipewire-lib--object-parameters object refresh)
(pipewire-lib--volume-%
(if node-p
(cdr (assoc "volume" parameters))
(nth port-id (cdr (assoc (if monitor-p "monitorVolumes" "channelVolumes") parameters)))))))
(defun pipewire-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)
(pipewire-lib--object-parameters object)
(let* ((property (cond
(node-p "volume")
(monitor-p "monitorVolumes")
(t "channelVolumes")))
(float-volume (pipewire-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))))))
(pipewire-access-set-properties pipewire-lib--accessor node-id (list (cons property value))))))
(defun pipewire-lib--set-default-node (object stored-p)
(let ((suffix (mapconcat #'downcase
(split-string (pipewire-lib-object-value object "media.class") "/")
"."))
(prefix (if stored-p "default.configured." "default."))
(node-name (pipewire-lib-object-value object "node.name")))
(pipewire-access-set-default pipewire-lib--accessor (concat prefix suffix) node-name)))
(defun pipewire-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 (pipewire-lib-object-type object)
("Device"
(dolist (node (pipewire-lib-children (pipewire-lib-object-id object) "Node"))
(pipewire-lib--set-default-node node stored-p)))
("Node"
(pipewire-lib--set-default-node object stored-p))
(_
(error "Cannot set this kind of object as default"))))
(provide 'pipewire-lib)
;; Local Variables:
;; checkdoc-force-docstrings-flag: nil
;; End:
;;; pipewire-lib.el ends here

446
pipewire.el Normal file
View File

@ -0,0 +1,446 @@
;;; pipewire.el --- PipeWire user interface -*- lexical-binding: t -*-
;; Copyright (C) 2022 Milan Zamazal <pdm@zamazal.org>
;; Author: Milan Zamazal <pdm@zamazal.org>
;; Version: 1
;; Package-Requires: ((emacs "28.1"))
;; Keywords: multimedia
;; URL: https://git.zamazal.org/pdm/pipewire-0
;; 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/>.
;;; Commentary:
;;
;; PipeWire user interface and library.
;; It currently uses pw-cli and pw-metadata command line utilities to
;; interact with PipeWire.
;;
;; An interactive PipeWire buffer can be displayed using `M-x pipewire'.
;; There you can view basic PipeWire status and change some settings.
;; `pipewire-increase-volume', `pipewire-decrease-volume' and
;; `pipewire-toggle-muted' functions can be used also standalone and
;; are suitable to bind on the multimedia keys.
;;
;; The package can be used also non-interactively in Elisp programs.
;; See pipewire-lib.el source file for available functions.
;;; Code:
(require 'pipewire-lib)
(defgroup pipewire ()
"PipeWire user interface."
:group 'multimedia)
(defcustom pipewire-volume-step 5
"How many percent points to add or subtract when changing volumes."
:type 'number
:group 'pipewire)
(defcustom pipewire-osd-enable t
"If non-nil, display on screen display indicator for some operations.
The indicator is displayed only on graphical terminals."
:type 'boolean
:group 'pipewire)
(defcustom pipewire-osd-timeout 3
"Number of seconds to show an on screen display indicator."
:type 'number
:group 'pipewire)
(defcustom pipewire-osd-width (/ 100 pipewire-volume-step)
"Width of the on screen display indicator in characters."
:type 'natnum
:group 'pipewire)
(defcustom pipewire-osd-volume-on-color "lime green"
"Color to use in the on screen display indicator for active volume."
:type 'color
:group 'pipewire)
(defcustom pipewire-osd-volume-off-color "pale green"
"Color to use in the on screen display indicator for inactive volume."
:type 'color
:group 'pipewire)
(defcustom pipewire-osd-frame-parameters
`((left . 0.05)
(top . 0.95))
"Alist of frame parameters for the on screen display indicator."
:type '(alist :key-type symbol :value-type sexp)
:group 'pip-frame)
(defface pipewire-label
'((t (:weight bold :overline t)))
"Face to use for PipeWire node group labels."
:group 'pipewire)
(defface pipewire-default-object
'((t (:weight bold)))
"Face to use for PipeWire default sinks and sources."
:group 'pipewire)
(defface pipewire-muted
'((t (:strike-through t)))
"Face to use for muted PipeWire sinks and sources."
:group 'pipewire)
(defface pipewire-volume
'((t (:inverse-video t)))
"Face to use for displaying volumes of PipeWire objects."
:group 'pipewire)
(defvar pipewire-buffer "*PipeWire*")
(defvar pipewire-properties-buffer "*PipWire-properties*")
(defun pipewire--label (label)
(propertize (concat label ":") 'face 'pipewire-label))
(defun pipewire--object-volume (object)
(propertize (pipewire-lib-volume object) 'face 'pipewire-volume))
(defun pipewire--object-name (object)
(let* ((type (pipewire-lib-object-type object))
(description-properties (if (equal type "Client")
'("application.name")
(let ((prefix (concat (downcase type) ".")))
(mapcar (lambda (suffix) (concat prefix suffix))
'("nick" "description" "name"))))))
(or (cl-find-if #'identity
(mapcar (lambda (p) (pipewire-lib-object-value object p))
description-properties))
"")))
(defun pipewire--object-label (object default-ids)
(let* ((id (pipewire-lib-object-id object))
(type (pipewire-lib-object-type object))
(text (format "%4s: %s" id (pipewire--object-name object)))
(profile (when (equal type "Device")
(pipewire-lib-current-profile (pipewire-lib-object-id object))))
(face (if (member id default-ids) 'pipewire-default-object 'default))
(media-class (pipewire-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 (pipewire-lib-muted-p object))
(setq face `(:inherit (pipewire-muted ,face))))
(let ((label (propertize text 'face face)))
(when volume-p
(let ((volume (pipewire-lib-volume object)))
(when volume
(setq label (concat label " "
(propertize (number-to-string volume)
'face 'pipewire-volume))))))
label))))
(defun pipewire--insert-line (line object)
(insert (propertize line 'pipewire-object-id (pipewire-lib-object-id object)) "\n"))
(defun pipewire-refresh (&optional _ignore-auto _noconfirm)
"Refresh PipeWire buffer."
(interactive)
(when (and (not (eq major-mode 'pipewire-mode))
(not (equal (buffer-name) pipewire-buffer)))
(user-error "Not in a PipeWire buffer"))
(pipewire-lib-refresh)
(let ((inhibit-read-only t)
(default-ids (mapcar #'cdr (pipewire-lib-default-nodes)))
(current-line (count-lines (point-min) (min (1+ (point)) (point-max)))))
(erase-buffer)
(insert (pipewire--label "Devices") "\n")
(dolist (device (pipewire-lib-objects "Device"))
(pipewire--insert-line (pipewire--object-label device default-ids) device)
(dolist (node (pipewire-lib-children (pipewire-lib-object-id device) "Node"))
(pipewire--insert-line (concat " " (pipewire--object-label node default-ids)) node)
(dolist (port (pipewire-lib-children (pipewire-lib-object-id node) "Port"))
(pipewire--insert-line (concat " " (pipewire--object-label port default-ids)) port))))
(insert (pipewire--label "Clients") "\n")
(dolist (client (pipewire-lib-objects "Client"))
(pipewire--insert-line (pipewire--object-label client default-ids) client))
(goto-char (point-min))
(forward-line (1- current-line))))
(defun pipewire--current-object-id ()
(get-text-property (point) 'pipewire-object-id))
(defun pipewire--current-object (&optional use-default-p allowed-types)
(let* ((id (pipewire--current-object-id))
(object (when id (pipewire-lib-get-object id))))
(when (and object
allowed-types
(not (member (pipewire-lib-object-type object) allowed-types)))
(setq object nil))
(when (and use-default-p (not object))
(setq object (or (car (pipewire-lib-default-playback-ports))
(pipewire-lib-default-audio-sink))))
object))
(defvar pipewire--osd-timer nil)
(defvar pipewire--osd-frame nil)
(defvar pipewire--osd-buffer nil)
(defvar pipewire--osd-buffer-name "*pipewire-osd*")
(defun pipewire--osd-display (string)
(when pipewire--osd-timer
(cancel-timer pipewire--osd-timer))
(let ((frame-width (+ 2 (length string))))
(when (and pipewire--osd-frame
(not (= frame-width (frame-width pipewire--osd-frame))))
(delete-frame pipewire--osd-frame)
(setq pipewire--osd-frame nil))
(with-current-buffer (setq pipewire--osd-buffer (get-buffer-create pipewire--osd-buffer-name))
(erase-buffer)
(insert " " string)
(setq mode-line-format nil)
(unless pipewire--osd-frame
(setq pipewire--osd-frame (make-frame `((unsplittable . t)
,@pipewire-osd-frame-parameters
(minibuffer . nil)
(parent-frame . ,(selected-frame))
(width . ,(+ 2 (length string)))
(height . 1)
(min-width . 1)
(min-height . 1)
(left-fringe . 0)
(right-fringe . 0)
(no-other-frame . t)
(undecorated . t)
(vertical-scroll-bars . nil)
(horizontal-scroll-bars . nil)
(menu-bar-lines . 0)
(tool-bar-lines . 0)
(tab-bar-lines . 0)
(cursor-type . nil)))))))
(setq pipewire--osd-timer
(run-with-timer
pipewire-osd-timeout nil
(lambda ()
(when pipewire--osd-frame
(ignore-errors (delete-frame pipewire--osd-frame)))
(when pipewire--osd-buffer
(ignore-errors (kill-buffer pipewire--osd-buffer)))
(setq pipewire--osd-frame nil
pipewire--osd-timer nil
pipewire--osd-buffer nil)))))
(defmacro pipewire--osd (&rest body)
(declare (debug (body))
(indent defun))
(let (($string (gensym)))
`(when (and window-system pipewire-osd-enable)
(if-let ((,$string (progn ,@body)))
(pipewire--osd-display ,$string)))))
(defun pipewire--update (&optional message)
(if (get-buffer pipewire-buffer)
(with-current-buffer pipewire-buffer
(pipewire-refresh))
(pipewire-lib-refresh))
(when message
(message message)))
(defun pipewire--osd-volume (object)
(pipewire--osd
(unless (eq (pipewire--current-object-id) (pipewire-lib-object-id object))
(let* ((object* (pipewire-lib-get-object (pipewire-lib-object-id object))) ; refreshed version
(volume (pipewire-lib-volume object*))
(muted-p (pipewire-lib-muted-p object*))
(step (/ 100.0 pipewire-osd-width))
(mark (if muted-p ?- ?|))
(n-active (round (/ volume step)))
(n-inactive (- pipewire-osd-width n-active)))
(format "%s%s"
(propertize (make-string n-active mark)
'face `(:background ,pipewire-osd-volume-on-color))
(propertize (make-string n-inactive mark)
'face `(:background ,pipewire-osd-volume-off-color)))))))
(defun pipewire--update-muted (object muted-p)
(let* ((object-name (pipewire--object-name object))
(parent-node (pipewire-lib-parent-node object))
(node-info (if parent-node
(format " in %s" (pipewire--object-name parent-node))
"")))
(pipewire--update (format "%s%s %s" object-name node-info (if muted-p "muted" "unmuted")))))
;;;###autoload
(defun pipewire-toggle-muted ()
"Switch mute status of an audio output or input.
If on a Node or Port in a PipeWire buffer, apply it on the given
object. Otherwise apply it on the default audio sink."
(interactive)
(let* ((object (pipewire--current-object t '("Node" "Port")))
(muted-p (pipewire-lib-toggle-mute object)))
(pipewire--update-muted object muted-p)
(pipewire--osd-volume object)))
;;;###autoload
(defun pipewire-toggle-microphone ()
"Switch mute status of the default audio input."
(interactive)
(let ((object (car (pipewire-lib-default-capture-ports))))
(if object
(pipewire--update-muted object (pipewire-lib-toggle-mute object))
(user-error "No default audio input"))))
;;;###autoload
(defun pipewire-set-volume (volume &optional object single-p)
"Set volume of an audio output or input.
VOLUME must be a number in the range 0-100.
If OBJECT is given (only Nodes and Ports are allowed) or if on a Node
or Port in a PipeWire buffer, apply it on the given object.
Otherwise apply it on the default audio sink.
If SINGLE-P is nil, apply it on all related channels, otherwise on the
corresponding object only."
(interactive "nVolume: ")
(setq volume (max 0 (min 100 volume)))
(unless object
(setq object (pipewire--current-object t '("Node" "Port"))))
(pipewire-lib-set-volume volume object single-p)
(pipewire--update (format "Volume %s for %s" volume (pipewire--object-name object)))
(pipewire--osd-volume object))
(defun pipewire--change-volume (step &optional single-p)
(let* ((object (pipewire--current-object t '("Node" "Port")))
(volume (pipewire-lib-volume object))
(new-volume (max 0 (min 100 (+ volume step)))))
(pipewire-set-volume new-volume object single-p)))
;;;###autoload
(defun pipewire-increase-volume (&optional single-p)
"Increase volume of an audio output or input.
The volume is increased by `pipewire-volume-step'.
If on a Node or Port in a PipeWire buffer, apply it on all the
channels of the given object, unless SINGLE-P is non-nil.
Otherwise apply it on the default audio sink."
(interactive)
(pipewire--change-volume pipewire-volume-step single-p))
;;;###autoload
(defun pipewire-increase-volume-single ()
"Increase volume of an audio output or input.
The volume is increased by `pipewire-volume-step'.
If on a Node or Port in a PipeWire buffer, apply it on the given
object. Otherwise apply it on the default audio sink."
(interactive)
(pipewire-increase-volume t))
;;;###autoload
(defun pipewire-decrease-volume (&optional single-p)
"Decrease volume of an audio output or input.
The volume is decreased by `pipewire-volume-step'.
If on a Node or Port in a PipeWire buffer, apply it on all the
channels of the given object, unless SINGLE-P is non-nil.
Otherwise apply it on the default audio sink."
(interactive)
(pipewire--change-volume (- pipewire-volume-step) single-p))
;;;###autoload
(defun pipewire-decrease-volume-single ()
"Decrease volume of an audio output or input.
The volume is decreased by `pipewire-volume-step'.
If on a Node or Port in a PipeWire buffer, apply it on the given
object. Otherwise apply it on the default audio sink."
(interactive)
(pipewire-decrease-volume t))
;;;###autoload
(defun pipewire-set-default ()
"Set default sink or source.
If on a Node in a PipeWire buffer, apply it on the given object.
If on a Device, apply it on all its nodes.
Otherwise ask for the Node to set as the default Node."
(interactive)
(let ((object (or (pipewire--current-object nil '("Device" "Node"))
(let* ((default-node-ids (mapcar #'cdr (pipewire-lib-default-nodes)))
(nodes (cl-remove-if
(lambda (n) (member (pipewire-lib-object-id n) default-node-ids))
(pipewire-lib-objects "Node")))
(node-mapping (mapcar (lambda (n) (cons (pipewire--object-name n)
(pipewire-lib-object-id n)))
nodes))
(node-name (completing-read "Default node: " node-mapping nil t)))
(pipewire-lib-get-object (cdr (assoc node-name node-mapping)))))))
(pipewire-lib-set-default object nil)
(pipewire-lib-set-default object t)
(pipewire--update)))
(defun pipewire-set-profile ()
"Set profile of the device at the current point."
(interactive)
(if-let ((device (pipewire--current-object nil '("Device")))
(device-id (pipewire-lib-object-id device))
(profiles (pipewire-lib-profiles device-id)))
(progn
(pipewire-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)
(pipewire--update))
(user-error "Nothing to set a profile for here")))
(defun pipewire-properties ()
"Display properties of the object at the current point."
(interactive)
(if-let ((object (pipewire--current-object)))
(progn
(pop-to-buffer pipewire-properties-buffer)
(let ((inhibit-read-only t))
(erase-buffer)
(dolist (p (sort (pipewire-lib-properties object) #'string-lessp))
(insert (format "%s: %s\n" p (pipewire-lib-object-value object p)))))
(goto-char (point-min))
(view-mode))
(user-error "No PipeWire object 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)
(define-key map "+" 'pipewire-increase-volume-single)
(define-key map "_" 'pipewire-decrease-volume-single)
(define-key map " " 'pipewire-properties)
map))
(define-derived-mode pipewire-mode special-mode "PW"
"Major mode for PipeWire user interface.
Selected PipeWire objects are displayed and basic operations may be
applied on some of them or the buffer:
\\{pipewire-mode-map}"
(set (make-local-variable 'revert-buffer-function)
'pipewire-refresh))
;;;###autoload
(defun pipewire ()
"Display a PipeWire buffer."
(interactive)
(pop-to-buffer pipewire-buffer)
(pipewire-refresh)
(pipewire-mode))
(provide 'pipewire)
;; Local Variables:
;; checkdoc-force-docstrings-flag: nil
;; End:
;;; pipewire.el ends here

View File

@ -1,253 +0,0 @@
;;; pw-access.el --- PipeWire access -*- 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/>.
;; A generic interface for communication with PipeWire (https://pipewire.org).
;; It abstracts communication with PipeWire to be backend independent.
;; Only functions from this module may communicate with PipeWire.
;;
;; Currently, pw-cli is used to talk to PipeWire. This is not optimal
;; because pw-cli doesn't seem to have documented output format and
;; the format changes accross PipeWire versions. But there seem to be
;; no better options currently. This module should allow switching to
;; other communication means easily, without any changes needed
;; outside this module, except for using a different communication
;; class.
(require 'eieio)
(defclass pw-accessor ()
()
:documentation
"Base PipeWire interface class.
All PipeWire interfaces should derive from this class.")
(cl-defgeneric pw-access-objects (class)
"Return all the objects currently reported by PipeWire.
It is a list of object data. Each of the elements has a form
(OBJECT-ID . INFO) where OBJECT-ID is a numeric OBJECT-ID as
reported by PipeWire and INFO is an association list of items
(NAME . VALUE) where NAME is a string item name as reported by
PipeWire and VALUE is the corresponding value. VALUE is a number for
object ids, a string otherwise.
A special entry with `type' symbol as its name contains the PipeWire
type of the objects, as a string (e.g. \"Device\", \"Node\", \"Port\",
\"Client\", ...).")
(cl-defgeneric pw-access-properties (class node-id)
"Return properties of the given node.
NODE-ID is a numeric PipeWire Node id (other kinds of PipeWire objects
are not supported in this method).
Object properties may be, unlike object info items, settable.
An assocation list is returned. Each list element is of the form
(PROPERTY . VALUE) where PROPERTY is a string name of the given
property. VALUE can be:
- \"true\" or \"false\" for boolean values (t and nil are not used to
avoid confusion with nil representing invalid or unavailable value).
- A number for numeric values (ids, integers, floats).
- A string for string values.
- A list of elements of any of these types for arrays and structs.")
(cl-defgeneric pw-access-set-properties (class node-id properties)
"Set PROPERTIES of the given node.
NODE-ID is a numeric PipeWire Node id (other kinds of PipeWire objects
are not supported in this method).
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
(KEY . NAME) where KEY is a string identifying the given kind of
default sink or source as reported by PipeWire and NAME is a string
name of the node assigned to the default.")
(cl-defgeneric pw-access-set-default (class key node-name)
"Set default sink or source.
KEY is a string identifying the given kind of default sink or source
as reported in `pw-access-defaults' and NODE-NAME is a string name of
the node that should be assigned to KEY.")
;; pw-cli interface
(defvar pw-cli-command "pw-cli"
"pw-cli command to use.")
(defvar pw-cli-metadata-command "pw-metadata"
"pw-metadata command to use.")
(defclass pw-cli-accessor (pw-accessor)
()
:documentation
"pw-cli based interface to PipeWire.
Note this interface may not work with all PipeWire versions.")
(defun pw-cli--command (command args)
(apply #'call-process command nil t nil args)
(goto-char (point-min)))
(defun pw-cli--next-line ()
(goto-char (line-beginning-position 2)))
(defun pw-cli--parse-list ()
(let ((objects '()))
(while (re-search-forward "^[\t]id \\([0-9]+\\), type PipeWire:Interface:\\(.*\\)/.*$" nil t)
(let ((id (string-to-number (match-string 1)))
(properties `((type . ,(match-string 2)))))
(pw-cli--next-line)
(while (looking-at "^ [\t][\t]\\([a-z.]+\\) = \"\\(.*\\)\"")
(let ((property (match-string 1))
(value (match-string 2)))
(when (string-suffix-p ".id" property)
(setq value (string-to-number value)))
(push (cons property value) properties))
(pw-cli--next-line))
(push (cons id properties) objects)))
(nreverse objects)))
(cl-defmethod pw-access-objects ((_class pw-cli-accessor))
(with-temp-buffer
(pw-cli--command pw-cli-command '("list-objects"))
(pw-cli--parse-list)))
(defun pw-cli--read-property (&optional nesting)
(unless nesting
(setq nesting 0))
(when (looking-at (concat (make-string (+ 6 (* 2 nesting)) ? )
"\\([A-Za-z:]+\\) \\(.*\\)"))
(let ((type (match-string 1))
(value (match-string 2)))
(pcase type
("Bool"
(if (equal value "true") 'true 'false))
((or "Float" "Id" "Int")
(string-to-number value))
("String"
(substring value 1 -1))
((or "Array:" "Struct:")
(let ((array '())
item)
(pw-cli--next-line)
(while (setq item (pw-cli--read-property (1+ nesting)))
(push item array)
(pw-cli--next-line))
(nreverse array)))))))
(defun pw-cli--parse-properties ()
(pw-cli--next-line)
(let ((end (or (save-excursion (re-search-forward "^ Object:" nil t))
(point-max)))
(properties '()))
(while (and (< (point) end)
(re-search-forward "^ Prop: key \\([A-Za-z:]+\\)" end t))
(pw-cli--next-line)
(let ((property (car (last (split-string (match-string 1) ":"))))
(value (pw-cli--read-property)))
(when value
(push (cons property value) properties))))
(goto-char end)
properties))
(cl-defmethod pw-access-properties ((_class pw-cli-accessor) node-id)
(with-temp-buffer
(pw-cli--command pw-cli-command `("enum-params" ,(number-to-string node-id) "Props"))
(pw-cli--parse-properties)))
(defun pw-cli--format-property-value (value)
(cond
((consp value)
(concat "[ " (mapconcat #'pw-cli--format-property-value value ", ") " ]"))
((numberp value)
(number-to-string value))
(t
value)))
(defun pw-cli--format-property (property)
(format "%s: %s" (car property) (pw-cli--format-property-value (cdr property))))
(defun pw-cli--format-properties (properties)
(concat "{ " (mapconcat #'pw-cli--format-property properties ", ") " }"))
(defun pw-cli--set-parameter (object-id parameter value)
(let* ((formatted (pw-cli--format-properties value)))
(call-process pw-cli-command nil pw-cli-command nil
"set-param" (number-to-string object-id) parameter formatted)))
(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 '()))
(while (re-search-forward
"key:'\\([a-z.]+\\)'.*\\(value\\|\"name\"\\): ?['\"]\\([^'\"]+\\)['\"]"
nil t)
(push (cons (match-string 1) (match-string 3)) metadata))
metadata))
(cl-defmethod pw-access-defaults ((_class pw-cli-accessor))
(with-temp-buffer
(pw-cli--command pw-cli-metadata-command '("0"))
(pw-cli--parse-metadata)))
(cl-defmethod pw-access-set-default ((_class pw-cli-accessor) property node-name)
(call-process pw-cli-metadata-command nil pw-cli-metadata-command nil
"0" property (format "{ \"name\": \"%s\" }" node-name)))
(provide 'pw-access)

314
pw-lib.el
View File

@ -1,314 +0,0 @@
;;; 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 '())
(defvar pw-lib--bindings nil)
(defvar pw-lib--defaults 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
pw-lib--defaults 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--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-parent-node (object)
"Return parent node of `object'.
This is typically used for ports.
Behavior is undefined if `object' has no parent node."
(pw-lib-get-object (pw-lib-object-value object "node.id")))
(defun pw-lib--node (object)
(if (equal (pw-lib-object-type object) "Node")
object
(pw-lib-parent-node object)))
(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."
(unless pw-lib--defaults
(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"))))
(setq pw-lib--defaults
(cl-remove-if-not #'cdr
(mapcar #'(lambda (d)
(cons (car d) (cdr (assoc (cdr d) nodes))))
defaults)))))
pw-lib--defaults)
(defun pw-lib--default-node (key)
(pw-lib-get-object (cdr (assoc key (pw-lib-default-nodes)))))
(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--node-ports (node &optional regexp)
(when node
(let ((ports (pw-lib-children (pw-lib-object-id node) "Port")))
(if regexp
(cl-delete-if-not #'(lambda (o)
(if-let ((name (pw-lib-object-value o "port.name")))
(string-match regexp name)))
ports)
ports))))
(defun pw-lib-default-audio-sink ()
"Return a PipeWire object that is the current default audio sink."
(pw-lib--default-node "default.audio.sink"))
(defun pw-lib-default-audio-source ()
"Return a PipeWire object that is the current default audio source."
(pw-lib--default-node "default.audio.source"))
(defun pw-lib-default-playback-ports ()
"Return list of PipeWire objects that are default playback ports."
(pw-lib--node-ports (pw-lib-default-audio-sink) "^playback"))
(defun pw-lib-default-capture-ports ()
"Return list of PipeWire objects that are default capture ports."
(pw-lib--node-ports (pw-lib-default-audio-source) "^capture"))
(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)

399
pw-ui.el
View File

@ -1,399 +0,0 @@
;;; pw-ui.el --- PipeWire user interface -*- 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/>.
;; PipeWire user interface based on pw-lib.
;; An interactive buffer can be displayed using `M-x pipewire'.
;; `pipewire-increase-volume', `pipewire-decrease-volume' and
;; `pipewire-toggle-muted' functions are also suitable to bind on the
;; multimedia keys.
(require 'pw-lib)
(defgroup pipewire ()
"PipeWire user interface.")
(defcustom pipewire-volume-step 5
"How many percent points to add or subtract when changing volumes."
:type 'number
:group 'pipewire)
(defcustom pipewire-osd-enable t
"If non-nil, display on screen display indicator for some operations.
The indicator is displayed only on graphical terminals."
:type 'boolean
:group 'pipewire)
(defcustom pipewire-osd-timeout 3
"Number of seconds to show an on screen display indicator."
:type 'number
:group 'pipewire)
(defcustom pipewire-osd-width (/ 100 pipewire-volume-step)
"Width of the on screen display indicator in characters."
:type 'natnum
:group 'pipewire)
(defcustom pipewire-osd-volume-on-color "lime green"
"Color to use in the on screen display indicator for active volume."
:type 'color
:group 'pipewire)
(defcustom pipewire-osd-volume-off-color "pale green"
"Color to use in the on screen display indicator for inactive volume."
:type 'color
:group 'pipewire)
(defcustom pipewire-osd-frame-parameters
`((left . 0.05)
(top . 0.95))
"Alist of frame parameters for the on screen display indicator."
:type '(alist :key-type symbol :value-type sexp)
:group 'pip-frame)
(defface pipewire-label-face
'((t (:weight bold :overline t)))
"Face to use for PipeWire node group labels."
:group 'pipewire)
(defface pipewire-default-object-face
'((t (:weight bold)))
"Face to use for PipeWire default sinks and sources."
:group 'pipewire)
(defface pipewire-muted-face
'((t (:strike-through t)))
"Face to use for muted PipeWire sinks and sources."
:group 'pipewire)
(defface pipewire-volume-face
'((t (:inverse-video t)))
"Face to use for displaying volumes of PipeWire objects."
:group 'pipewire)
(defvar pipewire-buffer "*PipeWire*")
(defun pw-ui--label (label)
(propertize (concat label ":") 'face 'pipewire-label-face))
(defun pw-ui--object-volume (object)
(propertize (pw-lib-volume object) 'face 'pipewire-volume-face))
(defun pw-ui--object-name (object)
(let* ((type (pw-lib-object-type object))
(description-properties (if (equal type "Client")
'("application.name")
(let ((prefix (concat (downcase type) ".")))
(mapcar #'(lambda (suffix) (concat prefix suffix))
'("description" "name"))))))
(or (cl-find-if #'identity
(mapcar #'(lambda (p) (pw-lib-object-value object p))
description-properties))
"")))
(defun pw-ui--object-label (object default-ids)
(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))))
(let ((label (propertize text 'face face)))
(when volume-p
(let ((volume (pw-lib-volume object)))
(when volume
(setq label (concat label " "
(propertize (number-to-string volume)
'face 'pipewire-volume-face))))))
label))))
(defun pw-ui--insert-line (line object)
(insert (propertize line 'pw-object-id (pw-lib-object-id object)) "\n"))
(defun pipewire-refresh (&optional _ignore-auto _noconfirm)
"Refresh PipeWire buffer."
(interactive)
(when (and (not (eq major-mode 'pipewire-mode))
(not (equal (buffer-name) pipewire-buffer)))
(error "Not in a PipeWire buffer"))
(pw-lib-refresh)
(let ((inhibit-read-only t)
(default-ids (mapcar #'cdr (pw-lib-default-nodes)))
(current-line (count-lines (point-min) (min (1+ (point)) (point-max)))))
(erase-buffer)
(insert (pw-ui--label "Devices") "\n")
(dolist (device (pw-lib-objects "Device"))
(pw-ui--insert-line (pw-ui--object-label device default-ids) device)
(dolist (node (pw-lib-children (pw-lib-object-id device) "Node"))
(pw-ui--insert-line (concat " " (pw-ui--object-label node default-ids)) node)
(dolist (port (pw-lib-children (pw-lib-object-id node) "Port"))
(pw-ui--insert-line (concat " " (pw-ui--object-label port default-ids)) port))))
(insert (pw-ui--label "Clients") "\n")
(dolist (client (pw-lib-objects "Client"))
(pw-ui--insert-line (pw-ui--object-label client default-ids) client))
(goto-char (point-min))
(forward-line (1- current-line))))
(defun pw-ui--current-object-id ()
(get-text-property (point) 'pw-object-id))
(defun pw-ui--current-object (&optional use-default-p allowed-types)
(let* ((id (pw-ui--current-object-id))
(object (when id (pw-lib-get-object id))))
(when (and object
(not (null allowed-types))
(not (member (pw-lib-object-type object) allowed-types)))
(setq object nil))
(when (and use-default-p (not object))
(setq object (or (car (pw-lib-default-playback-ports))
(pw-lib-default-audio-sink))))
object))
(defvar pw-ui--osd-timer nil)
(defvar pw-ui--osd-frame nil)
(defvar pw-ui--osd-buffer nil)
(defvar pw-ui--osd-buffer-name "*pipewire-osd*")
(defun pw-ui--osd-display (string)
(when pw-ui--osd-timer
(cancel-timer pw-ui--osd-timer))
(let ((frame-width (+ 2 (length string))))
(when (and pw-ui--osd-frame
(not (= frame-width (frame-width pw-ui--osd-frame))))
(delete-frame pw-ui--osd-frame)
(setq pw-ui--osd-frame nil))
(with-current-buffer (setq pw-ui--osd-buffer (get-buffer-create pw-ui--osd-buffer-name))
(erase-buffer)
(insert " " string)
(setq mode-line-format nil)
(unless pw-ui--osd-frame
(setq pw-ui--osd-frame (make-frame `((unsplittable . t)
,@pipewire-osd-frame-parameters
(minibuffer . nil)
(parent-frame . ,(selected-frame))
(width . ,(+ 2 (length string)))
(height . 1)
(min-width . 1)
(min-height . 1)
(left-fringe . 0)
(right-fringe . 0)
(no-other-frame . t)
(undecorated . t)
(vertical-scroll-bars . nil)
(horizontal-scroll-bars . nil)
(menu-bar-lines . 0)
(tool-bar-lines . 0)
(tab-bar-lines . 0)
(cursor-type . nil)))))))
(setq pw-ui--osd-timer
(run-with-timer
pipewire-osd-timeout nil
#'(lambda ()
(when pw-ui--osd-frame
(ignore-errors (delete-frame pw-ui--osd-frame)))
(when pw-ui--osd-buffer
(ignore-errors (kill-buffer pw-ui--osd-buffer)))
(setq pw-ui--osd-frame nil
pw-ui--osd-timer nil
pw-ui--osd-buffer nil)))))
(defmacro pw-ui--osd (&rest body)
(declare (indent defun))
(let (($string (gensym)))
`(when (and window-system pipewire-osd-enable)
(if-let ((,$string (progn ,@body)))
(pw-ui--osd-display ,$string)))))
(defun pw-ui--update (&optional message)
(if (get-buffer pipewire-buffer)
(with-current-buffer pipewire-buffer
(pipewire-refresh))
(pw-lib-refresh))
(when message
(message message)))
(defun pw-ui--osd-volume (object)
(pw-ui--osd
(unless (eq (pw-ui--current-object-id) (pw-lib-object-id object))
(let* ((object* (pw-lib-get-object (pw-lib-object-id object))) ; refreshed version
(volume (pw-lib-volume object*))
(muted-p (pw-lib-muted-p object*))
(step (/ 100.0 pipewire-osd-width))
(mark (if muted-p ?- ?|))
(n-active (round (/ volume step)))
(n-inactive (- pipewire-osd-width n-active)))
(format "%s%s"
(propertize (make-string n-active mark)
'face `(:background ,pipewire-osd-volume-on-color))
(propertize (make-string n-inactive mark)
'face `(:background ,pipewire-osd-volume-off-color)))))))
(defun pw-ui--update-muted (object muted-p)
(let ((object-name (pw-ui--object-name object))
(node-name (pw-ui--object-name (pw-lib-parent-node object))))
(pw-ui--update (format "%s in %s %s" object-name node-name (if muted-p "muted" "unmuted")))))
;;;###autoload
(defun pipewire-toggle-muted ()
"Switch mute status of an audio output or input.
If on a Node or Port in a PipeWire buffer, apply it on the given
object. Otherwise apply it on the default audio sink."
(interactive)
(let* ((object (pw-ui--current-object t '("Node" "Port")))
(muted-p (pw-lib-toggle-mute object)))
(pw-ui--update-muted object muted-p)
(pw-ui--osd-volume object)))
;;;###autoload
(defun pipewire-toggle-microphone ()
"Switch mute status of the default audio input."
(interactive)
(let* ((object (car (pw-lib-default-capture-ports)))
(muted-p (pw-lib-toggle-mute object)))
(pw-ui--update-muted object muted-p)))
;;;###autoload
(defun pipewire-set-volume (volume &optional object single-p)
"Set volume of an audio output or input.
VOLUME must be a number in the range 0-100.
If OBJECT is given (only Nodes and Ports are allowed) or if on a Node
or Port in a PipeWire buffer, apply it on the given object.
Otherwise apply it on the default audio sink."
(interactive "nVolume: ")
(setq volume (max 0 (min 100 volume)))
(unless object
(setq object (pw-ui--current-object t '("Node" "Port"))))
(pw-lib-set-volume volume object single-p)
(pw-ui--update (format "Volume %s for %s" volume (pw-ui--object-name object)))
(pw-ui--osd-volume object))
(defun pw-ui--change-volume (step &optional single-p)
(let* ((object (pw-ui--current-object t '("Node" "Port")))
(volume (pw-lib-volume object))
(new-volume (max 0 (min 100 (+ volume step)))))
(pipewire-set-volume new-volume object single-p)))
;;;###autoload
(defun pipewire-increase-volume (&optional single-p)
"Increase volume of an audio output or input.
The volume is increased by `pipewire-volume-step'.
If on a Node or Port in a PipeWire buffer, apply it on all the
channels of the given object. Otherwise apply it on the default audio
sink."
(interactive)
(pw-ui--change-volume pipewire-volume-step single-p))
;;;###autoload
(defun pipewire-increase-volume-single ()
"Increase volume of an audio output or input.
The volume is increased by `pipewire-volume-step'.
If on a Node or Port in a PipeWire buffer, apply it on the given
object. Otherwise apply it on the default audio sink."
(interactive)
(pipewire-increase-volume t))
;;;###autoload
(defun pipewire-decrease-volume (&optional single-p)
"Decrease volume of an audio output or input.
The volume is decreased by `pipewire-volume-step'.
If on a Node or Port in a PipeWire buffer, apply it on all the
channels of the given object. Otherwise apply it on the default audio
sink."
(interactive)
(pw-ui--change-volume (- pipewire-volume-step) single-p))
;;;###autoload
(defun pipewire-decrease-volume-single ()
"Decrease volume of an audio output or input.
The volume is decreased by `pipewire-volume-step'.
If on a Node or Port in a PipeWire buffer, apply it on the given
object. Otherwise apply it on the default audio sink."
(interactive)
(pipewire-decrease-volume t))
;;;###autoload
(defun pipewire-set-default ()
"Set default sink or source.
If on a Node in a PipeWire buffer, apply it on the given object.
If on a Device, apply it on all its nodes.
Otherwise ask for the Node to set as the default Node."
(interactive)
(let ((object (or (pw-ui--current-object nil '("Device" "Node"))
(let* ((default-node-ids (mapcar #'cdr (pw-lib-default-nodes)))
(nodes (cl-remove-if
#'(lambda (n) (member (pw-lib-object-id n) default-node-ids))
(pw-lib-objects "Node")))
(node-mapping (mapcar #'(lambda (n) (cons (pw-ui--object-name n)
(pw-lib-object-id n)))
nodes))
(node-name (completing-read "Default node: " node-mapping nil t)))
(pw-lib-get-object (cdr (assoc node-name node-mapping)))))))
(pw-lib-set-default object nil)
(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)
(define-key map "+" 'pipewire-increase-volume-single)
(define-key map "_" 'pipewire-decrease-volume-single)
map))
(define-derived-mode pipewire-mode special-mode "PW"
"Major mode for PipeWire user interface.
Selected PipeWire objects are displayed and basic operations may be
applied on some of them or the buffer:
\\{pipewire-mode-map}"
(set (make-local-variable 'revert-buffer-function)
'pipewire-refresh))
;;;###autoload
(defun pipewire ()
"Display a PipeWire buffer."
(interactive)
(pop-to-buffer pipewire-buffer)
(pipewire-refresh)
(pipewire-mode))
(provide 'pw-ui)