diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 18eebef0bd8ab6addd818e95cc3a039bd0902b0f..e0fce1cf137d7ed6f4e75757444ea9a6a6869358 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -25,8 +25,8 @@ include:
 .fedora:
   variables:
     # Update this tag when you want to trigger a rebuild
-    FDO_DISTRIBUTION_TAG: '2023-05-31.0'
-    FDO_DISTRIBUTION_VERSION: '37'
+    FDO_DISTRIBUTION_TAG: '2023-11-22.0'
+    FDO_DISTRIBUTION_VERSION: '39'
     FDO_DISTRIBUTION_PACKAGES: >-
       alsa-lib-devel
       avahi-devel
@@ -75,6 +75,7 @@ include:
       pulseaudio-utils
       openal-soft
       readline-devel
+      pandoc
 # Uncommenting the following two lines and disabling the meson entry above
 # will re-enable use of Meson via pip but please consider using a newer distro
 # image first or making the build system compatible instead! This is because
diff --git a/NEWS b/NEWS
index 12ce28dc8e21dfda13d19ce84afc542c648b9042..76a75c23be5c3aba9efff8f72ff955bea6a74e10 100644
--- a/NEWS
+++ b/NEWS
@@ -1,3 +1,139 @@
+# PipeWire 1.0.0 (2023-11-26)
+
+The PipeWire project is immensely proud to announce the 1.0 release
+of PipeWire.
+
+It is API and ABI compatible with previous 0.3.x releases.
+
+ "PipeWire represents the next evolution of audio handling for Linux, taking
+ the best of both pro-audio (JACK) and desktop audio servers (PulseAudio) and
+ linking them into a single, seamless, powerful new system."
+    - Paul Davis, JACK and Ardour author
+
+ "What exciting times! PipeWire 1.0 is the culmination of 15 years of
+ Linux audio expertise, blending lessons from PulseAudio into a high-performance,
+ flexible, and user-friendly foundation for audio and multimedia on Linux.
+ I'm looking forward to the next decade of progress in the free software
+ consumer and professional audio space!."
+    - Arun Raghavan, PulseAudio developer/maintainer.
+
+ "I'm thrilled to witness the first stable release of PipeWire after five years
+ of collaboration with its remarkable community, pushing the boundaries of
+ multimedia integration in the Linux ecosystem one step further.”
+    - George Kiagiadakis, WirePlumber author
+
+ "From the beginning of the libcamera project, we have always seen
+ PipeWire as the solution to handle desktop and mobile integration and
+ give a seemless multimedia integration to users while providing security
+ features and resource sharing between applications."
+    - Kieran Bingham, libcamera author
+
+Happy Holidays!
+
+
+## Highlights
+  - Fix a memfd/dmabuf leak when uploading buffers while shutting down.
+  - Handle concurrent jack_port_get_buffer() calls because ardour seems to
+    be doing this.
+  - Improve time reporting (less jitter) in ALSA when using IRQ.
+  - Many doc improvements.
+
+## PipeWire
+  - Respect PIPEWIRE_DLCLOSE everywhere, remove pw_in_valgrind().
+  - Remove a warning when a client tries to change ignored properties.
+
+## Modules
+  - Fix a memfd/dmabuf leak when uploading buffers while shutting down.
+  - Fix a potential segfault when copying mix structures. (#3658)
+  - Avoid races in setrlimit in module-rt.
+  - Fix a memory leak in filter-chain.
+  - Set rtp.ptime on senders, not receivers.
+  - The ROC modules were ported to ROC 0.3
+
+## SPA
+  - Improve time reporting (less jitter) in ALSA when using IRQ. (#3657)
+  - Add latency param query in libcamera.
+  - Fix some compiler warnings.
+  - The EVL plugin was updated.
+
+## Bluetooth
+  - LC3 codec and compatibility improvements.
+
+## Pulse server
+  - Fix emission of events when a sink/source state changes. (#3660)
+
+## JACK
+  - Improve transport and time handling. Use unique ids to make consistent
+    snapshots of the current time and transport.
+  - Avoid enumerating port params that we are not going to use.
+  - Optimize buffer reuse.
+  - Handle concurrent jack_port_get_buffer() calls because ardour seems to
+    be doing this. (#3632)
+
+## Docs
+  - Many doc improvements.
+  - Add man pages for pw-dump, pw-loopback, modules, pipewire-pulse.
+  - Manpages are now made with Doxygen.
+  - Add docs for pulse-modules
+
+Older versions:
+
+
+# PipeWire 0.3.85 (2023-11-16)
+
+This is the fifth (and last) 1.0 release candidate that is API and ABI
+compatible with previous 0.3.x releases.
+
+## Highlights
+  - Fix an issue where a link could end up paused while not negotiated.
+  - Fix an infinite recursion issue when finding runnable nodes.
+  - Support XDG base directories when loading ACP config.
+  - Fix MIDI event recording preview in Ardour.
+  - Many more small fixes, cleanups and improvements.
+
+
+## PipeWire
+  - Fix an issue where a link could end up paused while not negotiated.
+    (#3619)
+  - Fix an infinite recursion issue when finding runnable nodes by stopping
+    the scan on feedback links around the driver. (#3621)
+  - The system service now has better socket permissions.
+
+## Modules
+  - Add support for uclamp. This allows the scheduler to make better informed
+    decisions about where tasks should be placed, and what pstate to set
+    for the CPU it is running on.
+  - Emit warnings when applications are not doing the right locking instead
+    of crashing.
+  - Improve media.name for RAOP sinks. (#3801)
+  - Support pause/resume in pipe-tunnel. (#3197)
+  - Remove time rlimit when probing for realtime to avoid SIGXCPU.
+
+## SPA
+  - Fix a bug where the resampler would be activated even when there is an
+    ALSA pitch element. (#3628)
+  - Improve resume from suspend in ALSA. (#3646)
+  - Add option to expose ALSA controls as prop params.
+  - Support XDG base directories when loading ACP config. This makes it possible
+    to override the ACP config files.
+
+## Bluetooth
+  - Schedule nodes in the same ISO group together.
+  - More BAP fixes and cleanups.
+
+## JACK
+  - Fix MIDI events from peer ports. This makes the MIDI event recording preview
+    of Ardour work correctly.
+
+## GStreamer
+  - Fix some error handling in the source and sink.
+
+## ALSA plugin
+  - Improve poll descriptor handling. (#3648)
+
+## Docs
+  - Many improvements to the layout and organization.
+
 # PipeWire 0.3.84 (2023-11-02)
 
 This is the fourth 1.0 release candidate that is API and ABI compatible
@@ -56,9 +192,6 @@ with previous 0.3.x releases.
 ## ALSA
   - The ALSA plugin now handles NULL values from mmap_areas. (#3600)
 
-Older versions:
-
-
 # PipeWire 0.3.83 (2023-10-19)
 
 This is the third 1.0 release candidate that is API and ABI compatible
diff --git a/doc/Doxyfile.in b/doc/Doxyfile.in
index 15f9ae1ba0f82f97bdc205f732a647bf94160b8b..fc3d0223bd073782b54cc99dc3e21ac165165ef5 100644
--- a/doc/Doxyfile.in
+++ b/doc/Doxyfile.in
@@ -1,7 +1,7 @@
 PROJECT_NAME           = PipeWire
 PROJECT_NUMBER         = @PACKAGE_VERSION@
 OUTPUT_DIRECTORY       = "@output_directory@"
-FULL_PATH_NAMES        = NO
+FULL_PATH_NAMES        = YES
 JAVADOC_AUTOBRIEF      = YES
 TAB_SIZE               = 8
 OPTIMIZE_OUTPUT_FOR_C  = YES
@@ -26,6 +26,9 @@ EXAMPLE_PATH           = "@top_srcdir@/src/examples" \
                          "@top_srcdir@/doc"
 EXAMPLE_PATTERNS       = "*.c"
 
+GENERATE_MAN           = YES
+MAN_EXTENSION          = 3
+
 REFERENCED_BY_RELATION = NO
 REFERENCES_RELATION    = NO
 IGNORE_PREFIX          = pw_ \
@@ -36,6 +39,9 @@ GENERATE_TREEVIEW      = YES
 SEARCHENGINE           = YES
 GENERATE_LATEX         = NO
 
+TOC_INCLUDE_HEADINGS   = 0
+LAYOUT_FILE            = @layout@
+
 MACRO_EXPANSION        = YES
 EXPAND_ONLY_PREDEF     = YES
 PREDEFINED             = PA_C_DECL_BEGIN= \
diff --git a/doc/DoxygenLayout.xml b/doc/DoxygenLayout.xml
new file mode 100644
index 0000000000000000000000000000000000000000..29378f9dad20a645138c1061e89a226f14da41d2
--- /dev/null
+++ b/doc/DoxygenLayout.xml
@@ -0,0 +1,238 @@
+<doxygenlayout version="1.0">
+  <navindex>
+    <tab type="mainpage" visible="yes" title=""/>
+    <tab type="pages" visible="yes" title="Pages" intro=""/>
+    <tab type="modules" visible="yes" title="API Reference" intro="" />
+    <tab type="namespaces" visible="no" title="">
+      <tab type="namespacelist" visible="yes" title="" intro=""/>
+      <tab type="namespacemembers" visible="yes" title="" intro=""/>
+    </tab>
+    <tab type="concepts" visible="no" title="">
+    </tab>
+    <tab type="interfaces" visible="no" title="">
+      <tab type="interfacelist" visible="yes" title="" intro=""/>
+      <tab type="interfaceindex" visible="$ALPHABETICAL_INDEX" title=""/>
+      <tab type="interfacehierarchy" visible="yes" title="" intro=""/>
+    </tab>
+    <tab type="classes" visible="yes" title="">
+      <tab type="classlist" visible="yes" title="" intro=""/>
+      <tab type="classindex" visible="yes" title=""/>
+      <tab type="hierarchy" visible="yes" title="" intro=""/>
+      <tab type="classmembers" visible="no" title="" intro=""/>
+    </tab>
+    <tab type="structs" visible="no" title="">
+      <tab type="structlist" visible="yes" title="" intro=""/>
+      <tab type="structindex" visible="$ALPHABETICAL_INDEX" title=""/>
+    </tab>
+    <tab type="exceptions" visible="no" title="">
+      <tab type="exceptionlist" visible="yes" title="" intro=""/>
+      <tab type="exceptionindex" visible="$ALPHABETICAL_INDEX" title=""/>
+      <tab type="exceptionhierarchy" visible="yes" title="" intro=""/>
+    </tab>
+    <tab type="files" visible="yes" title="">
+      <tab type="filelist" visible="yes" title="" intro=""/>
+      <tab type="globals" visible="no" title="" intro=""/>
+    </tab>
+    <tab type="examples" visible="yes" title="" intro=""/>
+  </navindex>
+
+  <!-- Layout definition for a class page -->
+  <class>
+    <briefdescription visible="yes"/>
+    <includes visible="$SHOW_HEADERFILE"/>
+    <inheritancegraph visible="$CLASS_GRAPH"/>
+    <collaborationgraph visible="$COLLABORATION_GRAPH"/>
+    <memberdecl>
+      <nestedclasses visible="yes" title=""/>
+      <publictypes title=""/>
+      <services title=""/>
+      <interfaces title=""/>
+      <publicslots title=""/>
+      <signals title=""/>
+      <publicmethods title=""/>
+      <publicstaticmethods title=""/>
+      <publicattributes title=""/>
+      <publicstaticattributes title=""/>
+      <protectedtypes title=""/>
+      <protectedslots title=""/>
+      <protectedmethods title=""/>
+      <protectedstaticmethods title=""/>
+      <protectedattributes title=""/>
+      <protectedstaticattributes title=""/>
+      <packagetypes title=""/>
+      <packagemethods title=""/>
+      <packagestaticmethods title=""/>
+      <packageattributes title=""/>
+      <packagestaticattributes title=""/>
+      <properties title=""/>
+      <events title=""/>
+      <privatetypes title=""/>
+      <privateslots title=""/>
+      <privatemethods title=""/>
+      <privatestaticmethods title=""/>
+      <privateattributes title=""/>
+      <privatestaticattributes title=""/>
+      <friends title=""/>
+      <related title="" subtitle=""/>
+      <membergroups visible="yes"/>
+    </memberdecl>
+    <detaileddescription title=""/>
+    <memberdef>
+      <inlineclasses title=""/>
+      <typedefs title=""/>
+      <enums title=""/>
+      <services title=""/>
+      <interfaces title=""/>
+      <constructors title=""/>
+      <functions title=""/>
+      <related title=""/>
+      <variables title=""/>
+      <properties title=""/>
+      <events title=""/>
+    </memberdef>
+    <allmemberslink visible="yes"/>
+    <usedfiles visible="$SHOW_USED_FILES"/>
+    <authorsection visible="yes"/>
+  </class>
+
+  <!-- Layout definition for a namespace page -->
+  <namespace>
+    <briefdescription visible="yes"/>
+    <memberdecl>
+      <nestednamespaces visible="yes" title=""/>
+      <constantgroups visible="yes" title=""/>
+      <interfaces visible="yes" title=""/>
+      <classes visible="yes" title=""/>
+      <concepts visible="yes" title=""/>
+      <structs visible="yes" title=""/>
+      <exceptions visible="yes" title=""/>
+      <typedefs title=""/>
+      <sequences title=""/>
+      <dictionaries title=""/>
+      <enums title=""/>
+      <functions title=""/>
+      <variables title=""/>
+      <membergroups visible="yes"/>
+    </memberdecl>
+    <detaileddescription title=""/>
+    <memberdef>
+      <inlineclasses title=""/>
+      <typedefs title=""/>
+      <sequences title=""/>
+      <dictionaries title=""/>
+      <enums title=""/>
+      <functions title=""/>
+      <variables title=""/>
+    </memberdef>
+    <authorsection visible="yes"/>
+  </namespace>
+
+  <!-- Layout definition for a concept page -->
+  <concept>
+    <briefdescription visible="yes"/>
+    <includes visible="$SHOW_HEADERFILE"/>
+    <definition visible="yes" title=""/>
+    <detaileddescription title=""/>
+    <authorsection visible="yes"/>
+  </concept>
+
+  <!-- Layout definition for a file page -->
+  <file>
+    <briefdescription visible="yes"/>
+    <includes visible="$SHOW_INCLUDE_FILES"/>
+    <includegraph visible="$INCLUDE_GRAPH"/>
+    <includedbygraph visible="$INCLUDED_BY_GRAPH"/>
+    <sourcelink visible="yes"/>
+    <memberdecl>
+      <interfaces visible="yes" title=""/>
+      <classes visible="yes" title=""/>
+      <structs visible="yes" title=""/>
+      <exceptions visible="yes" title=""/>
+      <namespaces visible="yes" title=""/>
+      <concepts visible="yes" title=""/>
+      <constantgroups visible="yes" title=""/>
+      <defines title=""/>
+      <typedefs title=""/>
+      <sequences title=""/>
+      <dictionaries title=""/>
+      <enums title=""/>
+      <functions title=""/>
+      <variables title=""/>
+      <membergroups visible="yes"/>
+    </memberdecl>
+    <detaileddescription title=""/>
+    <memberdef>
+      <inlineclasses title=""/>
+      <defines title=""/>
+      <typedefs title=""/>
+      <sequences title=""/>
+      <dictionaries title=""/>
+      <enums title=""/>
+      <functions title=""/>
+      <variables title=""/>
+    </memberdef>
+    <authorsection/>
+  </file>
+
+  <!-- Layout definition for a group page -->
+  <group>
+    <briefdescription visible="yes"/>
+    <groupgraph visible="$GROUP_GRAPHS"/>
+    <memberdecl>
+      <nestedgroups visible="yes" title=""/>
+      <dirs visible="yes" title=""/>
+      <files visible="yes" title=""/>
+      <namespaces visible="yes" title=""/>
+      <concepts visible="yes" title=""/>
+      <classes visible="yes" title=""/>
+      <typedefs title=""/>
+      <sequences title=""/>
+      <dictionaries title=""/>
+      <enums title=""/>
+      <enumvalues title=""/>
+      <defines title=""/>
+      <functions title=""/>
+      <variables title=""/>
+      <signals title=""/>
+      <publicslots title=""/>
+      <protectedslots title=""/>
+      <privateslots title=""/>
+      <events title=""/>
+      <properties title=""/>
+      <friends title=""/>
+      <membergroups visible="yes"/>
+    </memberdecl>
+    <detaileddescription title=""/>
+    <memberdef>
+      <pagedocs/>
+      <inlineclasses title=""/>
+      <typedefs title=""/>
+      <sequences title=""/>
+      <dictionaries title=""/>
+      <enums title=""/>
+      <enumvalues title=""/>
+      <defines title=""/>
+      <functions title=""/>
+      <variables title=""/>
+      <signals title=""/>
+      <publicslots title=""/>
+      <protectedslots title=""/>
+      <privateslots title=""/>
+      <events title=""/>
+      <properties title=""/>
+      <friends title=""/>
+    </memberdef>
+    <authorsection visible="yes"/>
+  </group>
+
+  <!-- Layout definition for a directory page -->
+  <directory>
+    <briefdescription visible="yes"/>
+    <directorygraph visible="yes"/>
+    <memberdecl>
+      <dirs visible="yes"/>
+      <files visible="yes"/>
+    </memberdecl>
+    <detaileddescription title=""/>
+  </directory>
+</doxygenlayout>
diff --git a/doc/custom.css b/doc/custom.css
index 43690cb81af90dce857487ab53ecb138b0593504..97f033f75d8e50de1e6ffb441ad9d727d3d2cc86 100644
--- a/doc/custom.css
+++ b/doc/custom.css
@@ -17,3 +17,24 @@
 		--fragment-link: #729fcf;
 	}
 }
+
+#nav-tree .arrow {
+    opacity: 1;
+    padding-right: 0.25em;
+}
+
+.textblock h1 {
+    font-size: 150%;
+}
+.textblock h2 {
+    font-size: 100%;
+}
+.textblock h3, .textblock h4, .textblock h5, .textblock h6 {
+    font-size: 100%;
+    font-style: italic;
+    font-size: medium;
+}
+
+.textblock dl.section dd {
+    margin-left: 2rem;
+}
diff --git a/doc/api.dox b/doc/dox/api/index.dox
similarity index 99%
rename from doc/api.dox
rename to doc/dox/api/index.dox
index 880127e90b0e1836b6c9a4c7bc124916ec7a829d..96268669c2d5f7b0d85987d5fac8467d0ba429fa 100644
--- a/doc/api.dox
+++ b/doc/dox/api/index.dox
@@ -59,6 +59,7 @@ digraph API {
 It is common for clients to use both the \ref api_pw_core and the \ref api_pw_impl
 and both APIs are provided by the same library.
 
+- \subpage page_spa
 - \subpage page_client_impl
 - \subpage page_proxy
 - \subpage page_streams
diff --git a/doc/spa-buffer.dox b/doc/dox/api/spa-buffer.dox
similarity index 100%
rename from doc/spa-buffer.dox
rename to doc/dox/api/spa-buffer.dox
diff --git a/doc/spa-design.dox b/doc/dox/api/spa-design.dox
similarity index 100%
rename from doc/spa-design.dox
rename to doc/dox/api/spa-design.dox
diff --git a/doc/spa-index.dox b/doc/dox/api/spa-index.dox
similarity index 100%
rename from doc/spa-index.dox
rename to doc/dox/api/spa-index.dox
diff --git a/doc/spa-plugins.dox b/doc/dox/api/spa-plugins.dox
similarity index 100%
rename from doc/spa-plugins.dox
rename to doc/dox/api/spa-plugins.dox
diff --git a/doc/spa-pod.dox b/doc/dox/api/spa-pod.dox
similarity index 100%
rename from doc/spa-pod.dox
rename to doc/dox/api/spa-pod.dox
diff --git a/doc/index.dox b/doc/dox/index.dox
similarity index 95%
rename from doc/index.dox
rename to doc/dox/index.dox
index 36f3ebafbb1e167d7430f42464ed1d8359d4af52..93998e2ec8bda95b434bdec4d776f037d33d9e00 100644
--- a/doc/index.dox
+++ b/doc/dox/index.dox
@@ -17,7 +17,7 @@ PipeWire ships with the following components:
 
 - A \ref page_daemon that implements the IPC and graph processing.
 - An example \ref page_session_manager that manages objects in the \ref page_daemon.
-- A set of \ref page_tools to introspect and use the \ref page_daemon.
+- A set of \ref page_programs to introspect and use the \ref page_daemon.
 - A \ref page_library to develop PipeWire applications and plugins (\ref
   page_tutorial "tutorial").
 - The \ref page_spa used by both the \ref page_daemon and in the \ref
diff --git a/doc/pipewire-access.dox b/doc/dox/internals/access.dox
similarity index 100%
rename from doc/pipewire-access.dox
rename to doc/dox/internals/access.dox
diff --git a/doc/pipewire-audio.dox b/doc/dox/internals/audio.dox
similarity index 100%
rename from doc/pipewire-audio.dox
rename to doc/dox/internals/audio.dox
diff --git a/doc/pipewire-daemon.dox b/doc/dox/internals/daemon.dox
similarity index 100%
rename from doc/pipewire-daemon.dox
rename to doc/dox/internals/daemon.dox
diff --git a/doc/pipewire-design.dox b/doc/dox/internals/design.dox
similarity index 100%
rename from doc/pipewire-design.dox
rename to doc/dox/internals/design.dox
diff --git a/doc/dma-buf.dox b/doc/dox/internals/dma-buf.dox
similarity index 100%
rename from doc/dma-buf.dox
rename to doc/dox/internals/dma-buf.dox
diff --git a/doc/pipewire.dox b/doc/dox/internals/index.dox
similarity index 85%
rename from doc/pipewire.dox
rename to doc/dox/internals/index.dox
index 551adc0f43cb60d04e4a5c6059c2dc432095a98c..f7152d61cb865c99111afd566b2522f3648da285 100644
--- a/doc/pipewire.dox
+++ b/doc/dox/internals/index.dox
@@ -1,4 +1,4 @@
-/** \page page_pipewire PipeWire Design
+/** \page page_internals Internals
 
 # Internals
 
@@ -17,10 +17,8 @@
 # Components
 
 - \subpage page_daemon
-- \subpage page_tools
 - \subpage page_session_manager
 
-
 # Backends
 
 - \subpage page_pulseaudio
diff --git a/doc/pipewire-library.dox b/doc/dox/internals/library.dox
similarity index 100%
rename from doc/pipewire-library.dox
rename to doc/dox/internals/library.dox
diff --git a/doc/pipewire-midi.dox b/doc/dox/internals/midi.dox
similarity index 100%
rename from doc/pipewire-midi.dox
rename to doc/dox/internals/midi.dox
diff --git a/doc/pipewire-objects-design.dox b/doc/dox/internals/objects.dox
similarity index 100%
rename from doc/pipewire-objects-design.dox
rename to doc/dox/internals/objects.dox
diff --git a/doc/pipewire-portal.dox b/doc/dox/internals/portal.dox
similarity index 96%
rename from doc/pipewire-portal.dox
rename to doc/dox/internals/portal.dox
index 721d9817d37c18f057560951bf796fffa08008e6..935e26161ddb9ad358dc405675e345569c38c34e 100644
--- a/doc/pipewire-portal.dox
+++ b/doc/dox/internals/portal.dox
@@ -13,7 +13,7 @@ client is a portal-managed client. PipeWire can detect and enforce
 extra permission checks on the portal managed clients.
 
 Once such portal is the [camera
-portal](https://flatpak.github.io/xdg-desktop-portal/portal-docs.html#gdbus-org.freedesktop.portal.Camera)
+portal](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Camera.html)
 that provides a PipeWire session to stream video from a camera.
 
 
@@ -176,7 +176,7 @@ The session manager listens for new clients to appear. It will use the
 from the portal the session manager checks the requested `media_roles` and
 enables or disables access to the respective PipeWire objects.
 It might have to consult a database to decide what is allowed, for example the
-[org.freedesktop.impl.portal.PermissionStore](https://flatpak.github.io/xdg-desktop-portal/portal-docs.html#gdbus-org.freedesktop.impl.portal.PermissionStore).
+[org.freedesktop.impl.portal.PermissionStore](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.impl.portal.PermissionStore.html).
 
 \dot
 strict digraph pw {
diff --git a/doc/pipewire-protocol.dox b/doc/dox/internals/protocol.dox
similarity index 88%
rename from doc/pipewire-protocol.dox
rename to doc/dox/internals/protocol.dox
index 8e943b2ade7959a69f2d6005c81c3ee4ba9983e8..9db6217c090f82c8fc6943ede56a1b6b11cda0e0 100644
--- a/doc/pipewire-protocol.dox
+++ b/doc/dox/internals/protocol.dox
@@ -7,7 +7,9 @@ The reference implementation uses unix sockets and is implemented in
 
 We document the messages here.
 
-# Message header
+\tableofcontents
+
+# Message header {#native-protocol-message-header}
 
 Each message on the unix socket contains a 16 bytes header and a
 variable length payload size:
@@ -54,7 +56,7 @@ The payload is a single POD see \ref page_spa_pod for details.
 
 After the payload, there is an optional footer POD object.
 
-# Making a connection
+# Making a connection {#native-protocol-making-connection}
 
 First a connection is made to a unix domain socket. By default, the socket is
 named as "pipewire-0" and searched in the following directories:
@@ -106,7 +108,7 @@ The client then sends client properties to the server.
 This completes the setup of the client. The newly connected client will
 appear in the registry at this point.
 
-# Core proxy/resource
+# Core proxy/resource {#native-protocol-core}
 
 The core is always the object with Id 0.
 
@@ -444,7 +446,7 @@ registry.
 - global_id: the global_id as it will appear in the registry.
 - props: the properties of the global
 
-# Registry proxy/resource
+# Registry proxy/resource {#native-protocol-registry}
 
 The registry is obtained with the GetRegistry method on the Core object.
 The Id depends on the new_id that was provided.
@@ -522,7 +524,7 @@ A global with id was removed
 
 - id: the global id that was removed.
 
-# PipeWire:Interface:Client
+# PipeWire:Interface:Client {#native-protocol-client}
 
 The client object represents a client connect to the PipeWire server.
 Permissions of the client can be managed.
@@ -638,7 +640,7 @@ Emitted as the reply of the GetPermissions method.
 - id: the global id of the object
 - permissions: the permission for the given id
 
-# PipeWire:Interface:Device
+# PipeWire:Interface:Device {#native-protocol-device}
 
 A device is an object that manages other devices or nodes.
 
@@ -745,7 +747,7 @@ Emitted as a result of EnumParams or SubscribeParams.
 - param: the parameter. The object type depends on the id
 
 
-# PipeWire:Interface:Factory
+# PipeWire:Interface:Factory {#native-protocol-factory}
 
 A factory is an object that allows one to create new objects.
 
@@ -781,7 +783,7 @@ Info is emitted when binding to the factory global or when the information chang
 - props: optional properties of the factory, valid when change_mask is (1<<0)
 
 
-# PipeWire:Interface:Link
+# PipeWire:Interface:Link {#native-protocol-link}
 
 A link is a connection between 2 ports.
 
@@ -827,7 +829,7 @@ Info is emitted when binding to the link global or when the information changed.
 - props: optional properties of the link, valid when change_mask is (1<<2)
 
 
-# PipeWire:Interface:Module
+# PipeWire:Interface:Module {#native-protocol-module}
 
 A Module provides dynamically loaded functionality
 
@@ -864,7 +866,7 @@ Info is emitted when binding to the module global or when the information change
 - props: optional properties of the module, valid when change_mask has (1<<0)
 
 
-# PipeWire:Interface:Node
+# PipeWire:Interface:Node {#native-protocol-node}
 
 A Node is a processing element in the graph
 
@@ -991,7 +993,7 @@ Emitted as a result of EnumParams or SubscribeParams.
 - param: the parameter. The object type depends on the id
 
 
-# PipeWire:Interface:Port
+# PipeWire:Interface:Port {#native-protocol-port}
 
 A port is part of a node and allows links with other ports.
 
@@ -1080,7 +1082,7 @@ Emitted as a result of EnumParams or SubscribeParams.
 - next: the index of the next parameter
 
 
-# PipeWire:Interface:ClientNode
+# PipeWire:Interface:ClientNode {#native-protocol-clientnode}
 
 The ClientNode object is created from the `client-node` factory that is provided
 by the `libpipewire-module-client-node` module.
@@ -1497,7 +1499,7 @@ ports of a node.
 - peer_id: the id of the peer port
 - props: optional properties
 
-# PipeWire:Interface:Metadata
+# PipeWire:Interface:Metadata {#native-protocol-metadata}
 
 Metadata is a shared database of settings and properties.
 
@@ -1550,7 +1552,7 @@ A metadata key changed. This is also emitted when binding to the metadata.
 - type: an optional type
 - value: a value
 
-# PipeWire:Interface:Profiler
+# PipeWire:Interface:Profiler {#native-protocol-profiler}
 
 The profiler object allows one to receive profiler information of the pipewire
 graph.
@@ -1570,4 +1572,120 @@ The profiler has no methods
 ```
 - object: a SPA_TYPE_OBJECT_Profiler object. See enum spa_profiler 
 
+
+# Footer {#native-protocol-footer}
+
+The message footer contains additional messages, not directed to the
+destination object defined by the `Id` field.
+
+The footer consists of a single POD, immediately following the payload
+POD.  The footer POD consists of a sequence of footer opcode Ids and
+footer payload Structs containing their arguments:
+
+```
+   Struct(
+      Id: opcode1,
+      Struct { ... },
+      Id: opcode2,
+      Struct { ... },
+      ...
+   )
+```
+
+The footer opcodes are separate for server-to-client (`core`) and
+client-to-server (`client`) directions.
+
+The message footer is processed before other parts of the message,
+including the object Id lookup.
+
+## Core Generation (Footer Opcode 0)
+
+```
+   Struct(
+      Long: registry_generation,
+   )
+```
+
+Indicates to the client what is the current registry generation
+number of the \ref pw_context on the server side.
+
+The server shall include this footer in the next message it sends that
+follows the increment of the registry generation number.
+
+\see \ref native-protocol-registry-generation
+
+## Client Generation (Footer Opcode 0)
+
+```
+   Struct(
+      Long: client_generation,
+   )
+```
+
+Indicates to the server what is the last registry generation number
+the client has processed.
+
+The client shall include this footer in the next message it sends,
+after it has processed an incoming message whose footer includes a
+registry generation update.
+
+\see \ref native-protocol-registry-generation
+
+# Registry generation  {#native-protocol-registry-generation}
+
+The registry generation is a 64-bit integer in the PipeWire server
+\ref pw_context that increments by one when the server allocates a new
+global \ref PW_KEY_OBJECT_ID for an object.
+
+The server keeps track of each global *id* as a tuple ( *id*, *object
+generation* ) where *object generation* is the registry generation
+value when the *id* was allocated.
+
+When an object is destroyed, its *id* value may be reallocated to a
+different object. Because the protocol is asynchronous, the
+object *id* alone is not sufficient to uniquely identify objects.
+
+The server looks up objects based on a tuple ( *id*, *generation* ) as
+follows:
+
+1. Look up the \ref pw_global based on the *id*.
+
+2. The lookup fails if there is no global for the *id*, or
+   if *generation* < *object generation*.
+
+The protocol message generally contains only the object *id*.  The
+registry generation part is passed around as follows:
+
+1. The server sends the current *registry generation* to clients in the
+   protocol footer, if it has changed.
+
+2. The clients keep track of the latest registry generation of the
+   messages they have processed. This is the *client generation*.
+
+3. Each client sends their *client generation* in the protocol footer
+   of the next message to the server, if its value has changed.
+
+4. The server keeps track for each client the *client generation* they
+   have sent back.
+
+5. In each protocol message received from client, the server considers
+   each object *id* as tuple ( *id*, *client generation* ).
+
+This allows the server to know if the object *id* the client refers to was
+already destroyed, but the client has not yet processed the message
+indicating that the *id* is gone. The server indicates failed lookups
+of this type with error code ESTALE to the client.
+
+If a client has not sent any *client generation* updates to the
+server, then the server will not do any registry generation checks in
+object lookups.  This is for backward compatibility only.
+
+The registry generation is an internal detail of the server
+implementation and the native protocol, and is not visible to clients
+in the PipeWire API.  To identify objects uniquely, clients can use
+\ref PW_KEY_OBJECT_SERIAL, which are unique for objects and not
+reused, unlike \ref PW_KEY_OBJECT_ID
+
+\see \ref PW_KEY_OBJECT_SERIAL
+
 */
diff --git a/doc/pulseaudio.dox b/doc/dox/internals/pulseaudio.dox
similarity index 100%
rename from doc/pulseaudio.dox
rename to doc/dox/internals/pulseaudio.dox
diff --git a/doc/pipewire-scheduling.dox b/doc/dox/internals/scheduling.dox
similarity index 92%
rename from doc/pipewire-scheduling.dox
rename to doc/dox/internals/scheduling.dox
index 6f6f061f33020119fb8ab889743ecca19dab5505..dffd2b3ff3bc1687328dc33a31c27f113830fc90 100644
--- a/doc/pipewire-scheduling.dox
+++ b/doc/dox/internals/scheduling.dox
@@ -6,6 +6,22 @@ Graph are constructed from linked nodes together with their ports. This
 results in a dependency graph between nodes. Special care is taken for
 loopback links so that the graph remains a directed graph.
 
+# Processing threads
+
+The server (and clients) have two processing threads:
+
+- A main thread that will do all IPC with clients and server and configures the
+  nodes in the graph for processing.
+- A (or more) data processing thread that only does the data processing.
+
+
+The data processing threads are given realtime priority and are designed to
+run with as little overhead as possible. All of the node resources such as
+buffers, io areas and metadata will be set up in shared memory before the
+node is scheduled to run.
+
+This document describes the processing that happens in the data processing
+thread after the main-thread has configured it.
 
 # Nodes
 
diff --git a/doc/pipewire-session-manager.dox b/doc/dox/internals/session-manager.dox
similarity index 100%
rename from doc/pipewire-session-manager.dox
rename to doc/dox/internals/session-manager.dox
diff --git a/doc/pipewire-modules.dox b/doc/dox/modules.dox
similarity index 98%
rename from doc/pipewire-modules.dox
rename to doc/dox/modules.dox
index 460b0934f1223405562584391a0439cc589eca8c..1eaaf4875027c9dbc7f663d1ce713f1915877794 100644
--- a/doc/pipewire-modules.dox
+++ b/doc/dox/modules.dox
@@ -1,4 +1,4 @@
-/** \page page_pipewire_modules PipeWire Modules
+/** \page page_modules Modules
 
 A PipeWire module is effectively a PipeWire client in an `.so` file that
 shares the \ref pw_context with the loading entity. Usually modules are
diff --git a/doc/overview.dox b/doc/dox/overview.dox
similarity index 100%
rename from doc/overview.dox
rename to doc/dox/overview.dox
diff --git a/doc/dox/programs/index.md b/doc/dox/programs/index.md
new file mode 100644
index 0000000000000000000000000000000000000000..4ed988aff8e7317a5259414273ac8100f7139fbd
--- /dev/null
+++ b/doc/dox/programs/index.md
@@ -0,0 +1,23 @@
+\page page_programs Programs
+
+Manual pages:
+
+- \subpage page_man_pipewire_1
+- \subpage page_man_pipewire_conf_5
+- \subpage page_man_pipewire-pulse_1
+- \subpage page_man_pipewire-pulse_conf_5
+- \subpage page_man_pipewire-pulse-modules_7
+- \subpage page_man_pw-cat_1
+- \subpage page_man_pw-cli_1
+- \subpage page_man_pw-config_1
+- \subpage page_man_pw-dot_1
+- \subpage page_man_pw-dump_1
+- \subpage page_man_pw-jack_1
+- \subpage page_man_pw-link_1
+- \subpage page_man_pw-loopback_1
+- \subpage page_man_pw-metadata_1
+- \subpage page_man_pw-mididump_1
+- \subpage page_man_pw-mon_1
+- \subpage page_man_pw-profiler_1
+- \subpage page_man_pw-top_1
+- \subpage page_man_libpipewire-modules_7
diff --git a/doc/dox/programs/libpipewire-modules.7.md b/doc/dox/programs/libpipewire-modules.7.md
new file mode 100644
index 0000000000000000000000000000000000000000..ae3f88be116d8e8644ad237804dcdfa579dcca27
--- /dev/null
+++ b/doc/dox/programs/libpipewire-modules.7.md
@@ -0,0 +1,44 @@
+\page page_man_libpipewire-modules_7 libpipewire-modules
+
+PipeWire modules
+
+# DESCRIPTION
+
+A PipeWire module is effectively a PipeWire client running inside
+`pipewire(1)` which can host multiple modules. Usually modules are
+loaded when they are listed in the configuration files. For example the
+default configuration file loads several modules:
+
+    context.modules = [
+        ...
+        # The native communication protocol.
+        {   name = libpipewire-module-protocol-native }
+
+        # The profile module. Allows application to access profiler
+        # and performance data. It provides an interface that is used
+        # by pw-top and pw-profiler.
+        {   name = libpipewire-module-profiler }
+
+        # Allows applications to create metadata objects. It creates
+        # a factory for Metadata objects.
+        {   name = libpipewire-module-metadata }
+
+        # Creates a factory for making devices that run in the
+        # context of the PipeWire server.
+        {   name = libpipewire-module-spa-device-factory }
+        ...
+    ]
+
+# KNOWN MODULES
+
+$(LIBPIPEWIRE_MODULES)
+
+# AUTHORS
+
+The PipeWire Developers <$(PACKAGE_BUGREPORT)>;
+PipeWire is available from <$(PACKAGE_URL)>
+
+# SEE ALSO
+
+\ref page_man_pipewire_1 "pipewire(1)",
+\ref page_man_pipewire_conf_5 "pipewire.conf(5)"
diff --git a/doc/dox/programs/pipewire-pulse-modules.7.md b/doc/dox/programs/pipewire-pulse-modules.7.md
new file mode 100644
index 0000000000000000000000000000000000000000..f31e3e07d6cffb8819826dc240e498a4dc90e61b
--- /dev/null
+++ b/doc/dox/programs/pipewire-pulse-modules.7.md
@@ -0,0 +1,32 @@
+\page page_man_pipewire-pulse-modules_7 pipewire-pulse-modules
+
+PipeWire Pulseaudio modules
+
+# DESCRIPTION
+
+PipeWire's Pulseaudio emulation implements several Pulseaudio modules.
+It only supports its own built-in modules, and cannot load external
+modules written for Pulseaudio.
+
+The built-in modules can be loaded using Pulseaudio client programs, for
+example `pactl load-module \<module-name\> \<module-options\>`.
+They can also added to `pipewire-pulse.conf`, typically by a
+drop-in file in `~/.config/pipewire/pipewire-pulse.conf.d/`
+containing the module name and its arguments
+
+    pulse.cmd = [
+       { cmd = "load-module" args = "module-null-sink sink_name=foo" flags = [ ] }
+    ]
+
+# KNOWN MODULES
+
+$(PIPEWIRE_PULSE_MODULES)
+
+# AUTHORS
+
+The PipeWire Developers <$(PACKAGE_BUGREPORT)>;
+PipeWire is available from <$(PACKAGE_URL)>
+
+# SEE ALSO
+
+\ref page_man_pipewire-pulse_1 "pipewire-pulse(1)"
diff --git a/doc/dox/programs/pipewire-pulse.1.md b/doc/dox/programs/pipewire-pulse.1.md
new file mode 100644
index 0000000000000000000000000000000000000000..90ed98f168c33c6b1b36cefe5604334099f3b4c6
--- /dev/null
+++ b/doc/dox/programs/pipewire-pulse.1.md
@@ -0,0 +1,40 @@
+\page page_man_pipewire-pulse_1 pipewire-pulse
+
+The PipeWire PulseAudio replacement
+
+# SYNOPSIS
+
+**pipewire-pulse** \[*options*\]
+
+# DESCRIPTION
+
+**pipewire-pulse** starts a PulseAudio-compatible daemon that integrates
+with the PipeWire media server, by running a pipewire process through a
+systemd service. This daemon is a drop-in replacement for the PulseAudio
+daemon.
+
+# OPTIONS
+
+\par -h | \--help
+Show help.
+
+\par -v | \--verbose
+Increase the verbosity by one level. This option may be specified
+multiple times.
+
+\par \--version
+Show version information.
+
+\par -c | \--config=FILE
+Load the given config file (Default: pipewire-pulse.conf).
+
+# AUTHORS
+
+The PipeWire Developers <$(PACKAGE_BUGREPORT)>;
+PipeWire is available from <$(PACKAGE_URL)>
+
+# SEE ALSO
+
+\ref page_man_pipewire-pulse_conf_5 "pipewire-pulse.conf(5)",
+\ref page_man_pipewire_1 "pipewire(1)",
+\ref page_man_pipewire-pulse-modules_7 "pipewire-pulse-modules(7)"
diff --git a/doc/dox/programs/pipewire-pulse.conf.5.md b/doc/dox/programs/pipewire-pulse.conf.5.md
new file mode 100644
index 0000000000000000000000000000000000000000..fe94352f37b9f8661760653fafc8677de29c0d20
--- /dev/null
+++ b/doc/dox/programs/pipewire-pulse.conf.5.md
@@ -0,0 +1,56 @@
+\page page_man_pipewire-pulse_conf_5 pipewire-pulse.conf
+
+The PipeWire Pulseaudio server configuration file
+
+# SYNOPSIS
+
+*$XDG_CONFIG_HOME/pipewire/pipewire-pulse.conf*
+
+*$(PIPEWIRE_CONFIG_DIR)/pipewire-pulse.conf*
+
+*$(PIPEWIRE_CONFDATADIR)/pipewire-pulse.conf*
+
+*$(PIPEWIRE_CONFDATADIR)/pipewire-pulse.conf.d/*
+
+*$(PIPEWIRE_CONFIG_DIR)/pipewire-pulse.conf.d/*
+
+*$XDG_CONFIG_HOME/pipewire/pipewire-pulse.conf.d/*
+
+# DESCRIPTION
+
+Configuration for PipeWire's PulseAudio-compatible daemon.
+
+The configuration file format is the same as for `pipewire.conf(5)`.
+There are additional sections for configuring `pipewire-pulse(1)`
+settings.
+
+# CONFIGURATION FILE SECTIONS
+
+\par pulse.properties
+Dictionary. These properties configure the PipeWire Pulseaudio server
+properties.
+
+\par pulse.cmd
+Array of dictionaries. A set of commands to be executed on startup.
+
+\par pulse.rules
+Array of dictionaries. A set of match rules and actions to apply to
+clients.
+
+See \ref page_module_protocol_pulse "libpipewire-module-protocol-pulse(7)"
+for the detailed description.
+
+In addition, the general PipeWire daemon configuration sections apply,
+see \ref page_man_pipewire_conf_5 "pipewire.conf(5)".
+
+# AUTHORS
+
+The PipeWire Developers <$(PACKAGE_BUGREPORT)>;
+PipeWire is available from <$(PACKAGE_URL)>
+
+# SEE ALSO
+
+\ref page_module_protocol_pulse "libpipewire-module-protocol-pulse(7)",
+\ref page_man_pipewire_conf_5 "pipewire.conf(5)",
+\ref page_man_pipewire-pulse_1 "pipewire-pulse(1)",
+\ref page_man_pipewire-pulse-modules_7 "pipewire-pulse-modules(7)"
diff --git a/doc/dox/programs/pipewire.1.md b/doc/dox/programs/pipewire.1.md
new file mode 100644
index 0000000000000000000000000000000000000000..9b6c389d5aa26edd80c9921c3570ef72970d09e9
--- /dev/null
+++ b/doc/dox/programs/pipewire.1.md
@@ -0,0 +1,44 @@
+\page page_man_pipewire_1 pipewire
+
+The PipeWire media server
+
+# SYNOPSIS
+
+**pipewire** \[*options*\]
+
+# DESCRIPTION
+
+PipeWire is a service that facilitates sharing of multimedia content
+between devices and applications.
+
+The **pipewire** daemon reads a config file that is further documented
+in \ref page_man_pipewire_conf_5 "pipewire.conf(5)" manual page.
+
+# OPTIONS
+
+\par -h | \--help
+Show help.
+
+\par -v | \--verbose
+Increase the verbosity by one level. This option may be specified
+multiple times.
+
+\par \--version
+Show version information.
+
+\par -c | \--config=FILE
+Load the given config file (Default: pipewire.conf).
+
+# AUTHORS
+
+The PipeWire Developers <$(PACKAGE_BUGREPORT)>;
+PipeWire is available from <$(PACKAGE_URL)>
+
+# SEE ALSO
+
+\ref page_man_pw-top_1 "pw-top(1)",
+\ref page_man_pw-dump_1 "pw-dump(1)",
+\ref page_man_pw-mon_1 "pw-mon(1)",
+\ref page_man_pw-cat_1 "pw-cat(1)",
+\ref page_man_pw-cli_1 "pw-cli(1)",
+\ref page_man_libpipewire-modules_7 "libpipewire-modules(7)"
diff --git a/doc/dox/programs/pipewire.conf.5.md b/doc/dox/programs/pipewire.conf.5.md
new file mode 100644
index 0000000000000000000000000000000000000000..4abe2160593108119ab89dee9adf706936c9ed23
--- /dev/null
+++ b/doc/dox/programs/pipewire.conf.5.md
@@ -0,0 +1,102 @@
+\page page_man_pipewire_conf_5 pipewire.conf
+
+The PipeWire server configuration file
+
+# SYNOPSIS
+
+*$XDG_CONFIG_HOME/pipewire/pipewire.conf*
+
+*$(PIPEWIRE_CONFIG_DIR)/pipewire.conf*
+
+*$(PIPEWIRE_CONFDATADIR)/pipewire.conf*
+
+*$(PIPEWIRE_CONFDATADIR)/pipewire.conf.d/*
+
+*$(PIPEWIRE_CONFIG_DIR)/pipewire.conf.d/*
+
+*$XDG_CONFIG_HOME/pipewire/pipewire.conf.d/*
+
+# DESCRIPTION
+
+PipeWire is a service that facilitates sharing of multimedia content
+between devices and applications.
+
+On startup, the daemon reads a main configuration file to configure
+itself. It executes a series of commands listed in the config file.
+
+The config files are loaded in the order listed in the
+[SYNOPSIS](#synopsis). The environment variables `PIPEWIRE_CONFIG_DIR`,
+`PIPEWIRE_CONFIG_PREFIX` and `PIPEWIRE_CONFIG_NAME` can be used to
+specify an alternative config directory, subdirectory and file
+respectively.
+
+Next to the configuration file can be a directory with the same name as
+the file with a `.d/` suffix. All directories in the
+[SYNOPSIS](#synopsis) directory search paths are traversed in the listed
+order and the contents of the `*.conf` files inside them are appended to
+the main configuration file as overrides. Object sections are merged and
+array sections are appended.
+
+# CONFIGURATION FILE FORMAT
+
+The configuration file format is grouped into sections. A section is
+either a dictionary, {}, or an array, \[\]. Dictionary and array entries
+are separated by whitespace and may be simple value assignment, an array
+or a dictionary. For example:
+```
+    name = value # simple assignment
+
+    name = { key1 = value1 key2 = value2 } # a dictionary with two entries
+
+    name = [ value1 value2 ] # an array with two entries
+
+    name = [ { k = v1 } { k = v2 } ] # an array of dictionaries
+```
+
+The configuration files can be expressed in full JSON syntax but for
+ease of use, a relaxed format may be used where:
+
+- `:` to delimit keys and values can be substuted by `=` or a space.
+- <tt>\"</tt> around keys and string can be omitted as long as no special
+  characters are used in the strings.
+- `,` to separate objects can be replaced with a whitespace character.
+- `#` can be used to start a comment until the line end
+
+# CONFIGURATION FILE SECTIONS
+
+\par context.properties
+Dictionary. These properties configure the PipeWire instance.
+
+\par context.spa-libs
+Dictionary. Maps plugin features with globs to a spa library.
+
+\par context.modules
+Array of dictionaries. Each entry in the array is a dictionary with the
+*name* of the module to load, including optional *args* and *flags*.
+Most modules support being loaded multiple times.
+
+\par context.objects
+Array of dictionaries. Each entry in the array is a dictionary
+containing the *factory* to create an object from and optional extra
+arguments specific to that factory.
+
+\par context.exec
+\parblock
+Array of dictionaries. Each entry in the array is dictionary containing
+the *path* of a program to execute on startup and optional *args*.
+
+This array used to contain an entry to start the session manager but
+this mode of operation has since been demoted to development aid. Avoid
+starting a session manager in this way in production environment.
+\endparblock
+
+# AUTHORS
+
+The PipeWire Developers <$(PACKAGE_BUGREPORT)>;
+PipeWire is available from <$(PACKAGE_URL)>
+
+# SEE ALSO
+
+\ref page_man_pipewire_1 "pipewire(1)",
+\ref page_man_pw-mon_1 "pw-mon(1)",
+\ref page_man_libpipewire-modules_7 "libpipewire-modules(7)"
diff --git a/doc/dox/programs/pw-cat.1.md b/doc/dox/programs/pw-cat.1.md
new file mode 100644
index 0000000000000000000000000000000000000000..4860c427f044af9490d11454d3d602222a0831f6
--- /dev/null
+++ b/doc/dox/programs/pw-cat.1.md
@@ -0,0 +1,163 @@
+\page page_man_pw-cat_1 pw-cat
+
+Play and record media with PipeWire
+
+# SYNOPSIS
+
+**pw-cat** \[*options*\] \[*FILE* \| -\]
+
+**pw-play** \[*options*\] \[*FILE* \| -\]
+
+**pw-record** \[*options*\] \[*FILE* \| -\]
+
+**pw-midiplay** \[*options*\] \[*FILE* \| -\]
+
+**pw-midirecord** \[*options*\] \[*FILE* \| -\]
+
+**pw-dsdplay** \[*options*\] \[*FILE* \| -\]
+
+# DESCRIPTION
+
+**pw-cat** is a simple tool for playing back or capturing raw or encoded
+media files on a PipeWire server. It understands all audio file formats
+supported by `libsndfile` for PCM capture and playback. When capturing
+PCM, the filename extension is used to guess the file format with the
+WAV file format as the default.
+
+It understands standard MIDI files for playback and recording. This tool
+will not render MIDI files, it will simply make the MIDI events
+available to the graph. You need a MIDI renderer such as qsynth,
+timidity or a hardware MIDI rendered to hear the MIDI.
+
+DSD playback is supported with the DSF file format. This tool will only
+work with native DSD capable hardware and will produce an error when no
+such hardware was found.
+
+When the *FILE* is - input and output will be raw data from STDIN and
+STDOUT respectively.
+
+# OPTIONS
+
+\par -h | \--help
+Show help.
+
+\par \--version
+Show version information.
+
+\par -v | \--verbose
+Verbose operation.
+
+\par -R | \--remote=NAME
+The name the *remote* instance to connect to. If left unspecified, a
+connection is made to the default PipeWire instance.
+
+\par -p | \--playback
+Playback mode. Read data from the specified file, and play it back. If
+the tool is called under the name **pw-play** or **pw-midiplay** this is
+the default.
+
+\par -r | \--record
+Recording mode. Capture data and write it to the specified file. If the
+tool is called under the name **pw-record** or **pw-midirecord** this is
+the default.
+
+\par -m | \--midi
+MIDI mode. *FILE* is a MIDI file. If the tool is called under the name
+**pw-midiplay** or **pw-midirecord** this is the default. Note that this
+program will *not* render the MIDI events into audible samples, it will
+simply provide the MIDI events in the graph. You need a separate MIDI
+renderer such as qsynth, timidity or a hardware renderer to hear the
+MIDI.
+
+\par -d | \--dsd
+DSD mode. *FILE* is a DSF file. If the tool is called under the name
+**pw-dsdplay** this is the default. Note that this program will *not*
+render the DSD audio. You need a DSD capable device to play DSD content
+or this program will exit with an error.
+
+\par \--media-type=VALUE
+Set the media type property (default Audio/Midi depending on mode). The
+media type is used by the session manager to select a suitable target to
+link to.
+
+\par \--media-category=VALUE
+Set the media category property (default Playback/Capture depending on
+mode). The media type is used by the session manager to select a
+suitable target to link to.
+
+\par \--media-role=VALUE
+Set the media role property (default Music). The media type is used by
+the session manager to select a suitable target to link to.
+
+\par \--target=VALUE
+\parblock
+Set a node target (default auto). The value can be:
+
+- **auto**: Automatically select (Default)
+
+- **0**: Don't try to link this node
+
+- <b>\<id\></b>: The object.serial or the node.name of a target node
+\endparblock
+
+\par \--latency=VALUE\[*units*\]
+\parblock
+Set the node latency (default 100ms)
+
+The latency determines the minimum amount of time it takes for a sample
+to travel from application to device (playback) and from device to
+application (capture).
+
+The latency determines the size of the buffers that the application will
+be able to fill. Lower latency means smaller buffers but higher
+overhead. Higher latency means larger buffers and lower overhead.
+
+Units can be **s** for seconds, **ms** for milliseconds, **us** for
+microseconds, **ns** for nanoseconds. If no units are given, the latency
+value is samples with the samplerate of the file.
+\endparblock
+
+\par -P | \--properties=VALUE
+Set extra stream properties as a JSON object.
+
+\par -q | \--quality=VALUE
+Resampler quality. When the samplerate of the source or destination file
+does not match the samplerate of the server, the data will be resampled.
+Higher quality uses more CPU. Values between 0 and 15 are allowed, the
+default quality is 4.
+
+\par \--rate=VALUE
+The sample rate, default 48000.
+
+\par \--channels=VALUE
+The number of channels, default 2.
+
+\par \--channel-map=VALUE
+The channelmap. Possible values include: **mono**, **stereo**,
+**surround-21**, **quad**, **surround-22**, **surround-40**,
+**surround-31**, **surround-41**, **surround-50**, **surround-51**,
+**surround-51r**, **surround-70**, **surround-71** or a comma separated
+list of channel names: **FL**, **FR**, **FC**, **LFE**, **SL**, **SR**,
+**FLC**, **FRC**, **RC**, **RL**, **RR**, **TC**, **TFL**, **TFC**,
+**TFR**, **TRL**, **TRC**, **TRR**, **RLC**, **RRC**, **FLW**, **FRW**,
+**LFE2**, **FLH**, **FCH**, **FRH**, **TFLC**, **TFRC**, **TSL**,
+**TSR**, **LLFR**, **RLFE**, **BC**, **BLC**, **BRC**
+
+\par \--format=VALUE
+The sample format to use. One of: **u8**, **s8**, **s16** (default),
+**s24**, **s32**, **f32**, **f64**.
+
+\par \--volume=VALUE
+The stream volume, default 1.000. Depending on the locale you have
+configured, "," or "." may be used as a decimal separator. Check with
+**locale** command.
+
+# AUTHORS
+
+The PipeWire Developers <$(PACKAGE_BUGREPORT)>;
+PipeWire is available from <$(PACKAGE_URL)>
+
+# SEE ALSO
+
+\ref page_man_pipewire_1 "pipewire(1)",
+\ref page_man_pw-mon_1 "pw-mon(1)",
diff --git a/doc/dox/programs/pw-cli.1.md b/doc/dox/programs/pw-cli.1.md
new file mode 100644
index 0000000000000000000000000000000000000000..0108ef608f868b8286f161ad8aa098c11fc0b577
--- /dev/null
+++ b/doc/dox/programs/pw-cli.1.md
@@ -0,0 +1,189 @@
+\page page_man_pw-cli_1 pw-cli
+
+The PipeWire Command Line Interface
+
+# SYNOPSIS
+
+**pw-cli** \[*command*\]
+
+# DESCRIPTION
+
+Interact with a PipeWire instance.
+
+When a command is given, **pw-cli** will execute the command and exit
+
+When no command is given, **pw-cli** starts an interactive session with
+the default PipeWire instance *pipewire-0*.
+
+Connections to other, remote instances can be made. The current instance
+name is displayed at the prompt.
+
+Note that **pw-cli** also creates a local PipeWire instance. Some
+commands operate on the current (remote) instance and some on the local
+instance, such as module loading.
+
+Use the 'help' command to list the available commands.
+
+# GENERAL COMMANDS
+
+\par help | h
+Show a quick help on the commands available. It also lists the aliases
+for many commands.
+
+\par quit | q
+Exit from **pw-cli**
+
+# MODULE MANAGEMENT
+
+Modules are loaded and unloaded in the local instance, thus the pw-cli
+binary itself and can add functionality or objects to the local
+instance. It is not possible in PipeWire to load modules in another
+instance.
+
+\par load-module *name* \[*arguments...*\]
+\parblock
+Load a module specified by its name and arguments in the local instance.
+For most modules it is OK to be loaded more than once.
+
+This command returns a module variable that can be used to unload the
+module.
+
+The locally module is *not* visible in the remote instance. It is not
+possible in PipeWire to load modules in a remote instance.
+\endparblock
+
+\par unload-module *module-var*
+Unload a module, specified either by its variable.
+
+# OBJECT INTROSPECTION
+
+\par list-objects
+List the objects of the current instance.
+
+Objects are listed with their *id*, *type* and *version*.
+
+\par info *id* | *all*
+Get information about a specific object or *all* objects.
+
+Requesting info about an object will also notify you of changes.
+
+# WORKING WITH REMOTES
+
+\par connect \[*remote-name*\]
+\parblock
+Connect to a remote instance and make this the new current instance.
+
+If no remote name is specified, a connection is made to the default
+remote instance, usually *pipewire-0*.
+
+The special remote name called *internal* can be used to connect to the
+local **pw-cli** PipeWire instance.
+
+This command returns a remote var that can be used to disconnect or
+switch remotes.
+\endparblock
+
+\par disconnect \[*remote-var*\]
+\parblock
+Disconnect from a *remote instance*.
+
+If no remote name is specified, the current instance is disconnected.
+\endparblock
+
+\par list-remotes
+List all *remote instances*.
+
+\par switch-remote \[*remote-var*\]
+\parblock
+Make the specified *remote* the current instance.
+
+If no remote name is specified, the first instance is made current.
+\endparblock
+
+# NODE MANAGEMENT
+
+\par create-node *factory-name* \[*properties...*\]
+\parblock
+Create a node from a factory in the current instance.
+
+Properties are key=value pairs separated by whitespace.
+
+This command returns a *node variable*.
+\endparblock
+
+\par export-node *node-id* \[*remote-var*\]
+Export a node from the local instance to the specified instance. When no
+instance is specified, the node will be exported to the current
+instance.
+
+# DEVICE MANAGEMENT
+
+\par create-device *factory-name* \[*properties...*\]
+\parblock
+Create a device from a factory in the current instance.
+
+Properties are key=value pairs separated by whitespace.
+
+This command returns a *device variable*.
+\endparblock
+
+# LINK MANAGEMENT
+
+\par create-link *node-id* *port-id* *node-id* *port-id* \[*properties...*\]
+\parblock
+Create a link between 2 nodes and ports.
+
+Port *ids* can be *-1* to automatically select an available port.
+
+Properties are key=value pairs separated by whitespace.
+
+This command returns a *link variable*.
+\endparblock
+
+# GLOBALS MANAGEMENT
+
+\par destroy *object-id*
+Destroy a global object.
+
+# PARAMETER MANAGEMENT
+
+\par enum-params *object-id* *param-id*
+\parblock
+Enumerate params of an object.
+
+*param-id* can also be given as the param short name.
+\endparblock
+
+\par set-param *object-id* *param-id* *param-json*
+\parblock
+Set param of an object.
+
+*param-id* can also be given as the param short name.
+\endparblock
+
+# PERMISSION MANAGEMENT
+
+\par permissions *client-id* *object-id* *permission*
+\parblock
+Set permissions for a client.
+
+*object-id* can be *-1* to set the default permissions.
+\endparblock
+
+\par get-permissions *client-id*
+Get permissions of a client.
+
+# COMMAND MANAGEMENT
+
+\par send-command *object-id*
+Send a command to an object.
+
+# AUTHORS
+
+The PipeWire Developers <$(PACKAGE_BUGREPORT)>;
+PipeWire is available from <$(PACKAGE_URL)>
+
+# SEE ALSO
+
+\ref page_man_pipewire_1 "pipewire(1)",
+\ref page_man_pw-mon_1 "pw-mon(1)",
diff --git a/doc/dox/programs/pw-config.1.md b/doc/dox/programs/pw-config.1.md
new file mode 100644
index 0000000000000000000000000000000000000000..898f12eb3f708df8bc98035d170d32a3f92c79a5
--- /dev/null
+++ b/doc/dox/programs/pw-config.1.md
@@ -0,0 +1,97 @@
+\page page_man_pw-config_1 pw-config
+
+Debug PipeWire Config parsing
+
+# SYNOPSIS
+
+**pw-config** \[*options*\] paths
+
+**pw-config** \[*options*\] list \[*SECTION*\]
+
+**pw-config** \[*options*\] merge *SECTION*
+
+# DESCRIPTION
+
+List config paths and config sections and display the parsed output.
+
+This tool can be used to get an overview of the config file that will be
+parsed by the PipeWire server and clients.
+
+# COMMON OPTIONS
+
+\par -h | \--help
+Show help.
+
+\par \--version
+Show version information.
+
+\par -n | \--name=NAME
+Config Name (default 'pipewire.conf')
+
+\par -p | \--prefix=PREFIX
+Config Prefix (default '')
+
+\par -L | \--no-newline
+Omit newlines after values
+
+\par -r | \--recurse
+Reformat config sections recursively
+
+\par -N | \--no-colors
+Disable color output
+
+\par -C | \-color\[=WHEN\]
+whether to enable color support. WHEN is
+*never*, *always*, or *auto*
+
+# LISTING PATHS
+
+Specify the paths command. It will display all the config files that
+will be parsed and in what order.
+
+# LISTING CONFIG SECTIONS
+
+Specify the list command with an optional *SECTION* to list the
+configuration fragments used for *SECTION*. Without a *SECTION*, all
+sections will be listed.
+
+Use the -r options to reformat the sections.
+
+# MERGING A CONFIG SECTION
+
+With the merge option and a *SECTION*, pw-config will merge all config
+files into a merged config section and dump the results. This will be
+the section used by the client or server.
+
+Use the -r options to reformat the sections.
+
+# EXAMPLES
+
+\par pw-config
+List all config files that will be used
+
+\par pw-config -n pipewire-pulse.conf
+List all config files that will be used by the PipeWire pulseaudio
+server.
+
+\par pw-config -n pipewire-pulse.conf list
+List all config sections used by the PipeWire pulseaudio server
+
+\par pw-config -n jack.conf list context.properties
+List the context.properties fragments used by the JACK clients
+
+\par pw-config -n jack.conf merge context.properties
+List the merged context.properties used by the JACK clients
+
+\par pw-config -n pipewire.conf -r merge context.modules
+List the merged context.modules used by the PipeWire server and reformat
+
+# AUTHORS
+
+The PipeWire Developers <$(PACKAGE_BUGREPORT)>;
+PipeWire is available from <$(PACKAGE_URL)>
+
+# SEE ALSO
+
+\ref page_man_pipewire_1 "pipewire(1)",
+\ref page_man_pw-dump_1 "pw-dump(1)",
diff --git a/doc/dox/programs/pw-dot.1.md b/doc/dox/programs/pw-dot.1.md
new file mode 100644
index 0000000000000000000000000000000000000000..ab30c0d814f351f16041c91707054d9b00eacdd7
--- /dev/null
+++ b/doc/dox/programs/pw-dot.1.md
@@ -0,0 +1,56 @@
+\page page_man_pw-dot_1 pw-dot
+
+The PipeWire dot graph dump
+
+# SYNOPSIS
+
+**pw-dot** \[*options*\]
+
+# DESCRIPTION
+
+Create a .dot file of the PipeWire graph.
+
+The .dot file can then be visualized with a tool like **dotty** or
+rendered to a PNG file with `dot -Tpng pw.dot -o pw.png`.
+
+# OPTIONS
+
+\par -r | \--remote=NAME
+The name the remote instance to connect to. If left unspecified, a
+connection is made to the default PipeWire instance.
+
+\par -h | \--help
+Show help.
+
+\par \--version
+Show version information.
+
+\par -a | \--all
+Show all object types.
+
+\par -s | \--smart
+Show linked objects only.
+
+\par -d | \--detail
+Show all object properties.
+
+\par -o FILE | \--output=FILE
+Output file name (Default pw.dot). Use - for stdout.
+
+\par -L | \--lr
+Lay the graph from left to right, instead of dot's default top to
+bottom.
+
+\par -9 | \--90
+Lay the graph using 90-degree angles in edges.
+
+# AUTHORS
+
+The PipeWire Developers <$(PACKAGE_BUGREPORT)>;
+PipeWire is available from <$(PACKAGE_URL)>
+
+# SEE ALSO
+
+\ref page_man_pipewire_1 "pipewire(1)",
+\ref page_man_pw-cli_1 "pw-cli(1)",
+\ref page_man_pw-mon_1 "pw-mon(1)",
diff --git a/doc/dox/programs/pw-dump.1.md b/doc/dox/programs/pw-dump.1.md
new file mode 100644
index 0000000000000000000000000000000000000000..cf507e597d22d27b2089ccac04251ef41a485dc6
--- /dev/null
+++ b/doc/dox/programs/pw-dump.1.md
@@ -0,0 +1,43 @@
+\page page_man_pw-dump_1 pw-dump
+
+The PipeWire state dumper
+
+# SYNOPSIS
+
+**pw-dump** \[*options*\]
+
+# DESCRIPTION
+
+The *pw-dump* program produces a representation of the current PipeWire
+state as JSON, including the information on nodes, devices, modules,
+ports, and other objects.
+
+# OPTIONS
+
+\par -h | \--help
+Show help.
+
+\par -r | \--remote=NAME
+The name of the *remote* instance to dump. If left unspecified, a
+connection is made to the default PipeWire instance.
+
+\par -m | \--monitor
+Monitor PipeWire state changes, and output JSON arrays describing
+changes.
+
+\par -N | \--no-colors
+Disable color output.
+
+\par -C | \--color=WHEN
+Whether to enable color support. WHEN is `never`, `always`, or `auto`.
+
+# AUTHORS
+
+The PipeWire Developers <$(PACKAGE_BUGREPORT)>;
+PipeWire is available from <$(PACKAGE_URL)>
+
+# SEE ALSO
+
+\ref page_man_pipewire_1 "pipewire(1)",
+\ref page_man_pw-cli_1 "pw-cli(1)",
+\ref page_man_pw-top_1 "pw-top(1)",
diff --git a/doc/dox/programs/pw-jack.1.md b/doc/dox/programs/pw-jack.1.md
new file mode 100644
index 0000000000000000000000000000000000000000..f043d2130bbfd251a31b3a2d9d6a980f524b9df3
--- /dev/null
+++ b/doc/dox/programs/pw-jack.1.md
@@ -0,0 +1,45 @@
+\page page_man_pw-jack_1 pw-jack
+
+Use PipeWire instead of JACK
+
+# SYNOPSIS
+
+**pw-jack** \[*options*\] *COMMAND* \[*FILE*\]
+
+# DESCRIPTION
+
+**pw-jack** modifies the `LD_LIBRARY_PATH` environment variable so that
+applications will load PipeWire's reimplementation of the JACK client
+libraries instead of JACK's own libraries. This results in JACK clients
+being redirected to PipeWire.
+
+If PipeWire's reimplementation of the JACK client libraries has been
+installed as a system-wide replacement for JACK's own libraries, then
+the whole system already behaves in that way, in which case **pw-jack**
+has no practical effect.
+
+# OPTIONS
+
+\par -h
+Show help.
+
+\par -r NAME
+The name of the remote instance to connect to. If left unspecified, a
+connection is made to the default PipeWire instance.
+
+\par -v
+Verbose operation.
+
+# EXAMPLES
+
+\par pw-jack sndfile-jackplay /usr/share/sounds/freedesktop/stereo/bell.oga
+
+# AUTHORS
+
+The PipeWire Developers <$(PACKAGE_BUGREPORT)>;
+PipeWire is available from <$(PACKAGE_URL)>
+
+# SEE ALSO
+
+\ref page_man_pipewire_1 "pipewire(1)",
+**jackd(1)**,
diff --git a/doc/dox/programs/pw-link.1.md b/doc/dox/programs/pw-link.1.md
new file mode 100644
index 0000000000000000000000000000000000000000..aff53d600daf99cab9dcb07c89ff31e483d81758
--- /dev/null
+++ b/doc/dox/programs/pw-link.1.md
@@ -0,0 +1,135 @@
+\page page_man_pw-link_1 pw-link
+
+The PipeWire Link Command
+
+# SYNOPSIS
+
+**pw-link** \[*options*\] -o-l \[*out-pattern*\] \[*in-pattern*\]
+
+**pw-link** \[*options*\] *output* *input*
+
+**pw-link** \[*options*\] -d *output* *input*
+
+**pw-link** \[*options*\] -d *link-id*
+
+# DESCRIPTION
+
+List, create and destroy links between PipeWire ports.
+
+# COMMON OPTIONS
+
+\par -r | \--remote=NAME
+The name the *remote* instance to monitor. If left unspecified, a
+connection is made to the default PipeWire instance.
+
+\par -h | \--help
+Show help.
+
+\par \--version
+Show version information.
+
+# LISTING PORTS AND LINKS
+
+Specify one of -o, -i or -l to list the matching optional input and
+output ports and their links.
+
+\par -o | \--output
+List output ports
+
+\par -i | \--input
+List output ports
+
+\par -l | \--links
+List links
+
+\par -m | \--monitor
+Monitor links and ports. **pw-link** will not exit but monitor and print
+new and destroyed ports or links.
+
+\par -I | \--id
+List IDs. Also list the unique link and port ids.
+
+\par -v | \--verbose
+Verbose port properties. Also list the port-object-path and the
+port-alias.
+
+# CONNECTING PORTS
+
+Without any list option (-i, -o or -l), the given ports will be linked.
+Valid port specifications are:
+
+*port-id*
+
+As obtained with the -I option when listing ports.
+
+*node-name:port-name*
+
+As obtained when listing ports.
+
+*port-object-path*
+
+As obtained from the first alternative name for the port when listing
+them with the -v option.
+
+*port-alias*
+
+As obtained from the second alternative name for the ports when listing
+them with the -v option.
+
+Extra options when linking can be given:
+
+\par -L | \--linger
+Linger. Will create a link that exists after **pw-link** is destroyed.
+This is the default behaviour, unless the -m option is given.
+
+\par -P | \--passive
+Passive link. A passive link will keep both nodes it links inactive
+unless another non-passive link is activating the nodes. You can use
+this to link a sink to a filter and have them both suspended when
+nothing else is linked to either of them.
+
+\par -p | \--props=PROPS
+Properties as JSON object. Give extra properties when creaing the link.
+
+# DISCONNECTING PORTS
+
+When the -d option is given, an existing link between port is destroyed.
+
+To disconnect port, a single *link-id*, as obtained when listing links
+with the -I option, or two port specifications can be given. See the
+connecting ports section for valid port specifications.
+
+\par -d | \--disconnect
+Disconnect ports
+
+# EXAMPLES
+
+**pw-link** -iol
+
+List all port and their links.
+
+**pw-link** -lm
+
+List all links and monitor changes until **pw-link** is stopped.
+
+**pw-link** paplay:output_FL alsa_output.pci-0000_00_1b.0.analog-stereo:playback_FL
+
+Link the given output port to the input port.
+
+**pw-link** -lI
+
+List links and their Id.
+
+**pw-link** -d 89
+
+Destroy the link with id 89.
+
+# AUTHORS
+
+The PipeWire Developers <$(PACKAGE_BUGREPORT)>;
+PipeWire is available from <$(PACKAGE_URL)>
+
+# SEE ALSO
+
+\ref page_man_pipewire_1 "pipewire(1)",
+\ref page_man_pw-cli_1 "pw-cli(1)"
diff --git a/doc/dox/programs/pw-loopback.1.md b/doc/dox/programs/pw-loopback.1.md
new file mode 100644
index 0000000000000000000000000000000000000000..98293430b8d76a3178ac7385a24f299cf821ff10
--- /dev/null
+++ b/doc/dox/programs/pw-loopback.1.md
@@ -0,0 +1,67 @@
+\page page_man_pw-loopback_1 pw-loopback
+
+PipeWire loopback client
+
+# SYNOPSIS
+
+**pw-loopback** \[*options*\]
+
+# DESCRIPTION
+
+The *pw-loopback* program is a PipeWire client that uses the PipeWire
+loopback module to create loopback nodes, with configuration given via
+the command-line options.
+
+# OPTIONS
+
+\par -h | \--help
+Show help.
+
+\par -r | \--remote=NAME
+The name of the *remote* instance to connect to. If left unspecified, a
+connection is made to the default PipeWire instance.
+
+\par -n | \--name=NAME
+Name of the loopback node
+
+\par -g | \--group=NAME
+Name of the loopback node group
+
+\par -c | \--channels=NUMBER
+Number of channels to provide
+
+\par -m | \--channel-map=MAP
+Channel map (default `[ FL, FR ]`)
+
+\par -l | \--latency=LATENCY
+Desired latency in ms
+
+\par -d | \--delay=DELAY
+Added delay in seconds (floating point allowed)
+
+\par -C | \--capture=TARGET
+Target device to capture from
+
+\par -P | \--playback=TARGET
+Target device to play to
+
+\par \--capture-props=PROPS
+Wanted properties of capture node (in JSON)
+
+\par \--playback-props=PROPS
+Wanted properties of capture node (in JSON)
+
+# AUTHORS
+
+The PipeWire Developers <$(PACKAGE_BUGREPORT)>;
+PipeWire is available from <$(PACKAGE_URL)>
+
+# SEE ALSO
+
+\ref page_man_pipewire_1 "pipewire(1)",
+\ref page_man_pw-cat_1 "pw-cat(1)",
+**pactl(1)**
+
+Other ways to create loopback nodes are adding the loopback module in
+the configuration of a PipeWire daemon, or loading the loopback module
+using Pulseaudio commands (`pactl load-module module-loopback ...`).
diff --git a/doc/dox/programs/pw-metadata.1.md b/doc/dox/programs/pw-metadata.1.md
new file mode 100644
index 0000000000000000000000000000000000000000..77078de1e62667f7e5700a1b47582a08c073c5e0
--- /dev/null
+++ b/doc/dox/programs/pw-metadata.1.md
@@ -0,0 +1,73 @@
+\page page_man_pw-metadata_1 pw-metadata
+
+The PipeWire metadata
+
+# SYNOPSIS
+
+**pw-metadata** \[*options*\] \[*id* \[*key* \[*value* \[*type* \] \] \] \]
+
+# DESCRIPTION
+
+Monitor, set and delete metadata on PipeWire objects.
+
+Metadata are key/type/value triplets attached to objects identified by
+*id*. The metadata is shared between all applications binding to the
+same metadata object. When an object is destroyed, all its metadata is
+automatically removed.
+
+When no *value* is given, **pw-metadata** will query and log the
+metadata matching the optional arguments *id* and *key*. Without any
+arguments, all metadata is displayed.
+
+When *value* is given, **pw-metadata** will set the metadata for *id*
+and *key* to *value* and an optional *type*.
+
+# OPTIONS
+
+\par -r | \--remote=NAME
+The name the remote instance to use. If left unspecified, a connection
+is made to the default PipeWire instance.
+
+\par -h | \--help
+Show help.
+
+\par \--version
+Show version information.
+
+\par -l | \--list
+List available metadata objects
+
+\par -m | \--monitor
+Keeps running and log the changes to the metadata.
+
+\par -d | \--delete
+Delete all metadata for *id* or for the specified *key* of object *id*.
+Without any option, all metadata is removed.
+
+\par -n | \--name
+Metadata name (Default: "default").
+
+# EXAMPLES
+
+**pw-metadata**
+
+Show metadata in default name.
+
+**pw-metadata** -n settings 0
+
+Display settings.
+
+**pw-metadata** -n settings 0 clock.quantum 1024
+
+Change clock.quantum to 1024.
+
+# AUTHORS
+
+The PipeWire Developers <$(PACKAGE_BUGREPORT)>;
+PipeWire is available from <$(PACKAGE_URL)>
+
+# SEE ALSO
+
+\ref page_man_pipewire_1 "pipewire(1)",
+\ref page_man_pw-mon_1 "pw-mon(1)",
+\ref page_man_pw-cli_1 "pw-cli(1)",
diff --git a/doc/dox/programs/pw-mididump.1.md b/doc/dox/programs/pw-mididump.1.md
new file mode 100644
index 0000000000000000000000000000000000000000..83b229f23861e0b11dc8e36dff904f504f539063
--- /dev/null
+++ b/doc/dox/programs/pw-mididump.1.md
@@ -0,0 +1,38 @@
+\page page_man_pw-mididump_1 pw-mididump
+
+The PipeWire MIDI dump
+
+# SYNOPSIS
+
+**pw-mididump** \[*options*\] \[*FILE*\]
+
+# DESCRIPTION
+
+Dump MIDI messages to stdout.
+
+When a MIDI file is given, the events inside the file are printed.
+
+When no file is given, **pw-mididump** creates a PipeWire MIDI input
+stream and will print all MIDI events received on the port to stdout.
+
+# OPTIONS
+
+\par -r | \--remote=NAME
+The name the remote instance to monitor. If left unspecified, a
+connection is made to the default PipeWire instance.
+
+\par -h | \--help
+Show help.
+
+\par \--version
+Show version information.
+
+# AUTHORS
+
+The PipeWire Developers <$(PACKAGE_BUGREPORT)>;
+PipeWire is available from <$(PACKAGE_URL)>
+
+# SEE ALSO
+
+\ref page_man_pipewire_1 "pipewire(1)",
+\ref page_man_pw-cat_1 "pw-cat(1)"
diff --git a/doc/dox/programs/pw-mon.1.md b/doc/dox/programs/pw-mon.1.md
new file mode 100644
index 0000000000000000000000000000000000000000..6d9b63928fe07c63c94591d55b3ebde8ea25fd5a
--- /dev/null
+++ b/doc/dox/programs/pw-mon.1.md
@@ -0,0 +1,36 @@
+\page page_man_pw-mon_1 pw-mon
+
+The PipeWire monitor
+
+# SYNOPSIS
+
+**pw-mon** \[*options*\]
+
+# DESCRIPTION
+
+Monitor objects on the PipeWire instance.
+
+# OPTIONS
+
+\par -r | \--remote=NAME
+The name the *remote* instance to monitor. If left unspecified, a
+connection is made to the default PipeWire instance.
+
+\par -h | \--help
+Show help.
+
+\par \--version
+Show version information.
+
+\par -N | \--color=WHEN
+Whether to use color, one of 'never', 'always', or 'auto'. The default
+is 'auto'. **-N** is equivalent to **--color=never**.
+
+# AUTHORS
+
+The PipeWire Developers <$(PACKAGE_BUGREPORT)>;
+PipeWire is available from <$(PACKAGE_URL)>
+
+# SEE ALSO
+
+\ref page_man_pipewire_1 "pipewire(1)"
diff --git a/doc/dox/programs/pw-profiler.1.md b/doc/dox/programs/pw-profiler.1.md
new file mode 100644
index 0000000000000000000000000000000000000000..a02e5a219fcb75fae26c9270a835f202b567e119
--- /dev/null
+++ b/doc/dox/programs/pw-profiler.1.md
@@ -0,0 +1,46 @@
+\page page_man_pw-profiler_1 pw-profiler
+
+The PipeWire profiler
+
+# SYNOPSIS
+
+**pw-profiler** \[*options*\]
+
+# DESCRIPTION
+
+Start profiling a PipeWire instance.
+
+If the server has the profiler module loaded, this program will connect
+to it and log the profiler data. Profiler data contains times and
+durations when processing nodes and devices started and completed.
+
+When this program is stopped, a set of **gnuplot** files and a script to
+generate SVG files from the .plot files is generated, along with a .html
+file to visualize the profiling results in a browser.
+
+This function uses the same data used by *pw-top*.
+
+# OPTIONS
+
+\par -r | \--remote=NAME
+The name the remote instance to monitor. If left unspecified, a
+connection is made to the default PipeWire instance.
+
+\par -h | \--help
+Show help.
+
+\par \--version
+Show version information.
+
+\par -o | \--output=FILE
+Profiler output name (default "profiler.log").
+
+# AUTHORS
+
+The PipeWire Developers <$(PACKAGE_BUGREPORT)>;
+PipeWire is available from <$(PACKAGE_URL)>
+
+# SEE ALSO
+
+\ref page_man_pipewire_1 "pipewire(1)",
+\ref page_man_pw-top_1 "pw-top(1)"
diff --git a/doc/dox/programs/pw-top.1.md b/doc/dox/programs/pw-top.1.md
new file mode 100644
index 0000000000000000000000000000000000000000..5460581b7875d464900c21c0bd466b1c791ba594
--- /dev/null
+++ b/doc/dox/programs/pw-top.1.md
@@ -0,0 +1,207 @@
+\page page_man_pw-top_1 pw-top
+
+The PipeWire process viewer
+
+# SYNOPSIS
+
+**pw-top** \[*options*\]
+
+# DESCRIPTION
+
+The *pw-top* program provides a dynamic real-time view of the pipewire
+node and device statistics.
+
+A hierarchical view is shown of Driver nodes and follower nodes. The
+Driver nodes are actively using a timer to schedule dataflow in the
+followers. The followers of a driver node as shown below their driver
+with a + sign in a tree-like representation.
+
+The columns presented are as follows:
+
+\par S
+\parblock
+Node status.
+
+- E = ERROR
+- C = CREATING
+- S = SUSPENDED
+- I = IDLE
+- R = RUNNING
+\endparblock
+
+\par ID
+The ID of the pipewire node/device, as found in *pw-dump* and
+*pw-cli*
+
+\par QUANT
+\parblock
+The current quantum (for drivers) and the suggested quantum for
+follower nodes.
+
+The quantum by itself needs to be divided by the RATE column to
+calculate the duration of a scheduling period in fractions of a
+second.
+
+For a QUANT of 1024 and a RATE of 48000, the duration of one period
+in the graph is 1024/48000 or 21.3 milliseconds.
+
+Follower nodes can have a 0 QUANT field, which means that the node
+does not have a suggestion for the quantum and thus uses what the
+driver selected.
+
+The driver will use the lowest quantum of any of the followers. If
+none of the followers select a quantum, the default quantum in the
+pipewire configuration file will be used.
+
+The QUANT on the drivers usually translates directly into the number
+of audio samples processed per processing cycle of the graph.
+
+See also
+<https://gitlab.freedesktop.org/pipewire/pipewire/-/wikis/FAQ#pipewire-buffering-explained>
+\endparblock
+
+\par RATE
+\parblock
+The current rate (for drivers) and the suggested rate for follower
+nodes.
+
+This is the rate at which the *graph* processes data and needs to be
+combined with the QUANT value to derive the duration of a processing
+cycle in the graph.
+
+Some nodes can have a 0 RATE, which means that they don\'t have any
+rate suggestion for the graph. Nodes that suggest a rate can make
+the graph switch rates if the graph is otherwise idle and the new
+rate is allowed as a possible graph rate (see the pipewire
+configuration file).
+
+The RATE on (audio) driver nodes usually also translates directly to
+the samplerate used by the device. Although some devices might not
+be able to operate at the given samplerate, in which case resampling
+will need to be done. The negotiated samplerate with the device and
+stream can be found in the FORMAT column.
+
+\endparblock
+
+\par WAIT
+\parblock
+The waiting time of a node is the elapsed time between when the node
+is ready to start processing and when it actually started
+processing.
+
+For Driver nodes, this is the time between when the node wakes up to
+start processing the graph and when the driver (and thus also the
+graph) completes a cycle. The WAIT time for driver is thus the
+elapsed time processing the graph.
+
+For follower nodes, it is the time spent between being woken up
+(when all dependencies of the node are satisfied) and when
+processing starts. The WAIT time for follower nodes is thus mostly
+caused by context switching.
+
+A value of \-\-- means that the node was not signaled. A value of
++++ means that the node was signaled but not awake.
+\endparblock
+
+\par BUSY
+\parblock
+The processing time is started when the node starts processing until
+it completes and wakes up the next nodes in the graph.
+
+A value of \-\-- means that the node was not started. A value of +++
+means that the node was started but did not complete.
+\endparblock
+
+\par W/Q
+\parblock
+Ratio of WAIT / QUANT.
+
+The W/Q time of the driver node is a good measure of the graph load.
+The running averages of the driver W/Q ratios are used as the DSP
+load in other (JACK) tools.
+
+Values of \-\-- and +++ are copied from the WAIT column.
+
+\endparblock
+
+\par B/Q
+\parblock
+Ratio of BUSY / QUANT
+
+This is a good measure of the load of a particular driver or
+follower node.
+
+Values of \-\-- and +++ are copied from the BUSY column.
+\endparblock
+
+\par ERR
+\parblock
+Total of Xruns and Errors
+
+Xruns for drivers are when the graph did not complete a cycle. This
+can be because a node in the graph also has an Xrun. It can also be
+caused when scheduling delays cause a deadline to be missed, causing
+a hardware Xrun.
+
+Xruns for followers are incremented when the node started processing
+but did not complete before the end of the graph cycle deadline.
+\endparblock
+
+\par FORMAT
+\parblock
+The format used by the driver node or the stream. This is the
+hardware format negotiated with the device or stream.
+
+If the stream of driver has a different rate than the graph,
+resampling will be done.
+
+For raw audio formats, the layout is \<sampleformat\> \<channels\>
+\<samplerate\>.
+
+For IEC958 passthrough audio formats, the layout is IEC958 \<codec\>
+\<samplerate\>.
+
+For DSD formats, the layout is \<dsd-rate\> \<channels\>.
+
+For Video formats, the layout is \<pixelformat\>
+\<width\>x\<height\>.
+\endparblock
+
+\par NAME
+\parblock
+Name assigned to the device/node, as found in *pw-dump* node.name
+
+Names are prefixed by *+* when they are linked to a driver (entry
+above with no +)
+\endparblock
+
+
+# OPTIONS
+
+\par -h | \--help
+Show help.
+
+\par -b | \--batch-mode
+Run in non-interactive batch mode, similar to top\'s batch mode.
+
+\par -n | \--iterations=NUMBER
+Exit after NUMBER of batch iterations. Only used in batch mode.
+
+\par -r | \--remote=NAME
+The name the *remote* instance to monitor. If left unspecified, a
+connection is made to the default PipeWire instance.
+
+\par -V | \--version
+Show version information.
+
+# AUTHORS
+
+The PipeWire Developers <$(PACKAGE_BUGREPORT)>;
+PipeWire is available from <$(PACKAGE_URL)>
+
+# SEE ALSO
+
+\ref page_man_pipewire_1 "pipewire(1)",
+\ref page_man_pw-dump_1 "pw-dump(1)",
+\ref page_man_pw-cli_1 "pw-cli(1)",
+\ref page_man_pw-profiler_1 "pw-profiler(1)"
diff --git a/doc/dox/pulse-modules.dox b/doc/dox/pulse-modules.dox
new file mode 100644
index 0000000000000000000000000000000000000000..6ed4d343939a05493dfd30de7859a43e16e01540
--- /dev/null
+++ b/doc/dox/pulse-modules.dox
@@ -0,0 +1,149 @@
+/** \page page_pulse_modules Pulseaudio Modules
+
+PipeWire's Pulseaudio emulation implements several Pulseaudio modules.
+It only supports its own built-in modules, and cannot load external
+modules written for Pulseaudio.
+
+# Loading modules
+
+The built-in modules can be loaded using Pulseaudio client programs,
+for example `pactl load-module <module-name> <module-options>`. They
+can also added to `pipewire-pulse.conf`, typically by a drop-in file
+in `~/.config/pipewire/pipewire-pulse.conf.d/` containing the module
+name and its arguments
+```
+pulse.cmd = [
+    { cmd = "load-module" args = "module-null-sink sink_name=foo" flags = [ ] }
+]
+```
+
+To list all modules currently loaded, with their arguments:
+```
+pactl list modules
+```
+
+For a short list of loaded modules:
+```
+pactl list modules short
+```
+
+Modules may be unloaded using either the module-name or index number:
+
+```
+pactl load-module <module-name> <parameters>
+pactl unload-module <module-name|index#>
+```
+
+# Common module options
+
+Most modules that create streams/devices support the following properties:
+
+## sink_name, source_name
+
+Name for the sink (resp. source). Allowed characters in the name are a-z, A-Z, numbers, period (.) and underscore (_). The length must be 1-128 characters.
+
+## format
+
+The sample format. The supported audio formats are:
+
+### PCM
+ - u8: unsigned 8-bit integer
+ - aLaw: A-law encoded 8-bit integer
+ - uLaw: μ-law encoded 8-bit integer
+ - s16le: signed 16-bit little-endian integer
+ - s16be: signed 16-bit big-endian integer
+ - s16, s16ne: native-endian aliases for s16le or s16be
+ - s16re: reverse-endian alias for s16le or s16be
+ - float32le: 32-bit little-endian float
+ - float32be: 32-bit big-endian float
+ - float32, float32ne: native-endian aliases for float32le or float32be
+ - float32re: reverse-endian alias for float32le or float32be
+ - s32le: signed 32-bit little-endian integer
+ - s32be: signed 32-bit big-endian integer
+ - s32, s32ne: native-endian aliases for s32le or s32be
+ - s32re: reverse-endian alias for s32le or s32be
+ - s24le: signed 24-bit little-endian integer (note: ALSA calls this "S24_3LE")
+ - s24be: signed 24-bit big-endian integer (note: ALSA calls this "S24_3BE")
+ - s24, s24ne: native-endian aliases for s24le or s24be
+ - s24re: reverse-endian alias for s24le or s24be
+ - s24-32le: signed 24-bit little-endian integer, packed into a 32-bit integer so that the 8 most significant bits are ignored (note: ALSA calls this "S24_LE")
+ - s24-32be: signed 24-bit big-endian integer, packed into a 32-bit integer so that the 8 most significant bits are ignored (note: ALSA calls this "S24_BE")
+ - s24-32, s24-32ne: native-endian aliases for s24-32le or s24-32be
+ - s24-32re: reverse-endian alias for s24-32le or s24-32be
+
+### Compressed audio formats
+
+Below is a list of all supported compressed formats. The code at the beginning of each line is used whenever a textual identifier for a format is needed (for example in configuration files or on the command line). The formats whose identifier ends with -iec61937 have to be wrapped in IEC 61937 frames, which makes the compressed audio behave more like normal PCM audio.
+
+ - ac3-iec61937: Dolby Digital (DD / AC-3 / A/52)
+ - eac3-iec61937: Dolby Digital Plus (DD+ / E-AC-3)
+ - mpeg-iec61937: MPEG-1 or MPEG-2 Part 3 (not MPEG-2 AAC)
+ - dts-iec61937: DTS
+ - mpeg2-aac-iec61937: MPEG-2 AAC (supported since PulseAudio 4.0)
+ - truehd-iec61937: Dolby TrueHD (added in PulseAudio 13.0, but doesn't work yet in practice)
+ - dtshd-iec61937: DTS-HD Master Audio (added in PulseAudio 13.0, but doesn't work yet in practice)
+ - pcm: PCM (not a compressed format, but listed here, because pcm is one of the recognized encoding identifiers)
+ - any: (special identifier for indicating that any encoding can be used)
+
+
+## rate
+
+The sample rate.
+
+##channels
+
+Number of audio channels.
+
+## channel_map
+
+A channel map. A list of comma-separated channel names. The currently defined channel names are:
+`left`, `right`, `mono`, `center`, `front-left`, `front-right`, `front-center`,
+`rear-center`, `rear-left`, `rear-right`, `lfe`, `subwoofer`, `front-left-of-center`,
+`front-right-of-center`, `side-left`, `side-right`, `aux0`, `aux1` to `aux15`, `top-center`,
+`top-front-left`, `top-front-right`, `top-front-center`, `top-rear-left`, `top-rear-right`,
+`top-rear-center`
+
+## sink_properties, source_properties
+
+Set additional properties of the sink/source. For example, you can set the description directly
+when the module is loaded by setting this parameter.
+
+```
+load-module module-alsa-sink sink_name=headphones sink_properties=device.description=Headphones
+```
+
+# List of known built-in modules:
+
+- \subpage page_pulse_module_alsa_sink
+- \subpage page_pulse_module_alsa_source
+- \subpage page_pulse_module_always_sink
+- \subpage page_pulse_module_combine_sink
+- \subpage page_pulse_module_echo_cancel
+- \subpage page_pulse_module_gsettings
+- \subpage page_pulse_module_jackdbus_detect
+- \subpage page_pulse_module_ladspa_sink
+- \subpage page_pulse_module_ladspa_source
+- \subpage page_pulse_module_loopback
+- \subpage page_pulse_module_native_protocol_tcp
+- \subpage page_pulse_module_null_sink
+- \subpage page_pulse_module_pipe_sink
+- \subpage page_pulse_module_pipe_source
+- \subpage page_pulse_module_raop_discover
+- \subpage page_pulse_module_remap_sink
+- \subpage page_pulse_module_remap_source
+- \subpage page_pulse_module_roc_sink
+- \subpage page_pulse_module_roc_sink_input
+- \subpage page_pulse_module_roc_source
+- \subpage page_pulse_module_rtp_recv
+- \subpage page_pulse_module_rtp_send
+- \subpage page_pulse_module_simple_protocol_tcp
+- \subpage page_pulse_module_switch_on_connect
+- \subpage page_pulse_module_tunnel_sink
+- \subpage page_pulse_module_tunnel_source
+- \subpage page_pulse_module_virtual_sink
+- \subpage page_pulse_module_virtual_source
+- \subpage page_pulse_module_x11_bell
+- \subpage page_pulse_module_zeroconf_discover
+- \subpage page_pulse_module_zeroconf_publish
+
+*/
diff --git a/doc/tutorial.dox b/doc/dox/tutorial/index.dox
similarity index 89%
rename from doc/tutorial.dox
rename to doc/dox/tutorial/index.dox
index bc5372565313dc0357255305a6d67dddb4ed3485..a4fc4d572d50294c475b2a699076f19dcb471f2e 100644
--- a/doc/tutorial.dox
+++ b/doc/dox/tutorial/index.dox
@@ -1,6 +1,6 @@
 /** \page page_tutorial Tutorial
 
-Welcome to the PipeWire tutorial. The goal is to learn
+Welcome to the PipeWire API tutorial. The goal is to learn
 PipeWire API step-by-step with simple short examples.
 
 - \subpage page_tutorial1
diff --git a/doc/tutorial1.dox b/doc/dox/tutorial/tutorial1.dox
similarity index 100%
rename from doc/tutorial1.dox
rename to doc/dox/tutorial/tutorial1.dox
diff --git a/doc/tutorial2.dox b/doc/dox/tutorial/tutorial2.dox
similarity index 100%
rename from doc/tutorial2.dox
rename to doc/dox/tutorial/tutorial2.dox
diff --git a/doc/tutorial3.dox b/doc/dox/tutorial/tutorial3.dox
similarity index 100%
rename from doc/tutorial3.dox
rename to doc/dox/tutorial/tutorial3.dox
diff --git a/doc/tutorial4.dox b/doc/dox/tutorial/tutorial4.dox
similarity index 100%
rename from doc/tutorial4.dox
rename to doc/dox/tutorial/tutorial4.dox
diff --git a/doc/tutorial5.dox b/doc/dox/tutorial/tutorial5.dox
similarity index 100%
rename from doc/tutorial5.dox
rename to doc/dox/tutorial/tutorial5.dox
diff --git a/doc/tutorial6.dox b/doc/dox/tutorial/tutorial6.dox
similarity index 100%
rename from doc/tutorial6.dox
rename to doc/dox/tutorial/tutorial6.dox
diff --git a/doc/tutorial1.c b/doc/examples/tutorial1.c
similarity index 100%
rename from doc/tutorial1.c
rename to doc/examples/tutorial1.c
diff --git a/doc/tutorial2.c b/doc/examples/tutorial2.c
similarity index 100%
rename from doc/tutorial2.c
rename to doc/examples/tutorial2.c
diff --git a/doc/tutorial3.c b/doc/examples/tutorial3.c
similarity index 100%
rename from doc/tutorial3.c
rename to doc/examples/tutorial3.c
diff --git a/doc/tutorial4.c b/doc/examples/tutorial4.c
similarity index 97%
rename from doc/tutorial4.c
rename to doc/examples/tutorial4.c
index 67cb0c8dc7f39eb3693e3ba29e1741c9319560a9..bc445868b237911183cb50d1cd3dab5ff4ecd9b5 100644
--- a/doc/tutorial4.c
+++ b/doc/examples/tutorial4.c
@@ -42,6 +42,8 @@ static void on_process(void *userdata)
 
 	stride = sizeof(int16_t) * DEFAULT_CHANNELS;
 	n_frames = buf->datas[0].maxsize / stride;
+	if (b->requested)
+		n_frames = SPA_MIN(b->requested, n_frames);
 
 	for (i = 0; i < n_frames; i++) {
 		data->accumulator += M_PI_M2 * 440 / DEFAULT_RATE;
diff --git a/doc/tutorial5.c b/doc/examples/tutorial5.c
similarity index 100%
rename from doc/tutorial5.c
rename to doc/examples/tutorial5.c
diff --git a/doc/tutorial6.c b/doc/examples/tutorial6.c
similarity index 100%
rename from doc/tutorial6.c
rename to doc/examples/tutorial6.c
diff --git a/doc/input-filter.py b/doc/input-filter.py
new file mode 100755
index 0000000000000000000000000000000000000000..2b64a2d6518559d59449e0df57817784b233e731
--- /dev/null
+++ b/doc/input-filter.py
@@ -0,0 +1,53 @@
+#!/usr/bin/env python3
+# -*- mode: python; coding: utf-8; eval: (blacken-mode); -*-
+r"""
+Doxygen input filter that:
+
+- adds \privatesection to all files
+- removes macros
+- parses pulse_module_options and substitutes it into @pulse_module_options@
+
+This is used for .c files, and causes Doxygen to not include
+any symbols from them, unless they also appeared in a header file.
+
+The Pulse module option parsing is used in documentation of Pulseaudio modules.
+"""
+import sys
+import re
+import os
+
+
+def main():
+    fn = sys.argv[1]
+    with open(fn, "r") as f:
+        text = f.read()
+
+    text = re.sub("#define.*", "", text)
+
+    if "@pulse_module_options@" in text:
+        m = re.search(
+            r"static const char[* ]*const pulse_module_options\s+=\s+(.*?\")\s*;\s*$",
+            text,
+            re.M | re.S,
+        )
+        if m:
+            res = []
+            for line in m.group(1).splitlines():
+                m = re.match(r"\s*\"\s*([a-z0-9_]+)\s*=\s*(.*)\"\s*$", line)
+                if m:
+                    name = m.group(1)
+                    value = m.group(2).strip().strip("<>")
+                    res.append(f"- `{name}`: {value}")
+
+            res = "\n * ".join(res)
+            text = text.replace("@pulse_module_options@", res)
+
+    if os.path.basename(fn).startswith("module-") and fn.endswith(".c"):
+        text = re.sub(r"^ \* ##", r" * #", text, flags=re.M)
+
+    print("/** \\privatesection */")
+    print(text)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/doc/input-filter.sh b/doc/input-filter.sh
deleted file mode 100755
index 8c71befa11ce3b5b2e23d0af29f6db78f6477672..0000000000000000000000000000000000000000
--- a/doc/input-filter.sh
+++ /dev/null
@@ -1,10 +0,0 @@
-#!/bin/bash
-#
-# Doxygen input filter that adds \privatesection to all files,
-# and removes macros.
-#
-# This is used for .c files, and causes Doxygen to not include
-# any symbols from them, unless they also appeared in a header file.
-#
-echo -n "/** \privatesection */ "
-sed -e 's/#define.*//' < "$1"
diff --git a/doc/man-fixup.py b/doc/man-fixup.py
new file mode 100755
index 0000000000000000000000000000000000000000..758aa855ecedac478fb0a07ff3178141732b948b
--- /dev/null
+++ b/doc/man-fixup.py
@@ -0,0 +1,97 @@
+#!/usr/bin/python3
+# -*- mode: python; coding: utf-8; eval: (blacken-mode); -*-
+r"""
+Fetch right Doxygen man file, replace dummy parts, and fixup nroff
+"""
+import argparse
+import re
+import sys
+from subprocess import call
+from pathlib import Path
+
+
+def main():
+    p = argparse.ArgumentParser(description=__doc__.strip())
+    p.add_argument("htmldir", type=Path)
+    p.add_argument("page")
+    p.add_argument("name")
+    p.add_argument("section")
+    p.add_argument("version")
+    args = p.parse_args()
+
+    page, name, section, version = args.page, args.name, args.section, args.version
+
+    mandir = args.htmldir / ".." / "man" / "man3"
+    fn = mandir / f"{page}.3"
+
+    # Doxygen < 1.9.7 names .md file output differently...
+    if not fn.exists():
+        page2 = page.replace("page_man_", "md_doc_dox_programs_").replace("-", "_")
+        fn = mandir / f"{page2}.3"
+    else:
+        page2 = None
+
+    try:
+        with open(fn, "r") as f:
+            text = f.read()
+    except:
+        print(f"ERROR: man file {fn} missing!", file=sys.stderr)
+        call(["ls", "-R", str(args.htmldir / ".." / "man")], stdout=sys.stderr)
+        raise
+
+    text = text.replace(page, name)
+    if page2 is not None:
+        text = text.replace(page2, name)
+
+    # Replace bad nroff header
+    text = re.sub(
+        r"^(\.TH[^\n]*)\n",
+        rf'.TH "{name}" {section} "{version}" "PipeWire" \\" -*- nroff -*-\n',
+        text,
+    )
+
+    # Fixup name field (can't be done in Doxygen, otherwise HTML looks bac)
+    text = re.sub(
+        rf"^\.SH NAME\s*\n{name} \\- {name}\s*\n\.PP\n *",
+        rf".SH NAME\n{name} \\- ",
+        text,
+        count=1,
+        flags=re.M,
+    )
+
+    # Add DESCRIPTION section if missing and NAME field has extra stuff
+    if not re.search(r"^\.SH DESCRIPTION\s*\n", text):
+        text = re.sub(
+            r"^(.SH NAME\s*\n[^\.].*\n)\.PP\s*\n([^\.\n ]+)",
+            r"\1.SH DESCRIPTION\n.PP\n\2",
+            text,
+            count=1,
+            flags=re.M,
+        )
+
+    # Upcase titles
+    def upcase(m):
+        return m.group(0).upper()
+
+    text = re.sub(r"^\.SH .*?$", upcase, text, flags=re.M)
+
+    # Replace PW_KEY_*, SPA_KEY_* by their values
+    def pw_key(m):
+        key = m.group(0)
+        key = key.replace("PW_KEY_", "").lower().replace("_", ".")
+        if key in ("protocol", "access", "client.access") or key.startswith("sec."):
+            return f"pipewire.{key}"
+        return key
+
+    def spa_key(m):
+        key = m.group(0)
+        return key.replace("SPA_KEY_", "").lower().replace("_", ".")
+
+    text = re.sub(r"PW_KEY_[A-Z_]+", pw_key, text, flags=re.S)
+    text = re.sub(r"SPA_KEY_[A-Z_]+", spa_key, text, flags=re.S)
+
+    print(text)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/doc/manpage.dox.in b/doc/manpage.dox.in
deleted file mode 100644
index 9e6df78933f5553e5d3fd288dee1be1aa3828788..0000000000000000000000000000000000000000
--- a/doc/manpage.dox.in
+++ /dev/null
@@ -1,5 +0,0 @@
-/** \page @pagename@ @title@
-
-\verbinclude @filename@
-
-*/
diff --git a/doc/meson.build b/doc/meson.build
index f23248fe013f80c7ff883faeefca759d6cbfff1e..3edaeb9f1f049ee2d48509ccbbfbb930fafff43b 100644
--- a/doc/meson.build
+++ b/doc/meson.build
@@ -1,3 +1,5 @@
+fs = import('fs')
+
 doxyfile_conf = configuration_data()
 doxyfile_conf.set('PACKAGE_NAME', meson.project_name())
 doxyfile_conf.set('PACKAGE_VERSION', meson.project_version())
@@ -5,6 +7,14 @@ doxyfile_conf.set('top_srcdir', meson.project_source_root())
 doxyfile_conf.set('top_builddir', meson.project_build_root())
 doxyfile_conf.set('output_directory', meson.current_build_dir())
 
+doxygen_env = environment()
+doxygen_env.set('PACKAGE_NAME', meson.project_name())
+doxygen_env.set('PACKAGE_VERSION', meson.project_version())
+doxygen_env.set('PACKAGE_URL', 'https://pipewire.org')
+doxygen_env.set('PACKAGE_BUGREPORT', 'https://gitlab.freedesktop.org/pipewire/pipewire/issues')
+doxygen_env.set('PIPEWIRE_CONFIG_DIR', pipewire_configdir)
+doxygen_env.set('PIPEWIRE_CONFDATADIR', pipewire_confdatadir)
+
 dot_found = find_program('dot', required: false).found()
 summary({'dot (used with doxygen)': dot_found}, bool_yn: true, section: 'Optional programs')
 if dot_found
@@ -14,41 +24,74 @@ else
 endif
 
 # Note: order here is how doxygen will expose the pages in the sidebar
-# api-tree.dox should be first to determine ordering of Modules.
+# tree.dox should be first to determine the ordering.
 extra_docs = [
-  'api-tree.dox',
-  'index.dox',
-  'overview.dox',
-  'pipewire.dox',
-  'pipewire-design.dox',
-  'pipewire-access.dox',
-  'pipewire-midi.dox',
-  'pipewire-portal.dox',
-  'pipewire-daemon.dox',
-  'pipewire-library.dox',
-  'pipewire-modules.dox',
-  'pipewire-session-manager.dox',
-  'pipewire-objects-design.dox',
-  'pipewire-audio.dox',
-  'pipewire-scheduling.dox',
-  'pipewire-protocol.dox',
-  'tutorial.dox',
-  'tutorial1.dox',
-  'tutorial2.dox',
-  'tutorial3.dox',
-  'tutorial4.dox',
-  'tutorial5.dox',
-  'tutorial6.dox',
-  'api.dox',
-  'spa-index.dox',
-  'spa-plugins.dox',
-  'spa-design.dox',
-  'spa-pod.dox',
-  'spa-buffer.dox',
-  'pulseaudio.dox',
-  'dma-buf.dox',
+  'tree.dox',
+  'dox/index.dox',
+  'dox/overview.dox',
+  'dox/modules.dox',
+  'dox/pulse-modules.dox',
+  'dox/programs/index.md',
+  'dox/internals/index.dox',
+  'dox/internals/design.dox',
+  'dox/internals/access.dox',
+  'dox/internals/midi.dox',
+  'dox/internals/portal.dox',
+  'dox/internals/daemon.dox',
+  'dox/internals/library.dox',
+  'dox/internals/session-manager.dox',
+  'dox/internals/objects.dox',
+  'dox/internals/audio.dox',
+  'dox/internals/scheduling.dox',
+  'dox/internals/protocol.dox',
+  'dox/internals/pulseaudio.dox',
+  'dox/internals/dma-buf.dox',
+  'dox/tutorial/index.dox',
+  'dox/tutorial/tutorial1.dox',
+  'dox/tutorial/tutorial2.dox',
+  'dox/tutorial/tutorial3.dox',
+  'dox/tutorial/tutorial4.dox',
+  'dox/tutorial/tutorial5.dox',
+  'dox/tutorial/tutorial6.dox',
+  'dox/api/index.dox',
+  'dox/api/spa-index.dox',
+  'dox/api/spa-plugins.dox',
+  'dox/api/spa-design.dox',
+  'dox/api/spa-pod.dox',
+  'dox/api/spa-buffer.dox',
+]
+
+manpage_docs = [
+  'dox/programs/libpipewire-modules.7.md',
+  'dox/programs/pipewire-pulse-modules.7.md',
+  'dox/programs/pipewire-pulse.1.md',
+  'dox/programs/pipewire-pulse.conf.5.md',
+  'dox/programs/pipewire.1.md',
+  'dox/programs/pipewire.conf.5.md',
+  'dox/programs/pw-cat.1.md',
+  'dox/programs/pw-cli.1.md',
+  'dox/programs/pw-config.1.md',
+  'dox/programs/pw-dot.1.md',
+  'dox/programs/pw-dump.1.md',
+  'dox/programs/pw-jack.1.md',
+  'dox/programs/pw-link.1.md',
+  'dox/programs/pw-loopback.1.md',
+  'dox/programs/pw-metadata.1.md',
+  'dox/programs/pw-mididump.1.md',
+  'dox/programs/pw-mon.1.md',
+  'dox/programs/pw-profiler.1.md',
+  'dox/programs/pw-top.1.md',
 ]
 
+manpages = []
+
+foreach m : manpage_docs
+  name = fs.stem(fs.name(m))
+  pagepart = name.replace('.', '_')
+  manpages += [[name, f'page_man_@pagepart@']]
+  extra_docs += m
+endforeach
+
 inputs = []
 foreach extra : extra_docs
   inputs += meson.project_source_root() / 'doc' / extra
@@ -68,7 +111,9 @@ endforeach
 foreach h : module_sources
   inputs += meson.project_source_root() / 'src' / 'modules' / h
 endforeach
-inputs += meson.project_source_root() / 'test' / 'pwtest.h'
+foreach h : pipewire_module_protocol_pulse_sources
+  inputs += meson.project_source_root() / 'src' / 'modules' / h
+endforeach
 input_dirs = [ meson.project_source_root() / 'spa' / 'include' / 'spa' ]
 
 path_prefixes = [
@@ -117,35 +162,38 @@ examples_dox = configure_file(input: 'examples.dox.in',
 
 input_dirs += [ 'doc/examples.dox' ]
 
-man_doxygen = []
-man_subpages = []
-foreach m : manpages
-  manconf = configuration_data()
-  pagename = 'page_man_' + m.split('.rst.in').get(0).replace('.', '_').replace('-', '_')
-  filename = m.split('.rst.in').get(0) + '.dox'
-  manconf.set('pagename', pagename)
-  manconf.set('title', m.split('.rst.in').get(0).replace('.1','').replace('.5',''))
-  manconf.set('filename', meson.project_source_root() / 'man' / m)
-  manfile = configure_file(input: 'manpage.dox.in',
-                           output: filename,
-                           configuration: manconf)
-  man_doxygen += [manfile]
-  man_subpages += ['- \subpage ' + pagename]
-  input_dirs += [ 'doc/' + filename ]
+module_manpage_list = []
+foreach m : module_sources
+   name = fs.stem(m)
+   pagepart = name.replace('-', '_')
+   module_manpage_list += f'\\ref page_@pagepart@ "libpipewire-@name@(7)"'
+   manpages += [[f'libpipewire-@name@.7', f'page_@pagepart@']]
 endforeach
 
-pw_tools_dox_conf = configuration_data()
-pw_tools_dox_conf.set('man_subpages', '\n'.join(man_subpages))
-pw_tools_dox = configure_file(input: 'pipewire-tools.dox.in',
-                          output: 'pipewire-tools.dox',
-                          configuration: pw_tools_dox_conf)
-input_dirs += [ 'doc/pipewire-tools.dox' ]
+doxygen_env.set('LIBPIPEWIRE_MODULES', '<ul><li>' + '</li><li>'.join(module_manpage_list) + '</li></ul>')
+
+pulse_module_manpage_list = []
+foreach m : pipewire_module_protocol_pulse_sources
+  name = fs.stem(fs.name(m))
+  if m.contains('/modules/') and name.startswith('module-')
+    pagepart = name.replace('-', '_')
+    pulse_module_manpage_list += f'\\ref page_pulse_@pagepart@ "pipewire-pulse-@name@(7)"'
+    manpages += [[f'pipewire-pulse-@name@.7', f'page_pulse_@pagepart@']]
+  endif
+endforeach
+
+doxygen_env.set('PIPEWIRE_PULSE_MODULES', '<ul><li>' + '</li><li>'.join(pulse_module_manpage_list) + '</li></ul>')
+
+doxygen_layout = meson.project_source_root() / 'doc' / 'DoxygenLayout.xml'
+doxygen_filter_c = meson.project_source_root() / 'doc' / 'input-filter.py'
+doxygen_filter_h = meson.project_source_root() / 'doc' / 'input-filter-h.sh'
 
 doxyfile_conf.set('inputs', ' '.join(inputs + input_dirs))
 doxyfile_conf.set('cssfiles', ' '.join(cssfiles))
+doxyfile_conf.set('layout', doxygen_layout)
 doxyfile_conf.set('path_prefixes', ' '.join(path_prefixes))
-doxyfile_conf.set('c_input_filter', meson.project_source_root() / 'doc' / 'input-filter.sh')
-doxyfile_conf.set('h_input_filter', meson.project_source_root() / 'doc' / 'input-filter-h.sh')
+doxyfile_conf.set('c_input_filter', doxygen_filter_c)
+doxyfile_conf.set('h_input_filter', doxygen_filter_h)
 
 doxyfile = configure_file(input: 'Doxyfile.in',
                           output: 'Doxyfile',
@@ -157,8 +205,33 @@ if docdir == ''
 endif
 
 html_target = custom_target('pipewire-docs',
-                            input: [ doxyfile, examples_dox, pw_tools_dox ] + inputs + cssfiles + man_doxygen,
+                            input: [ doxyfile, doxygen_layout, examples_dox, doxygen_filter_c, doxygen_filter_h ] + inputs + cssfiles,
                             output: [ 'html' ],
                             command: [ doxygen, doxyfile ],
-                            install: true,
+                            env: doxygen_env,
+                            install: install_docs,
+                            install_tag: 'doc',
                             install_dir: docdir)
+
+
+man_fixup = files('man-fixup.py')
+
+manfiles = []
+
+foreach m : manpages
+  file = m.get(0)
+  page = m.get(1)
+  name = fs.stem(file)
+  section = file.split('.').get(-1)
+
+  manfiles += custom_target(file,
+     command : [ python, man_fixup, '@INPUT@', page, name, section, meson.project_version() ],
+     output : file,
+     input : html_target,
+     depend_files : [ man_fixup ],
+     capture : true,
+     install : install_man,
+     install_tag: 'man',
+     install_dir : get_option('mandir') / 'man' + section
+  )
+endforeach
diff --git a/doc/pipewire-architecture.dox b/doc/pipewire-architecture.dox
deleted file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000
diff --git a/doc/pipewire-tools.dox.in b/doc/pipewire-tools.dox.in
deleted file mode 100644
index e0bf1168051e3f3517272f312c4af7631ec68591..0000000000000000000000000000000000000000
--- a/doc/pipewire-tools.dox.in
+++ /dev/null
@@ -1,7 +0,0 @@
-/** \page page_tools PipeWire Tools
-
-Manual pages:
-
-@man_subpages@
-
-*/
diff --git a/doc/api-tree.dox b/doc/tree.dox
similarity index 91%
rename from doc/api-tree.dox
rename to doc/tree.dox
index 0c5c8fe4b95c4a1e762e1ac361c44d20074972ef..3e3588259ac54868b9a65c4d9d7b85a2699e260b 100644
--- a/doc/api-tree.dox
+++ b/doc/tree.dox
@@ -1,5 +1,7 @@
 /**
 
+This determines the ordering of items in Doxygen sidebar.
+
 \defgroup api_pw_core Core API
 \brief PipeWire Core API
 \{
@@ -89,6 +91,7 @@ Utility data structures, macros, etc.
 \addtogroup spa_hooks
 \addtogroup spa_interfaces
 \addtogroup spa_json
+\addtogroup spa_json_pod
 \addtogroup spa_keys
 \addtogroup spa_names
 \addtogroup spa_result
@@ -119,8 +122,12 @@ Support interfaces provided by host
 \{
 \}
 
-\defgroup pwtest Test Suite
-\{
-\}
+\page page_overview
+\page page_programs
+\page page_modules
+\page page_pulse_modules
+\page page_internals
+\page page_api
+\page page_tutorial
 
 */
diff --git a/man/meson.build b/man/meson.build
deleted file mode 100644
index e64b976b0e440e50c4ddf593845c4418d8b00a9c..0000000000000000000000000000000000000000
--- a/man/meson.build
+++ /dev/null
@@ -1,45 +0,0 @@
-manpage_conf = configuration_data()
-manpage_conf.set('PACKAGE_NAME', meson.project_name())
-manpage_conf.set('PACKAGE_VERSION', meson.project_version())
-manpage_conf.set('PACKAGE_URL', 'https://pipewire.org')
-manpage_conf.set('PACKAGE_BUGREPORT', 'https://gitlab.freedesktop.org/pipewire/pipewire/issues')
-manpage_conf.set('PIPEWIRE_CONFIG_DIR', pipewire_configdir)
-manpage_conf.set('PIPEWIRE_CONFDATADIR', pipewire_confdatadir)
-
-manpages = [
-  'pipewire.1.rst.in',
-  'pipewire-pulse.1.rst.in',
-  'pipewire.conf.5.rst.in',
-  'pw-cat.1.rst.in',
-  'pw-cli.1.rst.in',
-  'pw-config.1.rst.in',
-  'pw-dot.1.rst.in',
-  'pw-link.1.rst.in',
-  'pw-metadata.1.rst.in',
-  'pw-mididump.1.rst.in',
-  'pw-mon.1.rst.in',
-  'pw-profiler.1.rst.in',
-  'pw-top.1.rst.in',
-]
-
-if get_option('pipewire-jack').allowed()
-  manpages += 'pw-jack.1.rst.in'
-endif
-
-if not generate_manpages
-  subdir_done()
-endif
-
-foreach m : manpages
-  file = m.split('.rst.in').get(0)
-  rst = configure_file(input : m,
-                       output : file + '.rst',
-                       configuration : manpage_conf)
-  section = file.split('.').get(-1)
-  custom_target(file + '.target',
-                output : file,
-                input : rst,
-                command : [rst2man, '@INPUT@', '@OUTPUT@'],
-                install : true,
-                install_dir : get_option('mandir') / 'man' + section)
-endforeach
diff --git a/man/pipewire-pulse.1.rst.in b/man/pipewire-pulse.1.rst.in
deleted file mode 100644
index 4695ec0b940608da33dc33c2bf0b856ffaf20c89..0000000000000000000000000000000000000000
--- a/man/pipewire-pulse.1.rst.in
+++ /dev/null
@@ -1,48 +0,0 @@
-pipewire-pulse
-##############
-
------------------------------------
-The PipeWire PulseAudio replacement
------------------------------------
-
-:Manual section: 1
-:Manual group: General Commands Manual
-
-SYNOPSIS
-========
-
-| **pipewire-pulse** [*options*]
-
-DESCRIPTION
-===========
-
-**pipewire-pulse** starts a PulseAudio-compatible daemon that integrates with
-the PipeWire media server, by running a pipewire process through a systemd
-service. This daemon is a drop-in replacement for the PulseAudio daemon.
-
-OPTIONS
-=======
-
--h | --help
-  Show help.
-
--v | --verbose
-  Increase the verbosity by one level. This option may be specified multiple
-  times.
-
---version
-  Show version information.
-
--c | --config=FILE
-  Load the given config file (Default: pipewire-pulse.conf).
-
-AUTHORS
-=======
-
-The PipeWire Developers <@PACKAGE_BUGREPORT@>;
-PipeWire is available from @PACKAGE_URL@
-
-SEE ALSO
-========
-
-``pipewire(1)``
diff --git a/man/pipewire.1.rst.in b/man/pipewire.1.rst.in
deleted file mode 100644
index 8b63839696510960f4128cae1d980027d33bfb95..0000000000000000000000000000000000000000
--- a/man/pipewire.1.rst.in
+++ /dev/null
@@ -1,54 +0,0 @@
-pipewire
-########
-
--------------------------
-The PipeWire media server
--------------------------
-
-:Manual section: 1
-:Manual group: General Commands Manual
-
-SYNOPSIS
-========
-
-| **pipewire** [*options*]
-
-DESCRIPTION
-===========
-
-PipeWire is a service that facilitates sharing of multimedia content
-between devices and applications.
-
-The **pipewire** daemon reads a config file that is further documented in
-``pipewire.conf(5)`` manual page.
-
-OPTIONS
-=======
-
--h | --help
-  Show help.
-
--v | --verbose
-  Increase the verbosity by one level. This option may be specified multiple
-  times.
-
---version
-  Show version information.
-
--c | --config=FILE
-  Load the given config file (Default: pipewire.conf).
-
-AUTHORS
-=======
-
-The PipeWire Developers <@PACKAGE_BUGREPORT@>;
-PipeWire is available from @PACKAGE_URL@
-
-SEE ALSO
-========
-
-``pw-top(1)``,
-``pw-dump(1)``,
-``pw-mon(1)``,
-``pw-cat(1)``,
-``pw-cli(1)``,
diff --git a/man/pipewire.conf.5.rst.in b/man/pipewire.conf.5.rst.in
deleted file mode 100644
index fb8144bd4f70d3328ea8368cc6cfde4cb76cae8d..0000000000000000000000000000000000000000
--- a/man/pipewire.conf.5.rst.in
+++ /dev/null
@@ -1,112 +0,0 @@
-pipewire.conf
-#############
-
---------------------------------------
-The PipeWire server configuration file
---------------------------------------
-
-:Manual section: 5
-:Manual group: File Formats Manual
-
-.. _synopsis:
-
-SYNOPSIS
-========
-
-*$XDG_CONFIG_HOME/pipewire/pipewire.conf*
-
-*@PIPEWIRE_CONFIG_DIR@/pipewire.conf*
-
-*@PIPEWIRE_CONFDATADIR@/pipewire.conf*
-
-*@PIPEWIRE_CONFDATADIR@/pipewire.conf.d/*
-
-*@PIPEWIRE_CONFIG_DIR@/pipewire.conf.d/*
-
-*$XDG_CONFIG_HOME/pipewire/pipewire.conf.d/*
-
-DESCRIPTION
-===========
-
-PipeWire is a service that facilitates sharing of multimedia content
-between devices and applications.
-
-On startup, the daemon reads a main configuration file to configure
-itself. It executes a series of commands listed in the config
-file.
-
-The config files are loaded in the order listed in the SYNOPSIS_.
-The environment variables ``PIPEWIRE_CONFIG_DIR``, ``PIPEWIRE_CONFIG_PREFIX``
-and ``PIPEWIRE_CONFIG_NAME`` can be used to specify an alternative config
-directory, subdirectory and file respectively.
-
-Next to the configuration file can be a directory with the same name as
-the file with a ``.d/`` suffix. All directories in the SYNOPSIS_ directory
-search paths are traversed in the listed order and the contents of the
-``*.conf`` files inside them are appended to the main configuration file
-as overrides. Object sections are merged and array sections are appended.
-
-
-CONFIGURATION FILE FORMAT
-=========================
-
-The configuration file format is grouped into sections. A section
-is either a dictionary, {}, or an array, []. Dictionary and array
-entries are separated by whitespace and may be simple value
-assignment, an array or a dictionary. For example:
-
-name = value # simple assignment
-
-name = { key1 = value1 key2 = value2 } # a dictionary with two
-entries
-
-name = [ value1 value2 ] # an array with two entries
-
-name = [ { k = v1 } { k = v2 } ] # an array of dictionaries
-
-
-The configuration files can be expressed in full JSON syntax but for ease
-of use, a relaxed format may be used where:
-
- * ``:`` to delimit keys and values can be substuted by ``=`` or a space.
- * ``"`` around keys and string can be omitted as long as no special characters are used in the strings.
- * ``,`` to separate objects can be replaced with a whitespace character.
- * ``#`` can be used to start a comment until the line end
-
-
-CONFIGURATION FILE SECTIONS
-===========================
-
-context.properties
-  Dictionary. These properties configure the PipeWire instance.
-
-context.spa-libs
-  Dictionary. Maps plugin features with globs to a spa library.
-
-context.modules
-  Array of dictionaries. Each entry in the array is a dictionary with the *name* of the module to load,
-  including optional *args* and *flags*. Most modules support being loaded
-  multiple times.
-
-context.objects
-  Array of dictionaries. Each entry in the array is a dictionary containing the *factory* to create an
-  object from and optional extra arguments specific to that factory.
-
-context.exec
-  Array of dictionaries. Each entry in the array is dictionary containing the *path* of a program to
-  execute on startup and optional *args*.
-
-  This array used to contain an entry to start the session manager but this mode
-  of operation has since been demoted to development aid. Avoid starting a
-  session manager in this way in production environment.
-
-AUTHORS
-=======
-
-The PipeWire Developers <@PACKAGE_BUGREPORT@>; PipeWire is available from @PACKAGE_URL@
-
-SEE ALSO
-========
-
-``pipewire(1)``,
-``pw-mon(1)``,
diff --git a/man/pw-cat.1.rst.in b/man/pw-cat.1.rst.in
deleted file mode 100644
index 87d4ab6c52e0bce45d45befca160b51be06e73b8..0000000000000000000000000000000000000000
--- a/man/pw-cat.1.rst.in
+++ /dev/null
@@ -1,176 +0,0 @@
-pw-cat
-######
-
------------------------------------
-Play and record media with PipeWire
------------------------------------
-
-:Manual section: 1
-:Manual group: General Commands Manual
-
-SYNOPSIS
-========
-
-| **pw-cat** [*options*] [*FILE* \| -]
-| **pw-play** [*options*] [*FILE* \| -]
-| **pw-record** [*options*] [*FILE* \| -]
-| **pw-midiplay** [*options*] [*FILE* \| -]
-| **pw-midirecord** [*options*] [*FILE* \| -]
-| **pw-dsdplay** [*options*] [*FILE* \| -]
-
-DESCRIPTION
-===========
-
-**pw-cat** is a simple tool for playing back or
-capturing raw or encoded media files on a PipeWire
-server. It understands all audio file formats supported by
-``libsndfile`` for PCM capture and playback. When capturing PCM, the filename
-extension is used to guess the file format with the WAV file format as
-the default.
-
-It understands standard MIDI files for playback and recording. This tool
-will not render MIDI files, it will simply make the MIDI events available
-to the graph. You need a MIDI renderer such as qsynth, timidity or a hardware
-MIDI rendered to hear the MIDI.
-
-DSD playback is supported with the DSF file format. This tool will only work
-with native DSD capable hardware and will produce an error when no such
-hardware was found.
-
-When the *FILE* is - input and output will be raw data from STDIN and
-STDOUT respectively.
-
-OPTIONS
-=======
-
--h | --help
-  Show help.
-
---version
-  Show version information.
-
--v | --verbose
-  Verbose operation.
-
--R | --remote=NAME
-  The name the *remote* instance to connect to. If left unspecified,
-  a connection is made to the default PipeWire instance.
-
--p | --playback
-  Playback mode. Read data from the specified file, and play it back. If the tool
-  is called under the name **pw-play** or **pw-midiplay** this is the default.
-
--r | --record
-  Recording mode. Capture data and write it to the specified file. If the tool is
-  called under the name **pw-record** or **pw-midirecord** this is the default.
-
--m | --midi
-  MIDI mode. *FILE* is a MIDI file. If the tool is called under the name
-  **pw-midiplay** or **pw-midirecord** this is the default.
-  Note that this program will *not* render the MIDI events into audible samples,
-  it will simply provide the MIDI events in the graph. You need a separate
-  MIDI renderer such as qsynth, timidity or a hardware renderer to hear the MIDI.
-
--d | --dsd
-  DSD mode. *FILE* is a DSF file. If the tool is called under the name
-  **pw-dsdplay** this is the default.
-  Note that this program will *not* render the DSD audio. You need a DSD capable
-  device to play DSD content or this program will exit with an error.
-
---media-type=VALUE
-  Set the media type property (default Audio/Midi depending on mode).
-  The media type is used by the session manager to select a suitable target
-  to link to.
-
---media-category=VALUE
-  Set the media category property (default Playback/Capture depending on mode).
-  The media type is used by the session manager to select a suitable target
-  to link to.
-
---media-role=VALUE
-  Set the media role property (default Music).
-  The media type is used by the session manager to select a suitable target
-  to link to.
-
---target=VALUE
-  Set a node target (default auto). The value can be:
-
-  auto
-    Automatically select (Default)
-
-  0
-    Don't try to link this node
-
-  <id>
-    The object.serial or the node.name of a target node
-
---latency=VALUE[*units*]
-  Set the node latency (default 100ms)
-
-  The latency determines the minimum amount of time it takes
-  for a sample to travel from application to device (playback) and
-  from device to application (capture).
-
-  The latency determines the size of the buffers that the
-  application will be able to fill. Lower latency means smaller
-  buffers but higher overhead. Higher latency means larger buffers
-  and lower overhead.
-
-  Units can be **s** for seconds, **ms** for milliseconds,
-  **us** for microseconds, **ns** for nanoseconds.
-  If no units are given, the latency value is samples with the samplerate
-  of the file.
-
--P | --properties=VALUE
-  Set extra stream properties as a JSON object.
-
--q | --quality=VALUE
-  Resampler quality. When the samplerate of the source or
-  destination file does not match the samplerate of the server, the
-  data will be resampled. Higher quality uses more CPU. Values between 0 and 15 are
-  allowed, the default quality is 4.
-
---rate=VALUE
-  The sample rate, default 48000.
-
---channels=VALUE
-  The number of channels, default 2.
-
---channel-map=VALUE
-  The channelmap. Possible values include:
-  **mono**, **stereo**, **surround-21**,
-  **quad**, **surround-22**, **surround-40**,
-  **surround-31**, **surround-41**,
-  **surround-50**, **surround-51**,
-  **surround-51r**, **surround-70**,
-  **surround-71** or a comma separated list of channel names:
-  **FL**, **FR**, **FC**, **LFE**,
-  **SL**, **SR**, **FLC**, **FRC**,
-  **RC**, **RL**, **RR**, **TC**,
-  **TFL**, **TFC**, **TFR**, **TRL**,
-  **TRC**, **TRR**, **RLC**, **RRC**,
-  **FLW**, **FRW**, **LFE2**, **FLH**,
-  **FCH**, **FRH**, **TFLC**, **TFRC**,
-  **TSL**, **TSR**, **LLFR**, **RLFE**,
-  **BC**, **BLC**, **BRC**
-
---format=VALUE
-  The sample format to use. One of:
-  **u8**, **s8**, **s16** (default), **s24**, **s32**,
-  **f32**, **f64**.
-
---volume=VALUE
-  The stream volume, default 1.000.
-  Depending on the locale you have configured, "," or "." may be
-  used as a decimal separator. Check with **locale** command.
-
-AUTHORS
-=======
-
-The PipeWire Developers <@PACKAGE_BUGREPORT@>; PipeWire is available from @PACKAGE_URL@
-
-SEE ALSO
-========
-
-``PipeWire(1)``,
-``pw-mon(1)``,
diff --git a/man/pw-cli.1.rst.in b/man/pw-cli.1.rst.in
deleted file mode 100644
index 31a5acb9ea8c3ef88c4b3f4c0302e6838750c5b2..0000000000000000000000000000000000000000
--- a/man/pw-cli.1.rst.in
+++ /dev/null
@@ -1,195 +0,0 @@
-pw-cli
-######
-
------------------------------------
-The PipeWire Command Line Interface
------------------------------------
-
-:Manual section: 1
-:Manual group: General Commands Manual
-
-SYNOPSIS
-========
-
-| **pw-cli** [*command*]
-
-DESCRIPTION
-===========
-
-Interact with a PipeWire instance.
-
-When a command is given, **pw-cli**
-will execute the command and exit
-
-When no command is given, **pw-cli**
-starts an interactive session with the default PipeWire instance
-*pipewire-0*.
-
-Connections to other, remote instances can be made. The current instance
-name is displayed at the prompt.
-
-Note that **pw-cli** also creates a local PipeWire instance. Some commands
-operate on the current (remote) instance and some on the local instance, such
-as module loading.
-
-Use the 'help' command to list the available commands.
-
-GENERAL COMMANDS
-================
-
-help | h
-  Show a quick help on the commands available. It also lists the aliases
-  for many commands.
-
-quit | q
-  Exit from **pw-cli**
-
-MODULE MANAGEMENT
-=================
-
-| Modules are loaded and unloaded in the local instance, thus the pw-cli
-| binary itself and can add functionality or objects to the local
-| instance. It is not possible in PipeWire to load modules in another
-| instance.
-
-load-module *name* [*arguments...*]
-  Load a module specified by its name and arguments in the local instance.
-  For most modules it is OK to be loaded more than once.
-
-  This command returns a module variable that can be used
-  to unload the module.
-
-  The locally module is *not* visible in the remote instance. It is not
-  possible in PipeWire to load modules in a remote instance.
-
-unload-module *module-var*
-  Unload a module, specified either by its variable.
-
-OBJECT INTROSPECTION
-====================
-
-list-objects
-  List the objects of the current instance.
-
-  Objects are listed with their *id*, *type* and *version*.
-
-info *id* | *all*
-  Get information about a specific object or *all* objects.
-
-  Requesting info about an object will also notify you of changes.
-
-WORKING WITH REMOTES
-====================
-
-connect [*remote-name*]
-  Connect to a remote instance and make this the new current
-  instance.
-
-  If no remote name is specified, a connection is made to
-  the default remote instance, usually *pipewire-0*.
-
-  The special remote name called *internal* can be used to connect to
-  the local **pw-cli** PipeWire instance.
-
-  This command returns a remote var that can be used to disconnect or
-  switch remotes.
-
-disconnect [*remote-var*]
-  Disconnect from a *remote instance*.
-
-  If no remote name is specified, the current instance is disconnected.
-
-list-remotes
-  List all *remote instances*.
-
-switch-remote [*remote-var*]
-  Make the specified *remote* the current instance.
-
-  If no remote name is specified, the first instance is made current.
-
-NODE MANAGEMENT
-===============
-
-create-node *factory-name* [*properties...*]
-  Create a node from a factory in the current instance.
-
-  Properties are key=value pairs separated by whitespace.
-
-  This command returns a *node variable*.
-
-export-node *node-id* [*remote-var*]
-  Export a node from the local instance to the specified instance.
-  When no instance is specified, the node will be exported to the current
-  instance.
-
-DEVICE MANAGEMENT
-=================
-
-create-device *factory-name* [*properties...*]
-  Create a device from a factory in the current instance.
-
-  Properties are key=value pairs separated by whitespace.
-
-  This command returns a *device variable*.
-
-
-LINK MANAGEMENT
-===============
-
-create-link *node-id* *port-id* *node-id* *port-id* [*properties...*]
-  Create a link between 2 nodes and ports.
-
-  Port *ids* can be *-1* to automatically select an available port.
-
-  Properties are key=value pairs separated by whitespace.
-
-  This command returns a *link variable*.
-
-GLOBALS MANAGEMENT
-==================
-
-destroy *object-id*
-  Destroy a global object.
-
-
-PARAMETER MANAGEMENT
-====================
-
-enum-params *object-id* *param-id*
-  Enumerate params of an object.
-
-  *param-id* can also be given as the param short name.
-
-set-param *object-id* *param-id* *param-json*
-  Set param of an object.
-
-  *param-id* can also be given as the param short name.
-
-PERMISSION MANAGEMENT
-=====================
-
-permissions *client-id* *object-id* *permission*
-  Set permissions for a client.
-
-  *object-id* can be *-1* to set the default permissions.
-
-get-permissions *client-id*
-  Get permissions of a client.
-
-
-COMMAND MANAGEMENT
-==================
-
-send-command *object-id*
-  Send a command to an object.
-
-AUTHORS
-=======
-
-The PipeWire Developers <@PACKAGE_BUGREPORT@>; PipeWire is available from @PACKAGE_URL@
-
-SEE ALSO
-========
-
-``pipewire(1)``,
-``pw-mon(1)``,
diff --git a/man/pw-config.1.rst.in b/man/pw-config.1.rst.in
deleted file mode 100644
index a3df2dbf882095f5b66d06499a1a9787d9b1cf3f..0000000000000000000000000000000000000000
--- a/man/pw-config.1.rst.in
+++ /dev/null
@@ -1,110 +0,0 @@
-pw-config
-#########
-
------------------------------
-Debug PipeWire Config parsing
------------------------------
-
-:Manual section: 1
-:Manual group: General Commands Manual
-
-SYNOPSIS
-========
-
-| **pw-config** [*options*] paths
-
-| **pw-config** [*options*] list [*SECTION*]
-
-| **pw-config** [*options*] merge *SECTION*
-
-DESCRIPTION
-===========
-
-List config paths and config sections and display the parsed
-output.
-
-This tool can be used to get an overview of the config file that will be
-parsed by the PipeWire server and clients.
-
-COMMON OPTIONS
-==============
-
--h | --help
-  Show help.
-
---version
-  Show version information.
-
--n | --name=NAME
-  Config Name (default 'pipewire.conf')
-
--p | --prefix=PREFIX
-  Config Prefix (default '')
-
--L | --no-newline
-  Omit newlines after values
-
--r | --recurse
-  Reformat config sections recursively
-
--N | --no-colors
-  Disable color output
-
--C | --color[=WHEN]
-  whether to enable color support. WHEN is `never`, `always`, or `auto`
-
-LISTING PATHS
-=============
-
-Specify the paths command. It will display all the config files that will
-be parsed and in what order.
-
-LISTING CONFIG SECTIONS
-=======================
-
-Specify the list command with an optional *SECTION* to list the configuration
-fragments used for *SECTION*. Without a *SECTION*, all sections will be
-listed.
-
-Use the -r options to reformat the sections.
-
-MERGING A CONFIG SECTION
-========================
-
-With the merge option and a *SECTION*, pw-config will merge all config files into
-a merged config section and dump the results. This will be the section used by
-the client or server.
-
-Use the -r options to reformat the sections.
-
-EXAMPLES
-========
-
-**pw-config**
-  List all config files that will be used
-
-**pw-config** -n pipewire-pulse.conf
-  List all config files that will be used by the PipeWire pulseaudio server.
-
-**pw-config** -n pipewire-pulse.conf list
-  List all config sections used by the PipeWire pulseaudio server
-
-**pw-config** -n jack.conf list context.properties
-  List the context.properties fragments used by the JACK clients
-
-**pw-config** -n jack.conf merge context.properties
-  List the merged context.properties used by the JACK clients
-
-**pw-config** -n pipewire.conf -r merge context.modules
-  List the merged context.modules used by the PipeWire server and reformat
-
-AUTHORS
-=======
-
-The PipeWire Developers <@PACKAGE_BUGREPORT@>; PipeWire is available from @PACKAGE_URL@
-
-SEE ALSO
-========
-
-``pipewire(1)``,
-``pw-dump(1)``,
diff --git a/man/pw-dot.1.rst.in b/man/pw-dot.1.rst.in
deleted file mode 100644
index 459953982e2167f4ee4cfc5bdaded435344d004f..0000000000000000000000000000000000000000
--- a/man/pw-dot.1.rst.in
+++ /dev/null
@@ -1,65 +0,0 @@
-pw-dot
-######
-
----------------------------
-The PipeWire dot graph dump
----------------------------
-
-:Manual section: 1
-:Manual group: General Commands Manual
-
-SYNOPSIS
-========
-
-| **pw-dot** [*options*]
-
-DESCRIPTION
-===========
-
-Create a .dot file of the PipeWire graph.
-
-The .dot file can then be visualized with a tool like **dotty**
-or rendered to a PNG file with ``dot -Tpng pw.dot -o pw.png``.
-
-OPTIONS
-=======
-
--r | --remote=NAME
-  The name the remote instance to connect to. If left unspecified,
-  a connection is made to the default PipeWire instance.
-
--h | --help
-  Show help.
-
---version
-  Show version information.
-
--a | --all
-  Show all object types.
-
--s | --smart
-  Show linked objects only.
-
--d | --detail
-  Show all object properties.
-
--o FILE | --output=FILE
-  Output file name (Default pw.dot). Use - for stdout.
-
--L | --lr
-  Lay the graph from left to right, instead of dot's default top to bottom.
-
--9 | --90
-  Lay the graph using 90-degree angles in edges.
-
-AUTHORS
-=======
-
-The PipeWire Developers <@PACKAGE_BUGREPORT@>; PipeWire is available from @PACKAGE_URL@
-
-SEE ALSO
-========
-
-``pipewire(1)``,
-``pw-cli(1)``,
-``pw-mon(1)``,
diff --git a/man/pw-jack.1.rst.in b/man/pw-jack.1.rst.in
deleted file mode 100644
index 0781b3d7b2cec3f536fa4a92e57078e7520e54e1..0000000000000000000000000000000000000000
--- a/man/pw-jack.1.rst.in
+++ /dev/null
@@ -1,65 +0,0 @@
-pw-jack
-#######
-
-----------------------------
-Use PipeWire instead of JACK
-----------------------------
-
-:Manual section: 1
-:Manual group: General Commands Manual
-
-SYNOPSIS
-========
-
-| **pw-jack** [*options*] *COMMAND* [*FILE*]
-
-DESCRIPTION
-===========
-
-**pw-jack** modifies the ``LD_LIBRARY_PATH`` environment
-variable so that applications will load PipeWire's reimplementation
-of the JACK client libraries instead of JACK's own
-libraries. This results in JACK clients being redirected to
-PipeWire.
-
-If PipeWire's reimplementation of the JACK client libraries
-has been installed as a system-wide replacement for JACK's
-own libraries, then the whole system already behaves in that way,
-in which case **pw-jack** has no practical effect.
-
-OPTIONS
-=======
-
--h
-  Show help.
-
--r NAME
-  The name of the remote instance to connect to. If left
-  unspecified, a connection is made to the default PipeWire
-  instance.
-
--v
-  Verbose operation.
-
-EXAMPLES
-========
-
-| **pw-jack** sndfile-jackplay /usr/share/sounds/freedesktop/stereo/bell.oga
-
-NOTES
-=====
-
-Using PipeWire for audio is currently considered to be
-experimental.
-
-AUTHORS
-=======
-
-The PipeWire Developers <@PACKAGE_BUGREPORT@>;
-PipeWire is available from @PACKAGE_URL@
-
-SEE ALSO
-========
-
-``pipewire(1)``,
-``jackd(1)``,
diff --git a/man/pw-link.1.rst.in b/man/pw-link.1.rst.in
deleted file mode 100644
index 7d1001c58abdf2c57887af7a8bb324efb6307cbb..0000000000000000000000000000000000000000
--- a/man/pw-link.1.rst.in
+++ /dev/null
@@ -1,139 +0,0 @@
-pw-link
-#######
-
--------------------------
-The PipeWire Link Command
--------------------------
-
-:Manual section: 1
-:Manual group: General Commands Manual
-
-SYNOPSIS
-========
-
-| **pw-link** [*options*] -o|-i|-l [*out-pattern*] [*in-pattern*]
-
-| **pw-link** [*options*] *output* *input*
-
-| **pw-link** [*options*] -d *output* *input*
-
-| **pw-link** [*options*] -d *link-id*
-
-DESCRIPTION
-===========
-
-List, create and destroy links between PipeWire ports.
-
-COMMON OPTIONS
-==============
-
--r | --remote=NAME
-  The name the *remote* instance to monitor. If left unspecified,
-  a connection is made to the default PipeWire instance.
-
--h | --help
-  Show help.
-
---version
-  Show version information.
-
-LISTING PORTS AND LINKS
-=======================
-
-Specify one of -o, -i or -l to list the matching optional input and
-output ports and their links.
-
--o | --output
-  List output ports
-
--i | --input
-  List output ports
-
--l | --links
-  List links
-
--m | --monitor
-  Monitor links and ports. **pw-link** will not exit but monitor and
-  print new and destroyed ports or links.
-
--I | --id
-  List IDs. Also list the unique link and port ids.
-
--v | --verbose
-  Verbose port properties. Also list the port-object-path and the port-alias.
-
-CONNECTING PORTS
-================
-
-Without any list option (-i, -o or -l), the given ports will be linked.
-Valid port specifications are:
-
-*port-id*
-  As obtained with the -I option when listing ports.
-
-*node-name:port-name*
-  As obtained when listing ports.
-
-*port-object-path*
-  As obtained from the first alternative name for the port when listing
-  them with the -v option.
-
-*port-alias*
-  As obtained from the second alternative name for the ports when listing
-  them with the -v option.
-
-Extra options when linking can be given:
-
--L | --linger
-  Linger. Will create a link that exists after **pw-link** is destroyed.
-  This is the default behaviour, unless the -m option is given.
-
--P | --passive
-  Passive link. A passive link will keep both nodes it links inactive
-  unless another non-passive link is activating the nodes. You can use this
-  to link a sink to a filter and have them both suspended when nothing else
-  is linked to either of them.
-
--p | --props=PROPS
-  Properties as JSON object. Give extra properties when creaing the link.
-
-DISCONNECTING PORTS
-===================
-
-When the -d option is given, an existing link between port is destroyed.
-
-To disconnect port, a single *link-id*, as obtained when listing links with
-the -I option, or two port specifications can be given. See the connecting
-ports section for valid port specifications.
-
--d | --disconnect
-  Disconnect ports
-
-EXAMPLES
-========
-
-**pw-link** -iol
-  List all port and their links.
-
-**pw-link** -lm
-  List all links and monitor changes until **pw-link** is stopped.
-
-**pw-link** paplay:output_FL alsa_output.pci-0000_00_1b.0.analog-stereo:playback_FL
-  Link the given output port to the input port.
-
-**pw-link** -lI
-  List links and their Id.
-
-**pw-link** -d 89
-  Destroy the link with id 89.
-
-AUTHORS
-=======
-
-The PipeWire Developers <@PACKAGE_BUGREPORT@>; PipeWire is available from @PACKAGE_URL@
-
-SEE ALSO
-========
-
-``pipewire(1)``,
-``pw-cli(1)``,
diff --git a/man/pw-metadata.1.rst.in b/man/pw-metadata.1.rst.in
deleted file mode 100644
index 32f9c51177a4823b96ac546d277c6e946ae89153..0000000000000000000000000000000000000000
--- a/man/pw-metadata.1.rst.in
+++ /dev/null
@@ -1,82 +0,0 @@
-pw-metadata
-###########
-
----------------------
-The PipeWire metadata
----------------------
-
-:Manual section: 1
-:Manual group: General Commands Manual
-
-SYNOPSIS
-========
-
-| **pw-metadata** [*options*] [*id* [*key* [*value* [*type* ] ] ] ]
-
-DESCRIPTION
-===========
-
-Monitor, set and delete metadata on PipeWire objects.
-
-Metadata are key/type/value triplets attached to objects identified
-by *id*. The metadata is shared between all applications
-binding to the same metadata object. When an object is destroyed, all its
-metadata is automatically removed.
-
-When no *value* is given, **pw-metadata** will query and
-log the metadata matching the optional arguments *id*
-and *key*. Without any arguments, all metadata is displayed.
-
-When *value* is given, **pw-metadata** will set the
-metadata for *id* and *key* to *value* and
-an optional *type*.
-
-OPTIONS
-=======
-
--r | --remote=NAME
-  The name the remote instance to use. If left unspecified,
-  a connection is made to the default PipeWire instance.
-
--h | --help
-  Show help.
-
---version
-  Show version information.
-
--l | --list
-  List available metadata objects
-
--m | --monitor
-  Keeps running and log the changes to the metadata.
-
--d | --delete
-  Delete all metadata for *id* or for the specified *key* of object *id*.
-  Without any option, all metadata is removed.
-
--n | --name
-  Metadata name (Default: "default").
-
-EXAMPLES
-========
-
-**pw-metadata**
-  Show metadata in default name.
-
-**pw-metadata** -n settings 0
-  Display settings.
-
-**pw-metadata** -n settings 0 clock.quantum 1024
-  Change clock.quantum to 1024.
-
-AUTHORS
-=======
-
-The PipeWire Developers <@PACKAGE_BUGREPORT@>; PipeWire is available from @PACKAGE_URL@
-
-SEE ALSO
-========
-
-``pipewire(1)``,
-``pw-mon(1)``,
-``pw-cli(1)``,
diff --git a/man/pw-mididump.1.rst.in b/man/pw-mididump.1.rst.in
deleted file mode 100644
index bb56ec673232a0d8336a36fbc421685003f7b9ed..0000000000000000000000000000000000000000
--- a/man/pw-mididump.1.rst.in
+++ /dev/null
@@ -1,49 +0,0 @@
-pw-mididump
-###########
-
-----------------------
-The PipeWire MIDI dump
-----------------------
-
-:Manual section: 1
-:Manual group: General Commands Manual
-
-SYNOPSIS
-========
-
-| **pw-mididump** [*options*] [*FILE*]
-
-DESCRIPTION
-===========
-
-Dump MIDI messages to stdout.
-
-When a MIDI file is given, the events inside the file are printed.
-
-When no file is given, **pw-mididump** creates a PipeWire
-MIDI input stream and will print all MIDI events received on the port to
-stdout.
-
-OPTIONS
-=======
-
--r | --remote=NAME
-  The name the remote instance to monitor. If left unspecified,
-  a connection is made to the default PipeWire instance.
-
--h | --help
-  Show help.
-
---version
-  Show version information.
-
-AUTHORS
-=======
-
-The PipeWire Developers <@PACKAGE_BUGREPORT@>; PipeWire is available from @PACKAGE_URL@
-
-SEE ALSO
-========
-
-``pipewire(1)``,
-``pw-cat(1)``,
diff --git a/man/pw-mon.1.rst.in b/man/pw-mon.1.rst.in
deleted file mode 100644
index 775de0a4e412ca6e3b4a586788221e23b3238a82..0000000000000000000000000000000000000000
--- a/man/pw-mon.1.rst.in
+++ /dev/null
@@ -1,46 +0,0 @@
-pw-mon
-######
-
---------------------
-The PipeWire monitor
---------------------
-
-:Manual section: 1
-:Manual group: General Commands Manual
-
-SYNOPSIS
-========
-
-| **pw-mon** [*options*]
-
-DESCRIPTION
-===========
-
-Monitor objects on the PipeWire instance.
-
-OPTIONS
-=======
-
--r | --remote=NAME
-  The name the *remote* instance to monitor. If left unspecified,
-  a connection is made to the default PipeWire instance.
-
--h | --help
-  Show help.
-
---version
-  Show version information.
-
--N | --color=WHEN
-  Whether to use color, one of 'never', 'always', or 'auto'. The
-  default is 'auto'. **-N** is equivalent to **--color=never**.
-
-AUTHORS
-=======
-
-The PipeWire Developers <@PACKAGE_BUGREPORT@>; PipeWire is available from @PACKAGE_URL@
-
-SEE ALSO
-========
-
-``pipewire(1)``,
diff --git a/man/pw-profiler.1.rst.in b/man/pw-profiler.1.rst.in
deleted file mode 100644
index 6fb57c8eb29856b6c56366592b43099f4290acf1..0000000000000000000000000000000000000000
--- a/man/pw-profiler.1.rst.in
+++ /dev/null
@@ -1,57 +0,0 @@
-pw-profiler
-###########
-
----------------------
-The PipeWire profiler
----------------------
-
-:Manual section: 1
-:Manual group: General Commands Manual
-
-SYNOPSIS
-========
-
-| **pw-profiler** [*options*]
-
-DESCRIPTION
-===========
-
-Start profiling a PipeWire instance.
-
-If the server has the profiler module loaded, this program will
-connect to it and log the profiler data. Profiler data contains
-times and durations when processing nodes and devices started and
-completed.
-
-When this program is stopped, a set of **gnuplot** files and a script to generate
-SVG files from the .plot files is generated, along with a .html file to
-visualize the profiling results in a browser.
-
-This function uses the same data used by *pw-top*.
-
-OPTIONS
-=======
-
--r | --remote=NAME
-  The name the remote instance to monitor. If left unspecified,
-  a connection is made to the default PipeWire instance.
-
--h | --help
-  Show help.
-
---version
-  Show version information.
-
--o | --output=FILE
-  Profiler output name (default "profiler.log").
-
-AUTHORS
-=======
-
-The PipeWire Developers <@PACKAGE_BUGREPORT@>; PipeWire is available from @PACKAGE_URL@
-
-SEE ALSO
-========
-
-``pipewire(1)``,
-``pw-top(1)``,
diff --git a/man/pw-top.1.rst.in b/man/pw-top.1.rst.in
deleted file mode 100644
index 0320a27522a5a9dbcfb4b892cc3a874a65b3ca00..0000000000000000000000000000000000000000
--- a/man/pw-top.1.rst.in
+++ /dev/null
@@ -1,184 +0,0 @@
-pw-top
-######
-
----------------------------
-The PipeWire process viewer
----------------------------
-
-:Manual section: 1
-:Manual group: General Commands Manual
-
-SYNOPSIS
-========
-
-| **pw-top** [*options*]
-
-DESCRIPTION
-===========
-
-The *pw-top* program provides a dynamic real-time view of the pipewire
-node and device statistics.
-
-A hierarchical view is shown of Driver nodes and follower nodes. The Driver
-nodes are actively using a timer to schedule dataflow in the followers. The
-followers of a driver node as shown below their driver with a + sign in
-a tree-like representation.
-
-The columns presented are as follows:
-
-S
-  Node status.
-    E = ERROR
-    C = CREATING
-    S = SUSPENDED
-    I = IDLE
-    R = RUNNING
-
-ID
-  The ID of the pipewire node/device, as found in *pw-dump* and *pw-cli*
-
-QUANT
-  The current quantum (for drivers) and the suggested quantum for follower
-  nodes.
-
-  The quantum by itself needs to be divided by the RATE column to calculate
-  the duration of a scheduling period in fractions of a second.
-
-  For a QUANT of 1024 and a RATE of 48000, the duration of one period in the
-  graph is 1024/48000 or 21.3 milliseconds.
-
-  Follower nodes can have a 0 QUANT field, which means that the node does not
-  have a suggestion for the quantum and thus uses what the driver selected.
-
-  The driver will use the lowest quantum of any of the followers. If none of
-  the followers select a quantum, the default quantum in the pipewire configuration
-  file will be used.
-
-  The QUANT on the drivers usually translates directly into the number of audio
-  samples processed per processing cycle of the graph.
-
-  See also https://gitlab.freedesktop.org/pipewire/pipewire/-/wikis/FAQ#pipewire-buffering-explained
-
-RATE
-  The current rate (for drivers) and the suggested rate for follower
-  nodes.
-
-  This is the rate at which the *graph* processes data and needs to be combined with
-  the QUANT value to derive the duration of a processing cycle in the graph.
-
-  Some nodes can have a 0 RATE, which means that they don't have any rate
-  suggestion for the graph. Nodes that suggest a rate can make the graph switch
-  rates if the graph is otherwise idle and the new rate is allowed as
-  a possible graph rate (see the pipewire configuration file).
-
-  The RATE on (audio) driver nodes usually also translates directly to the
-  samplerate used by the device. Although some devices might not be able to
-  operate at the given samplerate, in which case resampling will need to be
-  done. The negotiated samplerate with the device and stream can be found in
-  the FORMAT column.
-
-WAIT
-  The waiting time of a node is the elapsed time between when the node
-  is ready to start processing and when it actually started processing.
-
-  For Driver nodes, this is the time between when the node wakes up to
-  start processing the graph and when the driver (and thus also the graph)
-  completes a cycle. The WAIT time for driver is thus the elapsed time
-  processing the graph.
-
-  For follower nodes, it is the time spent between being woken up (when all
-  dependencies of the node are satisfied) and when processing starts. The
-  WAIT time for follower nodes is thus mostly caused by context switching.
-
-  A value of --- means that the node was not signaled.  A value of +++
-  means that the node was signaled but not awake.
-
-BUSY
-  The processing time is started when the node starts processing until it
-  completes and wakes up the next nodes in the graph.
-
-  A value of --- means that the node was not started.  A value of +++
-  means that the node was started but did not complete.
-
-W/Q
-  Ratio of WAIT / QUANT.
-
-  The W/Q time of the driver node is a good measure of the graph
-  load. The running averages of the driver W/Q ratios are used as the DSP
-  load in other (JACK) tools.
-
-  Values of --- and +++ are copied from the WAIT column.
-
-B/Q
-  Ratio of BUSY / QUANT
-
-  This is a good measure of the load of a particular driver or follower
-  node.
-
-  Values of --- and +++ are copied from the BUSY column.
-
-ERR
-  Total of Xruns and Errors
-
-  Xruns for drivers are when the graph did not complete a cycle. This can
-  be because a node in the graph also has an Xrun. It can also be caused when
-  scheduling delays cause a deadline to be missed, causing a hardware
-  Xrun.
-
-  Xruns for followers are incremented when the node started processing but
-  did not complete before the end of the graph cycle deadline.
-
-FORMAT
-  The format used by the driver node or the stream. This is the hardware format
-  negotiated with the device or stream.
-
-  If the stream of driver has a different rate than the graph, resampling will
-  be done.
-
-  For raw audio formats, the layout is <sampleformat> <channels> <samplerate>.
-
-  For IEC958 passthrough audio formats, the layout is IEC958 <codec> <samplerate>.
-
-  For DSD formats, the layout is <dsd-rate> <channels>.
-
-  For Video formats, the layout is <pixelformat> <width>x<height>.
-
-NAME
-  Name assigned to the device/node, as found in *pw-dump* node.name
-
-  Names are prefixed by *+* when they are linked to a driver (entry above with no +)
-
-
-OPTIONS
-=======
-
--h | --help
-  Show help.
-
--b | --batch-mode
-  Run in non-interactive batch mode, similar to top's batch mode.
-
--n | --iterations=NUMBER
-  Exit after NUMBER of batch iterations. Only used in batch mode.
-
--r | --remote=NAME
-  The name the *remote* instance to monitor. If left unspecified,
-  a connection is made to the default PipeWire instance.
-
--V | --version
-  Show version information.
-
-
-AUTHORS
-=======
-
-The PipeWire Developers <@PACKAGE_BUGREPORT@>; PipeWire is available from @PACKAGE_URL@
-
-SEE ALSO
-========
-
-``pipewire(1)``,
-``pw-dump(1)``,
-``pw-cli(1)``,
-``pw-profiler(1)``,
-
diff --git a/meson.build b/meson.build
index 535f9176aa92bc2201a63db95e5cbcb9089ec222..70e358f81abd9de2d257f8f40a2d5ee004d113fb 100644
--- a/meson.build
+++ b/meson.build
@@ -1,5 +1,5 @@
 project('pipewire', ['c' ],
-  version : '0.3.84',
+  version : '1.0.0',
   license : [ 'MIT', 'LGPL-2.1-or-later', 'GPL-2.0-only' ],
   meson_version : '>= 0.61.1',
   default_options : [ 'warning_level=3',
@@ -219,8 +219,7 @@ cdata.set_quoted('MODULEDIR', modules_install_dir)
 cdata.set_quoted('PIPEWIRE_CONFIG_DIR', pipewire_configdir)
 cdata.set_quoted('PLUGINDIR', spa_plugindir)
 cdata.set_quoted('SPADATADIR', spa_datadir)
-cdata.set_quoted('PA_ALSA_PATHS_DIR', alsadatadir / 'paths')
-cdata.set_quoted('PA_ALSA_PROFILE_SETS_DIR', alsadatadir / 'profile-sets')
+cdata.set_quoted('PA_ALSA_DATA_DIR', alsadatadir)
 
 if host_machine.endian() == 'big'
   cdata.set('WORDS_BIGENDIAN', 1)
@@ -486,22 +485,21 @@ if alsa_dep.found()
   subdir('pipewire-alsa/tests')
 endif
 
-generate_manpages = false
-if get_option('man').allowed()
-  rst2man = find_program('rst2man', required: false)
-  if not rst2man.found()
-    rst2man = find_program('rst2man.py', required: get_option('man'))
-  endif
-  if rst2man.found()
-    generate_manpages = true
-  endif
+generate_docs = get_option('man').enabled() or get_option('docs').enabled()
+if get_option('man').allowed() or get_option('docs').allowed()
+  doxygen = find_program('doxygen', required : generate_docs)
+  pymod = import('python')
+  python = pymod.find_installation('python3', required: generate_docs)
+  generate_docs = doxygen.found() and python.found()
 endif
 
-summary({'Manpage generation': generate_manpages}, bool_yn: true)
-subdir('man')
+install_docs = get_option('docs').require(generate_docs).allowed()
+install_man = get_option('man').require(generate_docs).allowed()
+
+summary({'Documentation ': install_docs}, bool_yn: true)
+summary({'Man pages ': install_man}, bool_yn: true)
 
-doxygen = find_program('doxygen', required : get_option('docs'))
-if doxygen.found()
+if generate_docs
   subdir('doc')
 endif
 
diff --git a/meson_options.txt b/meson_options.txt
index 3344b62444e11dbef9671399817765447167eaed..a09b65859070b7135b0a7d5e0029364a5acdf6ed 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -2,17 +2,17 @@ option('docdir',
        type : 'string',
        description : 'Directory for installing documentation to (defaults to pipewire_datadir/doc/meson.project_name() )')
 option('docs',
-       description: 'Build documentation',
+       description: 'Documentation',
+       type: 'feature',
+       value: 'disabled')
+option('man',
+       description: 'Manual pages',
        type: 'feature',
        value: 'disabled')
 option('examples',
        description: 'Build examples',
        type: 'feature',
        value: 'enabled')
-option('man',
-       description: 'Build manpages',
-       type: 'feature',
-       value: 'auto')
 option('tests',
        description: 'Build tests',
        type: 'feature',
diff --git a/pipewire-alsa/alsa-plugins/pcm_pipewire.c b/pipewire-alsa/alsa-plugins/pcm_pipewire.c
index dc4230e8fdaef1b10772ea7fd36adefb38a77a0b..3464692dc7566feaa2f6cacc2037b301d6364bca 100644
--- a/pipewire-alsa/alsa-plugins/pcm_pipewire.c
+++ b/pipewire-alsa/alsa-plugins/pcm_pipewire.c
@@ -89,7 +89,7 @@ typedef struct {
 
 static int snd_pcm_pipewire_stop(snd_pcm_ioplug_t *io);
 
-static int check_active(snd_pcm_ioplug_t *io)
+static int update_active(snd_pcm_ioplug_t *io)
 {
 	snd_pcm_pipewire_t *pw = io->private_data;
 	snd_pcm_sframes_t avail;
@@ -97,7 +97,10 @@ static int check_active(snd_pcm_ioplug_t *io)
 
 	avail = snd_pcm_ioplug_avail(io, pw->hw_ptr, io->appl_ptr);
 
-	if (io->state == SND_PCM_STATE_DRAINING) {
+	if (pw->error > 0) {
+		active = true;
+	}
+	else if (io->state == SND_PCM_STATE_DRAINING) {
 		active = pw->drained;
 	}
 	else if (avail >= 0 && avail < (snd_pcm_sframes_t)pw->min_avail) {
@@ -105,33 +108,27 @@ static int check_active(snd_pcm_ioplug_t *io)
 	}
 	else if (avail >= (snd_pcm_sframes_t)pw->min_avail) {
 		active = true;
-	} else {
+	}
+	else {
 		active = false;
 	}
 	if (pw->active != active) {
+		uint64_t val;
+
 		pw_log_trace("%p: avail:%lu min-avail:%lu state:%s hw:%lu appl:%lu active:%d->%d state:%s",
 			pw, avail, pw->min_avail, snd_pcm_state_name(io->state),
 			pw->hw_ptr, io->appl_ptr, pw->active, active,
 			snd_pcm_state_name(io->state));
+
+		pw->active = active;
+		if (active)
+			spa_system_eventfd_write(pw->system, io->poll_fd, 1);
+		else
+			spa_system_eventfd_read(pw->system, io->poll_fd, &val);
 	}
 	return active;
 }
 
-
-static int update_active(snd_pcm_ioplug_t *io)
-{
-	snd_pcm_pipewire_t *pw = io->private_data;
-	pw->active = check_active(io);
-	uint64_t val;
-
-	if (pw->active || pw->error < 0)
-		spa_system_eventfd_write(pw->system, io->poll_fd, 1);
-	else
-		spa_system_eventfd_read(pw->system, io->poll_fd, &val);
-
-	return pw->active;
-}
-
 static void snd_pcm_pipewire_free(snd_pcm_pipewire_t *pw)
 {
 	if (pw == NULL)
@@ -162,15 +159,6 @@ static int snd_pcm_pipewire_close(snd_pcm_ioplug_t *io)
 	return 0;
 }
 
-static int snd_pcm_pipewire_poll_descriptors(snd_pcm_ioplug_t *io, struct pollfd *pfds, unsigned int space)
-{
-	snd_pcm_pipewire_t *pw = io->private_data;
-	update_active(io);
-	pfds->fd = pw->fd;
-	pfds->events = POLLIN | POLLERR | POLLNVAL;
-	return 1;
-}
-
 static int snd_pcm_pipewire_poll_revents(snd_pcm_ioplug_t *io,
 				     struct pollfd *pfds, unsigned int nfds,
 				     unsigned short *revents)
@@ -183,10 +171,10 @@ static int snd_pcm_pipewire_poll_revents(snd_pcm_ioplug_t *io,
 		return pw->error;
 
 	*revents = pfds[0].revents & ~(POLLIN | POLLOUT);
-	if (pfds[0].revents & POLLIN && check_active(io)) {
+	if (pfds[0].revents & POLLIN && update_active(io))
 		*revents |= (io->stream == SND_PCM_STREAM_PLAYBACK) ? POLLOUT : POLLIN;
-		update_active(io);
-	}
+
+	pw_log_trace_fp("poll %d", *revents);
 
 	return 0;
 }
@@ -911,7 +899,6 @@ static snd_pcm_ioplug_callback_t pipewire_pcm_callback = {
 	.delay = snd_pcm_pipewire_delay,
 	.drain = snd_pcm_pipewire_drain,
 	.prepare = snd_pcm_pipewire_prepare,
-	.poll_descriptors = snd_pcm_pipewire_poll_descriptors,
 	.poll_revents = snd_pcm_pipewire_poll_revents,
 	.hw_params = snd_pcm_pipewire_hw_params,
 	.sw_params = snd_pcm_pipewire_sw_params,
diff --git a/pipewire-jack/src/pipewire-jack.c b/pipewire-jack/src/pipewire-jack.c
index b248ff46aad01367468a01fba76f0d79f0a8a63c..34433fe1e09d79336d67d024687c95ab44f8da28 100644
--- a/pipewire-jack/src/pipewire-jack.c
+++ b/pipewire-jack/src/pipewire-jack.c
@@ -9,6 +9,7 @@
 #include <sys/mman.h>
 #include <regex.h>
 #include <math.h>
+#include <threads.h>
 
 #include <jack/jack.h>
 #include <jack/intclient.h>
@@ -110,6 +111,10 @@ struct globals {
 static struct globals globals;
 static bool mlock_warned = false;
 
+#define MIDI_SCRATCH_FRAMES	8192
+static thread_local float midi_scratch[MIDI_SCRATCH_FRAMES];
+
+
 #define OBJECT_CHUNK		8
 #define RECYCLE_THRESHOLD	128
 
@@ -122,9 +127,10 @@ struct object {
 
 	struct client *client;
 
-#define INTERFACE_Port		0
-#define INTERFACE_Node		1
-#define INTERFACE_Link		2
+#define INTERFACE_Invalid	0
+#define INTERFACE_Port		1
+#define INTERFACE_Node		2
+#define INTERFACE_Link		3
 	uint32_t type;
 	uint32_t id;
 	uint32_t serial;
@@ -296,6 +302,15 @@ struct metadata {
 	char default_audio_source[1024];
 };
 
+struct frame_times {
+	uint64_t frames;
+	uint64_t nsec;
+	uint64_t next_nsec;
+	uint32_t buffer_frames;
+	uint32_t sample_rate;
+	double rate_diff;
+};
+
 struct client {
 	char name[JACK_CLIENT_NAME_SIZE+1];
 
@@ -440,6 +455,7 @@ struct client {
 
 	jack_position_t jack_position;
 	jack_transport_state_t jack_state;
+	struct frame_times jack_times;
 };
 
 #define return_val_if_fail(expr, val)				\
@@ -502,7 +518,7 @@ static void recycle_objects(struct client *c, uint32_t remain)
 	pthread_mutex_lock(&globals.lock);
 	spa_list_for_each_safe(o, t, &c->context.objects, link) {
 		if (o->removed) {
-			pw_log_info("%p: recycle object:%p type:%d id:%u/%u",
+			pw_log_debug("%p: recycle object:%p type:%d id:%u/%u",
 					c, o, o->type, o->id, o->serial);
 			spa_list_remove(&o->link);
 			memset(o, 0, sizeof(struct object));
@@ -531,6 +547,15 @@ static void free_object(struct client *c, struct object *o)
 
 }
 
+static inline struct object *port_to_object(const jack_port_t *port)
+{
+	return (struct object*)port;
+}
+static inline jack_port_t *object_to_port(struct object *o)
+{
+	return (jack_port_t*)o;
+}
+
 struct io_info {
 	struct mix *mix;
 	void *data;
@@ -833,21 +858,6 @@ static struct object *find_link(struct client *c, uint32_t src, uint32_t dst)
 	return NULL;
 }
 
-static struct buffer *dequeue_buffer(struct client *c, struct mix *mix)
-{
-	struct buffer *b;
-
-	if (SPA_UNLIKELY(spa_list_is_empty(&mix->queue)))
-		return NULL;
-
-	b = spa_list_first(&mix->queue, struct buffer, link);
-	spa_list_remove(&b->link);
-	SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUT);
-	pw_log_trace_fp("%p: port %p: dequeue buffer %d", c, mix->port, b->id);
-
-	return b;
-}
-
 #if defined (__SSE__)
 #include <xmmintrin.h>
 static void mix_sse(float *dst, float *src[], uint32_t n_src, bool aligned, uint32_t n_samples)
@@ -1303,7 +1313,7 @@ static void client_remove_source(struct client *c)
 	}
 }
 
-static inline void reuse_buffer(struct client *c, struct mix *mix, uint32_t id)
+static inline void queue_buffer(struct client *c, struct mix *mix, uint32_t id)
 {
 	struct buffer *b;
 
@@ -1316,6 +1326,21 @@ static inline void reuse_buffer(struct client *c, struct mix *mix, uint32_t id)
 	}
 }
 
+static inline struct buffer *dequeue_buffer(struct client *c, struct mix *mix)
+{
+	struct buffer *b;
+
+	if (SPA_UNLIKELY(spa_list_is_empty(&mix->queue)))
+		return NULL;
+
+	b = spa_list_first(&mix->queue, struct buffer, link);
+	spa_list_remove(&b->link);
+	SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUT);
+	pw_log_trace_fp("%p: port %p: dequeue buffer %d", c, mix->port, b->id);
+
+	return b;
+}
+
 
 static size_t convert_from_midi(void *midi, void *buffer, size_t size)
 {
@@ -1455,7 +1480,7 @@ static inline void *get_buffer_output(struct port *p, uint32_t frames, uint32_t
 			c, p->object->port.name, p->port_id, frames,
 			mix->n_buffers, mix->io);
 
-	if (SPA_UNLIKELY((io = mix->io) == NULL))
+	if (SPA_UNLIKELY((io = mix->io) == NULL || mix->n_buffers == 0))
 		return NULL;
 
 	if (io->status == SPA_STATUS_HAVE_DATA &&
@@ -1463,21 +1488,26 @@ static inline void *get_buffer_output(struct port *p, uint32_t frames, uint32_t
 		b = &mix->buffers[io->buffer_id];
 		d = &b->datas[0];
 	} else {
-		if (io->buffer_id < mix->n_buffers) {
-			reuse_buffer(c, mix, io->buffer_id);
-			io->buffer_id = SPA_ID_INVALID;
-		}
-		if (SPA_UNLIKELY((b = dequeue_buffer(c, mix)) == NULL)) {
-			pw_log_warn("port %p: out of buffers", p);
-			return NULL;
+		if (mix->n_buffers == 1) {
+			b = &mix->buffers[0];
+		} else {
+			if (io->buffer_id < mix->n_buffers)
+				queue_buffer(c, mix, io->buffer_id);
+			b = dequeue_buffer(c, mix);
+
+			if (SPA_UNLIKELY(b == NULL)) {
+				pw_log_warn("port %p: out of buffers %d", p, mix->n_buffers);
+				io->buffer_id = SPA_ID_INVALID;
+				return NULL;
+			}
 		}
 		d = &b->datas[0];
 		d->chunk->offset = 0;
 		d->chunk->size = frames * sizeof(float);
 		d->chunk->stride = stride;
 
-		io->status = SPA_STATUS_HAVE_DATA;
 		io->buffer_id = b->id;
+		io->status = SPA_STATUS_HAVE_DATA;
 	}
 	ptr = d->data;
 	if (buf)
@@ -1506,9 +1536,14 @@ static inline void process_empty(struct port *p, uint32_t frames)
 	{
 		struct buffer *b;
 		ptr = get_buffer_output(p, c->max_frames, 1, &b);
-		if (SPA_LIKELY(ptr != NULL))
+		if (SPA_LIKELY(ptr != NULL)) {
+			/* first build the complete pod in scratch memory, then copy it
+			 * to the target buffer. This makes it possible for multiple threads
+			 * to do this concurrently */
 			b->datas[0].chunk->size = convert_from_midi(src,
-					ptr, c->max_frames * sizeof(float));
+					midi_scratch, MIDI_SCRATCH_FRAMES * sizeof(float));
+			memcpy(ptr, midi_scratch, b->datas[0].chunk->size);
+		}
 		break;
 	}
 	default:
@@ -1564,38 +1599,38 @@ static void complete_process(struct client *c, uint32_t frames)
 
 static inline void debug_position(struct client *c, jack_position_t *p)
 {
-	pw_log_trace("usecs:       %"PRIu64, p->usecs);
-	pw_log_trace("frame_rate:  %u", p->frame_rate);
-	pw_log_trace("frame:       %u", p->frame);
-	pw_log_trace("valid:       %08x", p->valid);
+	pw_log_trace_fp("usecs:       %"PRIu64, p->usecs);
+	pw_log_trace_fp("frame_rate:  %u", p->frame_rate);
+	pw_log_trace_fp("frame:       %u", p->frame);
+	pw_log_trace_fp("valid:       %08x", p->valid);
 
 	if (p->valid & JackPositionBBT) {
-		pw_log_trace("BBT");
-		pw_log_trace(" bar:              %u", p->bar);
-		pw_log_trace(" beat:             %u", p->beat);
-		pw_log_trace(" tick:             %u", p->tick);
-		pw_log_trace(" bar_start_tick:   %f", p->bar_start_tick);
-		pw_log_trace(" beats_per_bar:    %f", p->beats_per_bar);
-		pw_log_trace(" beat_type:        %f", p->beat_type);
-		pw_log_trace(" ticks_per_beat:   %f", p->ticks_per_beat);
-		pw_log_trace(" beats_per_minute: %f", p->beats_per_minute);
+		pw_log_trace_fp("BBT");
+		pw_log_trace_fp(" bar:              %u", p->bar);
+		pw_log_trace_fp(" beat:             %u", p->beat);
+		pw_log_trace_fp(" tick:             %u", p->tick);
+		pw_log_trace_fp(" bar_start_tick:   %f", p->bar_start_tick);
+		pw_log_trace_fp(" beats_per_bar:    %f", p->beats_per_bar);
+		pw_log_trace_fp(" beat_type:        %f", p->beat_type);
+		pw_log_trace_fp(" ticks_per_beat:   %f", p->ticks_per_beat);
+		pw_log_trace_fp(" beats_per_minute: %f", p->beats_per_minute);
 	}
 	if (p->valid & JackPositionTimecode) {
-		pw_log_trace("Timecode:");
-		pw_log_trace(" frame_time:       %f", p->frame_time);
-		pw_log_trace(" next_time:        %f", p->next_time);
+		pw_log_trace_fp("Timecode:");
+		pw_log_trace_fp(" frame_time:       %f", p->frame_time);
+		pw_log_trace_fp(" next_time:        %f", p->next_time);
 	}
 	if (p->valid & JackBBTFrameOffset) {
-		pw_log_trace("BBTFrameOffset:");
-		pw_log_trace(" bbt_offset:       %u", p->bbt_offset);
+		pw_log_trace_fp("BBTFrameOffset:");
+		pw_log_trace_fp(" bbt_offset:       %u", p->bbt_offset);
 	}
 	if (p->valid & JackAudioVideoRatio) {
-		pw_log_trace("AudioVideoRatio:");
-		pw_log_trace(" audio_frames_per_video_frame: %f", p->audio_frames_per_video_frame);
+		pw_log_trace_fp("AudioVideoRatio:");
+		pw_log_trace_fp(" audio_frames_per_video_frame: %f", p->audio_frames_per_video_frame);
 	}
 	if (p->valid & JackVideoFrameOffset) {
-		pw_log_trace("JackVideoFrameOffset:");
-		pw_log_trace(" video_offset:     %u", p->video_offset);
+		pw_log_trace_fp("JackVideoFrameOffset:");
+		pw_log_trace_fp(" video_offset:     %u", p->video_offset);
 	}
 }
 
@@ -1617,7 +1652,8 @@ static inline void jack_to_position(jack_position_t *s, struct pw_node_activatio
 	}
 }
 
-static inline jack_transport_state_t position_to_jack(struct pw_node_activation *a, jack_position_t *d)
+static inline jack_transport_state_t position_to_jack(struct pw_node_activation *a,
+		jack_position_t *d, struct frame_times *t)
 {
 	struct spa_io_position *s = &a->position;
 	jack_transport_state_t state;
@@ -1643,8 +1679,13 @@ static inline jack_transport_state_t position_to_jack(struct pw_node_activation
 		return state;
 
 	d->unique_1++;
-	d->usecs = s->clock.nsec / SPA_NSEC_PER_USEC;
-	d->frame_rate = s->clock.rate.denom;
+	t->frames = s->clock.position;
+	t->nsec = s->clock.nsec;
+	d->usecs = t->nsec / SPA_NSEC_PER_USEC;
+	t->next_nsec = s->clock.next_nsec;
+	t->rate_diff = s->clock.rate_diff;
+	t->buffer_frames = s->clock.duration;
+	d->frame_rate = t->sample_rate = s->clock.rate.denom;
 
 	if ((int64_t)s->clock.position < s->offset) {
 		d->frame = seg->position;
@@ -1763,7 +1804,7 @@ static inline uint32_t cycle_run(struct client *c)
 		return 0;
 
 	if (SPA_LIKELY(driver)) {
-		c->jack_state = position_to_jack(driver, &c->jack_position);
+		c->jack_state = position_to_jack(driver, &c->jack_position, &c->jack_times);
 
 		if (SPA_UNLIKELY(activation->pending_sync)) {
 			if (c->sync_callback == NULL ||
@@ -2522,6 +2563,17 @@ static int client_node_port_set_param(void *data,
 	return 0;
 }
 
+static void midi_init_buffer(void *data, uint32_t max_frames)
+{
+	struct midi_buffer *mb = data;
+	mb->magic = MIDI_BUFFER_MAGIC;
+	mb->buffer_size = max_frames * sizeof(float);
+	mb->nframes = max_frames;
+	mb->write_pos = 0;
+	mb->event_count = 0;
+	mb->lost_events = 0;
+}
+
 static inline void *init_buffer(struct port *p)
 {
 	struct client *c = p->client;
@@ -2531,12 +2583,7 @@ static inline void *init_buffer(struct port *p)
 
 	if (p->object->port.type_id == TYPE_ID_MIDI) {
 		struct midi_buffer *mb = data;
-		mb->magic = MIDI_BUFFER_MAGIC;
-		mb->buffer_size = c->max_frames * sizeof(float);
-		mb->nframes = c->max_frames;
-		mb->write_pos = 0;
-		mb->event_count = 0;
-		mb->lost_events = 0;
+		midi_init_buffer(data, c->max_frames);
 		pw_log_debug("port %p: init midi buffer size:%d", p, mb->buffer_size);
 	} else
 		memset(data, 0, c->max_frames * sizeof(float));
@@ -2681,7 +2728,7 @@ static int client_node_port_use_buffers(void *data,
 		}
 		SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUT);
 		if (direction == SPA_DIRECTION_OUTPUT)
-			reuse_buffer(c, mix, b->id);
+			queue_buffer(c, mix, b->id);
 
 	}
 	pw_log_debug("%p: have %d buffers", c, n_buffers);
@@ -3031,6 +3078,17 @@ static const char* type_to_string(jack_port_type_id_t type_id)
 		return NULL;
 	}
 }
+static bool type_is_dsp(jack_port_type_id_t type_id)
+{
+	switch(type_id) {
+	case TYPE_ID_AUDIO:
+	case TYPE_ID_MIDI:
+	case TYPE_ID_VIDEO:
+		return true;
+	default:
+		return false;
+	}
+}
 
 static jack_uuid_t client_make_uuid(uint32_t id, bool monitor)
 {
@@ -3218,7 +3276,7 @@ static void node_info(void *data, const struct pw_node_info *info)
 }
 
 static const struct pw_node_events node_events = {
-	PW_VERSION_NODE,
+	PW_VERSION_NODE_EVENTS,
 	.info = node_info,
 };
 
@@ -3243,7 +3301,7 @@ static void port_param(void *data, int seq,
 }
 
 static const struct pw_port_events port_events = {
-	PW_VERSION_PORT,
+	PW_VERSION_PORT_EVENTS,
 	.param = port_param,
 };
 
@@ -3441,8 +3499,9 @@ static void registry_event_global(void *data, uint32_t id,
 				pw_proxy_add_object_listener(o->proxy,
 						&o->object_listener, &port_events, o);
 
-				pw_port_subscribe_params((struct pw_port*)o->proxy,
-						ids, 1);
+				if (type_is_dsp(type_id))
+					pw_port_subscribe_params((struct pw_port*)o->proxy,
+							ids, 1);
 				do_sync = true;
 			}
 			pthread_mutex_lock(&c->context.lock);
@@ -5005,7 +5064,7 @@ jack_port_t * jack_port_register (jack_client_t *client,
 		goto error_free;
 	}
 
-	return (jack_port_t *) o;
+	return object_to_port(o);
 
 error_free:
 	free_port(c, p, true);
@@ -5037,7 +5096,7 @@ SPA_EXPORT
 int jack_port_unregister (jack_client_t *client, jack_port_t *port)
 {
 	struct client *c = (struct client *) client;
-	struct object *o = (struct object *) port;
+	struct object *o = port_to_object(port);
 	struct port *p;
 	int res;
 
@@ -5148,12 +5207,10 @@ static void *get_buffer_input_float(struct port *p, jack_nframes_t frames)
 static void *get_buffer_input_midi(struct port *p, jack_nframes_t frames)
 {
 	struct mix *mix;
-	void *ptr = p->emptyptr;
+	void *ptr = midi_scratch;
 	struct spa_pod_sequence *seq[MAX_MIX];
 	uint32_t n_seq = 0;
 
-	jack_midi_clear_buffer(ptr);
-
 	spa_list_for_each(mix, &p->mix, port_link) {
 		struct spa_data *d;
 		struct buffer *b;
@@ -5179,8 +5236,8 @@ static void *get_buffer_input_midi(struct port *p, jack_nframes_t frames)
 		if (n_seq == MAX_MIX)
 			break;
 	}
+	midi_init_buffer(ptr, MIDI_SCRATCH_FRAMES);
 	convert_to_midi(seq, n_seq, ptr, p->client->fix_midi_events);
-
 	return ptr;
 }
 
@@ -5214,41 +5271,58 @@ static void *get_buffer_input_empty(struct port *p, jack_nframes_t frames)
 SPA_EXPORT
 void * jack_port_get_buffer (jack_port_t *port, jack_nframes_t frames)
 {
-	struct object *o = (struct object *) port;
-	struct port *p;
-	void *ptr;
+	struct object *o = port_to_object(port);
+	struct port *p = NULL;
+	void *ptr = NULL;
 
 	return_val_if_fail(o != NULL, NULL);
 
 	if (o->type != INTERFACE_Port || o->client == NULL)
-		return NULL;
+		goto done;
 
 	if ((p = o->port.port) == NULL) {
 		struct mix *mix;
 		struct buffer *b;
 
 		if ((mix = find_mix_peer(o->client, o->id)) == NULL)
-			return NULL;
+			goto done;
 
 		pw_log_trace("peer mix: %p %d", mix, mix->peer_id);
 
 		if ((b = get_mix_buffer(mix, frames)) == NULL)
-			return NULL;
-
-		return get_buffer_data(b, frames);
+			goto done;
+
+		if (o->port.type_id == TYPE_ID_MIDI) {
+			struct spa_pod_sequence *seq[1];
+			struct spa_data *d;
+			void *pod;
+
+			ptr = midi_scratch;
+			midi_init_buffer(ptr, MIDI_SCRATCH_FRAMES);
+
+			d = &b->datas[0];
+			if ((pod = spa_pod_from_data(d->data, d->maxsize,
+							d->chunk->offset, d->chunk->size)) == NULL)
+				goto done;
+			if (!spa_pod_is_sequence(pod))
+				goto done;
+			seq[0] = pod;
+			convert_to_midi(seq, 1, ptr, o->client->fix_midi_events);
+		} else {
+			ptr = get_buffer_data(b, frames);
+		}
+	} else if (p->valid) {
+		ptr = p->get_buffer(p, frames);
 	}
-	if (!p->valid)
-		return NULL;
-
-	ptr = p->get_buffer(p, frames);
-	pw_log_trace_fp("%p: port %p buffer %p empty:%u", p->client, p, ptr, p->empty_out);
+done:
+	pw_log_trace_fp("%p: port %p buffer %p", o->client, p, ptr);
 	return ptr;
 }
 
 SPA_EXPORT
 jack_uuid_t jack_port_uuid (const jack_port_t *port)
 {
-	struct object *o = (struct object *) port;
+	struct object *o = port_to_object(port);
 	return_val_if_fail(o != NULL, 0);
 	return jack_port_uuid_generate(o->serial);
 }
@@ -5269,47 +5343,57 @@ static const char *port_name(struct object *o)
 SPA_EXPORT
 const char * jack_port_name (const jack_port_t *port)
 {
-	struct object *o = (struct object *) port;
+	struct object *o = port_to_object(port);
 	return_val_if_fail(o != NULL, NULL);
+	if (o->type != INTERFACE_Port)
+		return NULL;
 	return port_name(o);
 }
 
 SPA_EXPORT
 const char * jack_port_short_name (const jack_port_t *port)
 {
-	struct object *o = (struct object *) port;
+	struct object *o = port_to_object(port);
 	return_val_if_fail(o != NULL, NULL);
+	if (o->type != INTERFACE_Port)
+		return NULL;
 	return strchr(port_name(o), ':') + 1;
 }
 
 SPA_EXPORT
 int jack_port_flags (const jack_port_t *port)
 {
-	struct object *o = (struct object *) port;
+	struct object *o = port_to_object(port);
 	return_val_if_fail(o != NULL, 0);
+	if (o->type != INTERFACE_Port)
+		return 0;
 	return o->port.flags;
 }
 
 SPA_EXPORT
 const char * jack_port_type (const jack_port_t *port)
 {
-	struct object *o = (struct object *) port;
+	struct object *o = port_to_object(port);
 	return_val_if_fail(o != NULL, NULL);
+	if (o->type != INTERFACE_Port)
+		return NULL;
 	return type_to_string(o->port.type_id);
 }
 
 SPA_EXPORT
 jack_port_type_id_t jack_port_type_id (const jack_port_t *port)
 {
-	struct object *o = (struct object *) port;
+	struct object *o = port_to_object(port);
 	return_val_if_fail(o != NULL, 0);
+	if (o->type != INTERFACE_Port)
+		return TYPE_ID_OTHER;
 	return o->port.type_id;
 }
 
 SPA_EXPORT
 int jack_port_is_mine (const jack_client_t *client, const jack_port_t *port)
 {
-	struct object *o = (struct object *) port;
+	struct object *o = port_to_object(port);
 	return_val_if_fail(o != NULL, 0);
 	return o->type == INTERFACE_Port &&
 		o->port.port != NULL &&
@@ -5319,7 +5403,7 @@ int jack_port_is_mine (const jack_client_t *client, const jack_port_t *port)
 SPA_EXPORT
 int jack_port_connected (const jack_port_t *port)
 {
-	struct object *o = (struct object *) port;
+	struct object *o = port_to_object(port);
 	struct client *c;
 	struct object *l;
 	int res = 0;
@@ -5349,7 +5433,7 @@ SPA_EXPORT
 int jack_port_connected_to (const jack_port_t *port,
                             const char *port_name)
 {
-	struct object *o = (struct object *) port;
+	struct object *o = port_to_object(port);
 	struct client *c;
 	struct object *p, *l;
 	int res = 0;
@@ -5389,7 +5473,7 @@ int jack_port_connected_to (const jack_port_t *port,
 SPA_EXPORT
 const char ** jack_port_get_connections (const jack_port_t *port)
 {
-	struct object *o = (struct object *) port;
+	struct object *o = port_to_object(port);
 
 	return_val_if_fail(o != NULL, NULL);
 	if (o->type != INTERFACE_Port || o->client == NULL)
@@ -5403,7 +5487,7 @@ const char ** jack_port_get_all_connections (const jack_client_t *client,
                                              const jack_port_t *port)
 {
 	struct client *c = (struct client *) client;
-	struct object *o = (struct object *) port;
+	struct object *o = port_to_object(port);
 	struct object *p, *l;
 	const char **res;
 	int count = 0;
@@ -5446,8 +5530,8 @@ const char ** jack_port_get_all_connections (const jack_client_t *client,
 SPA_EXPORT
 int jack_port_tie (jack_port_t *src, jack_port_t *dst)
 {
-	struct object *s = (struct object *) src;
-	struct object *d = (struct object *) dst;
+	struct object *s = port_to_object(src);
+	struct object *d = port_to_object(dst);
 	struct port *sp, *dp;
 
 	sp = s->port.port;
@@ -5464,7 +5548,7 @@ int jack_port_tie (jack_port_t *src, jack_port_t *dst)
 SPA_EXPORT
 int jack_port_untie (jack_port_t *port)
 {
-	struct object *o = (struct object *) port;
+	struct object *o = port_to_object(port);
 	struct port *p;
 
 	p = o->port.port;
@@ -5485,7 +5569,7 @@ SPA_EXPORT
 int jack_port_rename (jack_client_t* client, jack_port_t *port, const char *port_name)
 {
 	struct client *c = (struct client *) client;
-	struct object *o = (struct object *) port;
+	struct object *o = port_to_object(port);
 	struct port *p;
 	int res = 0;
 
@@ -5527,7 +5611,7 @@ done:
 SPA_EXPORT
 int jack_port_set_alias (jack_port_t *port, const char *alias)
 {
-	struct object *o = (struct object *) port;
+	struct object *o = port_to_object(port);
 	struct client *c;
 	struct port *p;
 	const char *key;
@@ -5583,7 +5667,7 @@ done:
 SPA_EXPORT
 int jack_port_unset_alias (jack_port_t *port, const char *alias)
 {
-	struct object *o = (struct object *) port;
+	struct object *o = port_to_object(port);
 	struct client *c;
 	struct port *p;
 	const char *key;
@@ -5634,7 +5718,7 @@ done:
 SPA_EXPORT
 int jack_port_get_aliases (const jack_port_t *port, char* const aliases[2])
 {
-	struct object *o = (struct object *) port;
+	struct object *o = port_to_object(port);
 	int res = 0;
 
 	return_val_if_fail(o != NULL, -EINVAL);
@@ -5657,7 +5741,7 @@ int jack_port_get_aliases (const jack_port_t *port, char* const aliases[2])
 SPA_EXPORT
 int jack_port_request_monitor (jack_port_t *port, int onoff)
 {
-	struct object *o = (struct object *) port;
+	struct object *o = port_to_object(port);
 
 	return_val_if_fail(o != NULL, -EINVAL);
 
@@ -5688,13 +5772,13 @@ int jack_port_request_monitor_by_name (jack_client_t *client,
 		return -1;
 	}
 
-	return jack_port_request_monitor((jack_port_t*)p, onoff);
+	return jack_port_request_monitor(object_to_port(p), onoff);
 }
 
 SPA_EXPORT
 int jack_port_ensure_monitor (jack_port_t *port, int onoff)
 {
-	struct object *o = (struct object *) port;
+	struct object *o = port_to_object(port);
 
 	return_val_if_fail(o != NULL, -EINVAL);
 
@@ -5711,7 +5795,7 @@ int jack_port_ensure_monitor (jack_port_t *port, int onoff)
 SPA_EXPORT
 int jack_port_monitoring_input (jack_port_t *port)
 {
-	struct object *o = (struct object *) port;
+	struct object *o = port_to_object(port);
 	return_val_if_fail(o != NULL, -EINVAL);
 	return o->port.monitor_requests > 0;
 }
@@ -5889,7 +5973,7 @@ SPA_EXPORT
 int jack_port_disconnect (jack_client_t *client, jack_port_t *port)
 {
 	struct client *c = (struct client *) client;
-	struct object *o = (struct object *) port;
+	struct object *o = port_to_object(port);
 	struct object *l;
 	int res;
 
@@ -5950,7 +6034,7 @@ size_t jack_port_type_get_buffer_size (jack_client_t *client, const char *port_t
 SPA_EXPORT
 void jack_port_set_latency (jack_port_t *port, jack_nframes_t frames)
 {
-	struct object *o = (struct object *) port;
+	struct object *o = port_to_object(port);
 	struct client *c;
 	jack_latency_range_t range = { frames, frames };
 
@@ -5970,17 +6054,20 @@ void jack_port_set_latency (jack_port_t *port, jack_nframes_t frames)
 SPA_EXPORT
 void jack_port_get_latency_range (jack_port_t *port, jack_latency_callback_mode_t mode, jack_latency_range_t *range)
 {
-	struct object *o = (struct object *) port;
+	struct object *o = port_to_object(port);
 	struct client *c;
 	jack_nframes_t nframes, rate;
 	int direction;
 	struct spa_latency_info *info;
 
 	return_if_fail(o != NULL);
-	if (o->type != INTERFACE_Port || o->client == NULL)
-		return;
 	c = o->client;
 
+	if (o->type != INTERFACE_Port || c == NULL) {
+		range->min = range->max = 0;
+		return;
+	}
+
 	if (mode == JackCaptureLatency)
 		direction = SPA_DIRECTION_OUTPUT;
 	else
@@ -6012,7 +6099,7 @@ do_port_check_latency(struct spa_loop *loop,
 SPA_EXPORT
 void jack_port_set_latency_range (jack_port_t *port, jack_latency_callback_mode_t mode, jack_latency_range_t *range)
 {
-	struct object *o = (struct object *) port;
+	struct object *o = port_to_object(port);
 	struct client *c;
 	enum spa_direction direction;
 	struct spa_latency_info latency;
@@ -6065,7 +6152,7 @@ int jack_recompute_total_latencies (jack_client_t *client)
 
 static jack_nframes_t port_get_latency (jack_port_t *port)
 {
-	struct object *o = (struct object *) port;
+	struct object *o = port_to_object(port);
 	jack_latency_range_t range = { 0, 0 };
 
 	return_val_if_fail(o != NULL, 0);
@@ -6265,7 +6352,7 @@ jack_port_t * jack_port_by_name (jack_client_t *client, const char *port_name)
 	if (res == NULL)
 		pw_log_info("%p: port \"%s\" not found", c, port_name);
 
-	return (jack_port_t *)res;
+	return object_to_port(res);
 }
 
 SPA_EXPORT
@@ -6287,23 +6374,35 @@ jack_port_t * jack_port_by_id (jack_client_t *client,
 	if (res == NULL)
 		pw_log_info("%p: port %d not found", c, port_id);
 
-	return (jack_port_t *)res;
+	return object_to_port(res);
+}
+
+static inline void get_frame_times(struct client *c, struct frame_times *times)
+{
+	jack_unique_t u1;
+	uint32_t count = 0;
+	do {
+		u1 = c->jack_position.unique_1;
+		*times = c->jack_times;
+		if (++count == 10) {
+			pw_log_warn("could not get snapshot %lu %lu", u1, c->jack_position.unique_2);
+			break;
+		}
+	} while (u1 != c->jack_position.unique_2);
 }
 
 SPA_EXPORT
 jack_nframes_t jack_frames_since_cycle_start (const jack_client_t *client)
 {
 	struct client *c = (struct client *) client;
-	struct spa_io_position *pos;
+	struct frame_times times;
 	uint64_t diff;
 
 	return_val_if_fail(c != NULL, 0);
 
-	if (SPA_UNLIKELY((pos = c->rt.position) == NULL))
-		return 0;
-
-	diff = get_time_ns() - pos->clock.nsec;
-	return (jack_nframes_t) floor(((double)c->sample_rate * diff) / SPA_NSEC_PER_SEC);
+	get_frame_times(c, &times);
+	diff = get_time_ns() - times.nsec;
+	return (jack_nframes_t) floor(((double)times.sample_rate * diff) / SPA_NSEC_PER_SEC);
 }
 
 SPA_EXPORT
@@ -6316,14 +6415,13 @@ SPA_EXPORT
 jack_nframes_t jack_last_frame_time (const jack_client_t *client)
 {
 	struct client *c = (struct client *) client;
-	struct spa_io_position *pos;
+	struct frame_times times;
 
 	return_val_if_fail(c != NULL, 0);
 
-	if (SPA_UNLIKELY((pos = c->rt.position) == NULL))
-		return 0;
+	get_frame_times(c, &times);
 
-	return pos->clock.position;
+	return times.frames;
 }
 
 SPA_EXPORT
@@ -6334,17 +6432,20 @@ int jack_get_cycle_times(const jack_client_t *client,
                         float          *period_usecs)
 {
 	struct client *c = (struct client *) client;
-	struct spa_io_position *pos;
+	struct frame_times times;
 
 	return_val_if_fail(c != NULL, -EINVAL);
 
-	if (SPA_UNLIKELY((pos = c->rt.position) == NULL))
-		return -EIO;
+	get_frame_times(c, &times);
 
-	*current_frames = pos->clock.position;
-	*current_usecs = pos->clock.nsec / SPA_NSEC_PER_USEC;
-	*period_usecs = pos->clock.duration * (float)SPA_USEC_PER_SEC / (c->sample_rate * pos->clock.rate_diff);
-	*next_usecs = pos->clock.next_nsec / SPA_NSEC_PER_USEC;
+	*current_frames = times.frames;
+	*current_usecs = times.nsec / SPA_NSEC_PER_USEC;
+	*next_usecs = times.next_nsec / SPA_NSEC_PER_USEC;
+	if (times.sample_rate == 0 || times.rate_diff == 0.0)
+		*period_usecs = (times.next_nsec - times.nsec) / SPA_NSEC_PER_USEC;
+	else
+		*period_usecs = times.buffer_frames *
+			(float)SPA_USEC_PER_SEC / (times.sample_rate * times.rate_diff);
 
 	pw_log_trace("%p: %d %"PRIu64" %"PRIu64" %f", c, *current_frames,
 			*current_usecs, *next_usecs, *period_usecs);
@@ -6355,38 +6456,42 @@ SPA_EXPORT
 jack_time_t jack_frames_to_time(const jack_client_t *client, jack_nframes_t frames)
 {
 	struct client *c = (struct client *) client;
-	struct spa_io_position *pos;
+	struct frame_times times;
 
 	return_val_if_fail(c != NULL, -EINVAL);
 
-	if (SPA_UNLIKELY((pos = c->rt.position) == NULL) || c->buffer_frames == 0)
+	get_frame_times(c, &times);
+
+	if (times.buffer_frames == 0)
 		return 0;
 
-	uint32_t nf = (uint32_t)pos->clock.position;
-	uint64_t w = pos->clock.nsec/SPA_NSEC_PER_USEC;
-	uint64_t nw = pos->clock.next_nsec/SPA_NSEC_PER_USEC;
+	uint32_t nf = (uint32_t)times.frames;
+	uint64_t w = times.nsec/SPA_NSEC_PER_USEC;
+	uint64_t nw = times.next_nsec/SPA_NSEC_PER_USEC;
 	int32_t df = frames - nf;
 	int64_t dp = nw - w;
-	return w + (int64_t)rint((double) df * (double) dp / c->buffer_frames);
+	return w + (int64_t)rint((double) df * (double) dp / times.buffer_frames);
 }
 
 SPA_EXPORT
 jack_nframes_t jack_time_to_frames(const jack_client_t *client, jack_time_t usecs)
 {
 	struct client *c = (struct client *) client;
-	struct spa_io_position *pos;
+	struct frame_times times;
 
 	return_val_if_fail(c != NULL, -EINVAL);
 
-	if (SPA_UNLIKELY((pos = c->rt.position) == NULL))
+	get_frame_times(c, &times);
+
+	if (times.buffer_frames == 0)
 		return 0;
 
-	uint32_t nf = (uint32_t)pos->clock.position;
-	uint64_t w = pos->clock.nsec/SPA_NSEC_PER_USEC;
-	uint64_t nw = pos->clock.next_nsec/SPA_NSEC_PER_USEC;
+	uint32_t nf = (uint32_t)times.frames;
+	uint64_t w = times.nsec/SPA_NSEC_PER_USEC;
+	uint64_t nw = times.next_nsec/SPA_NSEC_PER_USEC;
 	int64_t du = usecs - w;
 	int64_t dp = nw - w;
-	return nf + (int32_t)rint((double)du / (double)dp * c->buffer_frames);
+	return nf + (int32_t)rint((double)du / (double)dp * times.buffer_frames);
 }
 
 SPA_EXPORT
@@ -6559,46 +6664,44 @@ jack_transport_state_t jack_transport_query (const jack_client_t *client,
 					     jack_position_t *pos)
 {
 	struct client *c = (struct client *) client;
-	struct pw_node_activation *a;
-	jack_transport_state_t jack_state = JackTransportStopped;
+	jack_transport_state_t state;
+	jack_unique_t u1;
+	uint32_t count = 0;
 
 	return_val_if_fail(c != NULL, JackTransportStopped);
 
-	if (SPA_LIKELY((a = c->rt.driver_activation) != NULL)) {
-		jack_state = position_to_jack(a, pos);
-	} else if ((a = c->driver_activation) != NULL) {
-		jack_state = position_to_jack(a, pos);
-	} else if (pos != NULL) {
-		memset(pos, 0, sizeof(jack_position_t));
-		pos->frame_rate = jack_get_sample_rate((jack_client_t*)client);
-	}
-	return jack_state;
+	do {
+		u1 = c->jack_position.unique_1;
+		state = c->jack_state;
+		if (pos != NULL)
+			*pos = c->jack_position;
+		if (++count == 10) {
+			pw_log_warn("could not get snapshot %lu %lu", u1, c->jack_position.unique_2);
+			break;
+		}
+	} while (u1 != c->jack_position.unique_2);
+
+	return state;
 }
 
 SPA_EXPORT
 jack_nframes_t jack_get_current_transport_frame (const jack_client_t *client)
 {
 	struct client *c = (struct client *) client;
-	struct pw_node_activation *a;
-	struct spa_io_position *pos;
-	struct spa_io_segment *seg;
-	uint64_t running;
+	jack_transport_state_t state;
+	jack_nframes_t res;
+	jack_position_t pos;
 
 	return_val_if_fail(c != NULL, -EINVAL);
 
-	if (SPA_UNLIKELY((a = c->rt.driver_activation) == NULL))
-		return -EIO;
+	state = jack_transport_query(client, &pos);
+	res = pos.frame;
 
-	pos = &a->position;
-	running = pos->clock.position - pos->offset;
-
-	if (pos->state == SPA_IO_POSITION_STATE_RUNNING) {
-		uint64_t nsecs = get_time_ns() - pos->clock.nsec;
-		running += (uint64_t)floor((((double) c->sample_rate) / SPA_NSEC_PER_SEC) * nsecs);
+	if (state == JackTransportRolling) {
+		float usecs = get_time_ns()/1000 - pos.usecs;
+		res += (jack_nframes_t)floor((((float) pos.frame_rate) / 1000000.0f) * usecs);
 	}
-	seg = &pos->segments[0];
-
-	return (running - seg->start) * seg->rate + seg->position;
+	return res;
 }
 
 SPA_EXPORT
diff --git a/spa/include/spa/buffer/alloc.h b/spa/include/spa/buffer/alloc.h
index 7329c9447e69aa353a895fd58c5e1c07261d41a9..8b9e55e50dc66777e74a887df20978494a79b6d1 100644
--- a/spa/include/spa/buffer/alloc.h
+++ b/spa/include/spa/buffer/alloc.h
@@ -300,7 +300,8 @@ spa_buffer_alloc_array(uint32_t n_buffers, uint32_t flags,
 {
 
 	struct spa_buffer **buffers;
-	struct spa_buffer_alloc_info info = { flags | SPA_BUFFER_ALLOC_FLAG_INLINE_ALL, };
+	struct spa_buffer_alloc_info info = { flags | SPA_BUFFER_ALLOC_FLAG_INLINE_ALL,
+                                        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
 	void *skel;
 
 	spa_buffer_alloc_fill_info(&info, n_metas, metas, n_datas, datas, data_aligns);
diff --git a/spa/include/spa/debug/log.h b/spa/include/spa/debug/log.h
index 43b3bd52b459a7e732b06e618c99dca31b380e86..aa16c5e9e8eab6ed58e3bc8fce3a0ecc47c38ea8 100644
--- a/spa/include/spa/debug/log.h
+++ b/spa/include/spa/debug/log.h
@@ -38,7 +38,7 @@ struct spa_debug_log_ctx {
 SPA_PRINTF_FUNC(2,3)
 static inline void spa_debug_log_log(struct spa_debug_context *ctx, const char *fmt, ...)
 {
-	struct spa_debug_log_ctx *c = (struct spa_debug_log_ctx*)ctx;
+	struct spa_debug_log_ctx *c = SPA_CONTAINER_OF(ctx, struct spa_debug_log_ctx, ctx);
 	va_list args;
 	va_start(args, fmt);
 	spa_log_logtv(c->log, c->level, c->topic, c->file, c->line, c->func, fmt, args);
diff --git a/spa/include/spa/graph/graph.h b/spa/include/spa/graph/graph.h
index bfcb9b3d144f7a9dbd2a31a394c7831751ec8d48..39049183a931bcb05d2916105bac06c75b60b382 100644
--- a/spa/include/spa/graph/graph.h
+++ b/spa/include/spa/graph/graph.h
@@ -215,7 +215,7 @@ spa_graph_node_init(struct spa_graph_node *node, struct spa_graph_state *state)
 }
 
 
-static inline int spa_graph_node_impl_sub_process(void *data, struct spa_graph_node *node)
+static inline int spa_graph_node_impl_sub_process(void *data SPA_UNUSED, struct spa_graph_node *node)
 {
 	struct spa_graph *graph = node->subgraph;
 	spa_debug("node %p: sub process %p", node, graph);
@@ -322,7 +322,7 @@ static inline int spa_graph_node_impl_process(void *data, struct spa_graph_node
         return state->status;
 }
 
-static inline int spa_graph_node_impl_reuse_buffer(void *data, struct spa_graph_node *node,
+static inline int spa_graph_node_impl_reuse_buffer(void *data, struct spa_graph_node *node SPA_UNUSED,
 		uint32_t port_id, uint32_t buffer_id)
 {
 	struct spa_node *n = (struct spa_node *)data;
diff --git a/spa/include/spa/monitor/utils.h b/spa/include/spa/monitor/utils.h
index fcea4ac6a6ed0512a3a2fb3e5b32568f02bae332..93f8f41b05c5f1294bb4e3756f4044bec44915ce 100644
--- a/spa/include/spa/monitor/utils.h
+++ b/spa/include/spa/monitor/utils.h
@@ -22,8 +22,8 @@ struct spa_result_device_params_data {
 	struct spa_result_device_params data;
 };
 
-static inline void spa_result_func_device_params(void *data, int seq, int res,
-		uint32_t type, const void *result)
+static inline void spa_result_func_device_params(void *data, int seq SPA_UNUSED, int res SPA_UNUSED,
+		uint32_t type SPA_UNUSED, const void *result)
 {
 	struct spa_result_device_params_data *d =
 		(struct spa_result_device_params_data *)data;
@@ -42,8 +42,8 @@ static inline int spa_device_enum_params_sync(struct spa_device *device,
 			struct spa_pod **param,
 			struct spa_pod_builder *builder)
 {
-	struct spa_result_device_params_data data = { builder, };
-	struct spa_hook listener = {{0}};
+	struct spa_result_device_params_data data = { builder, {0}};
+	struct spa_hook listener = {{0}, {0}, 0, 0};
 	static const struct spa_device_events device_events = {
 		.version = SPA_VERSION_DEVICE_EVENTS,
 		.info = NULL,
diff --git a/spa/include/spa/node/utils.h b/spa/include/spa/node/utils.h
index a88ef8959e3fe7361e85c214b91eddd6cbb575f2..01d249ab9e483c273e56157c0c312a676718af7c 100644
--- a/spa/include/spa/node/utils.h
+++ b/spa/include/spa/node/utils.h
@@ -24,7 +24,7 @@ struct spa_result_node_params_data {
 };
 
 static inline void spa_result_func_node_params(void *data,
-		int seq, int res, uint32_t type, const void *result)
+		int seq SPA_UNUSED, int res SPA_UNUSED, uint32_t type SPA_UNUSED, const void *result)
 {
 	struct spa_result_node_params_data *d =
 		(struct spa_result_node_params_data *) data;
@@ -43,8 +43,8 @@ static inline int spa_node_enum_params_sync(struct spa_node *node,
 			struct spa_pod **param,
 			struct spa_pod_builder *builder)
 {
-	struct spa_result_node_params_data data = { builder, };
-	struct spa_hook listener = {{0}};
+	struct spa_result_node_params_data data = { builder, {0}};
+	struct spa_hook listener = {{0}, {0}, 0, 0};
 	static const struct spa_node_events node_events = {
 		.version = SPA_VERSION_NODE_EVENTS,
 		.info = NULL,
@@ -77,8 +77,8 @@ static inline int spa_node_port_enum_params_sync(struct spa_node *node,
 			struct spa_pod **param,
 			struct spa_pod_builder *builder)
 {
-	struct spa_result_node_params_data data = { builder, };
-	struct spa_hook listener = {{0}};
+	struct spa_result_node_params_data data = { builder, {0}};
+	struct spa_hook listener = {{0}, {0}, 0, 0};
 	static const struct spa_node_events node_events = {
 		.version = SPA_VERSION_NODE_EVENTS,
 		.info = NULL,
diff --git a/spa/include/spa/param/props.h b/spa/include/spa/param/props.h
index 8665013feea8c6f27fb0a1da3eea0294aeb65a46..a7a2e4c2531d6d04ac6a2e20e38a473fd2de26a4 100644
--- a/spa/include/spa/param/props.h
+++ b/spa/include/spa/param/props.h
@@ -64,24 +64,26 @@ enum spa_prop {
 	SPA_PROP_patternType,
 	SPA_PROP_ditherType,
 	SPA_PROP_truncate,
-	SPA_PROP_channelVolumes,		/**< a volume array, one volume per channel
+	SPA_PROP_channelVolumes,		/**< a volume array, one (linear) volume per channel
 						  * (Array of Float). 0.0 is silence, 1.0 is
-						  *  without attenuation. This is the effective volume
-						  *  that is applied. It can result in a hardware volume
-						  *  and software volume (see softVolumes) */
+						  *  without attenuation. This is the effective
+						  *  volume that is applied. It can result
+						  *  in a hardware volume and software volume
+						  *  (see softVolumes) */
 	SPA_PROP_volumeBase,			/**< a volume base (Float) */
 	SPA_PROP_volumeStep,			/**< a volume step (Float) */
 	SPA_PROP_channelMap,			/**< a channelmap array
 						  * (Array (Id enum spa_audio_channel)) */
 	SPA_PROP_monitorMute,			/**< mute (Bool) */
-	SPA_PROP_monitorVolumes,		/**< a volume array, one volume per
+	SPA_PROP_monitorVolumes,		/**< a volume array, one (linear) volume per
 						  *  channel (Array of Float) */
 	SPA_PROP_latencyOffsetNsec,		/**< delay adjustment */
 	SPA_PROP_softMute,			/**< mute (Bool) applied in software */
-	SPA_PROP_softVolumes,			/**< a volume array, one volume per channel
+	SPA_PROP_softVolumes,			/**< a volume array, one (linear) volume per channel
 						  * (Array of Float). 0.0 is silence, 1.0 is without
-						  * attenuation. This is the volume applied in software,
-						  * there might be a part applied in hardware. */
+						  * attenuation. This is the volume applied in
+						  * software, there might be a part applied in
+						  * hardware. */
 
 	SPA_PROP_iec958Codecs,			/**< enabled IEC958 (S/PDIF) codecs,
 						  *  (Array (Id enum spa_audio_iec958_codec) */
diff --git a/spa/include/spa/pod/filter.h b/spa/include/spa/pod/filter.h
index 40e1710937e43ffda21fe66eedab2892159b1e2b..3a682e1acc23b66fe1d8b0a0a08a7d2cebd279cb 100644
--- a/spa/include/spa/pod/filter.h
+++ b/spa/include/spa/pod/filter.h
@@ -78,7 +78,7 @@ static inline int spa_pod_choice_fix_default(struct spa_pod_choice *choice)
 }
 
 static inline int spa_pod_filter_flags_value(struct spa_pod_builder *b,
-		uint32_t type, const void *r1, const void *r2, uint32_t size)
+		uint32_t type, const void *r1, const void *r2, uint32_t size SPA_UNUSED)
 {
 	switch (type) {
 	case SPA_TYPE_Int:
@@ -104,7 +104,7 @@ static inline int spa_pod_filter_flags_value(struct spa_pod_builder *b,
 }
 
 static inline int spa_pod_filter_is_step_of(uint32_t type, const void *r1,
-		const void *r2, uint32_t size)
+		const void *r2, uint32_t size SPA_UNUSED)
 {
 	switch (type) {
 	case SPA_TYPE_Int:
diff --git a/spa/include/spa/support/log-impl.h b/spa/include/spa/support/log-impl.h
index 0af62f64316826dd3e6d13edd6412985d28b68ed..214417fa54599ca6de5b333943d0c336116d27bf 100644
--- a/spa/include/spa/support/log-impl.h
+++ b/spa/include/spa/support/log-impl.h
@@ -19,7 +19,7 @@ extern "C" {
  * \{
  */
 
-static inline SPA_PRINTF_FUNC(7, 0) void spa_log_impl_logtv(void *object,
+static inline SPA_PRINTF_FUNC(7, 0) void spa_log_impl_logtv(void *object SPA_UNUSED,
 				     enum spa_log_level level,
 				     const struct spa_log_topic *topic,
 				     const char *file,
@@ -88,7 +88,7 @@ static inline SPA_PRINTF_FUNC(6,7) void spa_log_impl_log(void *object,
 	va_end(args);
 }
 
-static inline void spa_log_impl_topic_init(void *object, struct spa_log_topic *topic)
+static inline void spa_log_impl_topic_init(void *object SPA_UNUSED, struct spa_log_topic *topic SPA_UNUSED)
 {
 	/* noop */
 }
diff --git a/spa/include/spa/utils/json.h b/spa/include/spa/utils/json.h
index 376d4c875560eb668c905997721bb3742aa6f322..c97e253710a4a16c52e7b4795c14aec58ffd2cc1 100644
--- a/spa/include/spa/utils/json.h
+++ b/spa/include/spa/utils/json.h
@@ -38,13 +38,13 @@ struct spa_json {
 	uint32_t depth;
 };
 
-#define SPA_JSON_INIT(data,size) ((struct spa_json) { (data), (data)+(size), })
+#define SPA_JSON_INIT(data,size) ((struct spa_json) { (data), (data)+(size), 0, 0, 0 })
 
 static inline void spa_json_init(struct spa_json * iter, const char *data, size_t size)
 {
 	*iter =  SPA_JSON_INIT(data, size);
 }
-#define SPA_JSON_ENTER(iter) ((struct spa_json) { (iter)->cur, (iter)->end, (iter), })
+#define SPA_JSON_ENTER(iter) ((struct spa_json) { (iter)->cur, (iter)->end, (iter), 0, 0 })
 
 static inline void spa_json_enter(struct spa_json * iter, struct spa_json * sub)
 {
@@ -181,7 +181,7 @@ static inline int spa_json_is_container(const char *val, int len)
 	return len > 0 && (*val == '{'  || *val == '[');
 }
 
-static inline int spa_json_container_len(struct spa_json *iter, const char *value, int len)
+static inline int spa_json_container_len(struct spa_json *iter, const char *value, int len SPA_UNUSED)
 {
 	const char *val;
 	struct spa_json sub;
diff --git a/spa/include/spa/utils/keys.h b/spa/include/spa/utils/keys.h
index 5ef4e49da7c29d9d96b473a9cc67f65f64583c13..2f007ade38692b2ffae2f72a9f14f54944b3b4e2 100644
--- a/spa/include/spa/utils/keys.h
+++ b/spa/include/spa/utils/keys.h
@@ -44,6 +44,7 @@ extern "C" {
 #define SPA_KEY_API_ALSA_OPEN_UCM	"api.alsa.open.ucm"		/**< if UCM should be opened card */
 #define SPA_KEY_API_ALSA_DISABLE_LONGNAME	\
 					"api.alsa.disable-longname"	/**< if card long name should not be passed to MIDI port */
+#define SPA_KEY_API_ALSA_BIND_CTLS	"api.alsa.bind-ctls"		/**< alsa controls to bind as params */
 
 /** info from alsa card_info */
 #define SPA_KEY_API_ALSA_CARD_ID	"api.alsa.card.id"		/**< id from card_info */
diff --git a/spa/plugins/alsa/acp/alsa-mixer.c b/spa/plugins/alsa/acp/alsa-mixer.c
index 8138c6c6e71f38b22173018f5fca3fa6f74bce47..34680b5c034b713d2dc0c84e3e5f206c74ba79b3 100644
--- a/spa/plugins/alsa/acp/alsa-mixer.c
+++ b/spa/plugins/alsa/acp/alsa-mixer.c
@@ -2795,20 +2795,6 @@ static int path_verify(pa_alsa_path *p) {
     return 0;
 }
 
-static const char *get_default_paths_dir(void) {
-    const char *str;
-#ifdef HAVE_RUNNING_FROM_BUILD_TREE
-    if (pa_run_from_build_tree())
-        return PA_SRCDIR "mixer/paths";
-    else
-#endif
-    if (getenv("ACP_BUILDDIR") != NULL)
-        return "mixer/paths";
-    if ((str = getenv("ACP_PATHS_DIR")) != NULL)
-        return str;
-    return PA_ALSA_PATHS_DIR;
-}
-
 pa_alsa_path* pa_alsa_path_new(const char *paths_dir, const char *fname, pa_alsa_direction_t direction) {
     pa_alsa_path *p;
     char *fn;
@@ -2873,10 +2859,9 @@ pa_alsa_path* pa_alsa_path_new(const char *paths_dir, const char *fname, pa_alsa
     items[2].data = &p->description;
     items[3].data = &mute_during_activation;
 
-    if (!paths_dir)
-        paths_dir = get_default_paths_dir();
+    fn = get_data_path(paths_dir, "paths", fname);
 
-    fn = pa_maybe_prefix_path(fname, paths_dir);
+    pa_log_info("Loading path config: %s", fn);
 
     r = pa_config_parse(fn, NULL, items, p->proplist, false, p);
     pa_xfree(fn);
@@ -4827,20 +4812,6 @@ void pa_alsa_decibel_fix_dump(pa_alsa_decibel_fix *db_fix) {
     pa_xfree(db_values);
 }
 
-static const char *get_default_profile_dir(void) {
-    const char *str;
-#ifdef HAVE_RUNNING_FROM_BUILD_TREE
-    if (pa_run_from_build_tree())
-        return PA_SRCDIR "mixer/profile-sets";
-    else
-#endif
-    if (getenv("ACP_BUILDDIR") != NULL)
-        return "mixer/profile-sets";
-    if ((str = getenv("ACP_PROFILES_DIR")) != NULL)
-        return str;
-    return PA_ALSA_PROFILE_SETS_DIR;
-}
-
 pa_alsa_profile_set* pa_alsa_profile_set_new(const char *fname, const pa_channel_map *bonus) {
     pa_alsa_profile_set *ps;
     pa_alsa_profile *p;
@@ -4890,13 +4861,14 @@ pa_alsa_profile_set* pa_alsa_profile_set_new(const char *fname, const pa_channel
 
     items[0].data = &ps->auto_profiles;
 
-    fn = pa_maybe_prefix_path(fname ? fname : "default.conf",
-		    get_default_profile_dir());
+    fn = get_data_path(NULL, "profile-sets", fname ? fname : "default.conf");
+
+    pa_log_info("Loading profile set: %s", fn);
+
     if ((r = access(fn, R_OK)) != 0) {
         if (fname != NULL) {
             pa_log_warn("profile-set '%s' can't be accessed: %m", fn);
-            fn = pa_maybe_prefix_path("default.conf",
-			    get_default_profile_dir());
+            fn = get_data_path(NULL, "profile-sets", "default.conf");
             r = access(fn, R_OK);
 	}
 	if (r != 0) {
diff --git a/spa/plugins/alsa/acp/compat.c b/spa/plugins/alsa/acp/compat.c
index 770344403fa0a814af06e2f0a2bc8ebfe8706750..e2f317b09169edca886b83ac257fc7dd84da834e 100644
--- a/spa/plugins/alsa/acp/compat.c
+++ b/spa/plugins/alsa/acp/compat.c
@@ -18,9 +18,13 @@
   along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
 ***/
 
+#include <spa/utils/string.h>
+#include <spa/utils/cleanup.h>
+
 #include "compat.h"
 #include "device-port.h"
 #include "alsa-mixer.h"
+#include "config.h"
 
 static const char *port_types[] = {
 	[PA_DEVICE_PORT_TYPE_UNKNOWN] = "unknown",
@@ -208,3 +212,76 @@ bool pa_alsa_device_init_description(pa_proplist *p, pa_card *card) {
 
     return true;
 }
+
+static char *try_path(const char *fname, const char *path)
+{
+    char *result = pa_maybe_prefix_path(fname, path);
+
+    pa_log_trace("Check for file: %s", result);
+
+    if (access(result, R_OK) == 0)
+	return result;
+
+    pa_xfree(result);
+    return NULL;
+}
+
+static char *get_xdg_home(const char *key, const char *fallback)
+{
+    const char *e;
+
+    e = getenv(key);
+    if (e && *e) {
+	return strdup(e);
+    } else {
+	e = getenv("HOME");
+	if (!(e && *e))
+	    e = getenv("USERPROFILE");
+	if (e && *e)
+	    return spa_aprintf("%s/%s", e, fallback);
+    }
+    return NULL;
+}
+
+char *get_data_path(const char *data_dir, const char *data_type, const char *fname)
+{
+    static const char * const subpaths[] = {
+	"alsa-card-profile/mixer",
+	"alsa-card-profile",
+    };
+    const char *e;
+    spa_autofree char *base = NULL;
+    char *result;
+
+    if (data_dir)
+	if ((result = try_path(fname, data_dir)) != NULL)
+	    return result;
+
+    e = getenv("ACP_PATHS_DIR");
+    if (e && *e && spa_streq(data_type, "paths"))
+	if ((result = try_path(fname, e)) != NULL)
+	    return result;
+
+    e = getenv("ACP_PROFILES_DIR");
+    if (e && *e && spa_streq(data_type, "profile-sets"))
+	if ((result = try_path(fname, e)) != NULL)
+	    return result;
+
+    base = get_xdg_home("XDG_CONFIG_HOME", ".config");
+    if (base) {
+	SPA_FOR_EACH_ELEMENT_VAR(subpaths, subpath) {
+	    spa_autofree char *path = spa_aprintf("%s/%s/%s", base, *subpath, data_type);
+	    if ((result = try_path(fname, path)) != NULL)
+		return result;
+	}
+    }
+
+    SPA_FOR_EACH_ELEMENT_VAR(subpaths, subpath) {
+	spa_autofree char *path = spa_aprintf("/etc/%s/%s", *subpath, data_type);
+	if ((result = try_path(fname, path)) != NULL)
+	    return result;
+    }
+
+    spa_autofree char *path = spa_aprintf("%s/%s", PA_ALSA_DATA_DIR, data_type);
+    return pa_maybe_prefix_path(fname, path);
+}
diff --git a/spa/plugins/alsa/acp/compat.h b/spa/plugins/alsa/acp/compat.h
index d60f9ef90baf1b00653abc8deaa43e171fac7b26..3e704ddf67b9773f94781e5da28e3f4da4305a45 100644
--- a/spa/plugins/alsa/acp/compat.h
+++ b/spa/plugins/alsa/acp/compat.h
@@ -214,6 +214,7 @@ typedef enum pa_log_level {
 	PA_LOG_NOTICE = 2,    /* Notice messages */
 	PA_LOG_INFO   = 3,    /* Info messages */
 	PA_LOG_DEBUG  = 4,    /* Debug messages */
+	PA_LOG_TRACE = 5,
 	PA_LOG_LEVEL_MAX
 } pa_log_level_t;
 
@@ -245,6 +246,7 @@ static inline PA_PRINTF_FUNC(5, 6) void pa_log_level_meta(enum pa_log_level leve
 #define pa_log_notice(fmt,...)	pa_logl(PA_LOG_NOTICE, fmt, ##__VA_ARGS__)
 #define pa_log_info(fmt,...)	pa_logl(PA_LOG_INFO, fmt, ##__VA_ARGS__)
 #define pa_log_debug(fmt,...)	pa_logl(PA_LOG_DEBUG, fmt, ##__VA_ARGS__)
+#define pa_log_trace(fmt,...)	pa_logl(PA_LOG_TRACE, fmt, ##__VA_ARGS__)
 #define pa_log			pa_log_error
 
 #define pa_assert_se(expr)                                              \
@@ -677,6 +679,8 @@ static inline char *pa_readlink(const char *p) {
 #endif
 }
 
+char *get_data_path(const char *data_dir, const char *data_type, const char *fname);
+
 #include <spa/support/i18n.h>
 
 extern struct spa_i18n *acp_i18n;
diff --git a/spa/plugins/alsa/alsa-compress-offload-device.c b/spa/plugins/alsa/alsa-compress-offload-device.c
index 38695f60be2182ee552d51ff63d0d792999d1895..31f9518937fccce89778491507d8b7743849c060 100644
--- a/spa/plugins/alsa/alsa-compress-offload-device.c
+++ b/spa/plugins/alsa/alsa-compress-offload-device.c
@@ -86,7 +86,7 @@ static void emit_node(struct impl *this, const char *device_node, unsigned int d
 		 * hardware that can capture audio is difficult to do. The only hardware
 		 * known is the Wolfson ADSP; the only driver in the kernel that exposes
 		 * Compress-Offload capture devices is the one for that hardware. */
-		assert(false);
+		spa_assert_not_reached();
 	}
 
 	info.change_mask = SPA_DEVICE_OBJECT_CHANGE_MASK_PROPS;
diff --git a/spa/plugins/alsa/alsa-pcm-sink.c b/spa/plugins/alsa/alsa-pcm-sink.c
index a0731f7c305ef4d6e4b6685676482266482dc7f6..aa8105ab1a4164a2798ccbcedd20ae534757cf1b 100644
--- a/spa/plugins/alsa/alsa-pcm-sink.c
+++ b/spa/plugins/alsa/alsa-pcm-sink.c
@@ -8,8 +8,6 @@
 
 #include <spa/node/node.h>
 #include <spa/node/utils.h>
-#include <spa/node/keys.h>
-#include <spa/monitor/device.h>
 #include <spa/utils/keys.h>
 #include <spa/utils/names.h>
 #include <spa/utils/string.h>
@@ -31,75 +29,6 @@ static void reset_props(struct props *props)
 	props->use_chmap = DEFAULT_USE_CHMAP;
 }
 
-static void emit_node_info(struct state *this, bool full)
-{
-	uint64_t old = full ? this->info.change_mask : 0;
-
-	if (full)
-		this->info.change_mask = this->info_all;
-	if (this->info.change_mask) {
-		struct spa_dict_item items[7];
-		uint32_t i, n_items = 0;
-		char latency[64], period[64], nperiods[64], headroom[64];
-
-		items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_API, "alsa");
-		items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_MEDIA_CLASS, "Audio/Sink");
-		items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_NODE_DRIVER, "true");
-		if (this->have_format) {
-			snprintf(latency, sizeof(latency), "%lu/%d", this->buffer_frames / 2, this->rate);
-			items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_NODE_MAX_LATENCY, latency);
-			snprintf(period, sizeof(period), "%lu", this->period_frames);
-			items[n_items++] = SPA_DICT_ITEM_INIT("api.alsa.period-size", period);
-			snprintf(nperiods, sizeof(nperiods), "%lu",
-					this->period_frames != 0 ? this->buffer_frames / this->period_frames : 0);
-			items[n_items++] = SPA_DICT_ITEM_INIT("api.alsa.period-num", nperiods);
-			snprintf(headroom, sizeof(headroom), "%u", this->headroom);
-			items[n_items++] = SPA_DICT_ITEM_INIT("api.alsa.headroom", headroom);
-		} else {
-			items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_NODE_MAX_LATENCY, NULL);
-			items[n_items++] = SPA_DICT_ITEM_INIT("api.alsa.period-size", NULL);
-			items[n_items++] = SPA_DICT_ITEM_INIT("api.alsa.period-num", NULL);
-			items[n_items++] = SPA_DICT_ITEM_INIT("api.alsa.headroom", NULL);
-		}
-		this->info.props = &SPA_DICT_INIT(items, n_items);
-
-		if (this->info.change_mask & SPA_NODE_CHANGE_MASK_PARAMS) {
-			for (i = 0; i < this->info.n_params; i++) {
-				if (this->params[i].user > 0) {
-					this->params[i].flags ^= SPA_PARAM_INFO_SERIAL;
-					this->params[i].user = 0;
-				}
-			}
-		}
-		spa_node_emit_info(&this->hooks, &this->info);
-
-		this->info.change_mask = old;
-	}
-}
-
-static void emit_port_info(struct state *this, bool full)
-{
-	uint64_t old = full ? this->port_info.change_mask : 0;
-
-	if (full)
-		this->port_info.change_mask = this->port_info_all;
-	if (this->port_info.change_mask) {
-		uint32_t i;
-
-		if (this->port_info.change_mask & SPA_PORT_CHANGE_MASK_PARAMS) {
-			for (i = 0; i < this->port_info.n_params; i++) {
-				if (this->port_params[i].user > 0) {
-					this->port_params[i].flags ^= SPA_PARAM_INFO_SERIAL;
-					this->port_params[i].user = 0;
-				}
-			}
-		}
-		spa_node_emit_port_info(&this->hooks,
-				SPA_DIRECTION_INPUT, 0, &this->port_info);
-		this->port_info.change_mask = old;
-	}
-}
-
 static int impl_node_enum_params(void *object, int seq,
 				 uint32_t id, uint32_t start, uint32_t num,
 				 const struct spa_pod *filter)
@@ -348,8 +277,8 @@ static int impl_node_set_param(void *object, uint32_t id, uint32_t flags,
 			info.ns = lat_ns;
 			handle_process_latency(this, &info);
 		}
-		emit_node_info(this, false);
-		emit_port_info(this, false);
+		spa_alsa_emit_node_info(this, false);
+		spa_alsa_emit_port_info(this, false);
 		break;
 	}
 	case SPA_PARAM_ProcessLatency:
@@ -362,8 +291,8 @@ static int impl_node_set_param(void *object, uint32_t id, uint32_t flags,
 
 		handle_process_latency(this, &info);
 
-		emit_node_info(this, false);
-		emit_port_info(this, false);
+		spa_alsa_emit_node_info(this, false);
+		spa_alsa_emit_port_info(this, false);
 		break;
 	}
 	default:
@@ -425,8 +354,8 @@ impl_node_add_listener(void *object,
 
 	spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
 
-	emit_node_info(this, true);
-	emit_port_info(this, true);
+	spa_alsa_emit_node_info(this, true);
+	spa_alsa_emit_port_info(this, true);
 
 	spa_hook_list_join(&this->hooks, &save);
 
@@ -673,7 +602,7 @@ static int port_set_format(void *object,
 	}
 
 	this->info.change_mask |= SPA_NODE_CHANGE_MASK_PROPS;
-	emit_node_info(this, false);
+	spa_alsa_emit_node_info(this, false);
 
 	this->port_info.change_mask |= SPA_PORT_CHANGE_MASK_RATE;
 	this->port_info.rate = SPA_FRACTION(1, this->rate);
@@ -686,7 +615,7 @@ static int port_set_format(void *object,
 		this->port_params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
 		this->port_params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
 	}
-	emit_port_info(this, false);
+	spa_alsa_emit_port_info(this, false);
 
 	return err;
 }
@@ -722,7 +651,7 @@ impl_node_port_set_param(void *object,
 		this->latency[info.direction] = info;
 		this->port_info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
 		this->port_params[PORT_Latency].user++;
-		emit_port_info(this, false);
+		spa_alsa_emit_port_info(this, false);
 		break;
 	}
 	case SPA_PARAM_Tag:
@@ -741,7 +670,7 @@ impl_node_port_set_param(void *object,
 
 			this->port_info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
 			this->port_params[PORT_Tag].user++;
-			emit_port_info(this, false);
+			spa_alsa_emit_port_info(this, false);
 		}
 		break;
 	}
@@ -944,7 +873,12 @@ impl_init(const struct spa_handle_factory *factory,
 
 	this->data_system = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataSystem);
 	this->data_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataLoop);
+	this->main_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Loop);
 
+	if (this->main_loop == NULL) {
+		spa_log_error(this->log, "a main loop is needed");
+		return -EINVAL;
+	}
 	if (this->data_loop == NULL) {
 		spa_log_error(this->log, "a data loop is needed");
 		return -EINVAL;
diff --git a/spa/plugins/alsa/alsa-pcm-source.c b/spa/plugins/alsa/alsa-pcm-source.c
index 4e783aa924bd576403bda759fe3dd54e3a4e98ac..0a8ffec493329b0820dc42fbcf98e367d42bbeb0 100644
--- a/spa/plugins/alsa/alsa-pcm-source.c
+++ b/spa/plugins/alsa/alsa-pcm-source.c
@@ -8,12 +8,10 @@
 
 #include <spa/node/node.h>
 #include <spa/node/utils.h>
-#include <spa/node/keys.h>
 #include <spa/utils/keys.h>
 #include <spa/utils/names.h>
 #include <spa/utils/list.h>
 #include <spa/utils/string.h>
-#include <spa/monitor/device.h>
 #include <spa/param/audio/format.h>
 #include <spa/pod/filter.h>
 #include <spa/pod/dynamic.h>
@@ -32,73 +30,6 @@ static void reset_props(struct props *props)
 	props->use_chmap = DEFAULT_USE_CHMAP;
 }
 
-static void emit_node_info(struct state *this, bool full)
-{
-	uint64_t old = full ? this->info.change_mask : 0;
-	if (full)
-		this->info.change_mask = this->info_all;
-	if (this->info.change_mask) {
-		struct spa_dict_item items[7];
-		uint32_t i, n_items = 0;
-		char latency[64], period[64], nperiods[64], headroom[64];
-
-		items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_API, "alsa");
-		items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_MEDIA_CLASS, "Audio/Source");
-		items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_NODE_DRIVER, "true");
-		if (this->have_format) {
-			snprintf(latency, sizeof(latency), "%lu/%d", this->buffer_frames / 2, this->rate);
-			items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_NODE_MAX_LATENCY, latency);
-			snprintf(period, sizeof(period), "%lu", this->period_frames);
-			items[n_items++] = SPA_DICT_ITEM_INIT("api.alsa.period-size", period);
-			snprintf(nperiods, sizeof(nperiods), "%lu",
-					this->period_frames != 0 ? this->buffer_frames / this->period_frames : 0);
-			items[n_items++] = SPA_DICT_ITEM_INIT("api.alsa.period-num", nperiods);
-			snprintf(headroom, sizeof(headroom), "%u", this->headroom);
-			items[n_items++] = SPA_DICT_ITEM_INIT("api.alsa.headroom", headroom);
-		} else {
-			items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_NODE_MAX_LATENCY, NULL);
-			items[n_items++] = SPA_DICT_ITEM_INIT("api.alsa.period-size", NULL);
-			items[n_items++] = SPA_DICT_ITEM_INIT("api.alsa.period-num", NULL);
-			items[n_items++] = SPA_DICT_ITEM_INIT("api.alsa.headroom", NULL);
-		}
-		this->info.props = &SPA_DICT_INIT(items, n_items);
-
-		if (this->info.change_mask & SPA_NODE_CHANGE_MASK_PARAMS) {
-			for (i = 0; i < this->info.n_params; i++) {
-				if (this->params[i].user > 0) {
-					this->params[i].flags ^= SPA_PARAM_INFO_SERIAL;
-					this->params[i].user = 0;
-				}
-			}
-		}
-		spa_node_emit_info(&this->hooks, &this->info);
-		this->info.change_mask = old;
-	}
-}
-
-static void emit_port_info(struct state *this, bool full)
-{
-	uint64_t old = full ? this->port_info.change_mask : 0;
-	if (full)
-		this->port_info.change_mask = this->port_info_all;
-	if (this->port_info.change_mask) {
-		uint32_t i;
-
-		if (this->port_info.change_mask & SPA_PORT_CHANGE_MASK_PARAMS) {
-			for (i = 0; i < this->port_info.n_params; i++) {
-				if (this->port_params[i].user > 0) {
-					this->port_params[i].flags ^= SPA_PARAM_INFO_SERIAL;
-					this->port_params[i].user = 0;
-				}
-			}
-		}
-		spa_node_emit_port_info(&this->hooks,
-				SPA_DIRECTION_OUTPUT, 0, &this->port_info);
-		this->port_info.change_mask = old;
-	}
-}
-
-
 static int impl_node_enum_params(void *object, int seq,
 				 uint32_t id, uint32_t start, uint32_t num,
 				 const struct spa_pod *filter)
@@ -311,8 +242,8 @@ static int impl_node_set_param(void *object, uint32_t id, uint32_t flags,
 			handle_process_latency(this, &info);
 		}
 
-		emit_node_info(this, false);
-		emit_port_info(this, false);
+		spa_alsa_emit_node_info(this, false);
+		spa_alsa_emit_port_info(this, false);
 		break;
 	}
 	case SPA_PARAM_ProcessLatency:
@@ -325,8 +256,8 @@ static int impl_node_set_param(void *object, uint32_t id, uint32_t flags,
 
 		handle_process_latency(this, &info);
 
-		emit_node_info(this, false);
-		emit_port_info(this, false);
+		spa_alsa_emit_node_info(this, false);
+		spa_alsa_emit_port_info(this, false);
 		break;
 	}
 	default:
@@ -388,8 +319,8 @@ impl_node_add_listener(void *object,
 
 	spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
 
-	emit_node_info(this, true);
-	emit_port_info(this, true);
+	spa_alsa_emit_node_info(this, true);
+	spa_alsa_emit_port_info(this, true);
 
 	spa_hook_list_join(&this->hooks, &save);
 
@@ -607,7 +538,7 @@ static int port_set_format(void *object,
 	}
 
 	this->info.change_mask |= SPA_NODE_CHANGE_MASK_PROPS;
-	emit_node_info(this, false);
+	spa_alsa_emit_node_info(this, false);
 
 	this->port_info.change_mask |= SPA_PORT_CHANGE_MASK_RATE;
 	this->port_info.rate = SPA_FRACTION(1, this->rate);
@@ -620,7 +551,7 @@ static int port_set_format(void *object,
 		this->port_params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
 		this->port_params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
 	}
-	emit_port_info(this, false);
+	spa_alsa_emit_port_info(this, false);
 
 	return err;
 }
@@ -656,7 +587,7 @@ impl_node_port_set_param(void *object,
 		this->latency[info.direction] = info;
 		this->port_info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
 		this->port_params[PORT_Latency].user++;
-		emit_port_info(this, false);
+		spa_alsa_emit_port_info(this, false);
 		break;
 	}
 	case SPA_PARAM_Tag:
@@ -675,7 +606,7 @@ impl_node_port_set_param(void *object,
 
 			this->port_info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
 			this->port_params[PORT_Tag].user++;
-			emit_port_info(this, false);
+			spa_alsa_emit_port_info(this, false);
 		}
 		break;
 	}
@@ -898,7 +829,12 @@ impl_init(const struct spa_handle_factory *factory,
 
 	this->data_system = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataSystem);
 	this->data_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataLoop);
+	this->main_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Loop);
 
+	if (this->main_loop == NULL) {
+		spa_log_error(this->log, "a main loop is needed");
+		return -EINVAL;
+	}
 	if (this->data_loop == NULL) {
 		spa_log_error(this->log, "%p: a data loop is needed", this);
 		return -EINVAL;
diff --git a/spa/plugins/alsa/alsa-pcm.c b/spa/plugins/alsa/alsa-pcm.c
index f5e0edafc0ec240d2b6546aacd13340806cff937..e2d7835bbae168226ab03f0f632024a8480e36e3 100644
--- a/spa/plugins/alsa/alsa-pcm.c
+++ b/spa/plugins/alsa/alsa-pcm.c
@@ -13,6 +13,8 @@
 #include <spa/utils/result.h>
 #include <spa/support/system.h>
 #include <spa/utils/keys.h>
+#include <spa/node/keys.h>
+#include <spa/monitor/device.h>
 
 #include "alsa-pcm.h"
 
@@ -184,6 +186,69 @@ static int uint32_array_to_string(uint32_t *vals, uint32_t n_vals, char *val, si
 	return 0;
 }
 
+static struct spa_pod *enum_bind_ctl_propinfo(struct state *state, uint32_t idx, struct spa_pod_builder *b)
+{
+	char param_name[1024];
+	char param_desc[1024];
+	snd_ctl_elem_info_t *info = state->bound_ctls[idx].info;
+
+	if (!info) {
+		// This will end iteration early, so print a warning
+		spa_log_warn(state->log, "Don't have prop info for bind ctl, bailing");
+		return NULL;
+	}
+
+	snprintf(param_name, sizeof(param_name), "api.alsa.bind-ctl.%s",
+			snd_ctl_elem_info_get_name(info));
+	snprintf(param_desc, sizeof(param_desc), "Value of ALSA control '%s'",
+			snd_ctl_elem_info_get_name(info));
+
+	// We don't have meaningful default values
+	switch (snd_ctl_elem_info_get_type(info)) {
+		case SND_CTL_ELEM_TYPE_BOOLEAN:
+			return spa_pod_builder_add_object(b,
+					SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
+					SPA_PROP_INFO_name, SPA_POD_String(param_name),
+					SPA_PROP_INFO_description, SPA_POD_String(param_desc),
+					SPA_PROP_INFO_type, SPA_POD_Bool(false),
+					SPA_PROP_INFO_params, SPA_POD_Bool(true));
+
+		case SND_CTL_ELEM_TYPE_INTEGER:
+			return spa_pod_builder_add_object(b,
+					SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
+					SPA_PROP_INFO_name, SPA_POD_String(param_name),
+					SPA_PROP_INFO_description, SPA_POD_String(param_desc),
+					SPA_PROP_INFO_type, SPA_POD_Int(0),
+					SPA_PROP_INFO_params, SPA_POD_Bool(true));
+			break;
+
+		case SND_CTL_ELEM_TYPE_INTEGER64:
+			return spa_pod_builder_add_object(b,
+					SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
+					SPA_PROP_INFO_name, SPA_POD_String(param_name),
+					SPA_PROP_INFO_description, SPA_POD_String(param_desc),
+					SPA_PROP_INFO_type, SPA_POD_Long(0),
+					SPA_PROP_INFO_params, SPA_POD_Bool(true));
+			break;
+
+		case SND_CTL_ELEM_TYPE_ENUMERATED:
+			return spa_pod_builder_add_object(b,
+					SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
+					SPA_PROP_INFO_name, SPA_POD_String(param_name),
+					SPA_PROP_INFO_description, SPA_POD_String(param_desc),
+					SPA_PROP_INFO_type, SPA_POD_Int(0),
+					SPA_PROP_INFO_params, SPA_POD_Bool(true));
+			break;
+
+		default:
+			// FIXME: we can probably support bytes but the length seems unknown in the API
+			spa_log_warn(state->log, "%s ctl '%s' not supported",
+					snd_ctl_elem_type_name(snd_ctl_elem_info_get_type(info)),
+					snd_ctl_elem_info_get_name(info));
+			return NULL;
+	}
+}
+
 struct spa_pod *spa_alsa_enum_propinfo(struct state *state,
 		uint32_t idx, struct spa_pod_builder *b)
 {
@@ -346,12 +411,67 @@ struct spa_pod *spa_alsa_enum_propinfo(struct state *state,
 			SPA_PROP_INFO_type, SPA_POD_String(state->clock_name),
 			SPA_PROP_INFO_params, SPA_POD_Bool(true));
 		break;
+	// While adding params here, update the math in default too
 	default:
-		return NULL;
+		idx -= 17;
+		if (idx <= state->num_bind_ctls)
+			param = enum_bind_ctl_propinfo(state, idx - 1, b);
+		else
+			return NULL;
 	}
 	return param;
 }
 
+static void add_bind_ctl_param(struct state *state, const snd_ctl_elem_value_t *elem, const snd_ctl_elem_info_t *info,
+		struct spa_pod_builder *b)
+{
+	char param_name[1024];
+
+	snprintf(param_name, sizeof(param_name), "api.alsa.bind-ctl.%s",
+			snd_ctl_elem_info_get_name(info));
+	spa_pod_builder_string(b, param_name);
+
+	switch (snd_ctl_elem_info_get_type(info)) {
+		case SND_CTL_ELEM_TYPE_BOOLEAN:
+			spa_pod_builder_bool(b, snd_ctl_elem_value_get_boolean(elem, 0));
+			break;
+
+		case SND_CTL_ELEM_TYPE_INTEGER:
+			spa_pod_builder_int(b, snd_ctl_elem_value_get_integer(elem, 0));
+			break;
+
+		case SND_CTL_ELEM_TYPE_INTEGER64:
+			spa_pod_builder_long(b, snd_ctl_elem_value_get_integer64(elem, 0));
+			break;
+
+		case SND_CTL_ELEM_TYPE_ENUMERATED:
+			spa_pod_builder_int(b, snd_ctl_elem_value_get_enumerated(elem, 0));
+			break;
+
+		default:
+			// FIXME: we can probably support bytes but the length seems unknown in the API
+			spa_log_warn(state->log, "%s ctl '%s' not supported",
+					snd_ctl_elem_type_name(snd_ctl_elem_info_get_type(info)),
+					snd_ctl_elem_info_get_name(info));
+			break;
+	}
+}
+
+static void add_bind_ctl_params(struct state *state, struct spa_pod_builder *b)
+{
+	int err;
+
+	for (unsigned int i = 0; i < state->num_bind_ctls; i++) {
+		err = snd_ctl_elem_read(state->ctl, state->bound_ctls[i].value);
+		if (err < 0) {
+			spa_log_warn(state->log, "Could not read elem value for '%s': %s",
+					state->bound_ctls[i].name, snd_strerror(err));
+		}
+
+		add_bind_ctl_param(state, state->bound_ctls[i].value, state->bound_ctls[i].info, b);
+	}
+}
+
 int spa_alsa_add_prop_params(struct state *state, struct spa_pod_builder *b)
 {
 	struct spa_pod_frame f[1];
@@ -419,6 +539,8 @@ int spa_alsa_add_prop_params(struct state *state, struct spa_pod_builder *b)
 	spa_pod_builder_string(b, "clock.name");
 	spa_pod_builder_string(b, state->clock_name);
 
+	add_bind_ctl_params(state, b);
+
 	spa_pod_builder_pop(b, &f[0]);
 	return 0;
 }
@@ -498,6 +620,123 @@ static void silence_error_handler(const char *file, int line,
 {
 }
 
+static void fill_device_name(struct state *state, const char *params, char device_name[], size_t len)
+{
+	spa_scnprintf(device_name, len, "%s%s%s",
+			state->card->ucm_prefix ? state->card->ucm_prefix : "",
+			state->props.device, params ? params : "");
+}
+
+static void bind_ctl_event(struct spa_source *source)
+{
+	// We don't know if a bound element changed or not, so let's find out
+	struct state *state = source->data;
+	snd_ctl_elem_value_t *old_value;
+	bool changed = false;
+
+	snd_ctl_elem_value_alloca(&old_value);
+
+	for (unsigned int i = 0; i < state->num_bind_ctls; i++) {
+		int err;
+
+		snd_ctl_elem_value_copy(old_value, state->bound_ctls[i].value);
+
+		err = snd_ctl_elem_read(state->ctl, state->bound_ctls[i].value);
+		if (err < 0) {
+			spa_log_warn(state->log, "Could not read ctl '%s': %s",
+					state->bound_ctls[i].name, snd_strerror(err));
+			continue;
+		}
+
+		if (snd_ctl_elem_value_compare(old_value, state->bound_ctls[i].value) != 0) {
+			// We don't need to check all the ctls, if one changed,
+			// we'll emit a notification and they'll be read when
+			// the props are read
+			spa_log_debug(state->log, "bound ctl '%s' has changed", state->bound_ctls[i].name);
+			changed = true;
+			break;
+		}
+	}
+
+	if (changed) {
+		state->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS;
+		state->params[NODE_Props].user++;
+		spa_alsa_emit_node_info(state, false);
+	}
+}
+
+static void bind_ctls_for_params(struct state *state)
+{
+	struct pollfd pfds[16];
+	int err;
+
+	if (state->num_bind_ctls == 0)
+		return;
+
+	if (!state->ctl) {
+		char device_name[256];
+
+		fill_device_name(state, NULL, device_name, sizeof(device_name));
+
+		err = snd_ctl_open(&state->ctl, device_name, SND_CTL_NONBLOCK);
+		if (err < 0) {
+			spa_log_info(state->log, "%s could not find ctl device: %s",
+					state->props.device, snd_strerror(err));
+			state->ctl = NULL;
+			return;
+		}
+	}
+
+	state->ctl_n_fds = snd_ctl_poll_descriptors_count(state->ctl);
+	if (state->ctl_n_fds > (int)SPA_N_ELEMENTS(state->ctl_sources)) {
+		spa_log_warn(state->log, "Too many poll descriptors (%d), listening to a subset", state->ctl_n_fds);
+		state->ctl_n_fds = SPA_N_ELEMENTS(state->ctl_sources);
+	}
+
+	if ((err = snd_ctl_poll_descriptors(state->ctl, pfds, state->ctl_n_fds)) < 0) {
+		spa_log_warn(state->log, "Could not get poll descriptors: %s", snd_strerror(err));
+		return;
+	}
+
+	snd_ctl_subscribe_events(state->ctl, 1);
+
+	for (int i = 0; i < state->ctl_n_fds; i++) {
+		state->ctl_sources[i].func = bind_ctl_event;
+		state->ctl_sources[i].data = state;
+		state->ctl_sources[i].fd = pfds[i].fd;
+		state->ctl_sources[i].mask = SPA_IO_IN;
+		state->ctl_sources[i].rmask = 0;
+		spa_loop_add_source(state->main_loop, &state->ctl_sources[i]);
+	}
+
+	for (unsigned int i = 0; i < state->num_bind_ctls; i++) {
+		snd_ctl_elem_id_t *id;
+
+		snd_ctl_elem_id_alloca(&id);
+		snd_ctl_elem_id_set_name(id, state->bound_ctls[i].name);
+		snd_ctl_elem_id_set_interface(id, SND_CTL_ELEM_IFACE_PCM);
+
+		snd_ctl_elem_info_malloc(&state->bound_ctls[i].info);
+		snd_ctl_elem_info_set_id(state->bound_ctls[i].info, id);
+
+		err = snd_ctl_elem_info(state->ctl, state->bound_ctls[i].info);
+		if (err < 0) {
+			spa_log_warn(state->log, "Could not read elem info for '%s': %s",
+					state->bound_ctls[i].name, snd_strerror(err));
+
+			snd_ctl_elem_info_free(state->bound_ctls[i].info);
+			state->bound_ctls[i].info = NULL;
+			continue;
+		}
+
+		snd_ctl_elem_value_malloc(&state->bound_ctls[i].value);
+		snd_ctl_elem_value_set_id(state->bound_ctls[i].value, id);
+
+		spa_log_debug(state->log, "Binding ctl for '%s'",
+				snd_ctl_elem_info_get_name(state->bound_ctls[i].info));
+	}
+}
+
 int spa_alsa_init(struct state *state, const struct spa_dict *info)
 {
 	uint32_t i;
@@ -525,6 +764,24 @@ int spa_alsa_init(struct state *state, const struct spa_dict *info)
 			state->open_ucm = spa_atob(s);
 		} else if (spa_streq(k, "clock.quantum-limit")) {
 			spa_atou32(s, &state->quantum_limit, 0);
+		} else if (spa_streq(k, SPA_KEY_API_ALSA_BIND_CTLS)) {
+			struct spa_json it[2];
+			char v[256];
+			unsigned int i = 0;
+
+			/* Read a list of ALSA control names to bind as params */
+			spa_json_init(&it[0], s, strlen(s));
+			if (spa_json_enter_array(&it[0], &it[1]) <= 0)
+				spa_json_init(&it[1], s, strlen(s));
+
+			while (spa_json_get_string(&it[1], v, sizeof(v)) > 0 &&
+					i < SPA_N_ELEMENTS(state->bound_ctls)) {
+				strncpy(state->bound_ctls[i].name, v, sizeof(state->bound_ctls[i].name));
+				i++;
+			}
+			state->num_bind_ctls = i;
+
+			/* We'll do the actual binding after checking the card exists */
 		} else {
 			alsa_set_param(state, k, s);
 		}
@@ -558,6 +815,8 @@ int spa_alsa_init(struct state *state, const struct spa_dict *info)
 	state->rate_limit.interval = 2 * SPA_NSEC_PER_SEC;
 	state->rate_limit.burst = 1;
 
+	bind_ctls_for_params(state);
+
 	return 0;
 }
 
@@ -578,6 +837,26 @@ int spa_alsa_clear(struct state *state)
 	free(state->tag[0]);
 	free(state->tag[1]);
 
+	if (state->ctl) {
+		for (int i = 0; i < state->ctl_n_fds; i++) {
+			spa_loop_remove_source(state->main_loop, &state->ctl_sources[i]);
+		}
+
+		snd_ctl_close(state->ctl);
+		state->ctl = NULL;
+
+		for (unsigned int i = 0; i < state->num_bind_ctls; i++) {
+			if (state->bound_ctls[i].info) {
+				snd_ctl_elem_info_free(state->bound_ctls[i].info);
+				state->bound_ctls[i].info = NULL;
+			}
+			if (state->bound_ctls[i].value) {
+				snd_ctl_elem_value_free(state->bound_ctls[i].value);
+				state->bound_ctls[i].value = NULL;
+			}
+		}
+	}
+
 	return err;
 }
 
@@ -589,16 +868,20 @@ static int probe_pitch_ctl(struct state *state, const char* device_name)
 		state->stream == SND_PCM_STREAM_CAPTURE ?
 		"Capture Pitch 1000000" :
 		"Playback Pitch 1000000";
+	bool opened = false;
 	int err;
 
 	snd_lib_error_set_handler(silence_error_handler);
 
-	err = snd_ctl_open(&state->ctl, device_name, SND_CTL_NONBLOCK);
-	if (err < 0) {
-		spa_log_info(state->log, "%s could not find ctl device: %s",
-				device_name, snd_strerror(err));
-		state->ctl = NULL;
-		goto error;
+	if (!state->ctl) {
+		err = snd_ctl_open(&state->ctl, device_name, SND_CTL_NONBLOCK);
+		if (err < 0) {
+			spa_log_info(state->log, "%s could not find ctl device: %s",
+					device_name, snd_strerror(err));
+			state->ctl = NULL;
+			goto error;
+		}
+		opened = true;
 	}
 
 	snd_ctl_elem_id_alloca(&id);
@@ -616,9 +899,11 @@ static int probe_pitch_ctl(struct state *state, const char* device_name)
 		snd_ctl_elem_value_free(state->pitch_elem);
 		state->pitch_elem = NULL;
 
-		snd_ctl_close(state->ctl);
-		state->ctl = NULL;
-		goto error;
+		if (opened) {
+			snd_ctl_close(state->ctl);
+			state->ctl = NULL;
+			goto error;
+		}
 	}
 
 	snd_ctl_elem_value_set_integer(state->pitch_elem, 0, 1000000);
@@ -662,9 +947,7 @@ int spa_alsa_open(struct state *state, const char *params)
 	if (state->opened)
 		return 0;
 
-	spa_scnprintf(device_name, sizeof(device_name), "%s%s%s",
-			state->card->ucm_prefix ? state->card->ucm_prefix : "",
-			props->device, params ? params : "");
+	fill_device_name(state, params, device_name, sizeof(device_name));
 	spa_scnprintf(state->name, sizeof(state->name), "%s%s",
 			props->device, state->stream == SND_PCM_STREAM_CAPTURE ? "c" : "p");
 
@@ -757,8 +1040,11 @@ int spa_alsa_close(struct state *state)
 		snd_ctl_elem_value_free(state->pitch_elem);
 		state->pitch_elem = NULL;
 
-		snd_ctl_close(state->ctl);
-		state->ctl = NULL;
+		// Close it unless we've got some bind_ctls we're listening to
+		if (state->ctl_n_fds == 0) {
+			snd_ctl_close(state->ctl);
+			state->ctl = NULL;
+		}
 	}
 
 	return err;
@@ -1982,6 +2268,7 @@ static void reset_buffers(struct state *this)
 
 	spa_list_init(&this->free);
 	spa_list_init(&this->ready);
+	this->ready_offset = 0;
 
 	for (i = 0; i < this->n_buffers; i++) {
 		struct buffer *b = &this->buffers[i];
@@ -2032,7 +2319,7 @@ static int do_prepare(struct state *state)
 static inline int do_drop(struct state *state)
 {
 	int res;
-	spa_log_debug(state->log, "%p: snd_pcm_drop %u", state, state->linked);
+	spa_log_debug(state->log, "%p: snd_pcm_drop linked:%u", state, state->linked);
 	if (!state->linked && (res = snd_pcm_drop(state->hndl)) < 0) {
 		spa_log_error(state->log, "%s: snd_pcm_drop: %s",
 				state->name, snd_strerror(res));
@@ -2045,7 +2332,7 @@ static inline int do_start(struct state *state)
 {
 	int res;
 	if (SPA_UNLIKELY(!state->alsa_started)) {
-		spa_log_debug(state->log, "%p: snd_pcm_start %u", state, state->linked);
+		spa_log_debug(state->log, "%p: snd_pcm_start linked:%u", state, state->linked);
 		if (!state->linked && (res = snd_pcm_start(state->hndl)) < 0) {
 			spa_log_error(state->log, "%s: snd_pcm_start: %s",
 					state->name, snd_strerror(res));
@@ -2058,9 +2345,9 @@ static inline int do_start(struct state *state)
 
 static inline int check_position_config(struct state *state);
 
-static int alsa_recover(struct state *state, int err)
+static int alsa_recover(struct state *state)
 {
-	int res, st;
+	int res, st, retry = 0;
 	snd_pcm_status_t *status;
 	struct state *driver, *follower;
 
@@ -2100,10 +2387,12 @@ static int alsa_recover(struct state *state, int err)
 	case SND_PCM_STATE_SUSPENDED:
 		spa_log_info(state->log, "%s: recover from state %s",
 				state->name, snd_pcm_state_name(st));
-		res = snd_pcm_resume(state->hndl);
+		while (retry++ < 5 && (res = snd_pcm_resume(state->hndl)) == -EAGAIN)
+			/* wait until suspend flag is released */
+			poll(NULL, 0, 1000);
 		if (res >= 0)
-		        return res;
-		err = -ESTRPIPE;
+			return res;
+		/* try to drop and prepare below */
 		break;
 	default:
 		spa_log_error(state->log, "%s: recover from error state %s",
@@ -2112,11 +2401,6 @@ static int alsa_recover(struct state *state, int err)
 	}
 
 recover:
-	if (SPA_UNLIKELY((res = snd_pcm_recover(state->hndl, err, true)) < 0)) {
-		spa_log_error(state->log, "%s: snd_pcm_recover error: %s",
-				state->name, snd_strerror(res));
-		return res;
-	}
 	if (state->driver && state->linked)
 		driver = state->driver;
 	else
@@ -2139,8 +2423,7 @@ recover:
 		if (follower != driver && follower->linked)
 			do_start(follower);
 	}
-
-	return res;
+	return 0;
 }
 
 static inline snd_pcm_sframes_t alsa_avail(struct state *state)
@@ -2159,7 +2442,7 @@ static int get_avail(struct state *state, uint64_t current_time, snd_pcm_uframes
 	snd_pcm_sframes_t avail;
 
 	if (SPA_UNLIKELY((avail = alsa_avail(state)) < 0)) {
-		if ((res = alsa_recover(state, avail)) < 0)
+		if ((res = alsa_recover(state)) < 0)
 			return res;
 		if ((avail = alsa_avail(state)) < 0) {
 			if ((suppressed = spa_ratelimit_test(&state->rate_limit, current_time)) >= 0) {
@@ -2248,6 +2531,15 @@ static int get_status(struct state *state, uint64_t current_time, snd_pcm_uframe
 	return 0;
 }
 
+
+static uint64_t get_time_ns(struct state *state)
+{
+	struct timespec now;
+	if (spa_system_clock_gettime(state->data_system, CLOCK_MONOTONIC, &now) < 0)
+		return 0;
+	return SPA_TIMESPEC_TO_NSEC(&now);
+}
+
 static int update_time(struct state *state, uint64_t current_time, snd_pcm_sframes_t delay,
 		snd_pcm_sframes_t target, bool follower)
 {
@@ -2255,8 +2547,15 @@ static int update_time(struct state *state, uint64_t current_time, snd_pcm_sfram
 	int32_t diff;
 
 	if (state->disable_tsched && !follower) {
-		err = (int64_t)(current_time - state->next_time);
-		err = err / 1e9 * state->rate;
+		uint64_t now = get_time_ns(state);
+
+		if (SPA_UNLIKELY(state->dll.bw == 0.0)) {
+			current_time = now;
+			err = 0.0;
+		} else {
+			err = (int64_t)(now - current_time);
+			err = err / 1e9 * state->rate;
+		}
 	} else {
 		if (state->stream == SND_PCM_STREAM_PLAYBACK)
 			err = delay - target;
@@ -2351,7 +2650,8 @@ static int setup_matching(struct state *state)
 	if (spa_streq(state->position->clock.name, state->clock_name))
 		state->matching = false;
 
-	state->resample = !state->pitch_elem && (((uint32_t)state->rate != state->driver_rate.denom) || state->matching);
+	state->resample = !state->pitch_elem &&
+		(((uint32_t)state->rate != state->driver_rate.denom) || state->matching);
 	recalc_headroom(state);
 
 	spa_log_info(state->log, "driver clock:'%s'@%d our clock:'%s'@%d matching:%d resample:%d",
@@ -2404,7 +2704,8 @@ static inline int check_position_config(struct state *state)
 		state->threshold = SPA_SCALE32_UP(state->driver_duration, state->rate, state->driver_rate.denom);
 		state->max_error = SPA_MAX(256.0f, state->threshold / 2.0f);
 		state->max_resync = SPA_MIN(state->threshold, state->max_error);
-		state->resample = ((uint32_t)state->rate != state->driver_rate.denom) || state->matching;
+		state->resample = !state->pitch_elem &&
+			(((uint32_t)state->rate != state->driver_rate.denom) || state->matching);
 		state->alsa_sync = true;
 	}
 	return 0;
@@ -2420,7 +2721,7 @@ static int alsa_write_sync(struct state *state, uint64_t current_time)
 		return res;
 
 	if (SPA_UNLIKELY((res = get_status(state, current_time, &avail, &delay, &target)) < 0)) {
-		spa_log_error(state->log, "get_status error");
+		spa_log_error(state->log, "get_status error: %s", spa_strerror(res));
 		state->next_time += state->threshold * 1e9 / state->rate;
 		return res;
 	}
@@ -2482,7 +2783,7 @@ again:
 		if (SPA_UNLIKELY((res = snd_pcm_mmap_begin(hndl, &my_areas, &offset, &frames)) < 0)) {
 			spa_log_error(state->log, "%s: snd_pcm_mmap_begin error: %s",
 					state->name, snd_strerror(res));
-			alsa_recover(state, res);
+			alsa_recover(state);
 			return res;
 		}
 		spa_log_trace_fp(state->log, "%p: begin offset:%ld avail:%ld threshold:%d",
@@ -2681,7 +2982,7 @@ static int alsa_read_sync(struct state *state, uint64_t current_time)
 		return res;
 
 	if (SPA_UNLIKELY((res = get_status(state, current_time, &avail, &delay, &target)) < 0)) {
-		spa_log_error(state->log, "get_status error");
+		spa_log_error(state->log, "get_status error: %s", spa_strerror(res));
 		state->next_time += state->threshold * 1e9 / state->rate;
 		return res;
 	}
@@ -2748,7 +3049,7 @@ static int alsa_read_frames(struct state *state)
 		if ((res = snd_pcm_mmap_begin(hndl, &my_areas, &offset, &avail)) < 0) {
 			spa_log_error(state->log, "%s: snd_pcm_mmap_begin error: %s",
 					state->name, snd_strerror(res));
-			alsa_recover(state, res);
+			alsa_recover(state);
 			return res;
 		}
 		spa_log_trace_fp(state->log, "%p: begin offs:%ld frames:%ld avail:%ld thres:%d", state,
@@ -2881,14 +3182,6 @@ static int capture_ready(struct state *state)
 	return 0;
 }
 
-static uint64_t get_time_ns(struct state *state)
-{
-	struct timespec now;
-	if (spa_system_clock_gettime(state->data_system, CLOCK_MONOTONIC, &now) < 0)
-		return 0;
-	return SPA_TIMESPEC_TO_NSEC(&now);
-}
-
 static void alsa_wakeup_event(struct spa_source *source)
 {
 	struct state *state = source->data, *follower;
@@ -2900,8 +3193,6 @@ static void alsa_wakeup_event(struct spa_source *source)
 		int err;
 		unsigned short revents;
 
-		current_time = get_time_ns(state);
-
 		for (int i = 0; i < state->n_fds; i++) {
 			state->pfds[i].revents = state->source[i].rmask;
 			/* Reset so that we only handle all our sources' events once */
@@ -2919,6 +3210,11 @@ static void alsa_wakeup_event(struct spa_source *source)
 			spa_log_trace_fp(state->log, "Woken up with no work to do");
 			return;
 		}
+		if (revents & POLLERR) {
+			spa_log_trace_fp(state->log, "poll error");
+			if ((res = alsa_recover(state)) < 0)
+				return;
+		}
 	} else {
 		if (SPA_LIKELY(state->started)) {
 			if (SPA_UNLIKELY((res = spa_system_timerfd_read(state->data_system,
@@ -2932,8 +3228,8 @@ static void alsa_wakeup_event(struct spa_source *source)
 				return;
 			}
 		}
-		current_time = state->next_time;
 	}
+	current_time = state->next_time;
 
 	/* first do all the sync */
 	if (state->stream == SND_PCM_STREAM_CAPTURE)
@@ -3227,3 +3523,74 @@ int spa_alsa_pause(struct state *state)
 
 	return 0;
 }
+
+void spa_alsa_emit_node_info(struct state *state, bool full)
+{
+	uint64_t old = full ? state->info.change_mask : 0;
+
+	if (full)
+		state->info.change_mask = state->info_all;
+	if (state->info.change_mask) {
+		struct spa_dict_item items[7];
+		uint32_t i, n_items = 0;
+		char latency[64], period[64], nperiods[64], headroom[64];
+
+		items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_API, "alsa");
+		items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_MEDIA_CLASS,
+				state->stream == SND_PCM_STREAM_PLAYBACK ? "Audio/Sink" : "Audio/Source");
+		items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_NODE_DRIVER, "true");
+		if (state->have_format) {
+			snprintf(latency, sizeof(latency), "%lu/%d", state->buffer_frames / 2, state->rate);
+			items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_NODE_MAX_LATENCY, latency);
+			snprintf(period, sizeof(period), "%lu", state->period_frames);
+			items[n_items++] = SPA_DICT_ITEM_INIT("api.alsa.period-size", period);
+			snprintf(nperiods, sizeof(nperiods), "%lu",
+					state->period_frames != 0 ? state->buffer_frames / state->period_frames : 0);
+			items[n_items++] = SPA_DICT_ITEM_INIT("api.alsa.period-num", nperiods);
+			snprintf(headroom, sizeof(headroom), "%u", state->headroom);
+			items[n_items++] = SPA_DICT_ITEM_INIT("api.alsa.headroom", headroom);
+		} else {
+			items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_NODE_MAX_LATENCY, NULL);
+			items[n_items++] = SPA_DICT_ITEM_INIT("api.alsa.period-size", NULL);
+			items[n_items++] = SPA_DICT_ITEM_INIT("api.alsa.period-num", NULL);
+			items[n_items++] = SPA_DICT_ITEM_INIT("api.alsa.headroom", NULL);
+		}
+		state->info.props = &SPA_DICT_INIT(items, n_items);
+
+		if (state->info.change_mask & SPA_NODE_CHANGE_MASK_PARAMS) {
+			for (i = 0; i < state->info.n_params; i++) {
+				if (state->params[i].user > 0) {
+					state->params[i].flags ^= SPA_PARAM_INFO_SERIAL;
+					state->params[i].user = 0;
+				}
+			}
+		}
+		spa_node_emit_info(&state->hooks, &state->info);
+
+		state->info.change_mask = old;
+	}
+}
+
+void spa_alsa_emit_port_info(struct state *state, bool full)
+{
+	uint64_t old = full ? state->port_info.change_mask : 0;
+
+	if (full)
+		state->port_info.change_mask = state->port_info_all;
+	if (state->port_info.change_mask) {
+		uint32_t i;
+
+		if (state->port_info.change_mask & SPA_PORT_CHANGE_MASK_PARAMS) {
+			for (i = 0; i < state->port_info.n_params; i++) {
+				if (state->port_params[i].user > 0) {
+					state->port_params[i].flags ^= SPA_PARAM_INFO_SERIAL;
+					state->port_params[i].user = 0;
+				}
+			}
+		}
+		spa_node_emit_port_info(&state->hooks,
+				state->stream == SND_PCM_STREAM_PLAYBACK ? SPA_DIRECTION_INPUT : SPA_DIRECTION_OUTPUT,
+				0, &state->port_info);
+		state->port_info.change_mask = old;
+	}
+}
diff --git a/spa/plugins/alsa/alsa-pcm.h b/spa/plugins/alsa/alsa-pcm.h
index 92b355794e57fb40d4cae5ebd577377dbca0b13e..c3cfe0e51de96d665430bc9e5a8c1d8d25b6035e 100644
--- a/spa/plugins/alsa/alsa-pcm.h
+++ b/spa/plugins/alsa/alsa-pcm.h
@@ -92,6 +92,12 @@ struct rt_state {
 	unsigned int following:1;
 };
 
+struct bound_ctl {
+	char name[256];
+	snd_ctl_elem_info_t *info;
+	snd_ctl_elem_value_t *value;
+};
+
 struct state {
 	struct spa_handle handle;
 	struct spa_node node;
@@ -99,6 +105,7 @@ struct state {
 	struct spa_log *log;
 	struct spa_system *data_system;
 	struct spa_loop *data_loop;
+	struct spa_loop *main_loop;
 
 	FILE *log_file;
 	struct spa_ratelimit rate_limit;
@@ -240,11 +247,19 @@ struct state {
 
 	struct spa_pod *tag[2];
 
-	/* Rate match via an ALSA ctl */
+	/* for rate match and bind ctls */
 	snd_ctl_t *ctl;
+
+	/* Rate match via an ALSA ctl */
 	snd_ctl_elem_value_t *pitch_elem;
 	double last_rate;
 
+	/* ALSA ctls exposed as params */
+	unsigned int num_bind_ctls;
+	struct bound_ctl bound_ctls[16];
+	struct spa_source ctl_sources[MAX_POLL];
+	int ctl_n_fds;
+
 	struct spa_list link;
 
 	struct spa_list followers;
@@ -282,6 +297,9 @@ int spa_alsa_skip(struct state *state);
 
 void spa_alsa_recycle_buffer(struct state *state, uint32_t buffer_id);
 
+void spa_alsa_emit_node_info(struct state *state, bool full);
+void spa_alsa_emit_port_info(struct state *state, bool full);
+
 static inline uint32_t spa_alsa_format_from_name(const char *name, size_t len)
 {
 	int i;
diff --git a/spa/plugins/bluez5/bap-codec-lc3.c b/spa/plugins/bluez5/bap-codec-lc3.c
index 19e0af6bc1e51ec535c6f738f74e955491f1f4c7..1cffbeddeeb1faf00eea0d21e53486a9cd7a0317 100644
--- a/spa/plugins/bluez5/bap-codec-lc3.c
+++ b/spa/plugins/bluez5/bap-codec-lc3.c
@@ -14,6 +14,7 @@
 #include <spa/param/audio/format.h>
 #include <spa/param/audio/format-utils.h>
 #include <spa/utils/string.h>
+#include <spa/debug/log.h>
 
 #include <lc3.h>
 
@@ -22,6 +23,11 @@
 
 #define MAX_PACS	64
 
+static struct spa_log *log;
+static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.codecs.lc3");
+#undef SPA_LOG_TOPIC_DEFAULT
+#define SPA_LOG_TOPIC_DEFAULT &log_topic
+
 struct impl {
 	lc3_encoder_t enc[LC3_MAX_CHANNELS];
 	lc3_decoder_t dec[LC3_MAX_CHANNELS];
@@ -38,6 +44,7 @@ struct impl {
 struct pac_data {
 	const uint8_t *data;
 	size_t size;
+	int index;
 	uint32_t locations;
 };
 
@@ -126,7 +133,31 @@ static int codec_fill_caps(const struct media_codec *codec, uint32_t flags,
 	return data - caps;
 }
 
-static int parse_bluez_pacs(const uint8_t *data, size_t data_size, struct pac_data pacs[MAX_PACS])
+static void debugc_ltv(struct spa_debug_context *debug_ctx, int pac, struct ltv *ltv)
+{
+	switch (ltv->len) {
+	case 0:
+		spa_debugc(debug_ctx, "PAC %d: --", pac);
+		break;
+	case 2:
+		spa_debugc(debug_ctx, "PAC %d: 0x%02x %x", pac, ltv->type, ltv->value[0]);
+		break;
+	case 3:
+		spa_debugc(debug_ctx, "PAC %d: 0x%02x %x %x", pac, ltv->type, ltv->value[0], ltv->value[1]);
+		break;
+	case 5:
+		spa_debugc(debug_ctx, "PAC %d: 0x%02x %x %x %x %x", pac, ltv->type,
+				ltv->value[0], ltv->value[1], ltv->value[2], ltv->value[3]);
+		break;
+	default:
+		spa_debugc(debug_ctx, "PAC %d: 0x%02x", pac, ltv->type);
+		spa_debugc_mem(debug_ctx, 7, ltv->value, ltv->len - 1);
+		break;
+	}
+}
+
+static int parse_bluez_pacs(const uint8_t *data, size_t data_size, struct pac_data pacs[MAX_PACS],
+		struct spa_debug_context *debug_ctx)
 {
 	/*
 	 * BlueZ capabilites for the same codec may contain multiple
@@ -145,10 +176,11 @@ static int parse_bluez_pacs(const uint8_t *data, size_t data_size, struct pac_da
 				break;
 
 			++pac;
-			pacs[pac] = (struct pac_data){ data + 1, 0 };
+			pacs[pac] = (struct pac_data){ data + 1, 0, pac };
 		} else if (ltv->len >= data_size) {
 			return -EINVAL;
 		} else {
+			debugc_ltv(debug_ctx, pac, ltv);
 			pacs[pac].size += ltv->len + 1;
 		}
 		data_size -= ltv->len + 1;
@@ -172,13 +204,13 @@ static uint8_t get_num_channels(uint32_t channels)
 	return num;
 }
 
-static int select_channels(uint8_t channels, uint32_t locations, uint32_t *mapping)
+static int select_channels(uint8_t channels, uint32_t locations, uint32_t *mapping, unsigned int max_channels)
 {
-	unsigned int i, num;
+	unsigned int i, num = 0;
 
-	if (channels & LC3_CHAN_2)
+	if ((channels & LC3_CHAN_2) && max_channels >= 2)
 		num = 2;
-	else if (channels & LC3_CHAN_1)
+	else if ((channels & LC3_CHAN_1) && max_channels >= 1)
 		num = 1;
 	else
 		return -1;
@@ -202,12 +234,13 @@ static int select_channels(uint8_t channels, uint32_t locations, uint32_t *mappi
 	return 0;
 }
 
-static bool select_config(bap_lc3_t *conf, const struct pac_data *pac)
+static bool select_config(bap_lc3_t *conf, const struct pac_data *pac,	struct spa_debug_context *debug_ctx)
 {
 	const uint8_t *data = pac->data;
 	size_t data_size = pac->size;
 	uint16_t framelen_min = 0, framelen_max = 0;
 	int max_frames = -1;
+	uint8_t channels = 0;
 
 	if (!data_size)
 		return false;
@@ -221,8 +254,10 @@ static bool select_config(bap_lc3_t *conf, const struct pac_data *pac)
 	while (data_size > 0) {
 		struct ltv *ltv = (struct ltv *)data;
 
-		if (ltv->len < sizeof(struct ltv) || ltv->len >= data_size)
+		if (ltv->len < sizeof(struct ltv) || ltv->len >= data_size) {
+			spa_debugc(debug_ctx, "invalid LTV data");
 			return false;
+		}
 
 		switch (ltv->type) {
 		case LC3_TYPE_FREQ:
@@ -237,8 +272,10 @@ static bool select_config(bap_lc3_t *conf, const struct pac_data *pac)
 					conf->rate = LC3_CONFIG_FREQ_16KHZ;
 				else if (rate & LC3_FREQ_8KHZ)
 					conf->rate = LC3_CONFIG_FREQ_8KHZ;
-				else
+				else {
+					spa_debugc(debug_ctx, "unsupported rate: 0x%04x", rate);
 					return false;
+				}
 			}
 			break;
 		case LC3_TYPE_DUR:
@@ -249,17 +286,16 @@ static bool select_config(bap_lc3_t *conf, const struct pac_data *pac)
 					conf->frame_duration = LC3_CONFIG_DURATION_10;
 				else if (duration & LC3_DUR_7_5)
 					conf->frame_duration = LC3_CONFIG_DURATION_7_5;
-				else
+				else {
+					spa_debugc(debug_ctx, "unsupported duration: 0x%02x", duration);
 					return false;
+				}
 			}
 			break;
 		case LC3_TYPE_CHAN:
 			spa_return_val_if_fail(ltv->len == 2, false);
 			{
-				uint8_t channels = ltv->value[0];
-
-				if (select_channels(channels, pac->locations, &conf->channels) < 0)
-					return false;
+				channels = ltv->value[0];
 			}
 			break;
 		case LC3_TYPE_FRAMELEN:
@@ -272,22 +308,35 @@ static bool select_config(bap_lc3_t *conf, const struct pac_data *pac)
 			max_frames = ltv->value[0];
 			break;
 		default:
-			return false;
+			spa_debugc(debug_ctx, "unknown LTV type: 0x%02x", ltv->type);
+			break;
 		}
 		data_size -= ltv->len + 1;
 		data += ltv->len + 1;
 	}
 
+	if (select_channels(channels, pac->locations, &conf->channels, max_frames) < 0) {
+		spa_debugc(debug_ctx, "invalid channel configuration: 0x%02x %u",
+				channels, max_frames);
+		return false;
+	}
+
 	/* Default: 1 per channel (BAP v1.0.1 Sec 4.3.1) */
 	if (max_frames < 0)
 		max_frames = get_num_channels(conf->channels);
-	if (max_frames < get_num_channels(conf->channels))
+	if (max_frames < get_num_channels(conf->channels)) {
+		spa_debugc(debug_ctx, "invalid max frames per SDU: %u", max_frames);
 		return false;
+	}
 
-	if (framelen_min < LC3_MIN_FRAME_BYTES || framelen_max > LC3_MAX_FRAME_BYTES)
+	if (framelen_min < LC3_MIN_FRAME_BYTES || framelen_max > LC3_MAX_FRAME_BYTES) {
+		spa_debugc(debug_ctx, "invalid framelen: %u %u", framelen_min, framelen_max);
 		return false;
-	if (conf->frame_duration == 0xFF || !conf->rate)
+	}
+	if (conf->frame_duration == 0xFF || !conf->rate) {
+		spa_debugc(debug_ctx, "no frame duration or rate");
 		return false;
+	}
 
 	/* BAP v1.0.1 Table 5.2; high-reliability */
 	switch (conf->rate) {
@@ -316,7 +365,8 @@ static bool select_config(bap_lc3_t *conf, const struct pac_data *pac)
 			conf->framelen = 30;	/* 8_2_2 */
 		break;
 	default:
-			return false;
+		spa_debugc(debug_ctx, "invalid rate");
+		return false;
 	}
 
 	return true;
@@ -400,8 +450,12 @@ static int conf_cmp(const bap_lc3_t *conf1, int res1, const bap_lc3_t *conf2, in
 		return b - a;
 
 	PREFER_BOOL(conf->channels & LC3_CHAN_2);
+	PREFER_BOOL(conf->channels & LC3_CHAN_1);
 	PREFER_BOOL(conf->rate & (LC3_CONFIG_FREQ_48KHZ | LC3_CONFIG_FREQ_24KHZ | LC3_CONFIG_FREQ_16KHZ | LC3_CONFIG_FREQ_8KHZ));
 	PREFER_BOOL(conf->rate & LC3_CONFIG_FREQ_48KHZ);
+	PREFER_BOOL(conf->rate & LC3_CONFIG_FREQ_24KHZ);
+	PREFER_BOOL(conf->rate & LC3_CONFIG_FREQ_16KHZ);
+	PREFER_BOOL(conf->rate & LC3_CONFIG_FREQ_8KHZ);
 
 	return 0;
 
@@ -413,11 +467,12 @@ static int pac_cmp(const void *p1, const void *p2)
 {
 	const struct pac_data *pac1 = p1;
 	const struct pac_data *pac2 = p2;
+	struct spa_debug_log_ctx debug_ctx = SPA_LOG_DEBUG_INIT(log, SPA_LOG_LEVEL_TRACE);
 	bap_lc3_t conf1, conf2;
 	int res1, res2;
 
-	res1 = select_config(&conf1, pac1) ? (int)sizeof(bap_lc3_t) : -EINVAL;
-	res2 = select_config(&conf2, pac2) ? (int)sizeof(bap_lc3_t) : -EINVAL;
+	res1 = select_config(&conf1, pac1, &debug_ctx.ctx) ? (int)sizeof(bap_lc3_t) : -EINVAL;
+	res2 = select_config(&conf2, pac2, &debug_ctx.ctx) ? (int)sizeof(bap_lc3_t) : -EINVAL;
 
 	return conf_cmp(&conf1, res1, &conf2, res2);
 }
@@ -432,6 +487,7 @@ static int codec_select_config(const struct media_codec *codec, uint32_t flags,
 	bap_lc3_t conf;
 	uint8_t *data = config;
 	uint32_t locations = 0;
+	struct spa_debug_log_ctx debug_ctx = SPA_LOG_DEBUG_INIT(log, SPA_LOG_LEVEL_TRACE);
 	int i;
 
 	if (caps == NULL)
@@ -441,21 +497,29 @@ static int codec_select_config(const struct media_codec *codec, uint32_t flags,
 		for (i = 0; i < (int)settings->n_items; ++i)
 			if (spa_streq(settings->items[i].key, "bluez5.bap.locations"))
 				sscanf(settings->items[i].value, "%"PRIu32, &locations);
+
+		if (spa_atob(spa_dict_lookup(settings, "bluez5.bap.debug")))
+			debug_ctx = SPA_LOG_DEBUG_INIT(log, SPA_LOG_LEVEL_DEBUG);
 	}
 
 	/* Select best conf from those possible */
-	npacs = parse_bluez_pacs(caps, caps_size, pacs);
-	if (npacs < 0)
+	npacs = parse_bluez_pacs(caps, caps_size, pacs, &debug_ctx.ctx);
+	if (npacs < 0) {
+		spa_debugc(&debug_ctx.ctx, "malformed PACS");
 		return npacs;
-	else if (npacs == 0)
+	} else if (npacs == 0) {
+		spa_debugc(&debug_ctx.ctx, "no PACS");
 		return -EINVAL;
+	}
 
 	for (i = 0; i < npacs; ++i)
 		pacs[i].locations = locations;
 
 	qsort(pacs, npacs, sizeof(struct pac_data), pac_cmp);
 
-	if (!select_config(&conf, &pacs[0]))
+	spa_debugc(&debug_ctx.ctx, "selected PAC %d", pacs[0].index);
+
+	if (!select_config(&conf, &pacs[0], &debug_ctx.ctx))
 		return -ENOTSUP;
 
 	data += write_ltv_uint8(data, LC3_TYPE_FREQ, conf.rate);
@@ -556,12 +620,13 @@ static int codec_enum_config(const struct media_codec *codec, uint32_t flags,
 			spa_pod_builder_int(b, 8000);
 		spa_pod_builder_int(b, 8000);
 	}
-	if (i == 0)
-		return -EINVAL;
 	if (i > 1)
 		choice->body.type = SPA_CHOICE_Enum;
 	spa_pod_builder_pop(b, &f[1]);
 
+	if (i == 0)
+		return -EINVAL;
+
 	res = channels_to_positions(conf.channels, position);
 	if (res == 0)
 		return -EINVAL;
@@ -674,6 +739,8 @@ static int codec_get_qos(const struct media_codec *codec,
 	if (endpoint_qos->latency >= 0x0005 && endpoint_qos->latency <= 0x0FA0)
 		/* Values outside the range are RFU */
 		qos->latency = endpoint_qos->latency;
+	if (endpoint_qos->retransmission)
+		qos->retransmission = endpoint_qos->retransmission;
 	if (endpoint_qos->delay_min)
 		qos->delay = SPA_MAX(qos->delay, endpoint_qos->delay_min);
 	if (endpoint_qos->delay_max)
@@ -889,6 +956,12 @@ static int codec_increase_bitpool(void *data)
 	return -ENOTSUP;
 }
 
+static void codec_set_log(struct spa_log *global_log)
+{
+	log = global_log;
+	spa_log_topic_init(log, &log_topic);
+}
+
 const struct media_codec bap_codec_lc3 = {
 	.id = SPA_BLUETOOTH_AUDIO_CODEC_LC3,
 	.name = "lc3",
@@ -910,7 +983,8 @@ const struct media_codec bap_codec_lc3 = {
 	.start_decode = codec_start_decode,
 	.decode = codec_decode,
 	.reduce_bitpool = codec_reduce_bitpool,
-	.increase_bitpool = codec_increase_bitpool
+	.increase_bitpool = codec_increase_bitpool,
+	.set_log = codec_set_log,
 };
 
 MEDIA_CODEC_EXPORT_DEF(
diff --git a/spa/plugins/bluez5/bluez5-dbus.c b/spa/plugins/bluez5/bluez5-dbus.c
index e5207d9804ec153dd18289b70c715765324b988c..bb6e0a8b336d8cc0b03578ff3fb780a91507fc46 100644
--- a/spa/plugins/bluez5/bluez5-dbus.c
+++ b/spa/plugins/bluez5/bluez5-dbus.c
@@ -876,7 +876,7 @@ static DBusHandlerResult endpoint_select_properties(DBusConnection *conn, DBusMe
 	bool sink;
 	const char *err_msg = "Unknown error";
 	struct spa_dict settings;
-	struct spa_dict_item setting_items[SPA_N_ELEMENTS(monitor->global_setting_items) + 1];
+	struct spa_dict_item setting_items[SPA_N_ELEMENTS(monitor->global_setting_items) + 2];
 	int i;
 
 	const char *endpoint_path = NULL;
@@ -933,8 +933,9 @@ static DBusHandlerResult endpoint_select_properties(DBusConnection *conn, DBusMe
 
 	for (i = 0; i < (int)monitor->global_settings.n_items; ++i)
 		setting_items[i] = monitor->global_settings.items[i];
-	setting_items[i] = SPA_DICT_ITEM_INIT("bluez5.bap.locations", locations);
-	settings = SPA_DICT_INIT(setting_items, monitor->global_settings.n_items + 1);
+	setting_items[i++] = SPA_DICT_ITEM_INIT("bluez5.bap.locations", locations);
+	setting_items[i++] = SPA_DICT_ITEM_INIT("bluez5.bap.debug", "true");
+	settings = SPA_DICT_INIT(setting_items, i);
 
 	conf_size = codec->select_config(codec, 0, caps, caps_size, &monitor->default_audio_info, &settings, config);
 	if (conf_size < 0) {
@@ -971,6 +972,11 @@ static DBusHandlerResult endpoint_select_properties(DBusConnection *conn, DBusMe
 			goto error_invalid;
 		}
 
+		spa_log_debug(monitor->log, "select qos: interval:%d framing:%d phy:%d sdu:%d "
+				"rtn:%d latency:%d delay:%d target_latency:%d",
+				qos.interval, qos.framing, qos.phy, qos.sdu, qos.retransmission,
+				qos.latency, (int)qos.delay, qos.target_latency);
+
 		dbus_message_iter_open_container(&dict, DBUS_TYPE_DICT_ENTRY, NULL, &entry);
 		dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &entry_key);
 		dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT, "a{sv}", &variant);
@@ -3465,6 +3471,9 @@ static int transport_create_iso_io(struct spa_bt_transport *transport)
 				SPA_BT_PROFILE_BAP_BROADCAST_SINK | SPA_BT_PROFILE_BAP_BROADCAST_SOURCE)))
 			continue;
 
+		if (t->device->adapter != transport->device->adapter)
+			continue;
+
 		if ((transport->profile == SPA_BT_PROFILE_BAP_BROADCAST_SINK) ||
 			(transport->profile == SPA_BT_PROFILE_BAP_BROADCAST_SOURCE)) {
 			if (t->bap_big != transport->bap_big)
@@ -3587,10 +3596,10 @@ finish:
 		/* For broadcast there initiator moves the transport state to SPA_BT_TRANSPORT_STATE_ACTIVE */
 		if ((transport->profile == SPA_BT_PROFILE_BAP_BROADCAST_SINK) ||
 			(transport->profile == SPA_BT_PROFILE_BAP_BROADCAST_SOURCE))	{
-			spa_bt_transport_set_state(transport, SPA_BT_TRANSPORT_STATE_ACTIVE);
+			spa_bt_transport_set_state(t_linked, SPA_BT_TRANSPORT_STATE_ACTIVE);
 		} else {
 			if (!transport->bap_initiator)
-				spa_bt_transport_set_state(transport, SPA_BT_TRANSPORT_STATE_ACTIVE);
+				spa_bt_transport_set_state(t_linked, SPA_BT_TRANSPORT_STATE_ACTIVE);
 		}
 	}
 
diff --git a/spa/plugins/bluez5/codec-loader.c b/spa/plugins/bluez5/codec-loader.c
index 6ac2301e16fb52d12adacd04c8a8d44082dc45be..3e7e139eba628484e2c5935f9bdfcb323437c8a4 100644
--- a/spa/plugins/bluez5/codec-loader.c
+++ b/spa/plugins/bluez5/codec-loader.c
@@ -173,7 +173,7 @@ const struct media_codec * const *load_media_codecs(struct spa_plugin_loader *lo
 #undef MEDIA_CODEC_FACTORY_LIB
 	};
 
-	impl = calloc(sizeof(struct impl), 1);
+	impl = calloc(1, sizeof(struct impl));
 	if (impl == NULL)
 		return NULL;
 
diff --git a/spa/plugins/bluez5/iso-io.c b/spa/plugins/bluez5/iso-io.c
index 68ceb6b69142d3a06492720768f0de88ab478916..01391695a4de39aec7fd7fbcc569a667e44dff06 100644
--- a/spa/plugins/bluez5/iso-io.c
+++ b/spa/plugins/bluez5/iso-io.c
@@ -37,7 +37,7 @@ struct group {
 	struct spa_source source;
 	struct spa_list streams;
 	int timerfd;
-	uint8_t cig;
+	uint8_t id;
 	uint64_t next;
 	uint64_t duration;
 	uint32_t paused;
@@ -155,7 +155,7 @@ static void group_on_timeout(struct spa_source *source)
 	if ((res = spa_system_timerfd_read(group->data_system, group->timerfd, &exp)) < 0) {
 		if (res != -EAGAIN)
 			spa_log_warn(group->log, "%p: ISO group:%u error reading timerfd: %s",
-					group, group->cig, spa_strerror(res));
+					group, group->id, spa_strerror(res));
 		return;
 	}
 
@@ -178,7 +178,7 @@ static void group_on_timeout(struct spa_source *source)
 
 	if (group->paused) {
 		--group->paused;
-		spa_log_debug(group->log, "%p: ISO group:%d paused:%u", group, group->cig, group->paused);
+		spa_log_debug(group->log, "%p: ISO group:%u paused:%u", group, group->id, group->paused);
 	}
 
 	/* Produce output */
@@ -194,7 +194,7 @@ static void group_on_timeout(struct spa_source *source)
 		}
 		if (stream->this.size == 0) {
 			spa_log_debug(group->log, "%p: ISO group:%u miss fd:%d",
-					group, group->cig, stream->fd);
+					group, group->id, stream->fd);
 			if (stream_silence(stream) < 0) {
 				fail = true;
 				continue;
@@ -208,7 +208,7 @@ static void group_on_timeout(struct spa_source *source)
 		}
 
 		spa_log_trace(group->log, "%p: ISO group:%u sent fd:%d size:%u ts:%u idle:%d res:%d",
-				group, group->cig, stream->fd, (unsigned)stream->this.size,
+				group, group->id, stream->fd, (unsigned)stream->this.size,
 				(unsigned)stream->this.timestamp, stream->idle, res);
 
 		stream->this.size = 0;
@@ -243,19 +243,29 @@ static struct group *group_create(struct spa_bt_transport *t,
 		struct spa_log *log, struct spa_loop *data_loop, struct spa_system *data_system)
 {
 	struct group *group;
+	uint8_t id;
 
 	if (t->bap_interval <= 5000) {
 		errno = EINVAL;
 		return NULL;
 	}
 
+	if (t->profile & (SPA_BT_PROFILE_BAP_SINK | SPA_BT_PROFILE_BAP_SOURCE)) {
+		id = t->bap_cig;
+	} else if (t->profile & (SPA_BT_PROFILE_BAP_BROADCAST_SINK | SPA_BT_PROFILE_BAP_BROADCAST_SOURCE)) {
+		id = t->bap_big;
+	} else {
+		errno = EINVAL;
+		return NULL;
+	}
+
 	group = calloc(1, sizeof(struct group));
 	if (group == NULL)
 		return NULL;
 
 	spa_log_topic_init(log, &log_topic);
 
-	group->cig = t->bap_cig;
+	group->id = id;
 	group->log = log;
 	group->data_loop = data_loop;
 	group->data_system = data_system;
diff --git a/spa/plugins/bluez5/media-sink.c b/spa/plugins/bluez5/media-sink.c
index df8a2e0bc8534c33006b05204d590b2fb776fe39..63d9cd760cd0a6fb96d068b70617c1750e47101a 100644
--- a/spa/plugins/bluez5/media-sink.c
+++ b/spa/plugins/bluez5/media-sink.c
@@ -1405,6 +1405,21 @@ static int impl_node_send_command(void *object, const struct spa_command *comman
 
 static void emit_node_info(struct impl *this, bool full)
 {
+	char node_group_buf[256];
+	char *node_group = NULL;
+
+	if (this->transport && (this->transport->profile & SPA_BT_PROFILE_BAP_SINK)) {
+		spa_scnprintf(node_group_buf, sizeof(node_group_buf), "bluez-iso-%s-cig-%d",
+				this->transport->device->adapter->address,
+				this->transport->bap_cig);
+		node_group = node_group_buf;
+	} else if (this->transport && (this->transport->profile & SPA_BT_PROFILE_BAP_BROADCAST_SINK)) {
+		spa_scnprintf(node_group_buf, sizeof(node_group_buf), "bluez-iso-%s-big-%d",
+				this->transport->device->adapter->address,
+				this->transport->bap_big);
+		node_group = node_group_buf;
+	}
+
 	struct spa_dict_item node_info_items[] = {
 		{ SPA_KEY_DEVICE_API, "bluez5" },
 		{ SPA_KEY_MEDIA_CLASS, this->is_internal ? "Audio/Sink/Internal" :
@@ -1412,6 +1427,7 @@ static void emit_node_info(struct impl *this, bool full)
 		{ "media.name", ((this->transport && this->transport->device->name) ?
 					this->transport->device->name : this->codec->bap ? "BAP" : "A2DP" ) },
 		{ SPA_KEY_NODE_DRIVER, this->is_output ? "true" : "false" },
+		{ "node.group", node_group },
 	};
 	uint64_t old = full ? this->info.change_mask : 0;
 	if (full)
diff --git a/spa/plugins/libcamera/libcamera-device.cpp b/spa/plugins/libcamera/libcamera-device.cpp
index 70b7c144a4193869968fff060c5b5334de348405..0abf2f619520f4eeb44f0aece14ee8e2895d8940 100644
--- a/spa/plugins/libcamera/libcamera-device.cpp
+++ b/spa/plugins/libcamera/libcamera-device.cpp
@@ -106,7 +106,7 @@ static int emit_info(struct impl *impl, bool full)
 	uint32_t n_items = 0;
 	struct spa_device_info info;
 	struct spa_param_info params[2];
-	char path[256], model[256], name[256], devices_str[128];
+	char path[256], name[256], devices_str[128];
 	struct spa_strbuf buf;
 
 	info = SPA_DEVICE_INFO_INIT();
@@ -123,9 +123,10 @@ static int emit_info(struct impl *impl, bool full)
 	if (auto location = cameraLoc(impl->camera.get()))
 		ADD_ITEM(SPA_KEY_API_LIBCAMERA_LOCATION, location);
 
-	snprintf(model, sizeof(model), "%s", cameraModel(impl->camera.get()).c_str());
-	ADD_ITEM(SPA_KEY_DEVICE_PRODUCT_NAME, model);
-	ADD_ITEM(SPA_KEY_DEVICE_DESCRIPTION, model);
+	const auto model = cameraModel(impl->camera.get());
+	ADD_ITEM(SPA_KEY_DEVICE_PRODUCT_NAME, model.c_str());
+	ADD_ITEM(SPA_KEY_DEVICE_DESCRIPTION, model.c_str());
+
 	snprintf(name, sizeof(name), "libcamera_device.%s", impl->device_id.c_str());
 	ADD_ITEM(SPA_KEY_DEVICE_NAME, name);
 
diff --git a/spa/plugins/libcamera/libcamera-source.cpp b/spa/plugins/libcamera/libcamera-source.cpp
index 8a08dca2c9cb6e3bb29546d57323e6cf10aa9781..3d92734194daf786ebb256c5397e7b57533b19d5 100644
--- a/spa/plugins/libcamera/libcamera-source.cpp
+++ b/spa/plugins/libcamera/libcamera-source.cpp
@@ -26,6 +26,7 @@
 #include <spa/node/keys.h>
 #include <spa/param/video/format-utils.h>
 #include <spa/param/param.h>
+#include <spa/param/latency-utils.h>
 #include <spa/control/control.h>
 #include <spa/pod/filter.h>
 
@@ -96,7 +97,8 @@ struct port {
 #define PORT_IO		3
 #define PORT_Format	4
 #define PORT_Buffers	5
-#define N_PORT_PARAMS	6
+#define PORT_Latency	6
+#define N_PORT_PARAMS	7
 	struct spa_param_info params[N_PORT_PARAMS];
 
 	uint32_t fmt_index = 0;
@@ -114,6 +116,7 @@ struct port {
 		params[PORT_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
 		params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
 		params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+		params[PORT_Latency] = SPA_PARAM_INFO(SPA_PARAM_Latency, SPA_PARAM_INFO_READ);
 
 		info.flags = SPA_PORT_FLAG_LIVE | SPA_PORT_FLAG_PHYSICAL | SPA_PORT_FLAG_TERMINAL;
 		info.params = params;
@@ -152,6 +155,8 @@ struct impl {
 	struct spa_io_position *position = nullptr;
 	struct spa_io_clock *clock = nullptr;
 
+	struct spa_latency_info latency[2];
+
 	std::shared_ptr<CameraManager> manager;
 	std::shared_ptr<Camera> camera;
 
@@ -601,6 +606,15 @@ next:
 			return 0;
 		}
 		break;
+	case SPA_PARAM_Latency:
+		switch (result.index) {
+		case 0: case 1:
+			param = spa_latency_build(&b, id, &impl->latency[result.index]);
+			break;
+		default:
+			return 0;
+		}
+		break;
 	default:
 		return -ENOENT;
 	}
@@ -961,6 +975,9 @@ impl::impl(spa_log *log, spa_loop *data_loop, spa_system *system,
 	info.flags = SPA_NODE_FLAG_RT;
 	info.params = params;
 	info.n_params = N_NODE_PARAMS;
+
+	latency[SPA_DIRECTION_INPUT] = SPA_LATENCY_INFO(SPA_DIRECTION_INPUT);
+	latency[SPA_DIRECTION_OUTPUT] = SPA_LATENCY_INFO(SPA_DIRECTION_OUTPUT);
 }
 
 static size_t
diff --git a/spa/plugins/support/evl-system.c b/spa/plugins/support/evl-system.c
index 336b1080b0e241c0c84bcb9832b77db4f23b2845..a5cb9fd5e4955ab1fca249e2820a9de4cf92351b 100644
--- a/spa/plugins/support/evl-system.c
+++ b/spa/plugins/support/evl-system.c
@@ -11,6 +11,7 @@
 #include <fcntl.h>
 #include <sys/eventfd.h>
 #include <sys/signalfd.h>
+#include <pthread.h>
 
 #include <evl/evl.h>
 #include <evl/timer.h>
@@ -32,6 +33,7 @@ struct poll_entry {
 	int fd;
 	uint32_t events;
 	void *data;
+	unsigned attached:1;
 };
 
 struct impl {
@@ -45,6 +47,7 @@ struct impl {
 
 	uint32_t n_xbuf;
 	int attached;
+	pthread_t thread;
 	int pid;
 };
 
@@ -110,6 +113,21 @@ static int impl_pollfd_create(void *object, int flags)
 	return retval;
 }
 
+static inline struct poll_entry *find_free(struct impl *impl)
+{
+	uint32_t i;
+	for (i = 0; i < impl->n_entries; i++) {
+		struct poll_entry *e = &impl->entries[i];
+		if (e->fd == -1)
+			return e;
+	}
+	if (impl->n_entries == MAX_POLL) {
+		errno = ENOSPC;
+		return NULL;
+	}
+	return &impl->entries[impl->n_entries++];
+}
+
 static inline struct poll_entry *find_entry(struct impl *impl, int pfd, int fd)
 {
 	uint32_t i;
@@ -121,20 +139,36 @@ static inline struct poll_entry *find_entry(struct impl *impl, int pfd, int fd)
 	return NULL;
 }
 
+static int attach_entry(struct impl *impl, struct poll_entry *e)
+{
+	if (!e->attached && e->fd != -1) {
+		int res;
+
+		res = evl_add_pollfd(e->pfd, e->fd, e->events, evl_nil);
+		if (res < 0)
+			return res;
+
+		e->attached = true;
+	}
+	return 0;
+}
+
 static int impl_pollfd_add(void *object, int pfd, int fd, uint32_t events, void *data)
 {
 	struct impl *impl = object;
 	struct poll_entry *e;
+	int res = 0;
 
-	if (impl->n_entries == MAX_POLL)
-		return -ENOSPC;
+	if ((e = find_free(impl)) == NULL)
+		return -errno;
 
-	e = &impl->entries[impl->n_entries++];
 	e->pfd = pfd;
 	e->fd = fd;
 	e->events = events;
 	e->data = data;
-	return evl_add_pollfd(pfd, fd, e->events, evl_nil);
+	if (impl->attached != 0)
+		attach_entry(impl, e);
+	return res;
 }
 
 static int impl_pollfd_mod(void *object, int pfd, int fd, uint32_t events, void *data)
@@ -162,6 +196,7 @@ static int impl_pollfd_del(void *object, int pfd, int fd)
 
 	e->pfd = -1;
 	e->fd = -1;
+	e->attached = false;
 	return evl_del_pollfd(pfd, fd);
 }
 
@@ -178,6 +213,12 @@ static int impl_pollfd_wait(void *object, int pfd,
 		if (res < 0)
 			return res;
 		impl->attached = res;
+		impl->thread = pthread_self();
+
+		for (i = 0; i < (int)impl->n_entries; i++) {
+			struct poll_entry *e = &impl->entries[i];
+			attach_entry(impl, e);
+		}
 	}
 
 	if (timeout == -1) {
@@ -259,12 +300,14 @@ static int impl_eventfd_create(void *object, int flags)
 {
 	struct impl *impl = object;
 	int res, fl;
+	struct evl_flags flg;
 
-	fl = EVL_CLONE_PRIVATE;
+	fl = EVL_CLONE_PUBLIC;
 	if (flags & SPA_FD_NONBLOCK)
 		fl |= EVL_CLONE_NONBLOCK;
 
-	res = evl_create_xbuf(1024, 1024, fl, "xbuf-%d-%p-%d", impl->pid, impl, impl->n_xbuf);
+	res = evl_create_flags(&flg, EVL_CLOCK_MONOTONIC, 0, fl,
+			"flags-%d-%p-%d", impl->pid, impl, impl->n_xbuf);
 	if (res < 0)
 		return res;
 
@@ -275,14 +318,35 @@ static int impl_eventfd_create(void *object, int flags)
 
 static int impl_eventfd_write(void *object, int fd, uint64_t count)
 {
-	if (write(fd, &count, sizeof(uint64_t)) != sizeof(uint64_t))
-		return -errno;
-	return 0;
+	int res;
+	int flags = count;
+	struct impl *impl = object;
+	pthread_t tid = pthread_self();
+
+	if (impl->thread != tid)
+		res = write(fd, &flags, sizeof(flags));
+	else
+		res = oob_write(fd, &flags, sizeof(flags));
+
+	if (res != sizeof(flags))
+		res = -errno;
+	return res;
 }
 
 static int impl_eventfd_read(void *object, int fd, uint64_t *count)
 {
-	if (oob_read(fd, count, sizeof(uint64_t)) != sizeof(uint64_t))
+	int res;
+	int flags;
+	struct impl *impl = object;
+	pthread_t tid = pthread_self();
+
+	if (impl->thread != tid)
+		res = read(fd, &flags, sizeof(flags));
+	else
+		res = oob_read(fd, &flags, sizeof(flags));
+
+	*count = flags;
+	if (res != sizeof(flags))
 		return -errno;
 	return 0;
 }
@@ -400,12 +464,12 @@ impl_init(const struct spa_handle_factory *factory,
 
 	impl->pid = getpid();
 
-	if ((res = evl_attach_self("evl-system-%d-%p", impl->pid, impl)) < 0) {
+	if ((res = evl_init()) < 0) {
 		spa_log_error(impl->log, NAME " %p: init failed: %s", impl, spa_strerror(res));
 		return res;
 	}
 
-	spa_log_debug(impl->log, NAME " %p: initialized", impl);
+	spa_log_info(impl->log, NAME " %p: initialized", impl);
 
 	return 0;
 }
diff --git a/src/daemon/meson.build b/src/daemon/meson.build
index 8629a99ff69cd4a835def29a4a728489cf30057b..95f0948ff59626020317712a1759a37305e725f2 100644
--- a/src/daemon/meson.build
+++ b/src/daemon/meson.build
@@ -2,10 +2,6 @@ pipewire_daemon_sources = [
   'pipewire.c',
 ]
 
-pipewire_c_args = [
-  '-DG_LOG_DOMAIN=g_log_domain_pipewire',
-]
-
 conf_config = configuration_data()
 conf_config.set('VERSION', '"@0@"'.format(pipewire_version))
 conf_config.set('PIPEWIRE_CONFIG_DIR', pipewire_configdir)
@@ -106,7 +102,6 @@ endforeach
 pipewire_exec = executable('pipewire',
   pipewire_daemon_sources,
   install: true,
-  c_args : pipewire_c_args,
   include_directories : [ configinc ],
   dependencies : [ spa_dep, pipewire_dep, ],
 )
diff --git a/src/daemon/pipewire-pulse.conf.in b/src/daemon/pipewire-pulse.conf.in
index 4d2de2415ef4c6362cb6d12489605138d9e04c95..6f7feb98a14580d09c6c791adeccfd0d0990a654 100644
--- a/src/daemon/pipewire-pulse.conf.in
+++ b/src/daemon/pipewire-pulse.conf.in
@@ -31,6 +31,8 @@ context.modules = [
             rt.prio      = 65
             #rt.time.soft = -1
             #rt.time.hard = -1
+            #uclamp.min = 0
+            #uclamp.max = 1024
         }
         flags = [ ifexists nofail ]
     }
diff --git a/src/daemon/pipewire.conf.in b/src/daemon/pipewire.conf.in
index 776c167d009b79ab1d37c6b5491962640f73deef..e4eb8e5ae2c14eba9b99e9d680ea3c99b4f0afc7 100644
--- a/src/daemon/pipewire.conf.in
+++ b/src/daemon/pipewire.conf.in
@@ -90,13 +90,16 @@ context.modules = [
 
     # Uses realtime scheduling to boost the audio thread priorities. This uses
     # RTKit if the user doesn't have permission to use regular realtime
-    # scheduling.
+    # scheduling. You can also clamp utilisation values to improve scheduling
+    # on embedded and heterogeneous systems, e.g. Arm big.LITTLE devices.
     { name = libpipewire-module-rt
         args = {
             nice.level    = -11
             #rt.prio      = 88
             #rt.time.soft = -1
             #rt.time.hard = -1
+            #uclamp.min = 0
+            #uclamp.max = 1024
         }
         flags = [ ifexists nofail ]
     }
diff --git a/src/daemon/systemd/system/meson.build b/src/daemon/systemd/system/meson.build
index 84ca0b068c7514377aea9b99f171ea0825ed83c0..d06d3adf643226e4d622c5fdf1b2e48115f90006 100644
--- a/src/daemon/systemd/system/meson.build
+++ b/src/daemon/systemd/system/meson.build
@@ -3,7 +3,7 @@ if get_option('systemd-system-unit-dir') != ''
   systemd_system_services_dir = get_option('systemd-system-unit-dir')
 endif
 
-install_data(sources : 'pipewire.socket',
+install_data(sources : ['pipewire.socket', 'pipewire-manager.socket'],
              install_dir : systemd_system_services_dir)
 
 systemd_config = configuration_data()
diff --git a/src/daemon/systemd/system/pipewire-manager.socket b/src/daemon/systemd/system/pipewire-manager.socket
new file mode 100644
index 0000000000000000000000000000000000000000..235f3db67e9c02a70a36c2d8995980a306bb9cbe
--- /dev/null
+++ b/src/daemon/systemd/system/pipewire-manager.socket
@@ -0,0 +1,13 @@
+[Unit]
+Description=PipeWire Multimedia System Manager Socket
+
+[Socket]
+Service=pipewire.service
+Priority=6
+ListenStream=%t/pipewire/pipewire-0-manager
+SocketUser=pipewire
+SocketGroup=pipewire
+SocketMode=0600
+
+[Install]
+WantedBy=sockets.target
diff --git a/src/daemon/systemd/system/pipewire.service.in b/src/daemon/systemd/system/pipewire.service.in
index 8b75ba284e3ed8edbad825543b5526f67064819a..dc8db3f8f0aef5e43b2dc05a224468b080c69bdd 100644
--- a/src/daemon/systemd/system/pipewire.service.in
+++ b/src/daemon/systemd/system/pipewire.service.in
@@ -31,5 +31,5 @@ User=pipewire
 Environment=PIPEWIRE_RUNTIME_DIR=%t/pipewire
 
 [Install]
-Also=pipewire.socket
+Also=pipewire.socket pipewire-manager.socket
 WantedBy=default.target
diff --git a/src/daemon/systemd/system/pipewire.socket b/src/daemon/systemd/system/pipewire.socket
index 296fd374b6dcd0a901478d1698feda2b36a41cdc..2e3cb719f8eb3f9409772d7960d754c2622093aa 100644
--- a/src/daemon/systemd/system/pipewire.socket
+++ b/src/daemon/systemd/system/pipewire.socket
@@ -1,10 +1,9 @@
 [Unit]
-Description=PipeWire Multimedia System Sockets
+Description=PipeWire Multimedia System Socket
 
 [Socket]
 Priority=6
 ListenStream=%t/pipewire/pipewire-0
-ListenStream=%t/pipewire/pipewire-0-manager
 SocketUser=pipewire
 SocketGroup=pipewire
 SocketMode=0660
diff --git a/src/examples/audio-capture.c b/src/examples/audio-capture.c
index b5f9d82bf84dc35e46ed93b67c13002c4b68713a..3a6658b9d49fa67e1b45e6ef1aa988cf3461b40c 100644
--- a/src/examples/audio-capture.c
+++ b/src/examples/audio-capture.c
@@ -143,6 +143,7 @@ int main(int argc, char *argv[])
 	 * the data.
 	 */
 	props = pw_properties_new(PW_KEY_MEDIA_TYPE, "Audio",
+			PW_KEY_CONFIG_NAME, "client-rt.conf",
 			PW_KEY_MEDIA_CATEGORY, "Capture",
 			PW_KEY_MEDIA_ROLE, "Music",
 			NULL);
diff --git a/src/gst/gstpipewiresink.c b/src/gst/gstpipewiresink.c
index 36d158095c285e4508dda962608d821e7621994b..001ede9d5813df4cc42040d99ff3a6452b33b997 100644
--- a/src/gst/gstpipewiresink.c
+++ b/src/gst/gstpipewiresink.c
@@ -532,8 +532,13 @@ on_state_changed (void *data, enum pw_stream_state old, enum pw_stream_state sta
         pw_stream_trigger_process (pwsink->stream);
       break;
     case PW_STREAM_STATE_ERROR:
-      GST_ELEMENT_ERROR (pwsink, RESOURCE, FAILED,
-          ("stream error: %s", error), (NULL));
+      /* make the error permanent, if it is not already;
+         pw_stream_set_error() will recursively call us again */
+      if (pw_stream_get_state (pwsink->stream, NULL) != PW_STREAM_STATE_ERROR)
+        pw_stream_set_error (pwsink->stream, -EPIPE, "%s", error);
+      else
+        GST_ELEMENT_ERROR (pwsink, RESOURCE, FAILED,
+            ("stream error: %s", error), (NULL));
       break;
   }
   pw_thread_loop_signal (pwsink->core->loop, FALSE);
diff --git a/src/gst/gstpipewiresrc.c b/src/gst/gstpipewiresrc.c
index 0514e4caa6b021eb5e25d9dc7f1eaf98330b4f21..e473338ba2b0c45ce411e3ac4bcaac2f809a0b76 100644
--- a/src/gst/gstpipewiresrc.c
+++ b/src/gst/gstpipewiresrc.c
@@ -681,9 +681,13 @@ on_state_changed (void *data,
     case PW_STREAM_STATE_STREAMING:
       break;
     case PW_STREAM_STATE_ERROR:
-      pw_stream_set_error (pwsrc->stream, -EPIPE, "%s", error);
-      GST_ELEMENT_ERROR (pwsrc, RESOURCE, FAILED,
-          ("stream error: %s", error), (NULL));
+      /* make the error permanent, if it is not already;
+         pw_stream_set_error() will recursively call us again */
+      if (pw_stream_get_state (pwsrc->stream, NULL) != PW_STREAM_STATE_ERROR)
+        pw_stream_set_error (pwsrc->stream, -EPIPE, "%s", error);
+      else
+        GST_ELEMENT_ERROR (pwsrc, RESOURCE, FAILED,
+            ("stream error: %s", error), (NULL));
       break;
   }
   pw_thread_loop_signal (pwsrc->core->loop, FALSE);
diff --git a/src/modules/meson.build b/src/modules/meson.build
index a60b85eee017cbc220661914d566f3f662c8b755..1b434b7f628df54b4dddb8d0ba1689f9e121a327 100644
--- a/src/modules/meson.build
+++ b/src/modules/meson.build
@@ -218,8 +218,10 @@ endif
 summary({'ffado-driver': build_module_ffado_driver}, bool_yn: true, section: 'Optional Modules')
 
 opus_custom_h = cc.has_header('opus/opus_custom.h', dependencies: opus_dep)
+opus_custom_lib = cc.has_function('opus_custom_encoder_ctl', dependencies: opus_dep)
+
 # One would imagine that opus_dep is a requirement but for some reason it's not, so we need to manually check that
-if opus_dep.found() and opus_custom_h
+if opus_dep.found() and opus_custom_h and opus_custom_lib
   opus_custom_dep = declare_dependency(compile_args: ['-DHAVE_OPUS_CUSTOM'], dependencies: opus_dep)
 else
   opus_custom_dep = dependency('', required: false)
@@ -622,7 +624,7 @@ if build_module_raop
 endif
 summary({'raop-sink (requires OpenSSL)': build_module_raop}, bool_yn: true, section: 'Optional Modules')
 
-roc_dep = dependency('roc', required: get_option('roc'))
+roc_dep = dependency('roc', version: '>= 0.3.0', required: get_option('roc'))
 summary({'ROC': roc_dep.found()}, bool_yn: true, section: 'Streaming between daemons')
 
 pipewire_module_rtp_source = shared_library('pipewire-module-rtp-source',
diff --git a/src/modules/module-access.c b/src/modules/module-access.c
index 87dda86caef8a0cd4eec12800d862ff559aa6af2..fd43c8ff9bab08e02e06871d6920e9de2680d2ae 100644
--- a/src/modules/module-access.c
+++ b/src/modules/module-access.c
@@ -29,7 +29,7 @@
 
 #include "flatpak-utils.h"
 
-/** \page page_module_access PipeWire Module: Access
+/** \page page_module_access Access
  *
  *
  * The `access` module performs access checks on clients. The access check
@@ -63,6 +63,10 @@
  * `pipewire.access.portal.app_id` property is to the Flatpak application ID, if
  * found. In addition, `pipewire.sec.flatpak` is set to `true`.
  *
+ * ## Module Name
+ *
+ * `libpipewire-module-access`
+ *
  * ## Module Options
  *
  * Options specific to the behavior of this module
diff --git a/src/modules/module-adapter.c b/src/modules/module-adapter.c
index 2c26dd3f0c8bf6b2317354535dc255822305f9a5..d92bc965d70d3ec51dc527eef55475f41321a2b1 100644
--- a/src/modules/module-adapter.c
+++ b/src/modules/module-adapter.c
@@ -18,7 +18,11 @@
 #include "modules/spa/spa-node.h"
 #include "module-adapter/adapter.h"
 
-/** \page page_module_adapter PipeWire Module: Adapter
+/** \page page_module_adapter Adapter
+ *
+ * ## Module Name
+ *
+ * `libpipewire-module-adapter`
  */
 #define NAME "adapter"
 
diff --git a/src/modules/module-avb.c b/src/modules/module-avb.c
index ac7a70ca3bb6cc5c0a73a0a6032e157487901564..b730b17c1c34721603210682d7fa6c63e7a94ea1 100644
--- a/src/modules/module-avb.c
+++ b/src/modules/module-avb.c
@@ -21,7 +21,11 @@
 
 #include "module-avb/avb.h"
 
-/** \page page_module_avb PipeWire Module: AVB
+/** \page page_module_avb AVB
+ *
+ * ## Module Name
+ *
+ * `libpipewire-module-avb`
  */
 
 #define NAME "avb"
diff --git a/src/modules/module-client-device.c b/src/modules/module-client-device.c
index dccdbebdd96a267f9d788675361c4b24e43241fb..e2450e81b7b7ad67df7ce8cdd953f0bed285c4f3 100644
--- a/src/modules/module-client-device.c
+++ b/src/modules/module-client-device.c
@@ -15,7 +15,11 @@
 
 #include "module-client-device/client-device.h"
 
-/** \page page_module_client_device PipeWire Module: Client Device
+/** \page page_module_client_device Client Device
+ *
+ * ## Module Name
+ *
+ * `libpipewire-module-client-device`
  */
 
 #define NAME "client-device"
diff --git a/src/modules/module-client-node.c b/src/modules/module-client-node.c
index b3673bf6957a3fdb17fe312b68163cec853c6ebc..9e8922db8e2226540b4b08bdf5e7ee7d14bcf227 100644
--- a/src/modules/module-client-node.c
+++ b/src/modules/module-client-node.c
@@ -16,7 +16,11 @@
 #include "module-client-node/v0/client-node.h"
 #include "module-client-node/client-node.h"
 
-/** \page page_module_client_node PipeWire Module: Client Node
+/** \page page_module_client_node Client Node
+ *
+ * ## Module Name
+ *
+ * `libpipewire-module-client-node`
  */
 
 #define NAME "client-node"
diff --git a/src/modules/module-client-node/client-node.c b/src/modules/module-client-node/client-node.c
index fbaa8ecee078761de70773ab7efd6fbddf80ef49..c493e24c9c1a243ee8f71437c4f2f7e460586bd5 100644
--- a/src/modules/module-client-node/client-node.c
+++ b/src/modules/module-client-node/client-node.c
@@ -49,7 +49,6 @@ struct buffer {
 };
 
 struct mix {
-	unsigned int valid:1;
 	uint32_t mix_id;
 	struct port *port;
 	uint32_t peer_id;
@@ -79,7 +78,7 @@ struct port {
 	unsigned int removed:1;
 	unsigned int destroyed:1;
 
-	struct pw_array mix;
+	struct pw_map mix;
 };
 
 struct impl {
@@ -191,42 +190,62 @@ do_port_use_buffers(struct impl *impl,
 
 static struct mix *find_mix(struct port *p, uint32_t mix_id)
 {
-	struct mix *mix;
+	if (mix_id == SPA_ID_INVALID)
+		mix_id = 0;
+	else
+		mix_id++;
+
+	return pw_map_lookup(&p->mix, mix_id);
+}
+
+static struct mix *create_mix(struct port *p, uint32_t mix_id)
+{
+	struct mix *mix = NULL;
 	size_t len;
+	int res;
 
 	if (mix_id == SPA_ID_INVALID)
 		mix_id = 0;
 	else
 		mix_id++;
 
-	len = pw_array_get_len(&p->mix, struct mix);
-	if (mix_id >= len) {
-		size_t need = sizeof(struct mix) * (mix_id + 1 - len);
-		void *ptr = pw_array_add(&p->mix, need);
-		if (ptr == NULL)
-			return NULL;
-		memset(ptr, 0, need);
+	if (pw_map_lookup(&p->mix, mix_id) != NULL) {
+		errno = EEXIST;
+		return NULL;
 	}
-	mix = pw_array_get_unchecked(&p->mix, mix_id, struct mix);
-	return mix;
-}
 
-static void mix_init(struct mix *mix, struct port *p, uint32_t mix_id)
-{
-	mix->valid = true;
+	/* pad map size */
+	for (len = pw_map_get_size(&p->mix); len < mix_id; ++len)
+		if ((res = pw_map_insert_at(&p->mix, len, NULL)) < 0)
+			goto fail;
+
+	mix = calloc(1, sizeof(struct mix));
+	if (mix == NULL)
+		return NULL;
+	if ((res = pw_map_insert_at(&p->mix, mix_id, mix)) < 0)
+		goto fail;
+
 	mix->mix_id = mix_id;
 	mix->port = p;
 	mix->n_buffers = 0;
+	return mix;
+
+fail:
+	free(mix);
+	errno = -res;
+	return NULL;
 }
 
-static struct mix *create_mix(struct impl *impl, struct port *p, uint32_t mix_id)
+static void free_mix(struct port *p, struct mix *mix)
 {
-	struct mix *mix;
+	if (mix == NULL)
+		return;
 
-	if ((mix = find_mix(p, mix_id)) == NULL || mix->valid)
-		return NULL;
-	mix_init(mix, p, mix_id);
-	return mix;
+	/* never realloc so it's safe to call from pw_map_foreach */
+	if (mix->mix_id < pw_map_get_size(&p->mix))
+		pw_map_insert_at(&p->mix, mix->mix_id, NULL);
+
+	free(mix);
 }
 
 static void clear_data(struct impl *impl, struct spa_data *d)
@@ -255,19 +274,21 @@ static void clear_data(struct impl *impl, struct spa_data *d)
 	}
 }
 
-static int clear_buffers(struct impl *impl, struct mix *mix)
+static void clear_buffer(struct impl *impl, struct spa_buffer *b)
 {
-	uint32_t i, j;
+	uint32_t i;
+	for (i = 0; i < b->n_datas; i++)
+		clear_data(impl, &b->datas[i]);
+}
 
+static int clear_buffers(struct impl *impl, struct mix *mix)
+{
+	uint32_t i;
 	for (i = 0; i < mix->n_buffers; i++) {
 		struct buffer *b = &mix->buffers[i];
 
 		spa_log_debug(impl->log, "%p: clear buffer %d", impl, i);
-
-		for (j = 0; j < b->buffer.n_datas; j++) {
-			struct spa_data *d = &b->datas[j];
-			clear_data(impl, d);
-		}
+		clear_buffer(impl, &b->buffer);
 		pw_memblock_unref(b->mem);
 	}
 	mix->n_buffers = 0;
@@ -278,11 +299,10 @@ static void mix_clear(struct impl *impl, struct mix *mix)
 {
 	struct port *port = mix->port;
 
-	if (!mix->valid)
-		return;
 	do_port_use_buffers(impl, port->direction, port->id,
 			mix->mix_id, 0, NULL, 0);
-	mix->valid = false;
+
+	free_mix(port, mix);
 }
 
 static int impl_node_enum_params(void *object, int seq,
@@ -496,21 +516,25 @@ do_update_port(struct impl *impl,
 	}
 }
 
+static int mix_clear_cb(void *item, void *data)
+{
+	if (item)
+		mix_clear(data, item);
+	return 0;
+}
+
 static void
 clear_port(struct impl *impl, struct port *port)
 {
-	struct mix *mix;
-
 	spa_log_debug(impl->log, "%p: clear port %p", impl, port);
 
 	do_update_port(impl, port,
 		       PW_CLIENT_NODE_PORT_UPDATE_PARAMS |
 		       PW_CLIENT_NODE_PORT_UPDATE_INFO, 0, NULL, NULL);
 
-	pw_array_for_each(mix, &port->mix)
-		mix_clear(impl, mix);
-	pw_array_clear(&port->mix);
-	pw_array_init(&port->mix, sizeof(struct mix) * 2);
+	pw_map_for_each(&port->mix, mix_clear_cb, impl);
+	pw_map_clear(&port->mix);
+	pw_map_init(&port->mix, 0, 2);
 
 	pw_map_insert_at(&impl->ports[port->direction], port->id, NULL);
 
@@ -604,6 +628,13 @@ impl_node_port_enum_params(void *object, int seq,
 	return found ? 0 : -ENOENT;
 }
 
+static int clear_buffers_cb(void *item, void *data)
+{
+	if (item)
+		clear_buffers(data, item);
+	return 0;
+}
+
 static int
 impl_node_port_set_param(void *object,
 			 enum spa_direction direction, uint32_t port_id,
@@ -612,7 +643,6 @@ impl_node_port_set_param(void *object,
 {
 	struct impl *impl = object;
 	struct port *port;
-	struct mix *mix;
 
 	spa_return_val_if_fail(impl != NULL, -EINVAL);
 
@@ -624,10 +654,9 @@ impl_node_port_set_param(void *object,
 			direction, port_id,
 			spa_debug_type_find_name(spa_type_param, id), id);
 
-	if (id == SPA_PARAM_Format) {
-		pw_array_for_each(mix, &port->mix)
-			clear_buffers(impl, mix);
-	}
+	if (id == SPA_PARAM_Format)
+		pw_map_for_each(&port->mix, clear_buffers_cb, impl);
+
 	if (impl->resource == NULL)
 		return param == NULL ? 0 : -EIO;
 
@@ -656,7 +685,7 @@ static int do_port_set_io(struct impl *impl,
 	if (port == NULL)
 		return data == NULL ? 0 : -EINVAL;
 
-	if ((mix = find_mix(port, mix_id)) == NULL || !mix->valid)
+	if ((mix = find_mix(port, mix_id)) == NULL)
 		return -EINVAL;
 
 	old = pw_mempool_find_tag(impl->client_pool, tag, sizeof(tag));
@@ -729,7 +758,7 @@ do_port_use_buffers(struct impl *impl,
 	if (direction == SPA_DIRECTION_OUTPUT)
 		mix_id = SPA_ID_INVALID;
 
-	if ((mix = find_mix(p, mix_id)) == NULL || !mix->valid)
+	if ((mix = find_mix(p, mix_id)) == NULL)
 		return -EINVAL;
 
 	clear_buffers(impl, mix);
@@ -1017,11 +1046,16 @@ static int client_node_port_buffers(void *data,
 	if (direction == SPA_DIRECTION_OUTPUT)
 		mix_id = SPA_ID_INVALID;
 
-	if ((mix = find_mix(p, mix_id)) == NULL || !mix->valid)
-		return -EINVAL;
+	if ((mix = find_mix(p, mix_id)) == NULL)
+		goto invalid;
 
 	if (mix->n_buffers != n_buffers)
-		return -EINVAL;
+		goto invalid;
+
+	for (i = 0; i < n_buffers; i++) {
+		if (mix->buffers[i].outbuf->n_datas != buffers[i]->n_datas)
+			goto invalid;
+	}
 
 	for (i = 0; i < n_buffers; i++) {
 		struct spa_buffer *oldbuf, *newbuf;
@@ -1032,9 +1066,6 @@ static int client_node_port_buffers(void *data,
 
 		spa_log_debug(impl->log, "buffer %d n_datas:%d", i, newbuf->n_datas);
 
-		if (oldbuf->n_datas != newbuf->n_datas)
-			return -EINVAL;
-
 		for (j = 0; j < b->buffer.n_datas; j++) {
 			struct spa_chunk *oldchunk = oldbuf->datas[j].chunk;
 			struct spa_data *d = &newbuf->datas[j];
@@ -1051,9 +1082,11 @@ static int client_node_port_buffers(void *data,
 					d->maxsize);
 		}
 	}
-	mix->n_buffers = n_buffers;
-
 	return 0;
+invalid:
+	for (i = 0; i < n_buffers; i++)
+		clear_buffer(impl, buffers[i]);
+	return -EINVAL;
 }
 
 static const struct pw_client_node_methods client_node_methods = {
@@ -1377,12 +1410,12 @@ static int port_init_mix(void *data, struct pw_impl_port_mix *mix)
 	uint32_t idx, pos, len;
 	struct pw_memblock *area;
 
-	if ((m = create_mix(impl, port, mix->port.port_id)) == NULL)
+	if ((m = create_mix(port, mix->port.port_id)) == NULL)
 		return -ENOMEM;
 
 	mix->id = pw_map_insert_new(&impl->io_map, NULL);
 	if (mix->id == SPA_ID_INVALID) {
-		m->valid = false;
+		free_mix(port, m);
 		return -errno;
 	}
 
@@ -1416,7 +1449,7 @@ static int port_init_mix(void *data, struct pw_impl_port_mix *mix)
 	return 0;
 no_mem:
 	pw_map_remove(&impl->io_map, mix->id);
-	m->valid = false;
+	free_mix(port, m);
 	return -ENOMEM;
 }
 
@@ -1429,7 +1462,7 @@ static int port_release_mix(void *data, struct pw_impl_port_mix *mix)
 	pw_log_debug("%p: remove mix id:%d io:%p",
 			impl, mix->id, mix->io);
 
-	if ((m = find_mix(port, mix->port.port_id)) == NULL || !m->valid)
+	if ((m = find_mix(port, mix->port.port_id)) == NULL)
 		return -EINVAL;
 
 	if (impl->resource && impl->resource->version >= 4)
@@ -1438,7 +1471,7 @@ static int port_release_mix(void *data, struct pw_impl_port_mix *mix)
 					 mix->port.port_id, SPA_ID_INVALID, NULL);
 
 	pw_map_remove(&impl->io_map, mix->id);
-	m->valid = false;
+	free_mix(port, m);
 
 	return 0;
 }
@@ -1565,12 +1598,12 @@ static void node_port_init(void *data, struct pw_impl_port *port)
 	p->direction = port->direction;
 	p->id = port->port_id;
 	p->impl = impl;
-	pw_array_init(&p->mix, sizeof(struct mix) * 2);
+	pw_map_init(&p->mix, 2, 2);
 	p->mix_node.iface = SPA_INTERFACE_INIT(
 			SPA_TYPE_INTERFACE_Node,
 			SPA_VERSION_NODE,
 			&impl_port_mix, p);
-	create_mix(impl, p, SPA_ID_INVALID);
+	create_mix(p, SPA_ID_INVALID);
 
 	pw_map_insert_at(&impl->ports[p->direction], p->id, p);
 	return;
diff --git a/src/modules/module-combine-stream.c b/src/modules/module-combine-stream.c
index 141115c8019054266d727c05548da6fd7dbcbf67..22a9fc8b907a36eb5961f5bcee8eb0f3ba30182f 100644
--- a/src/modules/module-combine-stream.c
+++ b/src/modules/module-combine-stream.c
@@ -29,7 +29,7 @@
 #include <pipewire/impl.h>
 #include <pipewire/i18n.h>
 
-/** \page page_module_combine_stream PipeWire Module: Combine Stream
+/** \page page_module_combine_stream Combine Stream
  *
  * The combine stream can make:
  *
@@ -40,6 +40,10 @@
  * rules. This makes it possible to combine static nodes or nodes based on certain
  * properties.
  *
+ * ## Module Name
+ *
+ * `libpipewire-module-combine-stream`
+ *
  * ## Module Options
  *
  * - `node.name`: a unique name for the stream
diff --git a/src/modules/module-echo-cancel.c b/src/modules/module-echo-cancel.c
index ab3698308986f37230682d004919ff316e2383be..7b33c47ec8ca072cf9b3cf34fc7176e710997bb9 100644
--- a/src/modules/module-echo-cancel.c
+++ b/src/modules/module-echo-cancel.c
@@ -40,7 +40,7 @@
 
 #include <pipewire/extensions/profiler.h>
 
-/** \page page_module_echo_cancel PipeWire Module: Echo Cancel
+/** \page page_module_echo_cancel Echo Cancel
  *
  * The `echo-cancel` module performs echo cancellation. The module creates
  * virtual `echo-cancel-capture` source and `echo-cancel-playback` sink
@@ -80,6 +80,10 @@
  *   This data then goes into the application (the conference application) and
  *   does not contain the echo from the other participants anymore.
  *
+ * ## Module Name
+ *
+ * `libpipewire-module-echo-cancel`
+ *
  * ## Module Options
  *
  * Options specific to the behavior of this module
diff --git a/src/modules/module-example-filter.c b/src/modules/module-example-filter.c
index fa8eb544452a3e1a49726cfe66cf77d7e179a377..3de26caeffb73da10334e609b39c46dc2d013d2d 100644
--- a/src/modules/module-example-filter.c
+++ b/src/modules/module-example-filter.c
@@ -22,11 +22,15 @@
 #include <pipewire/impl.h>
 #include <pipewire/extensions/profiler.h>
 
-/** \page page_module_example_filter PipeWire Module: Example Filter
+/** \page page_module_example_filter Example Filter
  *
  * The example filter is a good starting point for writing a custom
  * filter. We refer to the source code for more information.
  *
+ * ## Module Name
+ *
+ * `libpipewire-module-example-filter`
+ *
  * ## Module Options
  *
  * - `node.description`: a human readable name for the filter streams
diff --git a/src/modules/module-example-sink.c b/src/modules/module-example-sink.c
index 7413933c3c5882ecb1d8aace76fee57ed17c3443..772a45d6e10f17f08680d01e8360ff9ca4babb77 100644
--- a/src/modules/module-example-sink.c
+++ b/src/modules/module-example-sink.c
@@ -28,11 +28,15 @@
 #include <pipewire/impl.h>
 #include <pipewire/i18n.h>
 
-/** \page page_module_example_sink PipeWire Module: Example Sink
+/** \page page_module_example_sink Example Sink
  *
  * The example sink is a good starting point for writing a custom
  * sink. We refer to the source code for more information.
  *
+ * ## Module Name
+ *
+ * `libpipewire-module-example-sink`
+ *
  * ## Module Options
  *
  * - `node.name`: a unique name for the stream
diff --git a/src/modules/module-example-source.c b/src/modules/module-example-source.c
index e6be6717e3c5283618290b78c112072f00162524..0b06a06cbe3a4f61679b2a6dee6c49fc69d9e84b 100644
--- a/src/modules/module-example-source.c
+++ b/src/modules/module-example-source.c
@@ -28,11 +28,15 @@
 #include <pipewire/impl.h>
 #include <pipewire/i18n.h>
 
-/** \page page_module_example_source PipeWire Module: Example Source
+/** \page page_module_example_source Example Source
  *
  * The example source is a good starting point for writing a custom
  * source. We refer to the source code for more information.
  *
+ * ## Module Name
+ *
+ * `libpipewire-module-example-source`
+ *
  * ## Module Options
  *
  * - `node.name`: a unique name for the stream
diff --git a/src/modules/module-fallback-sink.c b/src/modules/module-fallback-sink.c
index 17f0f505c01d775bf7f882b49509f1b8e3f7e659..86982e6242845fc3e392eecfd96c86fbcf3e5278 100644
--- a/src/modules/module-fallback-sink.c
+++ b/src/modules/module-fallback-sink.c
@@ -19,10 +19,19 @@
 #include <pipewire/impl.h>
 #include <pipewire/i18n.h>
 
-/** \page page_module_fallback_sink PipeWire Module: Fallback Sink
+/** \page page_module_fallback_sink Fallback Sink
  *
  * Fallback sink, which appear dynamically when no other sinks are
  * present. This is only useful for Pulseaudio compatibility.
+ *
+ * ## Module Name
+ *
+ * `libpipewire-module-fallback-sink`
+ *
+ * ## Module Options
+ *
+ * - `sink.name`: sink name
+ * - `sink.description`: sink description
  */
 
 #define NAME "fallback-sink"
diff --git a/src/modules/module-ffado-driver.c b/src/modules/module-ffado-driver.c
index 22dc3562fd6eb656b3191a2da272ab5ea9d80a66..61c11fb643961aa016fc0283d2e00150533a4d38 100644
--- a/src/modules/module-ffado-driver.c
+++ b/src/modules/module-ffado-driver.c
@@ -31,11 +31,15 @@
 
 #include <libffado/ffado.h>
 
-/** \page page_module_ffado_driver PipeWire Module: FFADO firewire audio driver
+/** \page page_module_ffado_driver FFADO firewire audio driver
  *
  * The ffado-driver module provides a source or sink using the libffado library for
  * reading and writing to firewire audio devices.
  *
+ * ## Module Name
+ *
+ * `libpipewire-module-ffado-driver`
+ *
  * ## Module Options
  *
  * - `driver.mode`: the driver mode, sink|source|duplex, default duplex
diff --git a/src/modules/module-filter-chain.c b/src/modules/module-filter-chain.c
index 7a60b289756bc18ef72c7c9484a932b29926b5fc..b0d5888ba97dd7db8733f9a8b3792c4721f6acd0 100644
--- a/src/modules/module-filter-chain.c
+++ b/src/modules/module-filter-chain.c
@@ -34,7 +34,7 @@ PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
 #define PW_LOG_TOPIC_DEFAULT mod_topic
 
 /**
- * \page page_module_filter_chain PipeWire Module: Filter-Chain
+ * \page page_module_filter_chain Filter-Chain
  *
  * The filter-chain allows you to create an arbitrary processing graph
  * from LADSPA, LV2 and builtin filters. This filter can be made into a
@@ -48,6 +48,10 @@ PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
  * manager can manage the configuration and connection with the sinks and
  * sources automatically.
  *
+ * ## Module Name
+ *
+ * `libpipewire-module-filter-chain`
+ *
  * ## Module Options
  *
  * - `node.description`: a human readable name for the filter chain
@@ -2854,6 +2858,9 @@ static void impl_destroy(struct impl *impl)
 	graph_free(&impl->graph);
 	spa_list_consume(pl, &impl->plugin_func_list, link)
 		free_plugin_func(pl);
+
+	free(impl->silence_data);
+	free(impl->discard_data);
 	free(impl);
 }
 
@@ -2974,8 +2981,18 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
 	impl->quantum_limit = pw_properties_get_uint32(
 			pw_context_get_properties(impl->context),
 			"default.clock.quantum-limit", 8192u);
+
 	impl->silence_data = calloc(impl->quantum_limit, sizeof(float));
+	if (impl->silence_data == NULL) {
+		res = -errno;
+		goto error;
+	}
+
 	impl->discard_data = calloc(impl->quantum_limit, sizeof(float));
+	if (impl->discard_data == NULL) {
+		res = -errno;
+		goto error;
+	}
 
 	cpu_iface = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_CPU);
 	impl->dsp.cpu_flags = cpu_iface ? spa_cpu_get_flags(cpu_iface) : 0;
diff --git a/src/modules/module-jack-tunnel.c b/src/modules/module-jack-tunnel.c
index 7eff91448c4cc2f077a93fa26363682136d33c99..5100860115e872bca4002ef168e951b88353159e 100644
--- a/src/modules/module-jack-tunnel.c
+++ b/src/modules/module-jack-tunnel.c
@@ -30,7 +30,7 @@
 
 #include "module-jack-tunnel/weakjack.h"
 
-/** \page page_module_jack_tunnel PipeWire Module: JACK Tunnel
+/** \page page_module_jack_tunnel JACK Tunnel
  *
  * The jack-tunnel module provides a source or sink that tunnels all audio to
  * a JACK server.
@@ -39,6 +39,10 @@
  * automatically load the tunnel with the right parameters based on dbus
  * information.
  *
+ * ## Module Name
+ *
+ * `libpipewire-module-jack-tunnel`
+ *
  * ## Module Options
  *
  * - `jack.library`: the libjack to load, by default libjack.so.0 is searched in
diff --git a/src/modules/module-jackdbus-detect.c b/src/modules/module-jackdbus-detect.c
index 750212abf55383b87fb76c860aef2fdb1d6a4092..c4b1e01eff6ae851ee5c81dec2b03052fb8f33fc 100644
--- a/src/modules/module-jackdbus-detect.c
+++ b/src/modules/module-jackdbus-detect.c
@@ -25,11 +25,15 @@
 #include "pipewire/module.h"
 #include "pipewire/utils.h"
 
-/** \page page_module_jackdbus_detect PipeWire Module: JACK DBus detect
+/** \page page_module_jackdbus_detect JACK DBus detect
  *
  * Automaticall creates a sink/source when a jackdbus server is started
  * and connect to JACK.
  *
+ * ## Module Name
+ *
+ * `libpipewire-module-jackdbus-detect`
+ *
  * ## Module Options
  *
  * There are no module-specific options, all arguments are passed to
diff --git a/src/modules/module-link-factory.c b/src/modules/module-link-factory.c
index eaddf654fb8d067fe0741aba7bf84687e8c79160..56a0b963f2a405da058acae522e4db9910898804 100644
--- a/src/modules/module-link-factory.c
+++ b/src/modules/module-link-factory.c
@@ -14,7 +14,11 @@
 
 #include <pipewire/impl.h>
 
-/** \page page_module_link_factory PipeWire Module: Link Factory
+/** \page page_module_link_factory Link Factory
+ *
+ * ## Module Name
+ *
+ * `libpipewire-module-link-factory`
  */
 
 #define NAME "link-factory"
diff --git a/src/modules/module-loopback.c b/src/modules/module-loopback.c
index d1ad52589ec4d1343e1110914fb0e148070373d8..c3f30a753f626e4fe5f5afaa577f5db822ebd6b5 100644
--- a/src/modules/module-loopback.c
+++ b/src/modules/module-loopback.c
@@ -22,7 +22,7 @@
 #include <pipewire/impl.h>
 #include <pipewire/extensions/profiler.h>
 
-/** \page page_module_loopback PipeWire Module: Loopback
+/** \page page_module_loopback Loopback
  *
  * The loopback module passes the output of a capture stream unmodified to a playback stream.
  * It can be used to construct a link between a source and sink but also to
@@ -31,6 +31,10 @@
  * Because both ends of the loopback are built with streams, the session manager can
  * manage the configuration and connection with the sinks and sources.
  *
+ * ## Module Name
+ *
+ * `libpipewire-module-loopback`
+ *
  * ## Module Options
  *
  * - `node.description`: a human readable name for the loopback streams
diff --git a/src/modules/module-metadata.c b/src/modules/module-metadata.c
index 33ec61c8a49c03551e7b94ccb532a66db1a7575b..2d05fb83f77882857e06f00c8dd5a9bdc778d03f 100644
--- a/src/modules/module-metadata.c
+++ b/src/modules/module-metadata.c
@@ -15,7 +15,11 @@
 #include <pipewire/impl.h>
 #include <pipewire/extensions/metadata.h>
 
-/** \page page_module_metadata PipeWire Module: Metadata
+/** \page page_module_metadata Metadata
+ *
+ * ## Module Name
+ *
+ * `libpipewire-module-metadata`
  */
 
 #define NAME "metadata"
diff --git a/src/modules/module-netjack2-driver.c b/src/modules/module-netjack2-driver.c
index d3b4addeb0dfc5260eb5e1d086a4444d46929a65..88e6f84c2af3a7e1319edab2b1bf35fb787f6098 100644
--- a/src/modules/module-netjack2-driver.c
+++ b/src/modules/module-netjack2-driver.c
@@ -40,10 +40,14 @@
 #define IPTOS_DSCP(x) ((x) & IPTOS_DSCP_MASK)
 #endif
 
-/** \page page_module_netjack2_driver PipeWire Module: Netjack2 driver
+/** \page page_module_netjack2_driver Netjack2 driver
  *
  * The netjack2-driver module provides a source or sink that is following a
- * netjack2 driver.
+ * netjack2 manager.
+ *
+ * ## Module Name
+ *
+ * `libpipewire-module-netjack2-driver`
  *
  * ## Module Options
  *
diff --git a/src/modules/module-netjack2-manager.c b/src/modules/module-netjack2-manager.c
index 586e2cc4b6b0257d3cf3f03a958f968eb03b98c8..3afe6a888d9f0837294bd35603961aa8b254c02d 100644
--- a/src/modules/module-netjack2-manager.c
+++ b/src/modules/module-netjack2-manager.c
@@ -42,11 +42,15 @@
 #define IPTOS_DSCP(x) ((x) & IPTOS_DSCP_MASK)
 #endif
 
-/** \page page_module_netjack2_manager PipeWire Module: Netjack2 manager
+/** \page page_module_netjack2_manager Netjack2 manager
  *
  * The netjack2 manager module listens for new netjack2 driver messages and will
  * start a communication channel with them.
  *
+ * ## Module Name
+ *
+ * `libpipewire-module-netjack2-manager`
+ *
  * ## Module Options
  *
  * - `local.ifname = <str>`: interface name to use
diff --git a/src/modules/module-pipe-tunnel.c b/src/modules/module-pipe-tunnel.c
index c95edbfc31bec0b13298b9a5e03b268c03c481be..0fce7eb830811b3f177487656e1cc8be7ccbf642 100644
--- a/src/modules/module-pipe-tunnel.c
+++ b/src/modules/module-pipe-tunnel.c
@@ -32,14 +32,19 @@
 #include <pipewire/impl.h>
 #include <pipewire/i18n.h>
 
-/** \page page_module_pipe_tunnel PipeWire Module: Unix Pipe Tunnel
+/** \page page_module_pipe_tunnel Unix Pipe Tunnel
  *
  * The pipe-tunnel module provides a source or sink that tunnels all audio to
  * or from a unix pipe respectively.
  *
+ * ## Module Name
+ *
+ * `libpipewire-module-pipe-tunnel`
+ *
  * ## Module Options
  *
  * - `tunnel.mode`: the desired tunnel to create. (Default `playback`)
+ * - `tunnel.may-pause`: if the tunnel stream is allowed to pause on xrun
  * - `pipe.filename`: the filename of the pipe.
  * - `stream.props`: Extra properties for the local stream.
  *
@@ -55,6 +60,12 @@
  * When `tunnel.mode` is `source`, a source node is created. Samples read from
  * the the pipe will be made available on the source.
  *
+ * `tunnel.may-pause` allows the tunnel stream to become inactive (paused) when
+ * there is no data in the fifo or when the fifo is full. For `capture` and
+ * `playback` `tunnel.mode` this is by default true. For `source` and `sink`
+ * `tunnel.mode`, this is by default false. A paused stream will consume no
+ * CPU and will resume when the fifo becomes readable or writable again.
+ *
  * When `pipe.filename` is not given, a default fifo in `/tmp/fifo_input` or
  * `/tmp/fifo_output` will be created that can be written and read respectively,
  * depending on the selected `tunnel.mode`.
@@ -86,6 +97,7 @@
  * {   name = libpipewire-module-pipe-tunnel
  *     args = {
  *         tunnel.mode = playback
+ *         #tunnel.may-pause = true
  *         # Set the pipe name to tunnel to
  *         pipe.filename = "/tmp/fifo_output"
  *         #audio.format=<sample format>
@@ -128,6 +140,7 @@ PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
 			"( audio.channels=<number of channels> ) "		\
 			"( audio.position=<channel map> ) "			\
 			"( tunnel.mode=capture|playback|sink|source )"		\
+			"( tunnel.may-pause=<bool, if the stream can pause> )"	\
 			"( pipe.filename=<filename> )"				\
 			"( stream.props=<properties> ) "
 
@@ -141,6 +154,7 @@ static const struct spa_dict_item module_props[] = {
 
 struct impl {
 	struct pw_context *context;
+	struct pw_loop *main_loop;
 	struct pw_loop *data_loop;
 
 #define MODE_PLAYBACK	0
@@ -174,6 +188,8 @@ struct impl {
 	unsigned int do_disconnect:1;
 	unsigned int driving:1;
 	unsigned int have_sync:1;
+	unsigned int may_pause:1;
+	unsigned int paused:1;
 
 	struct spa_ringbuffer ring;
 	void *buffer;
@@ -262,7 +278,7 @@ static void stream_state_changed(void *d, enum pw_stream_state old,
 		break;
 	case PW_STREAM_STATE_PAUSED:
 		if (impl->direction == PW_DIRECTION_OUTPUT) {
-			pw_loop_update_io(impl->data_loop, impl->socket, 0);
+			pw_loop_update_io(impl->data_loop, impl->socket, impl->paused ? SPA_IO_IN : 0);
 			set_timeout(impl, 0);
 		}
 		break;
@@ -281,6 +297,26 @@ static void stream_state_changed(void *d, enum pw_stream_state old,
 	}
 }
 
+static int do_pause(struct spa_loop *loop, bool async, uint32_t seq, const void *data,
+		size_t size, void *user_data)
+{
+	struct impl *impl = user_data;
+	const bool *paused = data;
+	pw_log_info("set paused: %d", *paused);
+	impl->paused = *paused;
+	pw_stream_set_active(impl->stream, !*paused);
+	return 0;
+}
+
+static void pause_stream(struct impl *impl, bool paused)
+{
+	if (!impl->may_pause)
+		return;
+	if (impl->direction == PW_DIRECTION_INPUT)
+		pw_loop_update_io(impl->data_loop, impl->socket, paused ? SPA_IO_OUT : 0);
+	pw_loop_invoke(impl->main_loop, do_pause, 1, &paused, sizeof(bool), false, impl);
+}
+
 static void playback_stream_process(void *data)
 {
 	struct impl *impl = data;
@@ -308,8 +344,8 @@ static void playback_stream_process(void *data)
 					continue;
 				} else if (errno == EAGAIN || errno == EWOULDBLOCK) {
 					/* Don't continue writing */
-					pw_log_debug("pipe (%s) overrun: %m",
-							impl->filename);
+					pw_log_debug("pipe (%s) overrun: %m", impl->filename);
+					pause_stream(impl, true);
 					break;
 				} else {
 					pw_log_warn("Failed to write to pipe (%s): %m",
@@ -370,8 +406,10 @@ static void capture_stream_process(void *data)
 
 	if (avail < (int32_t)size) {
 		memset(bd->data, 0, size);
-		if (avail > 0)
+		if (avail >= 0) {
 			pw_log_warn("underrun %d < %u", avail, size);
+			pause_stream(impl, true);
+		}
 		impl->have_sync = false;
 	}
 	if (avail > (int32_t)RINGBUFFER_SIZE) {
@@ -456,6 +494,8 @@ static int create_stream(struct impl *impl)
 	params[n_params++] = spa_format_audio_raw_build(&b,
 			SPA_PARAM_EnumFormat, &impl->info);
 
+	impl->paused = false;
+
 	if ((res = pw_stream_connect(impl->stream,
 			impl->direction,
 			PW_ID_ANY,
@@ -550,6 +590,8 @@ static void on_pipe_io(void *data, int fd, uint32_t mask)
 		pw_loop_update_io(impl->data_loop, impl->socket, 0);
 		return;
 	}
+	if (impl->paused)
+		pause_stream(impl, false);
 	if (mask & SPA_IO_IN)
 		handle_pipe_read(impl);
 }
@@ -839,6 +881,7 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
 
 	impl->module = module;
 	impl->context = context;
+	impl->main_loop = pw_context_get_main_loop(context);
 	data_loop = pw_context_get_data_loop(context);
 	impl->data_loop = pw_data_loop_get_loop(data_loop);
 
@@ -848,22 +891,28 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
 	if (spa_streq(str, "capture")) {
 		impl->mode = MODE_CAPTURE;
 		impl->direction = PW_DIRECTION_INPUT;
+		impl->may_pause = true;
 	} else if (spa_streq(str, "playback")) {
 		impl->mode = MODE_PLAYBACK;
 		impl->direction = PW_DIRECTION_OUTPUT;
+		impl->may_pause = true;
 	}else if (spa_streq(str, "sink")) {
 		impl->mode = MODE_SINK;
 		impl->direction = PW_DIRECTION_INPUT;
+		impl->may_pause = false;
 		media_class = "Audio/Sink";
 	} else if (spa_streq(str, "source")) {
 		impl->mode = MODE_SOURCE;
 		impl->direction = PW_DIRECTION_OUTPUT;
+		impl->may_pause = false;
 		media_class = "Audio/Source";
 	} else {
 		pw_log_error("invalid tunnel.mode '%s'", str);
 		res = -EINVAL;
 		goto error;
 	}
+	if ((str = pw_properties_get(props, "tunnel.may-pause")) != NULL)
+		impl->may_pause = spa_atob(str);
 
 	if (pw_properties_get(props, PW_KEY_NODE_VIRTUAL) == NULL)
 		pw_properties_set(props, PW_KEY_NODE_VIRTUAL, "true");
diff --git a/src/modules/module-portal.c b/src/modules/module-portal.c
index de45e445b3f819bd986f5bb4772bf5eb45bf6f50..f37db6b7fb17aba6ea45ea477a3333763153ef51 100644
--- a/src/modules/module-portal.c
+++ b/src/modules/module-portal.c
@@ -25,7 +25,7 @@
 #include "pipewire/module.h"
 #include "pipewire/utils.h"
 
-/** \page page_module_portal PipeWire Module: Portal
+/** \page page_module_portal Portal
  *
  * The `portal` module performs access control management for clients started
  * inside an XDG portal.
@@ -42,6 +42,10 @@
  *
  * Clients connecting from other PIDs are ignored by this module.
  *
+ * ## Module Name
+ *
+ * `libpipewire-module-portal`
+ *
  * ## Module Options
  *
  * There are no module-specific options.
diff --git a/src/modules/module-profiler.c b/src/modules/module-profiler.c
index 490e031af84ea4b7c19cebd9e8f303ad5cfcce99..04ba26775be1bcef50a1b04dfbf8b576ff4502d3 100644
--- a/src/modules/module-profiler.c
+++ b/src/modules/module-profiler.c
@@ -22,7 +22,7 @@
 #include <pipewire/impl.h>
 #include <pipewire/extensions/profiler.h>
 
-/** \page page_module_profiler PipeWire Module: Profiler
+/** \page page_module_profiler Profiler
  *
  * The profiler module provides a Profiler interface for applications that
  * can be used to receive profiling information.
@@ -30,6 +30,10 @@
  * Use tools like pw-top and pw-profiler to collect profiling information
  * about the pipewire graph.
  *
+ * ## Module Name
+ *
+ * `libpipewire-module-profiler`
+ *
  * ## Example configuration
  *
  * The module has no arguments and is usually added to the config file of
diff --git a/src/modules/module-protocol-native.c b/src/modules/module-protocol-native.c
index 486eb5561bbe2c63d539eabe7b08cbb89cf1850f..63cc149d00dc327859d1e4d159d3eaf809d6bc31 100644
--- a/src/modules/module-protocol-native.c
+++ b/src/modules/module-protocol-native.c
@@ -63,7 +63,7 @@ PW_LOG_TOPIC(mod_topic_connection, "conn." NAME);
 #include <spa/debug/pod.h>
 #include <spa/debug/types.h>
 
-/** \page page_module_protocol_native PipeWire Module: Protocol Native
+/** \page page_module_protocol_native Protocol Native
  *
  * The native protocol module implements the PipeWire communication between
  * a client and a server using unix local sockets.
@@ -71,6 +71,10 @@ PW_LOG_TOPIC(mod_topic_connection, "conn." NAME);
  * Normally this module is loaded in both client and server config files
  * so that they cam communicate.
  *
+ * ## Module Name
+ *
+ * `libpipewire-module-protocol-native`
+ *
  * ## Module Options
  *
  * The module supports the following arguments:
@@ -1489,6 +1493,7 @@ static int impl_ext_end_proxy(struct pw_proxy *proxy,
 {
 	struct pw_core *core = proxy->core;
 	struct client *impl = SPA_CONTAINER_OF(core->conn, struct client, this);
+	ensure_loop(impl->context->main_loop);
 	assert_single_pod(builder);
 	marshal_core_footers(&impl->footer_state, core, builder);
 	return core->send_seq = pw_protocol_native_connection_end(impl->connection, builder);
@@ -1518,6 +1523,7 @@ static int impl_ext_end_resource(struct pw_resource *resource,
 {
 	struct client_data *data = resource->client->user_data;
 	struct pw_impl_client *client = resource->client;
+	ensure_loop(client->context->main_loop);
 	assert_single_pod(builder);
 	marshal_client_footers(&data->footer_state, client, builder);
 	return client->send_seq = pw_protocol_native_connection_end(data->connection, builder);
diff --git a/src/modules/module-protocol-pulse.c b/src/modules/module-protocol-pulse.c
index ab5f8f921ea6b82488a2231a12be79489bc778c2..e50dc752cf833d27a052d4a427cfe33edae872d2 100644
--- a/src/modules/module-protocol-pulse.c
+++ b/src/modules/module-protocol-pulse.c
@@ -18,7 +18,7 @@
 
 #include "module-protocol-pulse/pulse-server.h"
 
-/** \page page_module_protocol_pulse PipeWire Module: Protocol Pulse
+/** \page page_module_protocol_pulse Protocol Pulse
  *
  * This module implements a complete PulseAudio server on top of
  * PipeWire.  This is only the server implementation, client are expected
@@ -33,6 +33,10 @@
  * The pulse server implements a sample cache that is otherwise not
  * available in PipeWire.
  *
+ * ## Module Name
+ *
+ * `libpipewire-module-protocol-pulse`
+ *
  * ## Module Options
  *
  * The module arguments can be the contents of the pulse.properties but
@@ -86,7 +90,7 @@
  *\endcode
  *
  * The addresses the server listens on when starting. Uncomment the `tcp:4713` entry to also
- * make the server listen on a tcp socket. This is equivalent to loading `module-native-protocol-tcp`.
+ * make the server listen on a tcp socket. This is equivalent to loading `libpipewire-module-native-protocol-tcp`.
  *
  * There is also a slightly more verbose syntax with more options:
  *
diff --git a/src/modules/module-protocol-pulse/collect.c b/src/modules/module-protocol-pulse/collect.c
index f7c1821461f316c1c0d54ad47e3004b7a9f05f8e..bf05122f21765074207df1a085ae7baea43c0976 100644
--- a/src/modules/module-protocol-pulse/collect.c
+++ b/src/modules/module-protocol-pulse/collect.c
@@ -65,7 +65,7 @@ uint32_t id_to_index(struct pw_manager *m, uint32_t id)
 	return SPA_ID_INVALID;
 }
 
-bool collect_is_linked(struct pw_manager *m, uint32_t id, enum pw_direction direction)
+static bool collect_is_linked(struct pw_manager *m, uint32_t id, enum pw_direction direction)
 {
 	struct pw_manager_object *o;
 	uint32_t in_node, out_node;
@@ -291,7 +291,7 @@ static void collect_device_info(struct pw_manager_object *device, struct pw_mana
 }
 
 static void update_device_info(struct pw_manager *manager, struct pw_manager_object *o,
-		enum pw_direction direction, bool monitor, struct defs *defs)
+		enum pw_direction direction, bool monitor, struct defs *defs, bool stream)
 {
 	const char *str;
 	const char *key = monitor ? "device.info.monitor" : "device.info";
@@ -313,6 +313,12 @@ static void update_device_info(struct pw_manager *manager, struct pw_manager_obj
 	}
 	collect_device_info(o, card, &di, monitor, defs);
 
+	di.state = node_state(info->state);
+	/* running sink/source that is not linked is reported as idle */
+	if (!stream && di.state == STATE_RUNNING &&
+	    !collect_is_linked(manager, o->id, pw_direction_reverse(direction)))
+		di.state = STATE_IDLE;
+
 	dev_info = pw_manager_object_get_data(o, key);
 	if (dev_info) {
 		if (memcmp(dev_info, &di, sizeof(di)) != 0) {
@@ -586,16 +592,16 @@ void update_object_info(struct pw_manager *manager, struct pw_manager_object *o,
 		struct defs *defs)
 {
 	if (pw_manager_object_is_sink(o)) {
-		update_device_info(manager, o, PW_DIRECTION_OUTPUT, false, defs);
-		update_device_info(manager, o, PW_DIRECTION_OUTPUT, true, defs);
+		update_device_info(manager, o, PW_DIRECTION_OUTPUT, false, defs, false);
+		update_device_info(manager, o, PW_DIRECTION_OUTPUT, true, defs, false);
 	}
 	if (pw_manager_object_is_source(o)) {
-		update_device_info(manager, o, PW_DIRECTION_INPUT, false, defs);
+		update_device_info(manager, o, PW_DIRECTION_INPUT, false, defs, false);
 	}
 	if (pw_manager_object_is_source_output(o)) {
-		update_device_info(manager, o, PW_DIRECTION_INPUT, false, defs);
+		update_device_info(manager, o, PW_DIRECTION_INPUT, false, defs, true);
 	}
 	if (pw_manager_object_is_sink_input(o)) {
-		update_device_info(manager, o, PW_DIRECTION_OUTPUT, false, defs);
+		update_device_info(manager, o, PW_DIRECTION_OUTPUT, false, defs, true);
 	}
 }
diff --git a/src/modules/module-protocol-pulse/collect.h b/src/modules/module-protocol-pulse/collect.h
index 40b08d62f375abac8bf971ffa77e8b27542ca85e..d5cfe95379bba4a7423a5c3ff9ceb697dae700e1 100644
--- a/src/modules/module-protocol-pulse/collect.h
+++ b/src/modules/module-protocol-pulse/collect.h
@@ -42,6 +42,7 @@ void update_object_info(struct pw_manager *manager, struct pw_manager_object *o,
 
 struct device_info {
 	uint32_t direction;
+	int state;
 
 	struct sample_spec ss;
 	struct channel_map map;
@@ -59,6 +60,7 @@ struct device_info {
 #define DEVICE_INFO_INIT(_dir) \
 	(struct device_info) {				\
 		.direction = _dir,			\
+		.state = STATE_INIT,			\
 		.ss = SAMPLE_SPEC_INIT,			\
 		.map = CHANNEL_MAP_INIT,		\
 		.volume_info = VOLUME_INFO_INIT,	\
@@ -146,6 +148,5 @@ uint32_t find_port_index(struct pw_manager_object *card, uint32_t direction, con
 struct pw_manager_object *find_peer_for_link(struct pw_manager *m,
 		struct pw_manager_object *o, uint32_t id, enum pw_direction direction);
 struct pw_manager_object *find_linked(struct pw_manager *m, uint32_t id, enum pw_direction direction);
-bool collect_is_linked(struct pw_manager *m, uint32_t id, enum pw_direction direction);
 
 #endif
diff --git a/src/modules/module-protocol-pulse/modules/module-alsa-sink.c b/src/modules/module-protocol-pulse/modules/module-alsa-sink.c
index e07662ad7b9051d741575c99b2af9523700c4587..49ca9404bf1a30a76f3f2667b3c6b2ef2af66d23 100644
--- a/src/modules/module-protocol-pulse/modules/module-alsa-sink.c
+++ b/src/modules/module-protocol-pulse/modules/module-alsa-sink.c
@@ -7,6 +7,43 @@
 #include "../manager.h"
 #include "../module.h"
 
+/** \page page_pulse_module_alsa_sink ALSA Sink
+ *
+ * ## Module Name
+ *
+ * `module-alsa-sink`
+ *
+ * ## Module Options
+ *
+ * @pulse_module_options@
+ */
+
+static const char *const pulse_module_options =
+	"name=<name of the sink, to be prefixed> "
+	"sink_name=<name for the sink> "
+	"sink_properties=<properties for the sink> "
+	"namereg_fail=<when false attempt to synthesise new sink_name if it is already taken> "
+	"device=<ALSA device> "
+	"device_id=<ALSA card index> "
+	"format=<sample format> "
+	"rate=<sample rate> "
+	"alternate_rate=<alternate sample rate> "
+	"channels=<number of channels> "
+	"channel_map=<channel map> "
+	"fragments=<number of fragments> "
+	"fragment_size=<fragment size> "
+	"mmap=<enable memory mapping?> "
+	"tsched=<enable system timer based scheduling mode?> "
+	"tsched_buffer_size=<buffer size when using timer based scheduling> "
+	"tsched_buffer_watermark=<lower fill watermark> "
+	"ignore_dB=<ignore dB information from the device?> "
+	"control=<name of mixer control, or name and index separated by a comma> "
+	"rewind_safeguard=<number of bytes that cannot be rewound> " 
+	"deferred_volume=<Synchronize software and hardware volume changes to avoid momentary jumps?> "
+	"deferred_volume_safety_margin=<usec adjustment depending on volume direction> "
+	"deferred_volume_extra_delay=<usec adjustment to HW volume changes> "
+	"fixed_latency_range=<disable latency range changes on underrun?> ";
+
 #define NAME "alsa-sink"
 
 #define DEFAULT_DEVICE "default"
@@ -131,31 +168,7 @@ static int module_alsa_sink_unload(struct module *module)
 static const struct spa_dict_item module_alsa_sink_info[] = {
 	{ PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
 	{ PW_KEY_MODULE_DESCRIPTION, "An ALSA sink" },
-	{ PW_KEY_MODULE_USAGE,
-		"name=<name of the sink, to be prefixed> "
-		"sink_name=<name for the sink> "
-		"sink_properties=<properties for the sink> "
-		"namereg_fail=<when false attempt to synthesise new sink_name if it is already taken> "
-		"device=<ALSA device> "
-		"device_id=<ALSA card index> "
-		"format=<sample format> "
-		"rate=<sample rate> "
-		"alternate_rate=<alternate sample rate> "
-		"channels=<number of channels> "
-		"channel_map=<channel map> "
-		"fragments=<number of fragments> "
-		"fragment_size=<fragment size> "
-		"mmap=<enable memory mapping?> "
-		"tsched=<enable system timer based scheduling mode?> "
-		"tsched_buffer_size=<buffer size when using timer based scheduling> "
-		"tsched_buffer_watermark=<lower fill watermark> "
-		"ignore_dB=<ignore dB information from the device?> "
-		"control=<name of mixer control, or name and index separated by a comma> "
-		"rewind_safeguard=<number of bytes that cannot be rewound> "
-		"deferred_volume=<Synchronize software and hardware volume changes to avoid momentary jumps?> "
-		"deferred_volume_safety_margin=<usec adjustment depending on volume direction> "
-		"deferred_volume_extra_delay=<usec adjustment to HW volume changes> "
-		"fixed_latency_range=<disable latency range changes on underrun?>" },
+	{ PW_KEY_MODULE_USAGE, pulse_module_options },
 	{ PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
 };
 
diff --git a/src/modules/module-protocol-pulse/modules/module-alsa-source.c b/src/modules/module-protocol-pulse/modules/module-alsa-source.c
index 4fe5f51e7895a3d4f6b038b2d770cad7d0009ff5..1ffce175003f30ac96642a1f1a7580310498df4d 100644
--- a/src/modules/module-protocol-pulse/modules/module-alsa-source.c
+++ b/src/modules/module-protocol-pulse/modules/module-alsa-source.c
@@ -7,6 +7,43 @@
 #include "../manager.h"
 #include "../module.h"
 
+/** \page page_pulse_module_alsa_source ALSA Source
+ *
+ * ## Module Name
+ *
+ * `module-alsa-source`
+ *
+ * ## Module Options
+ *
+ * @pulse_module_options@
+ */
+
+static const char *const pulse_module_options =
+		"name=<name of the source, to be prefixed> "
+		"source_name=<name for the source> "
+		"source_properties=<properties for the source> "
+		"namereg_fail=<when false attempt to synthesise new source_name if it is already taken> "
+		"device=<ALSA device> "
+		"device_id=<ALSA card index> "
+		"format=<sample format> "
+		"rate=<sample rate> "
+		"alternate_rate=<alternate sample rate> "
+		"channels=<number of channels> "
+		"channel_map=<channel map> "
+		"fragments=<number of fragments> "
+		"fragment_size=<fragment size> "
+		"mmap=<enable memory mapping?> "
+		"tsched=<enable system timer based scheduling mode?> "
+		"tsched_buffer_size=<buffer size when using timer based scheduling> "
+		"tsched_buffer_watermark=<lower fill watermark> "
+		"ignore_dB=<ignore dB information from the device?> "
+		"control=<name of mixer control, or name and index separated by a comma> "
+		"rewind_safeguard=<number of bytes that cannot be rewound> "
+		"deferred_volume=<Synchronize software and hardware volume changes to avoid momentary jumps?> "
+		"deferred_volume_safety_margin=<usec adjustment depending on volume direction> "
+		"deferred_volume_extra_delay=<usec adjustment to HW volume changes> "
+		"fixed_latency_range=<disable latency range changes on underrun?>";
+
 #define NAME "alsa-source"
 
 #define DEFAULT_DEVICE "default"
@@ -131,31 +168,7 @@ static int module_alsa_source_unload(struct module *module)
 static const struct spa_dict_item module_alsa_source_info[] = {
 	{ PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
 	{ PW_KEY_MODULE_DESCRIPTION, "An ALSA source" },
-	{ PW_KEY_MODULE_USAGE,
-		"name=<name of the source, to be prefixed> "
-		"source_name=<name for the source> "
-		"source_properties=<properties for the source> "
-		"namereg_fail=<when false attempt to synthesise new source_name if it is already taken> "
-		"device=<ALSA device> "
-		"device_id=<ALSA card index> "
-		"format=<sample format> "
-		"rate=<sample rate> "
-		"alternate_rate=<alternate sample rate> "
-		"channels=<number of channels> "
-		"channel_map=<channel map> "
-		"fragments=<number of fragments> "
-		"fragment_size=<fragment size> "
-		"mmap=<enable memory mapping?> "
-		"tsched=<enable system timer based scheduling mode?> "
-		"tsched_buffer_size=<buffer size when using timer based scheduling> "
-		"tsched_buffer_watermark=<lower fill watermark> "
-		"ignore_dB=<ignore dB information from the device?> "
-		"control=<name of mixer control, or name and index separated by a comma> "
-		"rewind_safeguard=<number of bytes that cannot be rewound> "
-		"deferred_volume=<Synchronize software and hardware volume changes to avoid momentary jumps?> "
-		"deferred_volume_safety_margin=<usec adjustment depending on volume direction> "
-		"deferred_volume_extra_delay=<usec adjustment to HW volume changes> "
-		"fixed_latency_range=<disable latency range changes on underrun?>" },
+	{ PW_KEY_MODULE_USAGE, pulse_module_options },
 	{ PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
 };
 
diff --git a/src/modules/module-protocol-pulse/modules/module-always-sink.c b/src/modules/module-protocol-pulse/modules/module-always-sink.c
index 041f767289bd2bb0b75a3ccf3e3f416aeefd4b0a..e0ba258f6b22b1927f3e184898b42438e360e76f 100644
--- a/src/modules/module-protocol-pulse/modules/module-always-sink.c
+++ b/src/modules/module-protocol-pulse/modules/module-always-sink.c
@@ -6,6 +6,19 @@
 
 #include "../module.h"
 
+/** \page page_pulse_module_always_sink Always Sink
+ *
+ * ## Module Name
+ *
+ * `module-always-sink`
+ *
+ * ## Module Options
+ *
+ * @pulse_module_options@
+ */
+
+static const char *const pulse_module_options = "sink_name=<name of sink>";
+
 #define NAME "always-sink"
 
 PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
@@ -77,7 +90,7 @@ static int module_always_sink_unload(struct module *module)
 static const struct spa_dict_item module_always_sink_info[] = {
 	{ PW_KEY_MODULE_AUTHOR, "Pauli Virtanen <pav@iki.fi>" },
 	{ PW_KEY_MODULE_DESCRIPTION, "Always keeps at least one sink loaded even if it's a null one" },
-	{ PW_KEY_MODULE_USAGE,  "sink_name=<name of sink>" },
+	{ PW_KEY_MODULE_USAGE, pulse_module_options },
 	{ PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
 };
 
diff --git a/src/modules/module-protocol-pulse/modules/module-combine-sink.c b/src/modules/module-protocol-pulse/modules/module-combine-sink.c
index 62c544be46a697f07ca9b6e7ecd1ff49d645fd68..4951b4e6b647bce752a78f2388e50a134b7d87f4 100644
--- a/src/modules/module-protocol-pulse/modules/module-combine-sink.c
+++ b/src/modules/module-protocol-pulse/modules/module-combine-sink.c
@@ -12,6 +12,31 @@
 #include "../manager.h"
 #include "../module.h"
 
+/** \page page_pulse_module_combine_sink Combine Sink
+ *
+ * ## Module Name
+ *
+ * `module-combine-sink`
+ *
+ * ## Module Options
+ *
+ * @pulse_module_options@
+ *
+ * ## See Also
+ *
+ * \ref page_module_combine_stream "libpipewire-module-combine-stream"
+ */
+
+static const char *const pulse_module_options =
+	"sink_name=<name of the sink> "
+	"sink_properties=<properties for the sink> "
+	"sinks=<sinks to combine> "
+	"rate=<sample rate> "
+	"channels=<number of channels> "
+	"channel_map=<channel map> "
+	"remix=<remix channels> "
+	"latency_compensate=<bool> ";
+
 #define NAME "combine-sink"
 
 PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
@@ -24,15 +49,7 @@ PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
 static const struct spa_dict_item module_combine_sink_info[] = {
 	{ PW_KEY_MODULE_AUTHOR, "Arun Raghavan <arun@asymptotic.io>" },
 	{ PW_KEY_MODULE_DESCRIPTION, "Combine multiple sinks into a single sink" },
-	{ PW_KEY_MODULE_USAGE, "sink_name=<name of the sink> "
-				"sink_properties=<properties for the sink> "
-				/* not a great name, but for backwards compatibility... */
-				"slaves=<sinks to combine> "
-				"rate=<sample rate> "
-				"channels=<number of channels> "
-				"channel_map=<channel map> "
-				"remix=<remix channels> "
-				"latency_compensate=<bool> " },
+	{ PW_KEY_MODULE_USAGE, pulse_module_options },
 	{ PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
 };
 
@@ -268,8 +285,10 @@ static int module_combine_sink_prepare(struct module * const module)
 	if ((str = pw_properties_get(props, "sink_properties")) != NULL)
 		module_args_add_props(combine_props, str);
 
-	if ((str = pw_properties_get(props, "slaves")) != NULL) {
+	if ((str = pw_properties_get(props, "sinks")) != NULL ||
+			(str = pw_properties_get(props, "slaves")) != NULL) {
 		sink_names = pw_split_strv(str, ",", MAX_SINKS, &num_sinks);
+		pw_properties_set(props, "sinks", NULL);
 		pw_properties_set(props, "slaves", NULL);
 	}
 	if ((str = pw_properties_get(props, "remix")) != NULL) {
diff --git a/src/modules/module-protocol-pulse/modules/module-echo-cancel.c b/src/modules/module-protocol-pulse/modules/module-echo-cancel.c
index cd089af22e5c351374a1f2a315fb7bb1fdad532b..9737266db774cb6db010ea1bfbccd4bd43eb43e1 100644
--- a/src/modules/module-protocol-pulse/modules/module-echo-cancel.c
+++ b/src/modules/module-protocol-pulse/modules/module-echo-cancel.c
@@ -11,6 +11,45 @@
 #include "../defs.h"
 #include "../module.h"
 
+/** \page page_pulse_module_echo_cancel Echo Cancel
+ *
+ * ## Module Name
+ *
+ * `module-echo-cancel`
+ *
+ * ## Module Options
+ *
+ * @pulse_module_options@
+ *
+ * ## See Also
+ *
+ * \ref page_module_echo_cancel "libpipewire-module-echo-cancel"
+ */
+
+static const char *const pulse_module_options =
+	"source_name=<name for the source> "
+	"source_properties=<properties for the source> "
+	"source_master=<name of source to filter> "
+	"sink_name=<name for the sink> "
+	"sink_properties=<properties for the sink> "
+	"sink_master=<name of sink to filter> "
+	"rate=<sample rate> "
+	"channels=<number of channels> "
+	"channel_map=<channel map> "
+	"aec_method=<implementation to use> "
+	"aec_args=<parameters for the AEC engine> ";
+#if 0
+	/* These are not implemented because they don't
+	 * really make sense in the PipeWire context */
+	"format=<sample format> "
+	"adjust_time=<how often to readjust rates in s> "
+	"adjust_threshold=<how much drift to readjust after in ms> "
+	"autoloaded=<set if this module is being loaded automatically> "
+	"save_aec=<save AEC data in /tmp> "
+	"use_volume_sharing=<yes or no> "
+	"use_master_format=<yes or no> "
+#endif
+
 #define NAME "echo-cancel"
 
 PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
@@ -114,29 +153,7 @@ static int module_echo_cancel_unload(struct module *module)
 static const struct spa_dict_item module_echo_cancel_info[] = {
 	{ PW_KEY_MODULE_AUTHOR, "Arun Raghavan <arun@asymptotic.io>" },
 	{ PW_KEY_MODULE_DESCRIPTION, "Acoustic echo canceller" },
-	{ PW_KEY_MODULE_USAGE, "source_name=<name for the source> "
-				"source_properties=<properties for the source> "
-				"source_master=<name of source to filter> "
-				"sink_name=<name for the sink> "
-				"sink_properties=<properties for the sink> "
-				"sink_master=<name of sink to filter> "
-				"rate=<sample rate> "
-				"channels=<number of channels> "
-				"channel_map=<channel map> "
-				"aec_method=<implementation to use> "
-				"aec_args=<parameters for the AEC engine> "
-#if 0
-				/* These are not implemented because they don't
-				 * really make sense in the PipeWire context */
-				"format=<sample format> "
-				"adjust_time=<how often to readjust rates in s> "
-				"adjust_threshold=<how much drift to readjust after in ms> "
-				"autoloaded=<set if this module is being loaded automatically> "
-				"save_aec=<save AEC data in /tmp> "
-				"use_volume_sharing=<yes or no> "
-				"use_master_format=<yes or no> "
-#endif
-	},
+	{ PW_KEY_MODULE_USAGE, pulse_module_options },
 	{ PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
 };
 
diff --git a/src/modules/module-protocol-pulse/modules/module-gsettings.c b/src/modules/module-protocol-pulse/modules/module-gsettings.c
index f9f713a488c27ae4489f2211c6c7cf4a5b60ad04..1968cc10181fba352eeee12e46589fc96f51586b 100644
--- a/src/modules/module-protocol-pulse/modules/module-gsettings.c
+++ b/src/modules/module-protocol-pulse/modules/module-gsettings.c
@@ -11,6 +11,17 @@
 
 #include "../module.h"
 
+/** \page page_pulse_module_gsettings GSettings
+ *
+ * ## Module Name
+ *
+ * `module-gsettings`
+ *
+ * ## Module Options
+ *
+ * No options.
+ */
+
 #define NAME "gsettings"
 
 PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
diff --git a/src/modules/module-protocol-pulse/modules/module-jackdbus-detect.c b/src/modules/module-protocol-pulse/modules/module-jackdbus-detect.c
index 33d7ba341a05a8029a506c32de41ecfe87faa88d..c015673f9cf32c5c3c9ec55091c8f59f53183a51 100644
--- a/src/modules/module-protocol-pulse/modules/module-jackdbus-detect.c
+++ b/src/modules/module-protocol-pulse/modules/module-jackdbus-detect.c
@@ -8,6 +8,35 @@
 #include "../defs.h"
 #include "../module.h"
 
+/** \page page_pulse_module_jackdbus_detect JackDBus Detect
+ *
+ * ## Module Name
+ *
+ * `module-jackdbus-detect`
+ *
+ * ## Module Options
+ *
+ * @pulse_module_options@
+ *
+ * ## See Also
+ *
+ * \ref page_module_jackdbus_detect "libpipewire-module-jackdbus-detect"
+ */
+
+static const char *const pulse_module_options =
+	"channels=<number of channels> "
+	"sink_name=<name for the sink> "
+	"sink_properties=<properties for the sink> "
+	"sink_client_name=<jack client name> "
+	"sink_channels=<number of channels> "
+	"sink_channel_map=<channel map> "
+	"source_name=<name for the source> "
+	"source_properties=<properties for the source> "
+	"source_client_name=<jack client name> "
+	"source_channels=<number of channels> "
+	"source_channel_map=<channel map> "
+	"connect=<connect ports?>";
+
 #define NAME "jackdbus-detect"
 
 PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
@@ -93,19 +122,7 @@ static int module_jackdbus_detect_unload(struct module *module)
 static const struct spa_dict_item module_jackdbus_detect_info[] = {
 	{ PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.con>" },
 	{ PW_KEY_MODULE_DESCRIPTION, "Creates a JACK client when jackdbus is started" },
-	{ PW_KEY_MODULE_USAGE,
-		"channels=<number of channels> "
-		"sink_name=<name for the sink> "
-		"sink_properties=<properties for the sink> "
-		"sink_client_name=<jack client name> "
-		"sink_channels=<number of channels> "
-		"sink_channel_map=<channel map> "
-		"source_name=<name for the source> "
-		"source_properties=<properties for the source> "
-		"source_client_name=<jack client name> "
-		"source_channels=<number of channels> "
-		"source_channel_map=<channel map> "
-		"connect=<connect ports?>" },
+	{ PW_KEY_MODULE_USAGE, pulse_module_options },
 	{ PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
 };
 
diff --git a/src/modules/module-protocol-pulse/modules/module-ladspa-sink.c b/src/modules/module-protocol-pulse/modules/module-ladspa-sink.c
index 5dd26567b93d9e4680a7fa9b4864c307fff245e7..50c368398e22c45844d955c570d9a089a4e8107a 100644
--- a/src/modules/module-protocol-pulse/modules/module-ladspa-sink.c
+++ b/src/modules/module-protocol-pulse/modules/module-ladspa-sink.c
@@ -11,6 +11,37 @@
 #include "../defs.h"
 #include "../module.h"
 
+/** \page page_pulse_module_ladspa_sink LADSPA Sink
+ *
+ * ## Module Name
+ *
+ * `module-ladspa-sink`
+ *
+ * ## Module Options
+ *
+ * @pulse_module_options@
+ *
+ * ## See Also
+ *
+ * \ref page_module_filter_chain "libpipewire-module-filter-chain"
+ */
+
+static const char *const pulse_module_options =
+	"sink_name=<name for the sink> "
+	"sink_properties=<properties for the sink> "
+	"sink_input_properties=<properties for the sink input> "
+	"master=<name of sink to filter> "
+	"sink_master=<name of sink to filter> "
+	"format=<sample format> "
+	"rate=<sample rate> "
+	"channels=<number of channels> "
+	"channel_map=<input channel map> "
+	"plugin=<ladspa plugin name> "
+	"label=<ladspa plugin label> "
+	"control=<comma separated list of input control values> "
+	"input_ladspaport_map=<comma separated list of input LADSPA port names> "
+	"output_ladspaport_map=<comma separated list of output LADSPA port names> ";
+
 #define NAME "ladspa-sink"
 
 PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
@@ -126,21 +157,7 @@ static int module_ladspa_sink_unload(struct module *module)
 static const struct spa_dict_item module_ladspa_sink_info[] = {
 	{ PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
 	{ PW_KEY_MODULE_DESCRIPTION, "Virtual LADSPA sink" },
-	{ PW_KEY_MODULE_USAGE,
-		"sink_name=<name for the sink> "
-		"sink_properties=<properties for the sink> "
-		"sink_input_properties=<properties for the sink input> "
-		"master=<name of sink to filter> "
-		"sink_master=<name of sink to filter> "
-		"format=<sample format> "
-		"rate=<sample rate> "
-		"channels=<number of channels> "
-		"channel_map=<input channel map> "
-		"plugin=<ladspa plugin name> "
-		"label=<ladspa plugin label> "
-		"control=<comma separated list of input control values> "
-		"input_ladspaport_map=<comma separated list of input LADSPA port names> "
-		"output_ladspaport_map=<comma separated list of output LADSPA port names> "},
+	{ PW_KEY_MODULE_USAGE, pulse_module_options },
 	{ PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
 };
 
diff --git a/src/modules/module-protocol-pulse/modules/module-ladspa-source.c b/src/modules/module-protocol-pulse/modules/module-ladspa-source.c
index 2e876ae4c18aba21e0e6d56206cada730e931ba5..09eb11ce4d0e453d7bdf7018a4482cddceb82e7f 100644
--- a/src/modules/module-protocol-pulse/modules/module-ladspa-source.c
+++ b/src/modules/module-protocol-pulse/modules/module-ladspa-source.c
@@ -11,6 +11,37 @@
 #include "../defs.h"
 #include "../module.h"
 
+/** \page page_pulse_module_ladspa_source LADSPA Source
+ *
+ * ## Module Name
+ *
+ * `module-ladspa-source`
+ *
+ * ## Module Options
+ *
+ * @pulse_module_options@
+ *
+ * ## See Also
+ *
+ * \ref page_module_filter_chain "libpipewire-module-filter-chain"
+ */
+
+static const char *const pulse_module_options =
+	"source_name=<name for the source> "
+	"source_properties=<properties for the source> "
+	"source_output_properties=<properties for the source output> "
+	"master=<name of source to filter> "
+	"source_master=<name of source to filter> "
+	"format=<sample format> "
+	"rate=<sample rate> "
+	"channels=<number of channels> "
+	"channel_map=<input channel map> "
+	"plugin=<ladspa plugin name> "
+	"label=<ladspa plugin label> "
+	"control=<comma separated list of input control values> "
+	"input_ladspaport_map=<comma separated list of input LADSPA port names> "
+	"output_ladspaport_map=<comma separated list of output LADSPA port names> ";
+
 #define NAME "ladspa-source"
 
 PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
@@ -126,21 +157,7 @@ static int module_ladspa_source_unload(struct module *module)
 static const struct spa_dict_item module_ladspa_source_info[] = {
 	{ PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
 	{ PW_KEY_MODULE_DESCRIPTION, "Virtual LADSPA source" },
-	{ PW_KEY_MODULE_USAGE,
-		"source_name=<name for the source> "
-		"source_properties=<properties for the source> "
-		"source_output_properties=<properties for the source output> "
-		"master=<name of source to filter> "
-		"source_master=<name of source to filter> "
-		"format=<sample format> "
-		"rate=<sample rate> "
-		"channels=<number of channels> "
-		"channel_map=<input channel map> "
-		"plugin=<ladspa plugin name> "
-		"label=<ladspa plugin label> "
-		"control=<comma separated list of input control values> "
-		"input_ladspaport_map=<comma separated list of input LADSPA port names> "
-		"output_ladspaport_map=<comma separated list of output LADSPA port names> "},
+	{ PW_KEY_MODULE_USAGE, pulse_module_options },
 	{ PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
 };
 
diff --git a/src/modules/module-protocol-pulse/modules/module-loopback.c b/src/modules/module-protocol-pulse/modules/module-loopback.c
index 49507f6355b4fa2553e4e311bc42a63066e3289c..2ef85fa5efb40b966a9e0bf21ccdefd7815951b6 100644
--- a/src/modules/module-protocol-pulse/modules/module-loopback.c
+++ b/src/modules/module-protocol-pulse/modules/module-loopback.c
@@ -11,6 +11,34 @@
 #include "../defs.h"
 #include "../module.h"
 
+/** \page page_pulse_module_loopback Loopback
+ *
+ * ## Module Name
+ *
+ * `module-loopback`
+ *
+ * ## Module Options
+ *
+ * @pulse_module_options@
+ *
+ * ## See Also
+ *
+ * \ref page_module_loopback "libpipewire-module-loopback"
+ */
+
+static const char *const pulse_module_options =
+	"source=<source to connect to> "
+	"sink=<sink to connect to> "
+	"latency_msec=<latency in ms> "
+	"rate=<sample rate> "
+	"channels=<number of channels> "
+	"channel_map=<channel map> "
+	"sink_input_properties=<proplist> "
+	"source_output_properties=<proplist> "
+	"source_dont_move=<boolean> "
+	"sink_dont_move=<boolean> "
+	"remix=<remix channels?> ";
+
 #define NAME "loopback"
 
 PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
@@ -99,17 +127,7 @@ static int module_loopback_unload(struct module *module)
 static const struct spa_dict_item module_loopback_info[] = {
 	{ PW_KEY_MODULE_AUTHOR, "Arun Raghavan <arun@asymptotic.io>" },
 	{ PW_KEY_MODULE_DESCRIPTION, "Loopback from source to sink" },
-	{ PW_KEY_MODULE_USAGE, "source=<source to connect to> "
-				"sink=<sink to connect to> "
-				"latency_msec=<latency in ms> "
-				"rate=<sample rate> "
-				"channels=<number of channels> "
-				"channel_map=<channel map> "
-				"sink_input_properties=<proplist> "
-				"source_output_properties=<proplist> "
-				"source_dont_move=<boolean> "
-				"sink_dont_move=<boolean> "
-				"remix=<remix channels?> " },
+	{ PW_KEY_MODULE_USAGE, pulse_module_options },
 	{ PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
 };
 
diff --git a/src/modules/module-protocol-pulse/modules/module-native-protocol-tcp.c b/src/modules/module-protocol-pulse/modules/module-native-protocol-tcp.c
index 9536945eadc6a834cbb23c8231c79da480aa8b46..6700ad458f4ed33332b157aea6d1df6a1ca577ba 100644
--- a/src/modules/module-protocol-pulse/modules/module-native-protocol-tcp.c
+++ b/src/modules/module-protocol-pulse/modules/module-native-protocol-tcp.c
@@ -8,6 +8,22 @@
 #include "../pulse-server.h"
 #include "../server.h"
 
+/** \page page_pulse_module_native_protocol_tcp Pulseaudio TCP Protocol
+ *
+ * ## Module Name
+ *
+ * `module-native-protocol-tcp`
+ *
+ * ## Module Options
+ *
+ * @pulse_module_options@
+ */
+
+static const char *const pulse_module_options =
+	"port=<TCP port number> "
+	"listen=<address to listen on> "
+	"auth-anonymous=<don't check for cookies?>";
+
 #define NAME "protocol-tcp"
 
 PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
@@ -53,9 +69,7 @@ static int module_native_protocol_tcp_unload(struct module *module)
 static const struct spa_dict_item module_native_protocol_tcp_info[] = {
 	{ PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
 	{ PW_KEY_MODULE_DESCRIPTION, "Native protocol (TCP sockets)" },
-	{ PW_KEY_MODULE_USAGE, "port=<TCP port number> "
-				"listen=<address to listen on> "
-				"auth-anonymous=<don't check for cookies?>"},
+	{ PW_KEY_MODULE_USAGE, pulse_module_options },
 	{ PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
 };
 
diff --git a/src/modules/module-protocol-pulse/modules/module-null-sink.c b/src/modules/module-protocol-pulse/modules/module-null-sink.c
index d132c6514449ca5ae4cd5aa6159c26d73c9b486d..2d85cd4846246ff9d40a7a2a79cd54824e0d49f2 100644
--- a/src/modules/module-protocol-pulse/modules/module-null-sink.c
+++ b/src/modules/module-protocol-pulse/modules/module-null-sink.c
@@ -7,6 +7,25 @@
 #include "../manager.h"
 #include "../module.h"
 
+/** \page page_pulse_module_null_sink Null Sink
+ *
+ * ## Module Name
+ *
+ * `module-null-sink`
+ *
+ * ## Module Options
+ *
+ * @pulse_module_options@
+ */
+
+static const char *const pulse_module_options =
+	"sink_name=<name of sink> "
+	"sink_properties=<properties for the sink> "
+	"format=<sample format> "
+	"rate=<sample rate> "
+	"channels=<number of channels> "
+	"channel_map=<channel map>";
+
 #define NAME "null-sink"
 
 PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
@@ -129,12 +148,7 @@ static int module_null_sink_unload(struct module *module)
 static const struct spa_dict_item module_null_sink_info[] = {
 	{ PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
 	{ PW_KEY_MODULE_DESCRIPTION, "A NULL sink" },
-	{ PW_KEY_MODULE_USAGE,  "sink_name=<name of sink> "
-				"sink_properties=<properties for the sink> "
-				"format=<sample format> "
-				"rate=<sample rate> "
-				"channels=<number of channels> "
-				"channel_map=<channel map>" },
+	{ PW_KEY_MODULE_USAGE, pulse_module_options },
 	{ PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
 };
 
diff --git a/src/modules/module-protocol-pulse/modules/module-pipe-sink.c b/src/modules/module-protocol-pulse/modules/module-pipe-sink.c
index b3a60f750616c8663ef7202c35a3afce8b7d4654..0f910ac6b996950efed490f681aebe48529e8f37 100644
--- a/src/modules/module-protocol-pulse/modules/module-pipe-sink.c
+++ b/src/modules/module-protocol-pulse/modules/module-pipe-sink.c
@@ -14,6 +14,31 @@
 #include "../defs.h"
 #include "../module.h"
 
+/** \page page_pulse_module_pipe_sink Pipe Sink
+ *
+ * ## Module Name
+ *
+ * `module-pipe-sink`
+ *
+ * ## Module Options
+ *
+ * @pulse_module_options@
+ *
+ * ## See Also
+ *
+ * \ref page_module_pipe_tunnel "libpipewire-module-pipe-tunnel"
+ */
+
+static const char *const pulse_module_options =
+	"file=<name of the FIFO special file to use> "
+	"sink_name=<name for the sink> "
+	"sink_properties=<sink properties> "
+	"format=<sample format> "
+	"rate=<sample rate> "
+	"channels=<number of channels> "
+	"channel_map=<channel map> "
+	"use_system_clock_for_timing=<yes or no> ";
+
 #define NAME "pipe-sink"
 
 PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
@@ -94,14 +119,7 @@ static int module_pipe_sink_unload(struct module *module)
 static const struct spa_dict_item module_pipe_sink_info[] = {
 	{ PW_KEY_MODULE_AUTHOR, "Sanchayan Maity <sanchayan@asymptotic.io>" },
 	{ PW_KEY_MODULE_DESCRIPTION, "Pipe sink" },
-	{ PW_KEY_MODULE_USAGE, "file=<name of the FIFO special file to use> "
-				"sink_name=<name for the sink> "
-				"sink_properties=<sink properties> "
-				"format=<sample format> "
-				"rate=<sample rate> "
-				"channels=<number of channels> "
-				"channel_map=<channel map> "
-				"use_system_clock_for_timing=<yes or no> " },
+	{ PW_KEY_MODULE_USAGE, pulse_module_options },
 	{ PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
 };
 
diff --git a/src/modules/module-protocol-pulse/modules/module-pipe-source.c b/src/modules/module-protocol-pulse/modules/module-pipe-source.c
index 1261c4ec53dde53a66b4ff9cd6fa5a0609dbe5d3..c9c31f106988b2c00e1e02de87ed1329e124bb61 100644
--- a/src/modules/module-protocol-pulse/modules/module-pipe-source.c
+++ b/src/modules/module-protocol-pulse/modules/module-pipe-source.c
@@ -14,6 +14,30 @@
 #include "../defs.h"
 #include "../module.h"
 
+/** \page page_pulse_module_pipe_source Pipe Source
+ *
+ * ## Module Name
+ *
+ * `module-pipe-source`
+ *
+ * ## Module Options
+ *
+ * @pulse_module_options@
+ *
+ * ## See Also
+ *
+ * \ref page_module_pipe_tunnel "libpipewire-module-pipe-tunnel"
+ */
+
+static const char *const pulse_module_options =
+	"file=<name of the FIFO special file to use> "
+	"source_name=<name for the source> "
+	"source_properties=<source properties> "
+	"format=<sample format> "
+	"rate=<sample rate> "
+	"channels=<number of channels> "
+	"channel_map=<channel map> ";
+
 #define NAME "pipe-source"
 
 PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
@@ -94,13 +118,7 @@ static int module_pipe_source_unload(struct module *module)
 static const struct spa_dict_item module_pipe_source_info[] = {
 	{ PW_KEY_MODULE_AUTHOR, "Sanchayan Maity <sanchayan@asymptotic.io>" },
 	{ PW_KEY_MODULE_DESCRIPTION, "Pipe source" },
-	{ PW_KEY_MODULE_USAGE, "file=<name of the FIFO special file to use> "
-				"source_name=<name for the source> "
-				"source_properties=<source properties> "
-				"format=<sample format> "
-				"rate=<sample rate> "
-				"channels=<number of channels> "
-				"channel_map=<channel map> " },
+	{ PW_KEY_MODULE_USAGE, pulse_module_options },
 	{ PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
 };
 
diff --git a/src/modules/module-protocol-pulse/modules/module-raop-discover.c b/src/modules/module-protocol-pulse/modules/module-raop-discover.c
index c4391837f106753ba3e0e95fa0566811d924f0fd..4e1cf8adbcd11a1815c7a8686335e8e981fc89ec 100644
--- a/src/modules/module-protocol-pulse/modules/module-raop-discover.c
+++ b/src/modules/module-protocol-pulse/modules/module-raop-discover.c
@@ -8,6 +8,21 @@
 #include "../defs.h"
 #include "../module.h"
 
+/** \page page_pulse_module_raop_discover RAOP Discover
+ *
+ * ## Module Name
+ *
+ * `module-raop-discover`
+ *
+ * ## Module Options
+ *
+ * No options.
+ *
+ * ## See Also
+ *
+ * \ref page_module_raop_discover "libpipewire-module-raop-discover"
+ */
+
 #define NAME "raop-discover"
 
 PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
diff --git a/src/modules/module-protocol-pulse/modules/module-remap-sink.c b/src/modules/module-protocol-pulse/modules/module-remap-sink.c
index 1f40c65204aad24a040c1ae8d8f433245207a211..103579fe2fcdd8fa6cf5d607989992bd6378e1f5 100644
--- a/src/modules/module-protocol-pulse/modules/module-remap-sink.c
+++ b/src/modules/module-protocol-pulse/modules/module-remap-sink.c
@@ -10,6 +10,33 @@
 #include "../defs.h"
 #include "../module.h"
 
+/** \page page_pulse_module_remap_sink Remap Sink
+ *
+ * ## Module Name
+ *
+ * `module-remap-sink`
+ *
+ * ## Module Options
+ *
+ * @pulse_module_options@
+ *
+ * ## See Also
+ *
+ * \ref page_module_loopback "libpipewire-module-loopback"
+ */
+
+static const char *const pulse_module_options =
+	"sink_name=<name for the sink> "
+	"sink_properties=<properties for the sink> "
+	"master=<name of sink to remap> "
+	"master_channel_map=<channel map> "
+	"format=<sample format> "
+	"rate=<sample rate> "
+	"channels=<number of channels> "
+	"channel_map=<channel map> "
+	"resample_method=<resampler> "
+	"remix=<remix channels?>";
+
 #define NAME "remap-sink"
 
 PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
@@ -96,16 +123,7 @@ static int module_remap_sink_unload(struct module *module)
 static const struct spa_dict_item module_remap_sink_info[] = {
 	{ PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
 	{ PW_KEY_MODULE_DESCRIPTION, "Remap sink channels" },
-	{ PW_KEY_MODULE_USAGE, "sink_name=<name for the sink> "
-			"sink_properties=<properties for the sink> "
-			"master=<name of sink to remap> "
-			"master_channel_map=<channel map> "
-			"format=<sample format> "
-			"rate=<sample rate> "
-			"channels=<number of channels> "
-			"channel_map=<channel map> "
-			"resample_method=<resampler> "
-			"remix=<remix channels?>" },
+	{ PW_KEY_MODULE_USAGE, pulse_module_options },
 	{ PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
 };
 
diff --git a/src/modules/module-protocol-pulse/modules/module-remap-source.c b/src/modules/module-protocol-pulse/modules/module-remap-source.c
index 6e806d1b67d8da4bd2c3aa9385a5bdcc5350cd73..eaff91b088eb8944bc51728a6c4fef7209c45430 100644
--- a/src/modules/module-protocol-pulse/modules/module-remap-source.c
+++ b/src/modules/module-protocol-pulse/modules/module-remap-source.c
@@ -10,6 +10,33 @@
 #include "../defs.h"
 #include "../module.h"
 
+/** \page page_pulse_module_remap_source Remap Source
+ *
+ * ## Module Name
+ *
+ * `module-remap-source`
+ *
+ * ## Module Options
+ *
+ * @pulse_module_options@
+ *
+ * ## See Also
+ *
+ * \ref page_module_loopback "libpipewire-module-loopback"
+ */
+
+static const char *const pulse_module_options =
+	"source_name=<name for the source> "
+	"source_properties=<properties for the source> "
+	"master=<name of source to filter> "
+	"master_channel_map=<channel map> "
+	"format=<sample format> "
+	"rate=<sample rate> "
+	"channels=<number of channels> "
+	"channel_map=<channel map> "
+	"resample_method=<resampler> "
+	"remix=<remix channels?>";
+
 #define NAME "remap-sink"
 
 PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
@@ -96,16 +123,7 @@ static int module_remap_source_unload(struct module *module)
 static const struct spa_dict_item module_remap_source_info[] = {
 	{ PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
 	{ PW_KEY_MODULE_DESCRIPTION, "Remap source channels" },
-	{ PW_KEY_MODULE_USAGE, "source_name=<name for the source> "
-			"source_properties=<properties for the source> "
-			"master=<name of source to filter> "
-			"master_channel_map=<channel map> "
-			"format=<sample format> "
-			"rate=<sample rate> "
-			"channels=<number of channels> "
-			"channel_map=<channel map> "
-			"resample_method=<resampler> "
-			"remix=<remix channels?>" },
+	{ PW_KEY_MODULE_USAGE, pulse_module_options },
 	{ PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
 };
 
diff --git a/src/modules/module-protocol-pulse/modules/module-roc-sink-input.c b/src/modules/module-protocol-pulse/modules/module-roc-sink-input.c
index 09df1a5e276761a6741ab72e4bcd80e68ede1e51..2200a7bfcc38222727b8df81fae59d217c02fc2c 100644
--- a/src/modules/module-protocol-pulse/modules/module-roc-sink-input.c
+++ b/src/modules/module-protocol-pulse/modules/module-roc-sink-input.c
@@ -9,7 +9,34 @@
 #include "../defs.h"
 #include "../module.h"
 
-#define NAME "roc-source"
+/** \page page_pulse_module_roc_sink_input ROC Sink Input
+ *
+ * ## Module Name
+ *
+ * `module-roc-sink-input`
+ *
+ * ## Module Options
+ *
+ * @pulse_module_options@
+ *
+ * ## See Also
+ *
+ * \ref page_module_roc_source "libpipewire-module-roc-source"
+ */
+
+static const char *const pulse_module_options =
+	"sink=<name for the sink> "
+	"sink_input_properties=<properties for the sink_input> "
+	"resampler_profile=<empty>|high|medium|low "
+	"fec_code=<empty>|disable|rs8m|ldpc "
+	"sess_latency_msec=<target network latency in milliseconds> "
+	"local_ip=<local receiver ip> "
+	"local_source_port=<local receiver port for source packets> "
+	"local_repair_port=<local receiver port for repair packets> "
+	"local_control_port=<local receiver port for control packets> "
+	;
+
+#define NAME "roc-sink-input"
 
 PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
 #define PW_LOG_TOPIC_DEFAULT mod_topic
@@ -92,14 +119,7 @@ static int module_roc_sink_input_unload(struct module *module)
 static const struct spa_dict_item module_roc_sink_input_info[] = {
 	{ PW_KEY_MODULE_AUTHOR, "Sanchayan Maity <sanchayan@asymptotic.io>" },
 	{ PW_KEY_MODULE_DESCRIPTION, "roc sink-input" },
-	{ PW_KEY_MODULE_USAGE, "sink=<name for the sink> "
-				"sink_input_properties=<properties for the sink_input> "
-				"resampler_profile=<empty>|disable|high|medium|low "
-				"fec_code=<empty>|disable|rs8m|ldpc "
-				"sess_latency_msec=<target network latency in milliseconds> "
-				"local_ip=<local receiver ip> "
-				"local_source_port=<local receiver port for source packets> "
-				"local_repair_port=<local receiver port for repair packets> " },
+	{ PW_KEY_MODULE_USAGE, pulse_module_options },
 	{ PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
 };
 
@@ -144,6 +164,11 @@ static int module_roc_sink_input_prepare(struct module * const module)
 		pw_properties_set(props, "local_repair_port", NULL);
 	}
 
+	if ((str = pw_properties_get(props, "local_control_port")) != NULL) {
+		pw_properties_set(roc_props, "local.control.port", str);
+		pw_properties_set(props, "local_control_port", NULL);
+	}
+
 	if ((str = pw_properties_get(props, "sess_latency_msec")) != NULL) {
 		pw_properties_set(roc_props, "sess.latency.msec", str);
 		pw_properties_set(props, "sess_latency_msec", NULL);
diff --git a/src/modules/module-protocol-pulse/modules/module-roc-sink.c b/src/modules/module-protocol-pulse/modules/module-roc-sink.c
index 0a4420d3213769b150c1722ab33c7c5b7c629ccc..6b03006ab46e162662e0dffcb4d18e39a8841cb1 100644
--- a/src/modules/module-protocol-pulse/modules/module-roc-sink.c
+++ b/src/modules/module-protocol-pulse/modules/module-roc-sink.c
@@ -9,6 +9,31 @@
 #include "../defs.h"
 #include "../module.h"
 
+/** \page page_pulse_module_roc_sink ROC Sink
+ *
+ * ## Module Name
+ *
+ * `module-roc-sink`
+ *
+ * ## Module Options
+ *
+ * @pulse_module_options@
+ *
+ * ## See Also
+ *
+ * \ref page_module_roc_sink "libpipewire-module-roc-sink"
+ */
+
+static const char *const pulse_module_options =
+	"sink_name=<name for the sink> "
+	"sink_properties=<properties for the sink> "
+	"fec_code=<empty>|disable|rs8m|ldpc "
+	"remote_ip=<remote receiver ip> "
+	"remote_source_port=<remote receiver port for source packets> "
+	"remote_repair_port=<remote receiver port for repair packets> "
+	"remote_control_port=<remote receiver port for control packets> "
+	;
+
 #define NAME "roc-sink"
 
 PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
@@ -96,17 +121,13 @@ static const char* const valid_args[] = {
 	"remote_ip",
 	"remote_source_port",
 	"remote_repair_port",
+	"remote_control_port",
 	NULL
 };
 static const struct spa_dict_item module_roc_sink_info[] = {
 	{ PW_KEY_MODULE_AUTHOR, "Sanchayan Maity <sanchayan@asymptotic.io>" },
 	{ PW_KEY_MODULE_DESCRIPTION, "roc sink" },
-	{ PW_KEY_MODULE_USAGE, "sink_name=<name for the sink> "
-				"sink_properties=<properties for the sink> "
-				"fec_code=<empty>|disable|rs8m|ldpc "
-				"remote_ip=<remote receiver ip> "
-				"remote_source_port=<remote receiver port for source packets> "
-				"remote_repair_port=<remote receiver port for repair packets> " },
+	{ PW_KEY_MODULE_USAGE, pulse_module_options },
 	{ PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
 };
 
@@ -159,6 +180,12 @@ static int module_roc_sink_prepare(struct module * const module)
 		pw_properties_set(roc_props, "remote.repair.port", str);
 		pw_properties_set(props, "remote_repair_port", NULL);
 	}
+
+	if ((str = pw_properties_get(props, "remote_control_port")) != NULL) {
+		pw_properties_set(roc_props, "remote.control.port", str);
+		pw_properties_set(props, "remote_control_port", NULL);
+	}
+
 	if ((str = pw_properties_get(props, "fec_code")) != NULL) {
 		pw_properties_set(roc_props, "fec.code", str);
 		pw_properties_set(props, "fec_code", NULL);
diff --git a/src/modules/module-protocol-pulse/modules/module-roc-source.c b/src/modules/module-protocol-pulse/modules/module-roc-source.c
index 9f23c421dad98d9c4e6eefc93e5c0deea675c92d..661153d1e05ab5e330ef7e6d239e572b4451b859 100644
--- a/src/modules/module-protocol-pulse/modules/module-roc-source.c
+++ b/src/modules/module-protocol-pulse/modules/module-roc-source.c
@@ -9,6 +9,33 @@
 #include "../defs.h"
 #include "../module.h"
 
+/** \page page_pulse_module_roc_source ROC Source
+ *
+ * ## Module Name
+ *
+ * `module-roc-source`
+ *
+ * ## Module Options
+ *
+ * @pulse_module_options@
+ *
+ * ## See Also
+ *
+ * \ref page_module_roc_source "libpipewire-module-roc-source"
+ */
+
+static const char *const pulse_module_options =
+	"source_name=<name for the source> "
+	"source_properties=<properties for the source> "
+	"resampler_profile=<empty>|high|medium|low "
+	"fec_code=<empty>|disable|rs8m|ldpc "
+	"sess_latency_msec=<target network latency in milliseconds> "
+	"local_ip=<local receiver ip> "
+	"local_source_port=<local receiver port for source packets> "
+	"local_repair_port=<local receiver port for repair packets> "
+	"local_control_port=<local receiver port for control packets> "
+	;
+
 #define NAME "roc-source"
 
 PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
@@ -98,20 +125,14 @@ static const char* const valid_args[] = {
 	"local_ip",
 	"local_source_port",
 	"local_repair_port",
+	"local_control_port",
 	NULL
 };
 
 static const struct spa_dict_item module_roc_source_info[] = {
 	{ PW_KEY_MODULE_AUTHOR, "Sanchayan Maity <sanchayan@asymptotic.io>" },
 	{ PW_KEY_MODULE_DESCRIPTION, "roc source" },
-	{ PW_KEY_MODULE_USAGE, "source_name=<name for the source> "
-				"source_properties=<properties for the source> "
-				"resampler_profile=<empty>|disable|high|medium|low "
-				"fec_code=<empty>|disable|rs8m|ldpc "
-				"sess_latency_msec=<target network latency in milliseconds> "
-				"local_ip=<local receiver ip> "
-				"local_source_port=<local receiver port for source packets> "
-				"local_repair_port=<local receiver port for repair packets> " },
+	{ PW_KEY_MODULE_USAGE, pulse_module_options },
 	{ PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
 };
 
@@ -161,6 +182,11 @@ static int module_roc_source_prepare(struct module * const module)
 		pw_properties_set(props, "local_repair_port", NULL);
 	}
 
+	if ((str = pw_properties_get(props, "local_control_port")) != NULL) {
+		pw_properties_set(roc_props, "local.control.port", str);
+		pw_properties_set(props, "local_control_port", NULL);
+	}
+
 	if ((str = pw_properties_get(props, "sess_latency_msec")) != NULL) {
 		pw_properties_set(roc_props, "sess.latency.msec", str);
 		pw_properties_set(props, "sess_latency_msec", NULL);
diff --git a/src/modules/module-protocol-pulse/modules/module-rtp-recv.c b/src/modules/module-protocol-pulse/modules/module-rtp-recv.c
index 2581e78cbfd6329d3dc6113db241f6010954ed30..1777caf611c6e7fbabb7283ecece80bba0e39d91 100644
--- a/src/modules/module-protocol-pulse/modules/module-rtp-recv.c
+++ b/src/modules/module-protocol-pulse/modules/module-rtp-recv.c
@@ -8,6 +8,26 @@
 #include "../defs.h"
 #include "../module.h"
 
+/** \page page_pulse_module_rtp_recv RTP Receiver
+ *
+ * ## Module Name
+ *
+ * `module-rtp-recv`
+ *
+ * ## Module Options
+ *
+ * @pulse_module_options@
+ *
+ * ## See Also
+ *
+ * \ref page_module_rtp_sap "libpipewire-module-rtp-sap"
+ */
+
+static const char *const pulse_module_options =
+	"sink=<name of the sink> "
+	"sap_address=<multicast address to listen on> "
+	"latency_msec=<latency in ms> ";
+
 #define NAME "rtp-recv"
 
 PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
@@ -94,9 +114,7 @@ static int module_rtp_recv_unload(struct module *module)
 static const struct spa_dict_item module_rtp_recv_info[] = {
 	{ PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
 	{ PW_KEY_MODULE_DESCRIPTION, "Receive data from a network via RTP/SAP/SDP" },
-	{ PW_KEY_MODULE_USAGE,	"sink=<name of the sink> "
-				"sap_address=<multicast address to listen on> "
-				"latency_msec=<latency in ms> " },
+	{ PW_KEY_MODULE_USAGE, pulse_module_options },
 	{ PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
 };
 
diff --git a/src/modules/module-protocol-pulse/modules/module-rtp-send.c b/src/modules/module-protocol-pulse/modules/module-rtp-send.c
index 3afa8ae41cadf0e5287ca0fdbc4e450ce76633cf..e84962956760ad717f6b540099310c73b791da33 100644
--- a/src/modules/module-protocol-pulse/modules/module-rtp-send.c
+++ b/src/modules/module-protocol-pulse/modules/module-rtp-send.c
@@ -8,6 +8,36 @@
 #include "../defs.h"
 #include "../module.h"
 
+/** \page page_pulse_module_rtp_send RTP Sender
+ *
+ * ## Module Name
+ *
+ * `module-rtp-send`
+ *
+ * ## Module Options
+ *
+ * @pulse_module_options@
+ *
+ * ## See Also
+ *
+ * \ref page_module_rtp_sink "libpipewire-module-rtp-sink"
+ */
+
+static const char *const pulse_module_options =
+	"source=<name of the source> "
+	"format=<sample format> "
+	"channels=<number of channels> "
+	"rate=<sample rate> "
+	"destination_ip=<destination IP address> "
+	"source_ip=<source IP address> "
+	"port=<port number> "
+	"mtu=<maximum transfer unit> "
+	"loop=<loopback to local host?> "
+	"ttl=<ttl value> "
+	"inhibit_auto_suspend=<always|never|only_with_non_monitor_sources> "
+	"stream_name=<name of the stream> "
+	"enable_opus=<enable OPUS codec>";
+
 #define NAME "rtp-send"
 
 PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
@@ -136,19 +166,7 @@ static int module_rtp_send_unload(struct module *module)
 static const struct spa_dict_item module_rtp_send_info[] = {
 	{ PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
 	{ PW_KEY_MODULE_DESCRIPTION, "Read data from source and send it to the network via RTP/SAP/SDP" },
-	{ PW_KEY_MODULE_USAGE,	"source=<name of the source> "
-				"format=<sample format> "
-				"channels=<number of channels> "
-				"rate=<sample rate> "
-				"destination_ip=<destination IP address> "
-				"source_ip=<source IP address> "
-				"port=<port number> "
-				"mtu=<maximum transfer unit> "
-				"loop=<loopback to local host?> "
-				"ttl=<ttl value> "
-				"inhibit_auto_suspend=<always|never|only_with_non_monitor_sources> "
-				"stream_name=<name of the stream> "
-				"enable_opus=<enable OPUS codec>" },
+	{ PW_KEY_MODULE_USAGE, pulse_module_options },
 	{ PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
 };
 
diff --git a/src/modules/module-protocol-pulse/modules/module-simple-protocol-tcp.c b/src/modules/module-protocol-pulse/modules/module-simple-protocol-tcp.c
index d0479a788662fc91882680f2df54f7a0c98da894..4ca4156c2731c712073d4fe71dc11ccc0f8de6fb 100644
--- a/src/modules/module-protocol-pulse/modules/module-simple-protocol-tcp.c
+++ b/src/modules/module-protocol-pulse/modules/module-simple-protocol-tcp.c
@@ -8,6 +8,33 @@
 #include "../defs.h"
 #include "../module.h"
 
+/** \page page_pulse_module_simple_protocol_tcp Simple TCP Protocol
+ *
+ * ## Module Name
+ *
+ * `module-simple-protocol-tcp`
+ *
+ * ## Module Options
+ *
+ * @pulse_module_options@
+ *
+ * ## See Also
+ *
+ * \ref page_module_protocol_simple "libpipewire-module-protocol-simple"
+ */
+
+static const char *const pulse_module_options =
+	"rate=<sample rate> "
+	"format=<sample format> "
+	"channels=<number of channels> "
+	"channel_map=<number of channels> "
+	"sink=<sink to connect to> "
+	"source=<source to connect to> "
+	"playback=<enable playback?> "
+	"record=<enable record?> "
+	"port=<TCP port number> "
+	"listen=<address to listen on>";
+
 #define NAME "simple-protocol-tcp"
 
 PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
@@ -84,16 +111,7 @@ static int module_simple_protocol_tcp_unload(struct module *module)
 static const struct spa_dict_item module_simple_protocol_tcp_info[] = {
 	{ PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
 	{ PW_KEY_MODULE_DESCRIPTION, "Simple protocol (TCP sockets)" },
-	{ PW_KEY_MODULE_USAGE, "rate=<sample rate> "
-				"format=<sample format> "
-				"channels=<number of channels> "
-				"channel_map=<number of channels> "
-				"sink=<sink to connect to> "
-				"source=<source to connect to> "
-				"playback=<enable playback?> "
-				"record=<enable record?> "
-				"port=<TCP port number> "
-				"listen=<address to listen on>" },
+	{ PW_KEY_MODULE_USAGE, pulse_module_options },
 	{ PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
 };
 
diff --git a/src/modules/module-protocol-pulse/modules/module-switch-on-connect.c b/src/modules/module-protocol-pulse/modules/module-switch-on-connect.c
index 76c0100ca4da40b2b2968734e4791248ba247706..e4dc2deacf6574a8197d24b4926b8af777f25646 100644
--- a/src/modules/module-protocol-pulse/modules/module-switch-on-connect.c
+++ b/src/modules/module-protocol-pulse/modules/module-switch-on-connect.c
@@ -15,6 +15,22 @@
 #include "../manager.h"
 #include "../collect.h"
 
+/** \page page_pulse_module_switch_on_connect Switch on Connect
+ *
+ * ## Module Name
+ *
+ * `module-switch-on-connect`
+ *
+ * ## Module Options
+ *
+ * @pulse_module_options@
+ */
+
+static const char *const pulse_module_options =
+	"only_from_unavailable=<boolean, only switch from unavailable ports (not implemented yet)> "
+	"ignore_virtual=<boolean, ignore new virtual sinks and sources, defaults to true> "
+	"blocklist=<regex, ignore matching devices, default=hdmi> ";
+
 #define NAME "switch-on-connect"
 
 PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
@@ -215,9 +231,7 @@ static const struct spa_dict_item module_switch_on_connect_info[] = {
 	  "This module exists for Pulseaudio compatibility, and is useful only when some applications "
 	  "try to manage the default sinks/sources themselves and interfere with PipeWire's builtin "
 	  "default device switching." },
-	{ PW_KEY_MODULE_USAGE, "only_from_unavailable=<boolean, only switch from unavailable ports (not implemented yet)> "
-				"ignore_virtual=<boolean, ignore new virtual sinks and sources, defaults to true> "
-				"blocklist=<regex, ignore matching devices, default=hdmi> " },
+	{ PW_KEY_MODULE_USAGE, pulse_module_options },
 	{ PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
 };
 
diff --git a/src/modules/module-protocol-pulse/modules/module-tunnel-sink.c b/src/modules/module-protocol-pulse/modules/module-tunnel-sink.c
index c2c1441793235e338e1153441d79279b62e921a5..b779a3e56acea877513022f53e06efe5ae930733 100644
--- a/src/modules/module-protocol-pulse/modules/module-tunnel-sink.c
+++ b/src/modules/module-protocol-pulse/modules/module-tunnel-sink.c
@@ -12,6 +12,33 @@
 #include "../defs.h"
 #include "../module.h"
 
+/** \page page_pulse_module_tunnel_sink Tunnel Sink
+ *
+ * ## Module Name
+ *
+ * `module-tunnel-sink`
+ *
+ * ## Module Options
+ *
+ * @pulse_module_options@
+ *
+ * ## See Also
+ *
+ * \ref page_module_pulse_tunnel "libpipewire-module-pulse-tunnel"
+ */
+
+static const char *const pulse_module_options =
+	"server=<address> "
+	"sink=<name of the remote sink> "
+	"sink_name=<name for the local sink> "
+	"sink_properties=<properties for the local sink> "
+	"format=<sample format> "
+	"channels=<number of channels> "
+	"rate=<sample rate> "
+	"channel_map=<channel map> "
+	"latency_msec=<fixed latency in ms> "
+	"cookie=<cookie file path>";
+
 #define NAME "tunnel-sink"
 
 PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
@@ -92,17 +119,7 @@ static int module_tunnel_sink_unload(struct module *module)
 static const struct spa_dict_item module_tunnel_sink_info[] = {
 	{ PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
 	{ PW_KEY_MODULE_DESCRIPTION, "Create a network sink which connects to a remote PulseAudio server" },
-	{ PW_KEY_MODULE_USAGE,
-		"server=<address> "
-		"sink=<name of the remote sink> "
-		"sink_name=<name for the local sink> "
-		"sink_properties=<properties for the local sink> "
-		"format=<sample format> "
-		"channels=<number of channels> "
-		"rate=<sample rate> "
-		"channel_map=<channel map> "
-		"latency_msec=<fixed latency in ms> "
-		"cookie=<cookie file path>" },
+	{ PW_KEY_MODULE_USAGE, pulse_module_options },
 	{ PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
 };
 
diff --git a/src/modules/module-protocol-pulse/modules/module-tunnel-source.c b/src/modules/module-protocol-pulse/modules/module-tunnel-source.c
index 5e7eda3cc4e2a944fabb251e6b374786a8d2727b..b67efd5b72cfa7637f7122648817f2f490bd9f0a 100644
--- a/src/modules/module-protocol-pulse/modules/module-tunnel-source.c
+++ b/src/modules/module-protocol-pulse/modules/module-tunnel-source.c
@@ -12,6 +12,33 @@
 #include "../defs.h"
 #include "../module.h"
 
+/** \page page_pulse_module_tunnel_source Tunnel Source
+ *
+ * ## Module Name
+ *
+ * `module-tunnel-source`
+ *
+ * ## Module Options
+ *
+ * @pulse_module_options@
+ *
+ * ## See Also
+ *
+ * \ref page_module_pulse_tunnel "libpipewire-module-pulse-tunnel"
+ */
+
+static const char *const pulse_module_options =
+	"server=<address> "
+	"source=<name of the remote source> "
+	"source_name=<name for the local source> "
+	"source_properties=<properties for the local source> "
+	"format=<sample format> "
+	"channels=<number of channels> "
+	"rate=<sample rate> "
+	"channel_map=<channel map> "
+	"latency_msec=<fixed latency in ms> "
+	"cookie=<cookie file path>";
+
 #define NAME "tunnel-source"
 
 PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
@@ -92,17 +119,7 @@ static int module_tunnel_source_unload(struct module *module)
 static const struct spa_dict_item module_tunnel_source_info[] = {
 	{ PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
 	{ PW_KEY_MODULE_DESCRIPTION, "Create a network source which connects to a remote PulseAudio server" },
-	{ PW_KEY_MODULE_USAGE,
-		"server=<address> "
-		"source=<name of the remote source> "
-		"source_name=<name for the local source> "
-		"source_properties=<properties for the local source> "
-		"format=<sample format> "
-		"channels=<number of channels> "
-		"rate=<sample rate> "
-		"channel_map=<channel map> "
-		"latency_msec=<fixed latency in ms> "
-		"cookie=<cookie file path>" },
+	{ PW_KEY_MODULE_USAGE, pulse_module_options },
 	{ PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
 };
 
diff --git a/src/modules/module-protocol-pulse/modules/module-virtual-sink.c b/src/modules/module-protocol-pulse/modules/module-virtual-sink.c
index a9ed6f35402c5ecc4edbd4affa911737f0e69a0b..e3193f2f671592186144e4a3593b9e274ed331a5 100644
--- a/src/modules/module-protocol-pulse/modules/module-virtual-sink.c
+++ b/src/modules/module-protocol-pulse/modules/module-virtual-sink.c
@@ -10,6 +10,30 @@
 #include "../defs.h"
 #include "../module.h"
 
+/** \page page_pulse_module_virtual_sink Virtual Sink
+ *
+ * ## Module Name
+ *
+ * `module-virtual-sink`
+ *
+ * ## Module Options
+ *
+ * @pulse_module_options@
+ *
+ * ## See Also
+ *
+ * \ref page_module_loopback "libpipewire-module-loopback"
+ */
+
+static const char *const pulse_module_options =
+	"sink_name=<name for the sink> "
+	"sink_properties=<properties for the sink> "
+	"master=<name of sink to filter> "
+	"channels=<number of channels> "
+	"channel_map=<channel map> "
+	"use_volume_sharing=<yes or no> "
+	"force_flat_volume=<yes or no> ";
+
 #define NAME "virtual-sink"
 
 PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
@@ -98,13 +122,7 @@ static int module_virtual_sink_unload(struct module *module)
 static const struct spa_dict_item module_virtual_sink_info[] = {
 	{ PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
 	{ PW_KEY_MODULE_DESCRIPTION, "Virtual sink" },
-	{ PW_KEY_MODULE_USAGE, "sink_name=<name for the sink> "
-				"sink_properties=<properties for the sink> "
-				"master=<name of sink to filter> "
-				"channels=<number of channels> "
-				"channel_map=<channel map> "
-				"use_volume_sharing=<yes or no> "
-				"force_flat_volume=<yes or no> " },
+	{ PW_KEY_MODULE_USAGE, pulse_module_options },
 	{ PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
 };
 
diff --git a/src/modules/module-protocol-pulse/modules/module-virtual-source.c b/src/modules/module-protocol-pulse/modules/module-virtual-source.c
index 731822b91be7b80ae4c39f5a98030c5c906bbba9..fe3f106489071bf684ee5f2fb708a3f8471bcf54 100644
--- a/src/modules/module-protocol-pulse/modules/module-virtual-source.c
+++ b/src/modules/module-protocol-pulse/modules/module-virtual-source.c
@@ -11,6 +11,31 @@
 #include "../defs.h"
 #include "../module.h"
 
+/** \page page_pulse_module_virtual_source Virtual Source
+ *
+ * ## Module Name
+ *
+ * `module-virtual-source`
+ *
+ * ## Module Options
+ *
+ * @pulse_module_options@
+ *
+ * ## See Also
+ *
+ * \ref page_module_loopback "libpipewire-module-loopback"
+ */
+
+static const char *const pulse_module_options =
+	"source_name=<name for the source> "
+	"source_properties=<properties for the source> "
+	"master=<name of source to filter> "
+	"uplink_sink=<name> (optional)"
+	"channels=<number of channels> "
+	"channel_map=<channel map> "
+	"use_volume_sharing=<yes or no> "
+	"force_flat_volume=<yes or no> ";
+
 #define NAME "virtual-source"
 
 PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
@@ -99,14 +124,7 @@ static int module_virtual_source_unload(struct module *module)
 static const struct spa_dict_item module_virtual_source_info[] = {
 	{ PW_KEY_MODULE_AUTHOR, "Arun Raghavan <arun@asymptotic.io>" },
 	{ PW_KEY_MODULE_DESCRIPTION, "Loopback from source to sink" },
-	{ PW_KEY_MODULE_USAGE, "source_name=<name for the source> "
-				"source_properties=<properties for the source> "
-				"master=<name of source to filter> "
-				"uplink_sink=<name> (optional)"
-				"channels=<number of channels> "
-				"channel_map=<channel map> "
-				"use_volume_sharing=<yes or no> "
-				"force_flat_volume=<yes or no> " },
+	{ PW_KEY_MODULE_USAGE, pulse_module_options },
 	{ PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
 };
 
diff --git a/src/modules/module-protocol-pulse/modules/module-x11-bell.c b/src/modules/module-protocol-pulse/modules/module-x11-bell.c
index 6e6781c1d3da79d49f35a8005f3a29a39f1a4f50..a29e2e85d5cfd169b1c904a31df025775b3ec1e8 100644
--- a/src/modules/module-protocol-pulse/modules/module-x11-bell.c
+++ b/src/modules/module-protocol-pulse/modules/module-x11-bell.c
@@ -6,6 +6,27 @@
 
 #include "../module.h"
 
+/** \page page_pulse_module_x11_bell X11 Bell
+ *
+ * ## Module Name
+ *
+ * `module-x11-bell`
+ *
+ * ## Module Options
+ *
+ * @pulse_module_options@
+ *
+ * ## See Also
+ *
+ * \ref page_module_x11_bell "libpipewire-module-x11-bell"
+ */
+
+static const char *const pulse_module_options =
+	"sink=<sink to connect to> "
+	"sample=<the sample to play> "
+	"display=<X11 display> "
+	"xauthority=<X11 Authority>";
+
 #define NAME "x11-bell"
 
 PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
@@ -93,10 +114,7 @@ static int module_x11_bell_prepare(struct module * const module)
 static const struct spa_dict_item module_x11_bell_info[] = {
 	{ PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
 	{ PW_KEY_MODULE_DESCRIPTION, "X11 bell interceptor" },
-	{ PW_KEY_MODULE_USAGE,  "sink=<sink to connect to> "
-				"sample=<the sample to play> "
-				"display=<X11 display> "
-				"xauthority=<X11 Authority>" },
+	{ PW_KEY_MODULE_USAGE, pulse_module_options },
 	{ PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
 };
 
diff --git a/src/modules/module-protocol-pulse/modules/module-zeroconf-discover.c b/src/modules/module-protocol-pulse/modules/module-zeroconf-discover.c
index c42e01fdd2204ce64148e2f332d885c0aa325d6e..5b1763eb84cfa0dabe4ebe4f4463e0388ec73646 100644
--- a/src/modules/module-protocol-pulse/modules/module-zeroconf-discover.c
+++ b/src/modules/module-protocol-pulse/modules/module-zeroconf-discover.c
@@ -8,6 +8,24 @@
 #include "../defs.h"
 #include "../module.h"
 
+/** \page page_pulse_module_zeroconf_discover Zeroconf Discover
+ *
+ * ## Module Name
+ *
+ * `module-zeroconf-discover`
+ *
+ * ## Module Options
+ *
+ * @pulse_module_options@
+ *
+ * ## See Also
+ *
+ * \ref page_module_zeroconf_discover "libpipewire-module-zeroconf-discover"
+ */
+
+static const char *const pulse_module_options =
+	"latency_msec=<fixed latency in ms> ";
+
 #define NAME "zeroconf-discover"
 
 PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
@@ -84,8 +102,7 @@ static int module_zeroconf_discover_unload(struct module *module)
 static const struct spa_dict_item module_zeroconf_discover_info[] = {
 	{ PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.con>" },
 	{ PW_KEY_MODULE_DESCRIPTION, "mDNS/DNS-SD Service Discovery" },
-	{ PW_KEY_MODULE_USAGE,
-		"latency_msec=<fixed latency in ms> " },
+	{ PW_KEY_MODULE_USAGE, pulse_module_options },
 	{ PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
 };
 
diff --git a/src/modules/module-protocol-pulse/modules/module-zeroconf-publish.c b/src/modules/module-protocol-pulse/modules/module-zeroconf-publish.c
index e83ccb90d54c742bbf7f478d3b1ccac001407263..21c41233dfe8ad04a1d195ba453e60408253fd38 100644
--- a/src/modules/module-protocol-pulse/modules/module-zeroconf-publish.c
+++ b/src/modules/module-protocol-pulse/modules/module-zeroconf-publish.c
@@ -24,6 +24,17 @@
 #include <avahi-common/domain.h>
 #include <avahi-common/malloc.h>
 
+/** \page page_pulse_module_zeroconf_publish Zeroconf Publish
+ *
+ * ## Module Name
+ *
+ * `module-zeroconf-publish`
+ *
+ * ## Module Options
+ *
+ * No options.
+ */
+
 #define NAME "zeroconf-publish"
 
 PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
diff --git a/src/modules/module-protocol-pulse/pulse-server.c b/src/modules/module-protocol-pulse/pulse-server.c
index 0f654cdc37b2304fd38be15c83f9015f09991136..c922ca0dbbd76b1a1f0d823de6c1b7c4d20d4dfb 100644
--- a/src/modules/module-protocol-pulse/pulse-server.c
+++ b/src/modules/module-protocol-pulse/pulse-server.c
@@ -3770,17 +3770,9 @@ static int fill_sink_info(struct client *client, struct message *m,
 			TAG_INVALID);
 	}
 	if (client->version >= 15) {
-		bool is_linked = collect_is_linked(manager, o->id, SPA_DIRECTION_INPUT);
-		int state = node_state(info->state);
-
-		/* running with nothing linked is probably the monitor that is
-		 * keeping this sink busy */
-		if (state == STATE_RUNNING && !is_linked)
-			state = STATE_IDLE;
-
 		message_put(m,
 			TAG_VOLUME, dev_info.volume_info.base,	/* base volume */
-			TAG_U32, state,				/* state */
+			TAG_U32, dev_info.state,		/* state */
 			TAG_U32, dev_info.volume_info.steps,	/* n_volume_steps */
 			TAG_U32, card ? card->index : SPA_ID_INVALID,	/* card index */
 			TAG_INVALID);
@@ -3974,17 +3966,9 @@ static int fill_source_info(struct client *client, struct message *m,
 			TAG_INVALID);
 	}
 	if (client->version >= 15) {
-		bool is_linked = collect_is_linked(manager, o->id, SPA_DIRECTION_OUTPUT);
-		int state = node_state(info->state);
-
-		/* running with nothing linked is probably the sink that is
-		 * keeping this source busy */
-		if (state == STATE_RUNNING && !is_linked)
-			state = STATE_IDLE;
-
 		message_put(m,
 			TAG_VOLUME, dev_info.volume_info.base,	/* base volume */
-			TAG_U32, state,				/* state */
+			TAG_U32, dev_info.state,		/* state */
 			TAG_U32, dev_info.volume_info.steps,	/* n_volume_steps */
 			TAG_U32, card ? card->index : SPA_ID_INVALID,	/* card index */
 			TAG_INVALID);
@@ -4104,7 +4088,7 @@ static int fill_sink_input_info(struct client *client, struct message *m,
 			TAG_INVALID);
 	if (client->version >= 19)
 		message_put(m,
-			TAG_BOOLEAN, info->state != PW_NODE_STATE_RUNNING,		/* corked */
+			TAG_BOOLEAN, dev_info.state != STATE_RUNNING,		/* corked */
 			TAG_INVALID);
 	if (client->version >= 20)
 		message_put(m,
@@ -4178,7 +4162,7 @@ static int fill_source_output_info(struct client *client, struct message *m,
 			TAG_INVALID);
 	if (client->version >= 19)
 		message_put(m,
-			TAG_BOOLEAN, info->state != PW_NODE_STATE_RUNNING,		/* corked */
+			TAG_BOOLEAN, dev_info.state != STATE_RUNNING,		/* corked */
 			TAG_INVALID);
 	if (client->version >= 22) {
 		struct format_info fi;
diff --git a/src/modules/module-protocol-simple.c b/src/modules/module-protocol-simple.c
index 697a6b841ddd6008583989f91e398d68466e3277..e6f54a90cca6a574fb357bd17e1540e3b3631dde 100644
--- a/src/modules/module-protocol-simple.c
+++ b/src/modules/module-protocol-simple.c
@@ -29,7 +29,7 @@
 
 #include <pipewire/impl.h>
 
-/** \page page_module_protocol_simple PipeWire Module: Protocol Simple
+/** \page page_module_protocol_simple Protocol Simple
  *
  * The simple protocol provides a bidirectional audio stream on a network
  * socket.
@@ -40,6 +40,10 @@
  * Each client that connects will create a capture and/or playback stream,
  * depending on the configuration options.
  *
+ * ## Module Name
+ *
+ * `libpipewire-module-protocol-simple`
+ *
  * ## Module Options
  *
  *  - `capture`: boolean if capture is enabled. This will create a capture stream
diff --git a/src/modules/module-pulse-tunnel.c b/src/modules/module-pulse-tunnel.c
index 82be905ebef5d9f92f0ee8491f52d098715720c6..a11936f57be6130a450998b6382adbeaa0b0bc6d 100644
--- a/src/modules/module-pulse-tunnel.c
+++ b/src/modules/module-pulse-tunnel.c
@@ -35,7 +35,7 @@
 #include "module-protocol-pulse/defs.h"
 #include "module-protocol-pulse/format.h"
 
-/** \page page_module_pulse_tunnel PipeWire Module: Pulse Tunnel
+/** \page page_module_pulse_tunnel Pulse Tunnel
  *
  * The pulse-tunnel module provides a source or sink that tunnels all audio to
  * a remote PulseAudio connection.
@@ -47,6 +47,10 @@
  * automatically load the tunnel with the right parameters based on zeroconf
  * information.
  *
+ * ## Module Name
+ *
+ * `libpipewire-module-pulse-tunnel`
+ *
  * ## Module Options
  *
  * - `tunnel.mode`: the desired tunnel to create, must be `source` or `sink`.
diff --git a/src/modules/module-raop-discover.c b/src/modules/module-raop-discover.c
index b157e571e960c5bff431fdc8bd62dfff5d47b380..b583744681c45a85b1c00a73c140b6d998c3201b 100644
--- a/src/modules/module-raop-discover.c
+++ b/src/modules/module-raop-discover.c
@@ -26,7 +26,7 @@
 #include "module-protocol-pulse/format.h"
 #include "module-zeroconf-discover/avahi-poll.h"
 
-/** \page page_module_raop_discover PipeWire Module: RAOP Discover
+/** \page page_module_raop_discover RAOP Discover
  *
  * Automatically creates RAOP (Airplay) sink devices based on zeroconf
  * information.
@@ -37,6 +37,10 @@
  * If no stream.rules are given, it will create a sink for all announced
  * streams.
  *
+ * ## Module Name
+ *
+ * `libpipewire-module-raop-discover`
+ *
  * ## Module Options
  *
  * Options specific to the behavior of this module
diff --git a/src/modules/module-raop-sink.c b/src/modules/module-raop-sink.c
index c91f80a9f94401ed6acd557d7b1ee050ad46a78e..fee6d520b84bce371da34d56bc8dd85fee3cda39 100644
--- a/src/modules/module-raop-sink.c
+++ b/src/modules/module-raop-sink.c
@@ -47,7 +47,7 @@
 #include "module-rtp/rtp.h"
 #include "module-rtp/stream.h"
 
-/** \page page_module_raop_sink PipeWire Module: AirPlay Sink
+/** \page page_module_raop_sink AirPlay Sink
  *
  * Creates a new Sink to stream to an Airplay device.
  *
@@ -55,6 +55,10 @@
  * with the right parameters but it is possible to manually create a RAOP sink
  * as well.
  *
+ * ## Module Name
+ *
+ * `libpipewire-module-raop-sink`
+ *
  * ## Module Options
  *
  * Options specific to the behavior of this module
@@ -1854,6 +1858,8 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
 	if (pw_properties_get(props, PW_KEY_NODE_NAME) == NULL)
 		pw_properties_setf(props, PW_KEY_NODE_NAME, "raop_sink.%s.%s.%s",
 				hostname, ip, port);
+	if (pw_properties_get(props, PW_KEY_MEDIA_NAME) == NULL)
+		pw_properties_setf(props, PW_KEY_MEDIA_NAME, "RAOP to %s", name);
 	if (pw_properties_get(props, PW_KEY_NODE_DESCRIPTION) == NULL)
 		pw_properties_setf(props, PW_KEY_NODE_DESCRIPTION, "%s", name);
 	if (pw_properties_get(props, PW_KEY_NODE_LATENCY) == NULL)
@@ -1891,6 +1897,7 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
 	copy_props(impl, props, PW_KEY_NODE_VIRTUAL);
 	copy_props(impl, props, PW_KEY_MEDIA_CLASS);
 	copy_props(impl, props, PW_KEY_MEDIA_FORMAT);
+	copy_props(impl, props, PW_KEY_MEDIA_NAME);
 	copy_props(impl, props, "net.mtu");
 	copy_props(impl, props, "rtp.sender-ts-offset");
 	copy_props(impl, props, "sess.media");
diff --git a/src/modules/module-roc-sink.c b/src/modules/module-roc-sink.c
index 177877e3df7f89dc973642d0bc0b134114a4a4c3..a93dbc7824b8164d063605985fca3cd47920583e 100644
--- a/src/modules/module-roc-sink.c
+++ b/src/modules/module-roc-sink.c
@@ -19,17 +19,22 @@
 #include <roc/log.h>
 #include <roc/sender.h>
 
+#include <pipewire/cleanup.h>
 #include <pipewire/pipewire.h>
 #include <pipewire/impl.h>
 
 #include "module-roc/common.h"
 
-/** \page page_module_roc_sink PipeWire Module: ROC sink
+/** \page page_module_roc_sink ROC sink
  *
  * The `roc-sink` module creates a PipeWire sink that sends samples to
  * a preconfigured receiver address. One can then connect an audio stream
  * of any running application to that sink or make it the default sink.
  *
+ * ## Module Name
+ *
+ * `libpipewire-module-roc-sink`
+ *
  * ## Module Options
  *
  * Options specific to the behavior of this module
@@ -39,6 +44,7 @@
  * - `remote.ip = <str>`: remote receiver ip
  * - `remote.source.port = <str>`: remote receiver TCP/UDP port for source packets
  * - `remote.repair.port = <str>`: remote receiver TCP/UDP port for receiver packets
+ * - `remote.control.port = <str>`: remote receiver TCP/UDP port for control packets
  * - `fec.code = <str>`: Possible values: `disable`, `rs8m`, `ldpc`
  *
  * ## General options
@@ -58,6 +64,7 @@
  *          remote.ip = 192.168.0.244
  *          remote.source.port = 10001
  *          remote.repair.port = 10002
+ *          remote.control.port = 10003
  *          sink.name = "ROC Sink"
  *          sink.props = {
  *             node.name = "roc-sink"
@@ -77,7 +84,6 @@ PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
 struct module_roc_sink_data {
 	struct pw_impl_module *module;
 	struct spa_hook module_listener;
-	struct pw_properties *props;
 	struct pw_context *module_context;
 
 	struct pw_core *core;
@@ -100,6 +106,9 @@ struct module_roc_sink_data {
 	char *remote_ip;
 	int remote_source_port;
 	int remote_repair_port;
+
+	roc_endpoint *remote_control_addr;
+	int remote_control_port;
 };
 
 static void stream_destroy(void *d)
@@ -207,17 +216,13 @@ static void impl_destroy(struct module_roc_sink_data *data)
 		pw_core_disconnect(data->core);
 
 	pw_properties_free(data->capture_props);
-	pw_properties_free(data->props);
 
-	if (data->sender)
-		roc_sender_close(data->sender);
-	if (data->context)
-		roc_context_close(data->context);
+	spa_clear_ptr(data->sender, roc_sender_close);
+	spa_clear_ptr(data->context, roc_context_close);
 
-	if (data->remote_source_addr)
-		(void) roc_endpoint_deallocate(data->remote_source_addr);
-	if (data->remote_repair_addr)
-		(void) roc_endpoint_deallocate(data->remote_repair_addr);
+	spa_clear_ptr(data->remote_source_addr, roc_endpoint_deallocate);
+	spa_clear_ptr(data->remote_repair_addr, roc_endpoint_deallocate);
+	spa_clear_ptr(data->remote_control_addr, roc_endpoint_deallocate);
 
 	free(data->remote_ip);
 	free(data);
@@ -255,11 +260,11 @@ static int roc_sink_setup(struct module_roc_sink_data *data)
 		return -EINVAL;
 	}
 
-	memset(&sender_config, 0, sizeof(sender_config));
+	spa_zero(sender_config);
 
-	sender_config.frame_sample_rate = data->rate;
-	sender_config.frame_channels = ROC_CHANNEL_SET_STEREO;
-	sender_config.frame_encoding = ROC_FRAME_ENCODING_PCM_FLOAT;
+	sender_config.frame_encoding.rate = data->rate;
+	sender_config.frame_encoding.channels = ROC_CHANNEL_LAYOUT_STEREO;
+	sender_config.frame_encoding.format = ROC_FORMAT_PCM_FLOAT32;
 	sender_config.fec_encoding = data->fec_code;
 
 	info.rate = data->rate;
@@ -278,21 +283,7 @@ static int roc_sink_setup(struct module_roc_sink_data *data)
 		return -EINVAL;
 	}
 
-	switch (data->fec_code) {
-	case ROC_FEC_ENCODING_DEFAULT:
-	case ROC_FEC_ENCODING_RS8M:
-		audio_proto = ROC_PROTO_RTP_RS8M_SOURCE;
-		repair_proto = ROC_PROTO_RS8M_REPAIR;
-		break;
-	case ROC_FEC_ENCODING_LDPC_STAIRCASE:
-		audio_proto = ROC_PROTO_RTP_LDPC_SOURCE;
-		repair_proto = ROC_PROTO_LDPC_REPAIR;
-		break;
-	default:
-		audio_proto = ROC_PROTO_RTP;
-		repair_proto = 0;
-		break;
-	}
+	pw_roc_fec_encoding_to_proto(data->fec_code, &audio_proto, &repair_proto);
 
 	res = pw_roc_create_endpoint(&data->remote_source_addr, audio_proto, data->remote_ip, data->remote_source_port);
 	if (res < 0) {
@@ -320,6 +311,18 @@ static int roc_sink_setup(struct module_roc_sink_data *data)
 		}
 	}
 
+	res = pw_roc_create_endpoint(&data->remote_control_addr, PW_ROC_DEFAULT_CONTROL_PROTO, data->remote_ip, data->remote_control_port);
+	if (res < 0) {
+		pw_log_error("failed to create control endpoint: %s", spa_strerror(res));
+		return res;
+	}
+
+	if (roc_sender_connect(data->sender, ROC_SLOT_DEFAULT, ROC_INTERFACE_AUDIO_CONTROL,
+				data->remote_control_addr) != 0) {
+		pw_log_error("can't connect roc sender to remote control address");
+		return -EINVAL;
+	}
+
 	data->capture = pw_stream_new(data->core,
 			"roc-sink capture", data->capture_props);
 	data->capture_props = NULL;
@@ -354,6 +357,7 @@ static const struct spa_dict_item module_roc_sink_info[] = {
 				"remote.ip=<remote receiver ip> "
 				"( remote.source.port=<remote receiver port for source packets> ) "
 				"( remote.repair.port=<remote receiver port for repair packets> ) "
+				"( remote.control.port=<remote receiver port for control packets> ) "
 				"( sink.props= { key=val ... } ) " },
 	{ PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
 };
@@ -363,7 +367,7 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
 {
 	struct pw_context *context = pw_impl_module_get_context(module);
 	struct module_roc_sink_data *data;
-	struct pw_properties *props = NULL, *capture_props = NULL;
+	struct pw_properties *capture_props = NULL;
 	const char *str;
 	int res = 0;
 
@@ -376,13 +380,12 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
 	if (args == NULL)
 		args = "";
 
-	props = pw_properties_new_string(args);
+	spa_autoptr(pw_properties) props = pw_properties_new_string(args);
 	if (props == NULL) {
 		res = -errno;
 		pw_log_error( "can't create properties: %m");
 		goto out;
 	}
-	data->props = props;
 
 	capture_props = pw_properties_new(NULL, NULL);
 	if (capture_props == NULL) {
@@ -397,7 +400,6 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
 
 	if ((str = pw_properties_get(props, "sink.name")) != NULL) {
 		pw_properties_set(capture_props, PW_KEY_NODE_NAME, str);
-		pw_properties_set(props, "sink.name", NULL);
 	}
 
 	if ((str = pw_properties_get(props, "sink.props")) != NULL)
@@ -414,13 +416,12 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
 	if ((str = pw_properties_get(capture_props, PW_KEY_MEDIA_CLASS)) == NULL)
 		pw_properties_set(capture_props, PW_KEY_MEDIA_CLASS, "Audio/Sink");
 
-	data->rate = pw_properties_get_uint32(capture_props, PW_KEY_AUDIO_RATE, data->rate);
+	data->rate = pw_properties_get_uint32(capture_props, PW_KEY_AUDIO_RATE, 0);
 	if (data->rate == 0)
 		data->rate = PW_ROC_DEFAULT_RATE;
 
 	if ((str = pw_properties_get(props, "remote.ip")) != NULL) {
 		data->remote_ip = strdup(str);
-		pw_properties_set(props, "remote.ip", NULL);
 	} else {
 		pw_log_error("Remote IP not specified");
 		res = -EINVAL;
@@ -429,24 +430,28 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
 
 	if ((str = pw_properties_get(props, "remote.source.port")) != NULL) {
 		data->remote_source_port = pw_properties_parse_int(str);
-		pw_properties_set(props, "remote.source.port", NULL);
 	} else {
 		data->remote_source_port = PW_ROC_DEFAULT_SOURCE_PORT;
 	}
 
 	if ((str = pw_properties_get(props, "remote.repair.port")) != NULL) {
 		data->remote_repair_port = pw_properties_parse_int(str);
-		pw_properties_set(props, "remote.repair.port", NULL);
 	} else {
 		data->remote_repair_port = PW_ROC_DEFAULT_REPAIR_PORT;
 	}
+
+	if ((str = pw_properties_get(props, "remote.control.port")) != NULL) {
+		data->remote_control_port = pw_properties_parse_int(str);
+	} else {
+		data->remote_control_port = PW_ROC_DEFAULT_CONTROL_PORT;
+	}
+
 	if ((str = pw_properties_get(props, "fec.code")) != NULL) {
 		if (pw_roc_parse_fec_encoding(&data->fec_code, str)) {
 			pw_log_error("Invalid fec code %s, using default", str);
 			data->fec_code = ROC_FEC_ENCODING_DEFAULT;
 		}
 		pw_log_info("using fec.code %s %d", str, data->fec_code);
-		pw_properties_set(props, "fec.code", NULL);
 	} else {
 		data->fec_code = ROC_FEC_ENCODING_DEFAULT;
 	}
diff --git a/src/modules/module-roc-source.c b/src/modules/module-roc-source.c
index 102450891c08e8fdc46edd1bc073cd272899784c..aca3cd3e8fbb456adf8ba936224dd19ddcaa39ee 100644
--- a/src/modules/module-roc-source.c
+++ b/src/modules/module-roc-source.c
@@ -19,17 +19,22 @@
 #include <roc/log.h>
 #include <roc/receiver.h>
 
+#include <pipewire/cleanup.h>
 #include <pipewire/pipewire.h>
 #include <pipewire/impl.h>
 
 #include "module-roc/common.h"
 
-/** \page page_module_roc_source PipeWire Module: ROC source
+/** \page page_module_roc_source ROC source
  *
  * The `roc-source` module creates a PipeWire source that receives samples
  * from ROC sender and passes them to the sink it is connected to. One can
  * then connect it to any audio device.
  *
+ * ## Module Name
+ *
+ * `libpipewire-module-roc-source`
+ *
  * ## Module Options
  *
  * Options specific to the behavior of this module
@@ -39,6 +44,7 @@
  * - `local.ip = <str>`: local sender ip
  * - `local.source.port = <str>`: local receiver TCP/UDP port for source packets
  * - `local.repair.port = <str>`: local receiver TCP/UDP port for receiver packets
+ * - `local.control.port = <str>`: local receiver TCP/UDP port for control packets
  * - `sess.latency.msec = <str>`: target network latency in milliseconds
  * - `resampler.profile = <str>`: Possible values: `disable`, `high`,
  *   `medium`, `low`.
@@ -63,6 +69,7 @@
  *          sess.latency.msec = 5000
  *          local.source.port = 10001
  *          local.repair.port = 10002
+ *          local.control.port = 10003
  *          source.name = "ROC Source"
  *          source.props = {
  *             node.name = "roc-source"
@@ -82,7 +89,6 @@ PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
 struct module_roc_source_data {
 	struct pw_impl_module *module;
 	struct spa_hook module_listener;
-	struct pw_properties *props;
 	struct pw_context *module_context;
 
 	struct pw_core *core;
@@ -108,6 +114,9 @@ struct module_roc_source_data {
 	int local_source_port;
 	int local_repair_port;
 	int sess_latency_msec;
+
+	roc_endpoint *local_control_addr;
+	int local_control_port;
 };
 
 static void stream_destroy(void *d)
@@ -216,17 +225,13 @@ static void impl_destroy(struct module_roc_source_data *data)
 		pw_core_disconnect(data->core);
 
 	pw_properties_free(data->playback_props);
-	pw_properties_free(data->props);
 
-	if (data->receiver)
-		roc_receiver_close(data->receiver);
-	if (data->context)
-		roc_context_close(data->context);
+	spa_clear_ptr(data->receiver, roc_receiver_close);
+	spa_clear_ptr(data->context, roc_context_close);
 
-	if (data->local_source_addr)
-		(void) roc_endpoint_deallocate(data->local_source_addr);
-	if (data->local_repair_addr)
-		(void) roc_endpoint_deallocate(data->local_repair_addr);
+	spa_clear_ptr(data->local_source_addr, roc_endpoint_deallocate);
+	spa_clear_ptr(data->local_repair_addr, roc_endpoint_deallocate);
+	spa_clear_ptr(data->local_control_addr, roc_endpoint_deallocate);
 
 	free(data->local_ip);
 	free(data);
@@ -264,9 +269,10 @@ static int roc_source_setup(struct module_roc_source_data *data)
 	}
 
 	spa_zero(receiver_config);
-	receiver_config.frame_sample_rate = data->rate;
-	receiver_config.frame_channels = ROC_CHANNEL_SET_STEREO;
-	receiver_config.frame_encoding = ROC_FRAME_ENCODING_PCM_FLOAT;
+
+	receiver_config.frame_encoding.rate = data->rate;
+	receiver_config.frame_encoding.channels = ROC_CHANNEL_LAYOUT_STEREO;
+	receiver_config.frame_encoding.format = ROC_FORMAT_PCM_FLOAT32;
 	receiver_config.resampler_profile = data->resampler_profile;
 
 	info.rate = data->rate;
@@ -291,7 +297,7 @@ static int roc_source_setup(struct module_roc_source_data *data)
 	 * See API reference:
 	 * https://roc-streaming.org/toolkit/docs/api/reference.html
 	 */
-	receiver_config.target_latency = (unsigned long long)data->sess_latency_msec * 1000000ULL;
+	receiver_config.target_latency = (unsigned long long)data->sess_latency_msec * SPA_NSEC_PER_MSEC;
 
 	res = roc_receiver_open(data->context, &receiver_config, &data->receiver);
 	if (res) {
@@ -299,21 +305,7 @@ static int roc_source_setup(struct module_roc_source_data *data)
 		return -EINVAL;
 	}
 
-	switch (data->fec_code) {
-	case ROC_FEC_ENCODING_DEFAULT:
-	case ROC_FEC_ENCODING_RS8M:
-		audio_proto = ROC_PROTO_RTP_RS8M_SOURCE;
-		repair_proto = ROC_PROTO_RS8M_REPAIR;
-		break;
-	case ROC_FEC_ENCODING_LDPC_STAIRCASE:
-		audio_proto = ROC_PROTO_RTP_LDPC_SOURCE;
-		repair_proto = ROC_PROTO_LDPC_REPAIR;
-		break;
-	default:
-		audio_proto = ROC_PROTO_RTP;
-		repair_proto = 0;
-		break;
-	}
+	pw_roc_fec_encoding_to_proto(data->fec_code, &audio_proto, &repair_proto);
 
 	res = pw_roc_create_endpoint(&data->local_source_addr, audio_proto, data->local_ip, data->local_source_port);
 	if (res < 0) {
@@ -341,6 +333,18 @@ static int roc_source_setup(struct module_roc_source_data *data)
 		}
 	}
 
+	res = pw_roc_create_endpoint(&data->local_control_addr, PW_ROC_DEFAULT_CONTROL_PROTO, data->local_ip, data->local_control_port);
+	if (res < 0) {
+		pw_log_error("failed to create control endpoint: %s", spa_strerror(res));
+		return res;
+	}
+
+	if (roc_receiver_bind(data->receiver, ROC_SLOT_DEFAULT, ROC_INTERFACE_AUDIO_CONTROL,
+				data->local_control_addr) != 0) {
+		pw_log_error("can't connect roc receiver to local control address");
+		return -EINVAL;
+	}
+
 	data->playback = pw_stream_new(data->core,
 			"roc-source playback", data->playback_props);
 	data->playback_props = NULL;
@@ -378,6 +382,7 @@ static const struct spa_dict_item module_roc_source_info[] = {
 				"( local.ip=<local receiver ip> ) "
 				"( local.source.port=<local receiver port for source packets> ) "
 				"( local.repair.port=<local receiver port for repair packets> ) "
+				"( local.control.port=<local receiver port for control packets> ) "
 				"( source.props= { key=value ... } ) " },
 	{ PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
 };
@@ -387,7 +392,7 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
 {
 	struct pw_context *context = pw_impl_module_get_context(module);
 	struct module_roc_source_data *data;
-	struct pw_properties *props = NULL, *playback_props = NULL;
+	struct pw_properties *playback_props = NULL;
 	const char *str;
 	int res = 0;
 
@@ -400,13 +405,12 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
 	if (args == NULL)
 		args = "";
 
-	props = pw_properties_new_string(args);
+	spa_autoptr(pw_properties) props = pw_properties_new_string(args);
 	if (props == NULL) {
 		res = -errno;
 		pw_log_error( "can't create properties: %m");
 		goto out;
 	}
-	data->props = props;
 
 	playback_props = pw_properties_new(NULL, NULL);
 	if (playback_props == NULL) {
@@ -421,7 +425,6 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
 
 	if ((str = pw_properties_get(props, "source.name")) != NULL) {
 		pw_properties_set(playback_props, PW_KEY_NODE_NAME, str);
-		pw_properties_set(props, "source.name", NULL);
 	}
 
 	if ((str = pw_properties_get(props, "source.props")) != NULL)
@@ -436,34 +439,36 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
 	if (pw_properties_get(playback_props, PW_KEY_NODE_NETWORK) == NULL)
 		pw_properties_set(playback_props, PW_KEY_NODE_NETWORK, "true");
 
-	data->rate = pw_properties_get_uint32(playback_props, PW_KEY_AUDIO_RATE, data->rate);
+	data->rate = pw_properties_get_uint32(playback_props, PW_KEY_AUDIO_RATE, 0);
 	if (data->rate == 0)
 		data->rate = PW_ROC_DEFAULT_RATE;
 
 	if ((str = pw_properties_get(props, "local.ip")) != NULL) {
 		data->local_ip = strdup(str);
-		pw_properties_set(props, "local.ip", NULL);
 	} else {
 		data->local_ip = strdup(PW_ROC_DEFAULT_IP);
 	}
 
 	if ((str = pw_properties_get(props, "local.source.port")) != NULL) {
 		data->local_source_port = pw_properties_parse_int(str);
-		pw_properties_set(props, "local.source.port", NULL);
 	} else {
 		data->local_source_port = PW_ROC_DEFAULT_SOURCE_PORT;
 	}
 
 	if ((str = pw_properties_get(props, "local.repair.port")) != NULL) {
 		data->local_repair_port = pw_properties_parse_int(str);
-		pw_properties_set(props, "local.repair.port", NULL);
 	} else {
 		data->local_repair_port = PW_ROC_DEFAULT_REPAIR_PORT;
 	}
 
+	if ((str = pw_properties_get(props, "local.control.port")) != NULL) {
+		data->local_control_port = pw_properties_parse_int(str);
+	} else {
+		data->local_control_port = PW_ROC_DEFAULT_CONTROL_PORT;
+	}
+
 	if ((str = pw_properties_get(props, "sess.latency.msec")) != NULL) {
 		data->sess_latency_msec = pw_properties_parse_int(str);
-		pw_properties_set(props, "sess.latency.msec", NULL);
 	} else {
 		data->sess_latency_msec = PW_ROC_DEFAULT_SESS_LATENCY;
 	}
@@ -473,7 +478,6 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
 			pw_log_warn("Invalid resampler profile %s, using default", str);
 			data->resampler_profile = ROC_RESAMPLER_PROFILE_DEFAULT;
 		}
-		pw_properties_set(props, "resampler.profile", NULL);
 	} else {
 		data->resampler_profile = ROC_RESAMPLER_PROFILE_DEFAULT;
 	}
@@ -482,7 +486,6 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
 			pw_log_error("Invalid fec code %s, using default", str);
 			data->fec_code = ROC_FEC_ENCODING_DEFAULT;
 		}
-		pw_properties_set(props, "fec.code", NULL);
 	} else {
 		data->fec_code = ROC_FEC_ENCODING_DEFAULT;
 	}
diff --git a/src/modules/module-roc/common.h b/src/modules/module-roc/common.h
index 248c66ebd820620fac1231794ad0fd6a54113dbf..2164a342837f4d1da2b73c98360850b75e6d99b8 100644
--- a/src/modules/module-roc/common.h
+++ b/src/modules/module-roc/common.h
@@ -9,8 +9,10 @@
 #define PW_ROC_DEFAULT_IP "0.0.0.0"
 #define PW_ROC_DEFAULT_SOURCE_PORT 10001
 #define PW_ROC_DEFAULT_REPAIR_PORT 10002
+#define PW_ROC_DEFAULT_CONTROL_PORT 10003
 #define PW_ROC_DEFAULT_SESS_LATENCY 200
 #define PW_ROC_DEFAULT_RATE 44100
+#define PW_ROC_DEFAULT_CONTROL_PROTO ROC_PROTO_RTCP
 
 static inline int pw_roc_parse_fec_encoding(roc_fec_encoding *out, const char *str)
 {
@@ -31,8 +33,6 @@ static inline int pw_roc_parse_resampler_profile(roc_resampler_profile *out, con
 {
 	if (!str || !*str)
 		*out = ROC_RESAMPLER_PROFILE_DEFAULT;
-	else if (spa_streq(str, "disable"))
-		*out = ROC_RESAMPLER_PROFILE_DISABLE;
 	else if (spa_streq(str, "high"))
 		*out = ROC_RESAMPLER_PROFILE_HIGH;
 	else if (spa_streq(str, "medium"))
@@ -68,4 +68,23 @@ out_error_free_ep:
 	return -EINVAL;
 }
 
+static inline void pw_roc_fec_encoding_to_proto(roc_fec_encoding fec_code, roc_protocol *audio, roc_protocol *repair)
+{
+	switch (fec_code) {
+	case ROC_FEC_ENCODING_DEFAULT:
+	case ROC_FEC_ENCODING_RS8M:
+		*audio = ROC_PROTO_RTP_RS8M_SOURCE;
+		*repair = ROC_PROTO_RS8M_REPAIR;
+		break;
+	case ROC_FEC_ENCODING_LDPC_STAIRCASE:
+		*audio = ROC_PROTO_RTP_LDPC_SOURCE;
+		*repair = ROC_PROTO_LDPC_REPAIR;
+		break;
+	default:
+		*audio = ROC_PROTO_RTP;
+		*repair = 0;
+		break;
+	}
+}
+
 #endif /* MODULE_ROC_COMMON_H */
diff --git a/src/modules/module-rt.c b/src/modules/module-rt.c
index 41ec9caadb73cc4260a13b46446ed7975b852be3..6535058864d8b8e85821abd0f0439d717d8e5b01 100644
--- a/src/modules/module-rt.c
+++ b/src/modules/module-rt.c
@@ -57,7 +57,7 @@
 #include <dbus/dbus.h>
 #endif
 
-/** \page page_module_rt PipeWire Module: RT
+/** \page page_module_rt RT
  *
  * The `rt` modules can give real-time priorities to processing threads.
  *
@@ -69,6 +69,10 @@
  * up and DBus is available, then this module will fall back to using the Portal
  * Realtime DBus API or RTKit.
  *
+ * ## Module Name
+ *
+ * `libpipewire-module-rt`
+ *
  * ## Module Options
  *
  * - `nice.level`: The nice value set for the application thread. It improves
@@ -82,6 +86,8 @@
  * - `rlimits.enabled`: enable the use of rtlimits, default true.
  * - `rtportal.enabled`: enable the use of realtime portal, default true
  * - `rtkit.enabled`: enable the use of rtkit, default true
+ * - `uclamp.min`: the minimum utilisation value the scheduler should consider
+ * - `uclamp.max`: the maximum utilisation value the scheduler should consider
 
  * The nice level is by default set to an invalid value so that clients don't
  * automatically have the nice level raised.
@@ -101,6 +107,8 @@
  *         #rlimits.enabled = true
  *         #rtportal.enabled = true
  *         #rtkit.enabled = true
+ *         #uclamp.min = 0
+ *         #uclamp.max = 1024
  *     }
  *     flags = [ ifexists nofail ]
  * }
@@ -131,13 +139,18 @@ PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
 #define DEFAULT_RT_TIME_SOFT	-1
 #define DEFAULT_RT_TIME_HARD	-1
 
+#define DEFAULT_UCLAMP_MIN      0
+#define DEFAULT_UCLAMP_MAX      1024
+
 #define MODULE_USAGE	"( nice.level=<priority: default "SPA_STRINGIFY(DEFAULT_NICE_LEVEL)"(don't change)> ) "	\
 			"( rt.prio=<priority: default "SPA_STRINGIFY(DEFAULT_RT_PRIO)"> ) "		\
 			"( rt.time.soft=<in usec: default "SPA_STRINGIFY(DEFAULT_RT_TIME_SOFT)"> ) "	\
 			"( rt.time.hard=<in usec: default "SPA_STRINGIFY(DEFAULT_RT_TIME_HARD)"> ) "	\
 			"( rlimits.enabled=<default true> ) " \
 			"( rtportal.enabled=<default true> ) " \
-			"( rtkit.enabled=<default true> ) "
+			"( rtkit.enabled=<default true> ) " \
+			"( uclamp.min=<default "SPA_STRINGIFY(DEFAULT_UCLAMP_MIN)"> ) " \
+			"( uclamp.max=<default "SPA_STRINGIFY(DEFAULT_UCLAMP_MAX)"> )"
 
 static const struct spa_dict_item module_props[] = {
 	{ PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
@@ -183,6 +196,9 @@ struct impl {
 	rlim_t rt_time_soft;
 	rlim_t rt_time_hard;
 
+	int uclamp_min;
+	int uclamp_max;
+
 	struct spa_hook module_listener;
 
 	unsigned rlimits_enabled:1;
@@ -214,6 +230,8 @@ struct impl {
 #define RLIMIT_RTTIME 15
 #endif
 
+static pthread_mutex_t rlimit_lock = PTHREAD_MUTEX_INITIALIZER;
+
 static pid_t _gettid(void)
 {
 #if defined(HAVE_GETTID)
@@ -537,12 +555,15 @@ static bool check_realtime_privileges(struct impl *impl)
 	int err, old_policy, new_policy, min, max;
 	struct sched_param old_sched_params;
 	struct sched_param new_sched_params;
+	struct rlimit old_rlim;
+	struct rlimit no_rlim = { -1, -1 };
 	int try = 0;
+	bool ret = false;
 
 	if (!impl->rlimits_enabled)
-		return false;
+		return ret;
 
-	while (try++ < 2) {
+	while (!ret && try++ < 2) {
 		/* We could check `RLIMIT_RTPRIO`, but the BSDs generally don't have
 		 * that available, and there are also other ways to use realtime
 		 * scheduling without that rlimit being set such as `CAP_SYS_NICE` or
@@ -550,11 +571,11 @@ static bool check_realtime_privileges(struct impl *impl)
 		 * just try if setting realtime scheduling works or not. */
 		if ((err = pthread_getschedparam(pthread_self(), &old_policy, &old_sched_params)) != 0) {
 			pw_log_warn("Failed to check RLIMIT_RTPRIO: %s", strerror(err));
-			return false;
+			break;
 		}
 		if ((err = get_rt_priority_range(&min, &max)) < 0) {
 			pw_log_warn("Failed to get priority range: %s", strerror(err));
-			return false;
+			break;
 		}
 		if (try == 2) {
 #ifdef RLIMIT_RTPRIO
@@ -570,7 +591,7 @@ static bool check_realtime_privileges(struct impl *impl)
 		}
 		if (max < DEFAULT_RT_PRIO_MIN) {
 			pw_log_info("Priority max (%d) must be at least %d", max, DEFAULT_RT_PRIO_MIN);
-			return false;
+			break;
 		}
 
 		/* If the current scheduling policy has `SCHED_RESET_ON_FORK` set, then
@@ -584,14 +605,29 @@ static bool check_realtime_privileges(struct impl *impl)
 		if ((old_policy & PW_SCHED_RESET_ON_FORK) != 0)
 			new_policy |= PW_SCHED_RESET_ON_FORK;
 
-		if (pthread_setschedparam(pthread_self(), new_policy, &new_sched_params) == 0) {
+		/* Disable RLIMIT_RTTIME in a thread safe way and hope that the application
+		 * doesn't also set RLIMIT_RTTIME while trying new_policy. */
+		pthread_mutex_lock(&rlimit_lock);
+		if (getrlimit(RLIMIT_RTTIME, &old_rlim) < 0)
+			pw_log_info("getrlimit() failed: %m");
+		if (setrlimit(RLIMIT_RTTIME, &no_rlim) < 0)
+			pw_log_info("setrlimit() failed: %m");
+		if ((err = pthread_setschedparam(pthread_self(), new_policy, &new_sched_params)) == 0) {
 			impl->rt_prio = new_sched_params.sched_priority;
 			pthread_setschedparam(pthread_self(), old_policy, &old_sched_params);
-			return true;
-		}
+			ret = true;
+		} else
+			pw_log_info("failed to set realtime policy: %s", strerror(err));
+		if (setrlimit(RLIMIT_RTTIME, &old_rlim) < 0)
+			pw_log_info("setrlimit() failed: %m");
+		pthread_mutex_unlock(&rlimit_lock);
 	}
-	pw_log_info("Can't set rt prio to %d: %m (try increasing rlimits)", (int)priority);
-	return false;
+
+	if (ret)
+		pw_log_debug("can set rt prio to %d", (int)priority);
+	else
+		pw_log_info("can't set rt prio to %d (try increasing rlimits)", (int)priority);
+	return ret;
 }
 
 static int sched_set_nice(pid_t pid, int nice_level)
@@ -636,18 +672,20 @@ static int set_nice(struct impl *impl, int nice_level, bool warn)
 	return res;
 }
 
-static int set_rlimit(struct impl *impl)
+static int set_rlimit(struct rlimit *rlim)
 {
 	int res = 0;
 
-	if (setrlimit(RLIMIT_RTTIME, &impl->rl) < 0)
+	pthread_mutex_lock(&rlimit_lock);
+	if (setrlimit(RLIMIT_RTTIME, rlim) < 0)
 		res = -errno;
+	pthread_mutex_unlock(&rlimit_lock);
 
 	if (res < 0)
-		pw_log_debug("setrlimit() failed: %s", spa_strerror(res));
+		pw_log_info("setrlimit() failed: %s", spa_strerror(res));
 	else
 		pw_log_debug("rt.time.soft:%"PRIi64" rt.time.hard:%"PRIi64,
-				(int64_t)impl->rl.rlim_cur, (int64_t)impl->rl.rlim_max);
+				(int64_t)rlim->rlim_cur, (int64_t)rlim->rlim_max);
 
 	return res;
 }
@@ -1011,12 +1049,56 @@ static int do_rtkit_setup(struct spa_loop *loop, bool async, uint32_t seq,
 	impl->rl.rlim_cur = SPA_MIN(impl->rl.rlim_cur, impl->rttime_max);
 	impl->rl.rlim_max = SPA_MIN(impl->rl.rlim_max, impl->rttime_max);
 
-	set_rlimit(impl);
+	set_rlimit(&impl->rl);
 
 	return 0;
 }
 #endif /* HAVE_DBUS */
 
+int set_uclamp(int uclamp_min, int uclamp_max, pid_t pid) {
+#ifdef __linux__
+	int ret;
+	struct sched_attr {
+		uint32_t size;
+		uint32_t sched_policy;
+		uint64_t sched_flags;
+		int32_t sched_nice;
+		uint32_t sched_priority;
+		uint64_t sched_runtime;
+		uint64_t sched_deadline;
+		uint64_t sched_period;
+		uint32_t sched_util_min;
+		uint32_t sched_util_max;
+	} attr;
+
+	ret = syscall(SYS_sched_getattr, pid, &attr, sizeof(struct sched_attr), 0);
+	if (ret) {
+		pw_log_warn("Could not retrieve scheduler attributes: %d", -errno);
+		return -errno;
+	}
+
+	/* SCHED_FLAG_KEEP_POLICY |
+	 * SCHED_FLAG_KEEP_PARAMS |
+	 * SCHED_FLAG_UTIL_CLAMP_MIN |
+	 * SCHED_FLAG_UTIL_CLAMP_MAX */
+	attr.sched_flags = 0x8 | 0x10 | 0x20 | 0x40;
+	attr.sched_util_min = uclamp_min;
+	attr.sched_util_max = uclamp_max;
+
+	ret = syscall(SYS_sched_setattr, pid, &attr, 0);
+
+	if (ret) {
+		pw_log_warn("Could not set scheduler attributes: %d", -errno);
+		return -errno;
+	}
+	return 0;
+#else
+	pw_log_warn("Setting UCLAMP values is only supported on Linux");
+	return -EOPNOTSUPP;
+#endif /* __linux__ */
+}
+
+
 SPA_EXPORT
 int pipewire__module_init(struct pw_impl_module *module, const char *args)
 {
@@ -1047,6 +1129,8 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
 	impl->rlimits_enabled = pw_properties_get_bool(props, "rlimits.enabled", true);
 	impl->rtportal_enabled = pw_properties_get_bool(props, "rtportal.enabled", true);
 	impl->rtkit_enabled = pw_properties_get_bool(props, "rtkit.enabled", true);
+	impl->uclamp_min = pw_properties_get_int32(props, "uclamp.min", DEFAULT_UCLAMP_MIN);
+	impl->uclamp_max = pw_properties_get_int32(props, "uclamp.max", DEFAULT_UCLAMP_MAX);
 
 	impl->rl.rlim_cur = impl->rt_time_soft;
 	impl->rl.rlim_max = impl->rt_time_hard;
@@ -1086,7 +1170,15 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
 			use_rtkit = can_use_rtkit;
 	}
 	if (!use_rtkit)
-		set_rlimit(impl);
+		set_rlimit(&impl->rl);
+
+	if (impl->uclamp_max > 1024) {
+		pw_log_warn("uclamp.max out of bounds. Got %d, clamping to 1024.", impl->uclamp_max);
+		impl->uclamp_max = 1024;
+	}
+
+	if (impl->uclamp_min || impl->uclamp_max < 1024)
+		set_uclamp(impl->uclamp_min, impl->uclamp_max, impl->main_pid);
 
 #ifdef HAVE_DBUS
 	impl->use_rtkit = use_rtkit;
diff --git a/src/modules/module-rtp-sap.c b/src/modules/module-rtp-sap.c
index d5b887e5f140298e8b981929eabb199947984430..839c131ac7351cfea95118aa6200d4fc702fe7b4 100644
--- a/src/modules/module-rtp-sap.c
+++ b/src/modules/module-rtp-sap.c
@@ -28,7 +28,7 @@
 #define ifr_ifindex ifr_index
 #endif
 
-/** \page page_module_rtp_sap PipeWire Module: SAP Announce and create RTP streams
+/** \page page_module_rtp_sap SAP Announce and create RTP streams
  *
  * The `rtp-sap` module announces RTP streams that match the rules with the
  * announce-stream action.
@@ -40,6 +40,10 @@
  * sess.sap.announce = true and it will create a receiver for all announced
  * streams.
  *
+ * ## Module Name
+ *
+ * `libpipewire-module-rtp-sap`
+ *
  * ## Module Options
  *
  * Options specific to the behavior of this module
diff --git a/src/modules/module-rtp-session.c b/src/modules/module-rtp-session.c
index 9733f16f0540f82199a0f68dd1e4d032b80a1230..fad1b21a05d45f5f6796b05c4b613d11bdad6023 100644
--- a/src/modules/module-rtp-session.c
+++ b/src/modules/module-rtp-session.c
@@ -41,7 +41,7 @@
 #define ifr_ifindex ifr_index
 #endif
 
-/** \page page_module_rtp_session PipeWire Module: RTP session
+/** \page page_module_rtp_session RTP session
  *
  * The `rtp-session` module creates a media session that is announced
  * with avahi/mDNS/Bonjour.
@@ -52,6 +52,10 @@
  * The session setup is based on apple-midi and is compatible with
  * apple-midi when the session is using midi.
  *
+ * ## Module Name
+ *
+ * `libpipewire-module-rtp-session`
+ *
  * ## Module Options
  *
  * Options specific to the behavior of this module
diff --git a/src/modules/module-rtp-sink.c b/src/modules/module-rtp-sink.c
index 35595f99b1902e1445d6598ab1a5b6b17d849a53..0290e383e127ce32cd236def6fe1b00b029c84e1 100644
--- a/src/modules/module-rtp-sink.c
+++ b/src/modules/module-rtp-sink.c
@@ -32,11 +32,15 @@
 #define IPTOS_DSCP(x) ((x) & IPTOS_DSCP_MASK)
 #endif
 
-/** \page page_module_rtp_sink PipeWire Module: RTP sink
+/** \page page_module_rtp_sink RTP sink
  *
  * The `rtp-sink` module creates a PipeWire sink that sends audio
  * RTP packets.
  *
+ * ## Module Name
+ *
+ * `libpipewire-module-rtp-sink`
+ *
  * ## Module Options
  *
  * Options specific to the behavior of this module
diff --git a/src/modules/module-rtp-source.c b/src/modules/module-rtp-source.c
index 3659ec9c66a4d79df07b87e80b72ce95685a5964..825d94774f8b292f0162ad67c91c5d876cbd6885 100644
--- a/src/modules/module-rtp-source.c
+++ b/src/modules/module-rtp-source.c
@@ -35,7 +35,7 @@
 #define ifr_ifindex ifr_index
 #endif
 
-/** \page page_module_rtp_source PipeWire Module: RTP source
+/** \page page_module_rtp_source RTP source
  *
  * The `rtp-source` module creates a PipeWire source that receives audio
  * and midi RTP packets.
@@ -43,6 +43,10 @@
  * This module is usually loaded from the \ref page_module_rtp_sap so that the
  * source.ip and source.port and format parameters matches that of the sender.
  *
+ * ## Module Name
+ *
+ * `libpipewire-module-rtp-source`
+ *
  * ## Module Options
  *
  * Options specific to the behavior of this module
diff --git a/src/modules/module-rtp/stream.c b/src/modules/module-rtp/stream.c
index 217c9a876fa0aa529de5ad72ef98d2a4e220f603..1893f79b7d91edb953b7d3d6ba2f2f4645386331 100644
--- a/src/modules/module-rtp/stream.c
+++ b/src/modules/module-rtp/stream.c
@@ -420,7 +420,7 @@ struct rtp_stream *rtp_stream_new(struct pw_core *core,
 	} else {
 		impl->psamples = impl->mtu / impl->stride;
 		impl->psamples = SPA_CLAMP(impl->psamples, min_samples, max_samples);
-		if (direction == PW_DIRECTION_OUTPUT)
+		if (direction == PW_DIRECTION_INPUT)
 			pw_properties_setf(props, "rtp.ptime", "%f",
 					impl->psamples * 1000.0 / impl->rate);
 	}
diff --git a/src/modules/module-session-manager.c b/src/modules/module-session-manager.c
index 66f5b5efdc9aac7c70f23c8d1189086805aa8fdc..e0db896e5c796b75b38c0c4624934d4f776d0b23 100644
--- a/src/modules/module-session-manager.c
+++ b/src/modules/module-session-manager.c
@@ -7,10 +7,14 @@
 
 #include <pipewire/impl.h>
 
-/** \page page_module_session_manager PipeWire Module: Session Manager
+/** \page page_module_session_manager Session Manager
  *
  * This module implements some usefull objects for implementing a session
  * manager. It is not yet actively used.
+ *
+ * ## Module Name
+ *
+ * `libpipewire-module-session-manager`
  */
 
 /* client-endpoint.c */
diff --git a/src/modules/module-vban-recv.c b/src/modules/module-vban-recv.c
index 72be2e72216cdaa84d7d4f9c4e62ff6828300619..da8cfde9fbdc3e070b1c500a949161971bd3383b 100644
--- a/src/modules/module-vban-recv.c
+++ b/src/modules/module-vban-recv.c
@@ -35,11 +35,15 @@
 #define ifr_ifindex ifr_index
 #endif
 
-/** \page page_module_vban_recv PipeWire Module: VBAN receiver
+/** \page page_module_vban_recv VBAN receiver
  *
  * The `vban-recv` module creates a PipeWire source that receives audio
  * and midi [VBAN](https://vb-audio.com) packets.
  *
+ * ## Module Name
+ *
+ * `libpipewire-module-vban-recv`
+ *
  * ## Module Options
  *
  * Options specific to the behavior of this module
diff --git a/src/modules/module-vban-send.c b/src/modules/module-vban-send.c
index 26961b81bf8a8f64e49ae136b6f6bf0c60d6453a..896d82aaa80f02e82aff421cb0248975750c4ae9 100644
--- a/src/modules/module-vban-send.c
+++ b/src/modules/module-vban-send.c
@@ -32,11 +32,15 @@
 #define IPTOS_DSCP(x) ((x) & IPTOS_DSCP_MASK)
 #endif
 
-/** \page page_module_vban_send PipeWire Module: VBAN sender
+/** \page page_module_vban_send VBAN sender
  *
  * The `vban-send` module creates a PipeWire sink that sends
  * audio and midi [VBAN](https://vb-audio.com) packets.
  *
+ * ## Module Name
+ *
+ * `libpipewire-module-vban-send`
+ *
  * ## Module Options
  *
  * Options specific to the behavior of this module
diff --git a/src/modules/module-x11-bell.c b/src/modules/module-x11-bell.c
index 3359ce8452985f6d34791a04c06d3c0625c649bb..0e3caae06d0b2e21b8c03b539ba729aa14ed4ee1 100644
--- a/src/modules/module-x11-bell.c
+++ b/src/modules/module-x11-bell.c
@@ -27,11 +27,15 @@
 #include <pipewire/pipewire.h>
 #include <pipewire/impl.h>
 
-/** \page page_module_x11_bell PipeWire Module: X11 Bell
+/** \page page_module_x11_bell X11 Bell
  *
  * The `x11-bell` module intercept the X11 bell events and uses libcanberra to
  * play a sound.
  *
+ * ## Module Name
+ *
+ * `libpipewire-module-x11-bell`
+ *
  * ## Module Options
  *
  * - `sink.name = <str>`: node.name of the sink to connect to
diff --git a/src/modules/module-zeroconf-discover.c b/src/modules/module-zeroconf-discover.c
index 4c4421b94ce5cc5267333d72554c79d95e5f120b..15c5e46bd8a9600d24ca485f73025a24b8ba5ce0 100644
--- a/src/modules/module-zeroconf-discover.c
+++ b/src/modules/module-zeroconf-discover.c
@@ -27,13 +27,17 @@
 #include "module-protocol-pulse/format.h"
 #include "module-zeroconf-discover/avahi-poll.h"
 
-/** \page page_module_zeroconf_discover PipeWire Module: Zeroconf Discover
+/** \page page_module_zeroconf_discover Zeroconf Discover
  *
  * Use zeroconf to detect and load module-pulse-tunnel with the right
  * parameters. This will automatically create sinks and sources to stream
  * audio to/from remote PulseAudio servers. It also works with
  * module-protocol-pulse.
  *
+ * ## Module Name
+ *
+ * `libpipewire-module-zeroconf-discover`
+ *
  * ## Module Options
  *
  * - `pulse.latency`: the latency to end-to-end latency in milliseconds to
diff --git a/src/pipewire/context.c b/src/pipewire/context.c
index b73dcc9c3a1e039d22154a30163fa8cec4f5af99..1046fc9ffe82c7a508754ee7b2ac0cc122541f09 100644
--- a/src/pipewire/context.c
+++ b/src/pipewire/context.c
@@ -32,6 +32,8 @@
 PW_LOG_TOPIC_EXTERN(log_context);
 #define PW_LOG_TOPIC_DEFAULT log_context
 
+#define MAX_HOPS	64
+
 /** \cond */
 struct impl {
 	struct pw_context this;
@@ -791,12 +793,17 @@ static int ensure_state(struct pw_impl_node *node, bool running)
  * and groups to active nodes and make them recursively runnable as well.
  */
 static inline int run_nodes(struct pw_context *context, struct pw_impl_node *node,
-		struct spa_list *nodes, enum pw_direction direction)
+		struct spa_list *nodes, enum pw_direction direction, int hop)
 {
 	struct pw_impl_node *t;
 	struct pw_impl_port *p;
 	struct pw_impl_link *l;
 
+	if (hop == MAX_HOPS) {
+		pw_log_warn("exceeded hops (%d)", hop);
+		return -EIO;
+	}
+
 	pw_log_debug("node %p: '%s' direction:%s", node, node->name,
 			pw_direction_as_string(direction));
 
@@ -810,10 +817,12 @@ static inline int run_nodes(struct pw_context *context, struct pw_impl_node *nod
 				if (!t->active || !l->prepared ||
 				    (!t->driving && SPA_FLAG_IS_SET(t->checked, 1u<<direction)))
 					continue;
+				if (t->driving && p->node == t)
+					continue;
 
 				pw_log_debug("  peer %p: '%s'", t, t->name);
 				t->runnable = true;
-				run_nodes(context, t, nodes, direction);
+				run_nodes(context, t, nodes, direction, hop + 1);
 			}
 		}
 	} else {
@@ -824,10 +833,12 @@ static inline int run_nodes(struct pw_context *context, struct pw_impl_node *nod
 				if (!t->active || !l->prepared ||
 				    (!t->driving && SPA_FLAG_IS_SET(t->checked, 1u<<direction)))
 					continue;
+				if (t->driving && p->node == t)
+					continue;
 
 				pw_log_debug("  peer %p: '%s'", t, t->name);
 				t->runnable = true;
-				run_nodes(context, t, nodes, direction);
+				run_nodes(context, t, nodes, direction, hop + 1);
 			}
 		}
 	}
@@ -847,7 +858,7 @@ static inline int run_nodes(struct pw_context *context, struct pw_impl_node *nod
 			pw_log_debug("  group %p: '%s'", t, t->name);
 			t->runnable = true;
 			if (!t->driving)
-				run_nodes(context, t, nodes, direction);
+				run_nodes(context, t, nodes, direction, hop + 1);
 		}
 	}
 	return 0;
@@ -950,8 +961,8 @@ static int collect_nodes(struct pw_context *context, struct pw_impl_node *node,
 	}
 	spa_list_for_each(n, collect, sort_link)
 		if (!n->driving && n->runnable) {
-			run_nodes(context, n, collect, PW_DIRECTION_OUTPUT);
-			run_nodes(context, n, collect, PW_DIRECTION_INPUT);
+			run_nodes(context, n, collect, PW_DIRECTION_OUTPUT, 0);
+			run_nodes(context, n, collect, PW_DIRECTION_INPUT, 0);
 		}
 
 	return 0;
diff --git a/src/pipewire/filter.c b/src/pipewire/filter.c
index 0561b9c8697b0dea9c175dae195150d3cf5e5e77..6dbf050ea8efae551baf9e30eb56e40cc224ab2e 100644
--- a/src/pipewire/filter.c
+++ b/src/pipewire/filter.c
@@ -1334,7 +1334,7 @@ pw_filter_new_simple(struct pw_loop *loop,
 	if (props == NULL)
 		return NULL;
 
-	context = pw_context_new(loop, NULL, 0);
+	context = pw_context_new(loop, pw_properties_copy(props), 0);
 	if (context == NULL) {
 		res = -errno;
 		goto error_cleanup;
diff --git a/src/pipewire/impl-client.c b/src/pipewire/impl-client.c
index ae5a1b1f46d5bc821342651ffca6084a660625ec..d5d865a6bd31ce1c352ca510a1ddcd0c8cc1949b 100644
--- a/src/pipewire/impl-client.c
+++ b/src/pipewire/impl-client.c
@@ -165,7 +165,7 @@ static bool check_client_property_update(struct pw_impl_client *client,
 
 	/* Refuse specific restricted keys */
 	if (has_key(ignored, key))
-		goto deny;
+		goto ignore;
 
 	/* Refuse all security keys */
 	if (spa_strstartswith(key, "pipewire.sec."))
@@ -184,6 +184,7 @@ deny:
 	if (!spa_streq(old, new))
 		pw_log_warn("%p: refuse property update '%s' from '%s' to '%s'",
 				client, key, old ? old : "<unset>", new ? new : "<unset>");
+ignore:
 	return false;
 }
 
diff --git a/src/pipewire/impl-client.h b/src/pipewire/impl-client.h
index f9278f5f8a6c5b95174686f52b6c8cffd3016de0..de9325cbd7281d15130ac7b8325e811dad64887b 100644
--- a/src/pipewire/impl-client.h
+++ b/src/pipewire/impl-client.h
@@ -12,6 +12,8 @@ extern "C" {
 #include <spa/utils/hook.h>
 
 /** \page page_client_impl Client Implementation
+ *
+ * \see \ref pw_impl_client
  *
  * \section sec_page_client_impl_overview Overview
  *
@@ -51,7 +53,7 @@ extern "C" {
  * Each client has its own list of resources it is bound to along with
  * a mapping between the client types and server types.
  *
- * See: \ref page_client_impl
+ * \see \ref page_client_impl
  */
 
 /**
diff --git a/src/pipewire/impl-link.c b/src/pipewire/impl-link.c
index d1984ac14999b58dae748cb1f490be6052636bd5..b8224753d6159e6195d837e99183a8641178b235 100644
--- a/src/pipewire/impl-link.c
+++ b/src/pipewire/impl-link.c
@@ -884,9 +884,11 @@ int pw_impl_link_deactivate(struct pw_impl_link *this)
 
 	impl->activated = false;
 	pw_log_info("(%s) deactivated", this->name);
-	link_update_state(this, this->destroyed ?
-			PW_LINK_STATE_INIT : PW_LINK_STATE_PAUSED,
-			0, NULL);
+	
+	if (this->info.state < PW_LINK_STATE_PAUSED || this->destroyed)
+		link_update_state(this, PW_LINK_STATE_INIT, 0, NULL);
+	else
+		link_update_state(this, PW_LINK_STATE_PAUSED, 0, NULL);
 	return 0;
 }
 
diff --git a/src/pipewire/impl-module.c b/src/pipewire/impl-module.c
index 23df06ccaec3fce5a6ea6199de28dcb2e0dc3f73..561f05c7202e187cce31aa3e03e2eb62ea384573 100644
--- a/src/pipewire/impl-module.c
+++ b/src/pipewire/impl-module.c
@@ -323,7 +323,7 @@ void pw_impl_module_destroy(struct pw_impl_module *module)
 		pw_work_queue_cancel(pw_context_get_work_queue(module->context),
 				     module, SPA_ID_INVALID);
 
-	if (!pw_in_valgrind() && dlclose(impl->hnd) != 0)
+	if (pw_should_dlclose() && dlclose(impl->hnd) != 0)
 		pw_log_warn("%p: dlclose failed: %s", module, dlerror());
 	free(impl);
 }
diff --git a/src/pipewire/pipewire.c b/src/pipewire/pipewire.c
index ada0d13be4fd25c43de13b55340826e310f8dee1..62e9ca57ff12a976eb41964f60aa24f1b90b09aa 100644
--- a/src/pipewire/pipewire.c
+++ b/src/pipewire/pipewire.c
@@ -149,7 +149,7 @@ unref_plugin(struct plugin *plugin)
 	if (--plugin->ref == 0) {
 		spa_list_remove(&plugin->link);
 		pw_log_debug("unloaded plugin:'%s'", plugin->filename);
-		if (global_support.do_dlclose)
+		if (pw_should_dlclose())
 			dlclose(plugin->hnd);
 		free(plugin->filename);
 		free(plugin);
@@ -785,10 +785,10 @@ const char *pw_get_host_name(void)
 	return hname;
 }
 
-SPA_EXPORT
-bool pw_in_valgrind(void)
+bool
+pw_should_dlclose(void)
 {
-	return global_support.in_valgrind;
+	return global_support.do_dlclose;
 }
 
 SPA_EXPORT
diff --git a/src/pipewire/pipewire.h b/src/pipewire/pipewire.h
index e5d078fda62670e625fdfcf877f50c1f7d25604c..0c495ed39f221851b81841b4d3d5a5b8ffee0ef1 100644
--- a/src/pipewire/pipewire.h
+++ b/src/pipewire/pipewire.h
@@ -72,8 +72,6 @@ pw_get_host_name(void);
 const char *
 pw_get_client_name(void);
 
-bool pw_in_valgrind(void);
-
 bool pw_check_option(const char *option, const char *value);
 
 enum pw_direction
diff --git a/src/pipewire/private.h b/src/pipewire/private.h
index 58b2b6085d5b87f7f3a7e375801c5e1b6b2c99b6..021a5d5f9a97eeaf7313ce35d6bbd41fe3d3aa78 100644
--- a/src/pipewire/private.h
+++ b/src/pipewire/private.h
@@ -1292,6 +1292,8 @@ void pw_settings_init(struct pw_context *context);
 int pw_settings_expose(struct pw_context *context);
 void pw_settings_clean(struct pw_context *context);
 
+bool pw_should_dlclose(void);
+
 /** \endcond */
 
 #ifdef __cplusplus
diff --git a/src/pipewire/proxy.h b/src/pipewire/proxy.h
index e92a44e43f603fe07e2a0bf0f0ca3079b8fad097..b46c417253f4d4b14e5257337b6d7d95fc568456 100644
--- a/src/pipewire/proxy.h
+++ b/src/pipewire/proxy.h
@@ -12,6 +12,8 @@ extern "C" {
 #include <spa/utils/hook.h>
 
 /** \page page_proxy Proxy
+ *
+ * \see \ref pw_proxy
  *
  * \section sec_page_proxy_overview Overview
  *
@@ -76,7 +78,7 @@ extern "C" {
  * invoked by the client to PipeWire messages. Events will call the handlers
  * set in listener.
  *
- * See \ref page_proxy
+ * \see \ref page_proxy
  */
 
 /**
diff --git a/src/pipewire/stream.c b/src/pipewire/stream.c
index db2c3f7b7f337dd512b8c612374e96d0d1c28278..39e603b2d4d3e8d34842372884cf44a06d8eeae7 100644
--- a/src/pipewire/stream.c
+++ b/src/pipewire/stream.c
@@ -1595,7 +1595,7 @@ pw_stream_new_simple(struct pw_loop *loop,
 	if (props == NULL)
 		return NULL;
 
-	context = pw_context_new(loop, NULL, 0);
+	context = pw_context_new(loop, pw_properties_copy(props), 0);
 	if (context == NULL) {
 		res = -errno;
 		goto error_cleanup;
diff --git a/src/pipewire/stream.h b/src/pipewire/stream.h
index f394eaf0b0a5a38dba5c425d1f0bfb3c47fb2154..bea90b45880cefa2a9b7e392138b1949b48d4aa2 100644
--- a/src/pipewire/stream.h
+++ b/src/pipewire/stream.h
@@ -10,6 +10,8 @@ extern "C" {
 #endif
 
 /** \page page_streams Streams
+ *
+ * \see \ref pw_stream
  *
  * \section sec_overview Overview
  *
@@ -162,7 +164,7 @@ extern "C" {
  * The stream object provides a convenient way to send and
  * receive data streams from/to PipeWire.
  *
- * See also \ref page_streams and \ref api_pw_core
+ * \see \ref page_streams, \ref api_pw_core
  */
 
 /**
diff --git a/src/pipewire/thread-loop.h b/src/pipewire/thread-loop.h
index 1d5725f03dbade52a9b96a09178d498f4e701430..f1eb1910ea98e08247369b670b07d7b58cdf2c38 100644
--- a/src/pipewire/thread-loop.h
+++ b/src/pipewire/thread-loop.h
@@ -12,6 +12,8 @@ extern "C" {
 #include <pipewire/loop.h>
 
 /** \page page_thread_loop Thread Loop
+ *
+ * \see \ref pw_thread_loop
  *
  * \section sec_thread_loop_overview Overview
  *
@@ -68,7 +70,7 @@ extern "C" {
  * All of the loop callbacks will be executed with the loop
  * lock held.
  *
- * See also \ref page_thread_loop
+ * \see \ref page_thread_loop
  */
 
 /**
diff --git a/src/pipewire/utils.c b/src/pipewire/utils.c
index 3bbbb170cc6023e4fe115191eedbc98a527807d1..aeb0f7803a2a8f0c6f8028fc6682abee2d2d8f20 100644
--- a/src/pipewire/utils.c
+++ b/src/pipewire/utils.c
@@ -162,7 +162,7 @@ char **pw_strv_parse(const char *val, size_t len, int max_tokens, int *n_tokens)
  * \since 0.3.84
  */
 SPA_EXPORT
-int pw_strv_find(char **a, char *b)
+int pw_strv_find(char **a, const char *b)
 {
 	int i;
 	if (a == NULL || b == NULL)
diff --git a/src/pipewire/utils.h b/src/pipewire/utils.h
index 92f8d004a37e970d1e79da7448479c9b72a67231..9889fd7f5b8f388915012aca818d2fc2c425b549 100644
--- a/src/pipewire/utils.h
+++ b/src/pipewire/utils.h
@@ -48,7 +48,7 @@ pw_split_ip(char *str, const char *delimiter, int max_tokens, char *tokens[]);
 
 char **pw_strv_parse(const char *val, size_t len, int max_tokens, int *n_tokens);
 
-int pw_strv_find(char **a, char *b);
+int pw_strv_find(char **a, const char *b);
 
 int pw_strv_find_common(char **a, char **b);
 
diff --git a/src/tools/pw-top.c b/src/tools/pw-top.c
index 50a0a5a987caa026ea472a2a8a0c1565b9f5c1d3..aafa0d5ebe5b8e7cf78bf7f939089f4f9a28bdf1 100644
--- a/src/tools/pw-top.c
+++ b/src/tools/pw-top.c
@@ -282,7 +282,7 @@ done:
 }
 
 static const struct pw_node_events node_events = {
-	PW_VERSION_NODE,
+	PW_VERSION_NODE_EVENTS,
 	.info = node_info,
 	.param = node_param,
 };