diff mbox series

[bug#61900] home: services: Add 'pulseaudio-rtp-sink' and 'pulseaudio-rtp-source'.

Message ID 20230301222601.22216-1-ludo@gnu.org
State New
Headers show
Series [bug#61900] home: services: Add 'pulseaudio-rtp-sink' and 'pulseaudio-rtp-source'. | expand

Commit Message

Ludovic Courtès March 1, 2023, 10:26 p.m. UTC
* gnu/home/services/sound.scm: New file.
* gnu/local.mk (GNU_SYSTEM_MODULES): Add it.
* doc/guix.texi (Sound Home Services): New section.
---
 doc/guix.texi               | 114 ++++++++++++++++++++++++---
 gnu/home/services/sound.scm | 151 ++++++++++++++++++++++++++++++++++++
 gnu/local.mk                |   3 +-
 3 files changed, 258 insertions(+), 10 deletions(-)
 create mode 100644 gnu/home/services/sound.scm

Hi there!

Here’s a Guix Home service I’ve been using for some time.  In a nutshell,
when I want to send audio to a device that’s further away from my laptop,
I run:

  herd start pulseaudio-rtp-sink

That adds a PulseAudio “sink”, which I can select in pavucontrol or
pulsemixer to send audio streams over there.  And then to turn that off:

  herd stop pulseaudio-rtp-sink

Thoughts?

Ludo’.


base-commit: ff5fbcc19bce6e94ead0cc79b27ae8ed0307463d

Comments

Andrew Tropin March 2, 2023, 2:16 p.m. UTC | #1
On 2023-03-01 23:26, Ludovic Courtès wrote:

> * gnu/home/services/sound.scm: New file.
> * gnu/local.mk (GNU_SYSTEM_MODULES): Add it.
> * doc/guix.texi (Sound Home Services): New section.
> ---
>  doc/guix.texi               | 114 ++++++++++++++++++++++++---
>  gnu/home/services/sound.scm | 151 ++++++++++++++++++++++++++++++++++++
>  gnu/local.mk                |   3 +-
>  3 files changed, 258 insertions(+), 10 deletions(-)
>  create mode 100644 gnu/home/services/sound.scm
>
> Hi there!
>
> Here’s a Guix Home service I’ve been using for some time.  In a nutshell,
> when I want to send audio to a device that’s further away from my laptop,
> I run:
>
>   herd start pulseaudio-rtp-sink
>
> That adds a PulseAudio “sink”, which I can select in pavucontrol or
> pulsemixer to send audio streams over there.  And then to turn that off:
>
>   herd stop pulseaudio-rtp-sink
>
> Thoughts?
>
> Ludo’.
>
> diff --git a/doc/guix.texi b/doc/guix.texi
> index a7ef00f421..7716686f87 100644
> --- a/doc/guix.texi
> +++ b/doc/guix.texi
> @@ -41513,15 +41513,16 @@ service with the @code{simple-service} procedure from @code{(gnu
>  services)}.
>  
>  @menu
> -* Essential Home Services::  Environment variables, packages, on-* scripts.
> -* Shells: Shells Home Services.                        POSIX shells, Bash, Zsh.
> -* Mcron: Mcron Home Service.                           Scheduled User's Job Execution.
> -* Power Management: Power Management Home Services.    Services for battery power.
> -* Shepherd: Shepherd Home Service.                     Managing User's Daemons.
> -* SSH: Secure Shell.                                   Setting up the secure shell client.
> -* Desktop: Desktop Home Services.                      Services for graphical environments.
> -* Guix: Guix Home Services.                            Services for Guix.
> -* Fonts: Fonts Home Services.                          Services for managing User's fonts.
> +* Essential Home Services::     Environment variables, packages, on-* scripts.
> +* Shells: Shells Home Services.  POSIX shells, Bash, Zsh.
> +* Mcron: Mcron Home Service.    Scheduled User's Job Execution.
> +* Power Management: Power Management Home Services.  Services for battery power.
> +* Shepherd: Shepherd Home Service.  Managing User's Daemons.
> +* SSH: Secure Shell.            Setting up the secure shell client.
> +* Desktop: Desktop Home Services.  Services for graphical environments.
> +* Guix: Guix Home Services.     Services for Guix.
> +* Fonts: Fonts Home Services.   Services for managing User's fonts.
> +* Sound: Sound Home Services.   Dealing with audio.
>  @end menu
>  @c In addition to that Home Services can provide
>  
> @@ -42435,6 +42436,101 @@ like this:
>  @end lisp
>  @end defvar
>  
> +@node Sound Home Services
> +@subsection Sound Home Services
> +
> +The @code{(gnu home services sound)} module provides services related to
> +sound support.
> +
> +@cindex PulseAudio, home service
> +@cindex RTP, for PulseAudio
> +
> +The following services dynamically reconfigure the
> +@uref{https://pulseaudio.org,PulseAudio sound server}: they let you
> +toggle broadcast of audio output over the network using the
> +@acronym{RTP, real-time transport protocol} and, correspondingly,
> +playback of sound received over RTP.  Once
> +@code{home-pulseaudio-rtp-sink-service-type} is among your home
> +services, you can start broadcasting audio output by running this
> +command:
> +
> +@example
> +herd start pulseaudio-rtp-sink
> +@end example
> +
> +You can then run a PulseAudio-capable mixer, such as @code{pavucontrol}
> +or @code{pulsemixer} (both from the same-named package) to control which
> +audio stream(s) should be sent to the RTP ``sink''.
> +
> +By default, audio is broadcasted to a multicast address: any device on
> +the @acronym{LAN, local area network} receives it and may play it.
> +Using multicast in this way puts a lot of pressure on the network and
> +degrades its performance, so you may instead prefer sending to
> +specifically one device.  The first way to do that is by specifying the
> +IP address of the target device when starting the service:
> +
> +@example
> +herd start pulseaudio-rtp-sink 192.168.1.42
> +@end example
> +
> +The other option is to specify this IP address as the one to use by
> +default in your home environment configuration:
> +
> +@lisp
> +(service home-pulseaudio-rtp-sink-service-type
> +         "192.168.1.42")
> +@end lisp
> +
> +On the device where you intend to receive and play the RTP stream, you
> +can use @code{home-pulseaudio-rtp-source-service-type}, like so:
> +
> +@lisp
> +(service home-pulseaudio-rtp-source-service-type)
> +@end lisp
> +
> +This will then let you start the receiving module for PulseAudio:
> +
> +@example
> +herd start pulseaudio-rtp-source
> +@end example
> +
> +Again, by default it will listen on the multicast address.  If, instead,
> +you'd like it to listen for direct incoming connections, you can do that
> +by running:
> +
> +@lisp
> +(service home-pulseaudio-rtp-source-service-type
> +         "0.0.0.0")
> +@end lisp
> +
> +The reference of these services is given below.
> +
> +@defvar home-pulseaudio-rtp-sink-service-type
> +@defvarx home-pulseaudio-rtp-source-service-type
> +This is the type of the service to send, respectively receive, audio
> +streams over @acronym{RTP, real-time transport protocol}.
> +
> +The value associated with this service is the IP address (a string)
> +where to send, respectively receive, the audio stream.  By default,
> +audio is sent/received on multicast address
> +@code{%pulseaudio-rtp-multicast-address}.
> +
> +This service defines one Shepherd service: @code{pulseaudio-rtp-sink},
> +respectively @code{pulseaudio-rtp-source}.  The service is not started
> +by default, so you have to explicitly start it when you want to turn it
> +on, as in this example:
> +
> +@example
> +herd start pulseaudio-rtp-sink
> +@end example
> +
> +Stopping the Shepherd service turns off broadcasting.
> +@end defvar
> +
> +@defvar %pulseaudio-rtp-multicast-address
> +This is the multicast address used by default by the two services above.
> +@end defvar
> +
>  @node Invoking guix home
>  @section Invoking @command{guix home}
>  
> diff --git a/gnu/home/services/sound.scm b/gnu/home/services/sound.scm
> new file mode 100644
> index 0000000000..22c1a99250
> --- /dev/null
> +++ b/gnu/home/services/sound.scm
> @@ -0,0 +1,151 @@
> +;;; GNU Guix --- Functional package management for GNU
> +;;; Copyright © 2023 Ludovic Courtès <ludo@gnu.org>
> +;;;
> +;;; This file is part of GNU Guix.
> +;;;
> +;;; GNU Guix 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.
> +;;;
> +;;; GNU Guix 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 GNU Guix.  If not, see <http://www.gnu.org/licenses/>.
> +
> +(define-module (gnu home services sound)
> +  #:use-module (gnu home services)
> +  #:use-module (gnu home services shepherd)
> +  #:use-module (guix records)
> +  #:use-module (guix gexp)
> +  #:use-module (srfi srfi-1)
> +  #:use-module (ice-9 match)
> +  #:export (home-pulseaudio-rtp-sink-service-type
> +            home-pulseaudio-rtp-source-service-type
> +            %pulseaudio-rtp-multicast-address))
> +
> +
> +;;;
> +;;; PulseAudio support.
> +;;;
> +
> +(define (with-pulseaudio-connection sock exp)
> +  ;; Wrap EXP in an expression where SOCK is bound to a socket connected to
> +  ;; the user's PulseAudio command-line interface socket.
> +  #~(let* ((#$sock (socket AF_UNIX SOCK_STREAM 0))
> +           (pulse-user-file
> +            (lambda (name)
> +              (string-append "/run/user/" (number->string (getuid))
> +                             "/pulse/" name)))
> +           (file (pulse-user-file "cli")))
> +      (let loop ((tries 0))
> +        (catch #t
> +          (lambda ()
> +            (connect #$sock AF_UNIX file)
> +            (let ((result #$exp))
> +              (close-port #$sock)
> +              result))
> +          (lambda (key . args)
> +            (if (and (eq? key 'system-error)
> +                     (= ENOENT (system-error-errno (cons key args)))
> +                     (< tries 3))
> +                ;; The CLI socket doesn't exist yet, so send pulseaudio
> +                ;; SIGUSR2 so that it creates it and listens to it.
> +                (let ((pid (call-with-input-file (pulse-user-file "pid")
> +                             read)))
> +                  (when (and (integer? pid) (> pid 1))
> +                    (kill pid SIGUSR2))
> +                  ((@ (fibers) sleep) 1)
> +                  (loop (+ tries 1)))
> +                (begin
> +                  (close-port #$sock)
> +                  (apply throw key args))))))))
> +
> +(define %pulseaudio-rtp-multicast-address
> +  ;; Default address used by 'module-rtp-sink' and 'module-rtp-recv'.  This is
> +  ;; a multicast address, for the Session Announcement Protocol (SAP) and the
> +  ;; Session Description Protocol (SDP).
> +  "224.0.0.56")
> +
> +(define (pulseaudio-rtp-sink-shepherd-services destination-ip)
> +  (list (shepherd-service
> +         (provision '(pulseaudio-rtp-sink))
> +         (start
> +          #~(lambda* (#:optional (destination-ip #$destination-ip))
> +              #$(with-pulseaudio-connection
> +                 #~sock
> +                 #~(begin
> +                     (display "\
> +load-module module-null-sink \
> +sink_name=rtp sink_properties=\"device.description='RTP network output'\"\n"
> +                              sock)
> +                     (display (string-append "\
> +load-module module-rtp-send source=rtp.monitor"
> +                                             (if destination-ip
> +                                                 (string-append
> +                                                  " destination_ip="
> +                                                  destination-ip)
> +                                                 "")
> +                                             "\n")
> +                              sock)
> +                     #t))))
> +         (stop
> +          #~(lambda (_)
> +              #$(with-pulseaudio-connection
> +                 #~sock
> +                 #~(begin
> +                     (display "unload-module module-rtp-send\n"
> +                              sock)
> +                     (display "unload-module module-null-sink\n"
> +                              sock)
> +                     #f))))
> +         (auto-start? #f))))
> +
> +(define home-pulseaudio-rtp-sink-service-type
> +  (service-type
> +   (name 'pulseaudio-rtp-sink)
> +   (extensions
> +    (list (service-extension home-shepherd-service-type
> +                             pulseaudio-rtp-sink-shepherd-services)))
> +   (description
> +    "Define a PulseAudio sink to broadcast audio output over RTP, which can
> +then by played by another PulseAudio instance.")
> +
> +   ;; By default, send to the SAP multicast address, 224.0.0.56, which can be
> +   ;; network-intensive.
> +   (default-value %pulseaudio-rtp-multicast-address)))
> +
> +(define (pulseaudio-rtp-source-shepherd-services source-ip)
> +  (list (shepherd-service
> +         (provision '(pulseaudio-rtp-source))
> +         (start
> +          #~(lambda* (#:optional (source-ip #$source-ip))
> +              #$(with-pulseaudio-connection
> +                 #~sock
> +                 #~(begin
> +                     (format sock "\
> +load-module module-rtp-recv sap_address=~a\n" source-ip)
> +                     #t))))
> +         (stop
> +          #~(lambda (_)
> +              #$(with-pulseaudio-connection
> +                 #~sock
> +                 #~(begin
> +                     (display "unload-module module-rtp-recv\n"
> +                              sock)
> +                     #f))))
> +         (auto-start? #f))))
> +
> +(define home-pulseaudio-rtp-source-service-type
> +  (service-type
> +   (name 'pulseaudio-rtp-source)
> +   (extensions
> +    (list (service-extension home-shepherd-service-type
> +                             pulseaudio-rtp-source-shepherd-services)))
> +   (description
> +    "Define a PulseAudio source to receive audio broadcasted over RTP by
> +another PulseAudio instance.")
> +   (default-value %pulseaudio-rtp-multicast-address)))
> diff --git a/gnu/local.mk b/gnu/local.mk
> index dd1d546be5..55b5295439 100644
> --- a/gnu/local.mk
> +++ b/gnu/local.mk
> @@ -1,5 +1,5 @@
>  # GNU Guix --- Functional package management for GNU
> -# Copyright © 2012-2021, 2021-2022 Ludovic Courtès <ludo@gnu.org>
> +# Copyright © 2012-2023 Ludovic Courtès <ludo@gnu.org>
>  # Copyright © 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2022 Andreas Enge <andreas@enge.fr>
>  # Copyright © 2016 Mathieu Lirzin <mthl@gnu.org>
>  # Copyright © 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021 Mark H Weaver <mhw@netris.org>
> @@ -94,6 +94,7 @@ GNU_SYSTEM_MODULES =				\
>    %D%/home/services/pm.scm			\
>    %D%/home/services/shells.scm			\
>    %D%/home/services/shepherd.scm		\
> +  %D%/home/services/sound.scm			\
>    %D%/home/services/ssh.scm			\
>    %D%/home/services/mcron.scm			\
>    %D%/home/services/utils.scm			\
>
> base-commit: ff5fbcc19bce6e94ead0cc79b27ae8ed0307463d

Hi Ludo, thank you for the interesting services!

I expected them to have an ability to specify a port, but it seems it is
supported only in pipewire, but not in pulseaudio.
https://docs.pipewire.org/page_module_rtp_source.html

The patch looks good to me.
Ludovic Courtès March 5, 2023, 10:20 p.m. UTC | #2
Hi Andrew,

Pushed as 674d8933169e018efa3471e4eac52e5ea1e6afee.

Thanks for taking a look!

Ludo’.
diff mbox series

Patch

diff --git a/doc/guix.texi b/doc/guix.texi
index a7ef00f421..7716686f87 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -41513,15 +41513,16 @@  service with the @code{simple-service} procedure from @code{(gnu
 services)}.
 
 @menu
-* Essential Home Services::  Environment variables, packages, on-* scripts.
-* Shells: Shells Home Services.                        POSIX shells, Bash, Zsh.
-* Mcron: Mcron Home Service.                           Scheduled User's Job Execution.
-* Power Management: Power Management Home Services.    Services for battery power.
-* Shepherd: Shepherd Home Service.                     Managing User's Daemons.
-* SSH: Secure Shell.                                   Setting up the secure shell client.
-* Desktop: Desktop Home Services.                      Services for graphical environments.
-* Guix: Guix Home Services.                            Services for Guix.
-* Fonts: Fonts Home Services.                          Services for managing User's fonts.
+* Essential Home Services::     Environment variables, packages, on-* scripts.
+* Shells: Shells Home Services.  POSIX shells, Bash, Zsh.
+* Mcron: Mcron Home Service.    Scheduled User's Job Execution.
+* Power Management: Power Management Home Services.  Services for battery power.
+* Shepherd: Shepherd Home Service.  Managing User's Daemons.
+* SSH: Secure Shell.            Setting up the secure shell client.
+* Desktop: Desktop Home Services.  Services for graphical environments.
+* Guix: Guix Home Services.     Services for Guix.
+* Fonts: Fonts Home Services.   Services for managing User's fonts.
+* Sound: Sound Home Services.   Dealing with audio.
 @end menu
 @c In addition to that Home Services can provide
 
@@ -42435,6 +42436,101 @@  like this:
 @end lisp
 @end defvar
 
+@node Sound Home Services
+@subsection Sound Home Services
+
+The @code{(gnu home services sound)} module provides services related to
+sound support.
+
+@cindex PulseAudio, home service
+@cindex RTP, for PulseAudio
+
+The following services dynamically reconfigure the
+@uref{https://pulseaudio.org,PulseAudio sound server}: they let you
+toggle broadcast of audio output over the network using the
+@acronym{RTP, real-time transport protocol} and, correspondingly,
+playback of sound received over RTP.  Once
+@code{home-pulseaudio-rtp-sink-service-type} is among your home
+services, you can start broadcasting audio output by running this
+command:
+
+@example
+herd start pulseaudio-rtp-sink
+@end example
+
+You can then run a PulseAudio-capable mixer, such as @code{pavucontrol}
+or @code{pulsemixer} (both from the same-named package) to control which
+audio stream(s) should be sent to the RTP ``sink''.
+
+By default, audio is broadcasted to a multicast address: any device on
+the @acronym{LAN, local area network} receives it and may play it.
+Using multicast in this way puts a lot of pressure on the network and
+degrades its performance, so you may instead prefer sending to
+specifically one device.  The first way to do that is by specifying the
+IP address of the target device when starting the service:
+
+@example
+herd start pulseaudio-rtp-sink 192.168.1.42
+@end example
+
+The other option is to specify this IP address as the one to use by
+default in your home environment configuration:
+
+@lisp
+(service home-pulseaudio-rtp-sink-service-type
+         "192.168.1.42")
+@end lisp
+
+On the device where you intend to receive and play the RTP stream, you
+can use @code{home-pulseaudio-rtp-source-service-type}, like so:
+
+@lisp
+(service home-pulseaudio-rtp-source-service-type)
+@end lisp
+
+This will then let you start the receiving module for PulseAudio:
+
+@example
+herd start pulseaudio-rtp-source
+@end example
+
+Again, by default it will listen on the multicast address.  If, instead,
+you'd like it to listen for direct incoming connections, you can do that
+by running:
+
+@lisp
+(service home-pulseaudio-rtp-source-service-type
+         "0.0.0.0")
+@end lisp
+
+The reference of these services is given below.
+
+@defvar home-pulseaudio-rtp-sink-service-type
+@defvarx home-pulseaudio-rtp-source-service-type
+This is the type of the service to send, respectively receive, audio
+streams over @acronym{RTP, real-time transport protocol}.
+
+The value associated with this service is the IP address (a string)
+where to send, respectively receive, the audio stream.  By default,
+audio is sent/received on multicast address
+@code{%pulseaudio-rtp-multicast-address}.
+
+This service defines one Shepherd service: @code{pulseaudio-rtp-sink},
+respectively @code{pulseaudio-rtp-source}.  The service is not started
+by default, so you have to explicitly start it when you want to turn it
+on, as in this example:
+
+@example
+herd start pulseaudio-rtp-sink
+@end example
+
+Stopping the Shepherd service turns off broadcasting.
+@end defvar
+
+@defvar %pulseaudio-rtp-multicast-address
+This is the multicast address used by default by the two services above.
+@end defvar
+
 @node Invoking guix home
 @section Invoking @command{guix home}
 
diff --git a/gnu/home/services/sound.scm b/gnu/home/services/sound.scm
new file mode 100644
index 0000000000..22c1a99250
--- /dev/null
+++ b/gnu/home/services/sound.scm
@@ -0,0 +1,151 @@ 
+;;; GNU Guix --- Functional package management for GNU
+;;; Copyright © 2023 Ludovic Courtès <ludo@gnu.org>
+;;;
+;;; This file is part of GNU Guix.
+;;;
+;;; GNU Guix 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.
+;;;
+;;; GNU Guix 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 GNU Guix.  If not, see <http://www.gnu.org/licenses/>.
+
+(define-module (gnu home services sound)
+  #:use-module (gnu home services)
+  #:use-module (gnu home services shepherd)
+  #:use-module (guix records)
+  #:use-module (guix gexp)
+  #:use-module (srfi srfi-1)
+  #:use-module (ice-9 match)
+  #:export (home-pulseaudio-rtp-sink-service-type
+            home-pulseaudio-rtp-source-service-type
+            %pulseaudio-rtp-multicast-address))
+
+
+;;;
+;;; PulseAudio support.
+;;;
+
+(define (with-pulseaudio-connection sock exp)
+  ;; Wrap EXP in an expression where SOCK is bound to a socket connected to
+  ;; the user's PulseAudio command-line interface socket.
+  #~(let* ((#$sock (socket AF_UNIX SOCK_STREAM 0))
+           (pulse-user-file
+            (lambda (name)
+              (string-append "/run/user/" (number->string (getuid))
+                             "/pulse/" name)))
+           (file (pulse-user-file "cli")))
+      (let loop ((tries 0))
+        (catch #t
+          (lambda ()
+            (connect #$sock AF_UNIX file)
+            (let ((result #$exp))
+              (close-port #$sock)
+              result))
+          (lambda (key . args)
+            (if (and (eq? key 'system-error)
+                     (= ENOENT (system-error-errno (cons key args)))
+                     (< tries 3))
+                ;; The CLI socket doesn't exist yet, so send pulseaudio
+                ;; SIGUSR2 so that it creates it and listens to it.
+                (let ((pid (call-with-input-file (pulse-user-file "pid")
+                             read)))
+                  (when (and (integer? pid) (> pid 1))
+                    (kill pid SIGUSR2))
+                  ((@ (fibers) sleep) 1)
+                  (loop (+ tries 1)))
+                (begin
+                  (close-port #$sock)
+                  (apply throw key args))))))))
+
+(define %pulseaudio-rtp-multicast-address
+  ;; Default address used by 'module-rtp-sink' and 'module-rtp-recv'.  This is
+  ;; a multicast address, for the Session Announcement Protocol (SAP) and the
+  ;; Session Description Protocol (SDP).
+  "224.0.0.56")
+
+(define (pulseaudio-rtp-sink-shepherd-services destination-ip)
+  (list (shepherd-service
+         (provision '(pulseaudio-rtp-sink))
+         (start
+          #~(lambda* (#:optional (destination-ip #$destination-ip))
+              #$(with-pulseaudio-connection
+                 #~sock
+                 #~(begin
+                     (display "\
+load-module module-null-sink \
+sink_name=rtp sink_properties=\"device.description='RTP network output'\"\n"
+                              sock)
+                     (display (string-append "\
+load-module module-rtp-send source=rtp.monitor"
+                                             (if destination-ip
+                                                 (string-append
+                                                  " destination_ip="
+                                                  destination-ip)
+                                                 "")
+                                             "\n")
+                              sock)
+                     #t))))
+         (stop
+          #~(lambda (_)
+              #$(with-pulseaudio-connection
+                 #~sock
+                 #~(begin
+                     (display "unload-module module-rtp-send\n"
+                              sock)
+                     (display "unload-module module-null-sink\n"
+                              sock)
+                     #f))))
+         (auto-start? #f))))
+
+(define home-pulseaudio-rtp-sink-service-type
+  (service-type
+   (name 'pulseaudio-rtp-sink)
+   (extensions
+    (list (service-extension home-shepherd-service-type
+                             pulseaudio-rtp-sink-shepherd-services)))
+   (description
+    "Define a PulseAudio sink to broadcast audio output over RTP, which can
+then by played by another PulseAudio instance.")
+
+   ;; By default, send to the SAP multicast address, 224.0.0.56, which can be
+   ;; network-intensive.
+   (default-value %pulseaudio-rtp-multicast-address)))
+
+(define (pulseaudio-rtp-source-shepherd-services source-ip)
+  (list (shepherd-service
+         (provision '(pulseaudio-rtp-source))
+         (start
+          #~(lambda* (#:optional (source-ip #$source-ip))
+              #$(with-pulseaudio-connection
+                 #~sock
+                 #~(begin
+                     (format sock "\
+load-module module-rtp-recv sap_address=~a\n" source-ip)
+                     #t))))
+         (stop
+          #~(lambda (_)
+              #$(with-pulseaudio-connection
+                 #~sock
+                 #~(begin
+                     (display "unload-module module-rtp-recv\n"
+                              sock)
+                     #f))))
+         (auto-start? #f))))
+
+(define home-pulseaudio-rtp-source-service-type
+  (service-type
+   (name 'pulseaudio-rtp-source)
+   (extensions
+    (list (service-extension home-shepherd-service-type
+                             pulseaudio-rtp-source-shepherd-services)))
+   (description
+    "Define a PulseAudio source to receive audio broadcasted over RTP by
+another PulseAudio instance.")
+   (default-value %pulseaudio-rtp-multicast-address)))
diff --git a/gnu/local.mk b/gnu/local.mk
index dd1d546be5..55b5295439 100644
--- a/gnu/local.mk
+++ b/gnu/local.mk
@@ -1,5 +1,5 @@ 
 # GNU Guix --- Functional package management for GNU
-# Copyright © 2012-2021, 2021-2022 Ludovic Courtès <ludo@gnu.org>
+# Copyright © 2012-2023 Ludovic Courtès <ludo@gnu.org>
 # Copyright © 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2022 Andreas Enge <andreas@enge.fr>
 # Copyright © 2016 Mathieu Lirzin <mthl@gnu.org>
 # Copyright © 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021 Mark H Weaver <mhw@netris.org>
@@ -94,6 +94,7 @@  GNU_SYSTEM_MODULES =				\
   %D%/home/services/pm.scm			\
   %D%/home/services/shells.scm			\
   %D%/home/services/shepherd.scm		\
+  %D%/home/services/sound.scm			\
   %D%/home/services/ssh.scm			\
   %D%/home/services/mcron.scm			\
   %D%/home/services/utils.scm			\