Compare commits

...

27 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
7 changed files with 655 additions and 531 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?

View File

@ -1,2 +0,0 @@
(define-package pipewire-0 "1"
"Interface to PipeWire.")

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

View File

@ -1,7 +1,13 @@
;;; pw-lib.el --- PipeWire library -*- lexical-binding: t -*-
;;; 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
@ -20,269 +26,279 @@
;;; Commentary:
;;
;; Backend-independent library to access PipeWire functionality.
;; It abstracts data returned from `pw-access' methods and provides
;; It abstracts data returned from `pipewire-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'.
;; 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 'pw-access)
(require 'pipewire-access)
(require 'pipewire-cli)
(defvar pw-lib--accessor (pw-cli-accessor))
(defvar pipewire-lib--accessor (pipewire-cli-accessor))
(defvar pw-lib--objects '())
(defvar pw-lib--bindings nil)
(defvar pw-lib--defaults nil)
(defvar pipewire-lib--objects '())
(defvar pipewire-lib--bindings nil)
(defvar pipewire-lib--defaults nil)
(defun pw-lib-refresh ()
(defun pipewire-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))
(setq pipewire-lib--objects (pipewire-access-objects pipewire-lib--accessor)
pipewire-lib--bindings nil
pipewire-lib--defaults nil))
(defun pw-lib-objects (&optional type)
(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 pw-lib functions
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 `pw-lib-refresh' first."
(unless pw-lib--objects
(pw-lib-refresh))
(let ((objects pw-lib--objects))
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 pw-lib-get-object (id)
(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 `pw-lib-refresh' first."
(assoc id pw-lib--objects))
version, call `pipewire-lib-refresh' first."
(assoc id pipewire-lib--objects))
(defun pw-lib-object-id (object)
(defun pipewire-lib-object-id (object)
"Return id of the given PipeWire OBJECT."
(car object))
(defun pw-lib--object-info (object)
(defun pipewire-lib--object-info (object)
(cdr object))
(defun pw-lib-object-value (object key &optional default)
(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 (pw-lib--object-info object)))
(or (cdr (assoc key (pipewire-lib--object-info object)))
default))
(defun pw-lib-object-type (object)
(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\", ..."
(pw-lib-object-value object 'type))
(pipewire-lib-object-value object 'type))
(defun pw-lib--profile-name (profile)
(defun pipewire-lib--profile-name (profile)
(cdr (or (assoc "description" profile)
(assoc "name" profile))))
(defun pw-lib-current-profile (device-id)
(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."
(pw-lib--profile-name (pw-access-current-profile pw-lib--accessor device-id)))
(pipewire-lib--profile-name (pipewire-access-current-profile pipewire-lib--accessor device-id)))
(defun pw-lib-profiles (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 #'pw-lib--profile-name (pw-access-profiles pw-lib--accessor device-id)))
(mapcar #'pipewire-lib--profile-name (pipewire-access-profiles pipewire-lib--accessor device-id)))
(defun pw-lib-set-profile (device-id profile)
(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 `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)))
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))
(pw-access-set-profile pw-lib--accessor device-id index))))
(pipewire-access-set-profile pipewire-lib--accessor device-id index))))
(defun pw-lib-parent-node (object)
"Return parent node of `object'.
(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."
(pw-lib-get-object (pw-lib-object-value object "node.id")))
Behavior is undefined if OBJECT has no parent node."
(pipewire-lib-get-object (pipewire-lib-object-value object "node.id")))
(defun pw-lib--node (object)
(if (equal (pw-lib-object-type object) "Node")
(defun pipewire-lib--node (object)
(if (equal (pipewire-lib-object-type object) "Node")
object
(pw-lib-parent-node object)))
(pipewire-lib-parent-node object)))
(defun pw-lib--node-parameters (object-or-id &optional refresh)
(defun pipewire-lib--node-parameters (object-or-id &optional refresh)
(let* ((object (if (numberp object-or-id)
(pw-lib-get-object object-or-id)
(pipewire-lib-get-object object-or-id)
object-or-id))
(node (pw-lib--node object))
(parameters (pw-lib-object-value node 'parameters)))
(node (pipewire-lib--node object))
(parameters (pipewire-lib-object-value node 'parameters)))
(when (or refresh (not parameters))
(setq parameters (pw-access-properties pw-lib--accessor (pw-lib-object-id node)))
(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 pw-lib-default-nodes ()
(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 `pw-lib-refresh' first."
(unless pw-lib--defaults
(let ((defaults (pw-access-defaults pw-lib--accessor))
version, call `pipewire-lib-refresh' first."
(unless pipewire-lib--defaults
(let ((defaults (pipewire-access-defaults pipewire-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
(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)))))
pw-lib--defaults)
pipewire-lib--defaults)
(defun pw-lib--default-node (key)
(pw-lib-get-object (cdr (assoc key (pw-lib-default-nodes)))))
(defun pipewire-lib--default-node (key)
(pipewire-lib-get-object (cdr (assoc key (pipewire-lib-default-nodes)))))
(defun pw-lib-bindings ()
(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 `pw-lib-refresh' first."
(or pw-lib--bindings
(setq pw-lib--bindings
version, call `pipewire-lib-refresh' first."
(or pipewire-lib--bindings
(setq pipewire-lib--bindings
(apply #'nconc
(mapcar (lambda (o)
(let ((o-id (pw-lib-object-id o)))
(let ((o-id (pipewire-lib-object-id o)))
(mapcar (lambda (p)
(cons o-id (cdr p)))
(cl-remove-if-not #'numberp (pw-lib--object-info o)
(cl-remove-if-not #'numberp (pipewire-lib--object-info o)
:key #'cdr))))
(pw-lib-objects))))))
(pipewire-lib-objects))))))
(defun pw-lib-children (id &optional type)
(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 `pw-lib-refresh' first."
(let ((children (mapcar #'pw-lib-get-object
version, call `pipewire-lib-refresh' first."
(let ((children (mapcar #'pipewire-lib-get-object
(mapcar #'car (cl-remove-if (lambda (b) (/= (cdr b) id))
(pw-lib-bindings))))))
(pipewire-lib-bindings))))))
(when type
(setq children (cl-remove-if-not (lambda (o) (equal (pw-lib-object-type o) type))
(setq children (cl-remove-if-not (lambda (o) (equal (pipewire-lib-object-type o) type))
children)))
children))
(defun pw-lib--node-ports (node &optional regexp)
(defun pipewire-lib--node-ports (node &optional regexp)
(when node
(let ((ports (pw-lib-children (pw-lib-object-id node) "Port")))
(let ((ports (pipewire-lib-children (pipewire-lib-object-id node) "Port")))
(if regexp
(cl-delete-if-not (lambda (o)
(if-let ((name (pw-lib-object-value o "port.name")))
(if-let ((name (pipewire-lib-object-value o "port.name")))
(string-match regexp name)))
ports)
ports))))
(defun pw-lib-default-audio-sink ()
(defun pipewire-lib-default-audio-sink ()
"Return a PipeWire object that is the current default audio sink."
(pw-lib--default-node "default.audio.sink"))
(pipewire-lib--default-node "default.audio.sink"))
(defun pw-lib-default-audio-source ()
(defun pipewire-lib-default-audio-source ()
"Return a PipeWire object that is the current default audio source."
(pw-lib--default-node "default.audio.source"))
(pipewire-lib--default-node "default.audio.source"))
(defun pw-lib-default-playback-ports ()
(defun pipewire-lib-default-playback-ports ()
"Return list of PipeWire objects that are default playback ports."
(pw-lib--node-ports (pw-lib-default-audio-sink) "^playback"))
(pipewire-lib--node-ports (pipewire-lib-default-audio-sink) "^playback"))
(defun pw-lib-default-capture-ports ()
(defun pipewire-lib-default-capture-ports ()
"Return list of PipeWire objects that are default capture ports."
(pw-lib--node-ports (pw-lib-default-audio-source) "^capture"))
(pipewire-lib--node-ports (pipewire-lib-default-audio-source) "^capture"))
(defun pw-lib--volume-% (volume)
(defun pipewire-lib--volume-% (volume)
(when volume
(round (* 100 volume))))
(defun pw-lib--volume-float (volume)
(defun pipewire-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))
(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 (pw-lib-object-value object "port.monitor") "true")))
(node-id (pw-lib-object-id (pw-lib--node object)))
(equal (pipewire-lib-object-value object "port.monitor") "true")))
(node-id (pipewire-lib-object-id (pipewire-lib--node object)))
(port-id (unless node-p
(pw-lib-object-value object "port.id"))))
(pipewire-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.
(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)
(pw-lib--object-parameters object refresh)
(pipewire-lib--object-parameters object refresh)
(eq (cdr (assoc (if monitor-p "monitorMute" "mute") parameters)) 'true)))
(defun pw-lib-toggle-mute (object &optional refresh)
(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)
(pw-lib--object-parameters object refresh)
(let* ((mute (not (pw-lib-muted-p object)))
(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")))
(pw-access-set-properties pw-lib--accessor node-id (list (cons property value)))
(pipewire-access-set-properties pipewire-lib--accessor node-id (list (cons property value)))
mute)))
(defun pw-lib-volume (object &optional refresh)
"Return volume of the given PipeWire object.
(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)
(pw-lib--object-parameters object refresh)
(pw-lib--volume-%
(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 pw-lib-set-volume (volume object &optional single-p)
(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)
(pw-lib--object-parameters object)
(pipewire-lib--object-parameters object)
(let* ((property (cond
(node-p "volume")
(monitor-p "monitorVolumes")
(t "channelVolumes")))
(float-volume (pw-lib--volume-float volume))
(float-volume (pipewire-lib--volume-float volume))
(value (if node-p
float-volume
(let ((orig-value (cdr (assoc property parameters))))
@ -290,27 +306,33 @@ otherwise set the volume to the same value for all the related channels."
(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))))))
(pipewire-access-set-properties pipewire-lib--accessor node-id (list (cons property value))))))
(defun pw-lib--set-default-node (object stored-p)
(defun pipewire-lib--set-default-node (object stored-p)
(let ((suffix (mapconcat #'downcase
(split-string (pw-lib-object-value object "media.class") "/")
(split-string (pipewire-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)))
(node-name (pipewire-lib-object-value object "node.name")))
(pipewire-access-set-default pipewire-lib--accessor (concat prefix suffix) node-name)))
(defun pw-lib-set-default (object stored-p)
(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 (pw-lib-object-type object)
(pcase (pipewire-lib-object-type object)
("Device"
(dolist (node (pw-lib-children (pw-lib-object-id object) "Node"))
(pw-lib--set-default-node node stored-p)))
(dolist (node (pipewire-lib-children (pipewire-lib-object-id object) "Node"))
(pipewire-lib--set-default-node node stored-p)))
("Node"
(pw-lib--set-default-node object stored-p))
(pipewire-lib--set-default-node object stored-p))
(_
(error "Cannot set this kind of object as default."))))
(error "Cannot set this kind of object as default"))))
(provide 'pw-lib)
(provide 'pipewire-lib)
;; Local Variables:
;; checkdoc-force-docstrings-flag: nil
;; End:
;;; pipewire-lib.el ends here

View File

@ -1,7 +1,13 @@
;;; pw-ui.el --- PipeWire user interface -*- lexical-binding: t -*-
;;; 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
@ -19,13 +25,22 @@
;;; Commentary:
;;
;; PipeWire user interface based on pw-lib.
;; An interactive buffer can be displayed using `M-x pipewire'.
;; 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 are also suitable to bind on the
;; multimedia keys.
;; `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.
(require 'pw-lib)
;;; Code:
(require 'pipewire-lib)
(defgroup pipewire ()
"PipeWire user interface."
@ -90,161 +105,162 @@ The indicator is displayed only on graphical terminals."
:group 'pipewire)
(defvar pipewire-buffer "*PipeWire*")
(defvar pipewire-properties-buffer "*PipWire-properties*")
(defun pw-ui--label (label)
(defun pipewire--label (label)
(propertize (concat label ":") 'face 'pipewire-label))
(defun pw-ui--object-volume (object)
(propertize (pw-lib-volume object) 'face 'pipewire-volume))
(defun pipewire--object-volume (object)
(propertize (pipewire-lib-volume object) 'face 'pipewire-volume))
(defun pw-ui--object-name (object)
(let* ((type (pw-lib-object-type object))
(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))
'("description" "name"))))))
'("nick" "description" "name"))))))
(or (cl-find-if #'identity
(mapcar (lambda (p) (pw-lib-object-value object p))
(mapcar (lambda (p) (pipewire-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)))
(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")
(pw-lib-current-profile (pw-lib-object-id object))))
(pipewire-lib-current-profile (pipewire-lib-object-id object))))
(face (if (member id default-ids) 'pipewire-default-object 'default))
(media-class (pw-lib-object-value object "media.class")))
(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 (pw-lib-muted-p object))
(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 (pw-lib-volume object)))
(let ((volume (pipewire-lib-volume object)))
(when volume
(setq label (concat label " "
(propertize (number-to-string volume)
'face 'pipewire-volume))))))
label))))
(defun pw-ui--insert-line (line object)
(insert (propertize line 'pw-object-id (pw-lib-object-id object)) "\n"))
(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)))
(error "Not in a PipeWire buffer"))
(pw-lib-refresh)
(user-error "Not in a PipeWire buffer"))
(pipewire-lib-refresh)
(let ((inhibit-read-only t)
(default-ids (mapcar #'cdr (pw-lib-default-nodes)))
(default-ids (mapcar #'cdr (pipewire-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))
(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 pw-ui--current-object-id ()
(get-text-property (point) 'pw-object-id))
(defun pipewire--current-object-id ()
(get-text-property (point) 'pipewire-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))))
(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
(not (null allowed-types))
(not (member (pw-lib-object-type object) allowed-types)))
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 (pw-lib-default-playback-ports))
(pw-lib-default-audio-sink))))
(setq object (or (car (pipewire-lib-default-playback-ports))
(pipewire-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*")
(defvar pipewire--osd-timer nil)
(defvar pipewire--osd-frame nil)
(defvar pipewire--osd-buffer nil)
(defvar pipewire--osd-buffer-name "*pipewire-osd*")
(defun pw-ui--osd-display (string)
(when pw-ui--osd-timer
(cancel-timer pw-ui--osd-timer))
(defun pipewire--osd-display (string)
(when pipewire--osd-timer
(cancel-timer pipewire--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))
(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 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
(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 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)))))
(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 pw-ui--osd (&rest body)
(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)))
(pw-ui--osd-display ,$string)))))
(pipewire--osd-display ,$string)))))
(defun pw-ui--update (&optional message)
(defun pipewire--update (&optional message)
(if (get-buffer pipewire-buffer)
(with-current-buffer pipewire-buffer
(pipewire-refresh))
(pw-lib-refresh))
(pipewire-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*))
(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)))
@ -255,13 +271,13 @@ The indicator is displayed only on graphical terminals."
(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))
(parent-node (pw-lib-parent-node object))
(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" (pw-ui--object-name parent-node))
(format " in %s" (pipewire--object-name parent-node))
"")))
(pw-ui--update (format "%s%s %s" object-name node-info (if muted-p "muted" "unmuted")))))
(pipewire--update (format "%s%s %s" object-name node-info (if muted-p "muted" "unmuted")))))
;;;###autoload
(defun pipewire-toggle-muted ()
@ -269,18 +285,19 @@ The indicator is displayed only on graphical terminals."
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)))
(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 (pw-lib-default-capture-ports)))
(muted-p (pw-lib-toggle-mute object)))
(pw-ui--update-muted object muted-p)))
(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)
@ -288,18 +305,20 @@ object. Otherwise apply it on the default audio sink."
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."
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 (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))
(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 pw-ui--change-volume (step &optional single-p)
(let* ((object (pw-ui--current-object t '("Node" "Port")))
(volume (pw-lib-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)))
@ -308,10 +327,10 @@ Otherwise apply it on the default audio sink."
"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."
channels of the given object, unless SINGLE-P is non-nil.
Otherwise apply it on the default audio sink."
(interactive)
(pw-ui--change-volume pipewire-volume-step single-p))
(pipewire--change-volume pipewire-volume-step single-p))
;;;###autoload
(defun pipewire-increase-volume-single ()
@ -327,10 +346,10 @@ object. Otherwise apply it on the default audio sink."
"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."
channels of the given object, unless SINGLE-P is non-nil.
Otherwise apply it on the default audio sink."
(interactive)
(pw-ui--change-volume (- pipewire-volume-step) single-p))
(pipewire--change-volume (- pipewire-volume-step) single-p))
;;;###autoload
(defun pipewire-decrease-volume-single ()
@ -348,32 +367,46 @@ 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)))
(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 (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)))
(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)))
(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)))
(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 (pw-ui--current-object nil '("Device")))
(device-id (pw-lib-object-id device))
(profiles (pw-lib-profiles device-id)))
(if-let ((device (pipewire--current-object nil '("Device")))
(device-id (pipewire-lib-object-id device))
(profiles (pipewire-lib-profiles device-id)))
(progn
(pw-lib-set-profile device-id (completing-read "Select profile: " profiles nil t))
(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)
(pw-ui--update))
(error "Nothing to set a profile for here")))
(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)))
@ -385,6 +418,7 @@ Otherwise ask for the Node to set as the default Node."
(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"
@ -403,4 +437,10 @@ applied on some of them or the buffer:
(pipewire-refresh)
(pipewire-mode))
(provide 'pw-ui)
(provide 'pipewire)
;; Local Variables:
;; checkdoc-force-docstrings-flag: nil
;; End:
;;; pipewire.el ends here

View File

@ -1,255 +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/>.
;;; 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.
;;
;; 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)