diff --git a/.cirrus.yml b/.cirrus.yml
index f99e8dbcc29fd1180ce0da232a07a57258b0bc1b..d26c4431fa1773e1bedad5b37f40734d3d82b174 100644
--- a/.cirrus.yml
+++ b/.cirrus.yml
@@ -1,7 +1,7 @@
 task:
   freebsd_instance:
     matrix:
-      - image_family: freebsd-13-0-snap
+      - image_family: freebsd-13-1-snap
   env:
     # /usr/ports/Mk/Uses/localbase.mk localbase:ldflags
     LOCALBASE: /usr/local
diff --git a/INSTALL.md b/INSTALL.md
index c66af68261e29eca21e0802e023c405ccbcb814e..8969a815b779ec62d96638578d1b25a305617de2 100644
--- a/INSTALL.md
+++ b/INSTALL.md
@@ -1,8 +1,8 @@
 ## Building
 
-PipeWire uses a build tool called *Meson* as a basis for its build
+PipeWire uses a build tool called [*Meson*](https://mesonbuild.com) as a basis for its build
 process.  It's a tool with some resemblance to Autotools and CMake. Meson
-again generates build files for a lower level build tool called *Ninja*,
+again generates build files for a lower level build tool called [*Ninja*](https://ninja-build.org/),
 working in about the same level of abstraction as more familiar GNU Make
 does.
 
@@ -39,7 +39,7 @@ section. They are defined in `meson_options.txt`.
 Finally, invoke the build:
 
 ```
-$ ninja -C builddir
+$ meson compile -C builddir
 ```
 
 Just to avoid any confusion: `autogen.sh` is a script invoked by *Jhbuild*,
@@ -68,10 +68,10 @@ make run
 ```
 
 This will use the default config file to configure and start the daemon.
-The default config will also start pipewire-media-session, a default
-example media session and pipewire-pulse, a PulseAudio compatible server.
+The default config will also start `pipewire-media-session`, a default
+example media session and `pipewire-pulse`, a PulseAudio compatible server.
 
-You can also enable more debugging with the PIPEWIRE_DEBUG environment
+You can also enable more debugging with the `PIPEWIRE_DEBUG` environment
 variable like so:
 
 ```
@@ -92,14 +92,15 @@ systemctl --user stop pipewire.service \
 
 ## Installing
 
-PipeWire comes with quite a bit of libraries and tools, run
-inside `builddir`:
+PipeWire comes with quite a bit of libraries and tools, run:
 
 ```
-sudo meson install
+meson install -C builddir
 ```
 
 to install everything onto the system into the specified prefix.
+Depending on the configured installation prefix, the above command
+may need to be run with elevated privileges (e.g. with `sudo`).
 Some additional steps will have to be performed to integrate
 with the distribution as shown below.
 
@@ -111,7 +112,7 @@ pipewire-pulse process running. PipeWire is usually started as a
 systemd unit using socket activation or as a service.
 
 Configuration of the PipeWire daemon can be found in
-/usr/share/pipewire/pipewire.conf. Please refer to the comments in the
+`/usr/share/pipewire/pipewire.conf`. Please refer to the comments in the
 config file for more information about the configuration options.
 
 The daemon is started with:
@@ -150,14 +151,14 @@ There is also a config file installed in:
 ```
 
 The plugin will be picked up by alsa when the following files
-are in /etc/alsa/conf.d/
+are in `/etc/alsa/conf.d/`:
 
 ```
 /etc/alsa/conf.d/50-pipewire.conf -> /usr/share/alsa/alsa.conf.d/50-pipewire.conf
 /etc/alsa/conf.d/99-pipewire-default.conf
 ```
 
-With this setup, aplay -l should list a pipewire: device that can be used as
+With this setup, `aplay -l` should list a pipewire device that can be used as
 a regular alsa device for playback and record.
 
 ### JACK emulation
@@ -180,7 +181,7 @@ These libraries are found here:
 
 ```
 
-The provided pw-jack script uses LD_LIBRARY_PATH to set the library
+The provided `pw-jack` script uses `LD_LIBRARY_PATH` to set the library
 search path to these replacement libraries. This allows you to run
 jack apps on both the real JACK server or on PipeWire with the script.
 
@@ -193,7 +194,7 @@ contents like:
 ```
 
 Note that when JACK is replaced by PipeWire, the SPA JACK plugin (installed
-in /usr/lib64/spa-0.2/jack/libspa-jack.so) is not useful anymore and
+in `/usr/lib64/spa-0.2/jack/libspa-jack.so`) is not useful anymore and
 distributions should make them conflict.
 
 
@@ -216,14 +217,21 @@ systemctl --user start pipewire-pulse.service pipewire-pulse.socket
 ```
 
 You can also start additional PulseAudio servers listening on other
-sockets with the -a option. See `pipewire-pulse -h` for more info.
+sockets with the `-a` option. See `pipewire-pulse -h` for more info.
 
 
 ## Uninstalling
 
-To uninstall, in the `builddir` directory run:
+To uninstall, run:
 
 ```
-sudo ninja uninstall
+ninja -C builddir uninstall
 ```
 
+Depending on the configured installation prefix, the above command
+may need to be run with elevated privileges (e.g. with `sudo`).
+
+Note that at the time of writing uninstallation only works with the
+same build directory that was used for installation. Meson stores the
+list of installed files in the build directory, and this list is
+necessary for uninstallation to work.
diff --git a/NEWS b/NEWS
index b7a4e1a79016c8a67a75eb872daf4014e7666dc9..eec8787760b81e07aeddfd058993d7261c2f528c 100644
--- a/NEWS
+++ b/NEWS
@@ -1,3 +1,326 @@
+# PipeWire 0.3.57 (2022-09-02)
+
+This is a bugfix release that is API and ABI compatible with previous
+0.3.x releases.
+
+## Highlights
+
+## PipeWire
+  - Support masking of conf.d/ files. (#2629)
+  - Add some more debug info to memfd.
+  - Improve data-loop invoke method. Also flush pending items. (#2631)
+  - Add a filter-chain systemd service file than can be used to start
+    custom filters placed in ~/.conf/pipewire/filter-chain.d/ (#2553)
+  - Improve triggered timestamps for remote nodes.
+  - Fix some potential cross compilation problems due to wrong
+    host_machine.
+  - Check return values of pw_getrandom().
+
+
+## Tools
+  - Updates to pw-cli manpages. (#2552)
+  - Remove the pw-cli dump command. It is mostly implemented as part of
+    wpctl status, pw-dump, pw-link, pw-top and others.
+  - Clean up resource in pw-cat correctly on errors. (#2651)
+
+## Modules
+  - Fix compilation of AVB on big-endian. Enable AVB only on Linux.
+  - Use org.freedesktop.portal.Realtime when available. This does the
+    correct PID/TID mappings to make realtime also work from flatpaks.
+  - Fix compilation of ROC module when headers are missing. (#2513)
+  - Improve some error cleanup paths in protocol-native. Improve connect
+    and disconnect.
+  - Fix a potential crash in FFT unload in filter-chain.
+  - Implement PIPEWIRE_NOTIFICATION_FD for notification when the socket
+    is ready.
+  - Try to use rtkit if set_nice() fails.
+  - Fix rate adjustement logic in pulse-tunnel. This would cause
+    increasing delays and hickups when using tunnels. (#2548)
+  - Handle disconnect in pulse-tunnel.
+
+## Bluetooth
+  - Add OPUS as a new vendor codec. Add OPUS-A2DP spec. PipeWire can now
+    send and reveive OPUS data over bluetooth.
+  - An AAC decoder was added so that PipeWire can now also function as
+    an A2DP AAC receiver.
+
+## SPA
+  - Tweak the resampler window function some more. (#2574)
+  - Improve format convert performance in some fallback cases.
+  - Fix rounding in format conversion on ARM NEON.
+  - Fix libcamera build error. (#2575)
+  - Fix some issues where the wrong samplerate was used. (#2614)
+  - Don't wait for more samples that can fit in the ringbuffer in ALSA.
+  - Improve buffer size handling in audioconvert, scale the buffers based
+    on the rate conversion and make things work with really large rate
+    conversions as well.
+  - Add more and better debug for ALSA devices.
+  - Improve channel mix: Filter FC and LFE when copying from a different
+    layout. Implement STEREO from FC. Avoid generating REAR from FC in PSD
+    mode.
+  - Fix rate match for sources. This fixes an error where follower sources
+    would generate many resync warnings.
+  - Improve ALSA format negotiation. If the ALSA node is not running and
+    there was a previously configured format, close and reopen the device
+    to enumerate and accept all possible formats again. (#2625).
+
+## ALSA
+  - The alsa plugin will now also save the volumes set with the control
+    API. This saves the volumes set with alsa-mixer, for example.
+
+## Pulse-server
+  - Flatpak apps with devices=all (Zoom) will now be granted Manager
+    permissions.
+  - Small tweaks to the amount of data sent to clients to work around an
+    issue in freerdp.
+
+## JACK
+  - Clean up the transport correctly when closing a client. (#2569)
+  - Match context properties in addition to node properties for the jack
+    client rules. (#2580)
+  - Make sure to return an error when disconnected from the server. (#2606)
+  - Fix thread cast problem in jack_client_thread_id().
+  - Increase jack_client_name_size() length and make sure we have space for
+    the \0 byte.
+  - JACK clients from the same application will be added to the same group
+    so that they share the quantum and rate.
+
+
+Older versions:
+
+# PipeWire 0.3.56 (2022-07-19)
+
+This is a quick bugfix release that is API and ABI compatible with previous
+0.3.x releases.
+
+## Highlights
+  - A critical bug that could crash JACK apps was fixed.
+  - Some more regressions in audiomixer were fixed. This should fix crackling
+    and stuttering in some cases as well as some channel mapping regressions.
+  - A bug in the alsa plugin was fixed that could cause stuttering in VMs.
+  - Bluetooth sources should have improved latency and rate control.
+  - Many more bugfixes and improvements.
+
+
+## Modules
+  - An experimental AVB module was added. It can expose PipeWire as an AVB
+    entity and initiate (broken) streaming between entities.
+  - module-loopback now handles the cases where the input and output channels
+    are different without crashing or producing silence.
+  - The filter-chain module now correctly calculates the output size without
+    crashing in some cases. It also skips invalid ports instead of crashing.
+  - Handle and report pthread errors better.
+
+## SPA
+  - The resampler qualities were tweaked a little.
+  - A bug that would sometimes cut off the last part of a buffer was fixed in
+    the alsa plugin. This could cause broken audio in VMs. (#2536)
+  - Access to the alsa mixer and devices is now checked more thoroughly.
+    (#2534)
+  - The spa-resample tool can now also handle large downsampling rates without
+    crashing.
+  - Audioconverter now uses rounding for float to int conversions, which
+    reduces distortions. Compilation of the c functions was separated and uses
+    its own optimization flags now. Unit tests were added. (#2543)
+  - Noise shaping was improved in audioconvert. A new Wannamaker 3 tap shaper
+    was added.
+  - Audioconvert now uses a pattern for generating keep alive noise. This
+    should have much less energy and be even more inaudible. (#2540)
+  - A channel mapping bug was fixed in audioconvert. Unit tests were added.
+  - The dsp audio mixer would sometimes not mix enough and cause dropouts.
+    (#2525)
+
+## JACK
+  - A critical bug in the mixer was fixed. It would cause most JACK apps to
+    segfault at startup.
+
+## Bluetooth
+  - A new rate control algorithm was implemented for the sources.
+  - The media role on HSP/HFP streams is now fixed.
+
+## Pulse Server
+  - Add the resampler delay to delay reporting as well.
+
+
+# PipeWire 0.3.55 (2022-07-12)
+
+This is a quick bugfix release that is API and ABI compatible with previous
+0.3.x releases.
+
+## Highlights
+  - Fix some more critical bugs in the new audioconvert and the queueing
+    in pw-stream that causes stuttering and hickups.
+  - HFP hardware volumes are now saved and restored.
+  - Format conversions and mixing was improved.
+  - Small bug fixes and improvements.
+
+## PipeWire
+  - The queueing in pw-stream was improved with support for buffer prefetch
+    in async mode.
+  - Add a pw-filter unit test.
+
+## tools
+  - pw-midiplay should now work again after improvements in pw-stream.
+
+## modules
+  - The RAOP module was improved to support auth_setup.
+  - The RAOP module should now handle timing packets better.
+  - Add some more filter-chain examples.
+  - The filter-chain now has a separate config file with the boilerplate
+    settings. The examples are now just config snippets that can be dropped
+    in .conf.d/ directories, such as the filter-chain.conf.d/ one.
+  - Start suggesting to use target.object instead of node.target in docs
+    and examples.
+
+## SPA
+  - Use the cosh window again for the resampler. It should now
+    give better resampler quality. (#2483)
+  - Rework the mixer functions. They were rewritten for higher precision and
+    better performance. Add unit tests and benchmarks.
+  - Improve format conversion for 32bits for avoid errors in clang because
+    of undefined behaviour at extreme ranges.
+  - Fix a bug in audioconvert where it would not consume the right
+    amount of samples when the resampler was disabled. This could cause
+    skipping and hickups. (#2519)
+  - Fix bug in audioconvert where it would try to convert the input samples
+    multiple times, causing strange artifacts when upmixing.
+  - Be more strict about valid JSON floats.
+  - device.vendor.id and device.product.id should now always show up in
+    0xXXXX format and should not be converted to floats in pw-dump anymore.
+  - Add triangular dither, add unit tests for noise generation, add some
+    more optimizations.
+
+## Bluetooth
+  - HFP and A2DP now expose different routes and thus can have different
+    volumes.
+  - HW Volumes for HFP are now synced better. Volume changes from HW buttons
+    are now also saved.
+
+# PipeWire 0.3.54 (2022-07-07)
+
+This is a quick bugfix release that is API and ABI compatible with previous
+0.3.x releases.
+
+## Highlights
+  - Some critical bugs in the new audioconvert were fixed. The old
+    adapter had internal buffering that was abused in some places.
+  - The bluetooth sources were rewritten using a ringbuffer to make them
+    more reliable to jitter and remove old audioconvert behaviour.
+  - Many improvements to the audio converter.
+  - Native DSD128 and up is now supported by pw-dsdplay.
+
+
+## tools
+  - Support DSD128 to DSD512 as well by scaling the amount of samples
+    to read per time slice.
+
+## SPA
+  - Format conversion is now generated with macros to remove duplication
+    of code.
+  - 24bits conversions were rewritten to use the generic conversion
+    functions.
+  - Temporary buffers in audioconvert are now made large enough in all
+    cases.
+  - Fix draining in audioconvert. This fixes speaker-test.
+  - Fix the channel remapping. (#2502, #2490)
+  - Audio conversion constants were tweaked to handle the maximum ranges
+    and provide lossless conversion between 24bits and floats.
+  - Vector code and C code are aligned and the unit tests are activated
+    again. A new lossless conversion test was added.
+  - Fix an underrun case where the adapter would not ask for more data.
+  - Fix PROP_INFO for audioconvert. (#2488)
+  - Use the blackman window again for the resampler, the cosh window has
+    some bugs that can cause distortion in some cases. (#2483)
+  - Add more unit tests for audioconvert. Add end-to-end conversion tests.
+  - Don't leak memory in format converter.
+
+## pulse-server
+  - Card properties are now also added to sinks and sources, just like
+    in pulseaudio.
+  - Increase the maxlength size to at least 4 times the fragsize to avoid
+    xruns.
+  - Fix a race when setting default devices.
+
+## Bluetooth
+  - The source was rewritten to use a ringbuffer. This avoids regressions
+    caused by audioconvert. 
+
+# PipeWire 0.3.53 (2022-06-30)
+
+This is a bugfix release that is API and ABI compatible with previous
+0.3.x releases.
+
+## Highlights
+  - The 44.1KHz samplerate was removed again from the defaults, it caused
+    all kinds of problems with various hardware.
+  - The ALSA plugin should now be able to deal with unsupported samplerates
+    and fall back to the nearest supported one.
+  - The rlimits performance tuning wiki page was updated. Please check
+    you limits.conf file, the version on the wiki used to give all
+    processes a -19 nice level instead of just the pipewire daemon.
+  - The audioconvert plugin was rewritten to be more maintainable and
+    faster. It also gained support for control ports and dithering with
+    optional noise shaping.
+  - An impossible buffering situation is avoided in pulse-server that would
+    cause some applications (sunshine, ...) to stutter.
+
+
+## PipeWire
+  - 44.1KHz was removed from the allowed rates again. It caused all kinds
+    of regressions due to driver bugs and timing issues on HDMI.
+
+## modules
+  - filter-chain now does some more error checking and reporting to
+    avoid some crashes.
+  - filter-chain now supports more channel layouts for input and output
+    that does not need to match the plugin layout.
+  - Format parsing is now more consistent in the modules.
+
+## Tools
+  - pw-cli can now also work without readline support.
+  - pw-cat can now also read multichannel ulaw/alaw/u8/s8.
+
+## SPA
+  - The audioconvert plugin was rewritten. This should make it more
+    maintainable. It also fixed some issues such as CPU spikes in some
+    cases and crashes in others. The old plugins were removed, for a
+    code reduction of some 6000 lines.
+  - The audioconvert plugin now supports control ports, which can be
+    enabled on nodes in the session manager. This makes it possible to
+    control audioconvert properties using timed events or midi.
+  - NoteOn 0-velocity MIDI events are no longer filtered out. This is
+    a valid event, nodes that can't deal with it should fix it up
+    themselves. The JACK layer still filters out these events by default
+    but this can now be configured with a per-client property.
+  - The running status on midi events is now disabled to match what
+    JACK does.
+  - The ALSA plugin will now deal with driver bugs when a driver announces
+    support for a samplerate but then refuses to use it later.
+  - The ALSA plugin has been optimized a little for sample IO.
+  - V4L2 now doesn't error when there are no controls.
+  - Error handling was improved in the audio converter.
+  - The audioconvert plugin now supports rectangular dithering and
+    noise shaping.
+  - The audioconvert plugin can now insert additional inaudible noise
+    that can be used to keep some amplifiers alive. (#705)
+  - The audioconvert format conversion was changed so that it now produces
+    the full 32 bits range in the C fallback conversion code as well.
+  - The resampler window function was changed to a cosh() window
+    function. (#2483)
+  - Vendor and device id are now in hex.
+
+## pulse-server
+  - Tweak the record buffer attributes some more and make sure we don't
+    end up in impossible buffering situations. Fixes an issue with
+    distorted sound in sunshine. (#2447)
+  - Fix a potential crash when updating the client property list.
+  - Some properties on cards were aligned with pulseaudio.
+
+## Wiki
+  - Change "priority" to "nice" in the example limits.conf file. It was
+    giving a -19 nice level to all processes, not just the pipewire
+    daemon.
+
 # PipeWire 0.3.52 (2022-06-09)
 
 This is a bugfix release that is API and ABI compatible with previous
@@ -112,9 +435,6 @@ This is a bugfix release that is API and ABI compatible with previous
   - Fixes to the source and fd use.
   - It is now possible to set client properties as well. (#1573)
 
-
-Older versions:
-
 # PipeWire 0.3.51 (2022-04-28)
 
 This is a bugfix release that is API and ABI compatible with previous
diff --git a/debian/changelog b/debian/changelog
index aca051198ae2fcf47712655c760d39929a92a462..49184d6f3f6551a0a706f01f715d00bad4d10911 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,85 @@
+pipewire (0.3.57-1~bpo11+1) bullseye-backports; urgency=medium
+
+  * Rebuild for bullseye-backports.
+  * Disable XFixes, minimum required version not available in Bullseye
+  * Disable libfreeaptx, not yet available in Bullseye
+
+ -- Dylan Aïssi <daissi@debian.org>  Wed, 07 Sep 2022 13:47:47 +0200
+
+pipewire (0.3.57-1) unstable; urgency=medium
+
+  * New upstream release
+  * Drop patches included in upstream release:
+      - aaa015d0: avb: fix compilation on big endian
+      - 1a5ec445: avb: fix compilation on big endian
+      - f857fd46: avb: fix compilation on big endian
+  * Don't install filter-chain.service for now.
+  * Update symbols file
+
+ -- Dylan Aïssi <daissi@debian.org>  Mon, 05 Sep 2022 09:57:50 +0200
+
+pipewire (0.3.56-1) unstable; urgency=medium
+
+  * New upstream release
+  * Drop patches included in upstream release:
+      - 40552a0e: jack: only mix when we have input to mix
+  * Cherry-pick upstream recommended patches:
+      - aaa015d0: avb: fix compilation on big endian
+      - 1a5ec445: avb: fix compilation on big endian
+      - f857fd46: avb: fix compilation on big endian
+  * Install the new avb module in libspa-0.2-modules
+  * Don't install new filter-chain example conf file
+  * Update symbols file
+
+ -- Dylan Aïssi <daissi@debian.org>  Tue, 19 Jul 2022 22:47:35 +0200
+
+pipewire (0.3.55-2) unstable; urgency=medium
+
+  * Source only upload for migration to testing
+
+ -- Dylan Aïssi <daissi@debian.org>  Mon, 18 Jul 2022 18:10:38 +0200
+
+pipewire (0.3.55-1) unstable; urgency=medium
+
+  * New upstream release
+  * Cherry-pick upstream recommended patch:
+      - 40552a0e: jack: only mix when we have input to mix
+  * Don't install new filter-chain example conf files
+  * Reintroduce pipewire-audio-client-libraries as a transitional package
+      to simplify the transition from Bullseye to Bookworm (Closes: #1014639)
+  * Mark pipewire-{alsa,jack,v4l2} as 'Multi-Arch: same' (Closes: #1014608)
+
+ -- Dylan Aïssi <daissi@debian.org>  Sun, 17 Jul 2022 10:42:21 +0200
+
+pipewire (0.3.54-2) unstable; urgency=medium
+
+  * Source only upload for migration to testing
+
+ -- Dylan Aïssi <daissi@debian.org>  Fri, 08 Jul 2022 10:07:26 +0200
+
+pipewire (0.3.54-1) unstable; urgency=medium
+
+  [ Sebastien Bacher ]
+  * Split the legacy pipewire-audio-client-libraries in alsa and jack
+
+  [ Dylan Aïssi ]
+  * New upstream release
+     - Fix issue with microphone input on bluetooth headset (Closes: #1014458)
+  * pipewire-jack: remove suggests pulseaudio-utils and libspa-0.2-bluetooth
+  * Switch section from video to sound for pipewire-pulse, pipewire-alsa
+      and pipewire-jack
+  * Improve description of pipewire-alsa and pipewire-jack
+  * Move the V4L2 plugin into its own package
+  * Update copyright file
+
+ -- Dylan Aïssi <daissi@debian.org>  Thu, 07 Jul 2022 17:11:37 +0200
+
+pipewire (0.3.53-1) unstable; urgency=medium
+
+  * New upstream release
+
+ -- Dylan Aïssi <daissi@debian.org>  Fri, 01 Jul 2022 14:08:59 +0200
+
 pipewire (0.3.52-1) unstable; urgency=medium
 
   [ Dylan Aïssi ]
diff --git a/debian/control b/debian/control
index 639c3ca0e2d7269702984ae6be45a66d843b3f64..04abc683df47068c01e0c025d197faa91d00c525 100644
--- a/debian/control
+++ b/debian/control
@@ -20,7 +20,7 @@ Build-Depends: debhelper-compat (= 13),
                libldacbt-enc-dev [!s390x !hppa !m68k !powerpc !ppc64 !sparc64],
                liblilv-dev,
                libncurses-dev,
-               libfreeaptx-dev,
+#               libfreeaptx-dev,
                libpulse-dev,
                libreadline-dev,
                libsbc-dev,
@@ -32,7 +32,7 @@ Build-Depends: debhelper-compat (= 13),
                libusb-1.0-0-dev,
                libv4l-dev,
                libwebrtc-audio-processing-dev,
-               libxfixes-dev (>= 1:6.0.0),
+#               libxfixes-dev (>= 1:6.0.0),
                meson (>= 0.59.0),
                pkg-config,
                python3-docutils,
@@ -218,7 +218,7 @@ Description: PipeWire multimedia server - programs
  the pipewire package instead.
 
 Package: pipewire-pulse
-Section: video
+Section: sound
 Architecture: linux-any
 Multi-Arch: foreign
 Replaces: pipewire-bin (<< 0.3.27-2)
@@ -239,14 +239,36 @@ Description: PipeWire PulseAudio daemon
  .
  This package contains the PulseAudio replacement daemon.
 
-Package: pipewire-audio-client-libraries
+Package: pipewire-alsa
+Section: sound
 Architecture: linux-any
 Multi-Arch: same
-Depends: pipewire (= ${binary:Version}), ${misc:Depends}, ${shlibs:Depends}
-Recommends: ${shlibs:Recommends}
-Breaks: pipewire (<< 0.3.5)
-Replaces: pipewire (<< 0.3.5)
-Description: PipeWire multimedia server - audio client libraries
+Replaces: pipewire-audio-client-libraries (<< 0.3.54-1~)
+Breaks: pipewire-audio-client-libraries (<< 0.3.54-1~)
+Depends: pipewire (= ${binary:Version}),
+         ${misc:Depends},
+         ${shlibs:Depends}
+Description: PipeWire ALSA plugin
+ PipeWire is a server and user space API to deal with multimedia
+ pipelines. This includes:
+ .
+  - Making available sources of video (such as from a capture devices or
+    application provided streams) and multiplexing this with clients.
+  - Accessing sources of video for consumption.
+  - Generating graphs for audio and video processing.
+ .
+ This package contains the ALSA plugin.
+
+Package: pipewire-jack
+Section: sound
+Architecture: linux-any
+Multi-Arch: same
+Replaces: pipewire-audio-client-libraries (<< 0.3.54-1~)
+Breaks: pipewire-audio-client-libraries (<< 0.3.54-1~)
+Depends: pipewire (= ${binary:Version}),
+         ${misc:Depends},
+         ${shlibs:Depends}
+Description: PipeWire JACK plugin
  PipeWire is a server and user space API to deal with multimedia
  pipelines. This includes:
  .
@@ -255,10 +277,29 @@ Description: PipeWire multimedia server - audio client libraries
   - Accessing sources of video for consumption.
   - Generating graphs for audio and video processing.
  .
- This package contains client libraries allowing programs designed for
- the ALSA, JACK and PulseAudio APIs to use a PipeWire server for audio
- playback and recording. They are not used by default, and are currently
- considered to be experimental.
+ This package contains the JACK plugin.
+
+Package: pipewire-v4l2
+Section: video
+Architecture: linux-any
+Multi-Arch: same
+Replaces: pipewire-bin (<< 0.3.54-1),
+          libpipewire-0.3-modules (<< 0.3.54-1)
+Breaks: pipewire-bin (<< 0.3.54-1),
+        libpipewire-0.3-modules (<< 0.3.54-1)
+Depends: pipewire (= ${binary:Version}),
+         ${misc:Depends},
+         ${shlibs:Depends}
+Description: PipeWire V4L2 plugin
+ PipeWire is a server and user space API to deal with multimedia
+ pipelines. This includes:
+ .
+  - Making available sources of video (such as from a capture devices or
+    application provided streams) and multiplexing this with clients.
+  - Accessing sources of video for consumption.
+  - Generating graphs for audio and video processing.
+ .
+ This package contains the V4L2 plugin.
 
 Package: pipewire-tests
 Architecture: linux-any
@@ -329,3 +370,15 @@ Description: libraries for the PipeWire multimedia server - JACK client
  This package contains a plugin to make PipeWire able to connect to a
  JACK server, which will be used for audio playback and recording.
  Using PipeWire for audio is considered to be experimental.
+
+Package: pipewire-audio-client-libraries
+Section: oldlibs
+Architecture: all
+Depends: pipewire-alsa,
+         pipewire-jack
+Description: transitional package for pipewire-alsa and pipewire-jack
+ PipeWire is a server and user space API to deal with multimedia
+ pipelines.
+ .
+ This is a transitional package for pipewire-alsa and pipewire-jack.
+ It can safely be removed.
diff --git a/debian/copyright b/debian/copyright
index 0bdf6775ab449974e14b6b1060e752c3acad4403..92d66f3d2ecca5d787fa542d96cb4a1a953df727 100644
--- a/debian/copyright
+++ b/debian/copyright
@@ -7,14 +7,45 @@ Files: *
 Copyright: 2009 Lennart Poettering
            2010 David Henningsson
            2013 Inigo Quilez
-           2015-2020 Wim Taymans
-           2016 Axis Communications
-           2018-2020 Collabora Ltd.
+           2015-2022 Wim Taymans
+           2016-2021 Axis Communications
+           2018-2022 Collabora Ltd.
            2020 Konsulko Group
            2020 Sergey Bugaev
            2020 Georges Basile Stavracas Neto
+           2021 jothepro
+           2019-2021 Red Hat, Inc.
+           2021 Arun Raghavan
+           2013 The Chromium OS Authors.
+           2010 Google Inc.
+           2017 HiFi-LoFi
+           2000-2002 Richard W.E. Furse, Paul Barton-Davis
+           2021 Sanchayan Maity
+           2021 Pauli Virtanen
+           2021 Florian Hülsmann
 License: Expat
 
+Files: include/*
+Copyright: 2000-2017 Julian Seward.
+License: BZIP2
+
+Files: pipewire-jack/jack/*
+Copyright: 2000-2013 Paul Davis
+           2003-2004 Jack OQuin
+           2002 Kai Vehmanen
+           2011-2014 David Robillard
+           2004 Ian Esten
+           2004-2012 Grame
+           2003 Rohan Drape
+           2010 Torben Hohn
+           2004 Rui Nuno Capela, Lee Revell
+License: LGPL-2.1+
+
+Files: pipewire-jack/jack/control.h
+Copyright: 2008 Nedko Arnaudov
+           2008 GRAME
+License: GPL-2
+
 Files: spa/plugins/alsa/90-pipewire-alsa.rules
        spa/plugins/alsa/acp/*
        spa/plugins/alsa/mixer/paths/*
@@ -30,24 +61,36 @@ Copyright: 1999 Tom Tromey
            2011 Arun Raghavan
            2011 Wolfson Microelectronics PLC
            2012 Feng Wei, Freescale Ltd.
-           2015-2020 Wim Taymans
+           2015-2022 Wim Taymans
 License: LGPL-2+ and LGPL-2.1+ and Expat
 
 Files: spa/plugins/bluez5/*
 Copyright: 2004-2010 Marcel Holtmann
            2006-2010 Nokia Corporation
            2016-2017 Arkadiusz Bokowy
-           2018 Wim Taymans
-           2019 Collabora Ltd.
+           2018-2022 Wim Taymans
+           2018-2022 Collabora Ltd.
+           2018 Pali Rohár
+           2021-2022 Pauli Virtanen
+           2013 Julien Pommier
 License: Expat and LGPL-2.1+
 
 Files: src/modules/module-client-node/v0/*
        src/modules/module-protocol-native/v0/*
        src/modules/module-portal.c
-Copyright: 2015-2017 Wim Taymans
+Copyright: 2015-2022 Wim Taymans
            2019 Red Hat Inc.
 License: LGPL-2+
 
+Files: src/modules/module-filter-chain/ladspa.h
+Copyright: 2000-2002 Richard W.E. Furse, Paul Barton-Davis
+License: LGPL-2.1+
+
+Files: src/modules/module-filter-chain/pffft.*
+Copyright: 2013 Julien Pommier
+           2004 The University Corporation for Atmospheric Research
+License: FFTPACK
+
 License: Expat
  Permission is hereby granted, free of charge, to any person obtaining a copy
  of this software and associated documentation files (the "Software"), to deal
@@ -100,3 +143,77 @@ License: LGPL-2.1+
  .
  On Debian systems, the complete text of the GNU Lesser General
  Public License can be found in "/usr/share/common-licenses/LGPL-2.1".
+
+License: FFTPACK
+ Redistribution and use of the Software in source and binary forms,
+ with or without modification, is permitted provided that the
+ following conditions are met:
+ .
+ - Neither the names of NCAR's Computational and Information Systems
+   Laboratory, the University Corporation for Atmospheric Research,
+   nor the names of its sponsors or contributors may be used to
+   endorse or promote products derived from this Software without
+   specific prior written permission.
+ .
+ - Redistributions of source code must retain the above copyright
+   notices, this list of conditions, and the disclaimer below.
+ .
+ - Redistributions in binary form must reproduce the above copyright
+   notice, this list of conditions, and the disclaimer below in the
+   documentation and/or other materials provided with the
+   distribution.
+ .
+ THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE CONTRIBUTORS OR COPYRIGHT
+ HOLDERS BE LIABLE FOR ANY CLAIM, INDIRECT, INCIDENTAL, SPECIAL,
+ EXEMPLARY, OR CONSEQUENTIAL DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE
+ SOFTWARE.
+
+License: BZIP2
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+ .
+ 1. Redistributions of source code must retain the above copyright
+    notice, this list of conditions and the following disclaimer.
+ .
+ 2. The origin of this software must not be misrepresented; you must
+    not claim that you wrote the original software.  If you use this
+    software in a product, an acknowledgment in the product
+    documentation would be appreciated but is not required.
+ .
+ 3. Altered source versions must be plainly marked as such, and must
+    not be misrepresented as being the original software.
+ .
+ 4. The name of the author may not be used to endorse or promote
+    products derived from this software without specific prior written
+    permission.
+ .
+ THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
+ OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
+ GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+License: GPL-2
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; version 2 of the License.
+ .
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ GNU General Public License for more details.
+ .
+ On Debian systems you can find the full text of the GNU General Public
+ License version 2 at /usr/share/common-licenses/GPL-2.
diff --git a/debian/libpipewire-0.3-0.symbols b/debian/libpipewire-0.3-0.symbols
index c3ba4300f8164bdfc6ce5c647a69c0c15ef2b60b..84324498643447c537e623198e0e4c397eefdc4a 100644
--- a/debian/libpipewire-0.3-0.symbols
+++ b/debian/libpipewire-0.3-0.symbols
@@ -9,6 +9,7 @@ libpipewire-0.3.so.0 libpipewire-0.3-0 #MINVER#
  pw_client_info_merge@Base 0.3.35
  pw_client_info_update@Base 0.3.1
  pw_conf_load_conf@Base 0.3.22
+ pw_conf_load_conf_for_context@Base 0.3.57
  pw_conf_load_state@Base 0.3.22
  pw_conf_save_state@Base 0.3.22
  pw_context_add_listener@Base 0.3.1
@@ -35,6 +36,7 @@ libpipewire-0.3.so.0 libpipewire-0.3-0 #MINVER#
  pw_context_find_spa_lib@Base 0.3.1
  pw_context_for_each_global@Base 0.3.1
  pw_context_get_conf_section@Base 0.3.22
+ pw_context_get_data_loop@Base 0.3.56
  pw_context_get_default_core@Base 0.3.1
  pw_context_get_main_loop@Base 0.3.1
  pw_context_get_object@Base 0.3.1
diff --git a/debian/libpipewire-0.3-modules.install b/debian/libpipewire-0.3-modules.install
index 0b52b118e7bc943443ade3df559e56af66c78311..9e442992980a498ad999d9b699723bdb2d205659 100644
--- a/debian/libpipewire-0.3-modules.install
+++ b/debian/libpipewire-0.3-modules.install
@@ -1,2 +1 @@
 usr/lib/*/pipewire-0.3/*.so
-usr/lib/*/pipewire-0.3/v4l2/libpw-v4l2.so
diff --git a/debian/libspa-0.2-modules.install b/debian/libspa-0.2-modules.install
index ea7494f2bbd7b211d062c897a27029024a213d6e..7accb5b3c0cfc6ef6ba4db449553f847ac6c3855 100644
--- a/debian/libspa-0.2-modules.install
+++ b/debian/libspa-0.2-modules.install
@@ -3,6 +3,7 @@ usr/lib/*/spa-0.2/alsa
 usr/lib/*/spa-0.2/audioconvert
 usr/lib/*/spa-0.2/audiomixer
 usr/lib/*/spa-0.2/audiotestsrc
+usr/lib/*/spa-0.2/avb
 usr/lib/*/spa-0.2/control
 usr/lib/*/spa-0.2/support
 usr/lib/*/spa-0.2/test
diff --git a/debian/not-installed b/debian/not-installed
index 31d9c16f10d8e5cf82eaeb480ddd421420f699d2..e197cdea21fc0c6613078d43931d8f18048a0bf6 100644
--- a/debian/not-installed
+++ b/debian/not-installed
@@ -1,9 +1,14 @@
+usr/lib/systemd/user/filter-chain.service
+usr/share/pipewire/filter-chain.conf
 usr/share/pipewire/filter-chain/demonic.conf
 usr/share/pipewire/filter-chain/duplicate-FL.conf
 usr/share/pipewire/filter-chain/sink-convolver.conf
 usr/share/pipewire/filter-chain/sink-dolby-surround.conf
 usr/share/pipewire/filter-chain/sink-eq6.conf
+usr/share/pipewire/filter-chain/sink-make-LFE.conf
 usr/share/pipewire/filter-chain/sink-matrix-spatialiser.conf
+usr/share/pipewire/filter-chain/sink-mix-FL-FR.conf
 usr/share/pipewire/filter-chain/sink-virtual-surround-5.1-kemar.conf
 usr/share/pipewire/filter-chain/sink-virtual-surround-7.1-hesuvi.conf
+usr/share/pipewire/filter-chain/source-duplicate-FL.conf
 usr/share/pipewire/filter-chain/source-rnnoise.conf
diff --git a/debian/patches/Don-t-build_same_binary_twice.patch b/debian/patches/Don-t-build_same_binary_twice.patch
index 4485b3c554b42788b950ac88862d589dc9d11774..5571943cbe8e7482479ab6fa26842411be4dc6d5 100644
--- a/debian/patches/Don-t-build_same_binary_twice.patch
+++ b/debian/patches/Don-t-build_same_binary_twice.patch
@@ -20,7 +20,7 @@ Signed-off-by: Dylan Aïssi <dylan.aissi@collabora.com>
 
 --- a/src/daemon/meson.build
 +++ b/src/daemon/meson.build
-@@ -90,12 +90,11 @@
+@@ -93,12 +93,11 @@
    dependencies : [ spa_dep, pipewire_dep, ],
  )
  
@@ -37,4 +37,4 @@ Signed-off-by: Dylan Aïssi <dylan.aissi@collabora.com>
 +        join_paths(get_option('prefix'), get_option('bindir'), 'pipewire-pulse'))
  )
  
- ln = find_program('ln')
+ executable('pipewire-avb',
diff --git a/debian/patches/series b/debian/patches/series
index adf4f7f600dc2b456a1398b6fb6751066ab774c4..68ea67cb3cb189bac09334b1c603440c2c43ecd8 100644
--- a/debian/patches/series
+++ b/debian/patches/series
@@ -1,2 +1,3 @@
 Don-t-automatically-start-pipewire-for-root-logins.patch
 Don-t-build_same_binary_twice.patch
+# Recommended patch for 0.3.5X
diff --git a/debian/pipewire-audio-client-libraries.install b/debian/pipewire-alsa.install
similarity index 63%
rename from debian/pipewire-audio-client-libraries.install
rename to debian/pipewire-alsa.install
index 917613102716241a2eb1565cc148794488105001..71834ccb424529703c1b619e789e529c98167bf6 100644
--- a/debian/pipewire-audio-client-libraries.install
+++ b/debian/pipewire-alsa.install
@@ -1,8 +1,4 @@
-debian/ld.so.conf.d/* usr/share/doc/pipewire/examples/ld.so.conf.d
-usr/bin/pw-jack
 usr/lib/*/alsa-lib/libasound_module_ctl_pipewire.so
 usr/lib/*/alsa-lib/libasound_module_pcm_pipewire.so
-usr/lib/*/pipewire-0.3/jack
 usr/share/alsa/alsa.conf.d/50-pipewire.conf
 usr/share/alsa/alsa.conf.d/99-pipewire-default.conf usr/share/doc/pipewire/examples/alsa.conf.d
-usr/share/man/man1/pw-jack.*
diff --git a/debian/pipewire-alsa.links b/debian/pipewire-alsa.links
new file mode 100644
index 0000000000000000000000000000000000000000..570661740a1df49b4f0ab0301f8baad4f0354377
--- /dev/null
+++ b/debian/pipewire-alsa.links
@@ -0,0 +1 @@
+usr/share/alsa/alsa.conf.d/50-pipewire.conf etc/alsa/conf.d/50-pipewire.conf
diff --git a/debian/pipewire-audio-client-libraries.links b/debian/pipewire-audio-client-libraries.links
deleted file mode 100644
index a90c61d2190c87e80a0d1c1d515bb0a2629c277d..0000000000000000000000000000000000000000
--- a/debian/pipewire-audio-client-libraries.links
+++ /dev/null
@@ -1,3 +0,0 @@
-usr/share/alsa/alsa.conf.d/50-pipewire.conf etc/alsa/conf.d/50-pipewire.conf
-usr/share/doc/pipewire-audio-client-libraries/README.Debian usr/share/doc/pipewire/examples/README.audio
-usr/share/doc/pipewire/examples usr/share/doc/pipewire-audio-client-libraries/examples
diff --git a/debian/pipewire-audio-client-libraries.shlibs.local b/debian/pipewire-audio-client-libraries.shlibs.local
deleted file mode 100644
index e9c0857b5ae38ec8f550bc9ae50e868d6a978bc7..0000000000000000000000000000000000000000
--- a/debian/pipewire-audio-client-libraries.shlibs.local
+++ /dev/null
@@ -1,4 +0,0 @@
-libjack 0 pipewire-audio-client-libraries (= ${binary:Version})
-libjacknet 0 pipewire-audio-client-libraries (= ${binary:Version})
-libjackserver 0 pipewire-audio-client-libraries (= ${binary:Version})
-libpipewire-0.3 0 libpipewire-0.3-0 (= ${binary:Version})
diff --git a/debian/pipewire-bin.install b/debian/pipewire-bin.install
index 9d7a9abdf7ae04d2da426efe75b9ff3bdea3cc35..e5808a3e4bc538a340cb230c9baedf1bead0237e 100644
--- a/debian/pipewire-bin.install
+++ b/debian/pipewire-bin.install
@@ -2,9 +2,11 @@ usr/share/pipewire/client-rt.conf
 usr/share/pipewire/client.conf
 usr/share/pipewire/jack.conf
 usr/share/pipewire/pipewire.conf
+usr/share/pipewire/pipewire-avb.conf
 usr/share/pipewire/minimal.conf
 lib/udev/rules.d
 usr/bin/pipewire
+usr/bin/pipewire-avb
 usr/bin/pw-cat
 usr/bin/pw-cli
 usr/bin/pw-dot
@@ -22,7 +24,6 @@ usr/bin/pw-profiler
 usr/bin/pw-record
 usr/bin/pw-reserve
 usr/bin/pw-top
-usr/bin/pw-v4l2
 usr/bin/spa-*
 usr/share/alsa-card-profile
 usr/share/man/man1/pipewire.*
diff --git a/debian/pipewire-jack.install b/debian/pipewire-jack.install
new file mode 100644
index 0000000000000000000000000000000000000000..e5ef3a51c0b393291653d88e74980a7701afdaa7
--- /dev/null
+++ b/debian/pipewire-jack.install
@@ -0,0 +1,5 @@
+usr/bin/pw-jack
+usr/lib/*/pipewire-0.3/jack
+usr/share/man/man1/pw-jack.*
+debian/ld.so.conf.d/* usr/share/doc/pipewire/examples/ld.so.conf.d
+
diff --git a/debian/pipewire-audio-client-libraries.lintian-overrides b/debian/pipewire-jack.lintian-overrides
similarity index 100%
rename from debian/pipewire-audio-client-libraries.lintian-overrides
rename to debian/pipewire-jack.lintian-overrides
diff --git a/debian/pipewire-jack.shlibs.local b/debian/pipewire-jack.shlibs.local
new file mode 100644
index 0000000000000000000000000000000000000000..1bba4c397bd7076e562c08ea2c54fbb0b6d785b8
--- /dev/null
+++ b/debian/pipewire-jack.shlibs.local
@@ -0,0 +1,4 @@
+libjack 0 pipewire-jack (= ${binary:Version})
+libjacknet 0 pipewire-jack (= ${binary:Version})
+libjackserver 0 pipewire-jack (= ${binary:Version})
+libpipewire-0.3 0 libpipewire-0.3-0 (= ${binary:Version})
diff --git a/debian/pipewire-v4l2.install b/debian/pipewire-v4l2.install
new file mode 100644
index 0000000000000000000000000000000000000000..08dbd90fd7f2ce721a1291d36efb24dbc536a819
--- /dev/null
+++ b/debian/pipewire-v4l2.install
@@ -0,0 +1,2 @@
+usr/bin/pw-v4l2
+usr/lib/*/pipewire-0.3/v4l2/libpw-v4l2.so
diff --git a/debian/pipewire-audio-client-libraries.README.Debian b/debian/pipewire.README.Debian
similarity index 100%
rename from debian/pipewire-audio-client-libraries.README.Debian
rename to debian/pipewire.README.Debian
diff --git a/debian/rules b/debian/rules
index d3910a5e5c9c552e2e3661b4dd61c421652118e4..bbf9fe65c35d10a62aaab32d5802cfd1a318c4bf 100755
--- a/debian/rules
+++ b/debian/rules
@@ -31,6 +31,7 @@ override_dh_auto_configure:
 		-Dauto_features=enabled \
 		-Davahi=enabled \
 		-Dbluez5-codec-aac=disabled \
+		-Dbluez5-codec-aptx=disabled \
 		-Dbluez5-codec-lc3plus=disabled \
 		-Dbluez5-codec-ldac=$(BLUEZ5_CODEC_LDAC) \
 		-Dlibcamera=disabled \
@@ -45,6 +46,7 @@ override_dh_auto_configure:
 		-Dvideotestsrc=enabled \
 		-Dvolume=enabled \
 		-Dvulkan=disabled \
+		-Dx11-xfixes=disabled \
 		$(NULL)
 	install -d debian/ld.so.conf.d
 	echo "/usr/lib/$(DEB_HOST_MULTIARCH)/pipewire-0.3/jack/" > "debian/ld.so.conf.d/pipewire-jack-$(DEB_HOST_MULTIARCH).conf"
@@ -77,10 +79,10 @@ override_dh_makeshlibs:
 
 override_dh_shlibdeps-arch:
 	dh_shlibdeps \
-		-ppipewire-audio-client-libraries \
+		-ppipewire-jack \
 		-l/usr/lib/$(DEB_HOST_MULTIARCH)/pipewire-0.3 \
 		-- \
-		-Ldebian/pipewire-audio-client-libraries.shlibs.local \
+		-Ldebian/pipewire-jack.shlibs.local \
 		$(NULL)
 	dh_shlibdeps \
 		--remaining-packages \
diff --git a/doc/pipewire-modules.dox b/doc/pipewire-modules.dox
index c1eea1dacbb9d26fed42cec2d20cc36af59f83af..0b2f1f6980add4cbb34cfd7ac1bdc9d12cf23919 100644
--- a/doc/pipewire-modules.dox
+++ b/doc/pipewire-modules.dox
@@ -51,6 +51,7 @@ List of known modules:
 
 - \subpage page_module_access
 - \subpage page_module_adapter
+- \subpage page_module_avb
 - \subpage page_module_client_device
 - \subpage page_module_client_node
 - \subpage page_module_echo_cancel
diff --git a/doc/tutorial3.c b/doc/tutorial3.c
index c7dd792cac62a93654eec9095481e9f3bb956663..17c0ee4457bb187fcdc85259c06e0b2d8b3af59f 100644
--- a/doc/tutorial3.c
+++ b/doc/tutorial3.c
@@ -7,32 +7,36 @@
 #include <pipewire/pipewire.h>
 
 /* [roundtrip] */
-static int roundtrip(struct pw_core *core, struct pw_main_loop *loop)
+struct roundtrip_data {
+	int pending;
+	struct pw_main_loop *loop;
+};
+
+static void on_core_done(void *data, uint32_t id, int seq)
+{
+	struct roundtrip_data *d = data;
+
+	if (id == PW_ID_CORE && seq == d->pending)
+		pw_main_loop_quit(d->loop);
+}
+
+static void roundtrip(struct pw_core *core, struct pw_main_loop *loop)
 {
-        struct spa_hook core_listener;
-        int pending, done = 0;
-        void core_event_done(void *object, uint32_t id, int seq) {
-                if (id == PW_ID_CORE && seq == pending) {
-                        done = 1;
-                        pw_main_loop_quit(loop);
-                }
-        }
-        const struct pw_core_events core_events = {
-                PW_VERSION_CORE_EVENTS,
-                .done = core_event_done,
-        };
-
-        spa_zero(core_listener);
-        pw_core_add_listener(core, &core_listener,
-                                 &core_events, NULL);
-
-        pending = pw_core_sync(core, PW_ID_CORE, 0);
-
-        while (!done) {
-                pw_main_loop_run(loop);
-        }
-        spa_hook_remove(&core_listener);
-        return 0;
+	static const struct pw_core_events core_events = {
+		PW_VERSION_CORE_EVENTS,
+		.done = on_core_done,
+	};
+
+	struct roundtrip_data d = { .loop = loop };
+	struct spa_hook core_listener;
+
+	pw_core_add_listener(core, &core_listener, &core_events, &d);
+
+	d.pending = pw_core_sync(core, PW_ID_CORE, 0);
+
+	pw_main_loop_run(loop);
+
+	spa_hook_remove(&core_listener);
 }
 /* [roundtrip] */
 
@@ -50,37 +54,36 @@ static const struct pw_registry_events registry_events = {
 
 int main(int argc, char *argv[])
 {
-        struct pw_main_loop *loop;
-        struct pw_context *context;
-        struct pw_core *core;
-        struct pw_registry *registry;
-        struct spa_hook registry_listener;
+	struct pw_main_loop *loop;
+	struct pw_context *context;
+	struct pw_core *core;
+	struct pw_registry *registry;
+	struct spa_hook registry_listener;
 
-        pw_init(&argc, &argv);
+	pw_init(&argc, &argv);
 
-        loop = pw_main_loop_new(NULL /* properties */);
-        context = pw_context_new(pw_main_loop_get_loop(loop),
-                        NULL /* properties */,
-                        0 /* user_data size */);
+	loop = pw_main_loop_new(NULL /* properties */);
+	context = pw_context_new(pw_main_loop_get_loop(loop),
+			NULL /* properties */,
+			0 /* user_data size */);
 
-        core = pw_context_connect(context,
-                        NULL /* properties */,
-                        0 /* user_data size */);
+	core = pw_context_connect(context,
+			NULL /* properties */,
+			0 /* user_data size */);
 
-        registry = pw_core_get_registry(core, PW_VERSION_REGISTRY,
-                        0 /* user_data size */);
+	registry = pw_core_get_registry(core, PW_VERSION_REGISTRY,
+			0 /* user_data size */);
 
-        spa_zero(registry_listener);
-        pw_registry_add_listener(registry, &registry_listener,
-                                       &registry_events, NULL);
+	pw_registry_add_listener(registry, &registry_listener,
+				       &registry_events, NULL);
 
-        roundtrip(core, loop);
+	roundtrip(core, loop);
 
-        pw_proxy_destroy((struct pw_proxy*)registry);
-        pw_core_disconnect(core);
-        pw_context_destroy(context);
-        pw_main_loop_destroy(loop);
+	pw_proxy_destroy((struct pw_proxy*)registry);
+	pw_core_disconnect(core);
+	pw_context_destroy(context);
+	pw_main_loop_destroy(loop);
 
-        return 0;
+	return 0;
 }
 /* [code] */
diff --git a/doc/tutorial3.dox b/doc/tutorial3.dox
index 621819d22c0fba8456e15a2d01f10e87cad4b07a..776ea141e3d336a3b8abd669c119336f235569b5 100644
--- a/doc/tutorial3.dox
+++ b/doc/tutorial3.dox
@@ -15,10 +15,9 @@ Let's take the following small method first:
 Let's take a look at what this method does.
 
 \code{.c}
-        struct spa_hook core_listener;
-        spa_zero(core_listener);
-        pw_core_add_listener(core, &core_listener,
-			&core_events, NULL);
+	struct spa_hook core_listener;
+
+	pw_core_add_listener(core, &core_listener, &core_events, &d);
 \endcode
 
 First of all we add a listener for the events of the core
@@ -26,37 +25,31 @@ object. We are only interested in the `done` event in this
 tutorial. This is the event handler:
 
 \code{.c}
-        int pending, done = 0;
-
-        void core_event_done(void *data, uint32_t id, int seq) {
-                if (id == PW_ID_CORE && seq == pending) {
-                        done = 1;
-                        pw_main_loop_quit(loop);
-                }
-        }
-        const struct pw_core_events core_events = {
-                PW_VERSION_CORE_EVENTS,
-                .done = core_event_done,
-        };
+static void on_core_done(void *data, uint32_t id, int seq)
+{
+	struct roundtrip_data *d = data;
+
+	if (id == PW_ID_CORE && seq == d->pending)
+		pw_main_loop_quit(d->loop);
+}
 \endcode
 
-When the done event is received for an object with id `PW_ID_CORE`
-and a certain sequence number `seq`, this function will set the done
-variable to 1 and call `pw_main_loop_quit()`.
+When the done event is received for an object with id `PW_ID_CORE` and
+a certain sequence number `seq`, this function will call `pw_main_loop_quit()`.
 
 Next we do:
 
 \code{.c}
-        pending = pw_core_sync(core, PW_ID_CORE, 0);
+	d.pending = pw_core_sync(core, PW_ID_CORE, 0);
 \endcode
 
 This triggers the `sync` method on the core object with id
 `PW_ID_CORE` and sequence number 0.
 
 Because this is a method on a proxy object, it will be executed
-asynchronously and the returns value will reflect this. PipeWire
+asynchronously and the return value will reflect this. PipeWire
 uses the return values of the underlying SPA (Simple Plugin API)
-helper objects (See also [error codes](spa-design.md#error-codes)).
+helper objects (See also \ref page_spa_design ).
 
 Because all messages on the PipeWire server are handled sequentially,
 the sync method will be executed after all previous methods are
@@ -68,9 +61,7 @@ We then run the mainloop to send the messages to the server and
 receive the events:
 
 \code{.c}
-        while (!done) {
-                pw_main_loop_run(loop);
-        }
+	pw_main_loop_run(loop);
 \endcode
 
 When we get the done event, we can compare it to the sync method
@@ -79,7 +70,7 @@ more pending methods on the server. We can quit the mainloop and
 remove the listener:
 
 \code{.c}
-        spa_hook_remove(&core_listener);
+	spa_hook_remove(&core_listener);
 \endcode
 
 If we add this roundtrip method to our code and call it instead of the
@@ -100,7 +91,7 @@ the objects we created. Let's destroy each of them in reverse order that we
 created them:
 
 \code{.c}
-        pw_proxy_destroy((struct pw_proxy*)registry);
+	pw_proxy_destroy((struct pw_proxy*)registry);
 \endcode
 
 The registry is a proxy and can be destroyed with the generic proxy destroy
@@ -110,7 +101,7 @@ an error to destroy an object more than once.
 We can disconnect from the server with:
 
 \code{.c}
-        pw_core_disconnect(core);
+	pw_core_disconnect(core);
 \endcode
 
 This will also destroy the core proxy object and will remove the proxies
@@ -119,8 +110,8 @@ that might have been created on this connection.
 We can finally destroy our context and mainloop to conclude this tutorial:
 
 \code{.c}
-        pw_context_destroy(context);
-        pw_main_loop_destroy(loop);
+	pw_context_destroy(context);
+	pw_main_loop_destroy(loop);
 \endcode
 
 \ref page_tutorial2 | \ref page_tutorial "Index" | \ref page_tutorial4
diff --git a/doc/tutorial4.c b/doc/tutorial4.c
index 94fb6d3e223fd942962a3d4873acea544916bb5b..ff0cb277440fef9c0f85be805384ffe2b78ae8a5 100644
--- a/doc/tutorial4.c
+++ b/doc/tutorial4.c
@@ -96,7 +96,7 @@ int main(int argc, char *argv[])
 
 	pw_stream_connect(data.stream,
 			  PW_DIRECTION_OUTPUT,
-			  argc > 1 ? (uint32_t)atoi(argv[1]) : PW_ID_ANY,
+			  PW_ID_ANY,
 			  PW_STREAM_FLAG_AUTOCONNECT |
 			  PW_STREAM_FLAG_MAP_BUFFERS |
 			  PW_STREAM_FLAG_RT_PROCESS,
diff --git a/doc/tutorial4.dox b/doc/tutorial4.dox
index 92840212df305f98efa26260ce7553786c85023f..b8d170744f90dc61d997fb1e8f44c6331cf479a4 100644
--- a/doc/tutorial4.dox
+++ b/doc/tutorial4.dox
@@ -118,8 +118,8 @@ Now we're ready to connect the stream and run the main loop:
 	pw_main_loop_run(data.loop);
 \endcode
 
-To connect we specify that we have a `PW_DIRECTION_OUTPUT` stream. `PW_ID_ANY`
-means that we are ok with connecting to any consumer. Next we set some flags:
+To connect we specify that we have a `PW_DIRECTION_OUTPUT` stream. The third argument
+is always `PW_ID_ANY`. Next we set some flags:
 
 - `PW_STREAM_FLAG_AUTOCONNECT`: Automatically connect this stream. This instructs
   the session manager to link us to some consumer.
diff --git a/doc/tutorial5.c b/doc/tutorial5.c
index 754fc025d73852b74a8e9a6f05b66f73a35d4127..e49da91168bf63522a80de1426176f97b1c57656 100644
--- a/doc/tutorial5.c
+++ b/doc/tutorial5.c
@@ -83,19 +83,23 @@ int main(int argc, char *argv[])
 	const struct spa_pod *params[1];
 	uint8_t buffer[1024];
 	struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
+	struct pw_properties *props;
 
 	pw_init(&argc, &argv);
 
 	data.loop = pw_main_loop_new(NULL);
 
+	props = pw_properties_new(PW_KEY_MEDIA_TYPE, "Video",
+			PW_KEY_MEDIA_CATEGORY, "Capture",
+			PW_KEY_MEDIA_ROLE, "Camera",
+			NULL);
+	if (argc > 1)
+		pw_properties_set(props, PW_KEY_TARGET_OBJECT, argv[1]);
+
 	data.stream = pw_stream_new_simple(
 			pw_main_loop_get_loop(data.loop),
 			"video-capture",
-			pw_properties_new(
-				PW_KEY_MEDIA_TYPE, "Video",
-				PW_KEY_MEDIA_CATEGORY, "Capture",
-				PW_KEY_MEDIA_ROLE, "Camera",
-				NULL),
+			props,
 			&stream_events,
 			&data);
 
@@ -122,7 +126,7 @@ int main(int argc, char *argv[])
 
 	pw_stream_connect(data.stream,
 			  PW_DIRECTION_INPUT,
-			  argc > 1 ? (uint32_t)atoi(argv[1]) : PW_ID_ANY,
+			  PW_ID_ANY,
 			  PW_STREAM_FLAG_AUTOCONNECT |
 			  PW_STREAM_FLAG_MAP_BUFFERS,
 			  params, 1);
diff --git a/doc/tutorial5.dox b/doc/tutorial5.dox
index b614fc531274013f3d4578e3dc1898f330bc7908..e73c1cf4463e976c0d645c19e4e124a8bae34bfd 100644
--- a/doc/tutorial5.dox
+++ b/doc/tutorial5.dox
@@ -23,18 +23,25 @@ We create a stream object with different properties to make it a Camera
 Video Capture stream.
 
 \code{.c}
+	props = pw_properties_new(PW_KEY_MEDIA_TYPE, "Video",
+			PW_KEY_MEDIA_CATEGORY, "Capture",
+			PW_KEY_MEDIA_ROLE, "Camera",
+			NULL);
+	if (argc > 1)
+		pw_properties_set(props, PW_KEY_TARGET_OBJECT, argv[1]);
+
 	data.stream = pw_stream_new_simple(
 			pw_main_loop_get_loop(data.loop),
 			"video-capture",
-			pw_properties_new(
-				PW_KEY_MEDIA_TYPE, "Video",
-				PW_KEY_MEDIA_CATEGORY, "Capture",
-				PW_KEY_MEDIA_ROLE, "Camera",
-				NULL),
+			props,
 			&stream_events,
 			&data);
 \endcode
 
+We also optionally allow the user to pass the name of the target node where the session
+manager is supposed to connect the node. The user may also give the value of the
+unique target node serial (`PW_KEY_OBJECT_SERIAL`) as the value.
+
 In addition to the `process` event, we are also going to listen to a new event,
 `param_changed`:
 
@@ -122,7 +129,7 @@ Now we're ready to connect the stream and run the main loop:
 \code{.c}
 	pw_stream_connect(data.stream,
 			  PW_DIRECTION_INPUT,
-			  argc > 1 ? (uint32_t)atoi(argv[1]) : PW_ID_ANY,
+			  PW_ID_ANY,
 			  PW_STREAM_FLAG_AUTOCONNECT |
 			  PW_STREAM_FLAG_MAP_BUFFERS,
 			  params, 1);
@@ -130,9 +137,8 @@ Now we're ready to connect the stream and run the main loop:
 	pw_main_loop_run(data.loop);
 \endcode
 
-To connect we specify that we have a `PW_DIRECTION_INPUT` stream. `PW_ID_ANY`
-means that we are ok with connecting to any producer. We also allow the user
-to pass an optional target id.
+To connect we specify that we have a `PW_DIRECTION_INPUT` stream. The third
+argument is always `PW_ID_ANY`.
 
 We're setting the `PW_STREAM_FLAG_AUTOCONNECT` flag to make an automatic
 connection to a suitable camera and `PW_STREAM_FLAG_MAP_BUFFERS` to let the
diff --git a/man/pw-cli.1.rst.in b/man/pw-cli.1.rst.in
index 98c5174c7c39efb1052d7de57e96f97051831215..ca0a2e80598359c69a033b7a6f2bbf8a76ea25fd 100644
--- a/man/pw-cli.1.rst.in
+++ b/man/pw-cli.1.rst.in
@@ -34,17 +34,20 @@ Use the 'help' command to list the available commands.
 GENERAL COMMANDS
 ================
 
-help
-  Show a quick help on the commands available.
+help | h
+  Show a quick help on the commands available. It also lists the aliases
+  for many commands.
 
-quit
+quit | q
   Exit from **pw-cli**
 
 MODULE MANAGEMENT
 =================
 
-| Modules are loaded and unloaded in the local instance and can add
-| functionality or objects to the local instance.
+| 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. For most
@@ -105,14 +108,22 @@ create-node *factory-name* [*properties...*]
 
   This command returns a *node variable*.
 
-destroy-node *node-var*
-  Destroy a node.
-
 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
 ===============
 
@@ -125,8 +136,44 @@ create-link *node-id* *port-id* *node-id* *port-id* [*properties...*]
 
   This command returns a *link variable*.
 
-destroy-link *link-var*
-  Destroy a link.
+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.
+
 
 EXAMPLES
 ========
diff --git a/meson.build b/meson.build
index 972a55771478f794c3299c85e3a746429736bc2a..2634b711757f544556703f2e5ac05f9da77fc0ca 100644
--- a/meson.build
+++ b/meson.build
@@ -1,5 +1,5 @@
 project('pipewire', ['c' ],
-  version : '0.3.52',
+  version : '0.3.57',
   license : [ 'MIT', 'LGPL-2.1-or-later', 'GPL-2.0-only' ],
   meson_version : '>= 0.59.0',
   default_options : [ 'warning_level=3',
@@ -266,6 +266,7 @@ if not readline_dep.found()
 endif
 
 summary({'readline (for pw-cli)': readline_dep.found()}, bool_yn: true, section: 'Misc dependencies')
+cdata.set('HAVE_READLINE', readline_dep.found())
 ncurses_dep = dependency('ncursesw', required : false)
 sndfile_dep = dependency('sndfile', version : '>= 1.0.20', required : get_option('sndfile'))
 summary({'sndfile': sndfile_dep.found()}, bool_yn: true, section: 'pw-cat/pw-play/pw-dump/filter-chain')
@@ -294,6 +295,11 @@ cdata.set('HAVE_LIBUSB', libusb_dep.found())
 cap_lib = dependency('libcap', required : false)
 cdata.set('HAVE_LIBCAP', cap_lib.found())
 
+glib2_dep = dependency('glib-2.0', required : get_option('flatpak'))
+summary({'GLib-2.0 (Flatpak support)': glib2_dep.found()}, bool_yn: true, section: 'Misc dependencies')
+flatpak_support = glib2_dep.found()
+cdata.set('HAVE_GLIB2', flatpak_support)
+
 gst_option = get_option('gstreamer')
 gst_deps_def = {
   'glib-2.0': {'version': '>=2.32.0'},
@@ -335,16 +341,16 @@ webrtc_dep = dependency('webrtc-audio-processing',
 summary({'WebRTC Echo Canceling': webrtc_dep.found()}, bool_yn: true, section: 'Misc dependencies')
 cdata.set('HAVE_WEBRTC', webrtc_dep.found())
 
-# On FreeBSD, epoll-shim library is required for eventfd() and timerfd()
-epoll_shim_dep = (build_machine.system() == 'freebsd'
+# On FreeBSD and MidnightBSD, epoll-shim library is required for eventfd() and timerfd()
+epoll_shim_dep = (host_machine.system() == 'freebsd' or host_machine.system() == 'midnightbsd'
     ? dependency('epoll-shim', required: true)
     : dependency('', required: false))
 
-libinotify_dep = (build_machine.system() == 'freebsd'
+libinotify_dep = (host_machine.system() == 'freebsd' or host_machine.system() == 'midnightbsd'
     ? dependency('libinotify', required: true)
     : dependency('', required: false))
 
-# On FreeBSD, libintl library is required for gettext
+# On FreeBSD and MidnightBSD, libintl library is required for gettext
 libintl_dep = cc.find_library('intl', required: false)
 if not libintl_dep.found()
     libintl_dep = dependency('intl', required: false)
@@ -355,8 +361,8 @@ need_alsa = get_option('pipewire-alsa').enabled() or 'media-session' in get_opti
 alsa_dep = dependency('alsa', version : '>=1.1.7', required: need_alsa)
 summary({'pipewire-alsa': alsa_dep.found()}, bool_yn: true)
 
-if build_machine.system() == 'freebsd'
-# On FreeBSD the OpenSSL library may come from base or a package.
+if host_machine.system() == 'freebsd' or host_machine.system() == 'midnightbsd'
+# On FreeBSD and MidnightBSD the OpenSSL library may come from base or a package.
 # Check for a package first and fallback to the base library if we can't find it via pkgconfig
     openssl_lib = dependency('openssl', required: false)
     if not openssl_lib.found()
diff --git a/meson_options.txt b/meson_options.txt
index 9e1466738b5e243bfdf9fd695b6fc7953fbbe8c6..e6a5623e02039d23cd0fa070f44939ada40fdf49 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -116,6 +116,10 @@ option('bluez5-codec-lc3plus',
         description: 'Enable LC3plus open source codec implementation',
         type: 'feature',
         value: 'auto')
+option('bluez5-codec-opus',
+        description: 'Enable Opus open source codec implementation',
+        type: 'feature',
+        value: 'auto')
 option('control',
        description: 'Enable control spa plugin integration',
        type: 'feature',
@@ -245,3 +249,11 @@ option('legacy-rtkit',
        description: 'Build legacy rtkit module',
        type: 'boolean',
        value: 'true')
+option('avb',
+       description: 'Enable AVB code',
+       type: 'feature',
+       value: 'auto')
+option('flatpak',
+       description: 'Enable Flatpak support',
+       type: 'feature',
+       value: 'enabled')
diff --git a/pipewire-alsa/alsa-plugins/ctl_pipewire.c b/pipewire-alsa/alsa-plugins/ctl_pipewire.c
index fd5dcc05a237c566ccb70910b1fdd03aba1a8a34..71f02f90ede795fb7a5aa5b47f79935de3589777 100644
--- a/pipewire-alsa/alsa-plugins/ctl_pipewire.c
+++ b/pipewire-alsa/alsa-plugins/ctl_pipewire.c
@@ -532,6 +532,7 @@ static int set_volume_mute(snd_ctl_pipewire_t *ctl, const char *name, struct vol
 		spa_pod_builder_add(&b,
 			SPA_PARAM_ROUTE_index, SPA_POD_Int(id),
 			SPA_PARAM_ROUTE_device, SPA_POD_Int(device_id),
+			SPA_PARAM_ROUTE_save, SPA_POD_Bool(true),
 			0);
 
 		spa_pod_builder_prop(&b, SPA_PARAM_ROUTE_props, 0);
diff --git a/pipewire-alsa/alsa-plugins/pcm_pipewire.c b/pipewire-alsa/alsa-plugins/pcm_pipewire.c
index 16940b1fcc04e6d4150a0f916d71aa011ad974c8..412b71fc26e6f8309cf437a48d5730d672c003e4 100644
--- a/pipewire-alsa/alsa-plugins/pcm_pipewire.c
+++ b/pipewire-alsa/alsa-plugins/pcm_pipewire.c
@@ -25,7 +25,7 @@
 #define __USE_GNU
 
 #include <limits.h>
-#ifndef __FreeBSD__
+#if !defined(__FreeBSD__) && !defined(__MidnightBSD__)
 #include <byteswap.h>
 #endif
 #include <sys/shm.h>
@@ -726,6 +726,7 @@ static int snd_pcm_pipewire_sw_params(snd_pcm_ioplug_t * io,
 {
 	snd_pcm_pipewire_t *pw = io->private_data;
 
+	pw_thread_loop_lock(pw->main_loop);
 	if (pw->stream) {
 		snd_pcm_uframes_t min_avail;
 		snd_pcm_sw_params_get_avail_min( sw_params, &min_avail);
@@ -746,6 +747,7 @@ static int snd_pcm_pipewire_sw_params(snd_pcm_ioplug_t * io,
 	} else {
 		pw_log_debug("%p: sw_params pre-prepare noop", pw);
 	}
+	pw_thread_loop_unlock(pw->main_loop);
 
 	return 0;
 }
diff --git a/pipewire-jack/src/meson.build b/pipewire-jack/src/meson.build
index bb1a7f0e972c5cae2220bafc9546666e2e08c746..20d1ccf8ea9e95b45c917cbfc997c17ec7c5afd1 100644
--- a/pipewire-jack/src/meson.build
+++ b/pipewire-jack/src/meson.build
@@ -78,7 +78,7 @@ if get_option('jack-devel') == true
   endif
 
   pkgconfig.generate(filebase : 'jack',
-  libraries : [pipewire_jack, pipewire_jackserver],
+  libraries : [pipewire_jack],
   name : 'jack',
   description : 'PipeWire JACK API',
   version : '1.9.17',
diff --git a/pipewire-jack/src/metadata.c b/pipewire-jack/src/metadata.c
index d50948ff46ab6cda67e786f0279a40f0e1b3262e..da3d75f41b8ea3b296984e60432d02e52c0336a9 100644
--- a/pipewire-jack/src/metadata.c
+++ b/pipewire-jack/src/metadata.c
@@ -210,7 +210,7 @@ static int update_property(struct client *c,
 	pthread_mutex_unlock(&globals.lock);
 
 	if (c->property_callback && changed > 0) {
-		pw_log_info("emit %lu %s", subject, key);
+		pw_log_info("emit %"PRIu64" %s", (uint64_t)subject, key);
 		c->property_callback(subject, key, change, c->property_arg);
 	}
 	return changed;
diff --git a/pipewire-jack/src/pipewire-jack.c b/pipewire-jack/src/pipewire-jack.c
index c4ef10aaf999102baa68e48314f38d066eb37034..7100a41b088b5d0c84aca37d99503ac17120db1b 100644
--- a/pipewire-jack/src/pipewire-jack.c
+++ b/pipewire-jack/src/pipewire-jack.c
@@ -62,12 +62,12 @@
 
 #define DEFAULT_RT_MAX	88
 
-#define JACK_CLIENT_NAME_SIZE		128
+#define JACK_CLIENT_NAME_SIZE		256
 #define JACK_PORT_NAME_SIZE		256
 #define JACK_PORT_TYPE_SIZE             32
 #define MONITOR_EXT			" Monitor"
 
-#define MAX_MIDI_MIX			1024
+#define MAX_MIX				1024
 #define MAX_BUFFER_FRAMES		8192
 
 #define MAX_ALIGN			16
@@ -106,9 +106,9 @@ static bool mlock_warned = false;
 #define OBJECT_CHUNK		8
 #define RECYCLE_THRESHOLD	128
 
-typedef void (*mix2_func) (float *dst, float *src1, float *src2, int n_samples);
+typedef void (*mix_func) (float *dst, float *src[], uint32_t n_src, bool aligned, uint32_t n_samples);
 
-static mix2_func mix2;
+static mix_func mix_function;
 
 struct object {
 	struct spa_list link;
@@ -400,6 +400,7 @@ struct client {
 	unsigned int default_as_system:1;
 	int self_connect_mode;
 	int rt_max;
+	unsigned int fix_midi_events:1;
 
 	jack_position_t jack_position;
 	jack_transport_state_t jack_state;
@@ -735,38 +736,40 @@ static struct buffer *dequeue_buffer(struct client *c, struct mix *mix)
 
 #if defined (__SSE__)
 #include <xmmintrin.h>
-static void mix2_sse(float *dst, float *src1, float *src2, int n_samples)
+static void mix_sse(float *dst, float *src[], uint32_t n_src, bool aligned, uint32_t n_samples)
 {
-	int n, unrolled;
-	__m128 in[2];
+	uint32_t i, n, unrolled;
+	__m128 in[1];
 
-	if (SPA_IS_ALIGNED(src1, 16) &&
-	    SPA_IS_ALIGNED(src2, 16) &&
-	    SPA_IS_ALIGNED(dst, 16))
-		unrolled = n_samples / 4;
+	if (SPA_IS_ALIGNED(dst, 16) && aligned)
+		unrolled = n_samples & ~3;
 	else
 		unrolled = 0;
 
-	for (n = 0; unrolled--; n += 4) {
-		in[0] = _mm_load_ps(&src1[n]),
-		in[1] = _mm_load_ps(&src2[n]),
-		in[0] = _mm_add_ps(in[0], in[1]);
+	for (n = 0; n < unrolled; n += 4) {
+		in[0] = _mm_load_ps(&src[0][n]);
+		for (i = 1; i < n_src; i++)
+			in[0] = _mm_add_ps(in[0], _mm_load_ps(&src[i][n]));
 		_mm_store_ps(&dst[n], in[0]);
 	}
 	for (; n < n_samples; n++) {
-		in[0] = _mm_load_ss(&src1[n]),
-		in[1] = _mm_load_ss(&src2[n]),
-		in[0] = _mm_add_ss(in[0], in[1]);
+		in[0] = _mm_load_ss(&src[0][n]);
+		for (i = 1; i < n_src; i++)
+			in[0] = _mm_add_ss(in[0], _mm_load_ss(&src[i][n]));
 		_mm_store_ss(&dst[n], in[0]);
 	}
 }
 #endif
 
-static void mix2_c(float *dst, float *src1, float *src2, int n_samples)
+static void mix_c(float *dst, float *src[], uint32_t n_src, bool aligned, uint32_t n_samples)
 {
-	int i;
-	for (i = 0; i < n_samples; i++)
-		dst[i] = src1[i] + src2[i];
+	uint32_t n, i;
+	for (n = 0; n < n_samples; n++)  {
+		float t = src[0][n];
+		for (i = 1; i < n_src; i++)
+			t += src[i][n];
+		dst[n] = t;
+	}
 }
 
 SPA_EXPORT
@@ -797,7 +800,7 @@ void jack_get_version(int *major_ptr, int *minor_ptr, int *micro_ptr, int *proto
 	} else {						\
 		if (c->active)					\
 			(expr);					\
-		pw_log_debug("skip " #callback 			\
+		pw_log_debug("skip " #callback			\
 			" cb:%p active:%d", c->callback,	\
 			c->active);				\
 	}							\
@@ -814,6 +817,9 @@ void jack_get_version(int *major_ptr, int *minor_ptr, int *micro_ptr, int *proto
 			res = c->callback(__VA_ARGS__);		\
 			c->rt_locked = false;			\
 			pthread_mutex_unlock(&c->rt_lock);	\
+		} else {					\
+			pw_log_debug("skip " #callback		\
+				" cb:%p", c->callback);		\
 		}						\
 	}							\
 	res;							\
@@ -868,6 +874,8 @@ static int do_sync(struct client *client)
 		pw_log_warn("sync requested from callback");
 		return 0;
 	}
+	if (client->last_res == -EPIPE)
+		return -EPIPE;
 
 	client->last_res = 0;
 	client->pending_sync = pw_proxy_sync((struct pw_proxy*)client->core, client->pending_sync);
@@ -985,10 +993,20 @@ static size_t convert_from_midi(void *midi, void *buffer, size_t size)
 	return b.state.offset;
 }
 
-static void convert_to_midi(struct spa_pod_sequence **seq, uint32_t n_seq, void *midi)
+static inline void fix_midi_event(uint8_t *data, size_t size)
+{
+	/* fixup NoteOn with vel 0 */
+	if (size > 2 && (data[0] & 0xF0) == 0x90 && data[2] == 0x00) {
+		data[0] = 0x80 + (data[0] & 0x0F);
+		data[2] = 0x40;
+	}
+}
+
+static void convert_to_midi(struct spa_pod_sequence **seq, uint32_t n_seq, void *midi, bool fix)
 {
 	struct spa_pod_control *c[n_seq];
 	uint32_t i;
+	int res;
 
 	for (i = 0; i < n_seq; i++)
 		c[i] = spa_pod_control_first(&seq[i]->body);
@@ -996,6 +1014,8 @@ static void convert_to_midi(struct spa_pod_sequence **seq, uint32_t n_seq, void
 	while (true) {
 		struct spa_pod_control *next = NULL;
 		uint32_t next_index = 0;
+		uint8_t *data;
+		size_t size;
 
 		for (i = 0; i < n_seq; i++) {
 			if (!spa_pod_control_is_inside(&seq[i]->body,
@@ -1010,12 +1030,17 @@ static void convert_to_midi(struct spa_pod_sequence **seq, uint32_t n_seq, void
 		if (SPA_UNLIKELY(next == NULL))
 			break;
 
+		data = SPA_POD_BODY(&next->value);
+		size = SPA_POD_BODY_SIZE(&next->value);
+
 		switch(next->type) {
 		case SPA_CONTROL_Midi:
-			jack_midi_event_write(midi,
-					next->offset,
-					SPA_POD_BODY(&next->value),
-					SPA_POD_BODY_SIZE(&next->value));
+			if (fix)
+				fix_midi_event(data, size);
+
+			if ((res = jack_midi_event_write(midi, next->offset, data, size)) < 0)
+				pw_log_warn("midi %p: can't write event: %s", midi,
+						spa_strerror(res));
 			break;
 		}
 		c[next_index] = spa_pod_control_next(c[next_index]);
@@ -3259,17 +3284,17 @@ jack_client_t * jack_client_open (const char *client_name,
 			"jack.properties", client->props);
 
 	pw_context_conf_section_match_rules(client->context.context, "jack.rules",
-			&client->props->dict, execute_match, client);
+			&client->context.context->properties->dict, execute_match, client);
 
 	support = pw_context_get_support(client->context.context, &n_support);
 
-	mix2 = mix2_c;
+	mix_function = mix_c;
 	cpu_iface = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_CPU);
 	if (cpu_iface) {
 #if defined (__SSE__)
 		uint32_t flags = spa_cpu_get_flags(cpu_iface);
 		if (flags & SPA_CPU_FLAG_SSE)
-			mix2 = mix2_sse;
+			mix_function = mix_sse;
 #endif
 	}
 	client->context.old_thread_utils =
@@ -3336,6 +3361,8 @@ jack_client_t * jack_client_open (const char *client_name,
 	}
 	if (pw_properties_get(client->props, PW_KEY_NODE_NAME) == NULL)
 		pw_properties_set(client->props, PW_KEY_NODE_NAME, client_name);
+	if (pw_properties_get(client->props, PW_KEY_NODE_GROUP) == NULL)
+		pw_properties_setf(client->props, PW_KEY_NODE_GROUP, "jack-%d", getpid());
 	if (pw_properties_get(client->props, PW_KEY_NODE_DESCRIPTION) == NULL)
 		pw_properties_set(client->props, PW_KEY_NODE_DESCRIPTION, client_name);
 	if (pw_properties_get(client->props, PW_KEY_MEDIA_TYPE) == NULL)
@@ -3383,6 +3410,7 @@ jack_client_t * jack_client_open (const char *client_name,
 	client->filter_name = pw_properties_get_bool(client->props, "jack.filter-name", false);
 	client->locked_process = pw_properties_get_bool(client->props, "jack.locked-process", true);
 	client->default_as_system = pw_properties_get_bool(client->props, "jack.default-as-system", false);
+	client->fix_midi_events = pw_properties_get_bool(client->props, "jack.fix-midi-events", true);
 
 	client->self_connect_mode = SELF_CONNECT_ALLOW;
 	if ((str = pw_properties_get(client->props, "jack.self-connect-mode")) != NULL) {
@@ -3471,6 +3499,8 @@ int jack_client_close (jack_client_t *client)
 
 	res = jack_deactivate(client);
 
+	clean_transport(c);
+
 	if (c->context.loop)
 		pw_thread_loop_stop(c->context.loop);
 
@@ -3556,8 +3586,9 @@ char *jack_get_internal_client_name (jack_client_t *client,
 SPA_EXPORT
 int jack_client_name_size (void)
 {
-	pw_log_trace("%d", JACK_CLIENT_NAME_SIZE);
-	return JACK_CLIENT_NAME_SIZE;
+	/* The JACK API specifies that this value includes the final NULL character. */
+	pw_log_trace("%d", JACK_CLIENT_NAME_SIZE+1);
+	return JACK_CLIENT_NAME_SIZE+1;
 }
 
 SPA_EXPORT
@@ -3744,12 +3775,12 @@ jack_native_thread_t jack_client_thread_id (jack_client_t *client)
 	struct client *c = (struct client *) client;
 	void *thr;
 
-	spa_return_val_if_fail(c != NULL, -EINVAL);
+	spa_return_val_if_fail(c != NULL, (pthread_t){0});
 
 	thr = pw_data_loop_get_thread(c->loop);
 	if (thr == NULL)
 		return pthread_self();
-	return *(pthread_t*)thr;
+	return (pthread_t) thr;
 }
 
 SPA_EXPORT
@@ -4397,13 +4428,14 @@ static void *get_buffer_input_float(struct port *p, jack_nframes_t frames)
 {
 	struct mix *mix;
 	struct buffer *b;
-	int layer = 0;
 	void *ptr = NULL;
+	float *mix_ptr[MAX_MIX], *np;
+	uint32_t n_ptr = 0;
+	bool ptr_aligned = true;
 
 	spa_list_for_each(mix, &p->mix, port_link) {
 		struct spa_data *d;
 		uint32_t offset, size;
-		void *np;
 
 		pw_log_trace_fp("%p: port %s mix %d.%d get buffer %d",
 				p->client, p->object->port.name, p->port_id, mix->id, frames);
@@ -4417,14 +4449,20 @@ static void *get_buffer_input_float(struct port *p, jack_nframes_t frames)
 		if (size / sizeof(float) < frames)
 			continue;
 
-		np = SPA_PTROFF(d->data, offset, void);
-		if (layer++ == 0) {
-			ptr = np;
-		} else {
-			mix2(p->emptyptr, ptr, np, frames);
-			ptr = p->emptyptr;
-			p->zeroed = false;
-		}
+		np = SPA_PTROFF(d->data, offset, float);
+		if (!SPA_IS_ALIGNED(np, 16))
+			ptr_aligned = false;
+
+		mix_ptr[n_ptr++] = np;
+		if (n_ptr == MAX_MIX)
+			break;
+	}
+	if (n_ptr == 1) {
+		ptr = mix_ptr[0];
+	} else if (n_ptr > 1) {
+		ptr = p->emptyptr;
+		mix_function(ptr, mix_ptr, n_ptr, ptr_aligned, frames);
+		p->zeroed = false;
 	}
 	if (ptr == NULL)
 		ptr = init_buffer(p);
@@ -4435,7 +4473,7 @@ static void *get_buffer_input_midi(struct port *p, jack_nframes_t frames)
 {
 	struct mix *mix;
 	void *ptr = p->emptyptr;
-	struct spa_pod_sequence *seq[MAX_MIDI_MIX];
+	struct spa_pod_sequence *seq[MAX_MIX];
 	uint32_t n_seq = 0;
 
 	jack_midi_clear_buffer(ptr);
@@ -4459,10 +4497,10 @@ static void *get_buffer_input_midi(struct port *p, jack_nframes_t frames)
 			continue;
 
 		seq[n_seq++] = pod;
-		if (n_seq == MAX_MIDI_MIX)
+		if (n_seq == MAX_MIX)
 			break;
 	}
-	convert_to_midi(seq, n_seq, ptr);
+	convert_to_midi(seq, n_seq, ptr, p->client->fix_midi_events);
 
 	return ptr;
 }
diff --git a/pipewire-v4l2/src/pipewire-v4l2.c b/pipewire-v4l2/src/pipewire-v4l2.c
index 47e1721c24fc04588b7c1a96d1aa23cf20f3a578..d2e13dbf741e56163abc6dff0803b85c1b5f1827 100644
--- a/pipewire-v4l2/src/pipewire-v4l2.c
+++ b/pipewire-v4l2/src/pipewire-v4l2.c
@@ -1756,7 +1756,7 @@ static int v4l2_ioctl(int fd, unsigned long int request, void *arg)
 	if ((file = find_file(fd)) == NULL)
 		return globals.old_fops.ioctl(fd, request, arg);
 
-#ifdef __FreeBSD__
+#if defined(__FreeBSD__) || defined(__MidnightBSD__)
 	if (arg == NULL && (request & IOC_DIRMASK != IOC_VOID)) {
 #else
 	if (arg == NULL && (_IOC_DIR(request) & (_IOC_WRITE | _IOC_READ))) {
diff --git a/pipewire-v4l2/src/v4l2-func.c b/pipewire-v4l2/src/v4l2-func.c
index 96e3ce76916836cfd9f38f18adb55b5a955833dd..c28e8342ca75dda0b2580949b5ced1544de5572c 100644
--- a/pipewire-v4l2/src/v4l2-func.c
+++ b/pipewire-v4l2/src/v4l2-func.c
@@ -22,6 +22,16 @@
  * DEALINGS IN THE SOFTWARE.
  */
 
+
+/*
+ * We need to export open* etc., but _FORTIFY_SOURCE defines conflicting
+ * always_inline versions. Disable _FORTIFY_SOURCE for this file, so we
+ * can define our overrides.
+ */
+#ifdef _FORTIFY_SOURCE
+#undef _FORTIFY_SOURCE
+#endif
+
 #include <stdio.h>
 #include <errno.h>
 #include <fcntl.h>
diff --git a/po/LINGUAS b/po/LINGUAS
index 981c7282ef7666b04d17d9c5021daa25980e8942..2532833e615678f968840d7d21edf5efe276c281 100644
--- a/po/LINGUAS
+++ b/po/LINGUAS
@@ -21,6 +21,7 @@ hu
 id
 it
 ja
+ka
 kk
 kn
 ko
diff --git a/po/ca.po b/po/ca.po
index b5c3c81e668a64c726849be4dbeed28668c61b65..6a9dc9f8fc60eff0342bd1c87e15886a1842c5fc 100644
--- a/po/ca.po
+++ b/po/ca.po
@@ -5,6 +5,7 @@
 # Xavier Conde Rueda <xavi.conde@gmail.com>, 2008.
 # Agustí Grau <fletxa@gmail.com>, 2009.
 # Judith Pintó Subirada <judithp@gmail.com>
+# Jordi Mas i Herǹandez, <jmas@softcatala.org>, 2022
 #
 # This file is translated according to the glossary and style guide of
 # Softcatalà. If you plan to modify this file, please read first the page
@@ -26,11 +27,10 @@
 msgid ""
 msgstr ""
 "Project-Id-Version: pipewire\n"
-"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/"
-"issues/new\n"
+"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/issues/new\n"
 "POT-Creation-Date: 2021-04-18 16:54+0800\n"
-"PO-Revision-Date: 2012-01-30 09:52+0000\n"
-"Last-Translator: Josep Torné Llavall <josep.torne@gmail.com>\n"
+"PO-Revision-Date: 2022-09-01 19:24+0000\n"
+"Last-Translator: Jordi Mas i Herǹandez, <jmas@softcatala.org>,\n"
 "Language-Team: Catalan <fedora@softcatala.net>\n"
 "Language: ca\n"
 "MIME-Version: 1.0\n"
@@ -45,14 +45,18 @@ msgid ""
 "      --version                         Show version\n"
 "  -c, --config                          Load config (Default %s)\n"
 msgstr ""
+"%s [opcions]\n"
+"  -h, --help                            Mostra aquesta ajuda\n"
+"      --version                         Mostra la versió\n"
+"  -c, --config                          Carrega la configuració (predeterminada %s)\n"
 
 #: src/daemon/pipewire.desktop.in:4
 msgid "PipeWire Media System"
-msgstr ""
+msgstr "Sistema multimèdia PipeWire"
 
 #: src/daemon/pipewire.desktop.in:5
 msgid "Start the PipeWire Media System"
-msgstr ""
+msgstr "Inicia el sistema multimèdia PipeWire"
 
 #: src/examples/media-session/alsa-monitor.c:526
 #: spa/plugins/alsa/acp/compat.c:187
@@ -66,7 +70,7 @@ msgstr "Mòdem"
 
 #: src/examples/media-session/alsa-monitor.c:539
 msgid "Unknown device"
-msgstr ""
+msgstr "Dispositiu desconegut"
 
 #: src/tools/pw-cat.c:991
 #, c-format
@@ -77,9 +81,13 @@ msgid ""
 "  -v, --verbose                         Enable verbose operations\n"
 "\n"
 msgstr ""
+"%s [opcions] <fitxer>\n"
+"  -h, --help                            Mostra aquesta ajuda\n"
+"      --version                         Mostra la versió\n"
+"  -v, --verbose                         Habilita les operacions detallades\n"
 
 #: src/tools/pw-cat.c:998
-#, c-format
+#, c-format, fuzzy
 msgid ""
 "  -R, --remote                          Remote daemon name\n"
 "      --media-type                      Set media type (default %s)\n"
@@ -90,42 +98,58 @@ msgid ""
 "      --latency                         Set node latency (default %s)\n"
 "                                          Xunit (unit = s, ms, us, ns)\n"
 "                                          or direct samples (256)\n"
-"                                          the rate is the one of the source "
-"file\n"
+"                                          the rate is the one of the source file\n"
 "      --list-targets                    List available targets for --target\n"
 "\n"
 msgstr ""
+"-R, --remote Nom del dimoni remot\n"
+"      --media-type Estableix el tipus de mitjà (per defecte %s)\n"
+"      --media-category Estableix la categoria dels mitjans (per defecte %s)\n"
+"      --media-role Estableix el rol del mitjà (per defecte %s)\n"
+"      --target Estableix l'objectiu del node (per defecte %s)\n"
+"                                          0 vol dir que no enllaça\n"
+"      --latency Estableix latència del node (per defecte %s)\n"
+"                                          Xunit (unitat = s, ms, us, ns)\n"
+"                                          o mostres directes (256)\n"
+"                                          la taxa és la del fitxer d'origen\n"
+"      --list-targets Llista d'objectius disponibles per a --target"
 
 #: src/tools/pw-cat.c:1016
-#, c-format
+#, c-format, fuzzy
 msgid ""
-"      --rate                            Sample rate (req. for rec) (default "
-"%u)\n"
-"      --channels                        Number of channels (req. for rec) "
-"(default %u)\n"
+"      --rate                            Sample rate (req. for rec) (default %u)\n"
+"      --channels                        Number of channels (req. for rec) (default %u)\n"
 "      --channel-map                     Channel map\n"
-"                                            one of: \"stereo\", "
-"\"surround-51\",... or\n"
-"                                            comma separated list of channel "
-"names: eg. \"FL,FR\"\n"
-"      --format                          Sample format %s (req. for rec) "
-"(default %s)\n"
+"                                            one of: \"stereo\", \"surround-51\",... or\n"
+"                                            comma separated list of channel names: eg. \"FL,FR\"\n"
+"      --format                          Sample format %s (req. for rec) (default %s)\n"
 "      --volume                          Stream volume 0-1.0 (default %.3f)\n"
-"  -q  --quality                         Resampler quality (0 - 15) (default "
-"%d)\n"
+"  -q  --quality                         Resampler quality (0 - 15) (default %d)\n"
 "\n"
 msgstr ""
+"--rate Freqüència de mostreig (req. per rec) (predeterminat %u)\n"
+"      --channels Nombre de canals (req. per rec) (predeterminat %u)\n"
+"      --channel-map Mapa de canals\n"
+"                                            un dels següents: \"estèreo\", \"surround-51\",... o\n"
+"                                            Llista separada per comes dels noms dels canals: per exemple. \"FL,FR\"\n"
+"      --format Format de mostra %s (req. per a rec) (predeterminat %s)\n"
+"      --volume Volum de flux 0-1.0 (predeterminat %.3f)\n"
+"  -q --qualitat Remostrador de qualitat (0 - 15) (per defecte %d)"
 
 #: src/tools/pw-cat.c:1033
+#, fuzzy
 msgid ""
 "  -p, --playback                        Playback mode\n"
 "  -r, --record                          Recording mode\n"
 "  -m, --midi                            Midi mode\n"
 "\n"
 msgstr ""
+"-p, --playback Mode de reproducció\n"
+"  -r, --record mode d'enregistrament\n"
+"  -m, --midi Mode MIDI"
 
 #: src/tools/pw-cli.c:2932
-#, c-format
+#, c-format, fuzzy
 msgid ""
 "%s [options] [command]\n"
 "  -h, --help                            Show this help\n"
@@ -134,10 +158,15 @@ msgid ""
 "  -r, --remote                          Remote daemon name\n"
 "\n"
 msgstr ""
+"%s ]opcions] ]ordre]\n"
+"  -h, --help Mostra aquesta ajuda\n"
+"      --version Mostra la versió\n"
+"  -d, --daemon Inicia com a dimoni (fals predeterminat)\n"
+"  -r, --remote Nom del dimoni remot"
 
 #: spa/plugins/alsa/acp/acp.c:290
 msgid "Pro Audio"
-msgstr ""
+msgstr "Pro Audio"
 
 #: spa/plugins/alsa/acp/acp.c:411 spa/plugins/alsa/acp/alsa-mixer.c:4704
 #: spa/plugins/bluez5/bluez5-device.c:1000
@@ -252,25 +281,23 @@ msgstr "Entrada analògica"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:2801
 msgid "Dock Microphone"
-msgstr ""
+msgstr "Micròfon de l'acoblador"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:2803
 msgid "Headset Microphone"
-msgstr ""
+msgstr "Micròfon d'auriculars"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:2807
 msgid "Analog Output"
 msgstr "Sortida analògica"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:2809
-#, fuzzy
 msgid "Headphones 2"
-msgstr "Auriculars"
+msgstr "Auriculars 2"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:2810
-#, fuzzy
 msgid "Headphones Mono Output"
-msgstr "Sortida mono analògica"
+msgstr "Sortida mono dels auriculars"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:2811
 msgid "Line Out"
@@ -297,49 +324,41 @@ msgid "Digital Input (S/PDIF)"
 msgstr "Entrada digital (S/PDIF)"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:2817
-#, fuzzy
 msgid "Multichannel Input"
-msgstr "Multicanal"
+msgstr "Entrada multicanal"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:2818
-#, fuzzy
 msgid "Multichannel Output"
-msgstr "Multicanal"
+msgstr "Sortida multicanal"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:2819
-#, fuzzy
 msgid "Game Output"
-msgstr "Sortida %s"
+msgstr "Sortida del joc"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:2820
 #: spa/plugins/alsa/acp/alsa-mixer.c:2821
-#, fuzzy
 msgid "Chat Output"
-msgstr "Sortida %s"
+msgstr "Sortida del xat"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:2822
-#, fuzzy
 msgid "Chat Input"
-msgstr "Entrada %s"
+msgstr "Entrada del xat"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:2823
-#, fuzzy
 msgid "Virtual Surround 7.1"
-msgstr "Envoltant analògic 7.1"
+msgstr "Envoltant virtual 7.1"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:4527
 msgid "Analog Mono"
 msgstr "Mono analògic"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:4528
-#, fuzzy
 msgid "Analog Mono (Left)"
-msgstr "Mono analògic"
+msgstr "Mono analògic (esquerra)"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:4529
-#, fuzzy
 msgid "Analog Mono (Right)"
-msgstr "Mono analògic"
+msgstr "Mono analògic (dreta)"
 
 #. Note: Not translated to "Analog Stereo Input", because the source
 #. * name gets "Input" appended to it automatically, so adding "Input"
@@ -364,13 +383,12 @@ msgstr "Estèreo"
 #: spa/plugins/alsa/acp/alsa-mixer.c:4698
 #: spa/plugins/bluez5/bluez5-device.c:1135
 msgid "Headset"
-msgstr "Auricular"
+msgstr "Auriculars"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:4541
 #: spa/plugins/alsa/acp/alsa-mixer.c:4699
-#, fuzzy
 msgid "Speakerphone"
-msgstr "Altaveu"
+msgstr "Altaveu del telèfon"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:4542
 #: spa/plugins/alsa/acp/alsa-mixer.c:4543
@@ -379,23 +397,23 @@ msgstr "Multicanal"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:4544
 msgid "Analog Surround 2.1"
-msgstr "So envoltant analògic 2.1"
+msgstr "Envoltant analògic 2.1"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:4545
 msgid "Analog Surround 3.0"
-msgstr "So envoltant analògic 3.0"
+msgstr "Envoltant analògic 3.0"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:4546
 msgid "Analog Surround 3.1"
-msgstr "So envoltant analògic 4.1"
+msgstr "Envoltant analògic 3.1"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:4547
 msgid "Analog Surround 4.0"
-msgstr "Envoltant analògic 4.0 "
+msgstr "Envoltant analògic 4.0"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:4548
 msgid "Analog Surround 4.1"
-msgstr "Envoltant analògic 4.1 "
+msgstr "Envoltant analògic 4.1"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:4549
 msgid "Analog Surround 5.0"
@@ -407,15 +425,15 @@ msgstr "Envoltant analògic 5.1"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:4551
 msgid "Analog Surround 6.0"
-msgstr "So envoltant analògic 6.0"
+msgstr "Envoltant analògic 6.0"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:4552
 msgid "Analog Surround 6.1"
-msgstr "So envoltant analògic 6.1"
+msgstr "Envoltant analògic 6.1"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:4553
 msgid "Analog Surround 7.0"
-msgstr "So envoltant analògic 7.0"
+msgstr "Envoltant analògic 7.0"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:4554
 msgid "Analog Surround 7.1"
@@ -431,11 +449,11 @@ msgstr "Envoltant digital 4.0 (IEC958/AC3)"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:4557
 msgid "Digital Surround 5.1 (IEC958/AC3)"
-msgstr "Envolvent digital 5.1 (IEC958/AC3)"
+msgstr "Envoltant digital 5.1 (IEC958/AC3)"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:4558
 msgid "Digital Surround 5.1 (IEC958/DTS)"
-msgstr "So envoltant digital 5.1 (IEC958/DTS)"
+msgstr "Envoltant digital 5.1 (IEC958/DTS)"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:4559
 msgid "Digital Stereo (HDMI)"
@@ -443,15 +461,15 @@ msgstr "Estèreo digital (HDMI)"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:4560
 msgid "Digital Surround 5.1 (HDMI)"
-msgstr "So envoltant digital 5.1 (HDMI)"
+msgstr "Envoltant digital 5.1 (HDMI)"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:4561
 msgid "Chat"
-msgstr ""
+msgstr "Xat"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:4562
 msgid "Game"
-msgstr ""
+msgstr "Joc"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:4696
 msgid "Analog Mono Duplex"
@@ -466,18 +484,16 @@ msgid "Digital Stereo Duplex (IEC958)"
 msgstr "Dúplex estèreo digital (IEC958)"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:4701
-#, fuzzy
 msgid "Multichannel Duplex"
-msgstr "Multicanal"
+msgstr "Dúplex Multicanal"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:4702
-#, fuzzy
 msgid "Stereo Duplex"
-msgstr "Dúplex estèreo analògic"
+msgstr "Dúplex estèreo"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:4703
 msgid "Mono Chat + 7.1 Surround"
-msgstr ""
+msgstr "Xat mono + 7.1 envoltant"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:4806
 #, c-format
@@ -492,115 +508,87 @@ msgstr "Entrada %s"
 #: spa/plugins/alsa/acp/alsa-util.c:1175 spa/plugins/alsa/acp/alsa-util.c:1269
 #, fuzzy, c-format
 msgid ""
-"snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu "
-"ms).\n"
-"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
-"to the ALSA developers."
+"snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue to the ALSA developers."
 msgid_plural ""
-"snd_pcm_avail() returned a value that is exceptionally large: %lu bytes (%lu "
-"ms).\n"
-"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
-"to the ALSA developers."
+"snd_pcm_avail() returned a value that is exceptionally large: %lu bytes (%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue to the ALSA developers."
 msgstr[0] ""
-"snd_pcm_avail() ha retornat un valor excepcionalment gran: %lu bytes (%lu "
-"ms).\n"
-"Probablement es tracta d'un error del controlador de l'ALSA '%s'. Informeu "
-"d'aquest incident als desenvolupadors de l'ALSA."
+"snd_pcm_avail() ha retornat un valor excepcionalment gran: %lu bytes (%lu ms).\n"
+"Probablement es tracta d'un error del controlador de l'ALSA '%s'. Informeu d'aquest incident als desenvolupadors de l'ALSA."
 msgstr[1] ""
-"snd_pcm_avail() ha retornat un valor excepcionalment gran: %lu bytes (%lu "
-"ms).\n"
-"Probablement es tracta d'un error del controlador de l'ALSA '%s'. Informeu "
-"d'aquest incident als desenvolupadors de l'ALSA."
+"snd_pcm_avail() ha retornat un valor excepcionalment gran: %lu bytes (%lu ms).\n"
+"Probablement es tracta d'un error del controlador de l'ALSA '%s'. Informeu d'aquest incident als desenvolupadors de l'ALSA."
 
 #: spa/plugins/alsa/acp/alsa-util.c:1241
 #, fuzzy, c-format
 msgid ""
-"snd_pcm_delay() returned a value that is exceptionally large: %li byte (%s"
-"%lu ms).\n"
-"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
-"to the ALSA developers."
+"snd_pcm_delay() returned a value that is exceptionally large: %li byte (%s%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue to the ALSA developers."
 msgid_plural ""
-"snd_pcm_delay() returned a value that is exceptionally large: %li bytes (%s"
-"%lu ms).\n"
-"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
-"to the ALSA developers."
+"snd_pcm_delay() returned a value that is exceptionally large: %li bytes (%s%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue to the ALSA developers."
 msgstr[0] ""
-"snd_pcm_delay() ha retornat un valor excepcionalment gran: %li bytes (%s%lu "
-"ms).\n"
-"Probablement es tracta d'un error del controlador de l'ALSA '%s'. Informeu "
-"d'aquest incident als desenvolupadors de l'ALSA."
+"snd_pcm_delay() ha retornat un valor excepcionalment gran: %li bytes (%s%lu ms).\n"
+"Probablement es tracta d'un error del controlador de l'ALSA '%s'. Informeu d'aquest incident als desenvolupadors de l'ALSA."
 msgstr[1] ""
-"snd_pcm_delay() ha retornat un valor excepcionalment gran: %li bytes (%s%lu "
-"ms).\n"
-"Probablement es tracta d'un error del controlador de l'ALSA '%s'. Informeu "
-"d'aquest incident als desenvolupadors de l'ALSA."
+"snd_pcm_delay() ha retornat un valor excepcionalment gran: %li bytes (%s%lu ms).\n"
+"Probablement es tracta d'un error del controlador de l'ALSA '%s'. Informeu d'aquest incident als desenvolupadors de l'ALSA."
 
 #: spa/plugins/alsa/acp/alsa-util.c:1288
 #, c-format
 msgid ""
-"snd_pcm_avail_delay() returned strange values: delay %lu is less than avail "
-"%lu.\n"
-"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
-"to the ALSA developers."
+"snd_pcm_avail_delay() returned strange values: delay %lu is less than avail %lu.\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue to the ALSA developers."
 msgstr ""
-"snd_pcm_avail() ha retornat un valor excepcionalment gran: %lu bytes (%lu "
-"ms).\n"
-"Probablement es tracta d'un error del controlador d'ALSA «%s». Informeu "
-"d'aquest problema als desenvolupadors d'ALSA."
+"snd_pcm_avail() ha retornat un valor excepcionalment gran: %lu bytes (%lu ms).\n"
+"Probablement es tracta d'un error del controlador d'ALSA «%s». Informeu d'aquest problema als desenvolupadors d'ALSA."
 
 #: spa/plugins/alsa/acp/alsa-util.c:1331
 #, fuzzy, c-format
 msgid ""
-"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu byte "
-"(%lu ms).\n"
-"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
-"to the ALSA developers."
+"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu byte (%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue to the ALSA developers."
 msgid_plural ""
-"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu bytes "
-"(%lu ms).\n"
-"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
-"to the ALSA developers."
+"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu bytes (%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue to the ALSA developers."
 msgstr[0] ""
-"snd_pcm_mmap_begin() ha retornat un valor excepcionalment gran: %lu bytes "
-"(%lu ms).\n"
-"Probablement es tracta d'un error del controlador de l'ALSA '%s'. Informeu "
-"d'aquest incident als desenvolupadors de l'ALSA."
+"snd_pcm_mmap_begin() ha retornat un valor excepcionalment gran: %lu bytes (%lu ms).\n"
+"Probablement es tracta d'un error del controlador de l'ALSA '%s'. Informeu d'aquest incident als desenvolupadors de l'ALSA."
 msgstr[1] ""
-"snd_pcm_mmap_begin() ha retornat un valor excepcionalment gran: %lu bytes "
-"(%lu ms).\n"
-"Probablement es tracta d'un error del controlador de l'ALSA '%s'. Informeu "
-"d'aquest incident als desenvolupadors de l'ALSA."
+"snd_pcm_mmap_begin() ha retornat un valor excepcionalment gran: %lu bytes (%lu ms).\n"
+"Probablement es tracta d'un error del controlador de l'ALSA '%s'. Informeu d'aquest incident als desenvolupadors de l'ALSA."
 
 #: spa/plugins/bluez5/bluez5-device.c:1010
 msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
-msgstr ""
+msgstr "Passarel·la d'àudio (A2DP Source & HSP/HFP AG)"
 
 #: spa/plugins/bluez5/bluez5-device.c:1033
 #, c-format
 msgid "High Fidelity Playback (A2DP Sink, codec %s)"
-msgstr ""
+msgstr "Reproducció d'alta fidelitat (Sink A2DP, còdec %s)"
 
 #: spa/plugins/bluez5/bluez5-device.c:1035
 #, c-format
 msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
-msgstr ""
+msgstr "Dúplex d'alta fidelitat (A2DP Source/Sink, còdec %s)"
 
 #: spa/plugins/bluez5/bluez5-device.c:1041
 msgid "High Fidelity Playback (A2DP Sink)"
-msgstr ""
+msgstr "Reproducció d'alta fidelitat (A2DP Sink)"
 
 #: spa/plugins/bluez5/bluez5-device.c:1043
 msgid "High Fidelity Duplex (A2DP Source/Sink)"
-msgstr ""
+msgstr "Dúplex d'alta fidelitat (A2DP Source/Sink)"
 
 #: spa/plugins/bluez5/bluez5-device.c:1070
 #, c-format
 msgid "Headset Head Unit (HSP/HFP, codec %s)"
-msgstr ""
+msgstr "Unitat d'ariculars pel cap (HSP/HFP, còdec %s)"
 
 #: spa/plugins/bluez5/bluez5-device.c:1074
 msgid "Headset Head Unit (HSP/HFP)"
-msgstr ""
+msgstr "Unitat d'ariculars pel cap (HSP/HFP)"
 
 #: spa/plugins/bluez5/bluez5-device.c:1140
 msgid "Handsfree"
@@ -612,7 +600,7 @@ msgstr "Auricular"
 
 #: spa/plugins/bluez5/bluez5-device.c:1160
 msgid "Portable"
-msgstr ""
+msgstr "Portable"
 
 #: spa/plugins/bluez5/bluez5-device.c:1165
 msgid "Car"
@@ -620,13 +608,12 @@ msgstr "Cotxe"
 
 #: spa/plugins/bluez5/bluez5-device.c:1170
 msgid "HiFi"
-msgstr ""
+msgstr "HiFi"
 
 #: spa/plugins/bluez5/bluez5-device.c:1175
 msgid "Phone"
 msgstr "Telèfon"
 
 #: spa/plugins/bluez5/bluez5-device.c:1181
-#, fuzzy
 msgid "Bluetooth"
-msgstr "Entrada bluetooth"
+msgstr "Bluetooth"
diff --git a/po/gl.po b/po/gl.po
index 5ceddce8e9f680963d1b6aea2480778436683651..8be384f17eb1df145ea03a26d9533bb9ef0b130e 100644
--- a/po/gl.po
+++ b/po/gl.po
@@ -4,25 +4,31 @@
 # Translators:
 # bassball93 <bassball93@gmail.com>, 2011.
 # mbouzada <mbouzada@gmail.com>, 2011.
-# Fran Dieguez <frandieguez@gnome.org>, 2012, 2019.
 # Marcos Lans <marcoslansgarza@gmail.com>, 2018.
+# Fran Dieguez <frandieguez@gnome.org>, 2012-2022.
+#
 msgid ""
 msgstr ""
 "Project-Id-Version: PipeWire\n"
-"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/"
-"issues/new\n"
-"POT-Creation-Date: 2021-04-18 16:54+0800\n"
-"PO-Revision-Date: 2019-02-20 01:36+0200\n"
+"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/-/"
+"issues\n"
+"POT-Creation-Date: 2022-07-10 03:27+0000\n"
+"PO-Revision-Date: 2022-08-23 09:47+0200\n"
 "Last-Translator: Fran Dieguez <frandieguez@gnome.org>\n"
-"Language-Team: Galician\n"
+"Language-Team: Galician <Proxecto Trasno <proxecto@trasno.gal>>\n"
 "Language: gl\n"
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"Plural-Forms: nplurals=2; plural=(n != 1);\n"
-"X-Generator: Virtaal 0.7.1\n"
-
-#: src/daemon/pipewire.c:43
+"Plural-Forms: nplurals=2; plural=(n != 1)\n"
+"X-Generator: Gtranslator 40.0\n"
+"X-DL-Team: gl\n"
+"X-DL-Module: PipeWire\n"
+"X-DL-Branch: master\n"
+"X-DL-Domain: po\n"
+"X-DL-State: Translating\n"
+
+#: src/daemon/pipewire.c:46
 #, c-format
 msgid ""
 "%s [options]\n"
@@ -30,6 +36,11 @@ msgid ""
 "      --version                         Show version\n"
 "  -c, --config                          Load config (Default %s)\n"
 msgstr ""
+"%s [opcións]\n"
+"  -h, --help                            Mostra esta axuda\n"
+"      --version                         Mostrar versión\n"
+"  -c, --config                          Cargar configuración (Predeterminado "
+"%s)\n"
 
 #: src/daemon/pipewire.desktop.in:4
 msgid "PipeWire Media System"
@@ -39,31 +50,52 @@ msgstr "Sistema multimedia PipeWire"
 msgid "Start the PipeWire Media System"
 msgstr "Iniciar o Sistema multimedia PipeWire"
 
-#: src/examples/media-session/alsa-monitor.c:526
-#: spa/plugins/alsa/acp/compat.c:187
-msgid "Built-in Audio"
-msgstr "Audio interno"
+#: src/modules/module-protocol-pulse/modules/module-tunnel-sink.c:180
+#: src/modules/module-protocol-pulse/modules/module-tunnel-source.c:180
+#, c-format
+msgid "Tunnel to %s/%s"
+msgstr "Túnel a %s/%s"
 
-#: src/examples/media-session/alsa-monitor.c:530
-#: spa/plugins/alsa/acp/compat.c:192
-msgid "Modem"
-msgstr "Módem"
+#: src/modules/module-fallback-sink.c:51
+#| msgid "Game Output"
+msgid "Dummy Output"
+msgstr "Saída de proba"
 
-#: src/examples/media-session/alsa-monitor.c:539
+#: src/modules/module-pulse-tunnel.c:648
+#, c-format
+msgid "Tunnel for %s@%s"
+msgstr "Túnel para %s@%s"
+
+#: src/modules/module-zeroconf-discover.c:332
 msgid "Unknown device"
-msgstr ""
+msgstr "Dispositivo descoñecido"
+
+#: src/modules/module-zeroconf-discover.c:344
+#, c-format
+msgid "%s on %s@%s"
+msgstr "%s en %s@%s"
+
+#: src/modules/module-zeroconf-discover.c:348
+#, c-format
+msgid "%s on %s"
+msgstr "%s en %s"
 
-#: src/tools/pw-cat.c:991
+#: src/tools/pw-cat.c:784
 #, c-format
 msgid ""
-"%s [options] <file>\n"
+"%s [options] [<file>|-]\n"
 "  -h, --help                            Show this help\n"
 "      --version                         Show version\n"
 "  -v, --verbose                         Enable verbose operations\n"
 "\n"
 msgstr ""
+"%s [opcións] [<ficheiro>|-]\n"
+"  -h, --help                            Mostrar esta axuda\n"
+"      --version                         Mostrar versión\n"
+"  -v, --verbose                         Activar operacións verbosas\n"
+"\n"
 
-#: src/tools/pw-cat.c:998
+#: src/tools/pw-cat.c:791
 #, c-format
 msgid ""
 "  -R, --remote                          Remote daemon name\n"
@@ -77,11 +109,29 @@ msgid ""
 "                                          or direct samples (256)\n"
 "                                          the rate is the one of the source "
 "file\n"
-"      --list-targets                    List available targets for --target\n"
+"  -P  --properties                      Set node properties\n"
 "\n"
 msgstr ""
+"  -R, --remote                          Nome do daemon remoto\n"
+"      --media-type                      Estabelecer o tipo de medio (por "
+"omisión %s)\n"
+"      --media-category                  Estabelecer a categoría multimedia "
+"(por omisión %s)\n"
+"      --media-role                      Estabelecer o rol multimedia (por "
+"omisión %s)\n"
+"      --target                          Estabelecer o nodo obxectivo (por "
+"omisión %s)\n"
+"                                          0 significa non ligar\n"
+"      --latency                         Estabelecer a latencia do nodo (por "
+"omisión %s)\n"
+"                                          Xunit (unidade = s, ms, us, ns)\n"
+"                                          ou mostras directas samples (256)\n"
+"                                          a taxa é un dos ficheiros de "
+"orixe\n"
+"  -P  --properties                      Estabelecer as propiedades do nodo\n"
+"\n"
 
-#: src/tools/pw-cat.c:1016
+#: src/tools/pw-cat.c:809
 #, c-format
 msgid ""
 "      --rate                            Sample rate (req. for rec) (default "
@@ -100,16 +150,38 @@ msgid ""
 "%d)\n"
 "\n"
 msgstr ""
+"      --rate                            Taxa de mostreo (solicitudes por "
+"segundo) (por omisión %u)\n"
+"      --channels                        Número de canles (solicitudes por "
+"segundo) (por omisión %u)\n"
+"      --channel-map                     Mapa de canles\n"
+"                                            un de: \"stereo\", "
+"\"surround-51\",... or\n"
+"                                            lista separada por comas dos "
+"nomes das canles: p.ex. \"FL,FR\"\n"
+"      --format                          Formato de mostras %s (solicitudes "
+"por segundo) (por omisión %s)\n"
+"      --volume                          Volume do fluxo 0-1.0 (por omisión "
+"%.3f)\n"
+"  -q  --quality                         Calidade do remostreador (0 - 15) "
+"(por omisión %d)\n"
+"\n"
 
-#: src/tools/pw-cat.c:1033
+#: src/tools/pw-cat.c:826
 msgid ""
 "  -p, --playback                        Playback mode\n"
 "  -r, --record                          Recording mode\n"
 "  -m, --midi                            Midi mode\n"
+"  -d, --dsd                             DSD mode\n"
 "\n"
 msgstr ""
+"  -p, --playback                        Modo de reprodución\n"
+"  -r, --record                          Modo de grabación\n"
+"  -m, --midi                            Modo MIDI\n"
+"  -d, --dsd                             Modo DSD\n"
+"\n"
 
-#: src/tools/pw-cli.c:2932
+#: src/tools/pw-cli.c:3165
 #, c-format
 msgid ""
 "%s [options] [command]\n"
@@ -119,355 +191,352 @@ msgid ""
 "  -r, --remote                          Remote daemon name\n"
 "\n"
 msgstr ""
+"%s [opcións] [orde]\n"
+"  -h, --help                            Mostrar esta axuda\n"
+"      --version                         Mostrar versión\n"
+"  -d, --daemon                          Iniciar como demonio (Por omisión "
+"falso)\n"
+"  -r, --remote                          Modo de demonio remoto\n"
+"\n"
 
-#: spa/plugins/alsa/acp/acp.c:290
+#: spa/plugins/alsa/acp/acp.c:321
 msgid "Pro Audio"
-msgstr ""
+msgstr "Pro Audio"
 
-#: spa/plugins/alsa/acp/acp.c:411 spa/plugins/alsa/acp/alsa-mixer.c:4704
-#: spa/plugins/bluez5/bluez5-device.c:1000
+#: spa/plugins/alsa/acp/acp.c:446 spa/plugins/alsa/acp/alsa-mixer.c:4648
+#: spa/plugins/bluez5/bluez5-device.c:1185
 msgid "Off"
 msgstr "Apagado"
 
-#: spa/plugins/alsa/acp/channelmap.h:466
-msgid "(invalid)"
-msgstr "(incorrecto)"
-
-#: spa/plugins/alsa/acp/alsa-mixer.c:2709
+#: spa/plugins/alsa/acp/alsa-mixer.c:2652
 msgid "Input"
 msgstr "Entrada"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2710
+#: spa/plugins/alsa/acp/alsa-mixer.c:2653
 msgid "Docking Station Input"
 msgstr "Entrada de estación acoplada (Docking Station)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2711
+#: spa/plugins/alsa/acp/alsa-mixer.c:2654
 msgid "Docking Station Microphone"
 msgstr "Micrófono da estación acoplada (Docking Station)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2712
+#: spa/plugins/alsa/acp/alsa-mixer.c:2655
 msgid "Docking Station Line In"
 msgstr "Entrada de estación acoplada (Docking Station)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2713
-#: spa/plugins/alsa/acp/alsa-mixer.c:2804
+#: spa/plugins/alsa/acp/alsa-mixer.c:2656
+#: spa/plugins/alsa/acp/alsa-mixer.c:2747
 msgid "Line In"
 msgstr "Liña de entrada"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2714
-#: spa/plugins/alsa/acp/alsa-mixer.c:2798
-#: spa/plugins/bluez5/bluez5-device.c:1145
+#: spa/plugins/alsa/acp/alsa-mixer.c:2657
+#: spa/plugins/alsa/acp/alsa-mixer.c:2741
+#: spa/plugins/bluez5/bluez5-device.c:1357
 msgid "Microphone"
 msgstr "Micrófono"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2715
-#: spa/plugins/alsa/acp/alsa-mixer.c:2799
+#: spa/plugins/alsa/acp/alsa-mixer.c:2658
+#: spa/plugins/alsa/acp/alsa-mixer.c:2742
 msgid "Front Microphone"
 msgstr "Micrófono frontal"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2716
-#: spa/plugins/alsa/acp/alsa-mixer.c:2800
+#: spa/plugins/alsa/acp/alsa-mixer.c:2659
+#: spa/plugins/alsa/acp/alsa-mixer.c:2743
 msgid "Rear Microphone"
 msgstr "Micrófono traseiro"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2717
+#: spa/plugins/alsa/acp/alsa-mixer.c:2660
 msgid "External Microphone"
 msgstr "Micrófono externo"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2718
-#: spa/plugins/alsa/acp/alsa-mixer.c:2802
+#: spa/plugins/alsa/acp/alsa-mixer.c:2661
+#: spa/plugins/alsa/acp/alsa-mixer.c:2745
 msgid "Internal Microphone"
 msgstr "Micrófono interno"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2719
-#: spa/plugins/alsa/acp/alsa-mixer.c:2805
+#: spa/plugins/alsa/acp/alsa-mixer.c:2662
+#: spa/plugins/alsa/acp/alsa-mixer.c:2748
 msgid "Radio"
 msgstr "Radio"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2720
-#: spa/plugins/alsa/acp/alsa-mixer.c:2806
+#: spa/plugins/alsa/acp/alsa-mixer.c:2663
+#: spa/plugins/alsa/acp/alsa-mixer.c:2749
 msgid "Video"
 msgstr "Vídeo"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2721
+#: spa/plugins/alsa/acp/alsa-mixer.c:2664
 msgid "Automatic Gain Control"
 msgstr "Control automático de ganancia"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2722
+#: spa/plugins/alsa/acp/alsa-mixer.c:2665
 msgid "No Automatic Gain Control"
 msgstr "Sen control automático de ganancia"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2723
+#: spa/plugins/alsa/acp/alsa-mixer.c:2666
 msgid "Boost"
 msgstr "Enfatizador"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2724
+#: spa/plugins/alsa/acp/alsa-mixer.c:2667
 msgid "No Boost"
 msgstr "Sen enfatizador"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2725
+#: spa/plugins/alsa/acp/alsa-mixer.c:2668
 msgid "Amplifier"
 msgstr "Amplificador"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2726
+#: spa/plugins/alsa/acp/alsa-mixer.c:2669
 msgid "No Amplifier"
 msgstr "Sen amplificador"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2727
+#: spa/plugins/alsa/acp/alsa-mixer.c:2670
 msgid "Bass Boost"
 msgstr "Enfatizador baixo"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2728
+#: spa/plugins/alsa/acp/alsa-mixer.c:2671
 msgid "No Bass Boost"
 msgstr "Sen enfatizador baixo"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2729
-#: spa/plugins/bluez5/bluez5-device.c:1150
+#: spa/plugins/alsa/acp/alsa-mixer.c:2672
+#: spa/plugins/bluez5/bluez5-device.c:1363
 msgid "Speaker"
 msgstr "Altofalante"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2730
-#: spa/plugins/alsa/acp/alsa-mixer.c:2808
+#: spa/plugins/alsa/acp/alsa-mixer.c:2673
+#: spa/plugins/alsa/acp/alsa-mixer.c:2751
 msgid "Headphones"
 msgstr "Auriculares"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2797
+#: spa/plugins/alsa/acp/alsa-mixer.c:2740
 msgid "Analog Input"
 msgstr "Entrada analóxica"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2801
+#: spa/plugins/alsa/acp/alsa-mixer.c:2744
 msgid "Dock Microphone"
 msgstr "Micrófono do acople"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2803
+#: spa/plugins/alsa/acp/alsa-mixer.c:2746
 msgid "Headset Microphone"
 msgstr "Micrófono con auricular"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2807
+#: spa/plugins/alsa/acp/alsa-mixer.c:2750
 msgid "Analog Output"
 msgstr "Saída analóxica"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2809
-#, fuzzy
+#: spa/plugins/alsa/acp/alsa-mixer.c:2752
 msgid "Headphones 2"
-msgstr "Auriculares"
+msgstr "Auriculares 2"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2810
+#: spa/plugins/alsa/acp/alsa-mixer.c:2753
 msgid "Headphones Mono Output"
 msgstr "Saída monoaural para auriculares"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2811
+#: spa/plugins/alsa/acp/alsa-mixer.c:2754
 msgid "Line Out"
 msgstr "Liña de saída"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2812
+#: spa/plugins/alsa/acp/alsa-mixer.c:2755
 msgid "Analog Mono Output"
 msgstr "Saída monoaural analóxica"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2813
+#: spa/plugins/alsa/acp/alsa-mixer.c:2756
 msgid "Speakers"
 msgstr "Altofalantes"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2814
+#: spa/plugins/alsa/acp/alsa-mixer.c:2757
 msgid "HDMI / DisplayPort"
 msgstr "HDMI / DisplayPort"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2815
+#: spa/plugins/alsa/acp/alsa-mixer.c:2758
 msgid "Digital Output (S/PDIF)"
 msgstr "Saída dixital (S/PDIF)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2816
+#: spa/plugins/alsa/acp/alsa-mixer.c:2759
 msgid "Digital Input (S/PDIF)"
 msgstr "Entrada dixital (S/PDIF)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2817
+#: spa/plugins/alsa/acp/alsa-mixer.c:2760
 msgid "Multichannel Input"
 msgstr "Entrada multicanle"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2818
+#: spa/plugins/alsa/acp/alsa-mixer.c:2761
 msgid "Multichannel Output"
 msgstr "Saída multicanle"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2819
+#: spa/plugins/alsa/acp/alsa-mixer.c:2762
 msgid "Game Output"
 msgstr "Saída do xogo"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2820
-#: spa/plugins/alsa/acp/alsa-mixer.c:2821
+#: spa/plugins/alsa/acp/alsa-mixer.c:2763
+#: spa/plugins/alsa/acp/alsa-mixer.c:2764
 msgid "Chat Output"
 msgstr "Saída do chat"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2822
-#, fuzzy
+#: spa/plugins/alsa/acp/alsa-mixer.c:2765
 msgid "Chat Input"
-msgstr "Saída do chat"
+msgstr "Entrada de chat"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2823
-#, fuzzy
+#: spa/plugins/alsa/acp/alsa-mixer.c:2766
 msgid "Virtual Surround 7.1"
-msgstr "Sumideiro envolvente virtual"
+msgstr "Envolvente virtual 7.1"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4527
+#: spa/plugins/alsa/acp/alsa-mixer.c:4471
 msgid "Analog Mono"
 msgstr "Monoaural analóxico"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4528
-#, fuzzy
+#: spa/plugins/alsa/acp/alsa-mixer.c:4472
 msgid "Analog Mono (Left)"
-msgstr "Monoaural analóxico"
+msgstr "Monoaural analóxico (Esquerda)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4529
-#, fuzzy
+#: spa/plugins/alsa/acp/alsa-mixer.c:4473
 msgid "Analog Mono (Right)"
-msgstr "Monoaural analóxico"
+msgstr "Monoaural analóxico (Dereita)"
 
 #. Note: Not translated to "Analog Stereo Input", because the source
 #. * name gets "Input" appended to it automatically, so adding "Input"
 #. * here would lead to the source name to become "Analog Stereo Input
 #. * Input". The same logic applies to analog-stereo-output,
 #. * multichannel-input and multichannel-output.
-#: spa/plugins/alsa/acp/alsa-mixer.c:4530
-#: spa/plugins/alsa/acp/alsa-mixer.c:4538
-#: spa/plugins/alsa/acp/alsa-mixer.c:4539
+#: spa/plugins/alsa/acp/alsa-mixer.c:4474
+#: spa/plugins/alsa/acp/alsa-mixer.c:4482
+#: spa/plugins/alsa/acp/alsa-mixer.c:4483
 msgid "Analog Stereo"
 msgstr "Estéreo analóxico"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4531
+#: spa/plugins/alsa/acp/alsa-mixer.c:4475
 msgid "Mono"
 msgstr "Mono"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4532
+#: spa/plugins/alsa/acp/alsa-mixer.c:4476
 msgid "Stereo"
 msgstr "Estéreo"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4540
-#: spa/plugins/alsa/acp/alsa-mixer.c:4698
-#: spa/plugins/bluez5/bluez5-device.c:1135
+#: spa/plugins/alsa/acp/alsa-mixer.c:4484
+#: spa/plugins/alsa/acp/alsa-mixer.c:4642
+#: spa/plugins/bluez5/bluez5-device.c:1345
 msgid "Headset"
 msgstr "Auriculares con micro"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4541
-#: spa/plugins/alsa/acp/alsa-mixer.c:4699
-#, fuzzy
+#: spa/plugins/alsa/acp/alsa-mixer.c:4485
+#: spa/plugins/alsa/acp/alsa-mixer.c:4643
 msgid "Speakerphone"
 msgstr "Altofalante"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4542
-#: spa/plugins/alsa/acp/alsa-mixer.c:4543
+#: spa/plugins/alsa/acp/alsa-mixer.c:4486
+#: spa/plugins/alsa/acp/alsa-mixer.c:4487
 msgid "Multichannel"
 msgstr "Multicanle"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4544
+#: spa/plugins/alsa/acp/alsa-mixer.c:4488
 msgid "Analog Surround 2.1"
 msgstr "Envolvente analóxico 2.1"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4545
+#: spa/plugins/alsa/acp/alsa-mixer.c:4489
 msgid "Analog Surround 3.0"
 msgstr "Envolvente analóxico 3.0"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4546
+#: spa/plugins/alsa/acp/alsa-mixer.c:4490
 msgid "Analog Surround 3.1"
 msgstr "Envolvente analóxico 3.1"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4547
+#: spa/plugins/alsa/acp/alsa-mixer.c:4491
 msgid "Analog Surround 4.0"
 msgstr "Envolvente analóxico 4.0"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4548
+#: spa/plugins/alsa/acp/alsa-mixer.c:4492
 msgid "Analog Surround 4.1"
 msgstr "Envolvente analóxico 4.1"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4549
+#: spa/plugins/alsa/acp/alsa-mixer.c:4493
 msgid "Analog Surround 5.0"
 msgstr "Envolvente analóxico 5.0"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4550
+#: spa/plugins/alsa/acp/alsa-mixer.c:4494
 msgid "Analog Surround 5.1"
 msgstr "Envolvente analóxico 5.1"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4551
+#: spa/plugins/alsa/acp/alsa-mixer.c:4495
 msgid "Analog Surround 6.0"
 msgstr "Envolvente analóxico 6.0"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4552
+#: spa/plugins/alsa/acp/alsa-mixer.c:4496
 msgid "Analog Surround 6.1"
 msgstr "Envolvente analóxico 6.1"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4553
+#: spa/plugins/alsa/acp/alsa-mixer.c:4497
 msgid "Analog Surround 7.0"
 msgstr "Envolvente analóxico 7.0"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4554
+#: spa/plugins/alsa/acp/alsa-mixer.c:4498
 msgid "Analog Surround 7.1"
 msgstr "Envolvente analóxico 7.1"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4555
+#: spa/plugins/alsa/acp/alsa-mixer.c:4499
 msgid "Digital Stereo (IEC958)"
 msgstr "Estéreo dixital (IEC958)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4556
+#: spa/plugins/alsa/acp/alsa-mixer.c:4500
 msgid "Digital Surround 4.0 (IEC958/AC3)"
 msgstr "Envolvente dixital 4.0 (IEC958/AC3)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4557
+#: spa/plugins/alsa/acp/alsa-mixer.c:4501
 msgid "Digital Surround 5.1 (IEC958/AC3)"
 msgstr "Envolvente dixital 5.1 (IEC958/AC3)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4558
+#: spa/plugins/alsa/acp/alsa-mixer.c:4502
 msgid "Digital Surround 5.1 (IEC958/DTS)"
 msgstr "Envolvente dixital 5.1 (IEC958/ACDTS)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4559
+#: spa/plugins/alsa/acp/alsa-mixer.c:4503
 msgid "Digital Stereo (HDMI)"
 msgstr "Estéreo dixital (HDMI)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4560
+#: spa/plugins/alsa/acp/alsa-mixer.c:4504
 msgid "Digital Surround 5.1 (HDMI)"
 msgstr "Envolvente dixital 5.1 (HDMI)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4561
+#: spa/plugins/alsa/acp/alsa-mixer.c:4505
 msgid "Chat"
-msgstr ""
+msgstr "Chat"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4562
+#: spa/plugins/alsa/acp/alsa-mixer.c:4506
 msgid "Game"
-msgstr ""
+msgstr "Xogo"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4696
+#: spa/plugins/alsa/acp/alsa-mixer.c:4640
 msgid "Analog Mono Duplex"
 msgstr "Monoaural analóxico dúplex"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4697
+#: spa/plugins/alsa/acp/alsa-mixer.c:4641
 msgid "Analog Stereo Duplex"
 msgstr "Estéreo analóxico dúplex"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4700
+#: spa/plugins/alsa/acp/alsa-mixer.c:4644
 msgid "Digital Stereo Duplex (IEC958)"
 msgstr "Estéreo dixital dúplex (IEC958)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4701
+#: spa/plugins/alsa/acp/alsa-mixer.c:4645
 msgid "Multichannel Duplex"
 msgstr "Dúplex multicanle"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4702
+#: spa/plugins/alsa/acp/alsa-mixer.c:4646
 msgid "Stereo Duplex"
 msgstr "Dúplex estéreo"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4703
+#: spa/plugins/alsa/acp/alsa-mixer.c:4647
 msgid "Mono Chat + 7.1 Surround"
-msgstr ""
+msgstr "Chat mono + envolvente 7.1"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4806
+#: spa/plugins/alsa/acp/alsa-mixer.c:4754
 #, c-format
 msgid "%s Output"
 msgstr "Saída %s"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4813
+#: spa/plugins/alsa/acp/alsa-mixer.c:4761
 #, c-format
 msgid "%s Input"
 msgstr "Entrada %s"
 
-#: spa/plugins/alsa/acp/alsa-util.c:1175 spa/plugins/alsa/acp/alsa-util.c:1269
+#: spa/plugins/alsa/acp/alsa-util.c:1173 spa/plugins/alsa/acp/alsa-util.c:1267
 #, c-format
 msgid ""
 "snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu "
@@ -490,16 +559,16 @@ msgstr[1] ""
 "O máis probábel é que sexa un erro do controlador ALSA «%s». Informe disto "
 "aos desenvolvedores de ALSA."
 
-#: spa/plugins/alsa/acp/alsa-util.c:1241
+#: spa/plugins/alsa/acp/alsa-util.c:1239
 #, c-format
 msgid ""
-"snd_pcm_delay() returned a value that is exceptionally large: %li byte (%s"
-"%lu ms).\n"
+"snd_pcm_delay() returned a value that is exceptionally large: %li byte "
+"(%s%lu ms).\n"
 "Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
 "to the ALSA developers."
 msgid_plural ""
-"snd_pcm_delay() returned a value that is exceptionally large: %li bytes (%s"
-"%lu ms).\n"
+"snd_pcm_delay() returned a value that is exceptionally large: %li bytes "
+"(%s%lu ms).\n"
 "Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
 "to the ALSA developers."
 msgstr[0] ""
@@ -513,7 +582,7 @@ msgstr[1] ""
 "O máis probábel é que sexa un erro do controlador ALSA «%s». Informe disto "
 "aos desenvolvedores de ALSA."
 
-#: spa/plugins/alsa/acp/alsa-util.c:1288
+#: spa/plugins/alsa/acp/alsa-util.c:1286
 #, c-format
 msgid ""
 "snd_pcm_avail_delay() returned strange values: delay %lu is less than avail "
@@ -526,7 +595,7 @@ msgstr ""
 "O máis probábel é que sexa un erro do controlador ALSA «%s». Informe disto "
 "aos desenvolvedores de ALSA."
 
-#: spa/plugins/alsa/acp/alsa-util.c:1331
+#: spa/plugins/alsa/acp/alsa-util.c:1329
 #, c-format
 msgid ""
 "snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu byte "
@@ -549,62 +618,90 @@ msgstr[1] ""
 "O máis probábel é que sexa un erro do controlador ALSA «%s». Informe disto "
 "aos desenvolvedores de ALSA."
 
-#: spa/plugins/bluez5/bluez5-device.c:1010
+#: spa/plugins/alsa/acp/channelmap.h:457
+msgid "(invalid)"
+msgstr "(incorrecto)"
+
+#: spa/plugins/alsa/acp/compat.c:189
+msgid "Built-in Audio"
+msgstr "Audio interno"
+
+#: spa/plugins/alsa/acp/compat.c:194
+msgid "Modem"
+msgstr "Módem"
+
+#: spa/plugins/bluez5/bluez5-device.c:1196
 msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
-msgstr ""
+msgstr "Porta de enlace de son (Orixe A2DP e HSP/HFP AG)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1033
+#: spa/plugins/bluez5/bluez5-device.c:1221
 #, c-format
 msgid "High Fidelity Playback (A2DP Sink, codec %s)"
-msgstr ""
+msgstr "Reprodución de alta fidelidade (Sumideiro A2DP, códec %s)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1035
+#: spa/plugins/bluez5/bluez5-device.c:1224
 #, c-format
 msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
-msgstr ""
+msgstr "Dúplex de alta fidelidade (Orixe/sumideiro A2DP, códec %s)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1041
+#: spa/plugins/bluez5/bluez5-device.c:1232
 msgid "High Fidelity Playback (A2DP Sink)"
-msgstr ""
+msgstr "Reprodución de alta fidelidade (Sumideiro A2DP)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1043
+#: spa/plugins/bluez5/bluez5-device.c:1234
 msgid "High Fidelity Duplex (A2DP Source/Sink)"
-msgstr ""
+msgstr "Dúplex de alta fidelidade (Orixe/sumideiro A2DP)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1070
+#: spa/plugins/bluez5/bluez5-device.c:1262
 #, c-format
 msgid "Headset Head Unit (HSP/HFP, codec %s)"
-msgstr ""
+msgstr "Unidade de auriculares de cabeza  (HSP/HFP, códec %s)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1074
+#: spa/plugins/bluez5/bluez5-device.c:1267
 msgid "Headset Head Unit (HSP/HFP)"
-msgstr ""
-
-#: spa/plugins/bluez5/bluez5-device.c:1140
+msgstr "Unidade de auriculares de cabeza  (HSP/HFP)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1346
+#: spa/plugins/bluez5/bluez5-device.c:1351
+#: spa/plugins/bluez5/bluez5-device.c:1358
+#: spa/plugins/bluez5/bluez5-device.c:1364
+#: spa/plugins/bluez5/bluez5-device.c:1370
+#: spa/plugins/bluez5/bluez5-device.c:1376
+#: spa/plugins/bluez5/bluez5-device.c:1382
+#: spa/plugins/bluez5/bluez5-device.c:1388
+#: spa/plugins/bluez5/bluez5-device.c:1394
 msgid "Handsfree"
 msgstr "Sen mans"
 
-#: spa/plugins/bluez5/bluez5-device.c:1155
+#: spa/plugins/bluez5/bluez5-device.c:1352
+#| msgid "Handsfree"
+msgid "Handsfree (HFP)"
+msgstr "Sen mans (HFP)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1369
 msgid "Headphone"
 msgstr "Auriculares"
 
-#: spa/plugins/bluez5/bluez5-device.c:1160
+#: spa/plugins/bluez5/bluez5-device.c:1375
 msgid "Portable"
 msgstr "Portátil"
 
-#: spa/plugins/bluez5/bluez5-device.c:1165
+#: spa/plugins/bluez5/bluez5-device.c:1381
 msgid "Car"
 msgstr "Automóbil"
 
-#: spa/plugins/bluez5/bluez5-device.c:1170
+#: spa/plugins/bluez5/bluez5-device.c:1387
 msgid "HiFi"
-msgstr "Hifi"
+msgstr "HiFi"
 
-#: spa/plugins/bluez5/bluez5-device.c:1175
+#: spa/plugins/bluez5/bluez5-device.c:1393
 msgid "Phone"
 msgstr "Teléfono"
 
-#: spa/plugins/bluez5/bluez5-device.c:1181
-#, fuzzy
+#: spa/plugins/bluez5/bluez5-device.c:1400
 msgid "Bluetooth"
-msgstr "Entrada de Bluetooth"
+msgstr "Bluetooth"
+
+#: spa/plugins/bluez5/bluez5-device.c:1401
+msgid "Bluetooth (HFP)"
+msgstr "Bluetooth (HFP)"
diff --git a/po/hr.po b/po/hr.po
index 64927d56d5188f2296cbed2c7d2df459fbc98952..e8c8c02119c0e65560f6d0f8ee121c4080d50b75 100644
--- a/po/hr.po
+++ b/po/hr.po
@@ -7,8 +7,8 @@ msgid ""
 msgstr ""
 "Project-Id-Version: pipewire\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2022-04-03 12:56+0200\n"
-"PO-Revision-Date: 2022-04-03 12:57+0200\n"
+"POT-Creation-Date: 2022-06-30 12:50+0200\n"
+"PO-Revision-Date: 2022-06-30 13:14+0200\n"
 "Last-Translator: gogo <trebelnik2@gmail.com>\n"
 "Language-Team: Croatian <https://translate.fedoraproject.org/projects/"
 "pipewire/pipewire/hr/>\n"
@@ -34,8 +34,8 @@ msgstr ""
 "      --version                         Prikaži inačicu\n"
 "  -c, --config                          Učitaj podešavanje (Zadano %s)\n"
 
-#: src/modules/module-protocol-pulse/modules/module-tunnel-sink.c:190
-#: src/modules/module-protocol-pulse/modules/module-tunnel-source.c:190
+#: src/modules/module-protocol-pulse/modules/module-tunnel-sink.c:180
+#: src/modules/module-protocol-pulse/modules/module-tunnel-source.c:180
 #, c-format
 msgid "Tunnel to %s/%s"
 msgstr "Tunel do %s/%s"
@@ -44,41 +44,41 @@ msgstr "Tunel do %s/%s"
 msgid "Dummy Output"
 msgstr "Lažni izlaz"
 
-#: src/modules/module-pulse-tunnel.c:545
+#: src/modules/module-pulse-tunnel.c:648
 #, c-format
 msgid "Tunnel for %s@%s"
 msgstr "Tunel za %s@%s"
 
-#: src/modules/module-zeroconf-discover.c:313
+#: src/modules/module-zeroconf-discover.c:332
 msgid "Unknown device"
 msgstr "Nepoznat uređaj"
 
-#: src/modules/module-zeroconf-discover.c:325
+#: src/modules/module-zeroconf-discover.c:344
 #, c-format
 msgid "%s on %s@%s"
 msgstr "%s na %s@%s"
 
-#: src/modules/module-zeroconf-discover.c:329
+#: src/modules/module-zeroconf-discover.c:348
 #, c-format
 msgid "%s on %s"
 msgstr "%s na %s"
 
-#: src/tools/pw-cat.c:1087
+#: src/tools/pw-cat.c:784
 #, c-format
 msgid ""
-"%s [options] <file>\n"
+"%s [options] [<file>|-]\n"
 "  -h, --help                            Show this help\n"
 "      --version                         Show version\n"
 "  -v, --verbose                         Enable verbose operations\n"
 "\n"
 msgstr ""
-"%s [mogućnosti] <datoteka>\n"
+"%s [mogućnosti] [<datoteka>|-]\n"
 "  -h, --help                            Prikaži ovu pomoć\n"
 "      --version                         Prikaži inačicu\n"
 "  -v, --verbose                         Omogući opširnije radnje\n"
 "\n"
 
-#: src/tools/pw-cat.c:1094
+#: src/tools/pw-cat.c:791
 #, c-format
 msgid ""
 "  -R, --remote                          Remote daemon name\n"
@@ -92,7 +92,7 @@ msgid ""
 "                                          or direct samples (256)\n"
 "                                          the rate is the one of the source "
 "file\n"
-"      --list-targets                    List available targets for --target\n"
+"  -P  --properties                      Set node properties\n"
 "\n"
 msgstr ""
 "  -R, --remote                          Naziv udaljenog pozadinskog "
@@ -111,11 +111,10 @@ msgstr ""
 "                                          ili izravne uzorke (256)\n"
 "                                          frekvencija je jednaka izvornoj "
 "datoteci\n"
-"      --list-targets                    Prikaži dostupna odredišta za --"
-"target\n"
+"  -P  --properties                      Postavi svojstva čvora\n"
 "\n"
 
-#: src/tools/pw-cat.c:1112
+#: src/tools/pw-cat.c:809
 #, c-format
 msgid ""
 "      --rate                            Sample rate (req. for rec) (default "
@@ -151,7 +150,7 @@ msgstr ""
 "15) (zadano je %d)\n"
 "\n"
 
-#: src/tools/pw-cat.c:1129
+#: src/tools/pw-cat.c:826
 msgid ""
 "  -p, --playback                        Playback mode\n"
 "  -r, --record                          Recording mode\n"
@@ -165,7 +164,7 @@ msgstr ""
 "  -d, --dsd                             DSD način\n"
 "\n"
 
-#: src/tools/pw-cli.c:3051
+#: src/tools/pw-cli.c:3165
 #, c-format
 msgid ""
 "%s [options] [command]\n"
@@ -188,8 +187,8 @@ msgstr ""
 msgid "Pro Audio"
 msgstr "Pro Audio"
 
-#: spa/plugins/alsa/acp/acp.c:444 spa/plugins/alsa/acp/alsa-mixer.c:4648
-#: spa/plugins/bluez5/bluez5-device.c:1159
+#: spa/plugins/alsa/acp/acp.c:446 spa/plugins/alsa/acp/alsa-mixer.c:4648
+#: spa/plugins/bluez5/bluez5-device.c:1161
 msgid "Off"
 msgstr "Isključeno"
 
@@ -216,7 +215,7 @@ msgstr "Ulaz"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:2657
 #: spa/plugins/alsa/acp/alsa-mixer.c:2741
-#: spa/plugins/bluez5/bluez5-device.c:1328
+#: spa/plugins/bluez5/bluez5-device.c:1330
 msgid "Microphone"
 msgstr "Mikrofon"
 
@@ -282,7 +281,7 @@ msgid "No Bass Boost"
 msgstr "Bez pojačanja basa"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:2672
-#: spa/plugins/bluez5/bluez5-device.c:1333
+#: spa/plugins/bluez5/bluez5-device.c:1335
 msgid "Speaker"
 msgstr "Zvučnik"
 
@@ -397,7 +396,7 @@ msgstr "Stereo"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:4484
 #: spa/plugins/alsa/acp/alsa-mixer.c:4642
-#: spa/plugins/bluez5/bluez5-device.c:1318
+#: spa/plugins/bluez5/bluez5-device.c:1320
 msgid "Headset"
 msgstr "Slušalice s mikrofonom"
 
@@ -521,8 +520,7 @@ msgstr "%s izlaz"
 msgid "%s Input"
 msgstr "%s ulaz"
 
-#: spa/plugins/alsa/acp/alsa-util.c:1173
-#: spa/plugins/alsa/acp/alsa-util.c:1267
+#: spa/plugins/alsa/acp/alsa-util.c:1173 spa/plugins/alsa/acp/alsa-util.c:1267
 #, c-format
 msgid ""
 "snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu "
@@ -619,7 +617,7 @@ msgstr[2] ""
 "Najvjerojatnije je ovo greška ALSA upravljačkog programa '%s'. Prijavite "
 "problem ALSA razvijateljima."
 
-#: spa/plugins/alsa/acp/channelmap.h:464
+#: spa/plugins/alsa/acp/channelmap.h:457
 msgid "(invalid)"
 msgstr "(neispravno)"
 
@@ -631,62 +629,62 @@ msgstr "Ugrađeni zvuk"
 msgid "Modem"
 msgstr "Modem"
 
-#: spa/plugins/bluez5/bluez5-device.c:1170
+#: spa/plugins/bluez5/bluez5-device.c:1172
 msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
 msgstr "Zvučni pristupnik (A2DP izvor i HSP/HFP AG)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1195
+#: spa/plugins/bluez5/bluez5-device.c:1197
 #, c-format
 msgid "High Fidelity Playback (A2DP Sink, codec %s)"
 msgstr "Reprodukcija visoke autentičnosti (A2DP slivnik, kôdek %s)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1198
+#: spa/plugins/bluez5/bluez5-device.c:1200
 #, c-format
 msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
 msgstr "Telefonija visoke autentičnosti (A2DP slivnik, kôdek %s)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1206
+#: spa/plugins/bluez5/bluez5-device.c:1208
 msgid "High Fidelity Playback (A2DP Sink)"
 msgstr "Reprodukcija visoke autentičnosti (A2DP slivnik)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1208
+#: spa/plugins/bluez5/bluez5-device.c:1210
 msgid "High Fidelity Duplex (A2DP Source/Sink)"
 msgstr "Telefonija visoke autentičnosti (A2DP izvor/slivnik)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1236
+#: spa/plugins/bluez5/bluez5-device.c:1238
 #, c-format
 msgid "Headset Head Unit (HSP/HFP, codec %s)"
 msgstr "Jedinica slušalice s mikrofonom (HSP/HFP, kôdek %s)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1241
+#: spa/plugins/bluez5/bluez5-device.c:1243
 msgid "Headset Head Unit (HSP/HFP)"
 msgstr "Jedinica slušalice s mikrofonom (HSP/HFP)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1323
+#: spa/plugins/bluez5/bluez5-device.c:1325
 msgid "Handsfree"
 msgstr "Bez-ruku"
 
-#: spa/plugins/bluez5/bluez5-device.c:1338
+#: spa/plugins/bluez5/bluez5-device.c:1340
 msgid "Headphone"
 msgstr "Slušalice"
 
-#: spa/plugins/bluez5/bluez5-device.c:1343
+#: spa/plugins/bluez5/bluez5-device.c:1345
 msgid "Portable"
 msgstr "Prijenosnik"
 
-#: spa/plugins/bluez5/bluez5-device.c:1348
+#: spa/plugins/bluez5/bluez5-device.c:1350
 msgid "Car"
 msgstr "Automobil"
 
-#: spa/plugins/bluez5/bluez5-device.c:1353
+#: spa/plugins/bluez5/bluez5-device.c:1355
 msgid "HiFi"
 msgstr "HiFi"
 
-#: spa/plugins/bluez5/bluez5-device.c:1358
+#: spa/plugins/bluez5/bluez5-device.c:1360
 msgid "Phone"
 msgstr "Telefon"
 
-#: spa/plugins/bluez5/bluez5-device.c:1364
+#: spa/plugins/bluez5/bluez5-device.c:1366
 msgid "Bluetooth"
 msgstr "Bluetooth"
 
diff --git a/po/ka.po b/po/ka.po
new file mode 100644
index 0000000000000000000000000000000000000000..1e78ffdfea21d246f9cd1d99e0391df811962d14
--- /dev/null
+++ b/po/ka.po
@@ -0,0 +1,661 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the pipewire package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: pipewire\n"
+"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/"
+"issues/new\n"
+"POT-Creation-Date: 2022-06-30 12:50+0200\n"
+"PO-Revision-Date: 2022-07-25 13:11+0200\n"
+"Last-Translator: Temuri Doghonadze <temuri.doghonadze@gmail.com>\n"
+"Language-Team: Georgian <(nothing)>\n"
+"Language: ka\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"X-Generator: Poedit 3.1.1\n"
+
+#: src/daemon/pipewire.c:46
+#, c-format
+msgid ""
+"%s [options]\n"
+"  -h, --help                            Show this help\n"
+"      --version                         Show version\n"
+"  -c, --config                          Load config (Default %s)\n"
+msgstr ""
+"%s [პარამეტრები]\n"
+"   -h, --help ამ დახმარების ჩვენება\n"
+"       --version ვერსიის ჩვენება\n"
+"   -c, --config ჩატვირთვის კონფიგურაცია (ნაგულისხმები %s)\n"
+
+#: src/modules/module-protocol-pulse/modules/module-tunnel-sink.c:180
+#: src/modules/module-protocol-pulse/modules/module-tunnel-source.c:180
+#, c-format
+msgid "Tunnel to %s/%s"
+msgstr "გვირაბი %s/%s -მდე"
+
+#: src/modules/module-fallback-sink.c:51
+msgid "Dummy Output"
+msgstr "ნულოვანი გამოყვანა"
+
+#: src/modules/module-pulse-tunnel.c:648
+#, c-format
+msgid "Tunnel for %s@%s"
+msgstr "გვირაბი %s@%s-სთვის"
+
+#: src/modules/module-zeroconf-discover.c:332
+msgid "Unknown device"
+msgstr "უცნობი მოწყობილობა"
+
+#: src/modules/module-zeroconf-discover.c:344
+#, c-format
+msgid "%s on %s@%s"
+msgstr "%s %s@%s -ზე"
+
+#: src/modules/module-zeroconf-discover.c:348
+#, c-format
+msgid "%s on %s"
+msgstr "%s %s-ზე"
+
+#: src/tools/pw-cat.c:784
+#, c-format
+msgid ""
+"%s [options] [<file>|-]\n"
+"  -h, --help                            Show this help\n"
+"      --version                         Show version\n"
+"  -v, --verbose                         Enable verbose operations\n"
+"\n"
+msgstr ""
+"%s [პარამეტრები] [<ფაილი>|-]\n"
+"   -h, --help ამ დახმარების ჩვენება\n"
+"       --version ვერსიის ჩვენება\n"
+"   -v, --verbose დამატებითი შეტყობინებების გამოტანა\n"
+"\n"
+
+#: src/tools/pw-cat.c:791
+#, c-format
+msgid ""
+"  -R, --remote                          Remote daemon name\n"
+"      --media-type                      Set media type (default %s)\n"
+"      --media-category                  Set media category (default %s)\n"
+"      --media-role                      Set media role (default %s)\n"
+"      --target                          Set node target (default %s)\n"
+"                                          0 means don't link\n"
+"      --latency                         Set node latency (default %s)\n"
+"                                          Xunit (unit = s, ms, us, ns)\n"
+"                                          or direct samples (256)\n"
+"                                          the rate is the one of the source "
+"file\n"
+"  -P  --properties                      Set node properties\n"
+"\n"
+msgstr ""
+"  -R, --remote დაშორებული დემონის სახელი\n"
+"       --media-type მედიის ტიპის დაყენება (ნაგულისხმები %s)\n"
+"       --media-category მედია კატეგორიის დაყენება (ნაგულისხმები %s)\n"
+"       --media-role მედიის როლის დაყენება (ნაგულისხმები %s)\n"
+"       --target კვანძის სამიზნის დაყენება (ნაგულისხმები %s)\n"
+"                                           0 ნიშნავს არ მიბმა\n"
+"       --latency კვანძის შეყოვნების დაყენება (ნაგულისხმები %s)\n"
+"                                           Xunit (ერთეული = s, ms, us, ns)\n"
+"                                           ან პირდაპირი ნიმუშები (256)\n"
+"                                           მაჩვენებელი არის ერთ-ერთი წყაროს "
+"ფაილი\n"
+"   -P --properties კვანძის თვისებების დაყენება\n"
+
+#: src/tools/pw-cat.c:809
+#, c-format
+msgid ""
+"      --rate                            Sample rate (req. for rec) (default "
+"%u)\n"
+"      --channels                        Number of channels (req. for rec) "
+"(default %u)\n"
+"      --channel-map                     Channel map\n"
+"                                            one of: \"stereo\", "
+"\"surround-51\",... or\n"
+"                                            comma separated list of channel "
+"names: eg. \"FL,FR\"\n"
+"      --format                          Sample format %s (req. for rec) "
+"(default %s)\n"
+"      --volume                          Stream volume 0-1.0 (default %.3f)\n"
+"  -q  --quality                         Resampler quality (0 - 15) (default "
+"%d)\n"
+"\n"
+msgstr ""
+"    --rate სემპლის_სიჩქარე (მოთხოვნილება rec.) (ნაგულისხმები %u)\n"
+"       --channels არხების რაოდენობა (მოთხოვნილი ჩანაწერისთვის) (ნაგულისხმები "
+"%u)\n"
+"       --channel-map არხის რუკა\n"
+"                                             ერთ-ერთი: \"stereo\", "
+"\"surround-51\",... ან\n"
+"                                             მძიმით გამოყოფილი არხის "
+"სახელების სია: მაგ. \"FL, FR\"\n"
+"       --format                     ნიმუშის ფორმატი %s (მოთხოვნილება rec.) "
+"(ნაგულისხმები %s)\n"
+"       --volume               ნაკადის მოცულობა 0-1.0 (ნაგულისხმები %.3f)\n"
+"   -q --quality                         Resampler ხარისხი (0 - 15) "
+"(ნაგულისხმები %d)\n"
+
+#: src/tools/pw-cat.c:826
+msgid ""
+"  -p, --playback                        Playback mode\n"
+"  -r, --record                          Recording mode\n"
+"  -m, --midi                            Midi mode\n"
+"  -d, --dsd                             DSD mode\n"
+"\n"
+msgstr ""
+"   -p, --playback                        დაკვრის რეჟიმი\n"
+"   -r, -- record                          ჩაწერის რეჟიმი\n"
+"   -m, --midi                            Midi რეჟიმი\n"
+"   -d, --dsd                             DSD რეჟიმი\n"
+"\n"
+
+#: src/tools/pw-cli.c:3165
+#, c-format
+msgid ""
+"%s [options] [command]\n"
+"  -h, --help                            Show this help\n"
+"      --version                         Show version\n"
+"  -d, --daemon                          Start as daemon (Default false)\n"
+"  -r, --remote                          Remote daemon name\n"
+"\n"
+msgstr ""
+"%s [პარამეტრები] [ბრძანება]\n"
+"   -h, --help                            ამ დახმარების ჩვენება\n"
+"       --version                         ვერსიის ჩვენება\n"
+"   -d, --daemon                          დაწყება როგორც დემონი (ნაგულისხმები "
+"false)\n"
+"   -r, --remote                          დაშორებული დემონის სახელი\n"
+
+#: spa/plugins/alsa/acp/acp.c:321
+msgid "Pro Audio"
+msgstr "Pro Audio"
+
+#: spa/plugins/alsa/acp/acp.c:446 spa/plugins/alsa/acp/alsa-mixer.c:4648
+#: spa/plugins/bluez5/bluez5-device.c:1161
+msgid "Off"
+msgstr "გამორთული"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2652
+msgid "Input"
+msgstr "შეყვანა"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2653
+msgid "Docking Station Input"
+msgstr "Docking Station-ის შეყვანა"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2654
+msgid "Docking Station Microphone"
+msgstr "Docking Station-ის მიკროფონი"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2655
+msgid "Docking Station Line In"
+msgstr "Docking Station Line In"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2656
+#: spa/plugins/alsa/acp/alsa-mixer.c:2747
+msgid "Line In"
+msgstr "Line In"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2657
+#: spa/plugins/alsa/acp/alsa-mixer.c:2741
+#: spa/plugins/bluez5/bluez5-device.c:1330
+msgid "Microphone"
+msgstr "მიკროფონი"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2658
+#: spa/plugins/alsa/acp/alsa-mixer.c:2742
+msgid "Front Microphone"
+msgstr "წინა მიკროფონი"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2659
+#: spa/plugins/alsa/acp/alsa-mixer.c:2743
+msgid "Rear Microphone"
+msgstr "უკანა მიკფოფონი"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2660
+msgid "External Microphone"
+msgstr "გარე მიკროფონი"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2661
+#: spa/plugins/alsa/acp/alsa-mixer.c:2745
+msgid "Internal Microphone"
+msgstr "შიდა მიკროფონი"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2662
+#: spa/plugins/alsa/acp/alsa-mixer.c:2748
+msgid "Radio"
+msgstr "რადიო"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2663
+#: spa/plugins/alsa/acp/alsa-mixer.c:2749
+msgid "Video"
+msgstr "ვიდეო"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2664
+msgid "Automatic Gain Control"
+msgstr "ხმის მომატების ავტომატური კონტროლი"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2665
+msgid "No Automatic Gain Control"
+msgstr "ხმის მომატების ავტომატური კონტროლის გამორთვა"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2666
+msgid "Boost"
+msgstr "გაძლიერება"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2667
+msgid "No Boost"
+msgstr "გაძლიერების გარეშე"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2668
+msgid "Amplifier"
+msgstr "გამაძლიერებელი"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2669
+msgid "No Amplifier"
+msgstr "გამაძლიერებლის გარეშე"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2670
+msgid "Bass Boost"
+msgstr "Bass-ის გაძლიერება"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2671
+msgid "No Bass Boost"
+msgstr "Bass-ის გაძლიერების გარეშე"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2672
+#: spa/plugins/bluez5/bluez5-device.c:1335
+msgid "Speaker"
+msgstr "დინამიკი"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2673
+#: spa/plugins/alsa/acp/alsa-mixer.c:2751
+msgid "Headphones"
+msgstr "ყურსაცვამები"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2740
+msgid "Analog Input"
+msgstr "ანალოგური შეყვანა"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2744
+msgid "Dock Microphone"
+msgstr "მისამაგრებელი მიკროფონი"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2746
+msgid "Headset Microphone"
+msgstr "ყურსაცვამის მიროფონი"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2750
+msgid "Analog Output"
+msgstr "ანალოგური გამოტანა"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2752
+msgid "Headphones 2"
+msgstr "ყურსაცვამები 2"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2753
+msgid "Headphones Mono Output"
+msgstr "ყურსაცვამები მონო"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2754
+msgid "Line Out"
+msgstr "ხაზოვანი გამოყვანა"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2755
+msgid "Analog Mono Output"
+msgstr "ანალოგური მონო გამოყვანა"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2756
+msgid "Speakers"
+msgstr "დინამიკები"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2757
+msgid "HDMI / DisplayPort"
+msgstr "HDMI / DisplayPort"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2758
+msgid "Digital Output (S/PDIF)"
+msgstr "ციფრული გამოყვანა (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2759
+msgid "Digital Input (S/PDIF)"
+msgstr "ციფრული შეტანა (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2760
+msgid "Multichannel Input"
+msgstr "მრავალარხიანი შეყვანა"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2761
+msgid "Multichannel Output"
+msgstr "მრავალარხიანი გამოყვანა"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2762
+msgid "Game Output"
+msgstr "თამაშის გამოყვანა"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2763
+#: spa/plugins/alsa/acp/alsa-mixer.c:2764
+msgid "Chat Output"
+msgstr "ჩატის გამოყვანა"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2765
+msgid "Chat Input"
+msgstr "ჩატის შეყვანა"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2766
+msgid "Virtual Surround 7.1"
+msgstr "ვირტუალური სივრცითი ხმა 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4471
+msgid "Analog Mono"
+msgstr "ანალოგური მონო"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4472
+msgid "Analog Mono (Left)"
+msgstr "ანალოგური მონო (მარცხენა)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4473
+msgid "Analog Mono (Right)"
+msgstr "ანალოგური მონო (მარჯვენა)"
+
+#. Note: Not translated to "Analog Stereo Input", because the source
+#. * name gets "Input" appended to it automatically, so adding "Input"
+#. * here would lead to the source name to become "Analog Stereo Input
+#. * Input". The same logic applies to analog-stereo-output,
+#. * multichannel-input and multichannel-output.
+#: spa/plugins/alsa/acp/alsa-mixer.c:4474
+#: spa/plugins/alsa/acp/alsa-mixer.c:4482
+#: spa/plugins/alsa/acp/alsa-mixer.c:4483
+msgid "Analog Stereo"
+msgstr "ანალოგური სტერეო"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4475
+msgid "Mono"
+msgstr "მონო"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4476
+msgid "Stereo"
+msgstr "სტერეო"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4484
+#: spa/plugins/alsa/acp/alsa-mixer.c:4642
+#: spa/plugins/bluez5/bluez5-device.c:1320
+msgid "Headset"
+msgstr "ყურსაცვამები & მიკროფონი"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4485
+#: spa/plugins/alsa/acp/alsa-mixer.c:4643
+msgid "Speakerphone"
+msgstr "სამაგიდო დინამიკი"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4486
+#: spa/plugins/alsa/acp/alsa-mixer.c:4487
+msgid "Multichannel"
+msgstr "მრავალარხიანი"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4488
+msgid "Analog Surround 2.1"
+msgstr "ანალოგური სივრცითი 2.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4489
+msgid "Analog Surround 3.0"
+msgstr "ანალოგური სივრცითი 3.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4490
+msgid "Analog Surround 3.1"
+msgstr "ანალოგური სივრცითი 3.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4491
+msgid "Analog Surround 4.0"
+msgstr "ანალოგური სივრცითი 4.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4492
+msgid "Analog Surround 4.1"
+msgstr "ანალოგური სივრცითი 4.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4493
+msgid "Analog Surround 5.0"
+msgstr "ანალოგური სივრცითი 5.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4494
+msgid "Analog Surround 5.1"
+msgstr "ანალოგური სივრცითი 5.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4495
+msgid "Analog Surround 6.0"
+msgstr "ანალოგური სივრცითი 6.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4496
+msgid "Analog Surround 6.1"
+msgstr "ანალოგური სივრცითი 6.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4497
+msgid "Analog Surround 7.0"
+msgstr "ანალოგური სივრცითი 7.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4498
+msgid "Analog Surround 7.1"
+msgstr "ანალოგური სივრცითი 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4499
+msgid "Digital Stereo (IEC958)"
+msgstr "ციფრული სტერეო (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4500
+msgid "Digital Surround 4.0 (IEC958/AC3)"
+msgstr "ციფრული სივრცითი 4.0 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4501
+msgid "Digital Surround 5.1 (IEC958/AC3)"
+msgstr "ციფრული სივრცითი 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4502
+msgid "Digital Surround 5.1 (IEC958/DTS)"
+msgstr "ციფრული სივრცითი 5.1 (IEC958/DTS)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4503
+msgid "Digital Stereo (HDMI)"
+msgstr "ციფრული სტერეო (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4504
+msgid "Digital Surround 5.1 (HDMI)"
+msgstr "ციფრული სივრცითი 5.1 (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4505
+msgid "Chat"
+msgstr "ჩატი"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4506
+msgid "Game"
+msgstr "თამაში"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4640
+msgid "Analog Mono Duplex"
+msgstr "ანალოგური მონო დუპლექსი"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4641
+msgid "Analog Stereo Duplex"
+msgstr "ანალოგური სტერეო დუპლექსი"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4644
+msgid "Digital Stereo Duplex (IEC958)"
+msgstr "ციფრული სტერეო დუპლექსი (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4645
+msgid "Multichannel Duplex"
+msgstr "მრავალარხიანი დუპლექსი"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4646
+msgid "Stereo Duplex"
+msgstr "სტერეო დუპლექსი"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4647
+msgid "Mono Chat + 7.1 Surround"
+msgstr "მონო ჩატი + 7.1 სივრცითი"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4754
+#, c-format
+msgid "%s Output"
+msgstr "%s გამოყვანა"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4761
+#, c-format
+msgid "%s Input"
+msgstr "%s შეყვანა"
+
+#: spa/plugins/alsa/acp/alsa-util.c:1173 spa/plugins/alsa/acp/alsa-util.c:1267
+#, c-format
+msgid ""
+"snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu "
+"ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_avail() returned a value that is exceptionally large: %lu bytes (%lu "
+"ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+"snd_pcm_avail()-ის მიერ დაბრუნებული მნიშვნელობა არაჩვეულებრივად დიდია: %lu "
+"ბაიტი (%lu მწმ).\n"
+"ყველაზე ხშირად ეს ALSA-ს დრაივერის (%s) შეცდომის გამო ხდება. დაუკავშირდით "
+"ALSA-ის პროგრამისტებს."
+msgstr[1] ""
+"snd_pcm_avail()-ის მიერ დაბრუნებული მნიშვნელობა არაჩვეულებრივად დიდია: %lu "
+"ბაიტი (%lu მწმ).\n"
+"ყველაზე ხშირად ეს ALSA-ს დრაივერის (%s) შეცდომის გამო ხდება. დაუკავშირდით "
+"ALSA-ის პროგრამისტებს."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1239
+#, c-format
+msgid ""
+"snd_pcm_delay() returned a value that is exceptionally large: %li byte (%s"
+"%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_delay() returned a value that is exceptionally large: %li bytes (%s"
+"%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+"snd_pcm_delay()-ის მიერ დაბრუნებული მნიშვნელობა არაჩვეულებრივად დიდია: %li "
+"ბაიტი (%s%lu მწმ).\n"
+"ყველაზე ხშირად ეს ALSA-ს დრაივერის (%s) შეცდომის გამო ხდება. დაუკავშირდით "
+"ALSA-ის პროგრამისტებს."
+msgstr[1] ""
+"snd_pcm_delay()-ის მიერ დაბრუნებული მნიშვნელობა არაჩვეულებრივად დიდია: %li "
+"ბაიტი (%s%lu მწმ).\n"
+"ყველაზე ხშირად ეს ALSA-ს დრაივერის (%s) შეცდომის გამო ხდება. დაუკავშირდით "
+"ALSA-ის პროგრამისტებს."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1286
+#, c-format
+msgid ""
+"snd_pcm_avail_delay() returned strange values: delay %lu is less than avail "
+"%lu.\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr ""
+"snd_pcm_avail_delay()-ის მიერ დაბრუნებული მნიშვნელობები უცნაურია: დაყოვნება "
+"%lu უფრო მცირეა, ვიდრე ხელმისაწვდომი დრო %lu.\n"
+"ყველაზე ხშირად ეს ALSA-ს დრაივერის (%s) შეცდომის გამო ხდება. დაუკავშირდით "
+"ALSA-ის პროგრამისტებს."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1329
+#, c-format
+msgid ""
+"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu byte "
+"(%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu bytes "
+"(%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+"snd_pcm_mmap_begin()-ის მიერ დაბრუნებული მნიშვნელობა არაჩვეულებრივად დიდია: "
+"%lu ბაიტი (%lu მწმ).\n"
+"ყველაზე ხშირად ეს ALSA-ს დრაივერის (%s) შეცდომის გამო ხდება. დაუკავშირდით "
+"ALSA-ის პროგრამისტებს."
+msgstr[1] ""
+"snd_pcm_mmap_begin()-ის მიერ დაბრუნებული მნიშვნელობა არაჩვეულებრივად დიდია: "
+"%lu ბაიტი (%lu მწმ).\n"
+"ყველაზე ხშირად ეს ALSA-ს დრაივერის (%s) შეცდომის გამო ხდება. დაუკავშირდით "
+"ALSA-ის პროგრამისტებს."
+
+#: spa/plugins/alsa/acp/channelmap.h:457
+msgid "(invalid)"
+msgstr "(არასწორი)"
+
+#: spa/plugins/alsa/acp/compat.c:189
+msgid "Built-in Audio"
+msgstr "ჩაშენებული აუდიო"
+
+#: spa/plugins/alsa/acp/compat.c:194
+msgid "Modem"
+msgstr "მოდემი"
+
+#: spa/plugins/bluez5/bluez5-device.c:1172
+msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
+msgstr "Audio Gateway (A2DP წყარო & HSP/HFP AG)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1197
+#, c-format
+msgid "High Fidelity Playback (A2DP Sink, codec %s)"
+msgstr "მაღალი ხარისხის ხმა (A2DP Sink, კოდეკი %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1200
+#, c-format
+msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
+msgstr "მაღალი ხარისხის დუპლექსი (A2DP წყარო/Sink, კოდეკი %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1208
+msgid "High Fidelity Playback (A2DP Sink)"
+msgstr "მაღალი ხარისხის ხმა (A2DP Sink)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1210
+msgid "High Fidelity Duplex (A2DP Source/Sink)"
+msgstr "მაღალი ხარისხის დუპლექსი(A2DP წყარო/Sink)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1238
+#, c-format
+msgid "Headset Head Unit (HSP/HFP, codec %s)"
+msgstr "Headset Head Unit (HSP/HFP, კოდეკი %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1243
+msgid "Headset Head Unit (HSP/HFP)"
+msgstr "Headset Head Unit (HSP/HFP)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1325
+msgid "Handsfree"
+msgstr "ხელის გარეშე სამართავი"
+
+#: spa/plugins/bluez5/bluez5-device.c:1340
+msgid "Headphone"
+msgstr "ყურსაცვამი"
+
+#: spa/plugins/bluez5/bluez5-device.c:1345
+msgid "Portable"
+msgstr "გადატანადი"
+
+#: spa/plugins/bluez5/bluez5-device.c:1350
+msgid "Car"
+msgstr "მანქანა"
+
+#: spa/plugins/bluez5/bluez5-device.c:1355
+msgid "HiFi"
+msgstr "HiFi"
+
+#: spa/plugins/bluez5/bluez5-device.c:1360
+msgid "Phone"
+msgstr "ტელეფონი"
+
+#: spa/plugins/bluez5/bluez5-device.c:1366
+msgid "Bluetooth"
+msgstr "Bluetooth"
diff --git a/po/pipewire.pot b/po/pipewire.pot
index 82b498e7f6e68d2e32398bd3ae230082893937d8..26d49f3d46b9729260ed2b821e0c36f70f0d5037 100644
--- a/po/pipewire.pot
+++ b/po/pipewire.pot
@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: pipewire\n"
 "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/issues/new\n"
-"POT-Creation-Date: 2022-04-03 12:56+0200\n"
+"POT-Creation-Date: 2022-06-30 12:50+0200\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -27,8 +27,8 @@ msgid ""
 "  -c, --config                          Load config (Default %s)\n"
 msgstr ""
 
-#: src/modules/module-protocol-pulse/modules/module-tunnel-sink.c:190
-#: src/modules/module-protocol-pulse/modules/module-tunnel-source.c:190
+#: src/modules/module-protocol-pulse/modules/module-tunnel-sink.c:180
+#: src/modules/module-protocol-pulse/modules/module-tunnel-source.c:180
 #, c-format
 msgid "Tunnel to %s/%s"
 msgstr ""
@@ -37,36 +37,36 @@ msgstr ""
 msgid "Dummy Output"
 msgstr ""
 
-#: src/modules/module-pulse-tunnel.c:545
+#: src/modules/module-pulse-tunnel.c:648
 #, c-format
 msgid "Tunnel for %s@%s"
 msgstr ""
 
-#: src/modules/module-zeroconf-discover.c:313
+#: src/modules/module-zeroconf-discover.c:332
 msgid "Unknown device"
 msgstr ""
 
-#: src/modules/module-zeroconf-discover.c:325
+#: src/modules/module-zeroconf-discover.c:344
 #, c-format
 msgid "%s on %s@%s"
 msgstr ""
 
-#: src/modules/module-zeroconf-discover.c:329
+#: src/modules/module-zeroconf-discover.c:348
 #, c-format
 msgid "%s on %s"
 msgstr ""
 
-#: src/tools/pw-cat.c:1087
+#: src/tools/pw-cat.c:784
 #, c-format
 msgid ""
-"%s [options] <file>\n"
+"%s [options] [<file>|-]\n"
 "  -h, --help                            Show this help\n"
 "      --version                         Show version\n"
 "  -v, --verbose                         Enable verbose operations\n"
 "\n"
 msgstr ""
 
-#: src/tools/pw-cat.c:1094
+#: src/tools/pw-cat.c:791
 #, c-format
 msgid ""
 "  -R, --remote                          Remote daemon name\n"
@@ -80,11 +80,11 @@ msgid ""
 "                                          or direct samples (256)\n"
 "                                          the rate is the one of the source "
 "file\n"
-"      --list-targets                    List available targets for --target\n"
+"  -P  --properties                      Set node properties\n"
 "\n"
 msgstr ""
 
-#: src/tools/pw-cat.c:1112
+#: src/tools/pw-cat.c:809
 #, c-format
 msgid ""
 "      --rate                            Sample rate (req. for rec) (default "
@@ -104,7 +104,7 @@ msgid ""
 "\n"
 msgstr ""
 
-#: src/tools/pw-cat.c:1129
+#: src/tools/pw-cat.c:826
 msgid ""
 "  -p, --playback                        Playback mode\n"
 "  -r, --record                          Recording mode\n"
@@ -113,7 +113,7 @@ msgid ""
 "\n"
 msgstr ""
 
-#: src/tools/pw-cli.c:3051
+#: src/tools/pw-cli.c:3165
 #, c-format
 msgid ""
 "%s [options] [command]\n"
@@ -128,8 +128,8 @@ msgstr ""
 msgid "Pro Audio"
 msgstr ""
 
-#: spa/plugins/alsa/acp/acp.c:444 spa/plugins/alsa/acp/alsa-mixer.c:4648
-#: spa/plugins/bluez5/bluez5-device.c:1159
+#: spa/plugins/alsa/acp/acp.c:446 spa/plugins/alsa/acp/alsa-mixer.c:4648
+#: spa/plugins/bluez5/bluez5-device.c:1161
 msgid "Off"
 msgstr ""
 
@@ -156,7 +156,7 @@ msgstr ""
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:2657
 #: spa/plugins/alsa/acp/alsa-mixer.c:2741
-#: spa/plugins/bluez5/bluez5-device.c:1328
+#: spa/plugins/bluez5/bluez5-device.c:1330
 msgid "Microphone"
 msgstr ""
 
@@ -222,7 +222,7 @@ msgid "No Bass Boost"
 msgstr ""
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:2672
-#: spa/plugins/bluez5/bluez5-device.c:1333
+#: spa/plugins/bluez5/bluez5-device.c:1335
 msgid "Speaker"
 msgstr ""
 
@@ -337,7 +337,7 @@ msgstr ""
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:4484
 #: spa/plugins/alsa/acp/alsa-mixer.c:4642
-#: spa/plugins/bluez5/bluez5-device.c:1318
+#: spa/plugins/bluez5/bluez5-device.c:1320
 msgid "Headset"
 msgstr ""
 
@@ -516,7 +516,7 @@ msgid_plural ""
 msgstr[0] ""
 msgstr[1] ""
 
-#: spa/plugins/alsa/acp/channelmap.h:464
+#: spa/plugins/alsa/acp/channelmap.h:457
 msgid "(invalid)"
 msgstr ""
 
@@ -528,61 +528,61 @@ msgstr ""
 msgid "Modem"
 msgstr ""
 
-#: spa/plugins/bluez5/bluez5-device.c:1170
+#: spa/plugins/bluez5/bluez5-device.c:1172
 msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
 msgstr ""
 
-#: spa/plugins/bluez5/bluez5-device.c:1195
+#: spa/plugins/bluez5/bluez5-device.c:1197
 #, c-format
 msgid "High Fidelity Playback (A2DP Sink, codec %s)"
 msgstr ""
 
-#: spa/plugins/bluez5/bluez5-device.c:1198
+#: spa/plugins/bluez5/bluez5-device.c:1200
 #, c-format
 msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
 msgstr ""
 
-#: spa/plugins/bluez5/bluez5-device.c:1206
+#: spa/plugins/bluez5/bluez5-device.c:1208
 msgid "High Fidelity Playback (A2DP Sink)"
 msgstr ""
 
-#: spa/plugins/bluez5/bluez5-device.c:1208
+#: spa/plugins/bluez5/bluez5-device.c:1210
 msgid "High Fidelity Duplex (A2DP Source/Sink)"
 msgstr ""
 
-#: spa/plugins/bluez5/bluez5-device.c:1236
+#: spa/plugins/bluez5/bluez5-device.c:1238
 #, c-format
 msgid "Headset Head Unit (HSP/HFP, codec %s)"
 msgstr ""
 
-#: spa/plugins/bluez5/bluez5-device.c:1241
+#: spa/plugins/bluez5/bluez5-device.c:1243
 msgid "Headset Head Unit (HSP/HFP)"
 msgstr ""
 
-#: spa/plugins/bluez5/bluez5-device.c:1323
+#: spa/plugins/bluez5/bluez5-device.c:1325
 msgid "Handsfree"
 msgstr ""
 
-#: spa/plugins/bluez5/bluez5-device.c:1338
+#: spa/plugins/bluez5/bluez5-device.c:1340
 msgid "Headphone"
 msgstr ""
 
-#: spa/plugins/bluez5/bluez5-device.c:1343
+#: spa/plugins/bluez5/bluez5-device.c:1345
 msgid "Portable"
 msgstr ""
 
-#: spa/plugins/bluez5/bluez5-device.c:1348
+#: spa/plugins/bluez5/bluez5-device.c:1350
 msgid "Car"
 msgstr ""
 
-#: spa/plugins/bluez5/bluez5-device.c:1353
+#: spa/plugins/bluez5/bluez5-device.c:1355
 msgid "HiFi"
 msgstr ""
 
-#: spa/plugins/bluez5/bluez5-device.c:1358
+#: spa/plugins/bluez5/bluez5-device.c:1360
 msgid "Phone"
 msgstr ""
 
-#: spa/plugins/bluez5/bluez5-device.c:1364
+#: spa/plugins/bluez5/bluez5-device.c:1366
 msgid "Bluetooth"
 msgstr ""
diff --git a/po/pl.po b/po/pl.po
index 72bda0316f6958b717a62988a0bb24a3ffaec096..b84b59b9c0538a3ac7d5699944418ebadcb68a19 100644
--- a/po/pl.po
+++ b/po/pl.po
@@ -8,8 +8,8 @@ msgstr ""
 "Project-Id-Version: pipewire\n"
 "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/-/"
 "issues\n"
-"POT-Creation-Date: 2022-05-20 15:26+0000\n"
-"PO-Revision-Date: 2022-05-21 12:49+0200\n"
+"POT-Creation-Date: 2022-08-27 13:57+0000\n"
+"PO-Revision-Date: 2022-08-27 16:00+0200\n"
 "Last-Translator: Piotr DrÄ…g <piotrdrag@gmail.com>\n"
 "Language-Team: Polish <community-poland@mozilla.org>\n"
 "Language: pl\n"
@@ -41,8 +41,8 @@ msgstr "System multimediów PipeWire"
 msgid "Start the PipeWire Media System"
 msgstr "Uruchomienie systemu multimediów PipeWire"
 
-#: src/modules/module-protocol-pulse/modules/module-tunnel-sink.c:183
-#: src/modules/module-protocol-pulse/modules/module-tunnel-source.c:183
+#: src/modules/module-protocol-pulse/modules/module-tunnel-sink.c:180
+#: src/modules/module-protocol-pulse/modules/module-tunnel-source.c:180
 #, c-format
 msgid "Tunnel to %s/%s"
 msgstr "Tunel do %s/%s"
@@ -51,7 +51,7 @@ msgstr "Tunel do %s/%s"
 msgid "Dummy Output"
 msgstr "Głuche wyjście"
 
-#: src/modules/module-pulse-tunnel.c:639
+#: src/modules/module-pulse-tunnel.c:648
 #, c-format
 msgid "Tunnel for %s@%s"
 msgstr "Tunel dla %s@%s"
@@ -70,7 +70,7 @@ msgstr "%s na %s@%s"
 msgid "%s on %s"
 msgstr "%s na %s"
 
-#: src/tools/pw-cat.c:872
+#: src/tools/pw-cat.c:784
 #, c-format
 msgid ""
 "%s [options] [<file>|-]\n"
@@ -85,7 +85,7 @@ msgstr ""
 "  -v, --verbose                         Wyświetla więcej komunikatów\n"
 "\n"
 
-#: src/tools/pw-cat.c:879
+#: src/tools/pw-cat.c:791
 #, c-format
 msgid ""
 "  -R, --remote                          Remote daemon name\n"
@@ -122,7 +122,7 @@ msgstr ""
 "  -P  --properties                      Ustawia właściwości węzła\n"
 "\n"
 
-#: src/tools/pw-cat.c:897
+#: src/tools/pw-cat.c:809
 #, c-format
 msgid ""
 "      --rate                            Sample rate (req. for rec) (default "
@@ -158,7 +158,7 @@ msgstr ""
 "(domyślnie %d)\n"
 "\n"
 
-#: src/tools/pw-cat.c:914
+#: src/tools/pw-cat.c:826
 msgid ""
 "  -p, --playback                        Playback mode\n"
 "  -r, --record                          Recording mode\n"
@@ -172,7 +172,7 @@ msgstr ""
 "  -d, --dsd                             Tryb DSD\n"
 "\n"
 
-#: src/tools/pw-cli.c:3139
+#: src/tools/pw-cli.c:2255
 #, c-format
 msgid ""
 "%s [options] [command]\n"
@@ -195,7 +195,7 @@ msgid "Pro Audio"
 msgstr "Dźwięk w zastosowaniach profesjonalnych"
 
 #: spa/plugins/alsa/acp/acp.c:444 spa/plugins/alsa/acp/alsa-mixer.c:4648
-#: spa/plugins/bluez5/bluez5-device.c:1161
+#: spa/plugins/bluez5/bluez5-device.c:1188
 msgid "Off"
 msgstr "Wyłączone"
 
@@ -222,7 +222,7 @@ msgstr "Wejście liniowe"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:2657
 #: spa/plugins/alsa/acp/alsa-mixer.c:2741
-#: spa/plugins/bluez5/bluez5-device.c:1330
+#: spa/plugins/bluez5/bluez5-device.c:1360
 msgid "Microphone"
 msgstr "Mikrofon"
 
@@ -288,7 +288,7 @@ msgid "No Bass Boost"
 msgstr "Brak podbicia basów"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:2672
-#: spa/plugins/bluez5/bluez5-device.c:1335
+#: spa/plugins/bluez5/bluez5-device.c:1366
 msgid "Speaker"
 msgstr "Głośnik"
 
@@ -403,7 +403,7 @@ msgstr "Stereo"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:4484
 #: spa/plugins/alsa/acp/alsa-mixer.c:4642
-#: spa/plugins/bluez5/bluez5-device.c:1320
+#: spa/plugins/bluez5/bluez5-device.c:1348
 msgid "Headset"
 msgstr "Słuchawki z mikrofonem"
 
@@ -527,7 +527,7 @@ msgstr "Wyjście %s"
 msgid "%s Input"
 msgstr "Wejście %s"
 
-#: spa/plugins/alsa/acp/alsa-util.c:1173 spa/plugins/alsa/acp/alsa-util.c:1267
+#: spa/plugins/alsa/acp/alsa-util.c:1187 spa/plugins/alsa/acp/alsa-util.c:1281
 #, c-format
 msgid ""
 "snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu "
@@ -552,7 +552,7 @@ msgstr[2] ""
 "Prawdopodobnie jest to błąd sterownika ALSA „%s”. Proszę zgłosić ten problem "
 "programistom usługi ALSA."
 
-#: spa/plugins/alsa/acp/alsa-util.c:1239
+#: spa/plugins/alsa/acp/alsa-util.c:1253
 #, c-format
 msgid ""
 "snd_pcm_delay() returned a value that is exceptionally large: %li byte (%s"
@@ -577,7 +577,7 @@ msgstr[2] ""
 "Prawdopodobnie jest to błąd sterownika ALSA „%s”. Proszę zgłosić ten problem "
 "programistom usługi ALSA."
 
-#: spa/plugins/alsa/acp/alsa-util.c:1286
+#: spa/plugins/alsa/acp/alsa-util.c:1300
 #, c-format
 msgid ""
 "snd_pcm_avail_delay() returned strange values: delay %lu is less than avail "
@@ -590,7 +590,7 @@ msgstr ""
 "Prawdopodobnie jest to błąd sterownika ALSA „%s”. Proszę zgłosić ten problem "
 "programistom usługi ALSA."
 
-#: spa/plugins/alsa/acp/alsa-util.c:1329
+#: spa/plugins/alsa/acp/alsa-util.c:1343
 #, c-format
 msgid ""
 "snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu byte "
@@ -615,7 +615,7 @@ msgstr[2] ""
 "Prawdopodobnie jest to błąd sterownika ALSA „%s”. Proszę zgłosić ten problem "
 "programistom usługi ALSA."
 
-#: spa/plugins/alsa/acp/channelmap.h:464
+#: spa/plugins/alsa/acp/channelmap.h:457
 msgid "(invalid)"
 msgstr "(nieprawidłowe)"
 
@@ -627,61 +627,77 @@ msgstr "Wbudowany dźwięk"
 msgid "Modem"
 msgstr "Modem"
 
-#: spa/plugins/bluez5/bluez5-device.c:1172
+#: spa/plugins/bluez5/bluez5-device.c:1199
 msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
 msgstr "Bramka dźwięku (źródło A2DP i AG HSP/HFP)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1197
+#: spa/plugins/bluez5/bluez5-device.c:1224
 #, c-format
 msgid "High Fidelity Playback (A2DP Sink, codec %s)"
 msgstr "Odtwarzanie o wysokiej dokładności (odpływ A2DP, kodek %s)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1200
+#: spa/plugins/bluez5/bluez5-device.c:1227
 #, c-format
 msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
 msgstr "Dupleks o wysokiej dokładności (źródło/odpływ A2DP, kodek %s)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1208
+#: spa/plugins/bluez5/bluez5-device.c:1235
 msgid "High Fidelity Playback (A2DP Sink)"
 msgstr "Odtwarzanie o wysokiej dokładności (odpływ A2DP)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1210
+#: spa/plugins/bluez5/bluez5-device.c:1237
 msgid "High Fidelity Duplex (A2DP Source/Sink)"
 msgstr "Dupleks o wysokiej dokładności (źródło/odpływ A2DP)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1238
+#: spa/plugins/bluez5/bluez5-device.c:1265
 #, c-format
 msgid "Headset Head Unit (HSP/HFP, codec %s)"
 msgstr "Jednostka główna słuchawek z mikrofonem (HSP/HFP, kodek %s)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1243
+#: spa/plugins/bluez5/bluez5-device.c:1270
 msgid "Headset Head Unit (HSP/HFP)"
 msgstr "Jednostka główna słuchawek z mikrofonem (HSP/HFP)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1325
+#: spa/plugins/bluez5/bluez5-device.c:1349
+#: spa/plugins/bluez5/bluez5-device.c:1354
+#: spa/plugins/bluez5/bluez5-device.c:1361
+#: spa/plugins/bluez5/bluez5-device.c:1367
+#: spa/plugins/bluez5/bluez5-device.c:1373
+#: spa/plugins/bluez5/bluez5-device.c:1379
+#: spa/plugins/bluez5/bluez5-device.c:1385
+#: spa/plugins/bluez5/bluez5-device.c:1391
+#: spa/plugins/bluez5/bluez5-device.c:1397
 msgid "Handsfree"
 msgstr "Zestaw głośnomówiący"
 
-#: spa/plugins/bluez5/bluez5-device.c:1340
+#: spa/plugins/bluez5/bluez5-device.c:1355
+msgid "Handsfree (HFP)"
+msgstr "Zestaw głośnomówiący (HFP)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1372
 msgid "Headphone"
 msgstr "Słuchawki"
 
-#: spa/plugins/bluez5/bluez5-device.c:1345
+#: spa/plugins/bluez5/bluez5-device.c:1378
 msgid "Portable"
 msgstr "Przenośne"
 
-#: spa/plugins/bluez5/bluez5-device.c:1350
+#: spa/plugins/bluez5/bluez5-device.c:1384
 msgid "Car"
 msgstr "Samochód"
 
-#: spa/plugins/bluez5/bluez5-device.c:1355
+#: spa/plugins/bluez5/bluez5-device.c:1390
 msgid "HiFi"
 msgstr "HiFi"
 
-#: spa/plugins/bluez5/bluez5-device.c:1360
+#: spa/plugins/bluez5/bluez5-device.c:1396
 msgid "Phone"
 msgstr "Telefon"
 
-#: spa/plugins/bluez5/bluez5-device.c:1366
+#: spa/plugins/bluez5/bluez5-device.c:1403
 msgid "Bluetooth"
 msgstr "Bluetooth"
+
+#: spa/plugins/bluez5/bluez5-device.c:1404
+msgid "Bluetooth (HFP)"
+msgstr "Bluetooth (HFP)"
diff --git a/po/sv.po b/po/sv.po
index a5dd5fb60b51c0e0779a8448a5a227cf990bed5f..f9a57802245ab89f1446ec91f7aa8c2e28e66010 100644
--- a/po/sv.po
+++ b/po/sv.po
@@ -19,8 +19,8 @@ msgstr ""
 "Project-Id-Version: pipewire\n"
 "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/-/"
 "issues\n"
-"POT-Creation-Date: 2022-05-20 15:26+0000\n"
-"PO-Revision-Date: 2022-05-23 11:01+0200\n"
+"POT-Creation-Date: 2022-07-19 15:27+0000\n"
+"PO-Revision-Date: 2022-07-10 10:22+0200\n"
 "Last-Translator: Anders Jonsson <anders.jonsson@norsjovallen.se>\n"
 "Language-Team: Swedish <tp-sv@listor.tp-sv.se>\n"
 "Language: sv\n"
@@ -28,7 +28,7 @@ msgstr ""
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
 "Plural-Forms: nplurals=2; plural=n != 1;\n"
-"X-Generator: Poedit 3.0.1\n"
+"X-Generator: Poedit 3.1\n"
 
 #: src/daemon/pipewire.c:46
 #, c-format
@@ -51,8 +51,8 @@ msgstr "PipeWire mediasystem"
 msgid "Start the PipeWire Media System"
 msgstr "Starta mediasystemet PipeWire"
 
-#: src/modules/module-protocol-pulse/modules/module-tunnel-sink.c:183
-#: src/modules/module-protocol-pulse/modules/module-tunnel-source.c:183
+#: src/modules/module-protocol-pulse/modules/module-tunnel-sink.c:180
+#: src/modules/module-protocol-pulse/modules/module-tunnel-source.c:180
 #, c-format
 msgid "Tunnel to %s/%s"
 msgstr "Tunnel till %s/%s"
@@ -61,7 +61,7 @@ msgstr "Tunnel till %s/%s"
 msgid "Dummy Output"
 msgstr "Attrapputgång"
 
-#: src/modules/module-pulse-tunnel.c:639
+#: src/modules/module-pulse-tunnel.c:648
 #, c-format
 msgid "Tunnel for %s@%s"
 msgstr "Tunnel för %s@%s"
@@ -80,7 +80,7 @@ msgstr "%s på %s@%s"
 msgid "%s on %s"
 msgstr "%s på %s"
 
-#: src/tools/pw-cat.c:872
+#: src/tools/pw-cat.c:784
 #, c-format
 msgid ""
 "%s [options] [<file>|-]\n"
@@ -95,7 +95,7 @@ msgstr ""
 "  -v, --verbose                         Aktivera utförliga operationer\n"
 "\n"
 
-#: src/tools/pw-cat.c:879
+#: src/tools/pw-cat.c:791
 #, c-format
 msgid ""
 "  -R, --remote                          Remote daemon name\n"
@@ -125,7 +125,7 @@ msgstr ""
 "  -P  --properties                      Sätt nodegenskaper\n"
 "\n"
 
-#: src/tools/pw-cat.c:897
+#: src/tools/pw-cat.c:809
 #, c-format
 msgid ""
 "      --rate                            Sample rate (req. for rec) (default "
@@ -160,7 +160,7 @@ msgstr ""
 "%d)\n"
 "\n"
 
-#: src/tools/pw-cat.c:914
+#: src/tools/pw-cat.c:826
 msgid ""
 "  -p, --playback                        Playback mode\n"
 "  -r, --record                          Recording mode\n"
@@ -174,7 +174,7 @@ msgstr ""
 "  -d, --dsd                             DSD-läge\n"
 "\n"
 
-#: src/tools/pw-cli.c:3139
+#: src/tools/pw-cli.c:3165
 #, c-format
 msgid ""
 "%s [options] [command]\n"
@@ -195,8 +195,8 @@ msgstr ""
 msgid "Pro Audio"
 msgstr "Professionellt ljud"
 
-#: spa/plugins/alsa/acp/acp.c:444 spa/plugins/alsa/acp/alsa-mixer.c:4648
-#: spa/plugins/bluez5/bluez5-device.c:1161
+#: spa/plugins/alsa/acp/acp.c:446 spa/plugins/alsa/acp/alsa-mixer.c:4648
+#: spa/plugins/bluez5/bluez5-device.c:1188
 msgid "Off"
 msgstr "Av"
 
@@ -223,7 +223,7 @@ msgstr "Linje in"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:2657
 #: spa/plugins/alsa/acp/alsa-mixer.c:2741
-#: spa/plugins/bluez5/bluez5-device.c:1330
+#: spa/plugins/bluez5/bluez5-device.c:1360
 msgid "Microphone"
 msgstr "Mikrofon"
 
@@ -289,7 +289,7 @@ msgid "No Bass Boost"
 msgstr "Ingen basökning"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:2672
-#: spa/plugins/bluez5/bluez5-device.c:1335
+#: spa/plugins/bluez5/bluez5-device.c:1366
 msgid "Speaker"
 msgstr "Högtalare"
 
@@ -404,7 +404,7 @@ msgstr "Stereo"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:4484
 #: spa/plugins/alsa/acp/alsa-mixer.c:4642
-#: spa/plugins/bluez5/bluez5-device.c:1320
+#: spa/plugins/bluez5/bluez5-device.c:1348
 msgid "Headset"
 msgstr "Headset"
 
@@ -554,13 +554,13 @@ msgstr[1] ""
 #: spa/plugins/alsa/acp/alsa-util.c:1239
 #, c-format
 msgid ""
-"snd_pcm_delay() returned a value that is exceptionally large: %li byte "
-"(%s%lu ms).\n"
+"snd_pcm_delay() returned a value that is exceptionally large: %li byte (%s"
+"%lu ms).\n"
 "Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
 "to the ALSA developers."
 msgid_plural ""
-"snd_pcm_delay() returned a value that is exceptionally large: %li bytes "
-"(%s%lu ms).\n"
+"snd_pcm_delay() returned a value that is exceptionally large: %li bytes (%s"
+"%lu ms).\n"
 "Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
 "to the ALSA developers."
 msgstr[0] ""
@@ -610,7 +610,7 @@ msgstr[1] ""
 "Förmodligen är detta ett fel i ALSA-drivrutinen ”%s”. Vänligen rapportera "
 "problemet till ALSA-utvecklarna."
 
-#: spa/plugins/alsa/acp/channelmap.h:464
+#: spa/plugins/alsa/acp/channelmap.h:457
 msgid "(invalid)"
 msgstr "(ogiltig)"
 
@@ -622,61 +622,77 @@ msgstr "Inbyggt ljud"
 msgid "Modem"
 msgstr "Modem"
 
-#: spa/plugins/bluez5/bluez5-device.c:1172
+#: spa/plugins/bluez5/bluez5-device.c:1199
 msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
 msgstr "Audio gateway (A2DP-källa & HSP/HFP AG)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1197
+#: spa/plugins/bluez5/bluez5-device.c:1224
 #, c-format
 msgid "High Fidelity Playback (A2DP Sink, codec %s)"
 msgstr "High fidelity-uppspelning (A2DP-utgång, kodek %s)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1200
+#: spa/plugins/bluez5/bluez5-device.c:1227
 #, c-format
 msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
 msgstr "High fidelity duplex (A2DP-källa/utgång, kodek %s)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1208
+#: spa/plugins/bluez5/bluez5-device.c:1235
 msgid "High Fidelity Playback (A2DP Sink)"
 msgstr "High fidelity-uppspelning (A2DP-utgång)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1210
+#: spa/plugins/bluez5/bluez5-device.c:1237
 msgid "High Fidelity Duplex (A2DP Source/Sink)"
 msgstr "High fidelity duplex (A2DP-källa/utgång)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1238
+#: spa/plugins/bluez5/bluez5-device.c:1265
 #, c-format
 msgid "Headset Head Unit (HSP/HFP, codec %s)"
 msgstr "Headset-huvudenhet (HSP/HFP, kodek %s)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1243
+#: spa/plugins/bluez5/bluez5-device.c:1270
 msgid "Headset Head Unit (HSP/HFP)"
 msgstr "Headset-huvudenhet (HSP/HFP)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1325
+#: spa/plugins/bluez5/bluez5-device.c:1349
+#: spa/plugins/bluez5/bluez5-device.c:1354
+#: spa/plugins/bluez5/bluez5-device.c:1361
+#: spa/plugins/bluez5/bluez5-device.c:1367
+#: spa/plugins/bluez5/bluez5-device.c:1373
+#: spa/plugins/bluez5/bluez5-device.c:1379
+#: spa/plugins/bluez5/bluez5-device.c:1385
+#: spa/plugins/bluez5/bluez5-device.c:1391
+#: spa/plugins/bluez5/bluez5-device.c:1397
 msgid "Handsfree"
 msgstr "Handsfree"
 
-#: spa/plugins/bluez5/bluez5-device.c:1340
+#: spa/plugins/bluez5/bluez5-device.c:1355
+msgid "Handsfree (HFP)"
+msgstr "Handsfree (HFP)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1372
 msgid "Headphone"
 msgstr "Hörlurar"
 
-#: spa/plugins/bluez5/bluez5-device.c:1345
+#: spa/plugins/bluez5/bluez5-device.c:1378
 msgid "Portable"
 msgstr "Bärbar"
 
-#: spa/plugins/bluez5/bluez5-device.c:1350
+#: spa/plugins/bluez5/bluez5-device.c:1384
 msgid "Car"
 msgstr "Bil"
 
-#: spa/plugins/bluez5/bluez5-device.c:1355
+#: spa/plugins/bluez5/bluez5-device.c:1390
 msgid "HiFi"
 msgstr "HiFi"
 
-#: spa/plugins/bluez5/bluez5-device.c:1360
+#: spa/plugins/bluez5/bluez5-device.c:1396
 msgid "Phone"
 msgstr "Telefon"
 
-#: spa/plugins/bluez5/bluez5-device.c:1366
+#: spa/plugins/bluez5/bluez5-device.c:1403
 msgid "Bluetooth"
 msgstr "Bluetooth"
+
+#: spa/plugins/bluez5/bluez5-device.c:1404
+msgid "Bluetooth (HFP)"
+msgstr "Bluetooth (HFP)"
diff --git a/po/uk.po b/po/uk.po
index e2e04ecb3941531e5684865f13bb2008fd6c5888..a8d04cb35108c12f33dbfb48e41b44fd6adbf94f 100644
--- a/po/uk.po
+++ b/po/uk.po
@@ -7,8 +7,8 @@ msgstr ""
 "Project-Id-Version: pipewire\n"
 "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/-/issue"
 "s\n"
-"POT-Creation-Date: 2022-05-05 03:28+0000\n"
-"PO-Revision-Date: 2022-05-05 21:06+0300\n"
+"POT-Creation-Date: 2022-05-20 15:26+0000\n"
+"PO-Revision-Date: 2022-06-18 13:07+0300\n"
 "Last-Translator: Yuri Chornoivan <yurchor@ukr.net>\n"
 "Language-Team: Ukrainian <trans-uk@lists.fedoraproject.org>\n"
 "Language: uk\n"
@@ -48,7 +48,6 @@ msgid "Tunnel to %s/%s"
 msgstr "Тунель до %s/%s"
 
 #: src/modules/module-fallback-sink.c:51
-#| msgid "Game Output"
 msgid "Dummy Output"
 msgstr "Фіктивний вихід"
 
@@ -71,23 +70,29 @@ msgstr "%s на %s@%s"
 msgid "%s on %s"
 msgstr "%s на %s"
 
-#: src/tools/pw-cat.c:871
+#: src/tools/pw-cat.c:872
 #, c-format
+#| msgid ""
+#| "%s [options] <file>\n"
+#| "  -h, --help                            Show this help\n"
+#| "      --version                         Show version\n"
+#| "  -v, --verbose                         Enable verbose operations\n"
+#| "\n"
 msgid ""
-"%s [options] <file>\n"
+"%s [options] [<file>|-]\n"
 "  -h, --help                            Show this help\n"
 "      --version                         Show version\n"
 "  -v, --verbose                         Enable verbose operations\n"
 "\n"
 msgstr ""
-"%s [параметри] <файл>\n"
+"%s [параметри] [<файл>|-]\n"
 "  -h, --help                            вивести довідку\n"
 "      --version                         вивести дані щодо версії\n"
 "  -v, --verbose                         ввімкнути відображення докладної "
 "інформації\n"
 "\n"
 
-#: src/tools/pw-cat.c:878
+#: src/tools/pw-cat.c:879
 #, c-format
 #| msgid ""
 #| "  -R, --remote                          Remote daemon name\n"
@@ -101,8 +106,6 @@ msgstr ""
 #| "                                          or direct samples (256)\n"
 #| "                                          the rate is the one of the "
 #| "source file\n"
-#| "      --list-targets                    List available targets for --"
-#| "target\n"
 #| "\n"
 msgid ""
 "  -R, --remote                          Remote daemon name\n"
@@ -116,6 +119,7 @@ msgid ""
 "                                          or direct samples (256)\n"
 "                                          the rate is the one of the source "
 "file\n"
+"  -P  --properties                      Set node properties\n"
 "\n"
 msgstr ""
 "  -R, --remote                          назва віддаленої фонової служби\n"
@@ -133,9 +137,10 @@ msgstr ""
 "ns)\n"
 "                                          або безпосередні семпли (256)\n"
 "                                          частота — частота з файла джерела\n"
+"  -P  --properties                      встановити властивості вузла\n"
 "\n"
 
-#: src/tools/pw-cat.c:895
+#: src/tools/pw-cat.c:897
 #, c-format
 msgid ""
 "      --rate                            Sample rate (req. for rec) (default "
@@ -171,7 +176,7 @@ msgstr ""
 "(типово, %d)\n"
 "\n"
 
-#: src/tools/pw-cat.c:912
+#: src/tools/pw-cat.c:914
 msgid ""
 "  -p, --playback                        Playback mode\n"
 "  -r, --record                          Recording mode\n"
diff --git a/spa/examples/adapter-control.c b/spa/examples/adapter-control.c
index 39a22a4e25da0583aeaefa5aacfb7b1454afacc8..7caf32f92f76873013fae7c8dd77d9bb54dc5c19 100644
--- a/spa/examples/adapter-control.c
+++ b/spa/examples/adapter-control.c
@@ -62,7 +62,9 @@
 
 static SPA_LOG_IMPL(default_log);
 
-#define MIN_LATENCY	  1024
+#define MIN_LATENCY	    1024
+#define CONTROL_BUFFER_SIZE 32768
+
 
 struct buffer {
 	struct spa_buffer buffer;
@@ -95,10 +97,19 @@ struct data {
 	struct spa_node *sink_follower_node;  // alsa-pcm-sink
 	struct spa_node *sink_node;  // adapter for alsa-pcm-sink
 
+	struct spa_io_position position;
 	struct spa_io_buffers source_sink_io[1];
 	struct spa_buffer *source_buffers[1];
 	struct buffer source_buffer[1];
-	uint8_t ctrl[1024];
+
+	struct spa_io_buffers control_io;
+	struct spa_buffer *control_buffers[1];
+	struct buffer control_buffer[1];
+
+	int buffer_count;
+	bool start_fade_in;
+	double volume_accum;
+	uint32_t volume_offs;
 
 	bool running;
 	pthread_t thread;
@@ -168,6 +179,11 @@ int init_data(struct data *data)
 		str = PLUGINDIR;
 	data->plugin_dir = str;
 
+	/* start not doing fade-in */
+	data->start_fade_in = true;
+	data->volume_accum = 0.0;
+	data->volume_offs = 0;
+
 	/* init the graph */
 	spa_graph_init(&data->graph, &data->graph_state);
 
@@ -274,29 +290,104 @@ exit_cleanup:
 	return res;
 }
 
-static int on_sink_node_ready(void *_data, int status)
+static int fade_in(struct data *data)
 {
-	struct data *data = _data;
+	struct spa_pod_builder b;
+	struct spa_pod_frame f[1];
+	void *buffer = data->control_buffer->datas[0].data;
+	uint32_t buffer_size = data->control_buffer->datas[0].maxsize;
+	data->control_buffer->datas[0].chunk[0].size = buffer_size;
+
+	printf ("fading in\n");
+
+	spa_pod_builder_init(&b, buffer, buffer_size);
+	spa_pod_builder_push_sequence(&b, &f[0], 0);
+	data->volume_offs = 0;
+	do {
+		spa_pod_builder_control(&b, data->volume_offs, SPA_CONTROL_Properties);
+			spa_pod_builder_add_object(&b,
+			SPA_TYPE_OBJECT_Props, 0,
+			SPA_PROP_volume, SPA_POD_Float(data->volume_accum));
+		data->volume_accum += 0.003;
+		data->volume_offs += 200;
+	} while (data->volume_accum < 1.0);
+	spa_pod_builder_pop(&b, &f[0]);
 
-	spa_graph_node_process(&data->graph_source_node);
-	spa_graph_node_process(&data->graph_sink_node);
 	return 0;
 }
 
-static int
-on_sink_node_reuse_buffer(void *_data, uint32_t port_id, uint32_t buffer_id)
+static int fade_out(struct data *data)
+{
+	struct spa_pod_builder b;
+	struct spa_pod_frame f[1];
+	void *buffer = data->control_buffer->datas[0].data;
+	uint32_t buffer_size = data->control_buffer->datas[0].maxsize;
+	data->control_buffer->datas[0].chunk[0].size = buffer_size;
+
+	printf ("fading out\n");
+
+	spa_pod_builder_init(&b, buffer, buffer_size);
+	spa_pod_builder_push_sequence(&b, &f[0], 0);
+	data->volume_offs = 200;
+	do {
+		spa_pod_builder_control(&b, data->volume_offs, SPA_CONTROL_Properties);
+			spa_pod_builder_add_object(&b,
+			SPA_TYPE_OBJECT_Props, 0,
+			SPA_PROP_volume, SPA_POD_Float(data->volume_accum));
+		data->volume_accum -= 0.003;
+		data->volume_offs += 200;
+	} while (data->volume_accum > 0.0);
+	spa_pod_builder_pop(&b, &f[0]);
+
+	return 0;
+}
+
+static void do_fade(struct data *data)
+{
+	switch (data->control_io.status) {
+	case SPA_STATUS_OK:
+	case SPA_STATUS_NEED_DATA:
+		break;
+	case SPA_STATUS_HAVE_DATA:
+	case SPA_STATUS_STOPPED:
+	default:
+		return;
+	}
+
+	/* fade */
+	if (data->start_fade_in)
+		fade_in(data);
+	else
+		fade_out(data);
+
+	data->control_io.status = SPA_STATUS_HAVE_DATA;
+	data->control_io.buffer_id = 0;
+
+	/* alternate */
+	data->start_fade_in = !data->start_fade_in;
+}
+
+static int on_sink_node_ready(void *_data, int status)
 {
 	struct data *data = _data;
 
-	printf ("reuse_buffer: port_id=%d\n", port_id);
-	data->source_sink_io[0].buffer_id = buffer_id;
+	/* only do fade in/out when buffer count is 0 */
+	if (data->buffer_count == 0)
+		  do_fade(data);
+
+	/* update buffer count */
+	data->buffer_count++;
+	if (data->buffer_count > 64)
+		  data->buffer_count = 0;
+
+	spa_graph_node_process(&data->graph_source_node);
+	spa_graph_node_process(&data->graph_sink_node);
 	return 0;
 }
 
 static const struct spa_node_callbacks sink_node_callbacks = {
 	SPA_VERSION_NODE_CALLBACKS,
 	.ready = on_sink_node_ready,
-	.reuse_buffer = on_sink_node_reuse_buffer
 };
 
 static int make_nodes(struct data *data, const char *device)
@@ -306,15 +397,17 @@ static int make_nodes(struct data *data, const char *device)
 	struct spa_pod_builder b = { 0 };
 	uint8_t buffer[1024];
 	char value[32];
-	struct spa_dict_item items[1];
+	struct spa_dict_item items[2];
 	struct spa_audio_info_raw info;
 	struct spa_pod *param;
 
+	items[0] = SPA_DICT_ITEM_INIT("clock.quantum-limit", "8192");
+
 	/* make the source node (audiotestsrc) */
 	if ((res = make_node(data, &data->source_follower_node,
-				   "audiotestsrc/libspa-audiotestsrc.so",
-				   "audiotestsrc",
-				   NULL)) < 0) {
+					"audiotestsrc/libspa-audiotestsrc.so",
+					"audiotestsrc",
+					&SPA_DICT_INIT(items, 1))) < 0) {
 		printf("can't create source follower node (audiotestsrc): %d\n", res);
 		return res;
 	}
@@ -335,11 +428,11 @@ static int make_nodes(struct data *data, const char *device)
 
 	/* make the sink adapter node */
 	snprintf(value, sizeof(value), "pointer:%p", data->source_follower_node);
-	items[0] = SPA_DICT_ITEM_INIT("audio.adapt.follower", value);
+	items[1] = SPA_DICT_ITEM_INIT("audio.adapt.follower", value);
 	if ((res = make_node(data, &data->source_node,
-			     "audioconvert/libspa-audioconvert.so",
-			     SPA_NAME_AUDIO_ADAPT,
-			     &SPA_DICT_INIT(items, 1))) < 0) {
+					"audioconvert/libspa-audioconvert.so",
+					SPA_NAME_AUDIO_ADAPT,
+					&SPA_DICT_INIT(items, 2))) < 0) {
 		printf("can't create source adapter node: %d\n", res);
 		return res;
 	}
@@ -376,20 +469,20 @@ static int make_nodes(struct data *data, const char *device)
 
 	/* make the sink follower node (alsa-pcm-sink) */
 	if ((res = make_node(data, &data->sink_follower_node,
-				   "alsa/libspa-alsa.so",
-				   SPA_NAME_API_ALSA_PCM_SINK,
-				   NULL)) < 0) {
+					"alsa/libspa-alsa.so",
+					SPA_NAME_API_ALSA_PCM_SINK,
+					&SPA_DICT_INIT(items, 1))) < 0) {
 		printf("can't create sink follower node (alsa-pcm-sink): %d\n", res);
 		return res;
 	}
 
 	/* make the sink adapter node */
 	snprintf(value, sizeof(value), "pointer:%p", data->sink_follower_node);
-	items[0] = SPA_DICT_ITEM_INIT("audio.adapt.follower", value);
+	items[1] = SPA_DICT_ITEM_INIT("audio.adapt.follower", value);
 	if ((res = make_node(data, &data->sink_node,
-			     "audioconvert/libspa-audioconvert.so",
-			     SPA_NAME_AUDIO_ADAPT,
-			     &SPA_DICT_INIT(items, 1))) < 0) {
+					"audioconvert/libspa-audioconvert.so",
+					SPA_NAME_AUDIO_ADAPT,
+					&SPA_DICT_INIT(items, 2))) < 0) {
 		printf("can't create sink adapter node: %d\n", res);
 		return res;
 	}
@@ -420,6 +513,7 @@ static int make_nodes(struct data *data, const char *device)
 		SPA_TYPE_OBJECT_ParamPortConfig,	SPA_PARAM_PortConfig,
 		SPA_PARAM_PORT_CONFIG_direction,	SPA_POD_Id(SPA_DIRECTION_INPUT),
 		SPA_PARAM_PORT_CONFIG_mode,		SPA_POD_Id(SPA_PARAM_PORT_CONFIG_MODE_dsp),
+		SPA_PARAM_PORT_CONFIG_control,		SPA_POD_Bool(true),
 		SPA_PARAM_PORT_CONFIG_format,		SPA_POD_Pod(param));
 	if ((res = spa_node_set_param(data->sink_node, SPA_PARAM_PortConfig, 0, param) < 0)) {
 		printf("can't setup sink node %d\n", res);
@@ -442,6 +536,42 @@ static int make_nodes(struct data *data, const char *device)
 		printf("can't set io buffers on port 0 of sink node: %d\n", res);
 		return res;
 	}
+	/* set io position and clock on source and sink nodes */
+	data->position.clock.rate = SPA_FRACTION(1, 48000);
+	data->position.clock.duration = 1024;
+	if ((res = spa_node_set_io(data->source_node,
+			SPA_IO_Position,
+			&data->position, sizeof(data->position))) < 0) {
+		printf("can't set io position on source node: %d\n", res);
+		return res;
+	}
+	if ((res = spa_node_set_io(data->sink_node,
+			  SPA_IO_Position,
+			  &data->position, sizeof(data->position))) < 0) {
+		printf("can't set io position on sink node: %d\n", res);
+		return res;
+	}
+	if ((res = spa_node_set_io(data->source_node,
+			SPA_IO_Clock,
+			&data->position.clock, sizeof(data->position.clock))) < 0) {
+		printf("can't set io clock on source node: %d\n", res);
+		return res;
+	}
+	if ((res = spa_node_set_io(data->sink_node,
+			  SPA_IO_Clock,
+			  &data->position.clock, sizeof(data->position.clock))) < 0) {
+		printf("can't set io clock on sink node: %d\n", res);
+		return res;
+	}
+
+	/* set io buffers on control port of sink node */
+	if ((res = spa_node_port_set_io(data->sink_node,
+			SPA_DIRECTION_INPUT, 1,
+			SPA_IO_Buffers,
+			&data->control_io, sizeof(data->control_io))) < 0) {
+		printf("can't set io buffers on control port 1 of sink node\n");
+		return res;
+	}
 
 	/* add source node to the graph */
 	spa_graph_node_init(&data->graph_source_node, &data->graph_source_state);
@@ -508,17 +638,6 @@ static int negotiate_formats(struct data *data)
 	uint32_t state = 0;
 	size_t buffer_size = 1024;
 
-	/* get the source follower node buffer size */
-	spa_pod_builder_init(&b, buffer, sizeof(buffer));
-	if (spa_node_port_enum_params_sync(data->source_follower_node,
-			SPA_DIRECTION_OUTPUT, 0,
-			SPA_PARAM_Buffers, &state, filter, &param, &b) != 1)
-		return -ENOTSUP;
-	spa_pod_fixate(param);
-	if ((res = spa_pod_parse_object(param, SPA_TYPE_OBJECT_ParamBuffers, NULL,
-		SPA_PARAM_BUFFERS_size, SPA_POD_Int(&buffer_size))) < 0)
-		return res;
-
 	/* set the sink and source formats */
 	spa_pod_builder_init(&b, buffer, sizeof(buffer));
 	param = spa_format_audio_dsp_build(&b, 0,
@@ -531,6 +650,26 @@ static int negotiate_formats(struct data *data)
 			SPA_DIRECTION_INPUT, 0, SPA_PARAM_Format, 0, param)) < 0)
 		return res;
 
+	spa_pod_builder_init(&b, buffer, sizeof(buffer));
+	param = spa_pod_builder_add_object(&b,
+			SPA_TYPE_OBJECT_Format, SPA_PARAM_Format,
+			SPA_FORMAT_mediaType,      SPA_POD_Id(SPA_MEDIA_TYPE_application),
+			SPA_FORMAT_mediaSubtype,   SPA_POD_Id(SPA_MEDIA_SUBTYPE_control));
+	if ((res = spa_node_port_set_param(data->sink_node,
+			SPA_DIRECTION_INPUT, 1, SPA_PARAM_Format, 0, param)) < 0)
+		return res;
+
+	/* get the source node buffer size */
+	spa_pod_builder_init(&b, buffer, sizeof(buffer));
+	if ((res = spa_node_port_enum_params_sync(data->source_node,
+			SPA_DIRECTION_OUTPUT, 0,
+			SPA_PARAM_Buffers, &state, filter, &param, &b)) != 1)
+		return res ? res : -ENOTSUP;
+	spa_pod_fixate(param);
+	if ((res = spa_pod_parse_object(param, SPA_TYPE_OBJECT_ParamBuffers, NULL,
+		SPA_PARAM_BUFFERS_size, SPA_POD_Int(&buffer_size))) < 0)
+		return res;
+
 	/* use buffers on the source and sink */
 	init_buffer(data, data->source_buffers, data->source_buffer, 1, buffer_size);
 	if ((res = spa_node_port_use_buffers(data->source_node,
@@ -540,6 +679,12 @@ static int negotiate_formats(struct data *data)
 		SPA_DIRECTION_INPUT, 0, 0, data->source_buffers, 1)) < 0)
 		return res;
 
+	/* Set the control buffers */
+	init_buffer(data, data->control_buffers, data->control_buffer, 1, CONTROL_BUFFER_SIZE);
+	if ((res = spa_node_port_use_buffers(data->sink_node,
+		SPA_DIRECTION_INPUT, 1, 0, data->control_buffers, 1)) < 0)
+		return res;
+
 	return 0;
 }
 
diff --git a/spa/include/spa/buffer/buffer.h b/spa/include/spa/buffer/buffer.h
index c4aa8a556051446c8f409ab3749785d66e9a03c3..47204acbcfd260ff1d869eba4cb8026f469ecbdf 100644
--- a/spa/include/spa/buffer/buffer.h
+++ b/spa/include/spa/buffer/buffer.h
@@ -63,6 +63,9 @@ struct spa_chunk {
 	int32_t stride;			/**< stride of valid data */
 #define SPA_CHUNK_FLAG_NONE		0
 #define SPA_CHUNK_FLAG_CORRUPTED	(1u<<0)	/**< chunk data is corrupted in some way */
+#define SPA_CHUNK_FLAG_EMPTY		(1u<<1)	/**< chunk data is empty with media specific
+						  *  neutral data such as silence or black. This
+						  *  could be used to optimize processing. */
 	int32_t flags;			/**< chunk flags */
 };
 
diff --git a/spa/include/spa/interfaces/audio/aec.h b/spa/include/spa/interfaces/audio/aec.h
index 17e4e4e463f63657a36168cb11beea4309a718a8..601f7b61e211b6d5c4f6edce01b266c9b9d2cab1 100644
--- a/spa/include/spa/interfaces/audio/aec.h
+++ b/spa/include/spa/interfaces/audio/aec.h
@@ -68,9 +68,9 @@ struct spa_audio_aec_methods {
 			const struct spa_audio_aec_events *events,
 			void *data);
 
-	int (*init) (void *data, const struct spa_dict *args, const struct spa_audio_info_raw *info);
-	int (*run) (void *data, const float *rec[], const float *play[], float *out[], uint32_t n_samples);
-	int (*set_props) (void *data, const struct spa_dict *args);
+	int (*init) (void *object, const struct spa_dict *args, const struct spa_audio_info_raw *info);
+	int (*run) (void *object, const float *rec[], const float *play[], float *out[], uint32_t n_samples);
+	int (*set_props) (void *object, const struct spa_dict *args);
 };
 
 #define spa_audio_aec_method(o,method,version,...)			\
diff --git a/spa/include/spa/node/node.h b/spa/include/spa/node/node.h
index f32adec16cd1e2563a93aa627c66e2eb3e6fcfc6..17be05d67f371088ab86dfa258d973acb7179c4f 100644
--- a/spa/include/spa/node/node.h
+++ b/spa/include/spa/node/node.h
@@ -632,6 +632,12 @@ struct spa_node_methods {
 	 *
 	 * When the node can accept new input in the next cycle, the
 	 * SPA_STATUS_NEED_DATA bit will be set.
+	 *
+	 * Note that the node might return SPA_STATUS_NEED_DATA even when
+	 * no input ports have this status. This means that the amount of
+	 * data still available on the input ports is likely not going to
+	 * be enough for the next cycle and the host might need to prefetch
+	 * data for the next cycle.
 	 */
 	int (*process) (void *object);
 };
diff --git a/spa/include/spa/param/audio/layout.h b/spa/include/spa/param/audio/layout.h
index 1868144ef34e333838e67f24043001a8485d3ae3..66154bf62e10226436465e8d7886ff216474c9f0 100644
--- a/spa/include/spa/param/audio/layout.h
+++ b/spa/include/spa/param/audio/layout.h
@@ -29,7 +29,7 @@
 extern "C" {
 #endif
 
-#ifndef __FreeBSD__
+#if !defined(__FreeBSD__) && !defined(__MidnightBSD__)
 #include <endian.h>
 #endif
 
diff --git a/spa/include/spa/param/audio/raw.h b/spa/include/spa/param/audio/raw.h
index 47fe90dfa158ba0d07b906a69977771c8592ea09..a34915c422b32e47af5ec10ef785d8100c7518a5 100644
--- a/spa/include/spa/param/audio/raw.h
+++ b/spa/include/spa/param/audio/raw.h
@@ -31,7 +31,7 @@ extern "C" {
 
 #include <stdint.h>
 
-#ifndef __FreeBSD__
+#if !defined(__FreeBSD__) && !defined(__MidnightBSD__)
 #include <endian.h>
 #endif
 
diff --git a/spa/include/spa/param/bluetooth/audio.h b/spa/include/spa/param/bluetooth/audio.h
index 5c215b411f273e88f3b80f81fef4236f27bb3493..f1037657c5df72c91222a0a8bdc9221afff77878 100644
--- a/spa/include/spa/param/bluetooth/audio.h
+++ b/spa/include/spa/param/bluetooth/audio.h
@@ -49,6 +49,11 @@ enum spa_bluetooth_audio_codec {
 	SPA_BLUETOOTH_AUDIO_CODEC_FASTSTREAM,
 	SPA_BLUETOOTH_AUDIO_CODEC_FASTSTREAM_DUPLEX,
 	SPA_BLUETOOTH_AUDIO_CODEC_LC3PLUS_HR,
+	SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05,
+	SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_51,
+	SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_71,
+	SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_DUPLEX,
+	SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_PRO,
 
 	/* HFP */
 	SPA_BLUETOOTH_AUDIO_CODEC_CVSD = 0x100,
diff --git a/spa/include/spa/param/bluetooth/type-info.h b/spa/include/spa/param/bluetooth/type-info.h
index 0471dcce853f77d93ad2d1321051ee3cb6ddd78d..8286b970ad2576556093b756d16fd46358e6c447 100644
--- a/spa/include/spa/param/bluetooth/type-info.h
+++ b/spa/include/spa/param/bluetooth/type-info.h
@@ -53,6 +53,11 @@ static const struct spa_type_info spa_type_bluetooth_audio_codec[] = {
 	{ SPA_BLUETOOTH_AUDIO_CODEC_FASTSTREAM, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "faststream", NULL },
 	{ SPA_BLUETOOTH_AUDIO_CODEC_FASTSTREAM_DUPLEX, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "faststream_duplex", NULL },
 	{ SPA_BLUETOOTH_AUDIO_CODEC_LC3PLUS_HR, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "lc3plus_hr", NULL },
+	{ SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "opus_05", NULL },
+	{ SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_51, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "opus_05_51", NULL },
+	{ SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_71, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "opus_05_71", NULL },
+	{ SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_DUPLEX, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "opus_05_duplex", NULL },
+	{ SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_PRO, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "opus_05_pro", NULL },
 
 	{ SPA_BLUETOOTH_AUDIO_CODEC_CVSD, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "cvsd", NULL },
 	{ SPA_BLUETOOTH_AUDIO_CODEC_MSBC, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "msbc", NULL },
diff --git a/spa/include/spa/utils/defs.h b/spa/include/spa/utils/defs.h
index 874fab410f6d23c58b9f139444e642aee168f30f..cf15cc2446cd2fd7d7850aaa0960c7a48799afb1 100644
--- a/spa/include/spa/utils/defs.h
+++ b/spa/include/spa/utils/defs.h
@@ -131,13 +131,13 @@ struct spa_fraction {
 ({				\
 	__typeof__(a) _min_a = (a);	\
 	__typeof__(b) _min_b = (b);	\
-	SPA_LIKELY(_min_a < _min_b) ? _min_a : _min_b;	\
+	SPA_LIKELY(_min_a <= _min_b) ? _min_a : _min_b;	\
 })
 #define SPA_MAX(a,b)		\
 ({				\
 	__typeof__(a) _max_a = (a);	\
 	__typeof__(b) _max_b = (b);	\
-	SPA_LIKELY(_max_a > _max_b) ? _max_a : _max_b;	\
+	SPA_LIKELY(_max_a >= _max_b) ? _max_a : _max_b;	\
 })
 #define SPA_CLAMP(v,low,high)				\
 ({							\
@@ -147,6 +147,12 @@ struct spa_fraction {
 	SPA_MIN(SPA_MAX(_v, _low), _high);		\
 })
 
+#define SPA_CLAMPF(v,low,high)				\
+({							\
+	fminf(fmaxf(v, low), high);			\
+})
+
+
 #define SPA_SWAP(a,b)					\
 ({							\
 	__typeof__(a) _t = (a);				\
@@ -209,6 +215,7 @@ struct spa_fraction {
 #define SPA_SENTINEL __attribute__((__sentinel__))
 #define SPA_UNUSED __attribute__ ((unused))
 #define SPA_NORETURN __attribute__ ((noreturn))
+#define SPA_WARN_UNUSED_RESULT __attribute__ ((warn_unused_result))
 #else
 #define SPA_PRINTF_FUNC(fmt, arg1)
 #define SPA_FORMAT_ARG_FUNC(arg1)
@@ -218,6 +225,7 @@ struct spa_fraction {
 #define SPA_SENTINEL
 #define SPA_UNUSED
 #define SPA_NORETURN
+#define SPA_WARN_UNUSED_RESULT
 #endif
 
 #if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L
diff --git a/spa/include/spa/utils/hook.h b/spa/include/spa/utils/hook.h
index 9b1a50b63bc98a1377743171cab2a16a4d317a00..953b97445ed05f0986358af25f828693db695467 100644
--- a/spa/include/spa/utils/hook.h
+++ b/spa/include/spa/utils/hook.h
@@ -382,7 +382,8 @@ static inline void spa_hook_list_prepend(struct spa_hook_list *list,
 /** Remove a hook */
 static inline void spa_hook_remove(struct spa_hook *hook)
 {
-	spa_list_remove(&hook->link);
+	if (spa_list_is_initialized(&hook->link))
+		spa_list_remove(&hook->link);
 	if (hook->removed)
 		hook->removed(hook);
 }
diff --git a/spa/include/spa/utils/json.h b/spa/include/spa/utils/json.h
index 73cad7f59666dc6f04dc0258b3211b3d741144b0..19093169b87fb43da4d72bec9a9f7e1f487faaed 100644
--- a/spa/include/spa/utils/json.h
+++ b/spa/include/spa/utils/json.h
@@ -240,6 +240,8 @@ static inline bool spa_json_is_null(const char *val, int len)
 static inline int spa_json_parse_float(const char *val, int len, float *result)
 {
 	char *end;
+	if (strspn(val, "+-0123456789.Ee") < (size_t)len)
+		return 0;
 	*result = spa_strtof(val, &end);
 	return len > 0 && end == val + len;
 }
diff --git a/spa/include/spa/utils/list.h b/spa/include/spa/utils/list.h
index 62aa9a3df7f9cc7cf6a4d5ee586d7ed1e46fc442..2300905edaf254418f54b543fc4f94084bbce183 100644
--- a/spa/include/spa/utils/list.h
+++ b/spa/include/spa/utils/list.h
@@ -51,6 +51,11 @@ static inline void spa_list_init(struct spa_list *list)
 	*list = SPA_LIST_INIT(list);
 }
 
+static inline int spa_list_is_initialized(struct spa_list *list)
+{
+	return !!list->prev;
+}
+
 #define spa_list_is_empty(l)  ((l)->next == (l))
 
 static inline void spa_list_insert(struct spa_list *list, struct spa_list *elem)
diff --git a/spa/meson.build b/spa/meson.build
index 82e274e9ff871c50f4217d78a7c3dc2c1b4fcde7..44b86eaf51968fd526b43e0266a0dea096d9aea8 100644
--- a/spa/meson.build
+++ b/spa/meson.build
@@ -62,6 +62,8 @@ if get_option('spa-plugins').allowed()
       endif
     endif
     summary({'LC3plus': lc3plus_dep.found()}, bool_yn: true, section: 'Bluetooth audio codecs')
+    opus_dep = dependency('opus', required : get_option('bluez5-codec-opus'))
+    summary({'Opus': opus_dep.found()}, bool_yn: true, section: 'Bluetooth audio codecs')
   endif
   avcodec_dep = dependency('libavcodec', required: get_option('ffmpeg'))
   jack_dep = dependency('jack', version : '>= 1.9.10', required: get_option('jack'))
diff --git a/spa/plugins/aec/aec-null.c b/spa/plugins/aec/aec-null.c
index 0dea9fc292b8ad81779248380373c4fa7f516e7b..b8d5b8528d4fb741749a1e6a7e7c0fe3694b113a 100644
--- a/spa/plugins/aec/aec-null.c
+++ b/spa/plugins/aec/aec-null.c
@@ -58,19 +58,18 @@ static int null_run(void *object, const float *rec[], const float *play[], float
 	return 0;
 }
 
-static struct spa_audio_aec_methods impl_aec = {
+static const struct spa_audio_aec_methods impl_aec = {
+	SPA_VERSION_AUDIO_AEC,
 	.init = null_init,
 	.run = null_run,
 };
 
 static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface)
 {
-	struct impl *impl;
-
 	spa_return_val_if_fail(handle != NULL, -EINVAL);
 	spa_return_val_if_fail(interface != NULL, -EINVAL);
 
-	impl = (struct impl *) handle;
+	struct impl *impl = (struct impl *) handle;
 
 	if (spa_streq(type, SPA_TYPE_INTERFACE_AUDIO_AEC))
 		*interface = &impl->aec;
@@ -101,15 +100,13 @@ impl_init(const struct spa_handle_factory *factory,
 	  const struct spa_support *support,
 	  uint32_t n_support)
 {
-	struct impl *impl;
-
 	spa_return_val_if_fail(factory != NULL, -EINVAL);
 	spa_return_val_if_fail(handle != NULL, -EINVAL);
 
 	handle->get_interface = impl_get_interface;
 	handle->clear = impl_clear;
 
-	impl = (struct impl *) handle;
+	struct impl *impl = (struct impl *) handle;
 
 	impl->aec.iface = SPA_INTERFACE_INIT(
 		SPA_TYPE_INTERFACE_AUDIO_AEC,
@@ -119,7 +116,7 @@ impl_init(const struct spa_handle_factory *factory,
 	impl->aec.info = NULL;
 	impl->aec.latency = NULL;
 
-	impl->log = (struct spa_log*)spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
+	impl->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
 	spa_log_topic_init(impl->log, &log_topic);
 
 	spa_hook_list_init(&impl->hooks_list);
@@ -151,7 +148,7 @@ impl_enum_interface_info(const struct spa_handle_factory *factory,
 	return 1;
 }
 
-const struct spa_handle_factory spa_aec_null_factory = {
+static const struct spa_handle_factory spa_aec_null_factory = {
 	SPA_VERSION_HANDLE_FACTORY,
 	SPA_NAME_AEC,
 	NULL,
@@ -160,7 +157,6 @@ const struct spa_handle_factory spa_aec_null_factory = {
 	impl_enum_interface_info,
 };
 
-
 SPA_EXPORT
 int spa_handle_factory_enum(const struct spa_handle_factory **factory, uint32_t *index)
 {
diff --git a/spa/plugins/aec/aec-webrtc.cpp b/spa/plugins/aec/aec-webrtc.cpp
index 89abb05926deed25d1ff77e99d404144658e1642..5c129c534b5c2ee5fa17c9bc24a2c601fdb2a2e3 100644
--- a/spa/plugins/aec/aec-webrtc.cpp
+++ b/spa/plugins/aec/aec-webrtc.cpp
@@ -50,19 +50,17 @@ static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.eac.webrtc");
 #undef SPA_LOG_TOPIC_DEFAULT
 #define SPA_LOG_TOPIC_DEFAULT &log_topic
 
-static bool webrtc_get_spa_bool(const struct spa_dict *args, const char *key, bool default_value) {
-	const char *str_val;
-	bool value = default_value;
-	str_val = spa_dict_lookup(args, key);
-	if (str_val != NULL)
-		value =spa_atob(str_val);
-
-	return value;
+static bool webrtc_get_spa_bool(const struct spa_dict *args, const char *key, bool default_value)
+{
+	if (auto str = spa_dict_lookup(args, key))
+		return spa_atob(str);
+
+	return default_value;
 }
 
-static int webrtc_init(void *data, const struct spa_dict *args, const struct spa_audio_info_raw *info)
+static int webrtc_init(void *object, const struct spa_dict *args, const struct spa_audio_info_raw *info)
 {
-	auto impl = reinterpret_cast<struct impl_data*>(data);
+	auto impl = static_cast<struct impl_data*>(object);
 
 	bool extended_filter = webrtc_get_spa_bool(args, "webrtc.extended_filter", true);
 	bool delay_agnostic = webrtc_get_spa_bool(args, "webrtc.delay_agnostic", true);
@@ -122,9 +120,9 @@ static int webrtc_init(void *data, const struct spa_dict *args, const struct spa
 	return 0;
 }
 
-static int webrtc_run(void *data, const float *rec[], const float *play[], float *out[], uint32_t n_samples)
+static int webrtc_run(void *object, const float *rec[], const float *play[], float *out[], uint32_t n_samples)
 {
-	auto impl = reinterpret_cast<struct impl_data*>(data);
+	auto impl = static_cast<struct impl_data*>(object);
 	webrtc::StreamConfig config =
 		webrtc::StreamConfig(impl->info.rate, impl->info.channels, false);
 	unsigned int num_blocks = n_samples * 1000 / impl->info.rate / 10;
@@ -160,7 +158,7 @@ static int webrtc_run(void *data, const float *rec[], const float *play[], float
 	return 0;
 }
 
-static struct spa_audio_aec_methods impl_aec = {
+static const struct spa_audio_aec_methods impl_aec = {
 	SPA_VERSION_AUDIO_AEC_METHODS,
 	.add_listener = NULL,
 	.init = webrtc_init,
@@ -220,7 +218,7 @@ impl_init(const struct spa_handle_factory *factory,
 	impl->aec.info = NULL;
 	impl->aec.latency = "480/48000",
 
-	impl->log = (struct spa_log*)spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
+	impl->log = static_cast<struct spa_log *>(spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log));
 	spa_log_topic_init(impl->log, &log_topic);
 
 	return 0;
@@ -250,7 +248,7 @@ impl_enum_interface_info(const struct spa_handle_factory *factory,
 	return 1;
 }
 
-const struct spa_handle_factory spa_aec_webrtc_factory = {
+static const struct spa_handle_factory spa_aec_webrtc_factory = {
 	SPA_VERSION_HANDLE_FACTORY,
 	SPA_NAME_AEC,
 	NULL,
@@ -259,7 +257,6 @@ const struct spa_handle_factory spa_aec_webrtc_factory = {
 	impl_enum_interface_info,
 };
 
-
 SPA_EXPORT
 int spa_handle_factory_enum(const struct spa_handle_factory **factory, uint32_t *index)
 {
diff --git a/spa/plugins/alsa/90-pipewire-alsa.rules b/spa/plugins/alsa/90-pipewire-alsa.rules
index 494e89d40c74157d50f49beaf51fd745ea81ed23..f451275b692d9fc158b224cd9336389213d8b114 100644
--- a/spa/plugins/alsa/90-pipewire-alsa.rules
+++ b/spa/plugins/alsa/90-pipewire-alsa.rules
@@ -177,6 +177,10 @@ ATTRS{idVendor}=="1395", ATTRS{idProduct}=="0300", ENV{ACP_PROFILE_SET}="usb-gam
 # Sennheiser GSP 670 USB headset
 ATTRS{idVendor}=="1395", ATTRS{idProduct}=="008a", ENV{ACP_PROFILE_SET}="usb-gaming-headset.conf"
 
+# Audioengine HD3 powered speakers support IEC958 but don't actually
+# have any digital outputs.
+ATTRS{idVendor}=="0a12", ATTRS{idProduct}=="4007", ENV{ACP_PROFILE_SET}="analog-only.conf"
+
 GOTO="pipewire_end"
 
 LABEL="pipewire_check_pci"
diff --git a/spa/plugins/alsa/acp/acp.c b/spa/plugins/alsa/acp/acp.c
index 39475baf5ef7cfe905901e6e0a52d954615a6148..aea4b440d02e677be9b5d130dea54f1496290df5 100644
--- a/spa/plugins/alsa/acp/acp.c
+++ b/spa/plugins/alsa/acp/acp.c
@@ -382,8 +382,7 @@ static int add_pro_profile(pa_card *impl, uint32_t index)
 							0, NULL, NULL, false))) {
 				pa_alsa_init_proplist_pcm(NULL, m->output_proplist, m->output_pcm);
 				pa_proplist_setf(m->output_proplist, "clock.name", "api.alsa.%u", index);
-				snd_pcm_close(m->output_pcm);
-				m->output_pcm = NULL;
+				pa_alsa_close(&m->output_pcm);
 				m->supported = true;
 				pa_channel_map_init_auto(&m->channel_map, m->sample_spec.channels, PA_CHANNEL_MAP_AUX);
 			}
@@ -413,8 +412,7 @@ static int add_pro_profile(pa_card *impl, uint32_t index)
 							0, NULL, NULL, false))) {
 				pa_alsa_init_proplist_pcm(NULL, m->input_proplist, m->input_pcm);
 				pa_proplist_setf(m->input_proplist, "clock.name", "api.alsa.%u", index);
-				snd_pcm_close(m->input_pcm);
-				m->input_pcm = NULL;
+				pa_alsa_close(&m->input_pcm);
 				m->supported = true;
 				pa_channel_map_init_auto(&m->channel_map, m->sample_spec.channels, PA_CHANNEL_MAP_AUX);
 			}
@@ -1058,6 +1056,9 @@ static int read_volume(pa_alsa_device *dev)
 	uint32_t i;
 	int res;
 
+	if (!dev->mixer_handle)
+		return 0;
+
 	if ((res = pa_alsa_path_get_volume(dev->mixer_path, dev->mixer_handle, &dev->mapping->channel_map, &r)) < 0)
 		return res;
 
@@ -1089,6 +1090,9 @@ static void set_volume(pa_alsa_device *dev, const pa_cvolume *v)
 
 	dev->real_volume = *v;
 
+	if (!dev->mixer_handle)
+		return;
+
 	/* Shift up by the base volume */
 	pa_sw_cvolume_divide_scalar(&r, &dev->real_volume, dev->base_volume);
 
@@ -1139,6 +1143,9 @@ static int read_mute(pa_alsa_device *dev)
 	bool mute;
 	int res;
 
+	if (!dev->mixer_handle)
+		return 0;
+
 	if ((res = pa_alsa_path_get_mute(dev->mixer_path, dev->mixer_handle, &mute)) < 0)
 		return res;
 
@@ -1157,6 +1164,10 @@ static int read_mute(pa_alsa_device *dev)
 static void set_mute(pa_alsa_device *dev, bool mute)
 {
 	dev->muted = mute;
+
+	if (!dev->mixer_handle)
+		return;
+
 	pa_alsa_path_set_mute(dev->mixer_path, dev->mixer_handle, mute);
 }
 
@@ -1735,7 +1746,8 @@ static void sync_mixer(pa_alsa_device *d, pa_device_port *port)
 		setting = data->setting;
 	}
 
-	pa_alsa_path_select(d->mixer_path, setting, d->mixer_handle, d->muted);
+	if (d->mixer_handle)
+		pa_alsa_path_select(d->mixer_path, setting, d->mixer_handle, d->muted);
 
 	if (d->set_mute)
 		d->set_mute(d, d->muted);
diff --git a/spa/plugins/alsa/acp/alsa-mixer.c b/spa/plugins/alsa/acp/alsa-mixer.c
index e19700ecb41fe4c827cb4ccd30de2125c098e24b..86425fd248c11ef34c1d85c1051851984a73638a 100644
--- a/spa/plugins/alsa/acp/alsa-mixer.c
+++ b/spa/plugins/alsa/acp/alsa-mixer.c
@@ -4966,8 +4966,7 @@ static void profile_finalize_probing(pa_alsa_profile *to_be_finalized, pa_alsa_p
                 continue;
 
             pa_alsa_init_proplist_pcm(NULL, m->output_proplist, m->output_pcm);
-            snd_pcm_close(m->output_pcm);
-            m->output_pcm = NULL;
+            pa_alsa_close(&m->output_pcm);
         }
 
     if (to_be_finalized->input_mappings)
@@ -4986,8 +4985,7 @@ static void profile_finalize_probing(pa_alsa_profile *to_be_finalized, pa_alsa_p
                 continue;
 
             pa_alsa_init_proplist_pcm(NULL, m->input_proplist, m->input_pcm);
-            snd_pcm_close(m->input_pcm);
-            m->input_pcm = NULL;
+            pa_alsa_close(&m->input_pcm);
         }
 }
 
diff --git a/spa/plugins/alsa/acp/alsa-ucm.c b/spa/plugins/alsa/acp/alsa-ucm.c
index eea173b323a05fd6910e5548b716d9d9eaf21cdf..f66b771997bd2876e5a9289688e096b4776b78f5 100644
--- a/spa/plugins/alsa/acp/alsa-ucm.c
+++ b/spa/plugins/alsa/acp/alsa-ucm.c
@@ -1941,8 +1941,7 @@ static void profile_finalize_probing(pa_alsa_profile *p) {
             continue;
 
         pa_alsa_init_proplist_pcm(NULL, m->output_proplist, m->output_pcm);
-        snd_pcm_close(m->output_pcm);
-        m->output_pcm = NULL;
+        pa_alsa_close(&m->output_pcm);
     }
 
     PA_IDXSET_FOREACH(m, p->input_mappings, idx) {
@@ -1953,8 +1952,7 @@ static void profile_finalize_probing(pa_alsa_profile *p) {
             continue;
 
         pa_alsa_init_proplist_pcm(NULL, m->input_proplist, m->input_pcm);
-        snd_pcm_close(m->input_pcm);
-        m->input_pcm = NULL;
+        pa_alsa_close(&m->input_pcm);
     }
 }
 
diff --git a/spa/plugins/alsa/acp/alsa-util.c b/spa/plugins/alsa/acp/alsa-util.c
index 0a365974ecb506b73bd79a4fd32e364e3cb344c1..c76cef3e21c7d1758e3d79679888406e9a36eeeb 100644
--- a/spa/plugins/alsa/acp/alsa-util.c
+++ b/spa/plugins/alsa/acp/alsa-util.c
@@ -656,6 +656,20 @@ snd_pcm_t *pa_alsa_open_by_device_id_mapping(
     return pcm_handle;
 }
 
+int pa_alsa_close(snd_pcm_t **pcm)
+{
+    int err;
+    pa_assert(pcm);
+    pa_log_info("ALSA device close %p", *pcm);
+    if (*pcm == NULL)
+	    return 0;
+    if ((err = snd_pcm_close(*pcm)) < 0) {
+        pa_log_warn("ALSA close failed: %s", snd_strerror(err));
+    }
+    *pcm = NULL;
+    return err;
+}
+
 snd_pcm_t *pa_alsa_open_by_device_string(
         const char *device,
         char **dev,
@@ -691,8 +705,8 @@ snd_pcm_t *pa_alsa_open_by_device_string(
             pa_log_info("Error opening PCM device %s: %s", d, pa_alsa_strerror(err));
             goto fail;
         }
-
-        pa_log_debug("Managed to open %s", d);
+        pa_log_info("ALSA device open '%s' %s: %p", d,
+			mode == SND_PCM_STREAM_CAPTURE ? "capture" : "playback", pcm_handle);
 
         if ((err = pa_alsa_set_hw_params(
                      pcm_handle,
@@ -707,7 +721,7 @@ snd_pcm_t *pa_alsa_open_by_device_string(
             if (!reformat) {
                 reformat = true;
 
-                snd_pcm_close(pcm_handle);
+                pa_alsa_close(&pcm_handle);
                 continue;
             }
 
@@ -721,12 +735,12 @@ snd_pcm_t *pa_alsa_open_by_device_string(
 
                 reformat = false;
 
-                snd_pcm_close(pcm_handle);
+                pa_alsa_close(&pcm_handle);
                 continue;
             }
 
             pa_log_info("Failed to set hardware parameters on %s: %s", d, pa_alsa_strerror(err));
-            snd_pcm_close(pcm_handle);
+            pa_alsa_close(&pcm_handle);
 
             goto fail;
         }
@@ -734,7 +748,7 @@ snd_pcm_t *pa_alsa_open_by_device_string(
         if (ss->channels > PA_CHANNELS_MAX) {
             pa_log("Device %s has %u channels, but PulseAudio supports only %u channels. Unable to use the device.",
                    d, ss->channels, PA_CHANNELS_MAX);
-            snd_pcm_close(pcm_handle);
+            pa_alsa_close(&pcm_handle);
             goto fail;
         }
 
@@ -1626,7 +1640,12 @@ static int mixer_class_event(snd_mixer_class_t *class, unsigned int mask,
 {
     int err;
     const char *name = snd_hctl_elem_get_name(helem);
-    if (mask & SND_CTL_EVENT_MASK_ADD) {
+    // NOTE: The remove event defined as '~0U`.
+    if (mask == SND_CTL_EVENT_MASK_REMOVE) {
+        // NOTE: unless remove pointer to melem from link-list at private_data of helem, hits
+	// assersion in alsa-lib since the list is not empty.
+        snd_mixer_elem_detach(melem, helem);
+    } else if (mask & SND_CTL_EVENT_MASK_ADD) {
         snd_ctl_elem_iface_t iface = snd_hctl_elem_get_interface(helem);
         if (iface == SND_CTL_ELEM_IFACE_CARD || iface == SND_CTL_ELEM_IFACE_PCM) {
             snd_mixer_elem_t *new_melem;
diff --git a/spa/plugins/alsa/acp/alsa-util.h b/spa/plugins/alsa/acp/alsa-util.h
index 76b8402ace34b8405f34defd10587226aaf7c1d8..b18b98df9ce98a85e8467831794f6609e82c5f1f 100644
--- a/spa/plugins/alsa/acp/alsa-util.h
+++ b/spa/plugins/alsa/acp/alsa-util.h
@@ -115,6 +115,7 @@ snd_pcm_t *pa_alsa_open_by_template(
 void pa_alsa_dump(pa_log_level_t level, snd_pcm_t *pcm);
 void pa_alsa_dump_status(snd_pcm_t *pcm);
 #endif
+int pa_alsa_close(snd_pcm_t **pcm);
 
 void pa_alsa_refcnt_inc(void);
 void pa_alsa_refcnt_dec(void);
diff --git a/spa/plugins/alsa/alsa-pcm-sink.c b/spa/plugins/alsa/alsa-pcm-sink.c
index b26554b683c0f8e53a194ea0e589cbbf2e6ca4df..a0b5b4e6862571828872d54c9456eaf85725a53d 100644
--- a/spa/plugins/alsa/alsa-pcm-sink.c
+++ b/spa/plugins/alsa/alsa-pcm-sink.c
@@ -616,7 +616,7 @@ static int port_set_format(void *object,
 			   const struct spa_pod *format)
 {
 	struct state *this = object;
-	int err;
+	int err = 0;
 
 	if (format == NULL) {
 		if (!this->have_format)
@@ -673,7 +673,7 @@ static int port_set_format(void *object,
 	}
 	emit_port_info(this, false);
 
-	return 0;
+	return err;
 }
 
 static int
@@ -707,6 +707,7 @@ impl_node_port_set_param(void *object,
 		this->port_info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
 		this->port_params[PORT_Latency].user++;
 		emit_port_info(this, false);
+		res = 0;
 		break;
 	}
 	default:
@@ -797,38 +798,38 @@ static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t
 static int impl_node_process(void *object)
 {
 	struct state *this = object;
-	struct spa_io_buffers *input;
+	struct spa_io_buffers *io;
 
 	spa_return_val_if_fail(this != NULL, -EINVAL);
 
-	input = this->io;
-	spa_return_val_if_fail(input != NULL, -EIO);
+	if ((io = this->io) == NULL)
+		return -EIO;
 
-	spa_log_trace_fp(this->log, "%p: process %d %d/%d", this, input->status,
-			input->buffer_id, this->n_buffers);
+	spa_log_trace_fp(this->log, "%p: process %d %d/%d", this, io->status,
+			io->buffer_id, this->n_buffers);
 
 	if (this->position && this->position->clock.flags & SPA_IO_CLOCK_FLAG_FREEWHEEL) {
-		input->status = SPA_STATUS_NEED_DATA;
+		io->status = SPA_STATUS_NEED_DATA;
 		return SPA_STATUS_HAVE_DATA;
 	}
-	if (input->status == SPA_STATUS_HAVE_DATA &&
-	    input->buffer_id < this->n_buffers) {
-		struct buffer *b = &this->buffers[input->buffer_id];
+	if (io->status == SPA_STATUS_HAVE_DATA &&
+	    io->buffer_id < this->n_buffers) {
+		struct buffer *b = &this->buffers[io->buffer_id];
 
 		if (!SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_OUT)) {
 			spa_log_warn(this->log, "%p: buffer %u in use",
-					this, input->buffer_id);
-			input->status = -EINVAL;
+					this, io->buffer_id);
+			io->status = -EINVAL;
 			return -EINVAL;
 		}
-		spa_log_trace_fp(this->log, "%p: queue buffer %u", this, input->buffer_id);
+		spa_log_trace_fp(this->log, "%p: queue buffer %u", this, io->buffer_id);
 		spa_list_append(&this->ready, &b->link);
 		SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_OUT);
-		input->buffer_id = SPA_ID_INVALID;
+		io->buffer_id = SPA_ID_INVALID;
 
 		spa_alsa_write(this);
 
-		input->status = SPA_STATUS_OK;
+		io->status = SPA_STATUS_OK;
 	}
 	return SPA_STATUS_HAVE_DATA;
 }
diff --git a/spa/plugins/alsa/alsa-pcm-source.c b/spa/plugins/alsa/alsa-pcm-source.c
index c4222dc8912285c9e00240f1d95622324741f55b..a1e9690cf138002c2647f914239c02d0e414309b 100644
--- a/spa/plugins/alsa/alsa-pcm-source.c
+++ b/spa/plugins/alsa/alsa-pcm-source.c
@@ -566,7 +566,7 @@ static int port_set_format(void *object,
 			   uint32_t flags, const struct spa_pod *format)
 {
 	struct state *this = object;
-	int err;
+	int err = 0;
 
 	if (format == NULL) {
 		if (!this->have_format)
@@ -610,7 +610,7 @@ static int port_set_format(void *object,
 	}
 	emit_port_info(this, false);
 
-	return 0;
+	return err;
 }
 
 static int
@@ -753,8 +753,8 @@ static int impl_node_process(void *object)
 
 	spa_return_val_if_fail(this != NULL, -EINVAL);
 
-	io = this->io;
-	spa_return_val_if_fail(io != NULL, -EIO);
+	if ((io = this->io) == NULL)
+		return -EIO;
 
 	spa_log_trace_fp(this->log, "%p; status %d", this, io->status);
 
diff --git a/spa/plugins/alsa/alsa-pcm.c b/spa/plugins/alsa/alsa-pcm.c
index de321ee3cd36f3a20a4ae11800f1fef42cf27316..c171e5f888a8203d4c1473e090dab0c095127803 100644
--- a/spa/plugins/alsa/alsa-pcm.c
+++ b/spa/plugins/alsa/alsa-pcm.c
@@ -10,6 +10,7 @@
 
 #include <spa/pod/filter.h>
 #include <spa/utils/string.h>
+#include <spa/utils/result.h>
 #include <spa/support/system.h>
 #include <spa/utils/keys.h>
 
@@ -442,9 +443,31 @@ int spa_alsa_parse_prop_params(struct state *state, struct spa_pod *params)
 	return changed;
 }
 
+#define CHECK(s,msg,...) if ((err = (s)) < 0) { spa_log_error(state->log, msg ": %s", ##__VA_ARGS__, snd_strerror(err)); return err; }
+
+static ssize_t log_write(void *cookie, const char *buf, size_t size)
+{
+	struct state *state = cookie;
+	int len;
+
+	while (size > 0) {
+		len = strcspn(buf, "\n");
+		if (len > 0)
+			spa_log_debug(state->log, "%.*s", (int)len, buf);
+		buf += len + 1;
+		size -= len + 1;
+	}
+	return size;
+}
+
+static cookie_io_functions_t io_funcs = {
+	.write = log_write,
+};
+
 int spa_alsa_init(struct state *state, const struct spa_dict *info)
 {
 	uint32_t i;
+	int err;
 
 	snd_config_update_free_global();
 
@@ -481,20 +504,31 @@ int spa_alsa_init(struct state *state, const struct spa_dict *info)
 		spa_log_error(state->log, "can't create card %u", state->card_index);
 		return -errno;
 	}
+	state->log_file = fopencookie(state, "w", io_funcs);
+	if (state->log_file == NULL) {
+		spa_log_error(state->log, "can't create log file");
+		return -errno;
+	}
+	CHECK(snd_output_stdio_attach(&state->output, state->log_file, 0), "attach failed");
+
 	return 0;
 }
 
 int spa_alsa_clear(struct state *state)
 {
+	int err;
+
 	release_card(state->card);
 
 	state->card = NULL;
 	state->card_index = SPA_ID_INVALID;
 
-	return 0;
-}
+	if ((err = snd_output_close(state->output)) < 0)
+		spa_log_warn(state->log, "output close failed: %s", snd_strerror(err));
+	fclose(state->log_file);
 
-#define CHECK(s,msg,...) if ((err = (s)) < 0) { spa_log_error(state->log, msg ": %s", ##__VA_ARGS__, snd_strerror(err)); return err; }
+	return err;
+}
 
 int spa_alsa_open(struct state *state, const char *params)
 {
@@ -505,8 +539,6 @@ int spa_alsa_open(struct state *state, const char *params)
 	if (state->opened)
 		return 0;
 
-	CHECK(snd_output_stdio_attach(&state->output, stderr, 0), "attach failed");
-
 	spa_scnprintf(device_name, sizeof(device_name), "%s%s%s",
 			state->card->ucm_prefix ? state->card->ucm_prefix : "",
 			props->device, params ? params : "");
@@ -538,6 +570,8 @@ int spa_alsa_open(struct state *state, const char *params)
 	return 0;
 
 error_exit_close:
+	spa_log_info(state->log, "%p: Device '%s' closing: %s", state, state->props.device,
+			spa_strerror(err));
 	snd_pcm_close(state->hndl);
 	return err;
 }
@@ -556,9 +590,6 @@ int spa_alsa_close(struct state *state)
 		spa_log_warn(state->log, "%s: close failed: %s", state->props.device,
 				snd_strerror(err));
 
-	if ((err = snd_output_close(state->output)) < 0)
-		spa_log_warn(state->log, "output close failed: %s", snd_strerror(err));
-
 	spa_system_close(state->data_system, state->timerfd);
 
 	if (state->have_format)
@@ -752,7 +783,7 @@ static bool uint32_array_contains(uint32_t *vals, uint32_t n_vals, uint32_t val)
 }
 
 static int add_rate(struct state *state, uint32_t scale, bool all, uint32_t index, uint32_t *next,
-		snd_pcm_hw_params_t *params, struct spa_pod_builder *b)
+		uint32_t min_allowed_rate, snd_pcm_hw_params_t *params, struct spa_pod_builder *b)
 {
 	struct spa_pod_frame f[1];
 	int err, dir;
@@ -763,6 +794,12 @@ static int add_rate(struct state *state, uint32_t scale, bool all, uint32_t inde
 	CHECK(snd_pcm_hw_params_get_rate_min(params, &min, &dir), "get_rate_min");
 	CHECK(snd_pcm_hw_params_get_rate_max(params, &max, &dir), "get_rate_max");
 
+	spa_log_debug(state->log, "min:%u max:%u min-allowed:%u scale:%u all:%d",
+			min, max, min_allowed_rate, scale, all);
+
+	min_allowed_rate /= scale;
+	min = SPA_MAX(min_allowed_rate, min);
+
 	if (!state->multi_rate && state->card->format_ref > 0)
 		rate = state->card->rate;
 	else
@@ -779,6 +816,9 @@ static int add_rate(struct state *state, uint32_t scale, bool all, uint32_t inde
 
 	rate = SPA_CLAMP(rate, min, max);
 
+	spa_log_debug(state->log, "rate:%u multi:%d card:%d def:%d",
+			rate, state->multi_rate, state->card->rate, state->default_rate);
+
 	spa_pod_builder_prop(b, SPA_FORMAT_AUDIO_rate, 0);
 
 	spa_pod_builder_push_choice(b, &f[0], SPA_CHOICE_None, 0);
@@ -830,7 +870,8 @@ static int add_channels(struct state *state, bool all, uint32_t index, uint32_t
 
 	CHECK(snd_pcm_hw_params_get_channels_min(params, &min), "get_channels_min");
 	CHECK(snd_pcm_hw_params_get_channels_max(params, &max), "get_channels_max");
-	spa_log_debug(state->log, "channels (%d %d)", min, max);
+	spa_log_debug(state->log, "channels (%d %d) default:%d all:%d",
+			min, max, state->default_channels, all);
 
 	if (state->default_channels != 0 && !all) {
 		if (min < state->default_channels)
@@ -911,6 +952,14 @@ skip_channels:
 	return 1;
 }
 
+static void debug_hw_params(struct state *state, const char *prefix, snd_pcm_hw_params_t *params)
+{
+	if (SPA_UNLIKELY(spa_log_level_topic_enabled(state->log, SPA_LOG_TOPIC_DEFAULT, SPA_LOG_LEVEL_DEBUG))) {
+		spa_log_debug(state->log, "%s:", prefix);
+		snd_pcm_hw_params_dump(params, state->output);
+		fflush(state->log_file);
+	}
+}
 static int enum_pcm_formats(struct state *state, uint32_t index, uint32_t *next,
 		struct spa_pod **result, struct spa_pod_builder *b)
 {
@@ -928,6 +977,8 @@ static int enum_pcm_formats(struct state *state, uint32_t index, uint32_t *next,
 	snd_pcm_hw_params_alloca(&params);
 	CHECK(snd_pcm_hw_params_any(hndl, params), "Broken configuration: no configurations available");
 
+	debug_hw_params(state, __func__, params);
+
 	CHECK(snd_pcm_hw_params_set_rate_resample(hndl, params, 0), "set_rate_resample");
 
 	if (state->default_channels != 0) {
@@ -1017,7 +1068,7 @@ static int enum_pcm_formats(struct state *state, uint32_t index, uint32_t *next,
 		choice->body.type = SPA_CHOICE_Enum;
 	spa_pod_builder_pop(b, &f[1]);
 
-	if ((res = add_rate(state, 1, false, index & 0xffff, next, params, b)) != 1)
+	if ((res = add_rate(state, 1, false, index & 0xffff, next, 0, params, b)) != 1)
 		return res;
 
 	if ((res = add_channels(state, false, index & 0xffff, next, params, b)) != 1)
@@ -1074,6 +1125,8 @@ static int enum_iec958_formats(struct state *state, uint32_t index, uint32_t *ne
 	snd_pcm_hw_params_alloca(&params);
 	CHECK(snd_pcm_hw_params_any(hndl, params), "Broken configuration: no configurations available");
 
+	debug_hw_params(state, __func__, params);
+
 	CHECK(snd_pcm_hw_params_set_rate_resample(hndl, params, 0), "set_rate_resample");
 
 	spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat);
@@ -1110,7 +1163,7 @@ static int enum_iec958_formats(struct state *state, uint32_t index, uint32_t *ne
 	}
 	spa_pod_builder_pop(b, &f[1]);
 
-	if ((res = add_rate(state, 1, true, index & 0xffff, next, params, b)) != 1)
+	if ((res = add_rate(state, 1, true, index & 0xffff, next, 0, params, b)) != 1)
 		return res;
 
 	(*next)++;
@@ -1135,6 +1188,8 @@ static int enum_dsd_formats(struct state *state, uint32_t index, uint32_t *next,
 	snd_pcm_hw_params_alloca(&params);
 	CHECK(snd_pcm_hw_params_any(hndl, params), "Broken configuration: no configurations available");
 
+	debug_hw_params(state, __func__, params);
+
 	snd_pcm_format_mask_alloca(&fmask);
 	snd_pcm_hw_params_get_format_mask(params, fmask);
 
@@ -1165,7 +1220,14 @@ static int enum_dsd_formats(struct state *state, uint32_t index, uint32_t *next,
 	spa_pod_builder_prop(b, SPA_FORMAT_AUDIO_interleave, 0);
 	spa_pod_builder_int(b, interleave);
 
-	if ((res = add_rate(state, SPA_ABS(interleave), true, index & 0xffff, next, params, b)) != 1)
+	/* Use a lower rate limit of 352800 (= 44100 * 64 / 8). This is because in
+	 * PipeWire, DSD rates are given in bytes, not bits, so 352800 corresponds
+	 * to the bit rate of DSD64. (The "64" in DSD64 means "64 times the rate
+	 * of 44.1 kHz".) Some hardware may report rates lower than that, for example
+	 * 176400. This would correspond to "DSD32" (which does not exist). Trying
+	 * to use such a rate with DSD hardware does not work and may cause undefined
+	 * behavior in said hardware. */
+	if ((res = add_rate(state, SPA_ABS(interleave), true, index & 0xffff, next, 44100 * 64 / 8, params, b)) != 1)
 		return res;
 
 	if ((res = add_channels(state, true, index & 0xffff, next, params, b)) != 1)
@@ -1187,7 +1249,12 @@ spa_alsa_enum_format(struct state *state, int seq, uint32_t start, uint32_t num,
 	struct spa_result_node_params result;
 	uint32_t count = 0;
 
+	spa_log_debug(state->log, "opened:%d format:%d started:%d", state->opened,
+			state->have_format, state->started);
+
 	opened = state->opened;
+	if (!state->started && state->have_format)
+		spa_alsa_close(state);
 	if ((err = spa_alsa_open(state, NULL)) < 0)
 		return err;
 
@@ -1248,6 +1315,9 @@ int spa_alsa_set_format(struct state *state, struct spa_audio_info *fmt, uint32_
 	bool match = true, planar = false, is_batch;
 	char spdif_params[128] = "";
 
+	spa_log_debug(state->log, "opened:%d format:%d started:%d", state->opened,
+			state->have_format, state->started);
+
 	state->use_mmap = !state->disable_mmap;
 
 	switch (fmt->media_subtype) {
@@ -1360,6 +1430,8 @@ int spa_alsa_set_format(struct state *state, struct spa_audio_info *fmt, uint32_
 		return -EINVAL;
 	}
 
+	if (!state->started && state->have_format)
+		spa_alsa_close(state);
 	if ((err = spa_alsa_open(state, spdif_params)) < 0)
 		return err;
 
@@ -1368,6 +1440,9 @@ int spa_alsa_set_format(struct state *state, struct spa_audio_info *fmt, uint32_
 	snd_pcm_hw_params_alloca(&params);
 	/* choose all parameters */
 	CHECK(snd_pcm_hw_params_any(hndl, params), "Broken configuration for playback: no configurations available");
+
+	debug_hw_params(state, __func__, params);
+
 	/* set hardware resampling, no resample */
 	CHECK(snd_pcm_hw_params_set_rate_resample(hndl, params, 0), "set_rate_resample");
 
@@ -1409,7 +1484,10 @@ int spa_alsa_set_format(struct state *state, struct spa_audio_info *fmt, uint32_
 				state->props.device, rchannels, val);
 		if (!SPA_FLAG_IS_SET(flags, SPA_NODE_PARAM_FLAG_NEAREST))
 			return -EINVAL;
+		if (fmt->media_subtype != SPA_MEDIA_SUBTYPE_raw)
+			return -EINVAL;
 		rchannels = val;
+		fmt->info.raw.channels = rchannels;
 		match = false;
 	}
 
@@ -1429,7 +1507,10 @@ int spa_alsa_set_format(struct state *state, struct spa_audio_info *fmt, uint32_
 				state->props.device, rrate, val);
 		if (!SPA_FLAG_IS_SET(flags, SPA_NODE_PARAM_FLAG_NEAREST))
 			return -EINVAL;
+		if (fmt->media_subtype != SPA_MEDIA_SUBTYPE_raw)
+			return -EINVAL;
 		rrate = val;
+		fmt->info.raw.rate = rrate;
 		match = false;
 	}
 
@@ -1549,6 +1630,12 @@ static int set_swparams(struct state *state)
 	/* write the parameters to the playback device */
 	CHECK(snd_pcm_sw_params(hndl, params), "sw_params");
 
+	if (SPA_UNLIKELY(spa_log_level_topic_enabled(state->log, SPA_LOG_TOPIC_DEFAULT, SPA_LOG_LEVEL_DEBUG))) {
+		spa_log_debug(state->log, "state after sw_params:");
+		snd_pcm_dump(hndl, state->output);
+		fflush(state->log_file);
+	}
+
 	return 0;
 }
 
@@ -1762,6 +1849,7 @@ static int get_status(struct state *state, uint64_t current_time,
 		*delay = avail;
 		*target = SPA_MAX(*target, state->read_size);
 	}
+	*target = SPA_MIN(*target, state->buffer_frames);
 	return 0;
 }
 
@@ -1895,6 +1983,7 @@ int spa_alsa_write(struct state *state)
 	snd_pcm_uframes_t written, frames, offset, off, to_write, total_written, max_write;
 	snd_pcm_sframes_t commitres;
 	int res = 0;
+	size_t frame_size = state->frame_size;
 
 	check_position_config(state);
 
@@ -1951,61 +2040,43 @@ again:
 	written = 0;
 
 	while (!spa_list_is_empty(&state->ready) && to_write > 0) {
-		uint8_t *dst, *src;
 		size_t n_bytes, n_frames;
 		struct buffer *b;
 		struct spa_data *d;
-		uint32_t i, index, offs, avail, size, maxsize, l0, l1;
+		uint32_t i, offs, size, last_offset;
 
 		b = spa_list_first(&state->ready, struct buffer, link);
 		d = b->buf->datas;
 
-		size = d[0].chunk->size;
-		maxsize = d[0].maxsize;
+		offs = d[0].chunk->offset + state->ready_offset;
+		last_offset = d[0].chunk->size;
+		size = last_offset - state->ready_offset;
 
-		index = d[0].chunk->offset + state->ready_offset;
-		avail = size - state->ready_offset;
-		avail /= state->frame_size;
+		offs = SPA_MIN(offs, d[0].maxsize);
+		size = SPA_MIN(d[0].maxsize - offs, size);
 
-		n_frames = SPA_MIN(avail, to_write);
-		n_bytes = n_frames * state->frame_size;
-
-		offs = index % maxsize;
-		l0 = SPA_MIN(n_bytes, maxsize - offs);
-		l1 = n_bytes - l0;
+		n_frames = SPA_MIN(size / frame_size, to_write);
+		n_bytes = n_frames * frame_size;
 
 		if (SPA_LIKELY(state->use_mmap)) {
 			for (i = 0; i < b->buf->n_datas; i++) {
-				dst = SPA_PTROFF(my_areas[i].addr, off * state->frame_size, uint8_t);
-				src = d[i].data;
-
-				spa_memcpy(dst, src + offs, l0);
-				if (SPA_UNLIKELY(l1 > 0))
-					spa_memcpy(dst + l0, src, l1);
+				spa_memcpy(SPA_PTROFF(my_areas[i].addr, off * frame_size, void),
+						SPA_PTROFF(d[i].data, offs, void), n_bytes);
 			}
 		} else {
-			if (state->planar) {
-				void *bufs[b->buf->n_datas];
-
-				for (i = 0; i < b->buf->n_datas; i++)
-					bufs[i] = SPA_PTROFF(d[i].data, offs, void);
-				snd_pcm_writen(hndl, bufs, l0 / state->frame_size);
-				if (SPA_UNLIKELY(l1 > 0)) {
-					for (i = 0; i < b->buf->n_datas; i++)
-						bufs[i] = d[i].data;
-					snd_pcm_writen(hndl, bufs, l1 / state->frame_size);
-				}
-			} else {
-				src = d[0].data;
-				snd_pcm_writei(hndl, src + offs, l0 / state->frame_size);
-				if (SPA_UNLIKELY(l1 > 0))
-					snd_pcm_writei(hndl, src, l1 / state->frame_size);
-			}
+			void *bufs[b->buf->n_datas];
+			for (i = 0; i < b->buf->n_datas; i++)
+				bufs[i] = SPA_PTROFF(d[i].data, offs, void);
+
+			if (state->planar)
+				snd_pcm_writen(hndl, bufs, n_frames);
+			else
+				snd_pcm_writei(hndl, bufs[0], n_frames);
 		}
 
 		state->ready_offset += n_bytes;
 
-		if (state->ready_offset >= size) {
+		if (state->ready_offset >= last_offset) {
 			spa_list_remove(&b->link);
 			SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUT);
 			state->io->buffer_id = b->id;
@@ -2071,8 +2142,7 @@ push_frames(struct state *state,
 		spa_log_warn(state->log, "%s: no more buffers", state->props.device);
 		total_frames = frames;
 	} else {
-		uint8_t *src;
-		size_t n_bytes, left;
+		size_t n_bytes, left, frame_size = state->frame_size;
 		struct buffer *b;
 		struct spa_data *d;
 		uint32_t i, avail, l0, l1;
@@ -2088,23 +2158,26 @@ push_frames(struct state *state,
 
 		d = b->buf->datas;
 
-		avail = d[0].maxsize / state->frame_size;
+		avail = d[0].maxsize / frame_size;
 		total_frames = SPA_MIN(avail, frames);
-		n_bytes = total_frames * state->frame_size;
+		n_bytes = total_frames * frame_size;
 
 		if (my_areas) {
 			left = state->buffer_frames - offset;
-			l0 = SPA_MIN(n_bytes, left * state->frame_size);
+			l0 = SPA_MIN(n_bytes, left * frame_size);
 			l1 = n_bytes - l0;
 
 			for (i = 0; i < b->buf->n_datas; i++) {
-				src = SPA_PTROFF(my_areas[i].addr, offset * state->frame_size, uint8_t);
-				spa_memcpy(d[i].data, src, l0);
-				if (l1 > 0)
-					spa_memcpy(SPA_PTROFF(d[i].data, l0, void), my_areas[i].addr, l1);
+				spa_memcpy(d[i].data,
+						SPA_PTROFF(my_areas[i].addr, offset * frame_size, void),
+						l0);
+				if (SPA_UNLIKELY(l1 > 0))
+					spa_memcpy(SPA_PTROFF(d[i].data, l0, void),
+							my_areas[i].addr,
+							l1);
 				d[i].chunk->offset = 0;
 				d[i].chunk->size = n_bytes;
-				d[i].chunk->stride = state->frame_size;
+				d[i].chunk->stride = frame_size;
 			}
 		} else {
 			void *bufs[b->buf->n_datas];
@@ -2112,7 +2185,7 @@ push_frames(struct state *state,
 				bufs[i] = d[i].data;
 				d[i].chunk->offset = 0;
 				d[i].chunk->size = n_bytes;
-				d[i].chunk->stride = state->frame_size;
+				d[i].chunk->stride = frame_size;
 			}
 			if (state->planar) {
 				snd_pcm_readn(state->hndl, bufs, total_frames);
@@ -2350,7 +2423,7 @@ static void alsa_on_timeout_event(struct spa_source *source)
 	}
 
 #ifndef FASTPATH
-	if (SPA_UNLIKELY(spa_log_level_enabled(state->log, SPA_LOG_LEVEL_TRACE))) {
+	if (SPA_UNLIKELY(spa_log_level_topic_enabled(state->log, SPA_LOG_TOPIC_DEFAULT, SPA_LOG_LEVEL_TRACE))) {
 		struct timespec now;
 		uint64_t nsec;
 		if (spa_system_clock_gettime(state->data_system, CLOCK_MONOTONIC, &now) < 0)
@@ -2445,8 +2518,6 @@ int spa_alsa_start(struct state *state)
 			state->following, state->matching, state->resample);
 
 	CHECK(set_swparams(state), "swparams");
-	if (SPA_UNLIKELY(spa_log_level_enabled(state->log, SPA_LOG_LEVEL_DEBUG)))
-		snd_pcm_dump(state->hndl, state->output);
 
 	if ((err = snd_pcm_prepare(state->hndl)) < 0 && err != -EBUSY) {
 		spa_log_error(state->log, "%s: snd_pcm_prepare error: %s",
diff --git a/spa/plugins/alsa/alsa-pcm.h b/spa/plugins/alsa/alsa-pcm.h
index 6f16e36961beba10b3e1c1c13bbd2373fa4789f6..91385f40d05342b80937672dac4d19a142e2bf10 100644
--- a/spa/plugins/alsa/alsa-pcm.h
+++ b/spa/plugins/alsa/alsa-pcm.h
@@ -106,6 +106,8 @@ struct state {
 	struct spa_system *data_system;
 	struct spa_loop *data_loop;
 
+	FILE *log_file;
+
 	uint32_t card_index;
 	struct card *card;
 	snd_pcm_stream_t stream;
diff --git a/spa/plugins/alsa/alsa-seq.c b/spa/plugins/alsa/alsa-seq.c
index 55f84b5ca76d2d9b2071fa2416568be8daebdd23..381bbc5e10d3661f4a800939587ccfdd1e7d2078 100644
--- a/spa/plugins/alsa/alsa-seq.c
+++ b/spa/plugins/alsa/alsa-seq.c
@@ -131,13 +131,19 @@ static int seq_close(struct seq_state *state, struct seq_conn *conn)
 static int init_stream(struct seq_state *state, enum spa_direction direction)
 {
 	struct seq_stream *stream = &state->streams[direction];
+	int res;
 	stream->direction = direction;
 	if (direction == SPA_DIRECTION_INPUT) {
 		stream->caps = SND_SEQ_PORT_CAP_SUBS_WRITE;
 	} else {
 		stream->caps = SND_SEQ_PORT_CAP_SUBS_READ;
 	}
-	snd_midi_event_new(MAX_EVENT_SIZE, &stream->codec);
+	if ((res = snd_midi_event_new(MAX_EVENT_SIZE, &stream->codec)) < 0) {
+		spa_log_error(state->log, "can make event decoder: %s",
+				snd_strerror(res));
+		return res;
+	}
+	snd_midi_event_no_status(stream->codec, 1);
 	memset(stream->ports, 0, sizeof(stream->ports));
 	return 0;
 }
@@ -145,7 +151,9 @@ static int init_stream(struct seq_state *state, enum spa_direction direction)
 static int uninit_stream(struct seq_state *state, enum spa_direction direction)
 {
 	struct seq_stream *stream = &state->streams[direction];
-	snd_midi_event_free(stream->codec);
+	if (stream->codec)
+		snd_midi_event_free(stream->codec);
+	stream->codec = NULL;
 	return 0;
 }
 
@@ -178,7 +186,7 @@ static void init_ports(struct seq_state *state)
 
 static void debug_event(struct seq_state *state, snd_seq_event_t *ev)
 {
-	if (SPA_LIKELY(!spa_log_level_enabled(state->log, SPA_LOG_LEVEL_TRACE)))
+	if (SPA_LIKELY(!spa_log_level_topic_enabled(state->log, SPA_LOG_TOPIC_DEFAULT, SPA_LOG_LEVEL_TRACE)))
 		return;
 
 	spa_log_trace(state->log, "event type:%d flags:0x%x", ev->type, ev->flags);
@@ -543,12 +551,6 @@ static int process_read(struct seq_state *state)
 			continue;
 		}
 
-		/* fixup NoteOn with vel 0 */
-		if ((data[0] & 0xF0) == 0x90 && data[2] == 0x00) {
-			data[0] = 0x80 + (data[0] & 0x0F);
-			data[2] = 0x40;
-		}
-
 		/* queue_time is the estimated current time of the queue as calculated by
 		 * the DLL. Calculate the age of the event. */
 		ev_time = SPA_TIMESPEC_TO_NSEC(&ev->time.time);
diff --git a/spa/plugins/alsa/alsa-udev.c b/spa/plugins/alsa/alsa-udev.c
index 8501ff642a6b086f11bcd741e6261434271babf2..8ee217d9dff3e380769770e81b7227d686e5a08b 100644
--- a/spa/plugins/alsa/alsa-udev.c
+++ b/spa/plugins/alsa/alsa-udev.c
@@ -478,11 +478,10 @@ static int emit_object_info(struct impl *this, struct device *device)
 		items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_SUBSYSTEM, str);
 	}
 	if ((str = udev_device_get_property_value(dev, "ID_VENDOR_ID")) && *str) {
-		char *dec = alloca(6); /* 65535 is max */
 		int32_t val;
-
 		if (spa_atoi32(str, &val, 16)) {
-			snprintf(dec, 6, "%d", val);
+			char *dec = alloca(12); /* 0xffffffff is max */
+			snprintf(dec, 12, "0x%04x", val);
 			items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_VENDOR_ID, dec);
 		}
 	}
@@ -501,11 +500,10 @@ static int emit_object_info(struct impl *this, struct device *device)
 		items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_VENDOR_NAME, str);
 	}
 	if ((str = udev_device_get_property_value(dev, "ID_MODEL_ID")) && *str) {
-		char *dec = alloca(6); /* 65535 is max */
 		int32_t val;
-
 		if (spa_atoi32(str, &val, 16)) {
-			snprintf(dec, 6, "%d", val);
+			char *dec = alloca(12); /* 0xffffffff is max */
+			snprintf(dec, 12, "0x%04x", val);
 			items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_PRODUCT_ID, dec);
 		}
 	}
@@ -540,11 +538,35 @@ static int emit_object_info(struct impl *this, struct device *device)
 
 static bool check_access(struct impl *this, struct device *device)
 {
-	char path[128];
-	bool accessible;
+	char path[128], prefix[32];
+	DIR *snd = NULL;
+	struct dirent *entry;
+	bool accessible = false;
 
 	snprintf(path, sizeof(path), "/dev/snd/controlC%u", device->id);
-	accessible = access(path, R_OK|W_OK) >= 0;
+	if (access(path, R_OK|W_OK) >= 0 && (snd = opendir("/dev/snd"))) {
+		/*
+		 * It's possible that controlCX is accessible before pcmCX* or
+		 * the other way around. Return true only if all devices are
+                 * accessible.
+		 */
+
+		accessible = true;
+		spa_scnprintf(prefix, sizeof(prefix), "pcmC%uD", device->id);
+		while ((entry = readdir(snd)) != NULL) {
+			if (!(entry->d_type == DT_CHR &&
+					spa_strstartswith(entry->d_name, prefix)))
+				continue;
+
+			snprintf(path, sizeof(path), "/dev/snd/%.32s", entry->d_name);
+			if (access(path, R_OK|W_OK) < 0) {
+				accessible = false;
+				break;
+			}
+		}
+		closedir(snd);
+	}
+
 	if (accessible != device->accessible)
 		spa_log_debug(this->log, "%s accessible:%u", path, accessible);
 	device->accessible = accessible;
@@ -657,10 +679,6 @@ static void impl_on_notify_events(struct spa_source *source)
 			/* Device becomes accessible or not busy */
 			if ((event->mask & (IN_ATTRIB | IN_CLOSE_WRITE))) {
 				bool access;
-
-				if ((event->mask & IN_ATTRIB) &&
-						spa_strstartswith(event->name, "pcm"))
-					continue;
 				if (sscanf(event->name, "controlC%u", &id) != 1 &&
 						sscanf(event->name, "pcmC%uD", &id) != 1)
 					continue;
diff --git a/spa/plugins/alsa/meson.build b/spa/plugins/alsa/meson.build
index 0584139876ac15c395adb459b314468d098ad84f..12bb5b9f4aa098a92190eb7f6f5c6a524e6d099d 100644
--- a/spa/plugins/alsa/meson.build
+++ b/spa/plugins/alsa/meson.build
@@ -34,13 +34,18 @@ executable('spa-acp-tool',
   install : true,
 )
 
-
 executable('test-timer',
   [ 'test-timer.c' ],
   dependencies : [ spa_dep, alsa_dep, mathlib, epoll_shim_dep ],
   install : false,
 )
 
+executable('test-hw-params',
+  [ 'test-hw-params.c' ],
+  dependencies : [ spa_dep, alsa_dep, mathlib ],
+  install : false,
+)
+
 if libudev_dep.found()
   install_data(alsa_udevrules,
     install_dir : udevrulesdir,
diff --git a/spa/plugins/alsa/mixer/profile-sets/analog-only.conf b/spa/plugins/alsa/mixer/profile-sets/analog-only.conf
new file mode 100644
index 0000000000000000000000000000000000000000..badd5ec0c11681e5e0fa8dc0fd1c9ed57e05caca
--- /dev/null
+++ b/spa/plugins/alsa/mixer/profile-sets/analog-only.conf
@@ -0,0 +1,102 @@
+# PulseAudio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 2.1 of the
+# License, or (at your option) any later version.
+#
+# PulseAudio is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+
+; Some USB DACs appear to support IEC958, but don't physically have any
+; digital outputs.
+
+[General]
+auto-profiles = yes
+
+[Mapping analog-stereo]
+device-strings = front:%f
+channel-map = left,right
+paths-output = analog-output analog-output-lineout analog-output-speaker analog-output-headphones analog-output-headphones-2
+paths-input = analog-input-front-mic analog-input-rear-mic analog-input-internal-mic analog-input-dock-mic analog-input analog-input-mic analog-input-linein analog-input-aux analog-input-video analog-input-tvtuner analog-input-fm analog-input-mic-line analog-input-headphone-mic analog-input-headset-mic
+priority = 15
+
+# If everything else fails, try to use hw:0 as a stereo device...
+[Mapping stereo-fallback]
+device-strings = hw:%f
+fallback = yes
+channel-map = front-left,front-right
+paths-output = analog-output analog-output-lineout analog-output-speaker analog-output-headphones analog-output-headphones-2
+paths-input = analog-input-front-mic analog-input-rear-mic analog-input-internal-mic analog-input-dock-mic analog-input analog-input-mic analog-input-linein analog-input-aux analog-input-video analog-input-tvtuner analog-input-fm analog-input-mic-line analog-input-headphone-mic analog-input-headset-mic
+priority = 1
+
+# ...and if even that fails, try to use hw:0 as a mono device.
+[Mapping mono-fallback]
+device-strings = hw:%f
+fallback = yes
+channel-map = mono
+paths-output = analog-output analog-output-lineout analog-output-speaker analog-output-headphones analog-output-headphones-2 analog-output-mono
+paths-input = analog-input-front-mic analog-input-rear-mic analog-input-internal-mic analog-input-dock-mic analog-input analog-input-mic analog-input-linein analog-input-aux analog-input-video analog-input-tvtuner analog-input-fm analog-input-mic-line analog-input-headset-mic
+priority = 1
+
+[Mapping analog-surround-21]
+device-strings = surround21:%f
+channel-map = front-left,front-right,lfe
+paths-input = analog-input analog-input-linein analog-input-mic
+paths-output = analog-output analog-output-lineout analog-output-speaker
+priority = 13
+
+[Mapping analog-surround-40]
+device-strings = surround40:%f
+channel-map = front-left,front-right,rear-left,rear-right
+paths-input = analog-input analog-input-linein analog-input-mic
+paths-output = analog-output analog-output-lineout analog-output-speaker
+priority = 12
+
+[Mapping analog-surround-41]
+device-strings = surround41:%f
+channel-map = front-left,front-right,rear-left,rear-right,lfe
+paths-input = analog-input analog-input-linein analog-input-mic
+paths-output = analog-output analog-output-lineout analog-output-speaker
+priority = 13
+
+[Mapping analog-surround-50]
+device-strings = surround50:%f
+channel-map = front-left,front-right,rear-left,rear-right,front-center
+paths-input = analog-input analog-input-linein analog-input-mic
+paths-output = analog-output analog-output-lineout analog-output-speaker
+priority = 12
+
+[Mapping analog-surround-51]
+device-strings = surround51:%f
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+paths-input = analog-input analog-input-linein analog-input-mic
+paths-output = analog-output analog-output-lineout analog-output-speaker
+priority = 13
+
+[Mapping analog-surround-71]
+device-strings = surround71:%f
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe,side-left,side-right
+description = Analog Surround 7.1
+paths-input = analog-input analog-input-linein analog-input-mic
+paths-output = analog-output analog-output-lineout analog-output-speaker
+priority = 12
+
+[Mapping multichannel-output]
+device-strings = hw:%f
+channel-map = left,right,rear-left,rear-right
+exact-channels = false
+fallback = yes
+priority = 1
+direction = output
+
+[Mapping multichannel-input]
+device-strings = hw:%f
+channel-map = left,right,rear-left,rear-right
+exact-channels = false
+fallback = yes
+priority = 1
+direction = input
diff --git a/spa/plugins/alsa/test-hw-params.c b/spa/plugins/alsa/test-hw-params.c
new file mode 100644
index 0000000000000000000000000000000000000000..7315061232ecd1fcbb732c52693d307ae9d26bad
--- /dev/null
+++ b/spa/plugins/alsa/test-hw-params.c
@@ -0,0 +1,173 @@
+/* Spa
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdio.h>
+#include <stdbool.h>
+#include <limits.h>
+#include <getopt.h>
+#include <math.h>
+
+#include <alsa/asoundlib.h>
+
+#include <spa/utils/defs.h>
+
+#define DEFAULT_DEVICE	"default"
+
+
+struct state {
+	const char *device;
+	snd_output_t *output;
+	snd_pcm_t *hndl;
+};
+
+#define CHECK(s,msg,...) {		\
+	int __err;			\
+	if ((__err = (s)) < 0) {	\
+		fprintf(stderr, msg ": %s\n", ##__VA_ARGS__, snd_strerror(__err));	\
+		return __err;		\
+	}				\
+}
+
+static const char *get_class(snd_pcm_class_t c)
+{
+	switch (c) {
+	case SND_PCM_CLASS_GENERIC:
+		return "generic";
+	case SND_PCM_CLASS_MULTI:
+		return "multichannel";
+	case SND_PCM_CLASS_MODEM:
+		return "modem";
+	case SND_PCM_CLASS_DIGITIZER:
+		return "digitizer";
+	default:
+		return "unknown";
+	}
+}
+
+static const char *get_subclass(snd_pcm_subclass_t c)
+{
+	switch (c) {
+	case SND_PCM_SUBCLASS_GENERIC_MIX:
+		return "generic-mix";
+	case SND_PCM_SUBCLASS_MULTI_MIX:
+		return "multichannel-mix";
+	default:
+		return "unknown";
+	}
+}
+
+static void show_help(const char *name, bool error)
+{
+        fprintf(error ? stderr : stdout, "%s [options]\n"
+		"  -h, --help                            Show this help\n"
+		"  -D, --device                          device name (default '%s')\n"
+		"  -C, --capture                         capture mode (default playback)\n",
+		name, DEFAULT_DEVICE);
+}
+
+int main(int argc, char *argv[])
+{
+	struct state state = { 0, };
+	snd_pcm_hw_params_t *hparams;
+	snd_pcm_info_t *info;
+	snd_pcm_sync_id_t sync;
+	snd_pcm_stream_t stream = SND_PCM_STREAM_PLAYBACK;
+	snd_pcm_chmap_query_t **maps;
+	int c, i;
+	static const struct option long_options[] = {
+		{ "help",	no_argument,		NULL, 'h' },
+		{ "device",	required_argument,	NULL, 'D' },
+		{ "capture",	no_argument,		NULL, 'C' },
+		{ NULL, 0, NULL, 0}
+	};
+	state.device = DEFAULT_DEVICE;
+
+	while ((c = getopt_long(argc, argv, "hD:C", long_options, NULL)) != -1) {
+		switch (c) {
+		case 'h':
+			show_help(argv[0], false);
+			return 0;
+		case 'D':
+			state.device = optarg;
+			break;
+		case 'C':
+			stream = SND_PCM_STREAM_CAPTURE;
+			break;
+		default:
+			show_help(argv[0], true);
+			return -1;
+		}
+	}
+
+	CHECK(snd_output_stdio_attach(&state.output, stdout, 0), "attach failed");
+
+	fprintf(stdout, "opening device: '%s'\n", state.device);
+
+	CHECK(snd_pcm_open(&state.hndl, state.device, stream, 0),
+			"open %s failed", state.device);
+
+	snd_pcm_info_alloca(&info);
+	snd_pcm_info(state.hndl, info);
+
+	fprintf(stdout, "info:\n");
+	fprintf(stdout, "  device: %u\n", snd_pcm_info_get_device(info));
+	fprintf(stdout, "  subdevice: %u\n", snd_pcm_info_get_subdevice(info));
+	fprintf(stdout, "  stream: %s\n", snd_pcm_stream_name(snd_pcm_info_get_stream(info)));
+	fprintf(stdout, "  card: %d\n", snd_pcm_info_get_card(info));
+	fprintf(stdout, "  id: '%s'\n", snd_pcm_info_get_id(info));
+	fprintf(stdout, "  name: '%s'\n", snd_pcm_info_get_name(info));
+	fprintf(stdout, "  subdevice name: '%s'\n", snd_pcm_info_get_subdevice_name(info));
+	fprintf(stdout, "  class: %s\n", get_class(snd_pcm_info_get_class(info)));
+	fprintf(stdout, "  subclass: %s\n", get_subclass(snd_pcm_info_get_subclass(info)));
+	fprintf(stdout, "  subdevice count: %u\n", snd_pcm_info_get_subdevices_count(info));
+	fprintf(stdout, "  subdevice avail: %u\n", snd_pcm_info_get_subdevices_avail(info));
+	sync = snd_pcm_info_get_sync(info);
+	fprintf(stdout, "  sync: %08x:%08x:%08x:%08x\n",
+			sync.id32[0], sync.id32[1], sync.id32[2],sync.id32[3]);
+
+	/* channel maps */
+	if ((maps = snd_pcm_query_chmaps(state.hndl)) != NULL) {
+		fprintf(stdout, "channels:\n");
+
+		for (i = 0; maps[i]; i++) {
+			snd_pcm_chmap_t* map = &maps[i]->map;
+			char buf[2048];
+
+			snd_pcm_chmap_print(map, sizeof(buf), buf);
+
+			fprintf(stdout, "  %d: %s\n", map->channels, buf);
+		}
+		snd_pcm_free_chmaps(maps);
+	}
+
+	/* hw params */
+	snd_pcm_hw_params_alloca(&hparams);
+	snd_pcm_hw_params_any(state.hndl, hparams);
+
+	snd_pcm_hw_params_dump(hparams, state.output);
+
+	snd_pcm_close(state.hndl);
+
+	return EXIT_SUCCESS;
+}
diff --git a/spa/plugins/audioconvert/audioadapter.c b/spa/plugins/audioconvert/audioadapter.c
index eeeb5d1fbba11a062dcb218ba1a064a77a5f2721..dcaed9ad3d8ee32644b0a7929b35eb88ecef4f1e 100644
--- a/spa/plugins/audioconvert/audioadapter.c
+++ b/spa/plugins/audioconvert/audioadapter.c
@@ -49,7 +49,7 @@ static struct spa_log_topic *log_topic = &SPA_LOG_TOPIC(0, "spa.audioadapter");
 
 #define DEFAULT_ALIGN	16
 
-#define MAX_PORTS	SPA_AUDIO_MAX_CHANNELS
+#define MAX_PORTS	(SPA_AUDIO_MAX_CHANNELS+1)
 
 /** \cond */
 
@@ -442,6 +442,29 @@ static int configure_format(struct impl *this, uint32_t flags, const struct spa_
 	if (format && spa_log_level_enabled(this->log, SPA_LOG_LEVEL_DEBUG))
 		spa_debug_format(0, NULL, format);
 
+	if ((res = spa_node_port_set_param(this->follower,
+					   this->direction, 0,
+					   SPA_PARAM_Format, flags,
+					   format)) < 0)
+			return res;
+	if (res > 0) {
+		uint8_t buffer[4096];
+		struct spa_pod_builder b = { 0 };
+		uint32_t state = 0;
+		struct spa_pod *fmt;
+
+		/* format was changed to nearest compatible format */
+		spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+		if ((res = spa_node_port_enum_params_sync(this->follower,
+					this->direction, 0,
+					SPA_PARAM_Format, &state,
+					NULL, &fmt, &b)) != 1)
+			return -EIO;
+
+		format = fmt;
+	}
+
 	if (this->target != this->follower && this->convert) {
 		if ((res = spa_node_port_set_param(this->convert,
 					   SPA_DIRECTION_REVERSE(this->direction), 0,
@@ -450,12 +473,6 @@ static int configure_format(struct impl *this, uint32_t flags, const struct spa_
 				return res;
 	}
 
-	if ((res = spa_node_port_set_param(this->follower,
-					   this->direction, 0,
-					   SPA_PARAM_Format, flags,
-					   format)) < 0)
-			return res;
-
 	this->have_format = format != NULL;
 	if (format == NULL) {
 		this->n_buffers = 0;
@@ -494,7 +511,7 @@ static int reconfigure_mode(struct impl *this, bool passthrough,
 	int res = 0;
 	struct spa_hook l;
 
-	spa_log_debug(this->log, "%p: passthrough mode %d", this, passthrough);
+	spa_log_info(this->log, "%p: passthrough mode %d", this, passthrough);
 
 	if (this->passthrough != passthrough) {
 		if (passthrough) {
@@ -513,7 +530,7 @@ static int reconfigure_mode(struct impl *this, bool passthrough,
 	/* set new target */
 	this->target = passthrough ? this->follower : this->convert;
 
-	if ((res = configure_format(this, 0, format)) < 0)
+	if ((res = configure_format(this, SPA_NODE_PARAM_FLAG_NEAREST, format)) < 0)
 		return res;
 
 	if (this->passthrough != passthrough) {
@@ -595,7 +612,6 @@ static int impl_node_set_param(void *object, uint32_t id, uint32_t flags,
 		enum spa_direction dir;
 		enum spa_param_port_config_mode mode;
 		struct spa_pod *format = NULL;
-		int monitor = false;
 
 		if (this->started) {
 			spa_log_error(this->log, "was started");
@@ -606,7 +622,6 @@ static int impl_node_set_param(void *object, uint32_t id, uint32_t flags,
 				SPA_TYPE_OBJECT_ParamPortConfig, NULL,
 				SPA_PARAM_PORT_CONFIG_direction,	SPA_POD_Id(&dir),
 				SPA_PARAM_PORT_CONFIG_mode,		SPA_POD_Id(&mode),
-				SPA_PARAM_PORT_CONFIG_monitor,		SPA_POD_OPT_Bool(&monitor),
 				SPA_PARAM_PORT_CONFIG_format,		SPA_POD_OPT_Pod(&format)) < 0)
 			return -EINVAL;
 
@@ -777,7 +792,7 @@ static int negotiate_format(struct impl *this)
 
 	spa_pod_fixate(format);
 
-	res = configure_format(this, 0, format);
+	res = configure_format(this, SPA_NODE_PARAM_FLAG_NEAREST, format);
 
 done:
 	spa_node_send_command(this->follower,
@@ -852,6 +867,12 @@ static void convert_node_info(void *data, const struct spa_node_info *info)
 			uint32_t idx;
 
 			switch (info->params[i].id) {
+			case SPA_PARAM_EnumPortConfig:
+				idx = IDX_EnumPortConfig;
+				break;
+			case SPA_PARAM_PortConfig:
+				idx = IDX_PortConfig;
+				break;
 			case SPA_PARAM_PropInfo:
 				idx = IDX_PropInfo;
 				break;
@@ -1132,8 +1153,21 @@ static int follower_ready(void *data, int status)
 	if (this->target != this->follower) {
 		this->driver = true;
 
-		if (this->direction == SPA_DIRECTION_OUTPUT)
-			status = spa_node_process(this->convert);
+		if (this->direction == SPA_DIRECTION_OUTPUT) {
+			int retry = 8;
+			while (retry--) {
+				status = spa_node_process(this->convert);
+				if (status & SPA_STATUS_HAVE_DATA)
+					break;
+
+				if (status & SPA_STATUS_NEED_DATA) {
+					status = spa_node_process(this->follower);
+					if (!(status & SPA_STATUS_HAVE_DATA))
+						break;
+				}
+			}
+
+		}
 	}
 
 	return spa_node_call_ready(&this->callbacks, status);
diff --git a/spa/plugins/audioconvert/audioconvert.c b/spa/plugins/audioconvert/audioconvert.c
index 02c7bc1a99bcc56c8aa0041850d473c5dd5f5b4a..7873efdb0436ef22488cbb6646b5401dd18bc921 100644
--- a/spa/plugins/audioconvert/audioconvert.c
+++ b/spa/plugins/audioconvert/audioconvert.c
@@ -1,6 +1,6 @@
 /* Spa
  *
- * Copyright © 2018 Wim Taymans
+ * Copyright © 2022 Wim Taymans
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
@@ -25,52 +25,153 @@
 #include <errno.h>
 #include <string.h>
 #include <stdio.h>
+#include <limits.h>
 
 #include <spa/support/plugin.h>
-#include <spa/support/log.h>
 #include <spa/support/cpu.h>
+#include <spa/support/log.h>
 #include <spa/utils/result.h>
 #include <spa/utils/list.h>
 #include <spa/utils/names.h>
 #include <spa/utils/string.h>
 #include <spa/node/node.h>
-#include <spa/buffer/alloc.h>
 #include <spa/node/io.h>
 #include <spa/node/utils.h>
+#include <spa/node/keys.h>
 #include <spa/param/audio/format-utils.h>
 #include <spa/param/param.h>
+#include <spa/param/latency-utils.h>
 #include <spa/pod/filter.h>
-#include <spa/debug/pod.h>
 #include <spa/debug/types.h>
+#include <spa/debug/pod.h>
+
+#include "volume-ops.h"
+#include "fmt-ops.h"
+#include "channelmix-ops.h"
+#include "resample.h"
 
 #undef SPA_LOG_TOPIC_DEFAULT
 #define SPA_LOG_TOPIC_DEFAULT log_topic
 static struct spa_log_topic *log_topic = &SPA_LOG_TOPIC(0, "spa.audioconvert");
 
-#define DEFAULT_ALIGN	16
+#define DEFAULT_RATE		48000
+#define DEFAULT_CHANNELS	2
+
+#define MAX_ALIGN	FMT_OPS_MAX_ALIGN
+#define MAX_BUFFERS	32
+#define MAX_DATAS	SPA_AUDIO_MAX_CHANNELS
+#define MAX_PORTS	(SPA_AUDIO_MAX_CHANNELS+1)
+
+#define DEFAULT_MUTE	false
+#define DEFAULT_VOLUME	VOLUME_NORM
+
+struct volumes {
+	bool mute;
+	uint32_t n_volumes;
+	float volumes[SPA_AUDIO_MAX_CHANNELS];
+};
+
+static void init_volumes(struct volumes *vol)
+{
+	uint32_t i;
+	vol->mute = DEFAULT_MUTE;
+	vol->n_volumes = 0;
+	for (i = 0; i < SPA_AUDIO_MAX_CHANNELS; i++)
+		vol->volumes[i] = DEFAULT_VOLUME;
+}
+
+struct props {
+	float volume;
+	uint32_t n_channels;
+	uint32_t channel_map[SPA_AUDIO_MAX_CHANNELS];
+	struct volumes channel;
+	struct volumes soft;
+	struct volumes monitor;
+	unsigned int have_soft_volume:1;
+	unsigned int mix_disabled:1;
+	unsigned int resample_quality;
+	unsigned int resample_disabled:1;
+	double rate;
+};
 
-#define MAX_PORTS	SPA_AUDIO_MAX_CHANNELS
+static void props_reset(struct props *props)
+{
+	uint32_t i;
+	props->volume = DEFAULT_VOLUME;
+	props->n_channels = 0;
+	for (i = 0; i < SPA_AUDIO_MAX_CHANNELS; i++)
+		props->channel_map[i] = SPA_AUDIO_CHANNEL_UNKNOWN;
+	init_volumes(&props->channel);
+	init_volumes(&props->soft);
+	init_volumes(&props->monitor);
+	props->mix_disabled = false;
+	props->rate = 1.0;
+	props->resample_quality = RESAMPLE_DEFAULT_QUALITY;
+	props->resample_disabled = false;
+}
 
 struct buffer {
-	struct spa_list link;
-#define BUFFER_FLAG_OUT		(1 << 0)
+	uint32_t id;
+#define BUFFER_FLAG_QUEUED	(1<<0)
 	uint32_t flags;
-	struct spa_buffer *outbuf;
-	struct spa_meta_header *h;
+	struct spa_list link;
+	struct spa_buffer *buf;
+	void *datas[MAX_DATAS];
 };
 
-struct link {
-	struct spa_node *out_node;
-	uint32_t out_port;
-	uint32_t out_flags;
-	struct spa_node *in_node;
-	uint32_t in_port;
-	uint32_t in_flags;
-	struct spa_io_buffers io;
-	uint32_t min_buffers;
+struct port {
+	uint32_t direction;
+	uint32_t id;
+
+	struct spa_io_buffers *io;
+
+	uint64_t info_all;
+	struct spa_port_info info;
+#define IDX_EnumFormat	0
+#define IDX_Meta	1
+#define IDX_IO		2
+#define IDX_Format	3
+#define IDX_Buffers	4
+#define IDX_Latency	5
+#define N_PORT_PARAMS	6
+	struct spa_param_info params[N_PORT_PARAMS];
+	char position[16];
+
+	struct buffer buffers[MAX_BUFFERS];
 	uint32_t n_buffers;
-	struct spa_buffer **buffers;
-	unsigned int negotiated:1;
+
+	struct spa_audio_info format;
+	unsigned int have_format:1;
+	unsigned int is_dsp:1;
+	unsigned int is_monitor:1;
+	unsigned int is_control:1;
+
+	uint32_t blocks;
+	uint32_t stride;
+
+	const struct spa_pod_sequence *ctrl;
+	uint32_t ctrl_offset;
+
+	struct spa_list queue;
+};
+
+struct dir {
+	struct port *ports[MAX_PORTS];
+	uint32_t n_ports;
+
+	enum spa_param_port_config_mode mode;
+
+	struct spa_audio_info format;
+	unsigned int have_format:1;
+	unsigned int have_profile:1;
+	struct spa_latency_info latency;
+
+	uint32_t remap[MAX_PORTS];
+
+	struct convert conv;
+	unsigned int need_remap:1;
+	unsigned int is_passthrough:1;
+	unsigned int control:1;
 };
 
 struct impl {
@@ -80,9 +181,15 @@ struct impl {
 	struct spa_log *log;
 	struct spa_cpu *cpu;
 
+	uint32_t cpu_flags;
 	uint32_t max_align;
+	uint32_t quantum_limit;
+	enum spa_direction direction;
 
-	struct spa_hook_list hooks;
+	struct props props;
+
+	struct spa_io_position *io_position;
+	struct spa_io_rate_match *io_rate_match;
 
 	uint64_t info_all;
 	struct spa_node_info info;
@@ -90,52 +197,48 @@ struct impl {
 #define IDX_PortConfig		1
 #define IDX_PropInfo		2
 #define IDX_Props		3
-	struct spa_param_info params[4];
-	uint32_t param_flags[4];
-
-	int n_links;
-	struct link links[8];
-	int n_nodes;
-	struct spa_node *nodes[8];
+#define N_NODE_PARAMS		4
+	struct spa_param_info params[N_NODE_PARAMS];
 
-	enum spa_param_port_config_mode mode[2];
-	bool fmt_removing[2];
-
-	struct spa_handle *hnd_merger;
-	struct spa_handle *hnd_convert_in;
-	struct spa_handle *hnd_channelmix;
-	struct spa_handle *hnd_resample;
-	struct spa_handle *hnd_convert_out;
-	struct spa_handle *hnd_splitter;
-
-	struct spa_node *merger;
-	struct spa_node *convert_in;
-	struct spa_node *channelmix;
-	struct spa_node *resample;
-	struct spa_node *convert_out;
-	struct spa_node *splitter;
+	struct spa_hook_list hooks;
 
-	struct spa_node *fmt[2];
-	struct spa_hook fmt_listener[2];
-	bool have_fmt_listener[2];
+	unsigned int monitor:1;
+	unsigned int monitor_channel_volumes:1;
 
-	struct spa_hook listener[2];
+	struct dir dir[2];
+	struct channelmix mix;
+	struct resample resample;
+	struct volume volume;
+	double rate_scale;
 
+	uint32_t in_offset;
+	uint32_t out_offset;
 	unsigned int started:1;
-	unsigned int add_listener:1;
+	unsigned int peaks:1;
+	unsigned int is_passthrough:1;
+	unsigned int drained:1;
+
+	uint32_t empty_size;
+	float *empty;
+	float *scratch;
+	float *tmp[2];
+	float *tmp_datas[2][MAX_PORTS];
 };
 
-#define IS_MONITOR_PORT(this,dir,port_id) (dir == SPA_DIRECTION_OUTPUT && port_id > 0 &&	\
-		this->mode[SPA_DIRECTION_INPUT] == SPA_PARAM_PORT_CONFIG_MODE_dsp &&		\
-		this->mode[SPA_DIRECTION_OUTPUT] != SPA_PARAM_PORT_CONFIG_MODE_dsp)
+#define CHECK_PORT(this,d,p)		((p) < this->dir[d].n_ports)
+#define GET_PORT(this,d,p)		(this->dir[d].ports[p])
+#define GET_IN_PORT(this,p)		GET_PORT(this,SPA_DIRECTION_INPUT,p)
+#define GET_OUT_PORT(this,p)		GET_PORT(this,SPA_DIRECTION_OUTPUT,p)
+
+#define PORT_IS_DSP(this,d,p)		(GET_PORT(this,d,p)->is_dsp)
+#define PORT_IS_CONTROL(this,d,p)	(GET_PORT(this,d,p)->is_control)
+
+static void set_volume(struct impl *this);
 
 static void emit_node_info(struct impl *this, bool full)
 {
-	uint32_t i;
 	uint64_t old = full ? this->info.change_mask : 0;
-
-	if (this->add_listener)
-		return;
+	uint32_t i;
 
 	if (full)
 		this->info.change_mask = this->info_all;
@@ -153,344 +256,103 @@ static void emit_node_info(struct impl *this, bool full)
 	}
 }
 
-static int make_link(struct impl *this,
-		struct spa_node *out_node, uint32_t out_port,
-		struct spa_node *in_node, uint32_t in_port, uint32_t min_buffers)
-{
-	struct link *l = &this->links[this->n_links++];
-
-	l->out_node = out_node;
-	l->out_port = out_port;
-	l->out_flags = 0;
-	l->in_node = in_node;
-	l->in_port = in_port;
-	l->in_flags = 0;
-	l->negotiated = false;
-	l->io = SPA_IO_BUFFERS_INIT;
-	l->n_buffers = 0;
-	l->min_buffers = min_buffers;
-
-	spa_node_port_set_io(out_node,
-			     SPA_DIRECTION_OUTPUT, out_port,
-			     SPA_IO_Buffers,
-			     &l->io, sizeof(l->io));
-	spa_node_port_set_io(in_node,
-			     SPA_DIRECTION_INPUT, in_port,
-			     SPA_IO_Buffers,
-			     &l->io, sizeof(l->io));
-	return 0;
-}
-
-static void clean_link(struct impl *this, struct link *link)
-{
-	spa_node_port_set_param(link->in_node,
-				SPA_DIRECTION_INPUT, link->in_port,
-				SPA_PARAM_Format, 0, NULL);
-	spa_node_port_set_param(link->out_node,
-				SPA_DIRECTION_OUTPUT, link->out_port,
-				SPA_PARAM_Format, 0, NULL);
-	if (link->buffers)
-		free(link->buffers);
-	link->buffers = NULL;
-}
-
-static int debug_params(struct impl *this, struct spa_node *node,
-		enum spa_direction direction, uint32_t port_id, uint32_t id, struct spa_pod *filter)
-{
-	struct spa_pod_builder b = { 0 };
-	uint8_t buffer[4096];
-	uint32_t state;
-	struct spa_pod *param;
-	int res;
-
-	spa_log_error(this->log, "params:");
-
-	state = 0;
-	while (true) {
-		spa_pod_builder_init(&b, buffer, sizeof(buffer));
-		res = spa_node_port_enum_params_sync(node,
-				       direction, port_id,
-				       id, &state,
-				       NULL, &param, &b);
-		if (res != 1)
-			break;
-
-		spa_debug_pod(2, NULL, param);
-	}
-
-	spa_log_error(this->log, "failed filter:");
-	if (filter)
-		spa_debug_pod(2, NULL, filter);
-
-	return 0;
-}
-
-static int negotiate_link_format(struct impl *this, struct link *link)
-{
-	struct spa_pod_builder b = { 0 };
-	uint8_t buffer[4096];
-	uint32_t state;
-	struct spa_pod *format, *filter;
-	int res;
-
-	if (link->negotiated)
-		return 0;
-
-	spa_pod_builder_init(&b, buffer, sizeof(buffer));
-
-	state = 0;
-	filter = NULL;
-	if ((res = spa_node_port_enum_params_sync(link->out_node,
-			       SPA_DIRECTION_OUTPUT, link->out_port,
-			       SPA_PARAM_EnumFormat, &state,
-			       filter, &format, &b)) != 1) {
-		debug_params(this, link->out_node, SPA_DIRECTION_OUTPUT, link->out_port,
-				SPA_PARAM_EnumFormat, filter);
-		return -ENOTSUP;
-	}
-	filter = format;
-	state = 0;
-	if ((res = spa_node_port_enum_params_sync(link->in_node,
-			       SPA_DIRECTION_INPUT, link->in_port,
-			       SPA_PARAM_EnumFormat, &state,
-			       filter, &format, &b)) != 1) {
-		debug_params(this, link->in_node, SPA_DIRECTION_INPUT, link->in_port,
-				SPA_PARAM_EnumFormat, filter);
-		return -ENOTSUP;
-	}
-	filter = format;
-
-	spa_pod_fixate(filter);
-
-	if ((res = spa_node_port_set_param(link->out_node,
-				   SPA_DIRECTION_OUTPUT, link->out_port,
-				   SPA_PARAM_Format, 0,
-				   filter)) < 0)
-		return res;
-
-	if ((res = spa_node_port_set_param(link->in_node,
-				   SPA_DIRECTION_INPUT, link->in_port,
-				   SPA_PARAM_Format, 0,
-				   filter)) < 0)
-		return res;
-
-	link->negotiated = true;
-
-	return 0;
-}
-
-static int setup_convert(struct impl *this)
+static void emit_port_info(struct impl *this, struct port *port, bool full)
 {
-	int i, j, res;
-
-	spa_log_debug(this->log, "setup convert n_links:%d", this->n_links);
+	uint64_t old = full ? port->info.change_mask : 0;
+	uint32_t i;
 
-	if (this->n_links > 0)
-		return 0;
+	if (full)
+		port->info.change_mask = port->info_all;
+	if (port->info.change_mask) {
+		struct spa_dict_item items[3];
+		uint32_t n_items = 0;
+
+		if (PORT_IS_DSP(this, port->direction, port->id)) {
+			items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_FORMAT_DSP, "32 bit float mono audio");
+			items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_AUDIO_CHANNEL, port->position);
+			if (port->is_monitor)
+				items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_PORT_MONITOR, "true");
+		} else if (PORT_IS_CONTROL(this, port->direction, port->id)) {
+			items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_PORT_NAME, "control");
+			items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_FORMAT_DSP, "8 bit raw midi");
+		}
+		port->info.props = &SPA_DICT_INIT(items, n_items);
 
-	this->n_nodes = 0;
-	/* unpack */
-	this->nodes[this->n_nodes++] = this->fmt[SPA_DIRECTION_INPUT];
-	/* down mix */
-	this->nodes[this->n_nodes++] = this->channelmix;
-	/* resample */
-	this->nodes[this->n_nodes++] = this->resample;
-	/* pack */
-	this->nodes[this->n_nodes++] = this->fmt[SPA_DIRECTION_OUTPUT];
-
-	make_link(this, this->nodes[0], 0, this->nodes[1], 0, 2);
-	make_link(this, this->nodes[1], 0, this->nodes[2], 0, 2);
-	make_link(this, this->nodes[2], 0, this->nodes[3], 0, 1);
-
-	for (i = 0, j = this->n_links - 1; j >= i; i++, j--) {
-		spa_log_debug(this->log, "negotiate %d", i);
-		if ((res = negotiate_link_format(this, &this->links[i])) < 0)
-			return res;
-		spa_log_debug(this->log, "negotiate %d", j);
-		if ((res = negotiate_link_format(this, &this->links[j])) < 0)
-			return res;
+		if (port->info.change_mask & SPA_PORT_CHANGE_MASK_PARAMS) {
+			for (i = 0; i < SPA_N_ELEMENTS(port->params); i++) {
+				if (port->params[i].user > 0) {
+					port->params[i].flags ^= SPA_PARAM_INFO_SERIAL;
+					port->params[i].user = 0;
+				}
+			}
+		}
+		spa_node_emit_port_info(&this->hooks, port->direction, port->id, &port->info);
+		port->info.change_mask = old;
 	}
-	return 0;
 }
 
-static int negotiate_link_buffers(struct impl *this, struct link *link)
+static int init_port(struct impl *this, enum spa_direction direction, uint32_t port_id,
+		uint32_t position, bool is_dsp, bool is_monitor, bool is_control)
 {
-	uint8_t buffer[4096];
-	struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
-	uint32_t state;
-	struct spa_pod *param = NULL, *filter;
-	int res;
-	bool in_alloc, out_alloc;
-	uint32_t i, size, buffers, blocks, align, flags;
-	uint32_t *aligns;
-	struct spa_data *datas;
+	struct port *port = GET_PORT(this, direction, port_id);
+	const char *name;
 
-	if (link->n_buffers > 0)
-		return 0;
+	spa_assert(port_id < MAX_PORTS);
 
-	state = 0;
-	filter = NULL;
-	if ((res = spa_node_port_enum_params_sync(link->in_node,
-			       SPA_DIRECTION_INPUT, link->in_port,
-			       SPA_PARAM_Buffers, &state,
-			       filter, &param, &b)) != 1) {
-		debug_params(this, link->in_node, SPA_DIRECTION_INPUT, link->in_port,
-				SPA_PARAM_Buffers, filter);
-		return -ENOTSUP;
-	}
-	state = 0;
-	filter = param;
-	if ((res = spa_node_port_enum_params_sync(link->out_node,
-			       SPA_DIRECTION_OUTPUT, link->out_port,
-			       SPA_PARAM_Buffers, &state,
-			       filter, &param, &b)) != 1) {
-		debug_params(this, link->out_node, SPA_DIRECTION_OUTPUT, link->out_port,
-				SPA_PARAM_Buffers, filter);
-		return -ENOTSUP;
+	if (port == NULL) {
+		port = calloc(1, sizeof(struct port));
+		if (port == NULL)
+			return -errno;
+		this->dir[direction].ports[port_id] = port;
 	}
-
-	spa_pod_fixate(param);
-
-	in_alloc = SPA_FLAG_IS_SET(link->in_flags,
-				SPA_PORT_FLAG_CAN_ALLOC_BUFFERS);
-	out_alloc = SPA_FLAG_IS_SET(link->out_flags,
-				SPA_PORT_FLAG_CAN_ALLOC_BUFFERS);
-
-	flags = 0;
-	if (out_alloc || in_alloc) {
-		flags |= SPA_BUFFER_ALLOC_FLAG_NO_DATA;
-		if (out_alloc)
-			in_alloc = false;
+	port->direction = direction;
+	port->id = port_id;
+
+	name = spa_debug_type_find_short_name(spa_type_audio_channel, position);
+	snprintf(port->position, sizeof(port->position), "%s", name ? name : "UNK");
+
+	port->info_all = SPA_PORT_CHANGE_MASK_FLAGS |
+			SPA_PORT_CHANGE_MASK_PROPS |
+			SPA_PORT_CHANGE_MASK_PARAMS;
+	port->info = SPA_PORT_INFO_INIT();
+	port->info.flags = SPA_PORT_FLAG_NO_REF |
+		SPA_PORT_FLAG_DYNAMIC_DATA;
+	port->params[IDX_EnumFormat] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ);
+	port->params[IDX_Meta] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ);
+	port->params[IDX_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
+	port->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+	port->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+	port->params[IDX_Latency] = SPA_PARAM_INFO(SPA_PARAM_Latency, SPA_PARAM_INFO_READWRITE);
+	port->info.params = port->params;
+	port->info.n_params = N_PORT_PARAMS;
+
+	port->n_buffers = 0;
+	port->have_format = false;
+	port->is_monitor = is_monitor;
+	port->is_dsp = is_dsp;
+	if (port->is_dsp) {
+		port->format.media_type = SPA_MEDIA_TYPE_audio;
+		port->format.media_subtype = SPA_MEDIA_SUBTYPE_dsp;
+		port->format.info.dsp.format = SPA_AUDIO_FORMAT_DSP_F32;
+		port->blocks = 1;
+		port->stride = 4;
 	}
-
-	align = DEFAULT_ALIGN;
-
-	if (spa_pod_parse_object(param,
-		SPA_TYPE_OBJECT_ParamBuffers, NULL,
-		SPA_PARAM_BUFFERS_buffers, SPA_POD_Int(&buffers),
-		SPA_PARAM_BUFFERS_blocks,  SPA_POD_Int(&blocks),
-		SPA_PARAM_BUFFERS_size,    SPA_POD_Int(&size),
-		SPA_PARAM_BUFFERS_align,   SPA_POD_OPT_Int(&align)) < 0)
-		return -EINVAL;
-
-	spa_log_debug(this->log, "%p: buffers %d, blocks %d, size %d, align %d %d:%d",
-			this, buffers, blocks, size, align, out_alloc, in_alloc);
-
-	align = SPA_MAX(align, this->max_align);
-
-	datas = alloca(sizeof(struct spa_data) * blocks);
-	memset(datas, 0, sizeof(struct spa_data) * blocks);
-	aligns = alloca(sizeof(uint32_t) * blocks);
-	for (i = 0; i < blocks; i++) {
-		datas[i].type = SPA_DATA_MemPtr;
-		datas[i].flags = SPA_DATA_FLAG_DYNAMIC;
-		datas[i].maxsize = size;
-		aligns[i] = align;
+	port->is_control = is_control;
+	if (port->is_control) {
+		port->format.media_type = SPA_MEDIA_TYPE_application;
+		port->format.media_subtype = SPA_MEDIA_SUBTYPE_control;
+		port->blocks = 1;
+		port->stride = 1;
 	}
+	spa_list_init(&port->queue);
 
-	buffers = SPA_MAX(link->min_buffers, buffers);
-
-	if (link->buffers)
-		free(link->buffers);
-	link->buffers = spa_buffer_alloc_array(buffers, flags, 0, NULL, blocks, datas, aligns);
-	if (link->buffers == NULL)
-		return -errno;
-
-	link->n_buffers = buffers;
-
-	if ((res = spa_node_port_use_buffers(link->out_node,
-		       SPA_DIRECTION_OUTPUT, link->out_port,
-		       out_alloc ? SPA_NODE_BUFFERS_FLAG_ALLOC : 0,
-		       link->buffers, link->n_buffers)) < 0)
-		return res;
-
-	if ((res = spa_node_port_use_buffers(link->in_node,
-		       SPA_DIRECTION_INPUT, link->in_port,
-		       in_alloc ? SPA_NODE_BUFFERS_FLAG_ALLOC : 0,
-		       link->buffers, link->n_buffers)) < 0)
-		return res;
-
-	return 0;
-}
-
-static void flush_convert(struct impl *this)
-{
-	int i;
-	spa_log_debug(this->log, "%p: %d", this, this->n_links);
-	for (i = 0; i < this->n_links; i++)
-		this->links[i].io.status = SPA_STATUS_OK;
-}
-
-static void clean_convert(struct impl *this)
-{
-	int i;
-
-	spa_log_debug(this->log, "%p: %d", this, this->n_links);
-
-	for (i = 0; i < this->n_links; i++)
-		clean_link(this, &this->links[i]);
-	this->n_links = 0;
-}
-
-static int setup_buffers(struct impl *this, enum spa_direction direction)
-{
-	int i, res;
-
-	spa_log_debug(this->log, "%p: %d %d", this, direction, this->n_links);
-
-	if (direction == SPA_DIRECTION_INPUT) {
-		for (i = 0; i < this->n_links; i++) {
-			if ((res = negotiate_link_buffers(this, &this->links[i])) < 0)
-				spa_log_error(this->log, "%p: buffers %d failed %s",
-						this, i, spa_strerror(res));
-		}
-	} else {
-		for (i = this->n_links-1; i >= 0 ; i--) {
-			if ((res = negotiate_link_buffers(this, &this->links[i])) < 0)
-				spa_log_error(this->log, "%p: buffers %d failed %s",
-						this, i, spa_strerror(res));
-		}
-	}
+	spa_log_info(this->log, "%p: add port %d:%d position:%s %d %d %d",
+			this, direction, port_id, port->position, is_dsp, is_monitor, is_control);
+	emit_port_info(this, port, true);
 
 	return 0;
 }
 
-static int enum_params(struct impl *this,
-				 uint32_t id,
-				 struct spa_result_node_params *result,
-				 const struct spa_pod *filter,
-				 struct spa_pod_builder *builder)
-{
-	int res;
-	result->param = NULL;
-	result->next = result->index;
-	if (result->next < 0x1000) {
-		if (this->fmt[SPA_DIRECTION_INPUT] == this->merger &&
-		    (res = spa_node_enum_params_sync(this->merger,
-				id, &result->next, filter, &result->param, builder)) == 1) {
-			return res;
-		}
-		result->next = 0x1000;
-	}
-	if (result->next < 0x2000) {
-		result->next &= 0xfff;
-		if ((res = spa_node_enum_params_sync(this->channelmix,
-				id, &result->next, filter, &result->param, builder)) == 1) {
-			result->next |= 0x1000;
-			return res;
-		}
-		result->next = 0x2000;
-	}
-	if (result->next >= 0x2000) {
-		result->next &= 0xfff;
-		if ((res = spa_node_enum_params_sync(this->resample,
-				id, &result->next, filter, &result->param, builder)) == 1) {
-			result->next |= 0x2000;
-			return res;
-		}
-	}
-	return 0;
-}
 static int impl_node_enum_params(void *object, int seq,
 				 uint32_t id, uint32_t start, uint32_t num,
 				 const struct spa_pod *filter)
@@ -501,7 +363,6 @@ static int impl_node_enum_params(void *object, int seq,
 	uint8_t buffer[4096];
 	struct spa_result_node_params result;
 	uint32_t count = 0;
-	int res;
 
 	spa_return_val_if_fail(this != NULL, -EINVAL);
 	spa_return_val_if_fail(num != 0, -EINVAL);
@@ -509,7 +370,6 @@ static int impl_node_enum_params(void *object, int seq,
 	result.id = id;
 	result.next = start;
       next:
-	res = 0;
 	result.index = result.next++;
 
 	spa_pod_builder_init(&b, buffer, sizeof(buffer));
@@ -552,13 +412,13 @@ static int impl_node_enum_params(void *object, int seq,
 			param = spa_pod_builder_add_object(&b,
 				SPA_TYPE_OBJECT_ParamPortConfig, id,
 				SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(SPA_DIRECTION_INPUT),
-				SPA_PARAM_PORT_CONFIG_mode,      SPA_POD_Id(this->mode[SPA_DIRECTION_INPUT]));
+				SPA_PARAM_PORT_CONFIG_mode,      SPA_POD_Id(this->dir[SPA_DIRECTION_INPUT].mode));
 			break;
 		case 1:
 			param = spa_pod_builder_add_object(&b,
 				SPA_TYPE_OBJECT_ParamPortConfig, id,
 				SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(SPA_DIRECTION_OUTPUT),
-				SPA_PARAM_PORT_CONFIG_mode,      SPA_POD_Id(this->mode[SPA_DIRECTION_OUTPUT]));
+				SPA_PARAM_PORT_CONFIG_mode,      SPA_POD_Id(this->dir[SPA_DIRECTION_OUTPUT].mode));
 			break;
 		default:
 			return 0;
@@ -566,23 +426,317 @@ static int impl_node_enum_params(void *object, int seq,
 		break;
 
 	case SPA_PARAM_PropInfo:
-		if ((res = enum_params(this, id, &result, filter, &b)) != 1)
-			return res;
+	{
+		struct props *p = &this->props;
+		struct spa_pod_frame f[2];
+		uint32_t i;
+
+		switch (result.index) {
+		case 0:
+			param = spa_pod_builder_add_object(&b,
+				SPA_TYPE_OBJECT_PropInfo, id,
+				SPA_PROP_INFO_id,   SPA_POD_Id(SPA_PROP_volume),
+				SPA_PROP_INFO_description, SPA_POD_String("Volume"),
+				SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float(p->volume, 0.0, 10.0));
+			break;
+		case 1:
+			param = spa_pod_builder_add_object(&b,
+				SPA_TYPE_OBJECT_PropInfo, id,
+				SPA_PROP_INFO_id,   SPA_POD_Id(SPA_PROP_mute),
+				SPA_PROP_INFO_description, SPA_POD_String("Mute"),
+				SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(p->channel.mute));
+			break;
+		case 2:
+			param = spa_pod_builder_add_object(&b,
+				SPA_TYPE_OBJECT_PropInfo, id,
+				SPA_PROP_INFO_id,   SPA_POD_Id(SPA_PROP_channelVolumes),
+				SPA_PROP_INFO_description, SPA_POD_String("Channel Volumes"),
+				SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float(p->volume, 0.0, 10.0),
+				SPA_PROP_INFO_container, SPA_POD_Id(SPA_TYPE_Array));
+			break;
+		case 3:
+			param = spa_pod_builder_add_object(&b,
+				SPA_TYPE_OBJECT_PropInfo, id,
+				SPA_PROP_INFO_id,   SPA_POD_Id(SPA_PROP_channelMap),
+				SPA_PROP_INFO_description, SPA_POD_String("Channel Map"),
+				SPA_PROP_INFO_type, SPA_POD_Id(SPA_AUDIO_CHANNEL_UNKNOWN),
+				SPA_PROP_INFO_container, SPA_POD_Id(SPA_TYPE_Array));
+			break;
+		case 4:
+			param = spa_pod_builder_add_object(&b,
+				SPA_TYPE_OBJECT_PropInfo, id,
+				SPA_PROP_INFO_id,   SPA_POD_Id(SPA_PROP_monitorMute),
+				SPA_PROP_INFO_description, SPA_POD_String("Monitor Mute"),
+				SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(p->monitor.mute));
+			break;
+		case 5:
+			param = spa_pod_builder_add_object(&b,
+				SPA_TYPE_OBJECT_PropInfo, id,
+				SPA_PROP_INFO_id,   SPA_POD_Id(SPA_PROP_monitorVolumes),
+				SPA_PROP_INFO_description, SPA_POD_String("Monitor Volumes"),
+				SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float(p->volume, 0.0, 10.0),
+				SPA_PROP_INFO_container, SPA_POD_Id(SPA_TYPE_Array));
+			break;
+		case 6:
+			param = spa_pod_builder_add_object(&b,
+				SPA_TYPE_OBJECT_PropInfo, id,
+				SPA_PROP_INFO_id,   SPA_POD_Id(SPA_PROP_softMute),
+				SPA_PROP_INFO_description, SPA_POD_String("Soft Mute"),
+				SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(p->soft.mute));
+			break;
+		case 7:
+			param = spa_pod_builder_add_object(&b,
+				SPA_TYPE_OBJECT_PropInfo, id,
+				SPA_PROP_INFO_id,   SPA_POD_Id(SPA_PROP_softVolumes),
+				SPA_PROP_INFO_description, SPA_POD_String("Soft Volumes"),
+				SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float(p->volume, 0.0, 10.0),
+				SPA_PROP_INFO_container, SPA_POD_Id(SPA_TYPE_Array));
+			break;
+		case 8:
+			param = spa_pod_builder_add_object(&b,
+				SPA_TYPE_OBJECT_PropInfo, id,
+				SPA_PROP_INFO_name, SPA_POD_String("monitor.channel-volumes"),
+				SPA_PROP_INFO_description, SPA_POD_String("Monitor channel volume"),
+				SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(
+					this->monitor_channel_volumes),
+				SPA_PROP_INFO_params, SPA_POD_Bool(true));
+			break;
+		case 9:
+			param = spa_pod_builder_add_object(&b,
+				SPA_TYPE_OBJECT_PropInfo, id,
+				SPA_PROP_INFO_name, SPA_POD_String("channelmix.disable"),
+				SPA_PROP_INFO_description, SPA_POD_String("Disable Channel mixing"),
+				SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(p->mix_disabled),
+				SPA_PROP_INFO_params, SPA_POD_Bool(true));
+			break;
+		case 10:
+			param = spa_pod_builder_add_object(&b,
+				SPA_TYPE_OBJECT_PropInfo, id,
+				SPA_PROP_INFO_name, SPA_POD_String("channelmix.normalize"),
+				SPA_PROP_INFO_description, SPA_POD_String("Normalize Volumes"),
+				SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(
+					SPA_FLAG_IS_SET(this->mix.options, CHANNELMIX_OPTION_NORMALIZE)),
+				SPA_PROP_INFO_params, SPA_POD_Bool(true));
+			break;
+		case 11:
+			param = spa_pod_builder_add_object(&b,
+				SPA_TYPE_OBJECT_PropInfo, id,
+				SPA_PROP_INFO_name, SPA_POD_String("channelmix.mix-lfe"),
+				SPA_PROP_INFO_description, SPA_POD_String("Mix LFE into channels"),
+				SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(
+					SPA_FLAG_IS_SET(this->mix.options, CHANNELMIX_OPTION_MIX_LFE)),
+				SPA_PROP_INFO_params, SPA_POD_Bool(true));
+			break;
+		case 12:
+			param = spa_pod_builder_add_object(&b,
+				SPA_TYPE_OBJECT_PropInfo, id,
+				SPA_PROP_INFO_name, SPA_POD_String("channelmix.upmix"),
+				SPA_PROP_INFO_description, SPA_POD_String("Enable upmixing"),
+				SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(
+					SPA_FLAG_IS_SET(this->mix.options, CHANNELMIX_OPTION_UPMIX)),
+				SPA_PROP_INFO_params, SPA_POD_Bool(true));
+			break;
+		case 13:
+			param = spa_pod_builder_add_object(&b,
+				SPA_TYPE_OBJECT_PropInfo, id,
+				SPA_PROP_INFO_name, SPA_POD_String("channelmix.lfe-cutoff"),
+				SPA_PROP_INFO_description, SPA_POD_String("LFE cutoff frequency"),
+				SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float(
+					this->mix.lfe_cutoff, 0.0, 1000.0),
+				SPA_PROP_INFO_params, SPA_POD_Bool(true));
+			break;
+		case 14:
+			param = spa_pod_builder_add_object(&b,
+				SPA_TYPE_OBJECT_PropInfo, id,
+				SPA_PROP_INFO_name, SPA_POD_String("channelmix.fc-cutoff"),
+				SPA_PROP_INFO_description, SPA_POD_String("FC cutoff frequency (Hz)"),
+				SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float(
+					this->mix.fc_cutoff, 0.0, 48000.0),
+				SPA_PROP_INFO_params, SPA_POD_Bool(true));
+			break;
+		case 15:
+			param = spa_pod_builder_add_object(&b,
+				SPA_TYPE_OBJECT_PropInfo, id,
+				SPA_PROP_INFO_name, SPA_POD_String("channelmix.rear-delay"),
+				SPA_PROP_INFO_description, SPA_POD_String("Rear channels delay (ms)"),
+				SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float(
+					this->mix.rear_delay, 0.0, 1000.0),
+				SPA_PROP_INFO_params, SPA_POD_Bool(true));
+			break;
+		case 16:
+			param = spa_pod_builder_add_object(&b,
+				SPA_TYPE_OBJECT_PropInfo, id,
+				SPA_PROP_INFO_name, SPA_POD_String("channelmix.stereo-widen"),
+				SPA_PROP_INFO_description, SPA_POD_String("Stereo widen"),
+				SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float(
+					this->mix.widen, 0.0, 1.0),
+				SPA_PROP_INFO_params, SPA_POD_Bool(true));
+			break;
+		case 17:
+			param = spa_pod_builder_add_object(&b,
+				SPA_TYPE_OBJECT_PropInfo, id,
+				SPA_PROP_INFO_name, SPA_POD_String("channelmix.hilbert-taps"),
+				SPA_PROP_INFO_description, SPA_POD_String("Taps for phase shift of rear"),
+				SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Int(
+					this->mix.hilbert_taps, 0, MAX_TAPS),
+				SPA_PROP_INFO_params, SPA_POD_Bool(true));
+			break;
+		case 18:
+			spa_pod_builder_push_object(&b, &f[0], SPA_TYPE_OBJECT_PropInfo, id);
+			spa_pod_builder_add(&b,
+				SPA_PROP_INFO_name, SPA_POD_String("channelmix.upmix-method"),
+				SPA_PROP_INFO_description, SPA_POD_String("Upmix method to use"),
+				SPA_PROP_INFO_type, SPA_POD_String(
+					channelmix_upmix_info[this->mix.upmix].label),
+				SPA_PROP_INFO_params, SPA_POD_Bool(true),
+				0);
+
+			spa_pod_builder_prop(&b, SPA_PROP_INFO_labels, 0);
+			spa_pod_builder_push_struct(&b, &f[1]);
+			for (i = 0; i < SPA_N_ELEMENTS(channelmix_upmix_info); i++) {
+				spa_pod_builder_string(&b, channelmix_upmix_info[i].label);
+				spa_pod_builder_string(&b, channelmix_upmix_info[i].description);
+			}
+			spa_pod_builder_pop(&b, &f[1]);
+			param = spa_pod_builder_pop(&b, &f[0]);
+			break;
+		case 19:
+			param = spa_pod_builder_add_object(&b,
+				SPA_TYPE_OBJECT_PropInfo, id,
+				SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_rate),
+				SPA_PROP_INFO_description, SPA_POD_String("Rate scaler"),
+				SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Double(p->rate, 0.0, 10.0));
+			break;
+		case 20:
+			param = spa_pod_builder_add_object(&b,
+				SPA_TYPE_OBJECT_PropInfo, id,
+				SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_quality),
+				SPA_PROP_INFO_name, SPA_POD_String("resample.quality"),
+				SPA_PROP_INFO_description, SPA_POD_String("Resample Quality"),
+				SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Int(p->resample_quality, 0, 14),
+				SPA_PROP_INFO_params, SPA_POD_Bool(true));
+			break;
+		case 21:
+			param = spa_pod_builder_add_object(&b,
+				SPA_TYPE_OBJECT_PropInfo, id,
+				SPA_PROP_INFO_name, SPA_POD_String("resample.disable"),
+				SPA_PROP_INFO_description, SPA_POD_String("Disable Resampling"),
+				SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(p->resample_disabled),
+				SPA_PROP_INFO_params, SPA_POD_Bool(true));
+			break;
+		case 22:
+			param = spa_pod_builder_add_object(&b,
+				SPA_TYPE_OBJECT_PropInfo, id,
+				SPA_PROP_INFO_name, SPA_POD_String("dither.noise"),
+				SPA_PROP_INFO_description, SPA_POD_String("Add noise bits"),
+				SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Int(this->dir[1].conv.noise_bits, 0, 16),
+				SPA_PROP_INFO_params, SPA_POD_Bool(true));
+			break;
+		case 23:
+			spa_pod_builder_push_object(&b, &f[0], SPA_TYPE_OBJECT_PropInfo, id);
+			spa_pod_builder_add(&b,
+				SPA_PROP_INFO_name, SPA_POD_String("dither.method"),
+				SPA_PROP_INFO_description, SPA_POD_String("The dithering method"),
+				SPA_PROP_INFO_type, SPA_POD_String(
+					dither_method_info[this->dir[1].conv.method].label),
+				SPA_PROP_INFO_params, SPA_POD_Bool(true),
+				0);
+			spa_pod_builder_prop(&b, SPA_PROP_INFO_labels, 0);
+			spa_pod_builder_push_struct(&b, &f[1]);
+			for (i = 0; i < SPA_N_ELEMENTS(dither_method_info); i++) {
+				spa_pod_builder_string(&b, dither_method_info[i].label);
+				spa_pod_builder_string(&b, dither_method_info[i].description);
+			}
+			spa_pod_builder_pop(&b, &f[1]);
+			param = spa_pod_builder_pop(&b, &f[0]);
+			break;
+		default:
+			return 0;
+		}
 		break;
+	}
 
 	case SPA_PARAM_Props:
-		if ((res = enum_params(this, id, &result, filter, &b)) != 1)
-			return res;
-		break;
+	{
+		struct props *p = &this->props;
+		struct spa_pod_frame f[2];
 
+		switch (result.index) {
+		case 0:
+			spa_pod_builder_push_object(&b, &f[0],
+                                SPA_TYPE_OBJECT_Props, id);
+			spa_pod_builder_add(&b,
+				SPA_PROP_volume,		SPA_POD_Float(p->volume),
+				SPA_PROP_mute,			SPA_POD_Bool(p->channel.mute),
+				SPA_PROP_channelVolumes,	SPA_POD_Array(sizeof(float),
+									SPA_TYPE_Float,
+									p->channel.n_volumes,
+									p->channel.volumes),
+				SPA_PROP_channelMap,		SPA_POD_Array(sizeof(uint32_t),
+									SPA_TYPE_Id,
+									p->n_channels,
+									p->channel_map),
+				SPA_PROP_softMute,		SPA_POD_Bool(p->soft.mute),
+				SPA_PROP_softVolumes,		SPA_POD_Array(sizeof(float),
+									SPA_TYPE_Float,
+									p->soft.n_volumes,
+									p->soft.volumes),
+				SPA_PROP_monitorMute,		SPA_POD_Bool(p->monitor.mute),
+				SPA_PROP_monitorVolumes,	SPA_POD_Array(sizeof(float),
+									SPA_TYPE_Float,
+									p->monitor.n_volumes,
+									p->monitor.volumes),
+				0);
+			spa_pod_builder_prop(&b, SPA_PROP_params, 0);
+			spa_pod_builder_push_struct(&b, &f[1]);
+			spa_pod_builder_string(&b, "monitor.channel-volumes");
+			spa_pod_builder_bool(&b, this->monitor_channel_volumes);
+			spa_pod_builder_string(&b, "channelmix.disable");
+			spa_pod_builder_bool(&b, this->props.mix_disabled);
+			spa_pod_builder_string(&b, "channelmix.normalize");
+			spa_pod_builder_bool(&b, SPA_FLAG_IS_SET(this->mix.options,
+						CHANNELMIX_OPTION_NORMALIZE));
+			spa_pod_builder_string(&b, "channelmix.mix-lfe");
+			spa_pod_builder_bool(&b, SPA_FLAG_IS_SET(this->mix.options,
+						CHANNELMIX_OPTION_MIX_LFE));
+			spa_pod_builder_string(&b, "channelmix.upmix");
+			spa_pod_builder_bool(&b, SPA_FLAG_IS_SET(this->mix.options,
+						CHANNELMIX_OPTION_UPMIX));
+			spa_pod_builder_string(&b, "channelmix.lfe-cutoff");
+			spa_pod_builder_float(&b, this->mix.lfe_cutoff);
+			spa_pod_builder_string(&b, "channelmix.fc-cutoff");
+			spa_pod_builder_float(&b, this->mix.fc_cutoff);
+			spa_pod_builder_string(&b, "channelmix.rear-delay");
+			spa_pod_builder_float(&b, this->mix.rear_delay);
+			spa_pod_builder_string(&b, "channelmix.stereo-widen");
+			spa_pod_builder_float(&b, this->mix.widen);
+			spa_pod_builder_string(&b, "channelmix.hilbert-taps");
+			spa_pod_builder_int(&b, this->mix.hilbert_taps);
+			spa_pod_builder_string(&b, "channelmix.upmix-method");
+			spa_pod_builder_string(&b, channelmix_upmix_info[this->mix.upmix].label);
+			spa_pod_builder_string(&b, "resample.quality");
+			spa_pod_builder_int(&b, p->resample_quality);
+			spa_pod_builder_string(&b, "resample.disable");
+			spa_pod_builder_bool(&b, p->resample_disabled);
+			spa_pod_builder_string(&b, "dither.noise");
+			spa_pod_builder_int(&b, this->dir[1].conv.noise_bits);
+			spa_pod_builder_string(&b, "dither.method");
+			spa_pod_builder_string(&b, dither_method_info[this->dir[1].conv.method].label);
+			spa_pod_builder_pop(&b, &f[1]);
+			param = spa_pod_builder_pop(&b, &f[0]);
+			break;
+		default:
+			return 0;
+		}
+		break;
+	}
 	default:
-		return -ENOENT;
+		return 0;
 	}
 
-	if (res == 0) {
-		if (spa_pod_filter(&b, &result.param, param, filter) < 0)
-			goto next;
-	}
+	if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+		goto next;
+
 	spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
 
 	if (++count != num)
@@ -594,7 +748,6 @@ static int impl_node_enum_params(void *object, int seq,
 static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size)
 {
 	struct impl *this = object;
-	int res;
 
 	spa_return_val_if_fail(this != NULL, -EINVAL);
 
@@ -602,230 +755,296 @@ static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size)
 
 	switch (id) {
 	case SPA_IO_Position:
-		res = spa_node_set_io(this->resample, id, data, size);
-		res = spa_node_set_io(this->channelmix, id, data, size);
-		res = spa_node_set_io(this->fmt[0], id, data, size);
-		res = spa_node_set_io(this->fmt[1], id, data, size);
+		this->io_position = data;
 		break;
 	default:
-		res = -ENOENT;
-		break;
+		return -ENOENT;
 	}
-	return res;
+	return 0;
 }
 
-static void fmt_input_port_info(void *data,
-		enum spa_direction direction, uint32_t port,
-		const struct spa_port_info *info)
+static int audioconvert_set_param(struct impl *this, const char *k, const char *s)
 {
-	struct impl *this = data;
-	bool is_monitor = IS_MONITOR_PORT(this, direction, port);
-
-	if (this->fmt_removing[direction])
-		info = NULL;
-	if (is_monitor && this->fmt_removing[SPA_DIRECTION_INPUT])
-		info = NULL;
-
-	spa_log_debug(this->log, "%p: %d.%d info", this, direction, port);
-
-	if (direction == SPA_DIRECTION_INPUT || is_monitor)
-		spa_node_emit_port_info(&this->hooks, direction, port, info);
+	if (spa_streq(k, "monitor.channel-volumes"))
+		this->monitor_channel_volumes = spa_atob(s);
+	else if (spa_streq(k, "channelmix.disable"))
+		this->props.mix_disabled = spa_atob(s);
+	else if (spa_streq(k, "channelmix.normalize"))
+		SPA_FLAG_UPDATE(this->mix.options, CHANNELMIX_OPTION_NORMALIZE, spa_atob(s));
+	else if (spa_streq(k, "channelmix.mix-lfe"))
+		SPA_FLAG_UPDATE(this->mix.options, CHANNELMIX_OPTION_MIX_LFE, spa_atob(s));
+	else if (spa_streq(k, "channelmix.upmix"))
+		SPA_FLAG_UPDATE(this->mix.options, CHANNELMIX_OPTION_UPMIX, spa_atob(s));
+	else if (spa_streq(k, "channelmix.lfe-cutoff"))
+		spa_atof(s, &this->mix.lfe_cutoff);
+	else if (spa_streq(k, "channelmix.fc-cutoff"))
+		spa_atof(s, &this->mix.fc_cutoff);
+	else if (spa_streq(k, "channelmix.rear-delay"))
+		spa_atof(s, &this->mix.rear_delay);
+	else if (spa_streq(k, "channelmix.stereo-widen"))
+		spa_atof(s, &this->mix.widen);
+	else if (spa_streq(k, "channelmix.hilbert-taps"))
+		spa_atou32(s, &this->mix.hilbert_taps, 0);
+	else if (spa_streq(k, "channelmix.upmix-method"))
+		this->mix.upmix = channelmix_upmix_from_label(s);
+	else if (spa_streq(k, "resample.quality"))
+		this->props.resample_quality = atoi(s);
+	else if (spa_streq(k, "resample.disable"))
+		this->props.resample_disabled = spa_atob(s);
+	else if (spa_streq(k, "dither.noise"))
+		spa_atou32(s, &this->dir[1].conv.noise_bits, 0);
+	else if (spa_streq(k, "dither.method"))
+		this->dir[1].conv.method = dither_method_from_label(s);
+	else
+		return 0;
+	return 1;
 }
 
-static const struct spa_node_events fmt_input_events = {
-	SPA_VERSION_NODE_EVENTS,
-	.port_info = fmt_input_port_info,
-};
-
-static void fmt_output_port_info(void *data,
-		enum spa_direction direction, uint32_t port,
-		const struct spa_port_info *info)
+static int parse_prop_params(struct impl *this, struct spa_pod *params)
 {
-	struct impl *this = data;
+	struct spa_pod_parser prs;
+	struct spa_pod_frame f;
+	int changed = 0;
 
-	if (this->fmt_removing[direction])
-		info = NULL;
+	spa_pod_parser_pod(&prs, params);
+	if (spa_pod_parser_push_struct(&prs, &f) < 0)
+		return 0;
 
-	spa_log_debug(this->log, "%p: %d.%d info", this, direction, port);
+	while (true) {
+		const char *name;
+		struct spa_pod *pod;
+		char value[512];
 
-	if (direction == SPA_DIRECTION_OUTPUT)
-		spa_node_emit_port_info(&this->hooks, direction, port, info);
-}
+		if (spa_pod_parser_get_string(&prs, &name) < 0)
+			break;
 
-static const struct spa_node_events fmt_output_events = {
-	SPA_VERSION_NODE_EVENTS,
-	.port_info = fmt_output_port_info,
-};
+		if (spa_pod_parser_get_pod(&prs, &pod) < 0)
+			break;
 
-static void on_channelmix_info(void *data, const struct spa_node_info *info)
-{
-	struct impl *this = data;
-	uint32_t i;
+		if (spa_pod_is_string(pod)) {
+			spa_pod_copy_string(pod, sizeof(value), value);
+		} else if (spa_pod_is_float(pod)) {
+			snprintf(value, sizeof(value), "%f",
+					SPA_POD_VALUE(struct spa_pod_float, pod));
+		} else if (spa_pod_is_int(pod)) {
+			snprintf(value, sizeof(value), "%d",
+					SPA_POD_VALUE(struct spa_pod_int, pod));
+		} else if (spa_pod_is_bool(pod)) {
+			snprintf(value, sizeof(value), "%s",
+					SPA_POD_VALUE(struct spa_pod_bool, pod) ?
+					"true" : "false");
+		} else
+			continue;
 
-	if ((info->change_mask & SPA_NODE_CHANGE_MASK_PARAMS) == 0)
-		return;
-
-	for (i = 0; i < info->n_params; i++) {
-		uint32_t idx;
+		spa_log_info(this->log, "key:'%s' val:'%s'", name, value);
+		changed += audioconvert_set_param(this, name, value);
+	}
+	if (changed) {
+		channelmix_init(&this->mix);
+		set_volume(this);
+	}
+	return changed;
+}
 
-		switch (info->params[i].id) {
-		case SPA_PARAM_PropInfo:
-			idx = IDX_PropInfo;
+static int apply_props(struct impl *this, const struct spa_pod *param)
+{
+	struct spa_pod_prop *prop;
+	struct spa_pod_object *obj = (struct spa_pod_object *) param;
+	struct props *p = &this->props;
+	bool have_channel_volume = false;
+	bool have_soft_volume = false;
+	int changed = 0;
+	uint32_t n;
+
+	SPA_POD_OBJECT_FOREACH(obj, prop) {
+		switch (prop->key) {
+		case SPA_PROP_volume:
+			if (spa_pod_get_float(&prop->value, &p->volume) == 0)
+				changed++;
 			break;
-		case SPA_PARAM_Props:
-			idx = IDX_Props;
+		case SPA_PROP_mute:
+			if (spa_pod_get_bool(&prop->value, &p->channel.mute) == 0) {
+				have_channel_volume = true;
+				changed++;
+			}
+			break;
+		case SPA_PROP_channelVolumes:
+			if ((n = spa_pod_copy_array(&prop->value, SPA_TYPE_Float,
+					p->channel.volumes, SPA_AUDIO_MAX_CHANNELS)) > 0) {
+				have_channel_volume = true;
+				p->channel.n_volumes = n;
+				changed++;
+			}
+			break;
+		case SPA_PROP_channelMap:
+			if ((n = spa_pod_copy_array(&prop->value, SPA_TYPE_Id,
+					p->channel_map, SPA_AUDIO_MAX_CHANNELS)) > 0) {
+				p->n_channels = n;
+				changed++;
+			}
+			break;
+		case SPA_PROP_softMute:
+			if (spa_pod_get_bool(&prop->value, &p->soft.mute) == 0) {
+				have_soft_volume = true;
+				changed++;
+			}
+			break;
+		case SPA_PROP_softVolumes:
+			if ((n = spa_pod_copy_array(&prop->value, SPA_TYPE_Float,
+					p->soft.volumes, SPA_AUDIO_MAX_CHANNELS)) > 0) {
+				have_soft_volume = true;
+				p->soft.n_volumes = n;
+				changed++;
+			}
+			break;
+		case SPA_PROP_monitorMute:
+			if (spa_pod_get_bool(&prop->value, &p->monitor.mute) == 0)
+				changed++;
+			break;
+		case SPA_PROP_monitorVolumes:
+			if ((n = spa_pod_copy_array(&prop->value, SPA_TYPE_Float,
+					p->monitor.volumes, SPA_AUDIO_MAX_CHANNELS)) > 0) {
+				p->monitor.n_volumes = n;
+				changed++;
+			}
+			break;
+		case SPA_PROP_rate:
+			spa_pod_get_double(&prop->value, &p->rate);
+			break;
+		case SPA_PROP_params:
+			changed += parse_prop_params(this, &prop->value);
 			break;
 		default:
-			continue;
+			break;
 		}
-		if (!this->add_listener &&
-		    this->param_flags[idx] == info->params[i].flags)
-			continue;
-
-		this->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS;
-		this->param_flags[idx] = info->params[i].flags;
-		this->params[idx].flags =
-			(this->params[idx].flags & SPA_PARAM_INFO_SERIAL) |
-			(info->params[i].flags & SPA_PARAM_INFO_READWRITE);
+	}
+	if (changed) {
+		if (have_soft_volume)
+			p->have_soft_volume = true;
+		else if (have_channel_volume)
+			p->have_soft_volume = false;
 
-		if (!this->add_listener)
-			this->params[idx].user++;
+		set_volume(this);
 	}
-	emit_node_info(this, false);
+	return changed;
 }
 
-static const struct spa_node_events channelmix_events = {
-	SPA_VERSION_NODE_EVENTS,
-	.info = on_channelmix_info,
-};
-
-static int reconfigure_mode(struct impl *this, enum spa_param_port_config_mode mode,
-		enum spa_direction direction, bool monitor, struct spa_audio_info *info)
+static int apply_midi(struct impl *this, const struct spa_pod *value)
 {
-	int res = 0;
-	struct spa_node *old, *new;
-	bool do_signal;
+	const uint8_t *val = SPA_POD_BODY(value);
+	uint32_t size = SPA_POD_BODY_SIZE(value);
+	struct props *p = &this->props;
 
-	spa_log_debug(this->log, "%p: mode %d", this, mode);
-
-	/* old node on input/output */
-	old = this->fmt[direction];
-
-	/* decide on new node based on mode and direction */
-	switch (mode) {
-	case SPA_PARAM_PORT_CONFIG_MODE_convert:
-		new = direction == SPA_DIRECTION_INPUT ?  this->convert_in : this->convert_out;
-		break;
+	if (size < 3)
+		return -EINVAL;
 
-	case SPA_PARAM_PORT_CONFIG_MODE_dsp:
-		new = direction == SPA_DIRECTION_INPUT ?  this->merger : this->splitter;
-		break;
-	case SPA_PARAM_PORT_CONFIG_MODE_none:
-		new = NULL;
-		break;
-	default:
-		return -EIO;
-	}
+	if ((val[0] & 0xf0) != 0xb0 || val[1] != 7)
+		return 0;
 
-	clean_convert(this);
+	p->volume = val[2] / 127.0f;
+	set_volume(this);
+	return 1;
+}
 
-	this->fmt[direction] = new;
+static int reconfigure_mode(struct impl *this, enum spa_param_port_config_mode mode,
+		enum spa_direction direction, bool monitor, bool control, struct spa_audio_info *info)
+{
+	struct dir *dir;
+	uint32_t i;
 
-	/* signal if we change nodes or when DSP config changes */
-	do_signal = this->fmt[direction] != old ||
-		mode == SPA_PARAM_PORT_CONFIG_MODE_dsp;
+	dir = &this->dir[direction];
 
-	if (do_signal && old != NULL) {
-		/* change, remove old ports. We trigger a new port_info event
-		 * on the old node with info set to NULL to mark delete */
-		if (this->have_fmt_listener[direction]) {
-			spa_hook_remove(&this->fmt_listener[direction]);
+	if (dir->have_profile && this->monitor == monitor && dir->mode == mode &&
+	    dir->control == control &&
+	    (info == NULL || memcmp(&dir->format, info, sizeof(*info)) == 0))
+		return 0;
 
-			this->fmt_removing[direction] = true;
-			spa_node_add_listener(old,
-				&this->fmt_listener[direction],
-				direction == SPA_DIRECTION_INPUT ?
-					&fmt_input_events : &fmt_output_events,
-				this);
-			this->fmt_removing[direction] = false;
+	spa_log_info(this->log, "%p: port config direction:%d monitor:%d control:%d mode:%d %d", this,
+			direction, monitor, control, mode, dir->n_ports);
 
-			spa_hook_remove(&this->fmt_listener[direction]);
-			this->have_fmt_listener[direction] = false;
-		}
+	for (i = 0; i < dir->n_ports; i++) {
+		spa_node_emit_port_info(&this->hooks, direction, i, NULL);
+		if (this->monitor && direction == SPA_DIRECTION_INPUT)
+			spa_node_emit_port_info(&this->hooks, SPA_DIRECTION_OUTPUT, i+1, NULL);
 	}
 
-	this->mode[direction] = mode;
-
-	if (new != NULL) {
-		struct spa_pod_builder b = { 0 };
-		uint8_t buffer[4096];
-		struct spa_pod *param = NULL;
-
-		spa_pod_builder_init(&b, buffer, sizeof(buffer));
+	this->monitor = monitor;
+	dir->control = control;
+	dir->have_profile = true;
+	dir->mode = mode;
 
+	switch (mode) {
+	case SPA_PARAM_PORT_CONFIG_MODE_dsp:
+	{
 		if (info) {
-			spa_log_debug(this->log, "%p: port config %d", this, info->info.raw.channels);
-			param = spa_format_audio_raw_build(&b, SPA_PARAM_Format, &info->info.raw);
-		}
-		if (mode == SPA_PARAM_PORT_CONFIG_MODE_dsp) {
-			param = spa_pod_builder_add_object(&b,
-				SPA_TYPE_OBJECT_ParamPortConfig, SPA_PARAM_PortConfig,
-				SPA_PARAM_PORT_CONFIG_direction,	SPA_POD_Id(direction),
-				SPA_PARAM_PORT_CONFIG_mode,		SPA_POD_Id(SPA_PARAM_PORT_CONFIG_MODE_dsp),
-				SPA_PARAM_PORT_CONFIG_monitor,		SPA_POD_Bool(monitor),
-				SPA_PARAM_PORT_CONFIG_format,		SPA_POD_Pod(param));
-			res = spa_node_set_param(this->fmt[direction], SPA_PARAM_PortConfig, 0, param);
+			dir->n_ports = info->info.raw.channels;
+			dir->format = *info;
+			dir->format.info.raw.format = SPA_AUDIO_FORMAT_DSP_F32;
+			dir->format.info.raw.rate = 0;
+			dir->have_format = true;
 		} else {
-			res = spa_node_port_set_param(this->fmt[direction], direction, 0,
-				SPA_PARAM_Format, 0, param);
+			dir->n_ports = 0;
 		}
-		if (res < 0)
-			return res;
 
-		this->info.change_mask |= SPA_NODE_CHANGE_MASK_FLAGS | SPA_NODE_CHANGE_MASK_PARAMS;
-		this->info.flags &= ~SPA_NODE_FLAG_NEED_CONFIGURE;
-		this->params[IDX_Props].user++;
-	}
-
-	/* notify ports of new node */
-	if (do_signal && new != NULL) {
-		if (this->have_fmt_listener[direction])
-			spa_hook_remove(&this->fmt_listener[direction]);
+		if (this->monitor && direction == SPA_DIRECTION_INPUT)
+			this->dir[SPA_DIRECTION_OUTPUT].n_ports = dir->n_ports + 1;
 
-		spa_node_add_listener(this->fmt[direction],
-				&this->fmt_listener[direction],
-				direction == SPA_DIRECTION_INPUT ?
-					&fmt_input_events : &fmt_output_events,
-				this);
-		this->have_fmt_listener[direction] = true;
+		for (i = 0; i < dir->n_ports; i++) {
+			init_port(this, direction, i, info->info.raw.position[i], true, false, false);
+			if (this->monitor && direction == SPA_DIRECTION_INPUT)
+				init_port(this, SPA_DIRECTION_OUTPUT, i+1,
+					info->info.raw.position[i], true, true, false);
+		}
+		break;
+	}
+	case SPA_PARAM_PORT_CONFIG_MODE_convert:
+	{
+		dir->n_ports = 1;
+		dir->have_format = false;
+		init_port(this, direction, 0, 0, false, false, false);
+		break;
+	}
+	default:
+		return -ENOTSUP;
+	}
+	if (direction == SPA_DIRECTION_INPUT && dir->control) {
+		i = dir->n_ports++;
+		init_port(this, direction, i, 0, false, false, true);
 	}
-	emit_node_info(this, false);
 
+	this->info.change_mask |= SPA_NODE_CHANGE_MASK_FLAGS | SPA_NODE_CHANGE_MASK_PARAMS;
+	this->info.flags &= ~SPA_NODE_FLAG_NEED_CONFIGURE;
+	this->params[IDX_Props].user++;
+	this->params[IDX_PortConfig].user++;
 	return 0;
 }
 
 static int impl_node_set_param(void *object, uint32_t id, uint32_t flags,
 			       const struct spa_pod *param)
 {
-	int res = 0;
 	struct impl *this = object;
 
 	spa_return_val_if_fail(this != NULL, -EINVAL);
 
+	if (param == NULL)
+		return 0;
+
 	switch (id) {
 	case SPA_PARAM_PortConfig:
 	{
-		enum spa_direction dir;
-		enum spa_param_port_config_mode mode;
-		struct spa_pod *format = NULL;
 		struct spa_audio_info info = { 0, }, *infop = NULL;
-		int monitor = false;
+		struct spa_pod *format = NULL;
+		enum spa_direction direction;
+		enum spa_param_port_config_mode mode;
+		bool monitor = false, control = false;
+		int res;
 
 		if (spa_pod_parse_object(param,
 				SPA_TYPE_OBJECT_ParamPortConfig, NULL,
-				SPA_PARAM_PORT_CONFIG_direction,	SPA_POD_Id(&dir),
+				SPA_PARAM_PORT_CONFIG_direction,	SPA_POD_Id(&direction),
 				SPA_PARAM_PORT_CONFIG_mode,		SPA_POD_Id(&mode),
 				SPA_PARAM_PORT_CONFIG_monitor,		SPA_POD_OPT_Bool(&monitor),
+				SPA_PARAM_PORT_CONFIG_control,		SPA_POD_OPT_Bool(&control),
 				SPA_PARAM_PORT_CONFIG_format,		SPA_POD_OPT_Pod(&format)) < 0)
 			return -EINVAL;
 
@@ -838,95 +1057,498 @@ static int impl_node_set_param(void *object, uint32_t id, uint32_t flags,
 
 			if (info.media_type != SPA_MEDIA_TYPE_audio ||
 			    info.media_subtype != SPA_MEDIA_SUBTYPE_raw)
-				return -ENOTSUP;
+				return -EINVAL;
 
 			if (spa_format_audio_raw_parse(format, &info.info.raw) < 0)
 				return -EINVAL;
 
-			if (info.info.raw.channels == 0 || info.info.raw.rate == 0)
+			if (info.info.raw.channels > SPA_AUDIO_MAX_CHANNELS)
 				return -EINVAL;
 
 			infop = &info;
 		}
 
-		spa_log_debug(this->log, "mode:%d direction:%d %d", mode, dir, monitor);
+		if ((res = reconfigure_mode(this, mode, direction, monitor, control, infop)) < 0)
+			return res;
 
-		switch (mode) {
-		case SPA_PARAM_PORT_CONFIG_MODE_passthrough:
-			return -ENOTSUP;
+		emit_node_info(this, false);
+		break;
+	}
+	case SPA_PARAM_Props:
+		if (apply_props(this, param) > 0)
+			emit_node_info(this, false);
+		break;
+	default:
+		return -ENOENT;
+	}
+	return 0;
+}
 
-		case SPA_PARAM_PORT_CONFIG_MODE_none:
-		case SPA_PARAM_PORT_CONFIG_MODE_convert:
-			break;
+static int int32_cmp(const void *v1, const void *v2)
+{
+	int32_t a1 = *(int32_t*)v1;
+	int32_t a2 = *(int32_t*)v2;
+	if (a1 == 0 && a2 != 0)
+		return 1;
+	if (a2 == 0 && a1 != 0)
+		return -1;
+	return a1 - a2;
+}
 
-		case SPA_PARAM_PORT_CONFIG_MODE_dsp:
-			info.info.raw.format = SPA_AUDIO_FORMAT_F32P;
+static int setup_in_convert(struct impl *this)
+{
+	uint32_t i, j;
+	struct dir *in = &this->dir[SPA_DIRECTION_INPUT];
+	struct spa_audio_info src_info, dst_info;
+	int res;
+	bool remap = false;
+
+	src_info = in->format;
+	dst_info = src_info;
+	dst_info.info.raw.format = SPA_AUDIO_FORMAT_DSP_F32;
+
+	spa_log_info(this->log, "%p: %s/%d@%d->%s/%d@%d", this,
+			spa_debug_type_find_name(spa_type_audio_format, src_info.info.raw.format),
+			src_info.info.raw.channels,
+			src_info.info.raw.rate,
+			spa_debug_type_find_name(spa_type_audio_format, dst_info.info.raw.format),
+			dst_info.info.raw.channels,
+			dst_info.info.raw.rate);
+
+	qsort(dst_info.info.raw.position, dst_info.info.raw.channels,
+					sizeof(uint32_t), int32_cmp);
+
+	for (i = 0; i < src_info.info.raw.channels; i++) {
+		for (j = 0; j < dst_info.info.raw.channels; j++) {
+			if (src_info.info.raw.position[i] !=
+			    dst_info.info.raw.position[j])
+				continue;
+			in->remap[i] = j;
+			if (i != j)
+				remap = true;
+			spa_log_debug(this->log, "%p: channel %d (%d) -> %d (%s -> %s)", this,
+					i, in->remap[i], j,
+					spa_debug_type_find_short_name(spa_type_audio_channel,
+						src_info.info.raw.position[i]),
+					spa_debug_type_find_short_name(spa_type_audio_channel,
+						dst_info.info.raw.position[j]));
+			dst_info.info.raw.position[j] = -1;
 			break;
-		default:
-			return -EINVAL;
 		}
+	}
+	if (in->conv.free)
+		convert_free(&in->conv);
+
+	in->conv.src_fmt = src_info.info.raw.format;
+	in->conv.dst_fmt = dst_info.info.raw.format;
+	in->conv.n_channels = dst_info.info.raw.channels;
+	in->conv.cpu_flags = this->cpu_flags;
+	in->need_remap = remap;
+
+	if ((res = convert_init(&in->conv)) < 0)
+		return res;
+
+	spa_log_debug(this->log, "%p: got converter features %08x:%08x passthrough:%d remap:%d %s", this,
+			this->cpu_flags, in->conv.cpu_flags, in->conv.is_passthrough,
+			remap, in->conv.func_name);
+
+	return 0;
+}
 
-		res = reconfigure_mode(this, mode, dir, monitor, infop);
+#define _MASK(ch)	(1ULL << SPA_AUDIO_CHANNEL_ ## ch)
+#define STEREO	(_MASK(FL)|_MASK(FR))
 
+static uint64_t default_mask(uint32_t channels)
+{
+	uint64_t mask = 0;
+	switch (channels) {
+	case 7:
+	case 8:
+		mask |= _MASK(RL);
+		mask |= _MASK(RR);
+		SPA_FALLTHROUGH
+	case 5:
+	case 6:
+		mask |= _MASK(SL);
+		mask |= _MASK(SR);
+		if ((channels & 1) == 0)
+			mask |= _MASK(LFE);
+		SPA_FALLTHROUGH
+	case 3:
+		mask |= _MASK(FC);
+		SPA_FALLTHROUGH
+	case 2:
+		mask |= _MASK(FL);
+		mask |= _MASK(FR);
 		break;
-	}
-	case SPA_PARAM_Props:
-	{
-		if (this->fmt[SPA_DIRECTION_INPUT] == this->merger)
-			res = spa_node_set_param(this->merger, id, flags, param);
-		res = spa_node_set_param(this->channelmix, id, flags, param);
-		res = spa_node_set_param(this->resample, id, flags, param);
+	case 1:
+		mask |= _MASK(MONO);
 		break;
-	}
-	default:
-		res = -ENOTSUP;
+	case 4:
+		mask |= _MASK(FL);
+		mask |= _MASK(FR);
+		mask |= _MASK(RL);
+		mask |= _MASK(RR);
 		break;
 	}
+	return mask;
+}
+
+static void fix_volumes(struct impl *this, struct volumes *vols, uint32_t channels)
+{
+	float s;
+	uint32_t i;
+	spa_log_debug(this->log, "%p %d -> %d", this, vols->n_volumes, channels);
+	if (vols->n_volumes > 0) {
+		s = 0.0f;
+		for (i = 0; i < vols->n_volumes; i++)
+			s += vols->volumes[i];
+		s /= vols->n_volumes;
+	} else {
+		s = 1.0f;
+	}
+	vols->n_volumes = channels;
+	for (i = 0; i < vols->n_volumes; i++)
+		vols->volumes[i] = s;
+}
+
+static int remap_volumes(struct impl *this, const struct spa_audio_info *info)
+{
+	struct props *p = &this->props;
+	uint32_t i, j, target = info->info.raw.channels;
+
+	for (i = 0; i < p->n_channels; i++) {
+		for (j = i; j < target; j++) {
+			spa_log_debug(this->log, "%d %d: %d <-> %d", i, j,
+					p->channel_map[i], info->info.raw.position[j]);
+			if (p->channel_map[i] != info->info.raw.position[j])
+				continue;
+			if (i != j) {
+				SPA_SWAP(p->channel_map[i], p->channel_map[j]);
+				SPA_SWAP(p->channel.volumes[i], p->channel.volumes[j]);
+				SPA_SWAP(p->soft.volumes[i], p->soft.volumes[j]);
+				SPA_SWAP(p->monitor.volumes[i], p->monitor.volumes[j]);
+			}
+			break;
+		}
+	}
+	p->n_channels = target;
+	for (i = 0; i < p->n_channels; i++)
+		p->channel_map[i] = info->info.raw.position[i];
+
+	if (target == 0)
+		return 0;
+	if (p->channel.n_volumes != target)
+		fix_volumes(this, &p->channel, target);
+	if (p->soft.n_volumes != target)
+		fix_volumes(this, &p->soft, target);
+	if (p->monitor.n_volumes != target)
+		fix_volumes(this, &p->monitor, target);
+
+	return 1;
+}
+
+static void set_volume(struct impl *this)
+{
+	struct volumes *vol;
+	uint32_t i;
+	float volumes[SPA_AUDIO_MAX_CHANNELS];
+	struct dir *dir = &this->dir[this->direction];
+
+	spa_log_debug(this->log, "%p", this);
+
+	if (dir->have_format)
+		remap_volumes(this, &dir->format);
+
+	if (this->mix.set_volume == NULL)
+		return;
+
+	if (this->props.have_soft_volume)
+		vol = &this->props.soft;
+	else
+		vol = &this->props.channel;
+
+	for (i = 0; i < vol->n_volumes; i++)
+		volumes[i] = vol->volumes[dir->remap[i]];
+
+	channelmix_set_volume(&this->mix, this->props.volume, vol->mute,
+			vol->n_volumes, volumes);
+
+	this->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS;
+	this->params[IDX_Props].user++;
+}
+
+static int setup_channelmix(struct impl *this)
+{
+	struct dir *in = &this->dir[SPA_DIRECTION_INPUT];
+	struct dir *out = &this->dir[SPA_DIRECTION_OUTPUT];
+	uint32_t i, src_chan, dst_chan, p;
+	uint64_t src_mask, dst_mask;
+	int res;
+
+	src_chan = in->format.info.raw.channels;
+	dst_chan = out->format.info.raw.channels;
+
+	for (i = 0, src_mask = 0; i < src_chan; i++) {
+		p = in->format.info.raw.position[i];
+		src_mask |= 1ULL << (p < 64 ? p : 0);
+	}
+	for (i = 0, dst_mask = 0; i < dst_chan; i++) {
+		p = out->format.info.raw.position[i];
+		dst_mask |= 1ULL << (p < 64 ? p : 0);
+	}
+
+	if (src_mask & 1)
+		src_mask = default_mask(src_chan);
+	if (dst_mask & 1)
+		dst_mask = default_mask(dst_chan);
+
+	spa_log_info(this->log, "%p: %s/%d@%d->%s/%d@%d %08"PRIx64":%08"PRIx64, this,
+			spa_debug_type_find_name(spa_type_audio_format, SPA_AUDIO_FORMAT_DSP_F32),
+			src_chan,
+			in->format.info.raw.rate,
+			spa_debug_type_find_name(spa_type_audio_format, SPA_AUDIO_FORMAT_DSP_F32),
+			dst_chan,
+			in->format.info.raw.rate,
+			src_mask, dst_mask);
+
+	this->mix.src_chan = src_chan;
+	this->mix.src_mask = src_mask;
+	this->mix.dst_chan = dst_chan;
+	this->mix.dst_mask = dst_mask;
+	this->mix.cpu_flags = this->cpu_flags;
+	this->mix.log = this->log;
+	this->mix.freq = in->format.info.raw.rate;
+
+	if ((res = channelmix_init(&this->mix)) < 0)
+		return res;
+
+	set_volume(this);
+
+	spa_log_debug(this->log, "%p: got channelmix features %08x:%08x flags:%08x %s",
+			this, this->cpu_flags, this->mix.cpu_flags,
+			this->mix.flags, this->mix.func_name);
+	return 0;
+}
+
+static int setup_resample(struct impl *this)
+{
+	struct dir *in = &this->dir[SPA_DIRECTION_INPUT];
+	struct dir *out = &this->dir[SPA_DIRECTION_OUTPUT];
+	int res;
+
+	spa_log_info(this->log, "%p: %s/%d@%d->%s/%d@%d", this,
+			spa_debug_type_find_name(spa_type_audio_format, SPA_AUDIO_FORMAT_DSP_F32),
+			out->format.info.raw.channels,
+			in->format.info.raw.rate,
+			spa_debug_type_find_name(spa_type_audio_format, SPA_AUDIO_FORMAT_DSP_F32),
+			out->format.info.raw.channels,
+			out->format.info.raw.rate);
+
+	if (this->resample.free)
+		resample_free(&this->resample);
+
+	this->resample.channels = out->format.info.raw.channels;
+	this->resample.i_rate = in->format.info.raw.rate;
+	this->resample.o_rate = out->format.info.raw.rate;
+	this->resample.log = this->log;
+	this->resample.quality = this->props.resample_quality;
+	this->resample.cpu_flags = this->cpu_flags;
+
+	if (this->peaks)
+		res = resample_peaks_init(&this->resample);
+	else
+		res = resample_native_init(&this->resample);
+
+	spa_log_debug(this->log, "%p: got resample features %08x:%08x %s",
+			this, this->cpu_flags, this->resample.cpu_flags,
+			this->resample.func_name);
 	return res;
 }
 
+static int calc_width(struct spa_audio_info *info)
+{
+	switch (info->info.raw.format) {
+	case SPA_AUDIO_FORMAT_U8:
+	case SPA_AUDIO_FORMAT_U8P:
+	case SPA_AUDIO_FORMAT_S8:
+	case SPA_AUDIO_FORMAT_S8P:
+	case SPA_AUDIO_FORMAT_ULAW:
+	case SPA_AUDIO_FORMAT_ALAW:
+		return 1;
+	case SPA_AUDIO_FORMAT_S16P:
+	case SPA_AUDIO_FORMAT_S16:
+	case SPA_AUDIO_FORMAT_S16_OE:
+		return 2;
+	case SPA_AUDIO_FORMAT_S24P:
+	case SPA_AUDIO_FORMAT_S24:
+	case SPA_AUDIO_FORMAT_S24_OE:
+		return 3;
+	case SPA_AUDIO_FORMAT_F64P:
+	case SPA_AUDIO_FORMAT_F64:
+	case SPA_AUDIO_FORMAT_F64_OE:
+		return 8;
+	default:
+		return 4;
+	}
+}
+
+static int setup_out_convert(struct impl *this)
+{
+	uint32_t i, j;
+	struct dir *out = &this->dir[SPA_DIRECTION_OUTPUT];
+	struct spa_audio_info src_info, dst_info;
+	int res;
+	bool remap = false;
+
+	dst_info = out->format;
+	src_info = dst_info;
+	src_info.info.raw.format = SPA_AUDIO_FORMAT_DSP_F32;
+
+	spa_log_info(this->log, "%p: %s/%d@%d->%s/%d@%d", this,
+			spa_debug_type_find_name(spa_type_audio_format, src_info.info.raw.format),
+			src_info.info.raw.channels,
+			src_info.info.raw.rate,
+			spa_debug_type_find_name(spa_type_audio_format, dst_info.info.raw.format),
+			dst_info.info.raw.channels,
+			dst_info.info.raw.rate);
+
+	qsort(src_info.info.raw.position, src_info.info.raw.channels,
+					sizeof(uint32_t), int32_cmp);
+
+	for (i = 0; i < src_info.info.raw.channels; i++) {
+		for (j = 0; j < dst_info.info.raw.channels; j++) {
+			if (src_info.info.raw.position[i] !=
+			    dst_info.info.raw.position[j])
+				continue;
+			out->remap[i] = j;
+			if (i != j)
+				remap = true;
+
+			spa_log_debug(this->log, "%p: channel %d (%d) -> %d (%s -> %s)", this,
+					i, out->remap[i], j,
+					spa_debug_type_find_short_name(spa_type_audio_channel,
+						src_info.info.raw.position[i]),
+					spa_debug_type_find_short_name(spa_type_audio_channel,
+						dst_info.info.raw.position[j]));
+			dst_info.info.raw.position[j] = -1;
+			break;
+		}
+	}
+	if (out->conv.free)
+		convert_free(&out->conv);
+
+	out->conv.src_fmt = src_info.info.raw.format;
+	out->conv.dst_fmt = dst_info.info.raw.format;
+	out->conv.rate = dst_info.info.raw.rate;
+	out->conv.n_channels = dst_info.info.raw.channels;
+	out->conv.cpu_flags = this->cpu_flags;
+	out->need_remap = remap;
+
+	if ((res = convert_init(&out->conv)) < 0)
+		return res;
+
+	spa_log_debug(this->log, "%p: got converter features %08x:%08x quant:%d:%d"
+			" passthrough:%d remap:%d %s", this,
+			this->cpu_flags, out->conv.cpu_flags, out->conv.method,
+			out->conv.noise_bits, out->conv.is_passthrough, remap, out->conv.func_name);
+
+	return 0;
+}
+
+static int setup_convert(struct impl *this)
+{
+	struct dir *in, *out;
+	uint32_t i, rate;
+	int res;
+
+	in = &this->dir[SPA_DIRECTION_INPUT];
+	out = &this->dir[SPA_DIRECTION_OUTPUT];
+
+	if (!in->have_format || !out->have_format)
+		return -EINVAL;
+
+	rate = this->io_position ?  this->io_position->clock.rate.denom : DEFAULT_RATE;
+
+	/* in DSP mode we always convert to the DSP rate */
+	if (in->mode == SPA_PARAM_PORT_CONFIG_MODE_dsp)
+		in->format.info.raw.rate = rate;
+	if (out->mode == SPA_PARAM_PORT_CONFIG_MODE_dsp)
+		out->format.info.raw.rate = rate;
+
+	/* try to passthrough the rates */
+	if (in->format.info.raw.rate == 0)
+		in->format.info.raw.rate = out->format.info.raw.rate;
+	else if (out->format.info.raw.rate == 0)
+		out->format.info.raw.rate = in->format.info.raw.rate;
+
+	/* try to passthrough the channels */
+	if (in->format.info.raw.channels == 0)
+		in->format.info.raw.channels = out->format.info.raw.channels;
+	else if (out->format.info.raw.channels == 0)
+		out->format.info.raw.channels = in->format.info.raw.channels;
+
+	if (in->format.info.raw.rate == 0 || out->format.info.raw.rate == 0)
+		return -EINVAL;
+	if (in->format.info.raw.channels == 0 || out->format.info.raw.channels == 0)
+		return -EINVAL;
+
+	if ((res = setup_in_convert(this)) < 0)
+		return res;
+	if ((res = setup_channelmix(this)) < 0)
+		return res;
+	if ((res = setup_resample(this)) < 0)
+		return res;
+	if ((res = setup_out_convert(this)) < 0)
+		return res;
+
+	for (i = 0; i < MAX_PORTS; i++) {
+		this->tmp_datas[0][i] = SPA_PTROFF(this->tmp[0], this->empty_size * i, void);
+		this->tmp_datas[0][i] = SPA_PTR_ALIGN(this->tmp_datas[0][i], MAX_ALIGN, void);
+		this->tmp_datas[1][i] = SPA_PTROFF(this->tmp[1], this->empty_size * i, void);
+		this->tmp_datas[1][i] = SPA_PTR_ALIGN(this->tmp_datas[1][i], MAX_ALIGN, void);
+	}
+
+	emit_node_info(this, false);
+
+	return 0;
+}
+
+static void reset_node(struct impl *this)
+{
+	if (this->resample.reset)
+		resample_reset(&this->resample);
+	this->in_offset = 0;
+	this->out_offset = 0;
+}
+
 static int impl_node_send_command(void *object, const struct spa_command *command)
 {
 	struct impl *this = object;
-	int res, i;
+	int res;
 
 	spa_return_val_if_fail(this != NULL, -EINVAL);
 	spa_return_val_if_fail(command != NULL, -EINVAL);
 
 	switch (SPA_NODE_COMMAND_ID(command)) {
 	case SPA_NODE_COMMAND_Start:
+		if (this->started)
+			return 0;
 		if ((res = setup_convert(this)) < 0)
 			return res;
-		if ((res = setup_buffers(this, SPA_DIRECTION_INPUT)) < 0)
-			return res;
+		this->started = true;
 		break;
-
 	case SPA_NODE_COMMAND_Suspend:
-		clean_convert(this);
-		SPA_FALLTHROUGH
+		SPA_FALLTHROUGH;
 	case SPA_NODE_COMMAND_Flush:
-		flush_convert(this);
-		SPA_FALLTHROUGH
+		reset_node(this);
+		SPA_FALLTHROUGH;
 	case SPA_NODE_COMMAND_Pause:
 		this->started = false;
 		break;
 	default:
 		return -ENOTSUP;
 	}
-
-	for (i = 0; i < this->n_nodes; i++) {
-		if ((res = spa_node_send_command(this->nodes[i], command)) < 0) {
-			spa_log_error(this->log, "%p: can't send command to node %d: %s",
-					this, i, spa_strerror(res));
-		}
-	}
-
-	switch (SPA_NODE_COMMAND_ID(command)) {
-	case SPA_NODE_COMMAND_Start:
-		this->started = true;
-		break;
-	}
-
 	return 0;
 }
 
@@ -937,37 +1559,21 @@ impl_node_add_listener(void *object,
 		void *data)
 {
 	struct impl *this = object;
+	uint32_t i;
 	struct spa_hook_list save;
-	struct spa_hook l[3];
 
 	spa_return_val_if_fail(this != NULL, -EINVAL);
 
-	spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
-
 	spa_log_trace(this->log, "%p: add listener %p", this, listener);
-
-	this->add_listener = true;
-
-	spa_zero(l);
-	if (this->fmt[SPA_DIRECTION_INPUT])
-		spa_node_add_listener(this->fmt[SPA_DIRECTION_INPUT],
-				&l[0], &fmt_input_events, this);
-	spa_node_add_listener(this->channelmix,
-			&l[1], &channelmix_events, this);
-	if (this->fmt[SPA_DIRECTION_OUTPUT])
-		spa_node_add_listener(this->fmt[SPA_DIRECTION_OUTPUT],
-				&l[2], &fmt_output_events, this);
-
-	if (this->fmt[SPA_DIRECTION_INPUT])
-		spa_hook_remove(&l[0]);
-	spa_hook_remove(&l[1]);
-	if (this->fmt[SPA_DIRECTION_OUTPUT])
-		spa_hook_remove(&l[2]);
-
-	this->add_listener = false;
+	spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
 
 	emit_node_info(this, true);
-
+	for (i = 0; i < this->dir[SPA_DIRECTION_INPUT].n_ports; i++) {
+		emit_port_info(this, GET_IN_PORT(this, i), true);
+	}
+	for (i = 0; i < this->dir[SPA_DIRECTION_OUTPUT].n_ports; i++) {
+		emit_port_info(this, GET_OUT_PORT(this, i), true);
+	}
 	spa_hook_list_join(&this->hooks, &save);
 
 	return 0;
@@ -978,7 +1584,7 @@ impl_node_set_callbacks(void *object,
 			const struct spa_node_callbacks *callbacks,
 			void *user_data)
 {
-	return -ENOTSUP;
+	return 0;
 }
 
 static int impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id,
@@ -993,81 +1599,374 @@ impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_
 	return -ENOTSUP;
 }
 
-static int
-impl_node_port_enum_params(void *object, int seq,
-			   enum spa_direction direction, uint32_t port_id,
-			   uint32_t id, uint32_t start, uint32_t num,
-			   const struct spa_pod *filter)
+static int port_enum_formats(void *object,
+			     enum spa_direction direction, uint32_t port_id,
+			     uint32_t index,
+			     struct spa_pod **param,
+			     struct spa_pod_builder *builder)
+{
+	struct impl *this = object;
+	struct port *port = GET_PORT(this, direction, port_id);
+
+	switch (index) {
+	case 0:
+		if (PORT_IS_DSP(this, direction, port_id)) {
+			struct spa_audio_info_dsp info;
+			info.format = SPA_AUDIO_FORMAT_DSP_F32;
+			*param = spa_format_audio_dsp_build(builder,
+				SPA_PARAM_EnumFormat, &info);
+		} else if (PORT_IS_CONTROL(this, direction, port_id)) {
+			*param = spa_pod_builder_add_object(builder,
+				SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat,
+				SPA_FORMAT_mediaType,      SPA_POD_Id(SPA_MEDIA_TYPE_application),
+				SPA_FORMAT_mediaSubtype,   SPA_POD_Id(SPA_MEDIA_SUBTYPE_control));
+		} else if (port->have_format) {
+			*param = spa_format_audio_raw_build(builder,
+				SPA_PARAM_EnumFormat, &this->dir[direction].format.info.raw);
+		}
+		else {
+			uint32_t rate = this->io_position ?
+				this->io_position->clock.rate.denom : DEFAULT_RATE;
+
+			*param = spa_pod_builder_add_object(builder,
+				SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat,
+				SPA_FORMAT_mediaType,      SPA_POD_Id(SPA_MEDIA_TYPE_audio),
+				SPA_FORMAT_mediaSubtype,   SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw),
+				SPA_FORMAT_AUDIO_format,   SPA_POD_CHOICE_ENUM_Id(25,
+							SPA_AUDIO_FORMAT_F32P,
+							SPA_AUDIO_FORMAT_F32P,
+							SPA_AUDIO_FORMAT_F32,
+							SPA_AUDIO_FORMAT_F32_OE,
+							SPA_AUDIO_FORMAT_F64P,
+							SPA_AUDIO_FORMAT_F64,
+							SPA_AUDIO_FORMAT_F64_OE,
+							SPA_AUDIO_FORMAT_S32P,
+							SPA_AUDIO_FORMAT_S32,
+							SPA_AUDIO_FORMAT_S32_OE,
+							SPA_AUDIO_FORMAT_S24_32P,
+							SPA_AUDIO_FORMAT_S24_32,
+							SPA_AUDIO_FORMAT_S24_32_OE,
+							SPA_AUDIO_FORMAT_S24P,
+							SPA_AUDIO_FORMAT_S24,
+							SPA_AUDIO_FORMAT_S24_OE,
+							SPA_AUDIO_FORMAT_S16P,
+							SPA_AUDIO_FORMAT_S16,
+							SPA_AUDIO_FORMAT_S16_OE,
+							SPA_AUDIO_FORMAT_S8P,
+							SPA_AUDIO_FORMAT_S8,
+							SPA_AUDIO_FORMAT_U8P,
+							SPA_AUDIO_FORMAT_U8,
+							SPA_AUDIO_FORMAT_ULAW,
+							SPA_AUDIO_FORMAT_ALAW),
+				SPA_FORMAT_AUDIO_rate,     SPA_POD_CHOICE_RANGE_Int(
+					rate, 1, INT32_MAX),
+				SPA_FORMAT_AUDIO_channels, SPA_POD_CHOICE_RANGE_Int(
+					DEFAULT_CHANNELS, 1, SPA_AUDIO_MAX_CHANNELS));
+		}
+		break;
+	default:
+		return 0;
+	}
+	return 1;
+}
+
+static int
+impl_node_port_enum_params(void *object, int seq,
+			   enum spa_direction direction, uint32_t port_id,
+			   uint32_t id, uint32_t start, uint32_t num,
+			   const struct spa_pod *filter)
+{
+	struct impl *this = object;
+	struct port *port;
+	struct spa_pod *param;
+	struct spa_pod_builder b = { 0 };
+	uint8_t buffer[2048];
+	struct spa_result_node_params result;
+	uint32_t count = 0;
+	int res;
+
+	spa_return_val_if_fail(this != NULL, -EINVAL);
+	spa_return_val_if_fail(num != 0, -EINVAL);
+
+	spa_log_debug(this->log, "%p: enum params port %d.%d %d %u",
+			this, direction, port_id, seq, id);
+
+	spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+	port = GET_PORT(this, direction, port_id);
+
+	result.id = id;
+	result.next = start;
+      next:
+	result.index = result.next++;
+
+	spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+	switch (id) {
+	case SPA_PARAM_EnumFormat:
+		if ((res = port_enum_formats(object, direction, port_id, result.index, &param, &b)) <= 0)
+			return res;
+		break;
+	case SPA_PARAM_Format:
+		if (!port->have_format)
+			return -EIO;
+		if (result.index > 0)
+			return 0;
+
+		if (PORT_IS_DSP(this, direction, port_id))
+			param = spa_format_audio_dsp_build(&b, id, &port->format.info.dsp);
+		else if (PORT_IS_CONTROL(this, direction, port_id))
+			param = spa_pod_builder_add_object(&b,
+				SPA_TYPE_OBJECT_Format,  id,
+				SPA_FORMAT_mediaType,    SPA_POD_Id(SPA_MEDIA_TYPE_application),
+				SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_control));
+		else
+			param = spa_format_audio_raw_build(&b, id, &port->format.info.raw);
+		break;
+	case SPA_PARAM_Buffers:
+	{
+		uint32_t size;
+		struct dir *dir;
+
+		if (!port->have_format)
+			return -EIO;
+		if (result.index > 0)
+			return 0;
+
+		dir = &this->dir[direction];
+		if (dir->mode == SPA_PARAM_PORT_CONFIG_MODE_dsp) {
+			/* DSP ports always use the quantum_limit as the buffer
+			 * size. */
+			size = this->quantum_limit;
+		} else {
+			uint32_t irate, orate;
+			/* Convert ports are scaled so that they can always
+			 * provide one quantum of data */
+			irate = dir->format.info.raw.rate;
+
+			/* collect the other port rate */
+			dir = &this->dir[SPA_DIRECTION_REVERSE(direction)];
+			if (dir->mode == SPA_PARAM_PORT_CONFIG_MODE_dsp)
+				orate = this->io_position ?  this->io_position->clock.rate.denom : DEFAULT_RATE;
+			else
+				orate = dir->format.info.raw.rate;
+
+			/* always keep some extra room for adaptive resampling */
+			size = this->quantum_limit * 2;
+			/*  scale the buffer size when we can. */
+			if (irate != 0 && orate != 0)
+				size = size * (irate + orate - 1) / orate;
+		}
+
+		param = spa_pod_builder_add_object(&b,
+			SPA_TYPE_OBJECT_ParamBuffers, id,
+			SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(2, 1, MAX_BUFFERS),
+			SPA_PARAM_BUFFERS_blocks,  SPA_POD_Int(port->blocks),
+			SPA_PARAM_BUFFERS_size,    SPA_POD_CHOICE_RANGE_Int(
+								size * port->stride,
+								16 * port->stride,
+								INT32_MAX),
+			SPA_PARAM_BUFFERS_stride,  SPA_POD_Int(port->stride));
+		break;
+	}
+	case SPA_PARAM_Meta:
+		switch (result.index) {
+		case 0:
+			param = spa_pod_builder_add_object(&b,
+				SPA_TYPE_OBJECT_ParamMeta, id,
+				SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header),
+				SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header)));
+			break;
+		default:
+			return 0;
+		}
+		break;
+	case SPA_PARAM_IO:
+		switch (result.index) {
+		case 0:
+			param = spa_pod_builder_add_object(&b,
+				SPA_TYPE_OBJECT_ParamIO, id,
+				SPA_PARAM_IO_id,   SPA_POD_Id(SPA_IO_Buffers),
+				SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers)));
+			break;
+		default:
+			return 0;
+		}
+		break;
+	case SPA_PARAM_Latency:
+		switch (result.index) {
+		case 0: case 1:
+			param = spa_latency_build(&b, id, &this->dir[result.index].latency);
+			break;
+		default:
+			return 0;
+		}
+		break;
+	default:
+		return -ENOENT;
+	}
+
+	if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+		goto next;
+
+	spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+	if (++count != num)
+		goto next;
+
+	return 0;
+}
+
+static int clear_buffers(struct impl *this, struct port *port)
+{
+	if (port->n_buffers > 0) {
+		spa_log_debug(this->log, "%p: clear buffers %p", this, port);
+		port->n_buffers = 0;
+		spa_list_init(&port->queue);
+	}
+	return 0;
+}
+
+static int port_set_latency(void *object,
+			   enum spa_direction direction,
+			   uint32_t port_id,
+			   uint32_t flags,
+			   const struct spa_pod *latency)
+{
+	struct impl *this = object;
+	struct port *port;
+	enum spa_direction other = SPA_DIRECTION_REVERSE(direction);
+	uint32_t i;
+
+	spa_log_debug(this->log, "%p: set latency direction:%d id:%d",
+			this, direction, port_id);
+
+	port = GET_PORT(this, direction, port_id);
+	if (port->is_monitor)
+		return 0;
+
+	if (latency == NULL) {
+		this->dir[other].latency = SPA_LATENCY_INFO(other);
+	} else {
+		struct spa_latency_info info;
+		if (spa_latency_parse(latency, &info) < 0 ||
+		    info.direction != other)
+			return -EINVAL;
+		this->dir[other].latency = info;
+	}
+
+	for (i = 0; i < this->dir[other].n_ports; i++) {
+		port = GET_PORT(this, other, i);
+		port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+		port->params[IDX_Latency].user++;
+		emit_port_info(this, port, false);
+	}
+	port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+	port->params[IDX_Latency].user++;
+	emit_port_info(this, port, false);
+	return 0;
+}
+
+static int port_set_format(void *object,
+			   enum spa_direction direction,
+			   uint32_t port_id,
+			   uint32_t flags,
+			   const struct spa_pod *format)
 {
 	struct impl *this = object;
-	struct spa_pod *param;
-	struct spa_pod_builder b = { 0 };
-	uint8_t buffer[4096];
-	struct spa_result_node_params result;
-	uint32_t count = 0;
+	struct port *port;
 	int res;
 
-	spa_return_val_if_fail(this != NULL, -EINVAL);
-	spa_return_val_if_fail(num != 0, -EINVAL);
-
-	spa_log_debug(this->log, "%p: port %d.%d %d %u", this, direction, port_id, seq, id);
+	port = GET_PORT(this, direction, port_id);
 
-	result.id = id;
-	result.next = start;
-      next:
-	result.index = result.next;
+	spa_log_debug(this->log, "%p: set format", this);
 
-	spa_pod_builder_init(&b, buffer, sizeof(buffer));
+	if (format == NULL) {
+		port->have_format = false;
+		clear_buffers(this, port);
+	} else {
+		struct spa_audio_info info = { 0 };
 
-	switch (id) {
-	case SPA_PARAM_IO:
-		switch (result.index) {
-		case 0:
-			param = spa_pod_builder_add_object(&b,
-				SPA_TYPE_OBJECT_ParamIO, id,
-				SPA_PARAM_IO_id,	SPA_POD_Id(SPA_IO_Buffers),
-				SPA_PARAM_IO_size,	SPA_POD_Int(sizeof(struct spa_io_buffers)));
-			break;
-		case 1:
-			param = spa_pod_builder_add_object(&b,
-				SPA_TYPE_OBJECT_ParamIO, id,
-				SPA_PARAM_IO_id,   SPA_POD_Id(SPA_IO_RateMatch),
-				SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_rate_match)));
-			break;
-		default:
-			return 0;
+		if ((res = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0) {
+			spa_log_error(this->log, "can't parse format %s", spa_strerror(res));
+			return res;
 		}
-		result.next++;
-		break;
-	default:
-	{
-		struct spa_node *target;
-
-		if (IS_MONITOR_PORT(this, direction, port_id))
-			target = this->fmt[SPA_DIRECTION_INPUT];
-		else
-			target = this->fmt[direction];
+		if (PORT_IS_DSP(this, direction, port_id)) {
+			if (info.media_type != SPA_MEDIA_TYPE_audio ||
+			    info.media_subtype != SPA_MEDIA_SUBTYPE_dsp) {
+				spa_log_error(this->log, "unexpected types %d/%d",
+						info.media_type, info.media_subtype);
+				return -EINVAL;
+			}
+			if ((res = spa_format_audio_dsp_parse(format, &info.info.dsp)) < 0) {
+				spa_log_error(this->log, "can't parse format %s", spa_strerror(res));
+				return res;
+			}
+			if (info.info.dsp.format != SPA_AUDIO_FORMAT_DSP_F32) {
+				spa_log_error(this->log, "unexpected format %d<->%d",
+					info.info.dsp.format, SPA_AUDIO_FORMAT_DSP_F32);
+				return -EINVAL;
+			}
+			port->blocks = 1;
+			port->stride = 4;
+		}
+		else if (PORT_IS_CONTROL(this, direction, port_id)) {
+			if (info.media_type != SPA_MEDIA_TYPE_application ||
+			    info.media_subtype != SPA_MEDIA_SUBTYPE_control) {
+				spa_log_error(this->log, "unexpected types %d/%d",
+						info.media_type, info.media_subtype);
+				return -EINVAL;
+			}
+			port->blocks = 1;
+			port->stride = 1;
+		}
+		else {
+			if (info.media_type != SPA_MEDIA_TYPE_audio ||
+			    info.media_subtype != SPA_MEDIA_SUBTYPE_raw) {
+				spa_log_error(this->log, "unexpected types %d/%d",
+						info.media_type, info.media_subtype);
+				return -EINVAL;
+			}
+			if ((res = spa_format_audio_raw_parse(format, &info.info.raw)) < 0) {
+				spa_log_error(this->log, "can't parse format %s", spa_strerror(res));
+				return res;
+			}
+			if (info.info.raw.channels > SPA_AUDIO_MAX_CHANNELS) {
+				spa_log_error(this->log, "too many channels %d > %d",
+						info.info.raw.channels, SPA_AUDIO_MAX_CHANNELS);
+				return -EINVAL;
+			}
+			port->stride = calc_width(&info);
+			if (SPA_AUDIO_FORMAT_IS_PLANAR(info.info.raw.format)) {
+				port->blocks = info.info.raw.channels;
+			} else {
+				port->stride *= info.info.raw.channels;
+				port->blocks = 1;
+			}
+			this->dir[direction].format = info;
+			this->dir[direction].have_format = true;
+		}
+		port->format = info;
+		port->have_format = true;
 
-		res = spa_node_port_enum_params_sync(target,
-				       direction, port_id,
-				       id, &result.next,
-				       NULL, &param, &b);
-		if (res != 1)
-			return res;
-	}
+		spa_log_debug(this->log, "%p: %d %d %d", this,
+				port_id, port->stride, port->blocks);
 	}
 
-	if (spa_pod_filter(&b, &result.param, param, filter) < 0)
-		goto next;
-
-	spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
-
-	if (++count != num)
-		goto next;
+	port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+	if (port->have_format) {
+		port->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE);
+		port->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ);
+	} else {
+		port->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+		port->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+	}
+	emit_port_info(this, port, false);
 
 	return 0;
 }
 
+
 static int
 impl_node_port_set_param(void *object,
 			 enum spa_direction direction, uint32_t port_id,
@@ -1075,41 +1974,56 @@ impl_node_port_set_param(void *object,
 			 const struct spa_pod *param)
 {
 	struct impl *this = object;
-	int res;
-	struct spa_node *target;
-	bool is_monitor;
 
 	spa_return_val_if_fail(this != NULL, -EINVAL);
 
-	spa_log_debug(this->log, "%p: set param %u on port %d:%d %p",
-				this, id, direction, port_id, param);
+	spa_log_debug(this->log, "%p: set param port %d.%d %u",
+			this, direction, port_id, id);
+
+	spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
 
 	switch (id) {
+	case SPA_PARAM_Latency:
+		return port_set_latency(this, direction, port_id, flags, param);
+	case SPA_PARAM_Format:
+		return port_set_format(this, direction, port_id, flags, param);
 	default:
-		is_monitor = IS_MONITOR_PORT(this, direction, port_id);
-		if (is_monitor)
-			target = this->fmt[SPA_DIRECTION_INPUT];
-		else
-			target = this->fmt[direction];
-		break;
+		return -ENOENT;
 	}
+}
 
-	if ((res = spa_node_port_set_param(target,
-					direction, port_id, id, flags, param)) < 0)
-		return res;
+static void queue_buffer(struct impl *this, struct port *port, uint32_t id)
+{
+	struct buffer *b = &port->buffers[id];
 
-	switch (id) {
-	case SPA_PARAM_Latency:
-		if (port_id == 0) {
-			target = this->fmt[SPA_DIRECTION_REVERSE(direction)];
-			if ((res = spa_node_port_set_param(target,
-						direction, port_id, id, flags, param)) < 0)
-				return res;
-		}
-		break;
-	}
+	spa_log_trace_fp(this->log, "%p: queue buffer %d on port %d %d",
+			this, id, port->id, b->flags);
+	if (SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_QUEUED))
+		return;
 
-	return res;
+	spa_list_append(&port->queue, &b->link);
+	SPA_FLAG_SET(b->flags, BUFFER_FLAG_QUEUED);
+}
+
+static struct buffer *peek_buffer(struct impl *this, struct port *port)
+{
+	struct buffer *b;
+
+	if (spa_list_is_empty(&port->queue))
+		return NULL;
+
+	b = spa_list_first(&port->queue, struct buffer, link);
+	spa_log_trace_fp(this->log, "%p: peek buffer %d on port %d %u",
+			this, b->id, port->id, b->flags);
+	return b;
+}
+
+static void dequeue_buffer(struct impl *this, struct port *port, struct buffer *b)
+{
+	spa_list_remove(&b->link);
+	SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_QUEUED);
+	spa_log_trace_fp(this->log, "%p: dequeue buffer %d on port %d %u",
+			this, b->id, port->id, b->flags);
 }
 
 static int
@@ -1121,21 +2035,75 @@ impl_node_port_use_buffers(void *object,
 			   uint32_t n_buffers)
 {
 	struct impl *this = object;
-	int res;
-	struct spa_node *target;
+	struct port *port;
+	uint32_t i, j, maxsize;
 
 	spa_return_val_if_fail(this != NULL, -EINVAL);
 
-	if (IS_MONITOR_PORT(this, direction, port_id))
-		target = this->fmt[SPA_DIRECTION_INPUT];
-	else
-		target = this->fmt[direction];
+	spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
 
-	if ((res = spa_node_port_use_buffers(target,
-					direction, port_id, flags, buffers, n_buffers)) < 0)
-		return res;
+	port = GET_PORT(this, direction, port_id);
 
-	return res;
+	spa_return_val_if_fail(port->have_format, -EIO);
+
+	spa_log_debug(this->log, "%p: use buffers %d on port %d:%d",
+			this, n_buffers, direction, port_id);
+
+	clear_buffers(this, port);
+
+	maxsize = this->quantum_limit * sizeof(float);
+
+	for (i = 0; i < n_buffers; i++) {
+		struct buffer *b;
+		uint32_t n_datas = buffers[i]->n_datas;
+		struct spa_data *d = buffers[i]->datas;
+
+		b = &port->buffers[i];
+		b->id = i;
+		b->flags = 0;
+		b->buf = buffers[i];
+
+		if (n_datas != port->blocks) {
+			spa_log_error(this->log, "%p: invalid blocks %d on buffer %d",
+					this, n_datas, i);
+			return -EINVAL;
+		}
+
+		for (j = 0; j < n_datas; j++) {
+			if (d[j].data == NULL) {
+				spa_log_error(this->log, "%p: invalid memory %d on buffer %d %d %p",
+						this, j, i, d[j].type, d[j].data);
+				return -EINVAL;
+			}
+			if (!SPA_IS_ALIGNED(d[j].data, this->max_align)) {
+				spa_log_warn(this->log, "%p: memory %d on buffer %d not aligned",
+						this, j, i);
+			}
+			if (direction == SPA_DIRECTION_OUTPUT &&
+			    !SPA_FLAG_IS_SET(d[j].flags, SPA_DATA_FLAG_DYNAMIC))
+				this->is_passthrough = false;
+
+			b->datas[j] = d[j].data;
+
+			maxsize = SPA_MAX(maxsize, d[j].maxsize);
+		}
+		if (direction == SPA_DIRECTION_OUTPUT)
+			queue_buffer(this, port, i);
+	}
+	if (maxsize > this->empty_size) {
+		this->empty = realloc(this->empty, maxsize + MAX_ALIGN);
+		this->scratch = realloc(this->scratch, maxsize + MAX_ALIGN);
+		this->tmp[0] = realloc(this->tmp[0], (maxsize + MAX_ALIGN) * MAX_PORTS);
+		this->tmp[1] = realloc(this->tmp[1], (maxsize + MAX_ALIGN) * MAX_PORTS);
+		if (this->empty == NULL || this->scratch == NULL ||
+		    this->tmp[0] == NULL || this->tmp[1] == NULL)
+			return -errno;
+		memset(this->empty, 0, maxsize + MAX_ALIGN);
+		this->empty_size = maxsize;
+	}
+	port->n_buffers = n_buffers;
+
+	return 0;
 }
 
 static int
@@ -1144,81 +2112,592 @@ impl_node_port_set_io(void *object,
 		      uint32_t id, void *data, size_t size)
 {
 	struct impl *this = object;
-	struct spa_node *target;
-	int res;
+	struct port *port;
 
 	spa_return_val_if_fail(this != NULL, -EINVAL);
 
-	spa_log_debug(this->log, "set io %d %d %d", id, direction, port_id);
+	spa_log_debug(this->log, "%p: set io %d on port %d:%d %p",
+			this, id, direction, port_id, data);
+
+	spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+	port = GET_PORT(this, direction, port_id);
 
 	switch (id) {
+	case SPA_IO_Buffers:
+		port->io = data;
+		break;
 	case SPA_IO_RateMatch:
-		res = spa_node_port_set_io(this->resample, direction, 0, id, data, size);
+		this->io_rate_match = data;
 		break;
 	default:
-		if (IS_MONITOR_PORT(this, direction, port_id))
-			target = this->fmt[SPA_DIRECTION_INPUT];
-		else
-			target = this->fmt[direction];
-
-		res = spa_node_port_set_io(target, direction, port_id, id, data, size);
-		break;
+		return -ENOENT;
 	}
-	return res;
+	return 0;
 }
 
 static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id)
 {
 	struct impl *this = object;
-	struct spa_node *target;
+	struct port *port;
 
 	spa_return_val_if_fail(this != NULL, -EINVAL);
+	spa_return_val_if_fail(CHECK_PORT(this, SPA_DIRECTION_OUTPUT, port_id), -EINVAL);
 
-	if (IS_MONITOR_PORT(this, SPA_DIRECTION_OUTPUT, port_id))
-		target = this->fmt[SPA_DIRECTION_INPUT];
-	else
-		target = this->fmt[SPA_DIRECTION_OUTPUT];
+	port = GET_OUT_PORT(this, port_id);
+	queue_buffer(this, port, buffer_id);
+
+	return 0;
+}
+
+static int channelmix_process_control(struct impl *this, struct port *ctrlport,
+				      void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
+				      uint32_t n_samples)
+{
+	struct spa_pod_control *c, *prev = NULL;
+	uint32_t avail_samples = n_samples;
+	uint32_t i;
+	const float *s[MAX_PORTS], **ss = (const float**) src;
+	float *d[MAX_PORTS], **sd = (float **) dst;
+	const struct spa_pod_sequence_body *body = &(ctrlport->ctrl)->body;
+	uint32_t size = SPA_POD_BODY_SIZE(ctrlport->ctrl);
+	bool end = false;
+
+	c = spa_pod_control_first(body);
+	while (true) {
+		uint32_t chunk;
+
+		if (c == NULL || !spa_pod_control_is_inside(body, size, c)) {
+			c = NULL;
+			end = true;
+		}
+		if (avail_samples == 0)
+			break;
+
+		/* ignore old control offsets */
+		if (c != NULL) {
+			if (c->offset <= ctrlport->ctrl_offset) {
+				prev = c;
+				if (c != NULL)
+					c = spa_pod_control_next(c);
+				continue;
+			}
+			chunk = SPA_MIN(avail_samples, c->offset - ctrlport->ctrl_offset);
+			spa_log_trace_fp(this->log, "%p: process %d-%d %d/%d", this,
+					ctrlport->ctrl_offset, c->offset, chunk, avail_samples);
+		} else {
+			chunk = avail_samples;
+			spa_log_trace_fp(this->log, "%p: process remain %d", this, chunk);
+		}
+
+		if (prev) {
+			switch (prev->type) {
+			case SPA_CONTROL_Midi:
+				apply_midi(this, &prev->value);
+				break;
+			case SPA_CONTROL_Properties:
+				apply_props(this, &prev->value);
+				break;
+			default:
+				continue;
+			}
+		}
+		if (ss == (const float**)src && chunk != avail_samples) {
+			for (i = 0; i < this->mix.src_chan; i++)
+				s[i] = ss[i];
+			for (i = 0; i < this->mix.dst_chan; i++)
+				d[i] = sd[i];
+			ss = s;
+			sd = d;
+		}
+
+		channelmix_process(&this->mix, (void**)sd, (const void**)ss, chunk);
+
+		if (chunk != avail_samples) {
+			for (i = 0; i < this->mix.src_chan; i++)
+				ss[i] += chunk;
+			for (i = 0; i < this->mix.dst_chan; i++)
+				sd[i] += chunk;
+		}
+		avail_samples -= chunk;
+		ctrlport->ctrl_offset += chunk;
+	}
+	return end ? 1 : 0;
+}
+
+static uint32_t resample_get_in_size(struct impl *this, bool passthrough, uint32_t out_size)
+{
+	uint32_t match_size = passthrough ? out_size : resample_in_len(&this->resample, out_size);
+	spa_log_trace_fp(this->log, "%p: current match %u", this, match_size);
+	return match_size;
+}
+
+static uint32_t resample_update_rate_match(struct impl *this, bool passthrough, uint32_t out_size, uint32_t in_queued)
+{
+	uint32_t delay, match_size;
 
-	return spa_node_port_reuse_buffer(target, port_id, buffer_id);
+	if (passthrough) {
+		delay = 0;
+		match_size = out_size;
+	} else {
+		double rate = this->rate_scale / this->props.rate;
+		if (this->io_rate_match &&
+		    SPA_FLAG_IS_SET(this->io_rate_match->flags, SPA_IO_RATE_MATCH_FLAG_ACTIVE))
+			rate *= this->io_rate_match->rate;
+		resample_update_rate(&this->resample, rate);
+		delay = resample_delay(&this->resample);
+		match_size = resample_in_len(&this->resample, out_size);
+	}
+	match_size -= SPA_MIN(match_size, in_queued);
+
+	spa_log_trace_fp(this->log, "%p: next match %u", this, match_size);
+
+	if (this->io_rate_match) {
+		this->io_rate_match->delay = delay;
+		this->io_rate_match->size = match_size;
+	}
+	return match_size;
+}
+
+static inline bool resample_is_passthrough(struct impl *this)
+{
+	return this->resample.i_rate == this->resample.o_rate && this->rate_scale == 1.0 &&
+		this->props.rate == 1.0 &&
+		(this->io_rate_match == NULL ||
+		 !SPA_FLAG_IS_SET(this->io_rate_match->flags, SPA_IO_RATE_MATCH_FLAG_ACTIVE));
 }
 
 static int impl_node_process(void *object)
 {
 	struct impl *this = object;
-	int r, i, res = SPA_STATUS_OK;
-	int ready;
+	const void *src_datas[MAX_PORTS], **in_datas;
+	void *dst_datas[MAX_PORTS], *remap_src_datas[MAX_PORTS], *remap_dst_datas[MAX_PORTS];
+	void **out_datas, **dst_remap;
+	uint32_t i, j, n_src_datas = 0, n_dst_datas = 0, n_mon_datas = 0, remap;
+	uint32_t n_samples, max_in, n_out, max_out, quant_samples;
+	struct port *port, *ctrlport = NULL;
+	struct buffer *buf, *out_bufs[MAX_PORTS];
+	struct spa_data *bd;
+	struct dir *dir;
+	int tmp = 0, res = 0;
+	bool in_passthrough, mix_passthrough, resample_passthrough, out_passthrough;
+	bool in_avail = false, flush_in = false, flush_out = false, draining = false, in_empty = true;
+	struct spa_io_buffers *io, *ctrlio = NULL;
+	const struct spa_pod_sequence *ctrl = NULL;
+
+	/* calculate quantum scale, this is how many samples we need to produce or
+	 * consume. Also update the rate scale, this is sent to the resampler to adjust
+	 * the rate, either when the graph clock changed or when the user adjusted the
+	 * rate.  */
+	if (SPA_LIKELY(this->io_position)) {
+		double r =  this->rate_scale;
+
+		quant_samples = this->io_position->clock.duration;
+		if (this->direction == SPA_DIRECTION_INPUT) {
+			if (this->io_position->clock.rate.denom != this->resample.o_rate)
+				r = (double) this->io_position->clock.rate.denom / this->resample.o_rate;
+			else
+				r = 1.0;
+		} else {
+			if (this->io_position->clock.rate.denom != this->resample.i_rate)
+				r = (double) this->resample.i_rate / this->io_position->clock.rate.denom;
+			else
+				r = 1.0;
+		}
+		if (this->rate_scale != r) {
+			spa_log_info(this->log, "scale %f->%f", this->rate_scale, r);
+			this->rate_scale = r;
+		}
+	}
+	else
+		quant_samples = this->quantum_limit;
+
+	dir = &this->dir[SPA_DIRECTION_INPUT];
+	in_passthrough = dir->conv.is_passthrough;
+	max_in = UINT32_MAX;
+
+	/* collect input port data */
+	for (i = 0; i < dir->n_ports; i++) {
+		port = GET_IN_PORT(this, i);
+
+		if (SPA_UNLIKELY((io = port->io) == NULL)) {
+			spa_log_trace_fp(this->log, "%p: no io on input port %d",
+					this, port->id);
+			buf = NULL;
+		} else if (SPA_UNLIKELY(io->status != SPA_STATUS_HAVE_DATA)) {
+			if (io->status & SPA_STATUS_DRAINED) {
+				spa_log_debug(this->log, "%p: port %d drained", this, port->id);
+				in_avail = flush_in = draining = true;
+			} else {
+				spa_log_trace_fp(this->log, "%p: empty input port %d %p %d %d %d",
+						this, port->id, io, io->status, io->buffer_id,
+						port->n_buffers);
+				this->drained = false;
+			}
+			buf = NULL;
+		} else if (SPA_UNLIKELY(io->buffer_id >= port->n_buffers)) {
+			spa_log_trace_fp(this->log, "%p: invalid input buffer port %d %p %d %d %d",
+					this, port->id, io, io->status, io->buffer_id,
+					port->n_buffers);
+			io->status = -EINVAL;
+			buf = NULL;
+		} else {
+			spa_log_trace_fp(this->log, "%p: input buffer port %d io:%p status:%d id:%d n:%d",
+					this, port->id, io, io->status, io->buffer_id,
+					port->n_buffers);
+			buf = &port->buffers[io->buffer_id];
+		}
+
+		if (SPA_UNLIKELY(buf == NULL)) {
+			for (j = 0; j < port->blocks; j++) {
+				if (port->is_control) {
+					spa_log_trace_fp(this->log, "%p: empty control %d", this,
+							i * port->blocks + j);
+				} else {
+					remap = n_src_datas++;
+					src_datas[remap] = SPA_PTR_ALIGN(this->empty, MAX_ALIGN, void);
+					spa_log_trace_fp(this->log, "%p: empty input %d->%d", this,
+							i * port->blocks + j, remap);
+					max_in = SPA_MIN(max_in, this->empty_size / port->stride);
+				}
+			}
+		} else {
+			in_avail = true;
+			for (j = 0; j < port->blocks; j++) {
+				uint32_t offs, size;
+
+				bd = &buf->buf->datas[j];
+
+				offs = SPA_MIN(bd->chunk->offset, bd->maxsize);
+				size = SPA_MIN(bd->maxsize - offs, bd->chunk->size);
+				if (!SPA_FLAG_IS_SET(bd->chunk->flags, SPA_CHUNK_FLAG_EMPTY))
+					in_empty = false;
+
+				if (SPA_UNLIKELY(port->is_control)) {
+					spa_log_trace_fp(this->log, "%p: control %d", this,
+							i * port->blocks + j);
+					ctrlport = port;
+					ctrlio = io;
+					ctrl = spa_pod_from_data(bd->data, bd->maxsize,
+							bd->chunk->offset, bd->chunk->size);
+					if (ctrl && !spa_pod_is_sequence(&ctrl->pod))
+						ctrl = NULL;
+					if (ctrl != ctrlport->ctrl) {
+						ctrlport->ctrl = ctrl;
+						ctrlport->ctrl_offset = 0;
+					}
+				} else  {
+					max_in = SPA_MIN(max_in, size / port->stride);
+
+					remap = n_src_datas++;
+					offs += this->in_offset * port->stride;
+					src_datas[remap] = SPA_PTROFF(bd->data, offs, void);
+
+					spa_log_trace_fp(this->log, "%p: input %d:%d:%d %d %d %d->%d", this,
+							offs, size, port->stride, this->in_offset, max_in,
+							i * port->blocks + j, remap);
+				}
+			}
+		}
+	}
+
+	/* calculate how many samples we are going to produce. */
+	if (this->direction == SPA_DIRECTION_INPUT) {
+		/* in split mode we need to output exactly the size of the
+		 * duration so we don't try to flush early */
+		max_out = quant_samples;
+		flush_out = false;
+	} else {
+		/* in merge mode we consume one duration of samples and
+		 * always output the resulting data */
+		max_out = this->quantum_limit;
+		flush_out = true;
+	}
+
+	dir = &this->dir[SPA_DIRECTION_OUTPUT];
+	/* collect output ports and monitor ports data */
+	for (i = 0; i < dir->n_ports; i++) {
+		port = GET_OUT_PORT(this, i);
+
+		if (SPA_UNLIKELY((io = port->io) == NULL ||
+		    io->status == SPA_STATUS_HAVE_DATA)) {
+			buf = NULL;
+		}
+		else {
+			if (SPA_LIKELY(io->buffer_id < port->n_buffers))
+				queue_buffer(this, port, io->buffer_id);
+
+			buf = peek_buffer(this, port);
+		}
+		out_bufs[i] = buf;
+
+		if (SPA_UNLIKELY(buf == NULL)) {
+			for (j = 0; j < port->blocks; j++) {
+				if (port->is_monitor) {
+					remap = n_mon_datas++;
+					spa_log_trace_fp(this->log, "%p: empty monitor %d", this,
+						remap);
+				} else if (port->is_control) {
+					spa_log_trace_fp(this->log, "%p: empty control %d", this, j);
+				} else {
+					remap = n_dst_datas++;
+					dst_datas[remap] = SPA_PTR_ALIGN(this->scratch, MAX_ALIGN, void);
+					spa_log_trace_fp(this->log, "%p: empty output %d->%d", this,
+						i * port->blocks + j, remap);
+					max_out = SPA_MIN(max_out, this->empty_size / port->stride);
+				}
+			}
+		} else {
+			for (j = 0; j < port->blocks; j++) {
+				bd = &buf->buf->datas[j];
+
+				bd->chunk->offset = 0;
+				bd->chunk->size = 0;
+				if (port->is_monitor) {
+					float volume;
+					uint32_t mon_max;
+
+					remap = n_mon_datas++;
+					volume = this->props.monitor.mute ? 0.0f : this->props.monitor.volumes[remap];
+					if (this->monitor_channel_volumes)
+						volume *= this->props.channel.mute ? 0.0f :
+							this->props.channel.volumes[remap];
+
+					mon_max = SPA_MIN(bd->maxsize / port->stride, max_in);
+
+					volume_process(&this->volume, bd->data, src_datas[remap],
+							volume, mon_max);
+
+					bd->chunk->size = mon_max * port->stride;
+
+					spa_log_trace_fp(this->log, "%p: monitor %d %d", this,
+							remap, mon_max);
+
+					dequeue_buffer(this, port, buf);
+					io->status = SPA_STATUS_HAVE_DATA;
+					io->buffer_id = buf->id;
+					res |= SPA_STATUS_HAVE_DATA;
+				} else if (SPA_UNLIKELY(port->is_control)) {
+					spa_log_trace_fp(this->log, "%p: control %d", this, j);
+				} else {
+					remap = n_dst_datas++;
+					dst_datas[remap] = SPA_PTROFF(bd->data,
+							this->out_offset * port->stride, void);
+					max_out = SPA_MIN(max_out, bd->maxsize / port->stride);
+
+					spa_log_trace_fp(this->log, "%p: output %d offs:%d %d->%d", this,
+							max_out, this->out_offset,
+							i * port->blocks + j, remap);
+				}
+			}
+		}
+	}
 
-	spa_return_val_if_fail(this != NULL, -EINVAL);
 
-	spa_log_trace_fp(this->log, "%p: process %d %d", this, this->n_links, this->n_nodes);
+	/* calculate how many samples at most we are going to consume. If we're
+	 * draining, we consume as much as we can. Otherwise we consume what is
+	 * left. */
+	if (SPA_UNLIKELY(draining))
+		n_samples = SPA_MIN(max_in, this->quantum_limit);
+	else {
+		n_samples = max_in - SPA_MIN(max_in, this->in_offset);
+	}
+	/* we only need to output the remaining samples */
+	n_out = max_out - SPA_MIN(max_out, this->out_offset);
+
+	resample_passthrough = resample_is_passthrough(this);
+
+	/* calculate how many samples we are going to consume. */
+	if (this->direction == SPA_DIRECTION_INPUT) {
+		if (!in_avail || this->drained) {
+			/* no input, ask for more, update rate-match first */
+			resample_update_rate_match(this, resample_passthrough, n_out, 0);
+			spa_log_trace_fp(this->log, "%p: no input drained:%d", this, this->drained);
+			res |= this->drained ? SPA_STATUS_DRAINED : SPA_STATUS_NEED_DATA;
+			return res;
+		}
+		/* else figure out how much input samples we need to consume */
+		n_samples = SPA_MIN(n_samples,
+				resample_get_in_size(this, resample_passthrough, n_out));
+	} else {
+		/* in merge mode we consume one duration of samples */
+		n_samples = SPA_MIN(n_samples, quant_samples);
+		flush_in = true;
+	}
+
+	mix_passthrough = SPA_FLAG_IS_SET(this->mix.flags, CHANNELMIX_FLAG_IDENTITY) &&
+		(ctrlport == NULL || ctrlport->ctrl == NULL);
 
-	while (1) {
-		res = SPA_STATUS_OK;
-		ready = 0;
-		for (i = 0; i < this->n_nodes; i++) {
-			r = spa_node_process(this->nodes[i]);
+	out_passthrough = dir->conv.is_passthrough;
+	if (in_passthrough && mix_passthrough && resample_passthrough)
+		out_passthrough = false;
+
+	if (out_passthrough && dir->need_remap) {
+		for (i = 0; i < dir->conv.n_channels; i++) {
+			remap_dst_datas[i] = dst_datas[dir->remap[i]];
+			spa_log_trace_fp(this->log, "%p: output remap %d -> %d", this, i, dir->remap[i]);
+		}
+		dst_remap = (void **)remap_dst_datas;
+	} else {
+		dst_remap = (void **)dst_datas;
+	}
 
-			spa_log_trace_fp(this->log, "%p: process %d %d: %s",
-					this, i, r, r < 0 ? spa_strerror(r) : "ok");
+	dir = &this->dir[SPA_DIRECTION_INPUT];
+	if (!in_passthrough) {
+		if (mix_passthrough && resample_passthrough && out_passthrough)
+			out_datas = (void **)dst_remap;
+		else
+			out_datas = (void **)this->tmp_datas[(tmp++) & 1];
 
-			if (SPA_UNLIKELY(r < 0))
-				return r;
+		if (dir->need_remap) {
+			for (i = 0; i < dir->conv.n_channels; i++) {
+				remap_src_datas[i] = out_datas[dir->remap[i]];
+				spa_log_trace_fp(this->log, "%p: input remap %d -> %d", this, dir->remap[i], i);
+			}
+		} else {
+			for (i = 0; i < dir->conv.n_channels; i++)
+				remap_src_datas[i] = out_datas[i];
+		}
 
-			if (r & SPA_STATUS_HAVE_DATA)
-				ready++;
+		spa_log_trace_fp(this->log, "%p: input convert %d", this, n_samples);
+		convert_process(&dir->conv, remap_src_datas, src_datas, n_samples);
+	} else {
+		if (dir->need_remap) {
+			for (i = 0; i < dir->conv.n_channels; i++) {
+				remap_src_datas[dir->remap[i]] = (void *)src_datas[i];
+				spa_log_trace_fp(this->log, "%p: input remap %d -> %d", this, dir->remap[i], i);
+			}
+			out_datas = (void **)remap_src_datas;
+		} else {
+			out_datas = (void **)src_datas;
+		}
+	}
 
-			if (SPA_UNLIKELY(i == 0))
-				res |= r & SPA_STATUS_NEED_DATA;
-			if (SPA_UNLIKELY(i == this->n_nodes-1))
-				res |= r & (SPA_STATUS_HAVE_DATA | SPA_STATUS_DRAINED);
+	if (!mix_passthrough) {
+		in_datas = (const void**)out_datas;
+		if (resample_passthrough && out_passthrough) {
+			out_datas = (void **)dst_remap;
+			n_samples = SPA_MIN(n_samples, n_out);
+		} else {
+			out_datas = (void **)this->tmp_datas[(tmp++) & 1];
 		}
-		if (res & SPA_STATUS_HAVE_DATA)
-			break;
-		if (ready == 0)
-			break;
+		spa_log_trace_fp(this->log, "%p: channelmix %d %d %d", this, n_samples,
+				resample_passthrough, out_passthrough);
+		if (ctrlport != NULL && ctrlport->ctrl != NULL) {
+			if (channelmix_process_control(this, ctrlport, out_datas,
+						in_datas, n_samples) == 1) {
+				ctrlio->status = SPA_STATUS_OK;
+				ctrlport->ctrl = NULL;
+			}
+		} else {
+			channelmix_process(&this->mix, out_datas, in_datas, n_samples);
+		}
+	}
+	if (!resample_passthrough) {
+		uint32_t in_len, out_len;
+
+		in_datas = (const void**)out_datas;
+		if (out_passthrough)
+			out_datas = (void **)dst_remap;
+		else
+			out_datas = (void **)this->tmp_datas[(tmp++) & 1];
+
+		in_len = n_samples;
+		out_len = n_out;
+		resample_process(&this->resample, in_datas, &in_len, out_datas, &out_len);
+		spa_log_trace_fp(this->log, "%p: resample %d/%d -> %d/%d %d", this,
+				n_samples, in_len, n_out, out_len, out_passthrough);
+		this->in_offset += in_len;
+		n_samples = out_len;
+	} else {
+		n_samples = SPA_MIN(n_samples, n_out);
+		this->in_offset += n_samples;
+	}
+	this->out_offset += n_samples;
+
+	if (!out_passthrough) {
+		dir = &this->dir[SPA_DIRECTION_OUTPUT];
+		if (dir->need_remap) {
+			for (i = 0; i < dir->conv.n_channels; i++) {
+				remap_dst_datas[dir->remap[i]] = out_datas[i];
+				spa_log_trace_fp(this->log, "%p: output remap %d -> %d", this, i, dir->remap[i]);
+			}
+			in_datas = (const void**)remap_dst_datas;
+		} else {
+			in_datas = (const void**)out_datas;
+		}
+		spa_log_trace_fp(this->log, "%p: output convert %d", this, n_samples);
+		convert_process(&dir->conv, dst_datas, in_datas, n_samples);
+	}
+
+	spa_log_trace_fp(this->log, "%d/%d  %d/%d %d->%d", this->in_offset, max_in,
+			this->out_offset, max_out, n_samples, n_out);
+
+	dir = &this->dir[SPA_DIRECTION_INPUT];
+	if (SPA_LIKELY(this->in_offset >= max_in || flush_in)) {
+		/* return input buffers */
+		for (i = 0; i < dir->n_ports; i++) {
+			port = GET_IN_PORT(this, i);
+			if (port->is_control)
+				continue;
+			if (SPA_UNLIKELY((io = port->io) == NULL))
+				continue;
+			spa_log_trace_fp(this->log, "return: input %d %d", port->id, io->buffer_id);
+			if (!draining)
+				io->status = SPA_STATUS_NEED_DATA;
+		}
+		this->in_offset = 0;
+		max_in = 0;
+		res |= SPA_STATUS_NEED_DATA;
 	}
 
-	spa_log_trace_fp(this->log, "%p: process result: %d", this, res);
+	dir = &this->dir[SPA_DIRECTION_OUTPUT];
+	if (SPA_LIKELY(n_samples > 0 && (this->out_offset >= max_out || flush_out))) {
+		/* queue output buffers */
+		for (i = 0; i < dir->n_ports; i++) {
+			port = GET_OUT_PORT(this, i);
+			if (SPA_UNLIKELY(port->is_monitor || port->is_control))
+				continue;
+			if (SPA_UNLIKELY((io = port->io) == NULL))
+				continue;
+
+			if (SPA_UNLIKELY((buf = out_bufs[i]) == NULL))
+				continue;
+
+			dequeue_buffer(this, port, buf);
+
+			for (j = 0; j < port->blocks; j++) {
+				bd = &buf->buf->datas[j];
+				bd->chunk->size = this->out_offset * port->stride;
+				SPA_FLAG_UPDATE(bd->chunk->flags, SPA_CHUNK_FLAG_EMPTY, in_empty);
+				spa_log_trace_fp(this->log, "out: %d %d %d", this->out_offset,
+						port->stride, bd->chunk->size);
+			}
+			io->status = SPA_STATUS_HAVE_DATA;
+			io->buffer_id = buf->id;
+		}
+		res |= SPA_STATUS_HAVE_DATA;
+		this->drained = draining;
+		this->out_offset = 0;
+	}
+	else if (n_samples == 0 && this->peaks) {
+		for (i = 0; i < dir->n_ports; i++) {
+			port = GET_OUT_PORT(this, i);
+			if (port->is_monitor || port->is_control)
+				continue;
+			if (SPA_UNLIKELY((io = port->io) == NULL))
+				continue;
+
+			io->status = SPA_STATUS_HAVE_DATA;
+			io->buffer_id = SPA_ID_INVALID;
+			res |= SPA_STATUS_HAVE_DATA;
+			spa_log_trace_fp(this->log, "%p: no output buffer", this);
+		}
+	}
+	if (resample_update_rate_match(this, resample_passthrough,
+			max_out - this->out_offset,
+			max_in - this->in_offset) > 0)
+		res |= SPA_STATUS_NEED_DATA;
 
 	return res;
 }
@@ -1261,44 +2740,36 @@ static int impl_get_interface(struct spa_handle *handle, const char *type, void
 static int impl_clear(struct spa_handle *handle)
 {
 	struct impl *this;
+	uint32_t i;
 
 	spa_return_val_if_fail(handle != NULL, -EINVAL);
 
 	this = (struct impl *) handle;
 
-	clean_convert(this);
-
-	spa_handle_clear(this->hnd_merger);
-	spa_handle_clear(this->hnd_convert_in);
-	spa_handle_clear(this->hnd_channelmix);
-	spa_handle_clear(this->hnd_resample);
-	spa_handle_clear(this->hnd_convert_out);
-	spa_handle_clear(this->hnd_splitter);
+	for (i = 0; i < MAX_PORTS; i++)
+		free(this->dir[SPA_DIRECTION_INPUT].ports[i]);
+	for (i = 0; i < MAX_PORTS; i++)
+		free(this->dir[SPA_DIRECTION_OUTPUT].ports[i]);
+	free(this->empty);
+	free(this->scratch);
+	free(this->tmp[0]);
+	free(this->tmp[1]);
+
+	if (this->resample.free)
+		resample_free(&this->resample);
+	if (this->dir[0].conv.free)
+		convert_free(&this->dir[0].conv);
+	if (this->dir[1].conv.free)
+		convert_free(&this->dir[1].conv);
 
 	return 0;
 }
 
-extern const struct spa_handle_factory spa_fmtconvert_factory;
-extern const struct spa_handle_factory spa_channelmix_factory;
-extern const struct spa_handle_factory spa_resample_factory;
-extern const struct spa_handle_factory spa_splitter_factory;
-extern const struct spa_handle_factory spa_merger_factory;
-
 static size_t
 impl_get_size(const struct spa_handle_factory *factory,
 	      const struct spa_dict *params)
 {
-	size_t size;
-
-	size = sizeof(struct impl);
-	size += spa_handle_factory_get_size(&spa_merger_factory, params);
-	size += spa_handle_factory_get_size(&spa_fmtconvert_factory, params);
-	size += spa_handle_factory_get_size(&spa_channelmix_factory, params);
-	size += spa_handle_factory_get_size(&spa_resample_factory, params);
-	size += spa_handle_factory_get_size(&spa_fmtconvert_factory, params);
-	size += spa_handle_factory_get_size(&spa_splitter_factory, params);
-
-	return size;
+	return sizeof(struct impl);
 }
 
 static int
@@ -1309,8 +2780,7 @@ impl_init(const struct spa_handle_factory *factory,
 	  uint32_t n_support)
 {
 	struct impl *this;
-	size_t size;
-	void *iface;
+	uint32_t i;
 
 	spa_return_val_if_fail(factory != NULL, -EINVAL);
 	spa_return_val_if_fail(handle != NULL, -EINVAL);
@@ -1324,8 +2794,40 @@ impl_init(const struct spa_handle_factory *factory,
 	spa_log_topic_init(this->log, log_topic);
 
 	this->cpu = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_CPU);
-	if (this->cpu)
-		this->max_align = spa_cpu_get_max_align(this->cpu);
+	if (this->cpu) {
+		this->cpu_flags = spa_cpu_get_flags(this->cpu);
+		this->max_align = SPA_MIN(MAX_ALIGN, spa_cpu_get_max_align(this->cpu));
+	}
+
+	props_reset(&this->props);
+
+	this->mix.options = CHANNELMIX_OPTION_UPMIX;
+	this->mix.upmix = CHANNELMIX_UPMIX_PSD;
+	this->mix.log = this->log;
+	this->mix.lfe_cutoff = 150.0f;
+	this->mix.fc_cutoff = 12000.0f;
+	this->mix.rear_delay = 12.0f;
+	this->mix.widen = 0.0f;
+
+	for (i = 0; info && i < info->n_items; i++) {
+		const char *k = info->items[i].key;
+		const char *s = info->items[i].value;
+		if (spa_streq(k, "clock.quantum-limit"))
+			spa_atou32(s, &this->quantum_limit, 0);
+		else if (spa_streq(k, "resample.peaks"))
+			this->peaks = spa_atob(s);
+		else if (spa_streq(k, "factory.mode")) {
+			if (spa_streq(s, "merge"))
+				this->direction = SPA_DIRECTION_OUTPUT;
+			else
+				this->direction = SPA_DIRECTION_INPUT;
+		}
+		else
+			audioconvert_set_param(this, k, s);
+	}
+
+	this->dir[SPA_DIRECTION_INPUT].latency = SPA_LATENCY_INFO(SPA_DIRECTION_INPUT);
+	this->dir[SPA_DIRECTION_OUTPUT].latency = SPA_LATENCY_INFO(SPA_DIRECTION_OUTPUT);
 
 	this->node.iface = SPA_INTERFACE_INIT(
 			SPA_TYPE_INTERFACE_Node,
@@ -1347,67 +2849,21 @@ impl_init(const struct spa_handle_factory *factory,
 	this->params[IDX_PropInfo] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ);
 	this->params[IDX_Props] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE);
 	this->info.params = this->params;
-	this->info.n_params = 4;
-
-	this->hnd_merger = SPA_PTROFF(this, sizeof(struct impl), struct spa_handle);
-	spa_handle_factory_init(&spa_merger_factory,
-				this->hnd_merger,
-				info, support, n_support);
-	size = spa_handle_factory_get_size(&spa_merger_factory, info);
-
-	this->hnd_convert_in = SPA_PTROFF(this->hnd_merger, size, struct spa_handle);
-	spa_handle_factory_init(&spa_fmtconvert_factory,
-				this->hnd_convert_in,
-				info, support, n_support);
-	size = spa_handle_factory_get_size(&spa_fmtconvert_factory, info);
-
-	this->hnd_channelmix = SPA_PTROFF(this->hnd_convert_in, size, struct spa_handle);
-	spa_handle_factory_init(&spa_channelmix_factory,
-				this->hnd_channelmix,
-				info, support, n_support);
-	size = spa_handle_factory_get_size(&spa_channelmix_factory, info);
-
-	this->hnd_resample = SPA_PTROFF(this->hnd_channelmix, size, struct spa_handle);
-	spa_handle_factory_init(&spa_resample_factory,
-				this->hnd_resample,
-				info, support, n_support);
-	size = spa_handle_factory_get_size(&spa_resample_factory, info);
-
-	this->hnd_convert_out = SPA_PTROFF(this->hnd_resample, size, struct spa_handle);
-	spa_handle_factory_init(&spa_fmtconvert_factory,
-				this->hnd_convert_out,
-				info, support, n_support);
-	size = spa_handle_factory_get_size(&spa_fmtconvert_factory, info);
-
-	this->hnd_splitter = SPA_PTROFF(this->hnd_convert_out, size, struct spa_handle);
-	spa_handle_factory_init(&spa_splitter_factory,
-				this->hnd_splitter,
-				info, support, n_support);
-
-	spa_handle_get_interface(this->hnd_merger, SPA_TYPE_INTERFACE_Node, &iface);
-	this->merger = iface;
-	spa_handle_get_interface(this->hnd_convert_in, SPA_TYPE_INTERFACE_Node, &iface);
-	this->convert_in = iface;
-	spa_handle_get_interface(this->hnd_channelmix, SPA_TYPE_INTERFACE_Node, &iface);
-	this->channelmix = iface;
-	spa_handle_get_interface(this->hnd_resample, SPA_TYPE_INTERFACE_Node, &iface);
-	this->resample = iface;
-	spa_handle_get_interface(this->hnd_convert_out, SPA_TYPE_INTERFACE_Node, &iface);
-	this->convert_out = iface;
-	spa_handle_get_interface(this->hnd_splitter, SPA_TYPE_INTERFACE_Node, &iface);
-	this->splitter = iface;
-
-	reconfigure_mode(this, SPA_PARAM_PORT_CONFIG_MODE_convert, SPA_DIRECTION_OUTPUT, false, NULL);
-	reconfigure_mode(this, SPA_PARAM_PORT_CONFIG_MODE_convert, SPA_DIRECTION_INPUT, false, NULL);
-
-	spa_node_add_listener(this->channelmix,
-			&this->listener[0], &channelmix_events, this);
+	this->info.n_params = N_NODE_PARAMS;
+
+	this->volume.cpu_flags = this->cpu_flags;
+	volume_init(&this->volume);
+
+	this->rate_scale = 1.0;
+
+	reconfigure_mode(this, SPA_PARAM_PORT_CONFIG_MODE_convert, SPA_DIRECTION_INPUT, false, false, NULL);
+	reconfigure_mode(this, SPA_PARAM_PORT_CONFIG_MODE_convert, SPA_DIRECTION_OUTPUT, false, false, NULL);
 
 	return 0;
 }
 
 static const struct spa_interface_info impl_interfaces[] = {
-	{ SPA_TYPE_INTERFACE_Node, },
+	{SPA_TYPE_INTERFACE_Node,},
 };
 
 static int
diff --git a/spa/plugins/audioconvert/benchmark-fmt-ops.c b/spa/plugins/audioconvert/benchmark-fmt-ops.c
index 0f26ac4c4c48f08462d3a9d857580a43b5e157e2..491b0730bb08294bb63d1b6c796db42d2a96feb7 100644
--- a/spa/plugins/audioconvert/benchmark-fmt-ops.c
+++ b/spa/plugins/audioconvert/benchmark-fmt-ops.c
@@ -271,18 +271,18 @@ static void test_s24_32_f32(void)
 
 static void test_interleave(void)
 {
-	run_test("test_interleave_8", "c", false, true, conv_interleave_8_c);
-	run_test("test_interleave_16", "c", false, true, conv_interleave_16_c);
-	run_test("test_interleave_24", "c", false, true, conv_interleave_24_c);
-	run_test("test_interleave_32", "c", false, true, conv_interleave_32_c);
+	run_test("test_8d_to_8", "c", false, true, conv_8d_to_8_c);
+	run_test("test_16d_to_16", "c", false, true, conv_16d_to_16_c);
+	run_test("test_24d_to_24", "c", false, true, conv_24d_to_24_c);
+	run_test("test_32d_to_32", "c", false, true, conv_32d_to_32_c);
 }
 
 static void test_deinterleave(void)
 {
-	run_test("test_deinterleave_8", "c", true, false, conv_deinterleave_8_c);
-	run_test("test_deinterleave_16", "c", true, false, conv_deinterleave_16_c);
-	run_test("test_deinterleave_24", "c", true, false, conv_deinterleave_24_c);
-	run_test("test_deinterleave_32", "c", true, false, conv_deinterleave_32_c);
+	run_test("test_8_to_8d", "c", true, false, conv_8_to_8d_c);
+	run_test("test_16_to_16d", "c", true, false, conv_16_to_16d_c);
+	run_test("test_24_to_24d", "c", true, false, conv_24_to_24d_c);
+	run_test("test_32_to_32d", "c", true, false, conv_32_to_32d_c);
 }
 
 static int compare_func(const void *_a, const void *_b)
diff --git a/spa/plugins/audioconvert/biquad.c b/spa/plugins/audioconvert/biquad.c
index 12d6b2c5056afba6ee717a086f75e845d4157e9b..409b6734a815bdee5b18421c1adc1ec2ad225791 100644
--- a/spa/plugins/audioconvert/biquad.c
+++ b/spa/plugins/audioconvert/biquad.c
@@ -9,8 +9,6 @@
  */
 
 
-#include "config.h"
-
 #include <spa/utils/defs.h>
 
 #include <math.h>
diff --git a/spa/plugins/audioconvert/channelmix-ops-c.c b/spa/plugins/audioconvert/channelmix-ops-c.c
index 1a6ff626562d4d29943094594021491d0e8e6a7b..9a28830042ad2f5fd3610304e46f6a9df7359c9d 100644
--- a/spa/plugins/audioconvert/channelmix-ops-c.c
+++ b/spa/plugins/audioconvert/channelmix-ops-c.c
@@ -177,6 +177,7 @@ void
 channelmix_f32_2_4_c(struct channelmix *mix, void * SPA_RESTRICT dst[],
 		   const void * SPA_RESTRICT src[], uint32_t n_samples)
 {
+	uint32_t i, n_dst = mix->dst_chan;
 	float **d = (float **)dst;
 	const float **s = (const float **)src;
 	const float v0 = mix->matrix[0][0];
@@ -184,10 +185,25 @@ channelmix_f32_2_4_c(struct channelmix *mix, void * SPA_RESTRICT dst[],
 	const float v2 = mix->matrix[2][0];
 	const float v3 = mix->matrix[3][1];
 
-	vol_c(d[0], s[0], v0, n_samples);
-	vol_c(d[1], s[1], v1, n_samples);
-	vol_c(d[2], s[0], v2, n_samples);
-	vol_c(d[3], s[1], v3, n_samples);
+	if (SPA_FLAG_IS_SET(mix->flags, CHANNELMIX_FLAG_ZERO)) {
+		for (i = 0; i < n_dst; i++)
+			clear_c(d[i], n_samples);
+	}
+	else {
+		vol_c(d[0], s[0], v0, n_samples);
+		vol_c(d[1], s[1], v1, n_samples);
+		if (mix->upmix != CHANNELMIX_UPMIX_PSD) {
+			vol_c(d[2], s[0], v2, n_samples);
+			vol_c(d[3], s[1], v3, n_samples);
+		} else {
+			sub_c(d[2], s[0], s[1], n_samples);
+
+			delay_convolve_run(mix->buffer[1], &mix->pos[1], BUFFER_SIZE, mix->delay,
+					   mix->taps, mix->n_taps, d[3], d[2], -v3, n_samples);
+			delay_convolve_run(mix->buffer[0], &mix->pos[0], BUFFER_SIZE, mix->delay,
+					   mix->taps, mix->n_taps, d[2], d[2], v2, n_samples);
+		}
+	}
 }
 
 #define MASK_3_1	_M(FL)|_M(FR)|_M(FC)|_M(LFE)
@@ -294,6 +310,32 @@ channelmix_f32_2_7p1_c(struct channelmix *mix, void * SPA_RESTRICT dst[],
 	}
 }
 
+/* FL+FR+FC+LFE -> FL+FR */
+void
+channelmix_f32_3p1_2_c(struct channelmix *mix, void * SPA_RESTRICT dst[],
+		   const void * SPA_RESTRICT src[], uint32_t n_samples)
+{
+	uint32_t n;
+	float **d = (float **) dst;
+	const float **s = (const float **) src;
+	const float v0 = mix->matrix[0][0];
+	const float v1 = mix->matrix[1][1];
+	const float clev = (mix->matrix[0][2] + mix->matrix[1][2]) * 0.5f;
+	const float llev = (mix->matrix[0][3] + mix->matrix[1][3]) * 0.5f;
+
+	if (SPA_FLAG_IS_SET(mix->flags, CHANNELMIX_FLAG_ZERO)) {
+		clear_c(d[0], n_samples);
+		clear_c(d[1], n_samples);
+	}
+	else {
+		for (n = 0; n < n_samples; n++) {
+			const float ctr = clev * s[2][n] + llev * s[3][n];
+			d[0][n] = s[0][n] * v0 + ctr;
+			d[1][n] = s[1][n] * v1 + ctr;
+		}
+	}
+}
+
 /* FL+FR+FC+LFE+SL+SR -> FL+FR */
 void
 channelmix_f32_5p1_2_c(struct channelmix *mix, void * SPA_RESTRICT dst[],
@@ -356,13 +398,9 @@ void
 channelmix_f32_5p1_4_c(struct channelmix *mix, void * SPA_RESTRICT dst[],
 		   const void * SPA_RESTRICT src[], uint32_t n_samples)
 {
-	uint32_t i, n, n_dst = mix->dst_chan;
+	uint32_t i, n_dst = mix->dst_chan;
 	float **d = (float **) dst;
 	const float **s = (const float **) src;
-	const float clev = mix->matrix[0][2];
-	const float llev = mix->matrix[0][3];
-	const float v0 = mix->matrix[0][0];
-	const float v1 = mix->matrix[1][1];
 	const float v4 = mix->matrix[2][4];
 	const float v5 = mix->matrix[3][5];
 
@@ -371,11 +409,8 @@ channelmix_f32_5p1_4_c(struct channelmix *mix, void * SPA_RESTRICT dst[],
 			clear_c(d[i], n_samples);
 	}
 	else {
-		for (n = 0; n < n_samples; n++) {
-			const float ctr = s[2][n] * clev + s[3][n] * llev;
-			d[0][n] = s[0][n] * v0 + ctr;
-			d[1][n] = s[1][n] * v1 + ctr;
-		}
+		channelmix_f32_3p1_2_c(mix, dst, src, n_samples);
+
 		vol_c(d[2], s[4], v4, n_samples);
 		vol_c(d[3], s[5], v5, n_samples);
 	}
diff --git a/spa/plugins/audioconvert/channelmix-ops-sse.c b/spa/plugins/audioconvert/channelmix-ops-sse.c
index f3ef7c06eb27139aa7c1171d3968bb4abcaf163f..37a02e22648b102a5b16f91c058f6c5b9426cfed 100644
--- a/spa/plugins/audioconvert/channelmix-ops-sse.c
+++ b/spa/plugins/audioconvert/channelmix-ops-sse.c
@@ -26,113 +26,104 @@
 
 #include <xmmintrin.h>
 
-void channelmix_copy_sse(struct channelmix *mix, void * SPA_RESTRICT dst[],
-		const void * SPA_RESTRICT src[], uint32_t n_samples)
+static inline void clear_sse(float *d, uint32_t n_samples)
 {
-	uint32_t i, n, unrolled, n_dst = mix->dst_chan;
-	float **d = (float **)dst;
-	const float **s = (const float **)src;
+	memset(d, 0, n_samples * sizeof(float));
+}
 
-	if (SPA_FLAG_IS_SET(mix->flags, CHANNELMIX_FLAG_ZERO)) {
-		for (i = 0; i < n_dst; i++)
-			memset(d[i], 0, n_samples * sizeof(float));
-	}
-	else if (SPA_FLAG_IS_SET(mix->flags, CHANNELMIX_FLAG_IDENTITY)) {
-		for (i = 0; i < n_dst; i++)
-			spa_memcpy(d[i], s[i], n_samples * sizeof(float));
-	}
-	else {
-		for (i = 0; i < n_dst; i++) {
-			float *di = d[i];
-			const float *si = s[i];
-			__m128 t[4];
-			const __m128 vol = _mm_set1_ps(mix->matrix[i][i]);
+static inline void copy_sse(float *d, const float *s, uint32_t n_samples)
+{
+	spa_memcpy(d, s, n_samples * sizeof(float));
+}
 
-			if (SPA_IS_ALIGNED(di, 16) &&
-			    SPA_IS_ALIGNED(si, 16))
-				unrolled = n_samples & ~15;
-			else
-				unrolled = 0;
+static inline void vol_sse(float *d, const float *s, float vol, uint32_t n_samples)
+{
+	uint32_t n, unrolled;
+	if (vol == 0.0f) {
+		clear_sse(d, n_samples);
+	} else if (vol == 1.0f) {
+		copy_sse(d, s, n_samples);
+	} else {
+		__m128 t[4];
+		const __m128 v = _mm_set1_ps(vol);
+
+		if (SPA_IS_ALIGNED(d, 16) &&
+		    SPA_IS_ALIGNED(s, 16))
+			unrolled = n_samples & ~15;
+		else
+			unrolled = 0;
 
-			for(n = 0; n < unrolled; n += 16) {
-				t[0] = _mm_load_ps(&si[n]);
-				t[1] = _mm_load_ps(&si[n+4]);
-				t[2] = _mm_load_ps(&si[n+8]);
-				t[3] = _mm_load_ps(&si[n+12]);
-				_mm_store_ps(&di[n], _mm_mul_ps(t[0], vol));
-				_mm_store_ps(&di[n+4], _mm_mul_ps(t[1], vol));
-				_mm_store_ps(&di[n+8], _mm_mul_ps(t[2], vol));
-				_mm_store_ps(&di[n+12], _mm_mul_ps(t[3], vol));
-			}
-			for(; n < n_samples; n++)
-				_mm_store_ss(&di[n], _mm_mul_ss(_mm_load_ss(&si[n]), vol));
+		for(n = 0; n < unrolled; n += 16) {
+			t[0] = _mm_load_ps(&s[n]);
+			t[1] = _mm_load_ps(&s[n+4]);
+			t[2] = _mm_load_ps(&s[n+8]);
+			t[3] = _mm_load_ps(&s[n+12]);
+			_mm_store_ps(&d[n], _mm_mul_ps(t[0], v));
+			_mm_store_ps(&d[n+4], _mm_mul_ps(t[1], v));
+			_mm_store_ps(&d[n+8], _mm_mul_ps(t[2], v));
+			_mm_store_ps(&d[n+12], _mm_mul_ps(t[3], v));
 		}
+		for(; n < n_samples; n++)
+			_mm_store_ss(&d[n], _mm_mul_ss(_mm_load_ss(&s[n]), v));
 	}
 }
 
-void
-channelmix_f32_2_4_sse(struct channelmix *mix, void * SPA_RESTRICT dst[],
+void channelmix_copy_sse(struct channelmix *mix, void * SPA_RESTRICT dst[],
 		const void * SPA_RESTRICT src[], uint32_t n_samples)
 {
-	uint32_t i, n, unrolled, n_dst = mix->dst_chan;
+	uint32_t i, n_dst = mix->dst_chan;
 	float **d = (float **)dst;
 	const float **s = (const float **)src;
-	const float m00 = mix->matrix[0][0];
-	const float m11 = mix->matrix[1][1];
-	__m128 in;
-	const float *sFL = s[0], *sFR = s[1];
-	float *dFL = d[0], *dFR = d[1], *dRL = d[2], *dRR = d[3];
+	for (i = 0; i < n_dst; i++)
+		vol_sse(d[i], s[i], mix->matrix[i][i], n_samples);
+}
 
-	if (SPA_IS_ALIGNED(sFL, 16) &&
-	    SPA_IS_ALIGNED(sFR, 16) &&
-	    SPA_IS_ALIGNED(dFL, 16) &&
-	    SPA_IS_ALIGNED(dFR, 16) &&
-	    SPA_IS_ALIGNED(dRL, 16) &&
-	    SPA_IS_ALIGNED(dRR, 16))
-		unrolled = n_samples & ~3;
-	else
-		unrolled = 0;
+/* FL+FR+FC+LFE -> FL+FR */
+void
+channelmix_f32_3p1_2_sse(struct channelmix *mix, void * SPA_RESTRICT dst[],
+		   const void * SPA_RESTRICT src[], uint32_t n_samples)
+{
+	float **d = (float **) dst;
+	const float **s = (const float **) src;
+	const float m0 = mix->matrix[0][0];
+	const float m1 = mix->matrix[1][1];
+	const float m2 = (mix->matrix[0][2] + mix->matrix[1][2]) * 0.5f;
+	const float m3 = (mix->matrix[0][3] + mix->matrix[1][3]) * 0.5f;
 
-	if (SPA_FLAG_IS_SET(mix->flags, CHANNELMIX_FLAG_ZERO)) {
-		for (i = 0; i < n_dst; i++)
-			memset(d[i], 0, n_samples * sizeof(float));
-	}
-	else if (m00 == 1.0f && m11 == 1.0f) {
-		for(n = 0; n < unrolled; n += 4) {
-			in = _mm_load_ps(&sFL[n]);
-			_mm_store_ps(&dFL[n], in);
-			_mm_store_ps(&dRL[n], in);
-			in = _mm_load_ps(&sFR[n]);
-			_mm_store_ps(&dFR[n], in);
-			_mm_store_ps(&dRR[n], in);
-		}
-		for(; n < n_samples; n++) {
-			in = _mm_load_ss(&sFL[n]);
-			_mm_store_ss(&dFL[n], in);
-			_mm_store_ss(&dRL[n], in);
-			in = _mm_load_ss(&sFR[n]);
-			_mm_store_ss(&dFR[n], in);
-			_mm_store_ss(&dRR[n], in);
-		}
+	if (m0 == 0.0f && m1 == 0.0f && m2 == 0.0f && m3 == 0.0f) {
+		clear_sse(d[0], n_samples);
+		clear_sse(d[1], n_samples);
 	}
 	else {
-		const __m128 v0 = _mm_set1_ps(m00);
-		const __m128 v1 = _mm_set1_ps(m11);
+		uint32_t n, unrolled;
+		const __m128 v0 = _mm_set1_ps(m0);
+		const __m128 v1 = _mm_set1_ps(m1);
+		const __m128 clev = _mm_set1_ps(m2);
+		const __m128 llev = _mm_set1_ps(m3);
+		__m128 ctr;
+
+		if (SPA_IS_ALIGNED(s[0], 16) &&
+		    SPA_IS_ALIGNED(s[1], 16) &&
+		    SPA_IS_ALIGNED(s[2], 16) &&
+		    SPA_IS_ALIGNED(s[3], 16) &&
+		    SPA_IS_ALIGNED(d[0], 16) &&
+		    SPA_IS_ALIGNED(d[1], 16))
+			unrolled = n_samples & ~3;
+		else
+			unrolled = 0;
+
 		for(n = 0; n < unrolled; n += 4) {
-			in = _mm_mul_ps(_mm_load_ps(&sFL[n]), v0);
-			_mm_store_ps(&dFL[n], in);
-			_mm_store_ps(&dRL[n], in);
-			in = _mm_mul_ps(_mm_load_ps(&sFR[n]), v1);
-			_mm_store_ps(&dFR[n], in);
-			_mm_store_ps(&dRR[n], in);
+			ctr = _mm_add_ps(
+					_mm_mul_ps(_mm_load_ps(&s[2][n]), clev),
+					_mm_mul_ps(_mm_load_ps(&s[3][n]), llev));
+			_mm_store_ps(&d[0][n], _mm_add_ps(_mm_mul_ps(_mm_load_ps(&s[0][n]), v0), ctr));
+			_mm_store_ps(&d[1][n], _mm_add_ps(_mm_mul_ps(_mm_load_ps(&s[1][n]), v1), ctr));
 		}
 		for(; n < n_samples; n++) {
-			in = _mm_mul_ss(_mm_load_ss(&sFL[n]), v0);
-			_mm_store_ss(&dFL[n], in);
-			_mm_store_ss(&dRL[n], in);
-			in = _mm_mul_ss(_mm_load_ss(&sFR[n]), v1);
-			_mm_store_ss(&dFR[n], in);
-			_mm_store_ss(&dRR[n], in);
+			ctr = _mm_add_ss(_mm_mul_ss(_mm_load_ss(&s[2][n]), clev),
+					_mm_mul_ss(_mm_load_ss(&s[3][n]), llev));
+			_mm_store_ss(&d[0][n], _mm_add_ss(_mm_mul_ss(_mm_load_ss(&s[0][n]), v0), ctr));
+			_mm_store_ss(&d[1][n], _mm_add_ss(_mm_mul_ss(_mm_load_ss(&s[1][n]), v1), ctr));
 		}
 	}
 }
@@ -152,77 +143,49 @@ channelmix_f32_5p1_2_sse(struct channelmix *mix, void * SPA_RESTRICT dst[],
 	const __m128 slev0 = _mm_set1_ps(mix->matrix[0][4]);
 	const __m128 slev1 = _mm_set1_ps(mix->matrix[1][5]);
 	__m128 in, ctr;
-	const float *sFL = s[0], *sFR = s[1], *sFC = s[2], *sLFE = s[3], *sSL = s[4], *sSR = s[5];
-	float *dFL = d[0], *dFR = d[1];
 
-	if (SPA_IS_ALIGNED(sFL, 16) &&
-	    SPA_IS_ALIGNED(sFR, 16) &&
-	    SPA_IS_ALIGNED(sFC, 16) &&
-	    SPA_IS_ALIGNED(sLFE, 16) &&
-	    SPA_IS_ALIGNED(sSL, 16) &&
-	    SPA_IS_ALIGNED(sSR, 16) &&
-	    SPA_IS_ALIGNED(dFL, 16) &&
-	    SPA_IS_ALIGNED(dFR, 16))
+	if (SPA_IS_ALIGNED(s[0], 16) &&
+	    SPA_IS_ALIGNED(s[1], 16) &&
+	    SPA_IS_ALIGNED(s[2], 16) &&
+	    SPA_IS_ALIGNED(s[3], 16) &&
+	    SPA_IS_ALIGNED(s[4], 16) &&
+	    SPA_IS_ALIGNED(s[5], 16) &&
+	    SPA_IS_ALIGNED(d[0], 16) &&
+	    SPA_IS_ALIGNED(d[1], 16))
 		unrolled = n_samples & ~3;
 	else
 		unrolled = 0;
 
 	if (SPA_FLAG_IS_SET(mix->flags, CHANNELMIX_FLAG_ZERO)) {
-		memset(dFL, 0, n_samples * sizeof(float));
-		memset(dFR, 0, n_samples * sizeof(float));
-	}
-	else if (m00 == 1.0f && m11 == 1.0f) {
-		for(n = 0; n < unrolled; n += 4) {
-			ctr = _mm_mul_ps(_mm_load_ps(&sFC[n]), clev);
-			ctr = _mm_add_ps(ctr, _mm_mul_ps(_mm_load_ps(&sLFE[n]), llev));
-			in = _mm_mul_ps(_mm_load_ps(&sSL[n]), slev0);
-			in = _mm_add_ps(in, ctr);
-			in = _mm_add_ps(in, _mm_load_ps(&sFL[n]));
-			_mm_store_ps(&dFL[n], in);
-			in = _mm_mul_ps(_mm_load_ps(&sSR[n]), slev1);
-			in = _mm_add_ps(in, ctr);
-			in = _mm_add_ps(in, _mm_load_ps(&sFR[n]));
-			_mm_store_ps(&dFR[n], in);
-		}
-		for(; n < n_samples; n++) {
-			ctr = _mm_mul_ss(_mm_load_ss(&sFC[n]), clev);
-			ctr = _mm_add_ss(ctr, _mm_mul_ss(_mm_load_ss(&sLFE[n]), llev));
-			in = _mm_mul_ss(_mm_load_ss(&sSL[n]), slev0);
-			in = _mm_add_ss(in, ctr);
-			in = _mm_add_ss(in, _mm_load_ss(&sFL[n]));
-			_mm_store_ss(&dFL[n], in);
-			in = _mm_mul_ss(_mm_load_ss(&sSR[n]), slev1);
-			in = _mm_add_ss(in, ctr);
-			in = _mm_add_ss(in, _mm_load_ss(&sFR[n]));
-			_mm_store_ss(&dFR[n], in);
-		}
+		clear_sse(d[0], n_samples);
+		clear_sse(d[1], n_samples);
 	}
 	else {
 		const __m128 v0 = _mm_set1_ps(m00);
 		const __m128 v1 = _mm_set1_ps(m11);
 		for(n = 0; n < unrolled; n += 4) {
-			ctr = _mm_mul_ps(_mm_load_ps(&sFC[n]), clev);
-			ctr = _mm_add_ps(ctr, _mm_mul_ps(_mm_load_ps(&sLFE[n]), llev));
-			in = _mm_mul_ps(_mm_load_ps(&sSL[n]), slev0);
+			ctr = _mm_add_ps(_mm_mul_ps(_mm_load_ps(&s[2][n]), clev),
+					_mm_mul_ps(_mm_load_ps(&s[3][n]), llev));
+			in = _mm_mul_ps(_mm_load_ps(&s[4][n]), slev0);
 			in = _mm_add_ps(in, ctr);
-			in = _mm_add_ps(in, _mm_mul_ps(_mm_load_ps(&sFL[n]), v0));
-			_mm_store_ps(&dFL[n], in);
-			in = _mm_mul_ps(_mm_load_ps(&sSR[n]), slev1);
+			in = _mm_add_ps(in, _mm_mul_ps(_mm_load_ps(&s[0][n]), v0));
+			_mm_store_ps(&d[0][n], in);
+			in = _mm_mul_ps(_mm_load_ps(&s[5][n]), slev1);
 			in = _mm_add_ps(in, ctr);
-			in = _mm_add_ps(in, _mm_mul_ps(_mm_load_ps(&sFR[n]), v1));
-			_mm_store_ps(&dFR[n], in);
+			in = _mm_add_ps(in, _mm_mul_ps(_mm_load_ps(&s[1][n]), v1));
+			_mm_store_ps(&d[1][n], in);
 		}
 		for(; n < n_samples; n++) {
-			ctr = _mm_mul_ss(_mm_load_ss(&sFC[n]), clev);
-			ctr = _mm_add_ss(ctr, _mm_mul_ss(_mm_load_ss(&sLFE[n]), llev));
-			in = _mm_mul_ss(_mm_load_ss(&sSL[n]), slev0);
+			ctr = _mm_mul_ss(_mm_load_ss(&s[2][n]), clev);
+			ctr = _mm_add_ss(ctr, _mm_mul_ss(_mm_load_ss(&s[3][n]), llev));
+			in = _mm_mul_ss(_mm_load_ss(&s[4][n]), slev0);
 			in = _mm_add_ss(in, ctr);
-			in = _mm_add_ss(in, _mm_mul_ss(_mm_load_ss(&sFL[n]), v0));
-			_mm_store_ss(&dFL[n], in);
-			in = _mm_mul_ss(_mm_load_ss(&sSR[n]), slev1);
+			in = _mm_add_ss(in, _mm_mul_ss(_mm_load_ss(&s[0][n]), v0));
+			_mm_store_ss(&d[0][n], in);
+			in = _mm_mul_ss(_mm_load_ss(&s[5][n]), slev1);
 			in = _mm_add_ss(in, ctr);
-			in = _mm_add_ss(in, _mm_mul_ss(_mm_load_ss(&sFR[n]), v1));
-			_mm_store_ss(&dFR[n], in);
+			in = _mm_add_ss(in, _mm_mul_ss(_mm_load_ss(&s[1][n]), v1));
+			_mm_store_ss(&d[1][n], in);
 		}
 	}
 }
@@ -235,73 +198,51 @@ channelmix_f32_5p1_3p1_sse(struct channelmix *mix, void * SPA_RESTRICT dst[],
 	uint32_t i, n, unrolled, n_dst = mix->dst_chan;
 	float **d = (float **) dst;
 	const float **s = (const float **) src;
-	const __m128 v0 = _mm_set1_ps(mix->matrix[0][0]);
-	const __m128 v1 = _mm_set1_ps(mix->matrix[1][1]);
-	const __m128 slev0 = _mm_set1_ps(mix->matrix[0][4]);
-	const __m128 slev1 = _mm_set1_ps(mix->matrix[1][5]);
-	const __m128 v2 = _mm_set1_ps(mix->matrix[2][2]);
-	const __m128 v3 = _mm_set1_ps(mix->matrix[3][3]);
-	__m128 avg[2];
-	const float *sFL = s[0], *sFR = s[1], *sFC = s[2], *sLFE = s[3], *sSL = s[4], *sSR = s[5];
-	float *dFL = d[0], *dFR = d[1], *dFC = d[2], *dLFE = d[3];
 
-	if (SPA_IS_ALIGNED(sFL, 16) &&
-	    SPA_IS_ALIGNED(sFR, 16) &&
-	    SPA_IS_ALIGNED(sFC, 16) &&
-	    SPA_IS_ALIGNED(sLFE, 16) &&
-	    SPA_IS_ALIGNED(sSL, 16) &&
-	    SPA_IS_ALIGNED(sSR, 16) &&
-	    SPA_IS_ALIGNED(dFL, 16) &&
-	    SPA_IS_ALIGNED(dFR, 16) &&
-	    SPA_IS_ALIGNED(dFC, 16) &&
-	    SPA_IS_ALIGNED(dLFE, 16))
-		unrolled = n_samples & ~7;
+	if (SPA_IS_ALIGNED(s[0], 16) &&
+	    SPA_IS_ALIGNED(s[1], 16) &&
+	    SPA_IS_ALIGNED(s[2], 16) &&
+	    SPA_IS_ALIGNED(s[3], 16) &&
+	    SPA_IS_ALIGNED(s[4], 16) &&
+	    SPA_IS_ALIGNED(s[5], 16) &&
+	    SPA_IS_ALIGNED(d[0], 16) &&
+	    SPA_IS_ALIGNED(d[1], 16) &&
+	    SPA_IS_ALIGNED(d[2], 16) &&
+	    SPA_IS_ALIGNED(d[3], 16))
+		unrolled = n_samples & ~3;
 	else
 		unrolled = 0;
 
 	if (SPA_FLAG_IS_SET(mix->flags, CHANNELMIX_FLAG_ZERO)) {
 		for (i = 0; i < n_dst; i++)
-			memset(d[i], 0, n_samples * sizeof(float));
+			clear_sse(d[i], n_samples);
 	}
 	else {
-		for(n = 0; n < unrolled; n += 8) {
-			avg[0] = _mm_add_ps(
-					_mm_mul_ps(_mm_load_ps(&sFL[n]), v0),
-					_mm_mul_ps(_mm_load_ps(&sSL[n]), slev0));
-			avg[1] = _mm_add_ps(
-					_mm_mul_ps(_mm_load_ps(&sFL[n+4]), v0),
-					_mm_mul_ps(_mm_load_ps(&sSL[n+4]), slev0));
-			_mm_store_ps(&dFL[n], avg[0]);
-			_mm_store_ps(&dFL[n+4], avg[1]);
+		const __m128 v0 = _mm_set1_ps(mix->matrix[0][0]);
+		const __m128 v1 = _mm_set1_ps(mix->matrix[1][1]);
+		const __m128 slev0 = _mm_set1_ps(mix->matrix[0][4]);
+		const __m128 slev1 = _mm_set1_ps(mix->matrix[1][5]);
 
-			avg[0] = _mm_add_ps(
-					_mm_mul_ps(_mm_load_ps(&sFR[n]), v1),
-					_mm_mul_ps(_mm_load_ps(&sSR[n]), slev1));
-			avg[1] = _mm_add_ps(
-					_mm_mul_ps(_mm_load_ps(&sFR[n+4]), v1),
-					_mm_mul_ps(_mm_load_ps(&sSR[n+4]), slev1));
-			_mm_store_ps(&dFR[n], avg[0]);
-			_mm_store_ps(&dFR[n+4], avg[1]);
+		for(n = 0; n < unrolled; n += 4) {
+			_mm_store_ps(&d[0][n], _mm_add_ps(
+					_mm_mul_ps(_mm_load_ps(&s[0][n]), v0),
+					_mm_mul_ps(_mm_load_ps(&s[4][n]), slev0)));
 
-			_mm_store_ps(&dFC[n], _mm_mul_ps(_mm_load_ps(&sFC[n]), v2));
-			_mm_store_ps(&dFC[n+4], _mm_mul_ps(_mm_load_ps(&sFC[n+4]), v2));
-			_mm_store_ps(&dLFE[n], _mm_mul_ps(_mm_load_ps(&sLFE[n]), v3));
-			_mm_store_ps(&dLFE[n+4], _mm_mul_ps(_mm_load_ps(&sLFE[n+4]), v3));
+			_mm_store_ps(&d[1][n], _mm_add_ps(
+					_mm_mul_ps(_mm_load_ps(&s[1][n]), v1),
+					_mm_mul_ps(_mm_load_ps(&s[5][n]), slev1)));
 		}
 		for(; n < n_samples; n++) {
-			avg[0] = _mm_add_ss(
-					_mm_mul_ss(_mm_load_ss(&sFL[n]), v0),
-					_mm_mul_ss(_mm_load_ss(&sSL[n]), slev0));
-			_mm_store_ss(&dFL[n], avg[0]);
-
-			avg[0] = _mm_add_ss(
-					_mm_mul_ss(_mm_load_ss(&sFR[n]), v1),
-					_mm_mul_ss(_mm_load_ss(&sSR[n]), slev1));
-			_mm_store_ss(&dFR[n], avg[0]);
+			_mm_store_ss(&d[0][n], _mm_add_ss(
+					_mm_mul_ss(_mm_load_ss(&s[0][n]), v0),
+					_mm_mul_ss(_mm_load_ss(&s[4][n]), slev0)));
 
-			_mm_store_ss(&dFC[n], _mm_mul_ss(_mm_load_ss(&sFC[n]), v2));
-			_mm_store_ss(&dLFE[n], _mm_mul_ss(_mm_load_ss(&sLFE[n]), v3));
+			_mm_store_ss(&d[1][n], _mm_add_ss(
+					_mm_mul_ss(_mm_load_ss(&s[1][n]), v1),
+					_mm_mul_ss(_mm_load_ss(&s[5][n]), slev1)));
 		}
+		vol_sse(d[2], s[2], mix->matrix[2][2], n_samples);
+		vol_sse(d[3], s[3], mix->matrix[3][3], n_samples);
 	}
 }
 
@@ -310,76 +251,20 @@ void
 channelmix_f32_5p1_4_sse(struct channelmix *mix, void * SPA_RESTRICT dst[],
 		const void * SPA_RESTRICT src[], uint32_t n_samples)
 {
-	uint32_t i, n, unrolled, n_dst = mix->dst_chan;
+	uint32_t i, n_dst = mix->dst_chan;
 	float **d = (float **) dst;
 	const float **s = (const float **) src;
-	const __m128 clev = _mm_set1_ps(mix->matrix[0][2]);
-	const __m128 llev = _mm_set1_ps(mix->matrix[0][3]);
-	const float m00 = mix->matrix[0][0];
-	const float m11 = mix->matrix[1][1];
-	const float m24 = mix->matrix[2][4];
-	const float m35 = mix->matrix[3][5];
-	__m128 ctr;
-	const float *sFL = s[0], *sFR = s[1], *sFC = s[2], *sLFE = s[3], *sSL = s[4], *sSR = s[5];
-	float *dFL = d[0], *dFR = d[1], *dRL = d[2], *dRR = d[3];
-
-	if (SPA_IS_ALIGNED(sFL, 16) &&
-	    SPA_IS_ALIGNED(sFR, 16) &&
-	    SPA_IS_ALIGNED(sFC, 16) &&
-	    SPA_IS_ALIGNED(sLFE, 16) &&
-	    SPA_IS_ALIGNED(sSL, 16) &&
-	    SPA_IS_ALIGNED(sSR, 16) &&
-	    SPA_IS_ALIGNED(dFL, 16) &&
-	    SPA_IS_ALIGNED(dFR, 16) &&
-	    SPA_IS_ALIGNED(dRL, 16) &&
-	    SPA_IS_ALIGNED(dRR, 16))
-		unrolled = n_samples & ~3;
-	else
-		unrolled = 0;
+	const float v4 = mix->matrix[2][4];
+	const float v5 = mix->matrix[3][5];
 
 	if (SPA_FLAG_IS_SET(mix->flags, CHANNELMIX_FLAG_ZERO)) {
 		for (i = 0; i < n_dst; i++)
-			memset(d[i], 0, n_samples * sizeof(float));
-	}
-	else if (m00 == 1.0f && m11 == 1.0f && m24 == 1.0f && m35 == 1.0f) {
-		for(n = 0; n < unrolled; n += 4) {
-			ctr = _mm_mul_ps(_mm_load_ps(&sFC[n]), clev);
-			ctr = _mm_add_ps(ctr, _mm_mul_ps(_mm_load_ps(&sLFE[n]), llev));
-			_mm_store_ps(&dFL[n], _mm_add_ps(_mm_load_ps(&sFL[n]), ctr));
-			_mm_store_ps(&dFR[n], _mm_add_ps(_mm_load_ps(&sFR[n]), ctr));
-			_mm_store_ps(&dRL[n], _mm_load_ps(&sSL[n]));
-			_mm_store_ps(&dRR[n], _mm_load_ps(&sSR[n]));
-		}
-		for(; n < n_samples; n++) {
-			ctr = _mm_mul_ss(_mm_load_ss(&sFC[n]), clev);
-			ctr = _mm_add_ss(ctr, _mm_mul_ss(_mm_load_ss(&sLFE[n]), llev));
-			_mm_store_ss(&dFL[n], _mm_add_ss(_mm_load_ss(&sFL[n]), ctr));
-			_mm_store_ss(&dFR[n], _mm_add_ss(_mm_load_ss(&sFR[n]), ctr));
-			_mm_store_ss(&dRL[n], _mm_load_ss(&sSL[n]));
-			_mm_store_ss(&dRR[n], _mm_load_ss(&sSR[n]));
-		}
+			clear_sse(d[i], n_samples);
 	}
 	else {
-		const __m128 v0 = _mm_set1_ps(m00);
-		const __m128 v1 = _mm_set1_ps(m11);
-		const __m128 v4 = _mm_set1_ps(m24);
-		const __m128 v5 = _mm_set1_ps(m35);
+		channelmix_f32_3p1_2_sse(mix, dst, src, n_samples);
 
-		for(n = 0; n < unrolled; n += 4) {
-			ctr = _mm_mul_ps(_mm_load_ps(&sFC[n]), clev);
-			ctr = _mm_add_ps(ctr, _mm_mul_ps(_mm_load_ps(&sLFE[n]), llev));
-			_mm_store_ps(&dFL[n], _mm_mul_ps(_mm_add_ps(_mm_load_ps(&sFL[n]), ctr), v0));
-			_mm_store_ps(&dFR[n], _mm_mul_ps(_mm_add_ps(_mm_load_ps(&sFR[n]), ctr), v1));
-			_mm_store_ps(&dRL[n], _mm_mul_ps(_mm_load_ps(&sSL[n]), v4));
-			_mm_store_ps(&dRR[n], _mm_mul_ps(_mm_load_ps(&sSR[n]), v5));
-		}
-		for(; n < n_samples; n++) {
-			ctr = _mm_mul_ss(_mm_load_ss(&sFC[n]), clev);
-			ctr = _mm_add_ss(ctr, _mm_mul_ss(_mm_load_ss(&sLFE[n]), llev));
-			_mm_store_ss(&dFL[n], _mm_mul_ss(_mm_add_ss(_mm_load_ss(&sFL[n]), ctr), v0));
-			_mm_store_ss(&dFR[n], _mm_mul_ss(_mm_add_ss(_mm_load_ss(&sFR[n]), ctr), v1));
-			_mm_store_ss(&dRL[n], _mm_mul_ss(_mm_load_ss(&sSL[n]), v4));
-			_mm_store_ss(&dRR[n], _mm_mul_ss(_mm_load_ss(&sSR[n]), v5));
-		}
+		vol_sse(d[2], s[4], v4, n_samples);
+		vol_sse(d[3], s[5], v5, n_samples);
 	}
 }
diff --git a/spa/plugins/audioconvert/channelmix-ops.c b/spa/plugins/audioconvert/channelmix-ops.c
index 8b6abf0d8eb041fa7c1e9c6c814572121af2d28d..54e094fe679871efd4297b4158c0e3430bc1bbfb 100644
--- a/spa/plugins/audioconvert/channelmix-ops.c
+++ b/spa/plugins/audioconvert/channelmix-ops.c
@@ -37,9 +37,6 @@
 #include "channelmix-ops.h"
 #include "hilbert.h"
 
-#undef SPA_LOG_TOPIC_DEFAULT
-#define SPA_LOG_TOPIC_DEFAULT log_topic
-struct spa_log_topic *log_topic = &SPA_LOG_TOPIC(0, "spa.channelmix");
 
 #define _M(ch)		(1UL << SPA_AUDIO_CHANNEL_ ## ch)
 #define MASK_MONO	_M(FC)|_M(MONO)|_M(UNKNOWN)
@@ -55,6 +52,9 @@ struct spa_log_topic *log_topic = &SPA_LOG_TOPIC(0, "spa.channelmix");
 typedef void (*channelmix_func_t) (struct channelmix *mix, void * SPA_RESTRICT dst[],
 			const void * SPA_RESTRICT src[], uint32_t n_samples);
 
+#define MAKE(sc,sm,dc,dm,func,...) \
+	{ sc, sm, dc, dm, func, #func, __VA_ARGS__ }
+
 static const struct channelmix_info {
 	uint32_t src_chan;
 	uint64_t src_mask;
@@ -62,50 +62,53 @@ static const struct channelmix_info {
 	uint64_t dst_mask;
 
 	channelmix_func_t process;
-	uint32_t cpu_flags;
 	const char *name;
+
+	uint32_t cpu_flags;
 } channelmix_table[] =
 {
 #if defined (HAVE_SSE)
-	{ 2, MASK_MONO, 2, MASK_MONO, channelmix_copy_sse, SPA_CPU_FLAG_SSE, "copy_sse" },
-	{ 2, MASK_STEREO, 2, MASK_STEREO, channelmix_copy_sse, SPA_CPU_FLAG_SSE, "copy_sse" },
-	{ EQ, 0, EQ, 0, channelmix_copy_sse, SPA_CPU_FLAG_SSE, "copy_sse" },
+	MAKE(2, MASK_MONO, 2, MASK_MONO, channelmix_copy_sse, SPA_CPU_FLAG_SSE),
+	MAKE(2, MASK_STEREO, 2, MASK_STEREO, channelmix_copy_sse, SPA_CPU_FLAG_SSE),
+	MAKE(EQ, 0, EQ, 0, channelmix_copy_sse, SPA_CPU_FLAG_SSE),
 #endif
-	{ 2, MASK_MONO, 2, MASK_MONO, channelmix_copy_c, 0, "copy_c" },
-	{ 2, MASK_STEREO, 2, MASK_STEREO, channelmix_copy_c, 0, "copy_c" },
-	{ EQ, 0, EQ, 0, channelmix_copy_c, 0 },
-
-	{ 1, MASK_MONO, 2, MASK_STEREO, channelmix_f32_1_2_c, 0, "f32_1_2_c" },
-	{ 2, MASK_STEREO, 1, MASK_MONO, channelmix_f32_2_1_c, 0, "f32_2_1_c" },
-	{ 4, MASK_QUAD, 1, MASK_MONO, channelmix_f32_4_1_c, 0, "f32_4_1_c" },
-	{ 4, MASK_3_1, 1, MASK_MONO, channelmix_f32_4_1_c, 0, "f32_4_1_c" },
+	MAKE(2, MASK_MONO, 2, MASK_MONO, channelmix_copy_c),
+	MAKE(2, MASK_STEREO, 2, MASK_STEREO, channelmix_copy_c),
+	MAKE(EQ, 0, EQ, 0, channelmix_copy_c),
+
+	MAKE(1, MASK_MONO, 2, MASK_STEREO, channelmix_f32_1_2_c),
+	MAKE(2, MASK_STEREO, 1, MASK_MONO, channelmix_f32_2_1_c),
+	MAKE(4, MASK_QUAD, 1, MASK_MONO, channelmix_f32_4_1_c),
+	MAKE(4, MASK_3_1, 1, MASK_MONO, channelmix_f32_4_1_c),
+	MAKE(2, MASK_STEREO, 4, MASK_QUAD, channelmix_f32_2_4_c),
+	MAKE(2, MASK_STEREO, 4, MASK_3_1, channelmix_f32_2_3p1_c),
+	MAKE(2, MASK_STEREO, 6, MASK_5_1, channelmix_f32_2_5p1_c),
+	MAKE(2, MASK_STEREO, 8, MASK_7_1, channelmix_f32_2_7p1_c),
 #if defined (HAVE_SSE)
-	{ 2, MASK_STEREO, 4, MASK_QUAD, channelmix_f32_2_4_sse, SPA_CPU_FLAG_SSE, "f32_2_4_sse" },
+	MAKE(4, MASK_3_1, 2, MASK_STEREO, channelmix_f32_3p1_2_sse, SPA_CPU_FLAG_SSE),
 #endif
-	{ 2, MASK_STEREO, 4, MASK_QUAD, channelmix_f32_2_4_c, 0, "f32_2_4_c" },
-	{ 2, MASK_STEREO, 4, MASK_3_1, channelmix_f32_2_3p1_c, 0, "f32_2_3p1_c" },
-	{ 2, MASK_STEREO, 6, MASK_5_1, channelmix_f32_2_5p1_c, 0, "f32_2_5p1_c" },
-	{ 2, MASK_STEREO, 8, MASK_7_1, channelmix_f32_2_7p1_c, 0, "f32_2_7p1_c" },
+	MAKE(4, MASK_3_1, 2, MASK_STEREO, channelmix_f32_3p1_2_c),
 #if defined (HAVE_SSE)
-	{ 6, MASK_5_1, 2, MASK_STEREO, channelmix_f32_5p1_2_sse, SPA_CPU_FLAG_SSE, "f32_5p1_2_sse" },
+	MAKE(6, MASK_5_1, 2, MASK_STEREO, channelmix_f32_5p1_2_sse, SPA_CPU_FLAG_SSE),
 #endif
-	{ 6, MASK_5_1, 2, MASK_STEREO, channelmix_f32_5p1_2_c, 0, "f32_5p1_2_c" },
+	MAKE(6, MASK_5_1, 2, MASK_STEREO, channelmix_f32_5p1_2_c),
 #if defined (HAVE_SSE)
-	{ 6, MASK_5_1, 4, MASK_QUAD, channelmix_f32_5p1_4_sse, SPA_CPU_FLAG_SSE, "f32_5p1_4_sse" },
+	MAKE(6, MASK_5_1, 4, MASK_QUAD, channelmix_f32_5p1_4_sse, SPA_CPU_FLAG_SSE),
 #endif
-	{ 6, MASK_5_1, 4, MASK_QUAD, channelmix_f32_5p1_4_c, 0, "f32_5p1_4_c" },
+	MAKE(6, MASK_5_1, 4, MASK_QUAD, channelmix_f32_5p1_4_c),
 
 #if defined (HAVE_SSE)
-	{ 6, MASK_5_1, 4, MASK_3_1, channelmix_f32_5p1_3p1_sse, SPA_CPU_FLAG_SSE, "f32_5p1_3p1_sse" },
+	MAKE(6, MASK_5_1, 4, MASK_3_1, channelmix_f32_5p1_3p1_sse, SPA_CPU_FLAG_SSE),
 #endif
-	{ 6, MASK_5_1, 4, MASK_3_1, channelmix_f32_5p1_3p1_c, 0, "f32_5p1_3p1_c" },
+	MAKE(6, MASK_5_1, 4, MASK_3_1, channelmix_f32_5p1_3p1_c),
 
-	{ 8, MASK_7_1, 2, MASK_STEREO, channelmix_f32_7p1_2_c, 0, "f32_7p1_2_c" },
-	{ 8, MASK_7_1, 4, MASK_QUAD, channelmix_f32_7p1_4_c, 0, "f32_7p1_4_c" },
-	{ 8, MASK_7_1, 4, MASK_3_1, channelmix_f32_7p1_3p1_c, 0, "f32_7p1_3p1_c" },
+	MAKE(8, MASK_7_1, 2, MASK_STEREO, channelmix_f32_7p1_2_c),
+	MAKE(8, MASK_7_1, 4, MASK_QUAD, channelmix_f32_7p1_4_c),
+	MAKE(8, MASK_7_1, 4, MASK_3_1, channelmix_f32_7p1_3p1_c),
 
-	{ ANY, 0, ANY, 0, channelmix_f32_n_m_c, 0, "f32_n_m_c" },
+	MAKE(ANY, 0, ANY, 0, channelmix_f32_n_m_c),
 };
+#undef MAKE
 
 #define MATCH_CHAN(a,b)		((a) == ANY || (a) == (b))
 #define MATCH_CPU_FLAGS(a,b)	((a) == 0 || ((a) & (b)) == a)
@@ -197,6 +200,7 @@ static int make_matrix(struct channelmix *mix)
 				matrix[i][i]= 1.0f;
 		}
 		src_mask = dst_mask = ~0LU;
+		filter_fc = filter_lfe = true;
 		goto done;
 	} else {
 		spa_log_debug(mix->log, "matching channels");
@@ -395,6 +399,15 @@ static int make_matrix(struct channelmix *mix)
 	spa_log_debug(mix->log, "unassigned upmix %08"PRIx64" lfe:%f",
 			unassigned, mix->lfe_cutoff);
 
+	if (unassigned & STEREO) {
+		if ((src_mask & FRONT) == FRONT) {
+			spa_log_debug(mix->log, "produce STEREO from FC");
+			_MATRIX(FL,FC) += clev;
+			_MATRIX(FR,FC) += clev;
+		} else {
+			spa_log_warn(mix->log, "can't produce STEREO");
+		}
+	}
 	if (unassigned & FRONT) {
 		if ((src_mask & STEREO) == STEREO) {
 			spa_log_debug(mix->log, "produce FC from STEREO");
@@ -426,12 +439,15 @@ static int make_matrix(struct channelmix *mix)
 			_MATRIX(SR,RR) += 1.0f;
 		} else if ((src_mask & STEREO) == STEREO) {
 			spa_log_debug(mix->log, "produce SIDE from STEREO");
-			_MATRIX(SL,FL) += 1.0f;
-			_MATRIX(SR,FR) += 1.0f;
-		} else if ((src_mask & FRONT) == FRONT) {
+			_MATRIX(SL,FL) += slev;
+			_MATRIX(SR,FR) += slev;
+		} else if ((src_mask & FRONT) == FRONT &&
+			mix->upmix == CHANNELMIX_UPMIX_SIMPLE) {
 			spa_log_debug(mix->log, "produce SIDE from FC");
 			_MATRIX(SL,FC) += clev;
 			_MATRIX(SR,FC) += clev;
+		} else {
+			spa_log_debug(mix->log, "won't produce SIDE");
 		}
 	}
 	if (unassigned & REAR) {
@@ -441,12 +457,15 @@ static int make_matrix(struct channelmix *mix)
 			_MATRIX(RR,SR) += 1.0f;
 		} else if ((src_mask & STEREO) == STEREO) {
 			spa_log_debug(mix->log, "produce REAR from STEREO");
-			_MATRIX(RL,FL) += 1.0f;
-			_MATRIX(RR,FR) += 1.0f;
-		} else if ((src_mask & FRONT) == FRONT) {
+			_MATRIX(RL,FL) += slev;
+			_MATRIX(RR,FR) += slev;
+		} else if ((src_mask & FRONT) == FRONT &&
+			mix->upmix == CHANNELMIX_UPMIX_SIMPLE) {
 			spa_log_debug(mix->log, "produce REAR from FC");
 			_MATRIX(RL,FC) += clev;
 			_MATRIX(RR,FC) += clev;
+		} else {
+			spa_log_debug(mix->log, "won't produce SIDE");
 		}
 	}
 
@@ -551,6 +570,10 @@ int channelmix_init(struct channelmix *mix)
 {
 	const struct channelmix_info *info;
 
+	if (mix->src_chan > SPA_AUDIO_MAX_CHANNELS ||
+	    mix->dst_chan > SPA_AUDIO_MAX_CHANNELS)
+		return -EINVAL;
+
 	info = find_channelmix_info(mix->src_chan, mix->src_mask, mix->dst_chan, mix->dst_mask,
 			mix->cpu_flags);
 	if (info == NULL)
@@ -561,6 +584,7 @@ int channelmix_init(struct channelmix *mix)
 	mix->set_volume = impl_channelmix_set_volume;
 	mix->cpu_flags = info->cpu_flags;
 	mix->delay = mix->rear_delay * mix->freq / 1000.0f;
+	mix->func_name = info->name;
 
 	spa_log_debug(mix->log, "selected %s delay:%d options:%08x", info->name, mix->delay,
 			mix->options);
diff --git a/spa/plugins/audioconvert/channelmix-ops.h b/spa/plugins/audioconvert/channelmix-ops.h
index f9a17976e123338286c3ce3ccf730a8d357dac19..89737c315fc090a5d003080cfc9bd49bc98c9296 100644
--- a/spa/plugins/audioconvert/channelmix-ops.h
+++ b/spa/plugins/audioconvert/channelmix-ops.h
@@ -29,10 +29,6 @@
 #include <spa/utils/string.h>
 #include <spa/param/audio/raw.h>
 
-#undef SPA_LOG_TOPIC_DEFAULT
-#define SPA_LOG_TOPIC_DEFAULT log_topic
-extern struct spa_log_topic *log_topic;
-
 #include "crossover.h"
 #include "delay.h"
 
@@ -47,8 +43,6 @@ extern struct spa_log_topic *log_topic;
 #define MASK_5_1	_M(FL)|_M(FR)|_M(FC)|_M(LFE)|_M(SL)|_M(SR)|_M(RL)|_M(RR)
 #define MASK_7_1	_M(FL)|_M(FR)|_M(FC)|_M(LFE)|_M(SL)|_M(SR)|_M(RL)|_M(RR)
 
-#define BUFFER_SIZE 4096
-
 #define BUFFER_SIZE 4096
 #define MAX_TAPS 255
 
@@ -62,12 +56,13 @@ struct channelmix {
 #define CHANNELMIX_OPTION_NORMALIZE	(1<<1)		/**< normalize volumes */
 #define CHANNELMIX_OPTION_UPMIX		(1<<2)		/**< do simple upmixing */
 	uint32_t options;
-#define CHANNELMIX_UPMIX_NONE		(0)		/**< disable upmixing */
-#define CHANNELMIX_UPMIX_SIMPLE		(1)		/**< simple upmixing */
-#define CHANNELMIX_UPMIX_PSD		(2)		/**< Passive Surround Decoding upmixing */
+#define CHANNELMIX_UPMIX_NONE		0		/**< disable upmixing */
+#define CHANNELMIX_UPMIX_SIMPLE		1		/**< simple upmixing */
+#define CHANNELMIX_UPMIX_PSD		2		/**< Passive Surround Decoding upmixing */
 	uint32_t upmix;
 
 	struct spa_log *log;
+	const char *func_name;
 
 #define CHANNELMIX_FLAG_ZERO		(1<<0)		/**< all zero components */
 #define CHANNELMIX_FLAG_IDENTITY	(1<<1)		/**< identity matrix */
@@ -142,6 +137,7 @@ DEFINE_FUNCTION(f32_2_4, c);
 DEFINE_FUNCTION(f32_2_3p1, c);
 DEFINE_FUNCTION(f32_2_5p1, c);
 DEFINE_FUNCTION(f32_2_7p1, c);
+DEFINE_FUNCTION(f32_3p1_2, c);
 DEFINE_FUNCTION(f32_5p1_2, c);
 DEFINE_FUNCTION(f32_5p1_3p1, c);
 DEFINE_FUNCTION(f32_5p1_4, c);
@@ -151,9 +147,11 @@ DEFINE_FUNCTION(f32_7p1_4, c);
 
 #if defined (HAVE_SSE)
 DEFINE_FUNCTION(copy, sse);
-DEFINE_FUNCTION(f32_2_4, sse);
+DEFINE_FUNCTION(f32_3p1_2, sse);
 DEFINE_FUNCTION(f32_5p1_2, sse);
 DEFINE_FUNCTION(f32_5p1_3p1, sse);
 DEFINE_FUNCTION(f32_5p1_4, sse);
 DEFINE_FUNCTION(f32_7p1_4, sse);
 #endif
+
+#undef DEFINE_FUNCTION
diff --git a/spa/plugins/audioconvert/channelmix.c b/spa/plugins/audioconvert/channelmix.c
deleted file mode 100644
index 7f3964874193412b9967f7559c1c81097b98cc77..0000000000000000000000000000000000000000
--- a/spa/plugins/audioconvert/channelmix.c
+++ /dev/null
@@ -1,1786 +0,0 @@
-/* Spa
- *
- * Copyright © 2018 Wim Taymans
- *
- * Permission is hereby granted, free of charge, to any person obtaining a
- * copy of this software and associated documentation files (the "Software"),
- * to deal in the Software without restriction, including without limitation
- * the rights to use, copy, modify, merge, publish, distribute, sublicense,
- * and/or sell copies of the Software, and to permit persons to whom the
- * Software is furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice (including the next
- * paragraph) shall be included in all copies or substantial portions of the
- * Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
- * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
- * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
- * DEALINGS IN THE SOFTWARE.
- */
-
-#include <errno.h>
-#include <string.h>
-#include <stdio.h>
-
-#include <spa/support/plugin.h>
-#include <spa/support/log.h>
-#include <spa/support/cpu.h>
-#include <spa/utils/list.h>
-#include <spa/utils/names.h>
-#include <spa/utils/json.h>
-#include <spa/utils/string.h>
-#include <spa/node/keys.h>
-#include <spa/node/node.h>
-#include <spa/node/io.h>
-#include <spa/node/utils.h>
-#include <spa/param/audio/format-utils.h>
-#include <spa/param/param.h>
-#include <spa/pod/filter.h>
-#include <spa/debug/types.h>
-
-#include "channelmix-ops.h"
-
-
-#define DEFAULT_RATE		48000
-#define DEFAULT_CHANNELS	2
-
-#define MAX_BUFFERS	32
-#define MAX_DATAS	SPA_AUDIO_MAX_CHANNELS
-#define MAX_ALIGN	CHANNELMIX_OPS_MAX_ALIGN
-
-#define DEFAULT_CONTROL_BUFFER_SIZE	32768
-
-struct impl;
-
-#define DEFAULT_MUTE	false
-#define DEFAULT_VOLUME	1.0f
-
-struct volumes {
-	bool mute;
-	uint32_t n_volumes;
-	float volumes[SPA_AUDIO_MAX_CHANNELS];
-};
-
-static void init_volumes(struct volumes *vol)
-{
-	uint32_t i;
-	vol->mute = DEFAULT_MUTE;
-	vol->n_volumes = 0;
-	for (i = 0; i < SPA_AUDIO_MAX_CHANNELS; i++)
-		vol->volumes[i] = DEFAULT_VOLUME;
-}
-
-struct props {
-	float volume;
-	uint32_t n_channels;
-	uint32_t channel_map[SPA_AUDIO_MAX_CHANNELS];
-	struct volumes channel;
-	struct volumes soft;
-	struct volumes monitor;
-	unsigned int have_soft_volume:1;
-	unsigned int disabled:1;
-};
-
-static void props_reset(struct props *props)
-{
-	uint32_t i;
-	props->volume = DEFAULT_VOLUME;
-	props->n_channels = 0;
-	for (i = 0; i < SPA_AUDIO_MAX_CHANNELS; i++)
-		props->channel_map[i] = SPA_AUDIO_CHANNEL_UNKNOWN;
-	init_volumes(&props->channel);
-	init_volumes(&props->soft);
-	init_volumes(&props->monitor);
-}
-
-struct buffer {
-	uint32_t id;
-#define BUFFER_FLAG_OUT		(1 << 0)
-	uint32_t flags;
-	struct spa_list link;
-	struct spa_buffer *outbuf;
-	struct spa_meta_header *h;
-	void *datas[MAX_DATAS];
-};
-
-struct port {
-	uint32_t direction;
-	uint32_t id;
-
-	uint64_t info_all;
-	struct spa_port_info info;
-#define IDX_EnumFormat	0
-#define IDX_Meta	1
-#define IDX_IO		2
-#define IDX_Format	3
-#define IDX_Buffers	4
-	struct spa_param_info params[5];
-
-	struct spa_io_buffers *io;
-
-	bool have_format;
-	struct spa_audio_info format;
-	uint32_t stride;
-	uint32_t blocks;
-	uint32_t size;
-
-	struct buffer buffers[MAX_BUFFERS];
-	uint32_t n_buffers;
-
-	struct spa_list queue;
-
-	struct spa_pod_sequence *ctrl;
-	uint32_t ctrl_offset;
-};
-
-struct impl {
-	struct spa_handle handle;
-	struct spa_node node;
-
-	struct spa_log *log;
-	struct spa_cpu *cpu;
-	uint32_t quantum_limit;
-
-	enum spa_direction direction;
-	struct spa_io_position *io_position;
-
-	struct spa_hook_list hooks;
-
-	uint64_t info_all;
-	struct spa_node_info info;
-	struct props props;
-#define IDX_PropInfo	0
-#define IDX_Props	1
-	struct spa_param_info params[2];
-
-
-	struct port control_port;
-	struct port in_port;
-	struct port out_port;
-
-	struct channelmix mix;
-	unsigned int started:1;
-	unsigned int is_passthrough:1;
-	uint32_t cpu_flags;
-	uint32_t max_align;
-};
-
-#define IS_CONTROL_PORT(this,d,id)	(id == 1 && d == SPA_DIRECTION_INPUT)
-#define IS_DATA_PORT(this,d,id)		(id == 0)
-
-#define CHECK_PORT(this,d,id)		(IS_CONTROL_PORT(this,d,id) || IS_DATA_PORT(this,d,id))
-#define GET_CONTROL_PORT(this,id)	(&this->control_port)
-#define GET_IN_PORT(this,id)		(&this->in_port)
-#define GET_OUT_PORT(this,id)		(&this->out_port)
-#define GET_PORT(this,d,id)		(IS_CONTROL_PORT(this,d,id) ? GET_CONTROL_PORT(this,id) : (d == SPA_DIRECTION_INPUT ? GET_IN_PORT(this,id) : GET_OUT_PORT(this,id)))
-
-#define _MASK(ch)	(1ULL << SPA_AUDIO_CHANNEL_ ## ch)
-#define STEREO	(_MASK(FL)|_MASK(FR))
-
-static void emit_info(struct impl *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) {
-		spa_node_emit_info(&this->hooks, &this->info);
-		this->info.change_mask = old;
-	}
-}
-
-static void emit_props_changed(struct impl *this)
-{
-	this->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS;
-	this->params[IDX_Props].flags ^= SPA_PARAM_INFO_SERIAL;
-	emit_info(this, false);
-}
-
-static uint64_t default_mask(uint32_t channels)
-{
-	uint64_t mask = 0;
-	switch (channels) {
-	case 7:
-	case 8:
-		mask |= _MASK(RL);
-		mask |= _MASK(RR);
-		SPA_FALLTHROUGH
-	case 5:
-	case 6:
-		mask |= _MASK(SL);
-		mask |= _MASK(SR);
-		if ((channels & 1) == 0)
-			mask |= _MASK(LFE);
-		SPA_FALLTHROUGH
-	case 3:
-		mask |= _MASK(FC);
-		SPA_FALLTHROUGH
-	case 2:
-		mask |= _MASK(FL);
-		mask |= _MASK(FR);
-		break;
-	case 1:
-		mask |= _MASK(MONO);
-		break;
-	case 4:
-		mask |= _MASK(FL);
-		mask |= _MASK(FR);
-		mask |= _MASK(RL);
-		mask |= _MASK(RR);
-		break;
-	}
-	return mask;
-}
-
-static void fix_volumes(struct volumes *vols, uint32_t channels)
-{
-	float s;
-	uint32_t i;
-	if (vols->n_volumes > 0) {
-		s = 0.0f;
-		for (i = 0; i < vols->n_volumes; i++)
-			s += vols->volumes[i];
-		s /= vols->n_volumes;
-	} else {
-		s = 1.0f;
-	}
-	vols->n_volumes = channels;
-	for (i = 0; i < vols->n_volumes; i++)
-		vols->volumes[i] = s;
-}
-
-static int remap_volumes(struct impl *this, const struct spa_audio_info *info)
-{
-	struct props *p = &this->props;
-	uint32_t i, j, target = info->info.raw.channels;
-
-	for (i = 0; i < p->n_channels; i++) {
-		for (j = i; j < target; j++) {
-			spa_log_debug(this->log, "%d %d: %d <-> %d", i, j,
-					p->channel_map[i], info->info.raw.position[j]);
-			if (p->channel_map[i] != info->info.raw.position[j])
-				continue;
-			if (i != j) {
-				SPA_SWAP(p->channel_map[i], p->channel_map[j]);
-				SPA_SWAP(p->channel.volumes[i], p->channel.volumes[j]);
-				SPA_SWAP(p->soft.volumes[i], p->soft.volumes[j]);
-				SPA_SWAP(p->monitor.volumes[i], p->monitor.volumes[j]);
-			}
-			break;
-		}
-	}
-	p->n_channels = target;
-	for (i = 0; i < p->n_channels; i++)
-		p->channel_map[i] = info->info.raw.position[i];
-
-	if (target == 0)
-		return 0;
-	if (p->channel.n_volumes != target)
-		fix_volumes(&p->channel, target);
-	if (p->soft.n_volumes != target)
-		fix_volumes(&p->soft, target);
-	if (p->monitor.n_volumes != target)
-		fix_volumes(&p->monitor, target);
-
-	return 1;
-}
-
-static void set_volume(struct impl *this)
-{
-	struct volumes *vol;
-
-	if (this->mix.set_volume == NULL ||
-	    this->props.disabled)
-		return;
-
-	if (this->props.have_soft_volume)
-		vol = &this->props.soft;
-	else
-		vol = &this->props.channel;
-
-	channelmix_set_volume(&this->mix, this->props.volume, vol->mute,
-			vol->n_volumes, vol->volumes);
-}
-
-static int setup_convert(struct impl *this,
-		enum spa_direction direction,
-		const struct spa_audio_info *info)
-{
-	const struct spa_audio_info *src_info, *dst_info;
-	uint32_t i, src_chan, dst_chan, p;
-	uint64_t src_mask, dst_mask;
-	int res;
-
-	if (direction == SPA_DIRECTION_INPUT) {
-		src_info = info;
-		dst_info = &GET_OUT_PORT(this, 0)->format;
-	} else {
-		src_info = &GET_IN_PORT(this, 0)->format;
-		dst_info = info;
-	}
-
-	src_chan = src_info->info.raw.channels;
-	dst_chan = dst_info->info.raw.channels;
-
-	for (i = 0, src_mask = 0; i < src_chan; i++) {
-		p = src_info->info.raw.position[i];
-		src_mask |= 1ULL << (p < 64 ? p : 0);
-	}
-	for (i = 0, dst_mask = 0; i < dst_chan; i++) {
-		p = dst_info->info.raw.position[i];
-		dst_mask |= 1ULL << (p < 64 ? p : 0);
-	}
-
-	if (src_mask & 1)
-		src_mask = default_mask(src_chan);
-	if (dst_mask & 1)
-		dst_mask = default_mask(dst_chan);
-
-	spa_log_info(this->log, "%p: %s/%d@%d->%s/%d@%d %08"PRIx64":%08"PRIx64, this,
-			spa_debug_type_find_name(spa_type_audio_format, src_info->info.raw.format),
-			src_chan,
-			src_info->info.raw.rate,
-			spa_debug_type_find_name(spa_type_audio_format, dst_info->info.raw.format),
-			dst_chan,
-			dst_info->info.raw.rate,
-			src_mask, dst_mask);
-
-	if (src_info->info.raw.rate != dst_info->info.raw.rate)
-		return -EINVAL;
-
-	this->mix.src_chan = src_chan;
-	this->mix.src_mask = src_mask;
-	this->mix.dst_chan = dst_chan;
-	this->mix.dst_mask = dst_mask;
-	this->mix.cpu_flags = this->cpu_flags;
-	this->mix.log = this->log;
-	this->mix.freq = src_info->info.raw.rate;
-
-	if ((res = channelmix_init(&this->mix)) < 0)
-		return res;
-
-	remap_volumes(this, this->direction == SPA_DIRECTION_INPUT ? src_info : dst_info);
-	set_volume(this);
-
-	emit_props_changed(this);
-
-	this->is_passthrough = SPA_FLAG_IS_SET(this->mix.flags, CHANNELMIX_FLAG_IDENTITY);
-	if (!this->is_passthrough && this->props.disabled)
-		return -EINVAL;
-
-	spa_log_debug(this->log, "%p: got channelmix features %08x:%08x flags:%08x passthrough:%d",
-			this, this->cpu_flags, this->mix.cpu_flags,
-			this->mix.flags, this->is_passthrough);
-
-
-	return 0;
-}
-
-static int impl_node_enum_params(void *object, int seq,
-				 uint32_t id, uint32_t start, uint32_t num,
-				 const struct spa_pod *filter)
-{
-	struct impl *this = object;
-	struct spa_pod *param;
-	struct spa_pod_builder b = { 0 };
-	uint8_t buffer[4096];
-	struct spa_result_node_params result;
-	uint32_t count = 0;
-
-	spa_return_val_if_fail(this != NULL, -EINVAL);
-	spa_return_val_if_fail(num != 0, -EINVAL);
-
-	result.id = id;
-	result.next = start;
-      next:
-	result.index = result.next++;
-
-	spa_pod_builder_init(&b, buffer, sizeof(buffer));
-
-	switch (id) {
-	case SPA_PARAM_PropInfo:
-	{
-		struct props *p = &this->props;
-
-		switch (result.index) {
-		case 0:
-			param = spa_pod_builder_add_object(&b,
-				SPA_TYPE_OBJECT_PropInfo, id,
-				SPA_PROP_INFO_id,   SPA_POD_Id(SPA_PROP_volume),
-				SPA_PROP_INFO_description, SPA_POD_String("Volume"),
-				SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float(p->volume, 0.0, 10.0));
-			break;
-		case 1:
-			param = spa_pod_builder_add_object(&b,
-				SPA_TYPE_OBJECT_PropInfo, id,
-				SPA_PROP_INFO_id,   SPA_POD_Id(SPA_PROP_mute),
-				SPA_PROP_INFO_description, SPA_POD_String("Mute"),
-				SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(p->channel.mute));
-			break;
-		case 2:
-			param = spa_pod_builder_add_object(&b,
-				SPA_TYPE_OBJECT_PropInfo, id,
-				SPA_PROP_INFO_id,   SPA_POD_Id(SPA_PROP_channelVolumes),
-				SPA_PROP_INFO_description, SPA_POD_String("Channel Volumes"),
-				SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float(p->volume, 0.0, 10.0),
-				SPA_PROP_INFO_container, SPA_POD_Id(SPA_TYPE_Array));
-			break;
-		case 3:
-			param = spa_pod_builder_add_object(&b,
-				SPA_TYPE_OBJECT_PropInfo, id,
-				SPA_PROP_INFO_id,   SPA_POD_Id(SPA_PROP_channelMap),
-				SPA_PROP_INFO_description, SPA_POD_String("Channel Map"),
-				SPA_PROP_INFO_type, SPA_POD_Id(SPA_AUDIO_CHANNEL_UNKNOWN),
-				SPA_PROP_INFO_container, SPA_POD_Id(SPA_TYPE_Array));
-			break;
-		case 4:
-			param = spa_pod_builder_add_object(&b,
-				SPA_TYPE_OBJECT_PropInfo, id,
-				SPA_PROP_INFO_id,   SPA_POD_Id(SPA_PROP_softMute),
-				SPA_PROP_INFO_description, SPA_POD_String("Soft Mute"),
-				SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(p->soft.mute));
-			break;
-		case 5:
-			param = spa_pod_builder_add_object(&b,
-				SPA_TYPE_OBJECT_PropInfo, id,
-				SPA_PROP_INFO_id,   SPA_POD_Id(SPA_PROP_softVolumes),
-				SPA_PROP_INFO_description, SPA_POD_String("Soft Volumes"),
-				SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float(p->volume, 0.0, 10.0),
-				SPA_PROP_INFO_container, SPA_POD_Id(SPA_TYPE_Array));
-			break;
-		case 6:
-			param = spa_pod_builder_add_object(&b,
-				SPA_TYPE_OBJECT_PropInfo, id,
-				SPA_PROP_INFO_id,   SPA_POD_Id(SPA_PROP_monitorMute),
-				SPA_PROP_INFO_description, SPA_POD_String("Monitor Mute"),
-				SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(p->monitor.mute));
-			break;
-		case 7:
-			param = spa_pod_builder_add_object(&b,
-				SPA_TYPE_OBJECT_PropInfo, id,
-				SPA_PROP_INFO_id,   SPA_POD_Id(SPA_PROP_monitorVolumes),
-				SPA_PROP_INFO_description, SPA_POD_String("Monitor Volumes"),
-				SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float(p->volume, 0.0, 10.0),
-				SPA_PROP_INFO_container, SPA_POD_Id(SPA_TYPE_Array));
-			break;
-		case 8:
-			param = spa_pod_builder_add_object(&b,
-				SPA_TYPE_OBJECT_PropInfo, id,
-				SPA_PROP_INFO_name, SPA_POD_String("channelmix.disable"),
-				SPA_PROP_INFO_description, SPA_POD_String("Disable Channel mixing"),
-				SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(p->disabled),
-				SPA_PROP_INFO_params, SPA_POD_Bool(true));
-			break;
-		case 9:
-			param = spa_pod_builder_add_object(&b,
-				SPA_TYPE_OBJECT_PropInfo, id,
-				SPA_PROP_INFO_name, SPA_POD_String("channelmix.normalize"),
-				SPA_PROP_INFO_description, SPA_POD_String("Normalize Volumes"),
-				SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(
-					SPA_FLAG_IS_SET(this->mix.options, CHANNELMIX_OPTION_NORMALIZE)),
-				SPA_PROP_INFO_params, SPA_POD_Bool(true));
-			break;
-		case 10:
-			param = spa_pod_builder_add_object(&b,
-				SPA_TYPE_OBJECT_PropInfo, id,
-				SPA_PROP_INFO_name, SPA_POD_String("channelmix.mix-lfe"),
-				SPA_PROP_INFO_description, SPA_POD_String("Mix LFE into channels"),
-				SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(
-					SPA_FLAG_IS_SET(this->mix.options, CHANNELMIX_OPTION_MIX_LFE)),
-				SPA_PROP_INFO_params, SPA_POD_Bool(true));
-			break;
-		case 11:
-			param = spa_pod_builder_add_object(&b,
-				SPA_TYPE_OBJECT_PropInfo, id,
-				SPA_PROP_INFO_name, SPA_POD_String("channelmix.upmix"),
-				SPA_PROP_INFO_description, SPA_POD_String("Enable upmixing"),
-				SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(
-					SPA_FLAG_IS_SET(this->mix.options, CHANNELMIX_OPTION_UPMIX)),
-				SPA_PROP_INFO_params, SPA_POD_Bool(true));
-			break;
-		case 12:
-			param = spa_pod_builder_add_object(&b,
-				SPA_TYPE_OBJECT_PropInfo, id,
-				SPA_PROP_INFO_name, SPA_POD_String("channelmix.lfe-cutoff"),
-				SPA_PROP_INFO_description, SPA_POD_String("LFE cutoff frequency (Hz)"),
-				SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float(
-					this->mix.lfe_cutoff, 0.0, 1000.0),
-				SPA_PROP_INFO_params, SPA_POD_Bool(true));
-			break;
-		case 13:
-			param = spa_pod_builder_add_object(&b,
-				SPA_TYPE_OBJECT_PropInfo, id,
-				SPA_PROP_INFO_name, SPA_POD_String("channelmix.fc-cutoff"),
-				SPA_PROP_INFO_description, SPA_POD_String("FC cutoff frequency (Hz)"),
-				SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float(
-					this->mix.fc_cutoff, 0.0, 48000.0),
-				SPA_PROP_INFO_params, SPA_POD_Bool(true));
-			break;
-		case 14:
-			param = spa_pod_builder_add_object(&b,
-				SPA_TYPE_OBJECT_PropInfo, id,
-				SPA_PROP_INFO_name, SPA_POD_String("channelmix.rear-delay"),
-				SPA_PROP_INFO_description, SPA_POD_String("Rear channels delay (ms)"),
-				SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float(
-					this->mix.rear_delay, 0.0, 1000.0),
-				SPA_PROP_INFO_params, SPA_POD_Bool(true));
-			break;
-		case 15:
-			param = spa_pod_builder_add_object(&b,
-				SPA_TYPE_OBJECT_PropInfo, id,
-				SPA_PROP_INFO_name, SPA_POD_String("channelmix.stereo-widen"),
-				SPA_PROP_INFO_description, SPA_POD_String("Stereo widen"),
-				SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float(
-					this->mix.widen, 0.0, 1.0),
-				SPA_PROP_INFO_params, SPA_POD_Bool(true));
-			break;
-		case 16:
-			param = spa_pod_builder_add_object(&b,
-				SPA_TYPE_OBJECT_PropInfo, id,
-				SPA_PROP_INFO_name, SPA_POD_String("channelmix.hilbert-taps"),
-				SPA_PROP_INFO_description, SPA_POD_String("Taps for phase shift of rear"),
-				SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Int(
-					this->mix.hilbert_taps, 0, MAX_TAPS),
-				SPA_PROP_INFO_params, SPA_POD_Bool(true));
-			break;
-		case 17:
-		{
-			struct spa_pod_frame f[2];
-			uint32_t i;
-			spa_pod_builder_push_object(&b, &f[0],
-				SPA_TYPE_OBJECT_PropInfo, id);
-			spa_pod_builder_add(&b,
-				SPA_PROP_INFO_name, SPA_POD_String("channelmix.upmix-method"),
-				SPA_PROP_INFO_description, SPA_POD_String("Upmix Method to use"),
-				SPA_PROP_INFO_type, SPA_POD_String(
-					channelmix_upmix_info[this->mix.upmix].label),
-				0);
-			spa_pod_builder_prop(&b, SPA_PROP_INFO_labels, 0);
-			spa_pod_builder_push_struct(&b, &f[1]);
-			for (i = 0; i < SPA_N_ELEMENTS(channelmix_upmix_info); i++) {
-				spa_pod_builder_string(&b, channelmix_upmix_info[i].label);
-				spa_pod_builder_string(&b, channelmix_upmix_info[i].description);
-			}
-			spa_pod_builder_pop(&b, &f[1]);
-			spa_pod_builder_add(&b,
-				SPA_PROP_INFO_params, SPA_POD_Bool(true),
-				0);
-			param = spa_pod_builder_pop(&b, &f[0]);
-			break;
-		}
-		default:
-			return 0;
-		}
-		break;
-	}
-	case SPA_PARAM_Props:
-	{
-		struct props *p = &this->props;
-		struct spa_pod_frame f[2];
-
-		switch (result.index) {
-		case 0:
-			spa_pod_builder_push_object(&b, &f[0],
-                                SPA_TYPE_OBJECT_Props, id);
-			spa_pod_builder_add(&b,
-				SPA_PROP_volume,		SPA_POD_Float(p->volume),
-				SPA_PROP_mute,			SPA_POD_Bool(p->channel.mute),
-				SPA_PROP_channelVolumes,	SPA_POD_Array(sizeof(float),
-									SPA_TYPE_Float,
-									p->channel.n_volumes,
-									p->channel.volumes),
-				SPA_PROP_channelMap,		SPA_POD_Array(sizeof(uint32_t),
-									SPA_TYPE_Id,
-									p->n_channels,
-									p->channel_map),
-				SPA_PROP_softMute,		SPA_POD_Bool(p->soft.mute),
-				SPA_PROP_softVolumes,		SPA_POD_Array(sizeof(float),
-									SPA_TYPE_Float,
-									p->soft.n_volumes,
-									p->soft.volumes),
-				SPA_PROP_monitorMute,		SPA_POD_Bool(p->monitor.mute),
-				SPA_PROP_monitorVolumes,	SPA_POD_Array(sizeof(float),
-									SPA_TYPE_Float,
-									p->monitor.n_volumes,
-									p->monitor.volumes),
-				0);
-			spa_pod_builder_prop(&b, SPA_PROP_params, 0);
-			spa_pod_builder_push_struct(&b, &f[1]);
-			spa_pod_builder_string(&b, "channelmix.disable");
-			spa_pod_builder_bool(&b, this->props.disabled);
-			spa_pod_builder_string(&b, "channelmix.normalize");
-			spa_pod_builder_bool(&b, SPA_FLAG_IS_SET(this->mix.options,
-						CHANNELMIX_OPTION_NORMALIZE));
-			spa_pod_builder_string(&b, "channelmix.mix-lfe");
-			spa_pod_builder_bool(&b, SPA_FLAG_IS_SET(this->mix.options,
-						CHANNELMIX_OPTION_MIX_LFE));
-			spa_pod_builder_string(&b, "channelmix.upmix");
-			spa_pod_builder_bool(&b, SPA_FLAG_IS_SET(this->mix.options,
-						CHANNELMIX_OPTION_UPMIX));
-			spa_pod_builder_string(&b, "channelmix.lfe-cutoff");
-			spa_pod_builder_float(&b, this->mix.lfe_cutoff);
-			spa_pod_builder_string(&b, "channelmix.fc-cutoff");
-			spa_pod_builder_float(&b, this->mix.fc_cutoff);
-			spa_pod_builder_string(&b, "channelmix.rear-delay");
-			spa_pod_builder_float(&b, this->mix.rear_delay);
-			spa_pod_builder_string(&b, "channelmix.stereo-widen");
-			spa_pod_builder_float(&b, this->mix.widen);
-			spa_pod_builder_string(&b, "channelmix.hilbert-taps");
-			spa_pod_builder_int(&b, this->mix.hilbert_taps);
-			spa_pod_builder_string(&b, "channelmix.upmix-method");
-			spa_pod_builder_string(&b, channelmix_upmix_info[this->mix.upmix].label);
-			spa_pod_builder_pop(&b, &f[1]);
-			param = spa_pod_builder_pop(&b, &f[0]);
-			break;
-		default:
-			return 0;
-		}
-		break;
-	}
-	default:
-		return -ENOENT;
-	}
-
-	if (spa_pod_filter(&b, &result.param, param, filter) < 0)
-		goto next;
-
-	spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
-
-	if (++count != num)
-		goto next;
-
-	return 0;
-}
-
-static int channelmix_set_param(struct impl *this, const char *k, const char *s)
-{
-	if (spa_streq(k, "channelmix.disable"))
-		this->props.disabled = spa_atob(s);
-	else if (spa_streq(k, "channelmix.normalize"))
-		SPA_FLAG_UPDATE(this->mix.options, CHANNELMIX_OPTION_NORMALIZE, spa_atob(s));
-	else if (spa_streq(k, "channelmix.mix-lfe"))
-		SPA_FLAG_UPDATE(this->mix.options, CHANNELMIX_OPTION_MIX_LFE, spa_atob(s));
-	else if (spa_streq(k, "channelmix.upmix"))
-		SPA_FLAG_UPDATE(this->mix.options, CHANNELMIX_OPTION_UPMIX, spa_atob(s));
-	else if (spa_streq(k, "channelmix.lfe-cutoff"))
-		spa_atof(s, &this->mix.lfe_cutoff);
-	else if (spa_streq(k, "channelmix.fc-cutoff"))
-		spa_atof(s, &this->mix.fc_cutoff);
-	else if (spa_streq(k, "channelmix.rear-delay"))
-		spa_atof(s, &this->mix.rear_delay);
-	else if (spa_streq(k, "channelmix.stereo-widen"))
-		spa_atof(s, &this->mix.widen);
-	else if (spa_streq(k, "channelmix.hilbert-taps"))
-		spa_atou32(s, &this->mix.hilbert_taps, 0);
-	else if (spa_streq(k, "channelmix.upmix-method"))
-		this->mix.upmix = channelmix_upmix_from_label(s);
-	else
-		return 0;
-	return 1;
-}
-
-static int parse_prop_params(struct impl *this, struct spa_pod *params)
-{
-	struct spa_pod_parser prs;
-	struct spa_pod_frame f;
-	int changed = 0;
-
-	spa_pod_parser_pod(&prs, params);
-	if (spa_pod_parser_push_struct(&prs, &f) < 0)
-		return 0;
-
-	while (true) {
-		const char *name;
-		struct spa_pod *pod;
-		char value[512], buf[128];
-
-		if (spa_pod_parser_get_string(&prs, &name) < 0)
-			break;
-
-		if (spa_pod_parser_get_pod(&prs, &pod) < 0)
-			break;
-
-		if (spa_pod_is_string(pod)) {
-			spa_pod_copy_string(pod, sizeof(value), value);
-		} else if (spa_pod_is_float(pod)) {
-			snprintf(value, sizeof(value), "%s",
-					spa_json_format_float(buf, sizeof(buf),
-						SPA_POD_VALUE(struct spa_pod_float, pod)));
-		} else if (spa_pod_is_int(pod)) {
-			snprintf(value, sizeof(value), "%d",
-					SPA_POD_VALUE(struct spa_pod_int, pod));
-		} else if (spa_pod_is_bool(pod)) {
-			snprintf(value, sizeof(value), "%s",
-					SPA_POD_VALUE(struct spa_pod_bool, pod) ?
-					"true" : "false");
-		} else
-			continue;
-
-		spa_log_info(this->log, "key:'%s' val:'%s'", name, value);
-		changed += channelmix_set_param(this, name, value);
-	}
-	if (changed && !this->props.disabled)
-		channelmix_init(&this->mix);
-	return changed;
-}
-
-static int apply_props(struct impl *this, const struct spa_pod *param)
-{
-	struct spa_pod_prop *prop;
-	struct spa_pod_object *obj = (struct spa_pod_object *) param;
-	struct props *p = &this->props;
-	int changed = 0;
-	bool have_channel_volume = false;
-	bool have_soft_volume = false;
-	uint32_t n;
-
-	if (param == NULL)
-		return 0;
-
-	SPA_POD_OBJECT_FOREACH(obj, prop) {
-		switch (prop->key) {
-		case SPA_PROP_volume:
-			if (spa_pod_get_float(&prop->value, &p->volume) == 0)
-				changed++;
-			break;
-		case SPA_PROP_mute:
-			if (spa_pod_get_bool(&prop->value, &p->channel.mute) == 0) {
-				changed++;
-				have_channel_volume = true;
-			}
-			break;
-		case SPA_PROP_channelVolumes:
-			if ((n = spa_pod_copy_array(&prop->value, SPA_TYPE_Float,
-					p->channel.volumes, SPA_AUDIO_MAX_CHANNELS)) > 0) {
-				p->channel.n_volumes = n;
-				changed++;
-				have_channel_volume = true;
-			}
-			break;
-		case SPA_PROP_channelMap:
-			if ((n = spa_pod_copy_array(&prop->value, SPA_TYPE_Id,
-					p->channel_map, SPA_AUDIO_MAX_CHANNELS)) > 0) {
-				p->n_channels = n;
-				changed++;
-			}
-			break;
-		case SPA_PROP_softMute:
-			if (spa_pod_get_bool(&prop->value, &p->soft.mute) == 0) {
-				changed++;
-				have_soft_volume = true;
-			}
-			break;
-		case SPA_PROP_softVolumes:
-			if ((n = spa_pod_copy_array(&prop->value, SPA_TYPE_Float,
-					p->soft.volumes, SPA_AUDIO_MAX_CHANNELS)) > 0) {
-				p->soft.n_volumes = n;
-				changed++;
-				have_soft_volume = true;
-			}
-			break;
-		case SPA_PROP_monitorMute:
-			if (spa_pod_get_bool(&prop->value, &p->monitor.mute) == 0)
-				changed++;
-			break;
-		case SPA_PROP_monitorVolumes:
-			if ((n = spa_pod_copy_array(&prop->value, SPA_TYPE_Float,
-					p->monitor.volumes, SPA_AUDIO_MAX_CHANNELS)) > 0) {
-				p->monitor.n_volumes = n;
-				changed++;
-			}
-			break;
-		case SPA_PROP_params:
-			changed += parse_prop_params(this, &prop->value);
-			break;
-		default:
-			break;
-		}
-	}
-
-	if (changed) {
-		struct port *port = GET_PORT(this, this->direction, 0);
-		if (have_soft_volume)
-			p->have_soft_volume = true;
-		else if (have_channel_volume)
-			p->have_soft_volume = false;
-
-		if (port->have_format)
-			remap_volumes(this, &port->format);
-
-		set_volume(this);
-	}
-	return changed;
-}
-
-static int apply_midi(struct impl *this, const struct spa_pod *value)
-{
-	const uint8_t *val = SPA_POD_BODY(value);
-	uint32_t size = SPA_POD_BODY_SIZE(value);
-	struct props *p = &this->props;
-
-	if (size < 3)
-		return -EINVAL;
-
-	if ((val[0] & 0xf0) != 0xb0 || val[1] != 7)
-		return 0;
-
-	p->volume = val[2] / 127.0;
-	set_volume(this);
-	return 1;
-}
-
-static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size)
-{
-	struct impl *this = object;
-
-	spa_return_val_if_fail(this != NULL, -EINVAL);
-
-	spa_log_debug(this->log, "%p: io %d %p/%zd", this, id, data, size);
-
-	switch (id) {
-	case SPA_IO_Position:
-		this->io_position = data;
-		break;
-	default:
-		return -ENOENT;
-	}
-	return 0;
-}
-
-static int impl_node_set_param(void *object, uint32_t id, uint32_t flags,
-			       const struct spa_pod *param)
-{
-	struct impl *this = object;
-
-	spa_return_val_if_fail(this != NULL, -EINVAL);
-
-	switch (id) {
-	case SPA_PARAM_Props:
-		if (apply_props(this, param) > 0)
-			emit_props_changed(this);
-		break;
-	default:
-		return -ENOENT;
-	}
-	return 0;
-}
-
-static int impl_node_send_command(void *object, const struct spa_command *command)
-{
-	struct impl *this = object;
-
-	spa_return_val_if_fail(this != NULL, -EINVAL);
-	spa_return_val_if_fail(command != NULL, -EINVAL);
-
-	switch (SPA_NODE_COMMAND_ID(command)) {
-	case SPA_NODE_COMMAND_Start:
-		this->started = true;
-		break;
-	case SPA_NODE_COMMAND_Suspend:
-	case SPA_NODE_COMMAND_Flush:
-	case SPA_NODE_COMMAND_Pause:
-		this->started = false;
-		break;
-	default:
-		return -ENOTSUP;
-	}
-	return 0;
-}
-
-static void emit_port_info(struct impl *this, struct port *port, bool full)
-{
-	uint64_t old = full ? port->info.change_mask : 0;
-	if (full)
-		port->info.change_mask = port->info_all;
-	if (port->info.change_mask) {
-		spa_node_emit_port_info(&this->hooks,
-				port->direction, port->id, &port->info);
-		port->info.change_mask = old;
-	}
-}
-
-static int
-impl_node_add_listener(void *object,
-		struct spa_hook *listener,
-		const struct spa_node_events *events,
-		void *data)
-{
-	struct impl *this = object;
-	struct spa_hook_list save;
-	struct spa_dict_item items[2];
-	uint32_t n_items = 0;
-
-	spa_return_val_if_fail(this != NULL, -EINVAL);
-
-	spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
-
-	emit_info(this, true);
-	emit_port_info(this, GET_IN_PORT(this, 0), true);
-	emit_port_info(this, GET_OUT_PORT(this, 0), true);
-
-	struct port *control_port = GET_CONTROL_PORT(this, 1);
-	items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_PORT_NAME, "control");
-	items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_FORMAT_DSP, "8 bit raw midi");
-	control_port->info.props = &SPA_DICT_INIT(items, n_items);
-	emit_port_info(this, control_port, true);
-
-	spa_hook_list_join(&this->hooks, &save);
-
-	return 0;
-}
-
-static int
-impl_node_set_callbacks(void *object,
-			const struct spa_node_callbacks *callbacks,
-			void *user_data)
-{
-	return 0;
-}
-
-static int impl_node_add_port(void *object,
-		enum spa_direction direction, uint32_t port_id,
-		const struct spa_dict *props)
-{
-	return -ENOTSUP;
-}
-
-static int impl_node_remove_port(void *object,
-		enum spa_direction direction, uint32_t port_id)
-{
-	return -ENOTSUP;
-}
-
-static int port_enum_formats(void *object,
-			     enum spa_direction direction, uint32_t port_id,
-			     uint32_t index,
-			     struct spa_pod **param,
-			     struct spa_pod_builder *builder)
-{
-	struct impl *this = object;
-
-	switch (index) {
-	case 0:
-		if (IS_CONTROL_PORT(this, direction, port_id)) {
-			*param = spa_pod_builder_add_object(builder,
-				SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat,
-				SPA_FORMAT_mediaType,	   SPA_POD_Id(SPA_MEDIA_TYPE_application),
-				SPA_FORMAT_mediaSubtype,   SPA_POD_Id(SPA_MEDIA_SUBTYPE_control));
-		} else {
-			struct spa_pod_frame f;
-			struct port *other;
-			int32_t channels, min = 1, max = INT32_MAX;
-
-			other = GET_PORT(this, SPA_DIRECTION_REVERSE(direction), 0);
-
-			spa_pod_builder_push_object(builder, &f,
-				SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat);
-			spa_pod_builder_add(builder,
-				SPA_FORMAT_mediaType,      SPA_POD_Id(SPA_MEDIA_TYPE_audio),
-				SPA_FORMAT_mediaSubtype,   SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw),
-				SPA_FORMAT_AUDIO_format,   SPA_POD_Id(SPA_AUDIO_FORMAT_F32P),
-				0);
-
-			if (other->have_format) {
-				channels = other->format.info.raw.channels;
-				if (this->props.disabled)
-					min = max = channels;
-
-				spa_pod_builder_add(builder,
-					SPA_FORMAT_AUDIO_rate, SPA_POD_Int(other->format.info.raw.rate),
-					SPA_FORMAT_AUDIO_channels, SPA_POD_CHOICE_RANGE_Int(
-						channels, min, max),
-					0);
-			} else {
-				uint32_t rate = this->io_position ?
-					this->io_position->clock.rate.denom : DEFAULT_RATE;
-				channels = DEFAULT_CHANNELS;
-
-				spa_pod_builder_add(builder,
-					SPA_FORMAT_AUDIO_rate, SPA_POD_CHOICE_RANGE_Int(rate, 0, INT32_MAX),
-					SPA_FORMAT_AUDIO_channels, SPA_POD_CHOICE_RANGE_Int(
-						channels, min, max),
-					0);
-			}
-			*param = spa_pod_builder_pop(builder, &f);
-		}
-		break;
-	default:
-		return 0;
-	}
-	return 1;
-}
-
-static int
-impl_node_port_enum_params(void *object, int seq,
-			   enum spa_direction direction, uint32_t port_id,
-			   uint32_t id, uint32_t start, uint32_t num,
-			   const struct spa_pod *filter)
-{
-	struct impl *this = object;
-	struct port *port, *other;
-	struct spa_pod *param;
-	struct spa_pod_builder b = { 0 };
-	uint8_t buffer[1024];
-	struct spa_result_node_params result;
-	uint32_t count = 0;
-	int res;
-
-	spa_return_val_if_fail(this != NULL, -EINVAL);
-	spa_return_val_if_fail(num != 0, -EINVAL);
-	spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
-
-	port = GET_PORT(this, direction, port_id);
-	other = GET_PORT(this, SPA_DIRECTION_REVERSE(direction), port_id);
-
-	spa_log_debug(this->log, "%p: enum params port %d.%d %d %u",
-			this, direction, port_id, seq, id);
-
-	result.id = id;
-	result.next = start;
-      next:
-	result.index = result.next++;
-
-	spa_pod_builder_init(&b, buffer, sizeof(buffer));
-
-	switch (id) {
-	case SPA_PARAM_EnumFormat:
-		if ((res = port_enum_formats(this, direction, port_id,
-						result.index, &param, &b)) <= 0)
-			return res;
-		break;
-
-	case SPA_PARAM_Format:
-		if (!port->have_format)
-			return -EIO;
-		if (result.index > 0)
-			return 0;
-
-		if (IS_CONTROL_PORT(this, direction, port_id))
-			param = spa_pod_builder_add_object(&b,
-				SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat,
-				SPA_FORMAT_mediaType,	   SPA_POD_Id(SPA_MEDIA_TYPE_application),
-				SPA_FORMAT_mediaSubtype,   SPA_POD_Id(SPA_MEDIA_SUBTYPE_control));
-		else
-			param = spa_format_audio_raw_build(&b, id, &port->format.info.raw);
-		break;
-
-	case SPA_PARAM_Buffers:
-	{
-		uint32_t buffers, size;
-
-		if (!port->have_format)
-			return -EIO;
-		if (result.index > 0)
-			return 0;
-
-		if (IS_CONTROL_PORT(this, direction, port_id)) {
-			param = spa_pod_builder_add_object(&b,
-				SPA_TYPE_OBJECT_ParamBuffers, id,
-				SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(1, 1, MAX_BUFFERS),
-				SPA_PARAM_BUFFERS_blocks,  SPA_POD_Int(1),
-				SPA_PARAM_BUFFERS_size,    SPA_POD_CHOICE_RANGE_Int(
-								DEFAULT_CONTROL_BUFFER_SIZE,
-								1024,
-								INT32_MAX),
-				SPA_PARAM_BUFFERS_stride,  SPA_POD_Int(1));
-		} else {
-			if (other->n_buffers > 0) {
-				buffers = other->n_buffers;
-				size = other->size / other->stride;
-			} else {
-				buffers = 1;
-				size = this->quantum_limit;
-			}
-
-			param = spa_pod_builder_add_object(&b,
-				SPA_TYPE_OBJECT_ParamBuffers, id,
-				SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(buffers, 1, MAX_BUFFERS),
-				SPA_PARAM_BUFFERS_blocks,  SPA_POD_Int(port->blocks),
-				SPA_PARAM_BUFFERS_size,    SPA_POD_CHOICE_RANGE_Int(
-								size * port->stride,
-								16 * port->stride,
-								INT32_MAX),
-				SPA_PARAM_BUFFERS_stride,  SPA_POD_Int(port->stride));
-		}
-		break;
-	}
-	case SPA_PARAM_Meta:
-		switch (result.index) {
-		case 0:
-			if (IS_CONTROL_PORT(this, direction, port_id))
-				return -EINVAL;
-
-			param = spa_pod_builder_add_object(&b,
-				SPA_TYPE_OBJECT_ParamMeta, id,
-				SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header),
-				SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header)));
-			break;
-		default:
-			return 0;
-		}
-		break;
-
-	case SPA_PARAM_IO:
-		switch (result.index) {
-		case 0:
-			param = spa_pod_builder_add_object(&b,
-				SPA_TYPE_OBJECT_ParamIO, id,
-				SPA_PARAM_IO_id,   SPA_POD_Id(SPA_IO_Buffers),
-				SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers)));
-			break;
-		default:
-			return 0;
-		}
-		break;
-	default:
-		return -ENOENT;
-	}
-
-	if (spa_pod_filter(&b, &result.param, param, filter) < 0)
-		goto next;
-
-	spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
-
-	if (++count != num)
-		goto next;
-
-	return 0;
-}
-
-static int clear_buffers(struct impl *this, struct port *port)
-{
-	if (port->n_buffers > 0) {
-		spa_log_debug(this->log, "%p: clear buffers %p", this, port);
-		port->n_buffers = 0;
-		spa_list_init(&port->queue);
-	}
-	return 0;
-}
-
-static int port_set_format(void *object,
-			   enum spa_direction direction,
-			   uint32_t port_id,
-			   uint32_t flags,
-			   const struct spa_pod *format)
-{
-	struct impl *this = object;
-	struct port *port, *other;
-	int res = 0;
-
-	port = GET_PORT(this, direction, port_id);
-	other = GET_PORT(this, SPA_DIRECTION_REVERSE(direction), port_id);
-
-	if (format == NULL) {
-		if (port->have_format) {
-			port->have_format = false;
-			clear_buffers(this, port);
-			if (this->mix.process)
-				channelmix_free(&this->mix);
-		}
-	} else {
-		struct spa_audio_info info = { 0 };
-
-		if ((res = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0)
-			return res;
-
-		if (IS_CONTROL_PORT(this, direction, port_id)) {
-			if (info.media_type != SPA_MEDIA_TYPE_application ||
-			    info.media_subtype != SPA_MEDIA_SUBTYPE_control)
-				return -EINVAL;
-		} else {
-			if (info.media_type != SPA_MEDIA_TYPE_audio ||
-			    info.media_subtype != SPA_MEDIA_SUBTYPE_raw)
-				return -EINVAL;
-
-			if (spa_format_audio_raw_parse(format, &info.info.raw) < 0)
-				return -EINVAL;
-
-			if (info.info.raw.format != SPA_AUDIO_FORMAT_F32P)
-				return -EINVAL;
-
-			port->stride = sizeof(float);
-			port->blocks = info.info.raw.channels;
-
-			if (other->have_format) {
-				if ((res = setup_convert(this, direction, &info)) < 0)
-					return res;
-			}
-		}
-		port->format = info;
-		port->have_format = true;
-
-		spa_log_debug(this->log, "%p: set format on port %d %d", this, port_id, res);
-	}
-
-	port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
-	if (port->have_format) {
-		port->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE);
-		port->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ);
-	} else {
-		port->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
-		port->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
-	}
-	emit_port_info(this, port, false);
-
-	return res;
-}
-
-static int
-impl_node_port_set_param(void *object,
-			 enum spa_direction direction, uint32_t port_id,
-			 uint32_t id, uint32_t flags,
-			 const struct spa_pod *param)
-{
-	struct impl *this = object;
-
-	spa_return_val_if_fail(this != NULL, -EINVAL);
-
-	spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
-
-	switch (id) {
-	case SPA_PARAM_Format:
-		return port_set_format(object, direction, port_id, flags, param);
-	default:
-		break;
-	}
-
-	return -ENOENT;
-}
-
-static int
-impl_node_port_use_buffers(void *object,
-		enum spa_direction direction, uint32_t port_id,
-		uint32_t flags,
-		struct spa_buffer **buffers, uint32_t n_buffers)
-{
-	struct impl *this = object;
-	struct port *port;
-	uint32_t i, j, size = SPA_ID_INVALID;
-
-	spa_return_val_if_fail(this != NULL, -EINVAL);
-	spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
-
-	port = GET_PORT(this, direction, port_id);
-
-	if (IS_DATA_PORT(this, direction, port_id))
-		spa_return_val_if_fail(port->have_format, -EIO);
-
-	spa_log_debug(this->log, "%p: use buffers %d on port %d", this, n_buffers, port_id);
-
-	clear_buffers(this, port);
-
-	for (i = 0; i < n_buffers; i++) {
-		struct buffer *b;
-		uint32_t n_datas = buffers[i]->n_datas;
-		struct spa_data *d = buffers[i]->datas;
-
-		b = &port->buffers[i];
-		b->id = i;
-		b->flags = 0;
-		b->outbuf = buffers[i];
-		b->h = spa_buffer_find_meta_data(buffers[i], SPA_META_Header, sizeof(*b->h));
-
-		for (j = 0; j < n_datas; j++) {
-			if (size == SPA_ID_INVALID)
-				size = d[j].maxsize;
-			else if (size != d[j].maxsize)
-				return -EINVAL;
-
-			if (d[j].data == NULL) {
-				spa_log_error(this->log, "%p: invalid memory on buffer %p", this,
-					      buffers[i]);
-				return -EINVAL;
-			}
-			if (!SPA_IS_ALIGNED(d[j].data, this->max_align)) {
-				spa_log_warn(this->log, "%p: memory %d on buffer %d not aligned",
-						this, j, i);
-			}
-			b->datas[j] = d[j].data;
-			if (direction == SPA_DIRECTION_OUTPUT &&
-			    !SPA_FLAG_IS_SET(d[j].flags, SPA_DATA_FLAG_DYNAMIC))
-				this->is_passthrough = false;
-		}
-		if (direction == SPA_DIRECTION_OUTPUT)
-			spa_list_append(&port->queue, &b->link);
-		else
-			SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUT);
-	}
-	port->n_buffers = n_buffers;
-	port->size = size;
-
-	return 0;
-}
-
-static int
-impl_node_port_set_io(void *object,
-		      enum spa_direction direction, uint32_t port_id,
-		      uint32_t id, void *data, size_t size)
-{
-	struct impl *this = object;
-	struct port *port;
-
-	spa_return_val_if_fail(this != NULL, -EINVAL);
-	spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
-
-	port = GET_PORT(this, direction, port_id);
-
-	switch (id) {
-	case SPA_IO_Buffers:
-		port->io = data;
-		break;
-	default:
-		return -ENOENT;
-	}
-	return 0;
-}
-
-static void recycle_buffer(struct impl *this, uint32_t id)
-{
-	struct port *port = GET_OUT_PORT(this, 0);
-	struct buffer *b = &port->buffers[id];
-
-	if (SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_OUT)) {
-		spa_list_append(&port->queue, &b->link);
-		SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_OUT);
-		spa_log_trace_fp(this->log, "%p: recycle buffer %d", this, id);
-	}
-}
-
-static struct buffer *dequeue_buffer(struct impl *this, struct port *port)
-{
-	struct buffer *b;
-
-	if (spa_list_is_empty(&port->queue))
-		return NULL;
-
-	b = spa_list_first(&port->queue, struct buffer, link);
-	spa_list_remove(&b->link);
-	SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUT);
-
-	return b;
-}
-
-
-static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id)
-{
-	struct impl *this = object;
-
-	spa_return_val_if_fail(this != NULL, -EINVAL);
-	spa_return_val_if_fail(CHECK_PORT(this, SPA_DIRECTION_OUTPUT, port_id), -EINVAL);
-
-	recycle_buffer(this, buffer_id);
-
-	return 0;
-}
-
-static int channelmix_process_control(struct impl *this, struct port *ctrlport,
-				      void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-				      uint32_t n_samples)
-{
-	struct spa_pod_control *c, *prev = NULL;
-	uint32_t avail_samples = n_samples;
-	uint32_t i;
-	const float **s = (const float **)src;
-	float **d = (float **)dst;
-
-	SPA_POD_SEQUENCE_FOREACH(ctrlport->ctrl, c) {
-		uint32_t chunk;
-
-		if (avail_samples == 0)
-			return 0;
-
-		/* ignore old control offsets */
-		if (c->offset <= ctrlport->ctrl_offset) {
-			prev = c;
-			continue;
-		}
-
-		switch (c->type) {
-		case SPA_CONTROL_Midi:
-		{
-			if (prev)
-				apply_midi(this, &prev->value);
-			break;
-		}
-		case SPA_CONTROL_Properties:
-		{
-			if (prev)
-				apply_props(this, &prev->value);
-			break;
-		}
-		default:
-			continue;
-		}
-
-		chunk = SPA_MIN(avail_samples, c->offset - ctrlport->ctrl_offset);
-
-		spa_log_trace_fp(this->log, "%p: process %d %d", this,
-				c->offset, chunk);
-
-		channelmix_process(&this->mix, dst, src, chunk);
-		for (i = 0; i < this->mix.src_chan; i++)
-			s[i] += chunk;
-		for (i = 0; i < this->mix.dst_chan; i++)
-			d[i] += chunk;
-
-		avail_samples -= chunk;
-		ctrlport->ctrl_offset += chunk;
-
-		prev = c;
-	}
-
-	/* when we get here we run out of control points but still have some
-	 * remaining samples */
-	spa_log_trace_fp(this->log, "%p: remain %d", this, avail_samples);
-	if (avail_samples > 0)
-		channelmix_process(&this->mix, dst, src, avail_samples);
-
-	return 1;
-}
-
-static int impl_node_process(void *object)
-{
-	struct impl *this = object;
-	struct port *outport, *inport, *ctrlport;
-	struct spa_io_buffers *outio, *inio, *ctrlio;
-	struct buffer *sbuf, *dbuf;
-	struct spa_pod_sequence *ctrl = NULL;
-
-	spa_return_val_if_fail(this != NULL, -EINVAL);
-
-	outport = GET_OUT_PORT(this, 0);
-	inport = GET_IN_PORT(this, 0);
-	ctrlport = GET_CONTROL_PORT(this, 1);
-
-	outio = outport->io;
-	inio = inport->io;
-	ctrlio = ctrlport->io;
-
-	spa_return_val_if_fail(outio != NULL, -EIO);
-	spa_return_val_if_fail(inio != NULL, -EIO);
-
-	spa_log_trace_fp(this->log, "%p: status %p %d %d -> %p %d %d", this,
-			inio, inio->status, inio->buffer_id,
-			outio, outio->status, outio->buffer_id);
-
-	if (SPA_UNLIKELY(outio->status == SPA_STATUS_HAVE_DATA))
-		return SPA_STATUS_HAVE_DATA;
-	/* recycle */
-	if (SPA_LIKELY(outio->buffer_id < outport->n_buffers)) {
-		recycle_buffer(this, outio->buffer_id);
-		outio->buffer_id = SPA_ID_INVALID;
-	}
-	if (SPA_UNLIKELY(inio->status != SPA_STATUS_HAVE_DATA))
-		return outio->status = inio->status;
-
-	if (SPA_UNLIKELY(inio->buffer_id >= inport->n_buffers))
-		return inio->status = -EINVAL;
-
-	if (SPA_UNLIKELY((dbuf = dequeue_buffer(this, outport)) == NULL))
-		return outio->status = -EPIPE;
-
-	sbuf = &inport->buffers[inio->buffer_id];
-
-	if (ctrlio != NULL &&
-	    ctrlio->status == SPA_STATUS_HAVE_DATA &&
-	    ctrlio->buffer_id < ctrlport->n_buffers) {
-		struct buffer *cbuf = &ctrlport->buffers[ctrlio->buffer_id];
-		struct spa_data *d = &cbuf->outbuf->datas[0];
-
-		ctrl = spa_pod_from_data(d->data, d->maxsize, d->chunk->offset, d->chunk->size);
-		if (ctrl && !spa_pod_is_sequence(&ctrl->pod))
-			ctrl = NULL;
-		if (ctrl != ctrlport->ctrl) {
-			ctrlport->ctrl = ctrl;
-			ctrlport->ctrl_offset = 0;
-		}
-	}
-
-	{
-		uint32_t i, n_samples;
-		struct spa_buffer *sb = sbuf->outbuf, *db = dbuf->outbuf;
-		uint32_t n_src_datas = sb->n_datas;
-		uint32_t n_dst_datas = db->n_datas;
-		const void *src_datas[n_src_datas];
-		void *dst_datas[n_dst_datas];
-		bool is_passthrough;
-
-		is_passthrough = this->is_passthrough &&
-			SPA_FLAG_IS_SET(this->mix.flags, CHANNELMIX_FLAG_IDENTITY) &&
-			ctrlport->ctrl == NULL;
-
-		n_samples = sb->datas[0].chunk->size / inport->stride;
-
-		for (i = 0; i < n_src_datas; i++)
-			src_datas[i] = sb->datas[i].data;
-		for (i = 0; i < n_dst_datas; i++) {
-			dst_datas[i] = is_passthrough ? (void*)src_datas[i] : dbuf->datas[i];
-			db->datas[i].data = dst_datas[i];
-			db->datas[i].chunk->size = n_samples * outport->stride;
-		}
-
-		spa_log_trace_fp(this->log, "%p: n_src:%d n_dst:%d n_samples:%d p:%d",
-				this, n_src_datas, n_dst_datas, n_samples, is_passthrough);
-
-		if (!is_passthrough) {
-			if (ctrlport->ctrl != NULL) {
-				/* if return value is 1, the sequence has been processed */
-				if (channelmix_process_control(this, ctrlport, dst_datas,
-						src_datas, n_samples) == 1) {
-					ctrlio->status = SPA_STATUS_OK;
-					ctrlport->ctrl = NULL;
-				}
-			} else {
-				channelmix_process(&this->mix, dst_datas,
-						src_datas, n_samples);
-			}
-		}
-	}
-
-	outio->status = SPA_STATUS_HAVE_DATA;
-	outio->buffer_id = dbuf->id;
-
-	inio->status = SPA_STATUS_NEED_DATA;
-
-	return SPA_STATUS_HAVE_DATA | SPA_STATUS_NEED_DATA;
-}
-
-static const struct spa_node_methods impl_node = {
-	SPA_VERSION_NODE_METHODS,
-	.add_listener = impl_node_add_listener,
-	.set_callbacks = impl_node_set_callbacks,
-	.enum_params = impl_node_enum_params,
-	.set_param = impl_node_set_param,
-	.set_io = impl_node_set_io,
-	.send_command = impl_node_send_command,
-	.add_port = impl_node_add_port,
-	.remove_port = impl_node_remove_port,
-	.port_enum_params = impl_node_port_enum_params,
-	.port_set_param = impl_node_port_set_param,
-	.port_use_buffers = impl_node_port_use_buffers,
-	.port_set_io = impl_node_port_set_io,
-	.port_reuse_buffer = impl_node_port_reuse_buffer,
-	.process = impl_node_process,
-};
-
-static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface)
-{
-	struct impl *this;
-
-	spa_return_val_if_fail(handle != NULL, -EINVAL);
-	spa_return_val_if_fail(interface != NULL, -EINVAL);
-
-	this = (struct impl *) handle;
-
-	if (spa_streq(type, SPA_TYPE_INTERFACE_Node))
-		*interface = &this->node;
-	else
-		return -ENOENT;
-
-	return 0;
-}
-
-static int impl_clear(struct spa_handle *handle)
-{
-	return 0;
-}
-
-static size_t
-impl_get_size(const struct spa_handle_factory *factory,
-	      const struct spa_dict *params)
-{
-	return sizeof(struct impl);
-}
-
-static uint32_t channel_from_name(const char *name)
-{
-	int i;
-	for (i = 0; spa_type_audio_channel[i].name; i++) {
-		if (spa_streq(name, spa_debug_type_short_name(spa_type_audio_channel[i].name)))
-			return spa_type_audio_channel[i].type;
-	}
-	return SPA_AUDIO_CHANNEL_UNKNOWN;
-}
-
-static inline uint32_t parse_position(uint32_t *pos, const char *val, size_t len)
-{
-	struct spa_json it[2];
-	char v[256];
-	uint32_t i = 0;
-
-	spa_json_init(&it[0], val, len);
-        if (spa_json_enter_array(&it[0], &it[1]) <= 0)
-                spa_json_init(&it[1], val, len);
-
-	while (spa_json_get_string(&it[1], v, sizeof(v)) > 0 &&
-	    i < SPA_AUDIO_MAX_CHANNELS) {
-		pos[i++] = channel_from_name(v);
-	}
-	return i;
-}
-
-static int
-impl_init(const struct spa_handle_factory *factory,
-	  struct spa_handle *handle,
-	  const struct spa_dict *info,
-	  const struct spa_support *support,
-	  uint32_t n_support)
-{
-	struct impl *this;
-	struct port *port;
-	uint32_t i;
-
-	spa_return_val_if_fail(factory != NULL, -EINVAL);
-	spa_return_val_if_fail(handle != NULL, -EINVAL);
-
-	handle->get_interface = impl_get_interface;
-	handle->clear = impl_clear;
-
-	this = (struct impl *) handle;
-
-	this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
-	spa_log_topic_init(this->log, log_topic);
-
-	this->cpu = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_CPU);
-	if (this->cpu) {
-		this->cpu_flags = spa_cpu_get_flags(this->cpu);
-		this->max_align = SPA_MIN(MAX_ALIGN, spa_cpu_get_max_align(this->cpu));
-	}
-
-	spa_hook_list_init(&this->hooks);
-
-	props_reset(&this->props);
-
-	this->mix.options = CHANNELMIX_OPTION_UPMIX;
-	this->mix.upmix = CHANNELMIX_UPMIX_PSD;
-	this->mix.log = this->log;
-	this->mix.lfe_cutoff = 150.0f;
-	this->mix.fc_cutoff = 12000.0f;
-	this->mix.rear_delay = 12.0f;
-	this->mix.widen = 0.0f;
-
-	for (i = 0; info && i < info->n_items; i++) {
-		const char *k = info->items[i].key;
-		const char *s = info->items[i].value;
-		if (spa_streq(k, SPA_KEY_AUDIO_POSITION))
-			this->props.n_channels = parse_position(this->props.channel_map, s, strlen(s));
-		else if (spa_streq(k, "clock.quantum-limit"))
-			spa_atou32(s, &this->quantum_limit, 0);
-		else if (spa_streq(k, "factory.mode")) {
-			if (spa_streq(s, "merge"))
-				this->direction = SPA_DIRECTION_OUTPUT;
-			else
-				this->direction = SPA_DIRECTION_INPUT;
-		}
-		else
-			channelmix_set_param(this, k, s);
-
-	}
-	this->props.channel.n_volumes = this->props.n_channels;
-	this->props.soft.n_volumes = this->props.n_channels;
-	this->props.monitor.n_volumes = this->props.n_channels;
-
-	this->node.iface = SPA_INTERFACE_INIT(
-			SPA_TYPE_INTERFACE_Node,
-			SPA_VERSION_NODE,
-			&impl_node, this);
-	this->info_all = SPA_NODE_CHANGE_MASK_FLAGS |
-			SPA_NODE_CHANGE_MASK_PARAMS;
-	this->info = SPA_NODE_INFO_INIT();
-	this->info.flags = SPA_NODE_FLAG_RT;
-	this->info.max_input_ports = 2;
-	this->info.max_output_ports = 1;
-	this->params[IDX_PropInfo] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ);
-	this->params[IDX_Props] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE);
-	this->info.params = this->params;
-	this->info.n_params = 2;
-
-	port = GET_OUT_PORT(this, 0);
-	port->direction = SPA_DIRECTION_OUTPUT;
-	port->id = 0;
-	port->info_all = SPA_PORT_CHANGE_MASK_FLAGS |
-			SPA_PORT_CHANGE_MASK_PARAMS;
-	port->info = SPA_PORT_INFO_INIT();
-	port->info.flags = SPA_PORT_FLAG_DYNAMIC_DATA;
-	port->params[IDX_EnumFormat] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ);
-	port->params[IDX_Meta] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ);
-	port->params[IDX_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
-	port->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
-	port->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
-	port->info.params = port->params;
-	port->info.n_params = 5;
-	spa_list_init(&port->queue);
-
-	port = GET_IN_PORT(this, 0);
-	port->direction = SPA_DIRECTION_INPUT;
-	port->id = 0;
-	port->info_all = SPA_PORT_CHANGE_MASK_FLAGS |
-			SPA_PORT_CHANGE_MASK_PARAMS;
-	port->info = SPA_PORT_INFO_INIT();
-	port->info.flags = SPA_PORT_FLAG_NO_REF |
-		SPA_PORT_FLAG_DYNAMIC_DATA;
-	port->params[IDX_EnumFormat] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ);
-	port->params[IDX_Meta] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ);
-	port->params[IDX_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
-	port->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
-	port->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
-	port->info.params = port->params;
-	port->info.n_params = 0;
-	spa_list_init(&port->queue);
-
-	port = GET_CONTROL_PORT(this, 1);
-	port->direction = SPA_DIRECTION_INPUT;
-	port->id = 1;
-	port->info_all = SPA_PORT_CHANGE_MASK_FLAGS |
-			SPA_PORT_CHANGE_MASK_PROPS |
-			SPA_PORT_CHANGE_MASK_PARAMS;
-	port->info = SPA_PORT_INFO_INIT();
-	port->info.flags = SPA_PORT_FLAG_NO_REF |
-		SPA_PORT_FLAG_DYNAMIC_DATA;
-	port->params[IDX_EnumFormat] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ);
-	port->params[IDX_Meta] = SPA_PARAM_INFO(SPA_PARAM_Meta, 0);
-	port->params[IDX_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
-	port->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
-	port->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
-	port->info.params = port->params;
-	port->info.n_params = 4;
-	spa_list_init(&port->queue);
-
-	return 0;
-}
-
-static const struct spa_interface_info impl_interfaces[] = {
-	{SPA_TYPE_INTERFACE_Node,},
-};
-
-static int
-impl_enum_interface_info(const struct spa_handle_factory *factory,
-			 const struct spa_interface_info **info,
-			 uint32_t *index)
-{
-	spa_return_val_if_fail(factory != NULL, -EINVAL);
-	spa_return_val_if_fail(info != NULL, -EINVAL);
-	spa_return_val_if_fail(index != NULL, -EINVAL);
-
-	switch (*index) {
-	case 0:
-		*info = &impl_interfaces[*index];
-		break;
-	default:
-		return 0;
-	}
-	(*index)++;
-	return 1;
-}
-
-const struct spa_handle_factory spa_channelmix_factory = {
-	SPA_VERSION_HANDLE_FACTORY,
-	SPA_NAME_AUDIO_PROCESS_CHANNELMIX,
-	NULL,
-	impl_get_size,
-	impl_init,
-	impl_enum_interface_info,
-};
diff --git a/spa/plugins/audioconvert/crossover.c b/spa/plugins/audioconvert/crossover.c
index 1ce1d221d8161611cb74b5d779baaec5930138b5..7575833cdbe5e96759144865ce92b3224008706c 100644
--- a/spa/plugins/audioconvert/crossover.c
+++ b/spa/plugins/audioconvert/crossover.c
@@ -3,8 +3,6 @@
  * found in the LICENSE file.
  */
 
-#include "config.h"
-
 #include <float.h>
 #include <string.h>
 
diff --git a/spa/plugins/audioconvert/fmt-ops-avx2.c b/spa/plugins/audioconvert/fmt-ops-avx2.c
index 065fa997ebc938716f137a47b105a127a577e5a5..723aea369bc67f5959c7e5b99fe30707d902464e 100644
--- a/spa/plugins/audioconvert/fmt-ops-avx2.c
+++ b/spa/plugins/audioconvert/fmt-ops-avx2.c
@@ -34,6 +34,15 @@
 #  define _mm256_setr_m128i(v0, v1) _mm256_set_m128i((v1), (v0))
 #endif
 
+#define _MM_CLAMP_PS(r,min,max)				\
+	_mm_min_ps(_mm_max_ps(r, min), max)
+
+#define _MM256_CLAMP_PS(r,min,max)			\
+	_mm256_min_ps(_mm256_max_ps(r, min), max)
+
+#define _MM_CLAMP_SS(r,min,max)				\
+	_mm_min_ss(_mm_max_ss(r, min), max)
+
 static void
 conv_s16_to_f32d_1s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src,
 		uint32_t n_channels, uint32_t n_samples)
@@ -41,7 +50,7 @@ conv_s16_to_f32d_1s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA
 	const int16_t *s = src;
 	float *d0 = dst[0];
 	uint32_t n, unrolled;
-	__m256i in;
+	__m256i in = _mm256_setzero_si256();
 	__m256 out, factor = _mm256_set1_ps(1.0f / S16_SCALE);
 
 	if (SPA_LIKELY(SPA_IS_ALIGNED(d0, 32)))
@@ -67,7 +76,7 @@ conv_s16_to_f32d_1s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA
 	}
 	for(; n < n_samples; n++) {
 		__m128 out, factor = _mm_set1_ps(1.0f / S16_SCALE);
-		out = _mm_cvtsi32_ss(out, s[0]);
+		out = _mm_cvtsi32_ss(factor, s[0]);
 		out = _mm_mul_ss(out, factor);
 		_mm_store_ss(&d0[n], out);
 		s += n_channels;
@@ -133,9 +142,9 @@ conv_s16_to_f32d_2_avx2(struct convert *conv, void * SPA_RESTRICT dst[], const v
 	}
 	for(; n < n_samples; n++) {
 		__m128 out[4], factor = _mm_set1_ps(1.0f / S16_SCALE);
-		out[0] = _mm_cvtsi32_ss(out[0], s[0]);
+		out[0] = _mm_cvtsi32_ss(factor, s[0]);
 		out[0] = _mm_mul_ss(out[0], factor);
-		out[1] = _mm_cvtsi32_ss(out[1], s[1]);
+		out[1] = _mm_cvtsi32_ss(factor, s[1]);
 		out[1] = _mm_mul_ss(out[1], factor);
 		_mm_store_ss(&d0[n], out[0]);
 		_mm_store_ss(&d1[n], out[1]);
@@ -147,7 +156,7 @@ void
 conv_s24_to_f32d_1s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src,
 		uint32_t n_channels, uint32_t n_samples)
 {
-	const uint8_t *s = src;
+	const int24_t *s = src;
 	float *d0 = dst[0];
 	uint32_t n, unrolled;
 	__m128i in;
@@ -164,21 +173,21 @@ conv_s24_to_f32d_1s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA
 	for(n = 0; n < unrolled; n += 4) {
 		in = _mm_setr_epi32(
 			*((uint32_t*)&s[0 * n_channels]),
-			*((uint32_t*)&s[3 * n_channels]),
-			*((uint32_t*)&s[6 * n_channels]),
-			*((uint32_t*)&s[9 * n_channels]));
+			*((uint32_t*)&s[1 * n_channels]),
+			*((uint32_t*)&s[2 * n_channels]),
+			*((uint32_t*)&s[3 * n_channels]));
 		in = _mm_slli_epi32(in, 8);
 		in = _mm_srai_epi32(in, 8);
 		out = _mm_cvtepi32_ps(in);
 		out = _mm_mul_ps(out, factor);
 		_mm_store_ps(&d0[n], out);
-		s += 12 * n_channels;
+		s += 4 * n_channels;
 	}
 	for(; n < n_samples; n++) {
-		out = _mm_cvtsi32_ss(out, read_s24(s));
+		out = _mm_cvtsi32_ss(factor, s24_to_s32(*s));
 		out = _mm_mul_ss(out, factor);
 		_mm_store_ss(&d0[n], out);
-		s += 3 * n_channels;
+		s += n_channels;
 	}
 }
 
@@ -186,7 +195,7 @@ static void
 conv_s24_to_f32d_2s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src,
 		uint32_t n_channels, uint32_t n_samples)
 {
-	const uint8_t *s = src;
+	const int24_t *s = src;
 	float *d0 = dst[0], *d1 = dst[1];
 	uint32_t n, unrolled;
 	__m128i in[2];
@@ -205,14 +214,14 @@ conv_s24_to_f32d_2s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA
 	for(n = 0; n < unrolled; n += 4) {
 		in[0] = _mm_setr_epi32(
 			*((uint32_t*)&s[0 + 0*n_channels]),
-			*((uint32_t*)&s[0 + 3*n_channels]),
-			*((uint32_t*)&s[0 + 6*n_channels]),
-			*((uint32_t*)&s[0 + 9*n_channels]));
+			*((uint32_t*)&s[0 + 1*n_channels]),
+			*((uint32_t*)&s[0 + 2*n_channels]),
+			*((uint32_t*)&s[0 + 3*n_channels]));
 		in[1] = _mm_setr_epi32(
-			*((uint32_t*)&s[3 + 0*n_channels]),
-			*((uint32_t*)&s[3 + 3*n_channels]),
-			*((uint32_t*)&s[3 + 6*n_channels]),
-			*((uint32_t*)&s[3 + 9*n_channels]));
+			*((uint32_t*)&s[1 + 0*n_channels]),
+			*((uint32_t*)&s[1 + 1*n_channels]),
+			*((uint32_t*)&s[1 + 2*n_channels]),
+			*((uint32_t*)&s[1 + 3*n_channels]));
 
 		in[0] = _mm_slli_epi32(in[0], 8);
 		in[1] = _mm_slli_epi32(in[1], 8);
@@ -229,23 +238,23 @@ conv_s24_to_f32d_2s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA
 		_mm_store_ps(&d0[n], out[0]);
 		_mm_store_ps(&d1[n], out[1]);
 
-		s += 12 * n_channels;
+		s += 4 * n_channels;
 	}
 	for(; n < n_samples; n++) {
-		out[0] = _mm_cvtsi32_ss(out[0], read_s24(s));
-		out[1] = _mm_cvtsi32_ss(out[1], read_s24(s+3));
+		out[0] = _mm_cvtsi32_ss(factor, s24_to_s32(*s));
+		out[1] = _mm_cvtsi32_ss(factor, s24_to_s32(*(s+1)));
 		out[0] = _mm_mul_ss(out[0], factor);
 		out[1] = _mm_mul_ss(out[1], factor);
 		_mm_store_ss(&d0[n], out[0]);
 		_mm_store_ss(&d1[n], out[1]);
-		s += 3 * n_channels;
+		s += n_channels;
 	}
 }
 static void
 conv_s24_to_f32d_4s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src,
 		uint32_t n_channels, uint32_t n_samples)
 {
-	const uint8_t *s = src;
+	const int24_t *s = src;
 	float *d0 = dst[0], *d1 = dst[1], *d2 = dst[2], *d3 = dst[3];
 	uint32_t n, unrolled;
 	__m128i in[4];
@@ -266,24 +275,24 @@ conv_s24_to_f32d_4s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA
 	for(n = 0; n < unrolled; n += 4) {
 		in[0] = _mm_setr_epi32(
 			*((uint32_t*)&s[0 + 0*n_channels]),
-			*((uint32_t*)&s[0 + 3*n_channels]),
-			*((uint32_t*)&s[0 + 6*n_channels]),
-			*((uint32_t*)&s[0 + 9*n_channels]));
+			*((uint32_t*)&s[0 + 1*n_channels]),
+			*((uint32_t*)&s[0 + 2*n_channels]),
+			*((uint32_t*)&s[0 + 3*n_channels]));
 		in[1] = _mm_setr_epi32(
-			*((uint32_t*)&s[3 + 0*n_channels]),
-			*((uint32_t*)&s[3 + 3*n_channels]),
-			*((uint32_t*)&s[3 + 6*n_channels]),
-			*((uint32_t*)&s[3 + 9*n_channels]));
+			*((uint32_t*)&s[1 + 0*n_channels]),
+			*((uint32_t*)&s[1 + 1*n_channels]),
+			*((uint32_t*)&s[1 + 2*n_channels]),
+			*((uint32_t*)&s[1 + 3*n_channels]));
 		in[2] = _mm_setr_epi32(
-			*((uint32_t*)&s[6 + 0*n_channels]),
-			*((uint32_t*)&s[6 + 3*n_channels]),
-			*((uint32_t*)&s[6 + 6*n_channels]),
-			*((uint32_t*)&s[6 + 9*n_channels]));
+			*((uint32_t*)&s[2 + 0*n_channels]),
+			*((uint32_t*)&s[2 + 1*n_channels]),
+			*((uint32_t*)&s[2 + 2*n_channels]),
+			*((uint32_t*)&s[2 + 3*n_channels]));
 		in[3] = _mm_setr_epi32(
-			*((uint32_t*)&s[9 + 0*n_channels]),
-			*((uint32_t*)&s[9 + 3*n_channels]),
-			*((uint32_t*)&s[9 + 6*n_channels]),
-			*((uint32_t*)&s[9 + 9*n_channels]));
+			*((uint32_t*)&s[3 + 0*n_channels]),
+			*((uint32_t*)&s[3 + 1*n_channels]),
+			*((uint32_t*)&s[3 + 2*n_channels]),
+			*((uint32_t*)&s[3 + 3*n_channels]));
 
 		in[0] = _mm_slli_epi32(in[0], 8);
 		in[1] = _mm_slli_epi32(in[1], 8);
@@ -310,13 +319,13 @@ conv_s24_to_f32d_4s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA
 		_mm_store_ps(&d2[n], out[2]);
 		_mm_store_ps(&d3[n], out[3]);
 
-		s += 12 * n_channels;
+		s += 4 * n_channels;
 	}
 	for(; n < n_samples; n++) {
-		out[0] = _mm_cvtsi32_ss(out[0], read_s24(s));
-		out[1] = _mm_cvtsi32_ss(out[1], read_s24(s+3));
-		out[2] = _mm_cvtsi32_ss(out[2], read_s24(s+6));
-		out[3] = _mm_cvtsi32_ss(out[3], read_s24(s+9));
+		out[0] = _mm_cvtsi32_ss(factor, s24_to_s32(*s));
+		out[1] = _mm_cvtsi32_ss(factor, s24_to_s32(*(s+1)));
+		out[2] = _mm_cvtsi32_ss(factor, s24_to_s32(*(s+2)));
+		out[3] = _mm_cvtsi32_ss(factor, s24_to_s32(*(s+3)));
 		out[0] = _mm_mul_ss(out[0], factor);
 		out[1] = _mm_mul_ss(out[1], factor);
 		out[2] = _mm_mul_ss(out[2], factor);
@@ -325,7 +334,7 @@ conv_s24_to_f32d_4s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA
 		_mm_store_ss(&d1[n], out[1]);
 		_mm_store_ss(&d2[n], out[2]);
 		_mm_store_ss(&d3[n], out[3]);
-		s += 3 * n_channels;
+		s += n_channels;
 	}
 }
 
@@ -373,11 +382,6 @@ conv_s32_to_f32d_4s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA
 		in[2] = _mm256_i64gather_epi64((long long int *)&s[0*n_channels], mask3, 4);
 		in[3] = _mm256_i64gather_epi64((long long int *)&s[0*n_channels], mask4, 4);
 
-		in[0] = _mm256_srai_epi32(in[0], 8); /* a0 b0 c0 d0 a4 b4 c4 d4 */
-		in[1] = _mm256_srai_epi32(in[1], 8); /* a1 b1 c1 d1 a5 b5 c5 d5 */
-		in[2] = _mm256_srai_epi32(in[2], 8); /* a2 b2 c2 d2 a6 b6 c6 d6 */
-		in[3] = _mm256_srai_epi32(in[3], 8); /* a3 b3 c3 d3 a7 b7 c7 d7 */
-
 		t[0] = _mm256_unpacklo_epi32(in[0], in[1]);   /* a0 a1 b0 b1 a4 a5 b4 b5 */
 		t[1] = _mm256_unpackhi_epi32(in[0], in[1]);   /* c0 c1 d0 d1 c4 c5 d4 d5 */
 		t[2] = _mm256_unpacklo_epi32(in[2], in[3]);   /* a2 a3 b2 b3 a6 a7 b6 b7 */
@@ -387,6 +391,11 @@ conv_s32_to_f32d_4s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA
 		in[2] = _mm256_unpacklo_epi64(t[1], t[3]);     /* c0 c1 c2 c3 c4 c5 c6 c7 */
 		in[3] = _mm256_unpackhi_epi64(t[1], t[3]);     /* d0 d1 d2 d3 d4 d5 d6 d7 */
 
+		in[0] = _mm256_srai_epi32(in[0], 8);
+		in[1] = _mm256_srai_epi32(in[1], 8);
+		in[2] = _mm256_srai_epi32(in[2], 8);
+		in[3] = _mm256_srai_epi32(in[3], 8);
+
 		out[0] = _mm256_cvtepi32_ps(in[0]);
 		out[1] = _mm256_cvtepi32_ps(in[1]);
 		out[2] = _mm256_cvtepi32_ps(in[2]);
@@ -406,10 +415,10 @@ conv_s32_to_f32d_4s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA
 	}
 	for(; n < n_samples; n++) {
 		__m128 out[4], factor = _mm_set1_ps(1.0f / S24_SCALE);
-		out[0] = _mm_cvtsi32_ss(out[0], s[0]>>8);
-		out[1] = _mm_cvtsi32_ss(out[1], s[1]>>8);
-		out[2] = _mm_cvtsi32_ss(out[2], s[2]>>8);
-		out[3] = _mm_cvtsi32_ss(out[3], s[3]>>8);
+		out[0] = _mm_cvtsi32_ss(factor, s[0] >> 8);
+		out[1] = _mm_cvtsi32_ss(factor, s[1] >> 8);
+		out[2] = _mm_cvtsi32_ss(factor, s[2] >> 8);
+		out[3] = _mm_cvtsi32_ss(factor, s[3] >> 8);
 		out[0] = _mm_mul_ss(out[0], factor);
 		out[1] = _mm_mul_ss(out[1], factor);
 		out[2] = _mm_mul_ss(out[2], factor);
@@ -445,15 +454,15 @@ conv_s32_to_f32d_2s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA
 		in[0] = _mm256_i64gather_epi64((long long int *)s, mask1, 4);
 		in[1] = _mm256_i64gather_epi64((long long int *)s, mask2, 4);
 
-		in[0] = _mm256_srai_epi32(in[0], 8);
-		in[1] = _mm256_srai_epi32(in[1], 8);
-
 		t[0] = _mm256_permutevar8x32_epi32(in[0], perm);
 		t[1] = _mm256_permutevar8x32_epi32(in[1], perm);
 
 		in[0] = _mm256_permute2x128_si256(t[0], t[1], 0 | (2 << 4));
 		in[1] = _mm256_permute2x128_si256(t[0], t[1], 1 | (3 << 4));
 
+		in[0] = _mm256_srai_epi32(in[0], 8);
+		in[1] = _mm256_srai_epi32(in[1], 8);
+
 		out[0] = _mm256_cvtepi32_ps(in[0]);
 		out[1] = _mm256_cvtepi32_ps(in[1]);
 
@@ -467,8 +476,8 @@ conv_s32_to_f32d_2s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA
 	}
 	for(; n < n_samples; n++) {
 		__m128 out[2], factor = _mm_set1_ps(1.0f / S24_SCALE);
-		out[0] = _mm_cvtsi32_ss(out[0], s[0]>>8);
-		out[1] = _mm_cvtsi32_ss(out[1], s[1]>>8);
+		out[0] = _mm_cvtsi32_ss(factor, s[0] >> 8);
+		out[1] = _mm_cvtsi32_ss(factor, s[1] >> 8);
 		out[0] = _mm_mul_ss(out[0], factor);
 		out[1] = _mm_mul_ss(out[1], factor);
 		_mm_store_ss(&d0[n], out[0]);
@@ -518,7 +527,7 @@ conv_s32_to_f32d_1s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA
 	}
 	for(; n < n_samples; n++) {
 		__m128 out, factor = _mm_set1_ps(1.0f / S24_SCALE);
-		out = _mm_cvtsi32_ss(out, s[0]>>8);
+		out = _mm_cvtsi32_ss(factor, s[0] >> 8);
 		out = _mm_mul_ss(out, factor);
 		_mm_store_ss(&d0[n], out);
 		s += n_channels;
@@ -549,8 +558,9 @@ conv_f32d_to_s32_1s_avx2(void *data, void * SPA_RESTRICT dst, const void * SPA_R
 	uint32_t n, unrolled;
 	__m128 in[1];
 	__m128i out[4];
-	__m128 scale = _mm_set1_ps(S32_SCALE);
-	__m128 int_min = _mm_set1_ps(S32_MIN);
+	__m128 scale = _mm_set1_ps(S24_SCALE);
+	__m128 int_max = _mm_set1_ps(S24_MAX);
+	__m128 int_min = _mm_set1_ps(S24_MIN);
 
 	if (SPA_IS_ALIGNED(s0, 16))
 		unrolled = n_samples & ~3;
@@ -559,8 +569,9 @@ conv_f32d_to_s32_1s_avx2(void *data, void * SPA_RESTRICT dst, const void * SPA_R
 
 	for(n = 0; n < unrolled; n += 4) {
 		in[0] = _mm_mul_ps(_mm_load_ps(&s0[n]), scale);
-		in[0] = _mm_min_ps(in[0], int_min);
+		in[0] = _MM_CLAMP_PS(in[0], int_min, int_max);
 		out[0] = _mm_cvtps_epi32(in[0]);
+		out[0] = _mm_slli_epi32(out[0], 8);
 		out[1] = _mm_shuffle_epi32(out[0], _MM_SHUFFLE(0, 3, 2, 1));
 		out[2] = _mm_shuffle_epi32(out[0], _MM_SHUFFLE(1, 0, 3, 2));
 		out[3] = _mm_shuffle_epi32(out[0], _MM_SHUFFLE(2, 1, 0, 3));
@@ -574,8 +585,8 @@ conv_f32d_to_s32_1s_avx2(void *data, void * SPA_RESTRICT dst, const void * SPA_R
 	for(; n < n_samples; n++) {
 		in[0] = _mm_load_ss(&s0[n]);
 		in[0] = _mm_mul_ss(in[0], scale);
-		in[0] = _mm_min_ss(in[0], int_min);
-		*d = _mm_cvtss_si32(in[0]);
+		in[0] = _MM_CLAMP_SS(in[0], int_min, int_max);
+		*d = _mm_cvtss_si32(in[0]) << 8;
 		d += n_channels;
 	}
 }
@@ -589,8 +600,9 @@ conv_f32d_to_s32_2s_avx2(void *data, void * SPA_RESTRICT dst, const void * SPA_R
 	uint32_t n, unrolled;
 	__m256 in[2];
 	__m256i out[2], t[2];
-	__m256 scale = _mm256_set1_ps(S32_SCALE);
-	__m256 int_min = _mm256_set1_ps(S32_MIN);
+	__m256 scale = _mm256_set1_ps(S24_SCALE);
+	__m256 int_min = _mm256_set1_ps(S24_MIN);
+	__m256 int_max = _mm256_set1_ps(S24_MAX);
 
 	if (SPA_IS_ALIGNED(s0, 32) &&
 	    SPA_IS_ALIGNED(s1, 32))
@@ -602,11 +614,13 @@ conv_f32d_to_s32_2s_avx2(void *data, void * SPA_RESTRICT dst, const void * SPA_R
 		in[0] = _mm256_mul_ps(_mm256_load_ps(&s0[n]), scale);
 		in[1] = _mm256_mul_ps(_mm256_load_ps(&s1[n]), scale);
 
-		in[0] = _mm256_min_ps(in[0], int_min);
-		in[1] = _mm256_min_ps(in[1], int_min);
+		in[0] = _MM256_CLAMP_PS(in[0], int_min, int_max);
+		in[1] = _MM256_CLAMP_PS(in[1], int_min, int_max);
 
 		out[0] = _mm256_cvtps_epi32(in[0]);	/* a0 a1 a2 a3 a4 a5 a6 a7 */
 		out[1] = _mm256_cvtps_epi32(in[1]);	/* b0 b1 b2 b3 b4 b5 b6 b7 */
+		out[0] = _mm256_slli_epi32(out[0], 8);
+		out[1] = _mm256_slli_epi32(out[1], 8);
 
 		t[0] = _mm256_unpacklo_epi32(out[0], out[1]); /* a0 b0 a1 b1 a4 b4 a5 b5 */
 		t[1] = _mm256_unpackhi_epi32(out[0], out[1]); /* a2 b2 a3 b3 a6 b6 a7 b7 */
@@ -635,8 +649,9 @@ conv_f32d_to_s32_2s_avx2(void *data, void * SPA_RESTRICT dst, const void * SPA_R
 	for(; n < n_samples; n++) {
 		__m128 in[2];
 		__m128i out[2];
-		__m128 scale = _mm_set1_ps(S32_SCALE);
-		__m128 int_min = _mm_set1_ps(S32_MIN);
+		__m128 scale = _mm_set1_ps(S24_SCALE);
+		__m128 int_min = _mm_set1_ps(S24_MIN);
+		__m128 int_max = _mm_set1_ps(S24_MAX);
 
 		in[0] = _mm_load_ss(&s0[n]);
 		in[1] = _mm_load_ss(&s1[n]);
@@ -644,8 +659,9 @@ conv_f32d_to_s32_2s_avx2(void *data, void * SPA_RESTRICT dst, const void * SPA_R
 		in[0] = _mm_unpacklo_ps(in[0], in[1]);
 
 		in[0] = _mm_mul_ps(in[0], scale);
-		in[0] = _mm_min_ps(in[0], int_min);
+		in[0] = _MM_CLAMP_PS(in[0], int_min, int_max);
 		out[0] = _mm_cvtps_epi32(in[0]);
+		out[0] = _mm_slli_epi32(out[0], 8);
 		_mm_storel_epi64((__m128i*)d, out[0]);
 		d += n_channels;
 	}
@@ -660,8 +676,9 @@ conv_f32d_to_s32_4s_avx2(void *data, void * SPA_RESTRICT dst, const void * SPA_R
 	uint32_t n, unrolled;
 	__m256 in[4];
 	__m256i out[4], t[4];
-	__m256 scale = _mm256_set1_ps(S32_SCALE);
-	__m256 int_min = _mm256_set1_ps(S32_MIN);
+	__m256 scale = _mm256_set1_ps(S24_SCALE);
+	__m256 int_min = _mm256_set1_ps(S24_MIN);
+	__m256 int_max = _mm256_set1_ps(S24_MAX);
 
 	if (SPA_IS_ALIGNED(s0, 32) &&
 	    SPA_IS_ALIGNED(s1, 32) &&
@@ -677,15 +694,19 @@ conv_f32d_to_s32_4s_avx2(void *data, void * SPA_RESTRICT dst, const void * SPA_R
 		in[2] = _mm256_mul_ps(_mm256_load_ps(&s2[n]), scale);
 		in[3] = _mm256_mul_ps(_mm256_load_ps(&s3[n]), scale);
 
-		in[0] = _mm256_min_ps(in[0], int_min);
-		in[1] = _mm256_min_ps(in[1], int_min);
-		in[2] = _mm256_min_ps(in[2], int_min);
-		in[3] = _mm256_min_ps(in[3], int_min);
+		in[0] = _MM256_CLAMP_PS(in[0], int_min, int_max);
+		in[1] = _MM256_CLAMP_PS(in[1], int_min, int_max);
+		in[2] = _MM256_CLAMP_PS(in[2], int_min, int_max);
+		in[3] = _MM256_CLAMP_PS(in[3], int_min, int_max);
 
 		out[0] = _mm256_cvtps_epi32(in[0]); /* a0 a1 a2 a3 a4 a5 a6 a7 */
 		out[1] = _mm256_cvtps_epi32(in[1]); /* b0 b1 b2 b3 b4 b5 b6 b7 */
 		out[2] = _mm256_cvtps_epi32(in[2]); /* c0 c1 c2 c3 c4 c5 c6 c7 */
 		out[3] = _mm256_cvtps_epi32(in[3]); /* d0 d1 d2 d3 d4 d5 d6 d7 */
+		out[0] = _mm256_slli_epi32(out[0], 8);
+		out[1] = _mm256_slli_epi32(out[1], 8);
+		out[2] = _mm256_slli_epi32(out[2], 8);
+		out[3] = _mm256_slli_epi32(out[3], 8);
 
 		t[0] = _mm256_unpacklo_epi32(out[0], out[1]); /* a0 b0 a1 b1 a4 b4 a5 b5 */
 		t[1] = _mm256_unpackhi_epi32(out[0], out[1]); /* a2 b2 a3 b3 a6 b6 a7 b7 */
@@ -710,8 +731,9 @@ conv_f32d_to_s32_4s_avx2(void *data, void * SPA_RESTRICT dst, const void * SPA_R
 	for(; n < n_samples; n++) {
 		__m128 in[4];
 		__m128i out[4];
-		__m128 scale = _mm_set1_ps(S32_SCALE);
-		__m128 int_min = _mm_set1_ps(S32_MIN);
+		__m128 scale = _mm_set1_ps(S24_SCALE);
+		__m128 int_min = _mm_set1_ps(S24_MIN);
+		__m128 int_max = _mm_set1_ps(S24_MAX);
 
 		in[0] = _mm_load_ss(&s0[n]);
 		in[1] = _mm_load_ss(&s1[n]);
@@ -723,8 +745,9 @@ conv_f32d_to_s32_4s_avx2(void *data, void * SPA_RESTRICT dst, const void * SPA_R
 		in[0] = _mm_unpacklo_ps(in[0], in[1]);
 
 		in[0] = _mm_mul_ps(in[0], scale);
-		in[0] = _mm_min_ps(in[0], int_min);
+		in[0] = _MM_CLAMP_PS(in[0], int_min, int_max);
 		out[0] = _mm_cvtps_epi32(in[0]);
+		out[0] = _mm_slli_epi32(out[0], 8);
 		_mm_storeu_si128((__m128i*)d, out[0]);
 		d += n_channels;
 	}
@@ -754,8 +777,9 @@ conv_f32d_to_s16_1s_avx2(void *data, void * SPA_RESTRICT dst, const void * SPA_R
 	uint32_t n, unrolled;
 	__m128 in[2];
 	__m128i out[2];
-	__m128 int_max = _mm_set1_ps(S16_MAX_F);
-        __m128 int_min = _mm_sub_ps(_mm_setzero_ps(), int_max);
+	__m128 int_scale = _mm_set1_ps(S16_SCALE);
+	__m128 int_max = _mm_set1_ps(S16_MAX);
+	__m128 int_min = _mm_set1_ps(S16_MIN);
 
 	if (SPA_IS_ALIGNED(s0, 16))
 		unrolled = n_samples & ~7;
@@ -763,8 +787,8 @@ conv_f32d_to_s16_1s_avx2(void *data, void * SPA_RESTRICT dst, const void * SPA_R
 		unrolled = 0;
 
 	for(n = 0; n < unrolled; n += 8) {
-		in[0] = _mm_mul_ps(_mm_load_ps(&s0[n]), int_max);
-		in[1] = _mm_mul_ps(_mm_load_ps(&s0[n+4]), int_max);
+		in[0] = _mm_mul_ps(_mm_load_ps(&s0[n]), int_scale);
+		in[1] = _mm_mul_ps(_mm_load_ps(&s0[n+4]), int_scale);
 		out[0] = _mm_cvtps_epi32(in[0]);
 		out[1] = _mm_cvtps_epi32(in[1]);
 		out[0] = _mm_packs_epi32(out[0], out[1]);
@@ -780,8 +804,8 @@ conv_f32d_to_s16_1s_avx2(void *data, void * SPA_RESTRICT dst, const void * SPA_R
 		d += 8*n_channels;
 	}
 	for(; n < n_samples; n++) {
-		in[0] = _mm_mul_ss(_mm_load_ss(&s0[n]), int_max);
-		in[0] = _mm_min_ss(int_max, _mm_max_ss(in[0], int_min));
+		in[0] = _mm_mul_ss(_mm_load_ss(&s0[n]), int_scale);
+		in[0] = _MM_CLAMP_SS(in[0], int_min, int_max);
 		*d = _mm_cvtss_si32(in[0]);
 		d += n_channels;
 	}
@@ -796,7 +820,7 @@ conv_f32d_to_s16_2s_avx2(void *data, void * SPA_RESTRICT dst, const void * SPA_R
 	uint32_t n, unrolled;
 	__m256 in[2];
 	__m256i out[4], t[2];
-	__m256 int_max = _mm256_set1_ps(S16_MAX_F);
+	__m256 int_scale = _mm256_set1_ps(S16_SCALE);
 
 	if (SPA_IS_ALIGNED(s0, 32) &&
 	    SPA_IS_ALIGNED(s1, 32))
@@ -805,8 +829,8 @@ conv_f32d_to_s16_2s_avx2(void *data, void * SPA_RESTRICT dst, const void * SPA_R
 		unrolled = 0;
 
 	for(n = 0; n < unrolled; n += 8) {
-		in[0] = _mm256_mul_ps(_mm256_load_ps(&s0[n+0]), int_max);
-		in[1] = _mm256_mul_ps(_mm256_load_ps(&s1[n+0]), int_max);
+		in[0] = _mm256_mul_ps(_mm256_load_ps(&s0[n+0]), int_scale);
+		in[1] = _mm256_mul_ps(_mm256_load_ps(&s1[n+0]), int_scale);
 
 		out[0] = _mm256_cvtps_epi32(in[0]); /* a0 a1 a2 a3 a4 a5 a6 a7 */
 		out[1] = _mm256_cvtps_epi32(in[1]); /* b0 b1 b2 b3 b4 b5 b6 b7 */
@@ -829,13 +853,14 @@ conv_f32d_to_s16_2s_avx2(void *data, void * SPA_RESTRICT dst, const void * SPA_R
 	}
 	for(; n < n_samples; n++) {
 		__m128 in[2];
-		__m128 int_max = _mm_set1_ps(S16_MAX_F);
-	        __m128 int_min = _mm_sub_ps(_mm_setzero_ps(), int_max);
-
-		in[0] = _mm_mul_ss(_mm_load_ss(&s0[n]), int_max);
-		in[1] = _mm_mul_ss(_mm_load_ss(&s1[n]), int_max);
-		in[0] = _mm_min_ss(int_max, _mm_max_ss(in[0], int_min));
-		in[1] = _mm_min_ss(int_max, _mm_max_ss(in[1], int_min));
+		__m128 int_scale = _mm_set1_ps(S16_SCALE);
+		__m128 int_max = _mm_set1_ps(S16_MAX);
+		__m128 int_min = _mm_set1_ps(S16_MIN);
+
+		in[0] = _mm_mul_ss(_mm_load_ss(&s0[n]), int_scale);
+		in[1] = _mm_mul_ss(_mm_load_ss(&s1[n]), int_scale);
+		in[0] = _MM_CLAMP_SS(in[0], int_min, int_max);
+		in[1] = _MM_CLAMP_SS(in[1], int_min, int_max);
 		d[0] = _mm_cvtss_si32(in[0]);
 		d[1] = _mm_cvtss_si32(in[1]);
 		d += n_channels;
@@ -851,7 +876,7 @@ conv_f32d_to_s16_4s_avx2(void *data, void * SPA_RESTRICT dst, const void * SPA_R
 	uint32_t n, unrolled;
 	__m256 in[4];
 	__m256i out[4], t[4];
-	__m256 int_max = _mm256_set1_ps(S16_MAX_F);
+	__m256 int_scale = _mm256_set1_ps(S16_SCALE);
 
 	if (SPA_IS_ALIGNED(s0, 32) &&
 	    SPA_IS_ALIGNED(s1, 32) &&
@@ -862,10 +887,10 @@ conv_f32d_to_s16_4s_avx2(void *data, void * SPA_RESTRICT dst, const void * SPA_R
 		unrolled = 0;
 
 	for(n = 0; n < unrolled; n += 8) {
-		in[0] = _mm256_mul_ps(_mm256_load_ps(&s0[n]), int_max);
-		in[1] = _mm256_mul_ps(_mm256_load_ps(&s1[n]), int_max);
-		in[2] = _mm256_mul_ps(_mm256_load_ps(&s2[n]), int_max);
-		in[3] = _mm256_mul_ps(_mm256_load_ps(&s3[n]), int_max);
+		in[0] = _mm256_mul_ps(_mm256_load_ps(&s0[n]), int_scale);
+		in[1] = _mm256_mul_ps(_mm256_load_ps(&s1[n]), int_scale);
+		in[2] = _mm256_mul_ps(_mm256_load_ps(&s2[n]), int_scale);
+		in[3] = _mm256_mul_ps(_mm256_load_ps(&s3[n]), int_scale);
 
 		t[0] = _mm256_cvtps_epi32(in[0]);  /* a0 a1 a2 a3 a4 a5 a6 a7 */
 		t[1] = _mm256_cvtps_epi32(in[1]);  /* b0 b1 b2 b3 b4 b5 b6 b7 */
@@ -905,17 +930,18 @@ conv_f32d_to_s16_4s_avx2(void *data, void * SPA_RESTRICT dst, const void * SPA_R
 	}
 	for(; n < n_samples; n++) {
 		__m128 in[4];
-		__m128 int_max = _mm_set1_ps(S16_MAX_F);
-	        __m128 int_min = _mm_sub_ps(_mm_setzero_ps(), int_max);
-
-		in[0] = _mm_mul_ss(_mm_load_ss(&s0[n]), int_max);
-		in[1] = _mm_mul_ss(_mm_load_ss(&s1[n]), int_max);
-		in[2] = _mm_mul_ss(_mm_load_ss(&s2[n]), int_max);
-		in[3] = _mm_mul_ss(_mm_load_ss(&s3[n]), int_max);
-		in[0] = _mm_min_ss(int_max, _mm_max_ss(in[0], int_min));
-		in[1] = _mm_min_ss(int_max, _mm_max_ss(in[1], int_min));
-		in[2] = _mm_min_ss(int_max, _mm_max_ss(in[2], int_min));
-		in[3] = _mm_min_ss(int_max, _mm_max_ss(in[3], int_min));
+		__m128 int_scale = _mm_set1_ps(S16_SCALE);
+		__m128 int_max = _mm_set1_ps(S16_MAX);
+		__m128 int_min = _mm_set1_ps(S16_MIN);
+
+		in[0] = _mm_mul_ss(_mm_load_ss(&s0[n]), int_scale);
+		in[1] = _mm_mul_ss(_mm_load_ss(&s1[n]), int_scale);
+		in[2] = _mm_mul_ss(_mm_load_ss(&s2[n]), int_scale);
+		in[3] = _mm_mul_ss(_mm_load_ss(&s3[n]), int_scale);
+		in[0] = _MM_CLAMP_SS(in[0], int_min, int_max);
+		in[1] = _MM_CLAMP_SS(in[1], int_min, int_max);
+		in[2] = _MM_CLAMP_SS(in[2], int_min, int_max);
+		in[3] = _MM_CLAMP_SS(in[3], int_min, int_max);
 		d[0] = _mm_cvtss_si32(in[0]);
 		d[1] = _mm_cvtss_si32(in[1]);
 		d[2] = _mm_cvtss_si32(in[2]);
@@ -948,7 +974,7 @@ conv_f32d_to_s16_4_avx2(struct convert *conv, void * SPA_RESTRICT dst[], const v
 	uint32_t n, unrolled;
 	__m256 in[4];
 	__m256i out[4], t[4];
-	__m256 int_max = _mm256_set1_ps(S16_MAX_F);
+	__m256 int_scale = _mm256_set1_ps(S16_SCALE);
 
 	if (SPA_IS_ALIGNED(s0, 32) &&
 	    SPA_IS_ALIGNED(s1, 32) &&
@@ -959,10 +985,10 @@ conv_f32d_to_s16_4_avx2(struct convert *conv, void * SPA_RESTRICT dst[], const v
 		unrolled = 0;
 
 	for(n = 0; n < unrolled; n += 8) {
-		in[0] = _mm256_mul_ps(_mm256_load_ps(&s0[n]), int_max);
-		in[1] = _mm256_mul_ps(_mm256_load_ps(&s1[n]), int_max);
-		in[2] = _mm256_mul_ps(_mm256_load_ps(&s2[n]), int_max);
-		in[3] = _mm256_mul_ps(_mm256_load_ps(&s3[n]), int_max);
+		in[0] = _mm256_mul_ps(_mm256_load_ps(&s0[n]), int_scale);
+		in[1] = _mm256_mul_ps(_mm256_load_ps(&s1[n]), int_scale);
+		in[2] = _mm256_mul_ps(_mm256_load_ps(&s2[n]), int_scale);
+		in[3] = _mm256_mul_ps(_mm256_load_ps(&s3[n]), int_scale);
 
 		t[0] = _mm256_cvtps_epi32(in[0]);  /* a0 a1 a2 a3 a4 a5 a6 a7 */
 		t[1] = _mm256_cvtps_epi32(in[1]);  /* b0 b1 b2 b3 b4 b5 b6 b7 */
@@ -987,17 +1013,18 @@ conv_f32d_to_s16_4_avx2(struct convert *conv, void * SPA_RESTRICT dst[], const v
 	}
 	for(; n < n_samples; n++) {
 		__m128 in[4];
-		__m128 int_max = _mm_set1_ps(S16_MAX_F);
-	        __m128 int_min = _mm_sub_ps(_mm_setzero_ps(), int_max);
-
-		in[0] = _mm_mul_ss(_mm_load_ss(&s0[n]), int_max);
-		in[1] = _mm_mul_ss(_mm_load_ss(&s1[n]), int_max);
-		in[2] = _mm_mul_ss(_mm_load_ss(&s2[n]), int_max);
-		in[3] = _mm_mul_ss(_mm_load_ss(&s3[n]), int_max);
-		in[0] = _mm_min_ss(int_max, _mm_max_ss(in[0], int_min));
-		in[1] = _mm_min_ss(int_max, _mm_max_ss(in[1], int_min));
-		in[2] = _mm_min_ss(int_max, _mm_max_ss(in[2], int_min));
-		in[3] = _mm_min_ss(int_max, _mm_max_ss(in[3], int_min));
+		__m128 int_scale = _mm_set1_ps(S16_SCALE);
+		__m128 int_max = _mm_set1_ps(S16_MAX);
+		__m128 int_min = _mm_set1_ps(S16_MIN);
+
+		in[0] = _mm_mul_ss(_mm_load_ss(&s0[n]), int_scale);
+		in[1] = _mm_mul_ss(_mm_load_ss(&s1[n]), int_scale);
+		in[2] = _mm_mul_ss(_mm_load_ss(&s2[n]), int_scale);
+		in[3] = _mm_mul_ss(_mm_load_ss(&s3[n]), int_scale);
+		in[0] = _MM_CLAMP_SS(in[0], int_min, int_max);
+		in[1] = _MM_CLAMP_SS(in[1], int_min, int_max);
+		in[2] = _MM_CLAMP_SS(in[2], int_min, int_max);
+		in[3] = _MM_CLAMP_SS(in[3], int_min, int_max);
 		d[0] = _mm_cvtss_si32(in[0]);
 		d[1] = _mm_cvtss_si32(in[1]);
 		d[2] = _mm_cvtss_si32(in[2]);
@@ -1014,7 +1041,7 @@ conv_f32d_to_s16_2_avx2(struct convert *conv, void * SPA_RESTRICT dst[], const v
 	uint32_t n, unrolled;
 	__m256 in[4];
 	__m256i out[4], t[4];
-	__m256 int_max = _mm256_set1_ps(S16_MAX_F);
+	__m256 int_scale = _mm256_set1_ps(S16_SCALE);
 
 	if (SPA_IS_ALIGNED(s0, 32) &&
 	    SPA_IS_ALIGNED(s1, 32))
@@ -1023,10 +1050,10 @@ conv_f32d_to_s16_2_avx2(struct convert *conv, void * SPA_RESTRICT dst[], const v
 		unrolled = 0;
 
 	for(n = 0; n < unrolled; n += 16) {
-		in[0] = _mm256_mul_ps(_mm256_load_ps(&s0[n+0]), int_max);
-		in[1] = _mm256_mul_ps(_mm256_load_ps(&s1[n+0]), int_max);
-		in[2] = _mm256_mul_ps(_mm256_load_ps(&s0[n+8]), int_max);
-		in[3] = _mm256_mul_ps(_mm256_load_ps(&s1[n+8]), int_max);
+		in[0] = _mm256_mul_ps(_mm256_load_ps(&s0[n+0]), int_scale);
+		in[1] = _mm256_mul_ps(_mm256_load_ps(&s1[n+0]), int_scale);
+		in[2] = _mm256_mul_ps(_mm256_load_ps(&s0[n+8]), int_scale);
+		in[3] = _mm256_mul_ps(_mm256_load_ps(&s1[n+8]), int_scale);
 
 		out[0] = _mm256_cvtps_epi32(in[0]); /* a0 a1 a2 a3 a4 a5 a6 a7 */
 		out[1] = _mm256_cvtps_epi32(in[1]); /* b0 b1 b2 b3 b4 b5 b6 b7 */
@@ -1048,13 +1075,14 @@ conv_f32d_to_s16_2_avx2(struct convert *conv, void * SPA_RESTRICT dst[], const v
 	}
 	for(; n < n_samples; n++) {
 		__m128 in[4];
-		__m128 int_max = _mm_set1_ps(S16_MAX_F);
-		__m128 int_min = _mm_sub_ps(_mm_setzero_ps(), int_max);
-
-		in[0] = _mm_mul_ss(_mm_load_ss(&s0[n]), int_max);
-		in[1] = _mm_mul_ss(_mm_load_ss(&s1[n]), int_max);
-		in[0] = _mm_min_ss(int_max, _mm_max_ss(in[0], int_min));
-		in[1] = _mm_min_ss(int_max, _mm_max_ss(in[1], int_min));
+		__m128 int_scale = _mm_set1_ps(S16_SCALE);
+		__m128 int_max = _mm_set1_ps(S16_MAX);
+		__m128 int_min = _mm_set1_ps(S16_MIN);
+
+		in[0] = _mm_mul_ss(_mm_load_ss(&s0[n]), int_scale);
+		in[1] = _mm_mul_ss(_mm_load_ss(&s1[n]), int_scale);
+		in[0] = _MM_CLAMP_SS(in[0], int_min, int_max);
+		in[1] = _MM_CLAMP_SS(in[1], int_min, int_max);
 		d[0] = _mm_cvtss_si32(in[0]);
 		d[1] = _mm_cvtss_si32(in[1]);
 		d += 2;
diff --git a/spa/plugins/audioconvert/fmt-ops-c.c b/spa/plugins/audioconvert/fmt-ops-c.c
index 95dfec7c2a0183312e403718b72157e55eff7368..f3d91d0356544404020dc902edc2ad6358f4e9cd 100644
--- a/spa/plugins/audioconvert/fmt-ops-c.c
+++ b/spa/plugins/audioconvert/fmt-ops-c.c
@@ -33,1499 +33,380 @@
 #include "fmt-ops.h"
 #include "law.h"
 
-void
-conv_copy8d_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	uint32_t i, n_channels = conv->n_channels;
-	for (i = 0; i < n_channels; i++)
-		spa_memcpy(dst[i], src[i], n_samples);
-}
-
-void
-conv_copy8_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	spa_memcpy(dst[0], src[0], n_samples * conv->n_channels);
-}
-
-
-void
-conv_copy16d_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	uint32_t i, n_channels = conv->n_channels;
-	for (i = 0; i < n_channels; i++)
-		spa_memcpy(dst[i], src[i], n_samples * sizeof(int16_t));
-}
-
-void
-conv_copy16_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	spa_memcpy(dst[0], src[0], n_samples * sizeof(int16_t) * conv->n_channels);
-}
-
-void
-conv_copy24d_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	uint32_t i, n_channels = conv->n_channels;
-	for (i = 0; i < n_channels; i++)
-		spa_memcpy(dst[i], src[i], n_samples * 3);
-}
-
-void
-conv_copy24_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	spa_memcpy(dst[0], src[0], n_samples * 3 * conv->n_channels);
-}
-
-void
-conv_copy32d_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	uint32_t i, n_channels = conv->n_channels;
-	for (i = 0; i < n_channels; i++)
-		spa_memcpy(dst[i], src[i], n_samples * sizeof(int32_t));
-}
-
-void
-conv_copy32_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	spa_memcpy(dst[0], src[0], n_samples * sizeof(int32_t) * conv->n_channels);
-}
-
-void
-conv_copy64d_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	uint32_t i, n_channels = conv->n_channels;
-	for (i = 0; i < n_channels; i++)
-		spa_memcpy(dst[i], src[i], n_samples * sizeof(int64_t));
-}
-
-void
-conv_copy64_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	spa_memcpy(dst[0], src[0], n_samples * sizeof(int64_t) * conv->n_channels);
-}
-
-void
-conv_u8d_to_f32d_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (i = 0; i < n_channels; i++) {
-		const uint8_t *s = src[i];
-		float *d = dst[i];
-
-		for (j = 0; j < n_samples; j++)
-			d[j] = U8_TO_F32(s[j]);
-	}
-}
-
-void
-conv_u8_to_f32_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	uint32_t i, n_channels = conv->n_channels;
-	const uint8_t *s = src[0];
-	float *d = dst[0];
-
-	n_samples *= n_channels;
-
-	for (i = 0; i < n_samples; i++)
-		d[i] = U8_TO_F32(s[i]);
-}
-
-void
-conv_u8_to_f32d_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	const uint8_t *s = src[0];
-	float **d = (float **) dst;
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (j = 0; j < n_samples; j++) {
-		for (i = 0; i < n_channels; i++)
-			d[i][j] = U8_TO_F32(*s++);
-	}
-}
-
-void
-conv_u8d_to_f32_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	const uint8_t **s = (const uint8_t **) src;
-	float *d = dst[0];
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (j = 0; j < n_samples; j++) {
-		for (i = 0; i < n_channels; i++)
-			*d++ = U8_TO_F32(s[i][j]);
-	}
-}
-
-void
-conv_s8d_to_f32d_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (i = 0; i < n_channels; i++) {
-		const int8_t *s = src[i];
-		float *d = dst[i];
-		for (j = 0; j < n_samples; j++)
-			d[j] = S8_TO_F32(s[j]);
-	}
-}
-
-void
-conv_s8_to_f32_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	uint32_t i, n_channels = conv->n_channels;
-	const int8_t *s = src[0];
-	float *d = dst[0];
-
-	n_samples *= n_channels;
-
-	for (i = 0; i < n_samples; i++)
-		d[i] = S8_TO_F32(s[i]);
-}
-
-void
-conv_s8_to_f32d_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	const int8_t *s = src[0];
-	float **d = (float **) dst;
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (j = 0; j < n_samples; j++) {
-		for (i = 0; i < n_channels; i++)
-			d[i][j] = S8_TO_F32(*s++);
-	}
-}
-
-void
-conv_s8d_to_f32_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	const int8_t **s = (const int8_t **) src;
-	float *d = dst[0];
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (j = 0; j < n_samples; j++) {
-		for (i = 0; i < n_channels; i++)
-			*d++ = S8_TO_F32(s[i][j]);
-	}
-}
-
-void
-conv_alaw_to_f32d_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	const uint8_t *s = src[0];
-	float **d = (float **) dst;
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (j = 0; j < n_samples; j++) {
-		for (i = 0; i < n_channels; i++)
-			d[i][j] = alaw_to_f32(*s++);
-	}
-}
-
-void
-conv_ulaw_to_f32d_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	const uint8_t *s = src[0];
-	float **d = (float **) dst;
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (j = 0; j < n_samples; j++) {
-		for (i = 0; i < n_channels; i++)
-			d[i][j] = ulaw_to_f32(*s++);
-	}
-}
-
-void
-conv_u16_to_f32_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	uint32_t i, n_channels = conv->n_channels;
-	const uint16_t *s = src[0];
-	float *d = dst[0];
-
-	n_samples *= n_channels;
-
-	for (i = 0; i < n_samples; i++)
-		d[i] = U16_TO_F32(s[i]);
-}
-
-void
-conv_u16_to_f32d_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	const uint16_t *s = src[0];
-	float **d = (float **) dst;
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (j = 0; j < n_samples; j++) {
-		for (i = 0; i < n_channels; i++)
-			d[i][j] = U16_TO_F32(*s++);
-	}
-}
-
-void
-conv_s16d_to_f32d_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (i = 0; i < n_channels; i++) {
-		const int16_t *s = src[i];
-		float *d = dst[i];
-		for (j = 0; j < n_samples; j++)
-			d[j] = S16_TO_F32(s[j]);
-	}
-}
-
-void
-conv_s16_to_f32_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	uint32_t i, n_channels = conv->n_channels;
-	const int16_t *s = src[0];
-	float *d = dst[0];
-
-	n_samples *= n_channels;
-
-	for (i = 0; i < n_samples; i++)
-		d[i] = S16_TO_F32(s[i]);
-}
-
-void
-conv_s16_to_f32d_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	const int16_t *s = src[0];
-	float **d = (float **) dst;
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (j = 0; j < n_samples; j++) {
-		for (i = 0; i < n_channels; i++)
-			d[i][j] = S16_TO_F32(*s++);
-	}
-}
-
-void
-conv_s16s_to_f32d_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	const int16_t *s = src[0];
-	float **d = (float **) dst;
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (j = 0; j < n_samples; j++) {
-		for (i = 0; i < n_channels; i++)
-			d[i][j] = S16S_TO_F32(*s++);
-	}
-}
-
-void
-conv_s16d_to_f32_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	const int16_t **s = (const int16_t **) src;
-	float *d = dst[0];
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (j = 0; j < n_samples; j++) {
-		for (i = 0; i < n_channels; i++)
-			*d++ = S16_TO_F32(s[i][j]);
-	}
-}
-
-void
-conv_u32_to_f32_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	uint32_t i, n_channels = conv->n_channels;
-	const uint32_t *s = src[0];
-	float *d = dst[0];
-
-	n_samples *= n_channels;
-
-	for (i = 0; i < n_samples; i++)
-		d[i] = U32_TO_F32(s[i]);
-}
-
-void
-conv_u32_to_f32d_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	const uint32_t *s = src[0];
-	float **d = (float **) dst;
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (j = 0; j < n_samples; j++) {
-		for (i = 0; i < n_channels; i++)
-			d[i][j] = U32_TO_F32(*s++);
-	}
-}
-
-void
-conv_s32d_to_f32d_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (i = 0; i < n_channels; i++) {
-		const int32_t *s = src[i];
-		float *d = dst[i];
-
-		for (j = 0; j < n_samples; j++)
-			d[j] = S32_TO_F32(s[j]);
-	}
-}
-
-void
-conv_s32_to_f32_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	uint32_t i, n_channels = conv->n_channels;
-	const int32_t *s = src[0];
-	float *d = dst[0];
-
-	n_samples *= n_channels;
-
-	for (i = 0; i < n_samples; i++)
-		d[i] = S32_TO_F32(s[i]);
-}
-
-void
-conv_s32_to_f32d_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	const int32_t *s = src[0];
-	float **d = (float **) dst;
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (j = 0; j < n_samples; j++) {
-		for (i = 0; i < n_channels; i++)
-			d[i][j] = S32_TO_F32(*s++);
-	}
-}
-
-void
-conv_s32s_to_f32d_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	const int32_t *s = src[0];
-	float **d = (float **) dst;
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (j = 0; j < n_samples; j++) {
-		for (i = 0; i < n_channels; i++)
-			d[i][j] = S32S_TO_F32(*s++);
-	}
-}
-
-void
-conv_s32d_to_f32_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	const int32_t **s = (const int32_t **) src;
-	float *d = dst[0];
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (j = 0; j < n_samples; j++) {
-		for (i = 0; i < n_channels; i++)
-			*d++ = S32_TO_F32(s[i][j]);
-	}
-}
-
-void
-conv_u24_to_f32_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	uint32_t i, n_channels = conv->n_channels;
-	const uint8_t *s = src[0];
-	float *d = dst[0];
-
-	n_samples *= n_channels;
-
-	for (i = 0; i < n_samples; i++) {
-		d[i] = U24_TO_F32(read_u24(s));
-		s += 3;
-	}
-}
-
-void
-conv_u24_to_f32d_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	const uint8_t *s = src[0];
-	float **d = (float **) dst;
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (j = 0; j < n_samples; j++) {
-		for (i = 0; i < n_channels; i++) {
-			d[i][j] = U24_TO_F32(read_u24(s));
-			s += 3;
-		}
-	}
-}
-
-void
-conv_s24d_to_f32d_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (i = 0; i < n_channels; i++) {
-		const int8_t *s = src[i];
-		float *d = dst[i];
-
-		for (j = 0; j < n_samples; j++) {
-			d[j] = S24_TO_F32(read_s24(s));
-			s += 3;
-		}
-	}
-}
-
-void
-conv_s24_to_f32_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	uint32_t i, n_channels = conv->n_channels;
-	const int8_t *s = src[0];
-	float *d = dst[0];
-
-	n_samples *= n_channels;
-
-	for (i = 0; i < n_samples; i++) {
-		d[i] = S24_TO_F32(read_s24(s));
-		s += 3;
-	}
-}
-
-void
-conv_s24_to_f32d_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	const uint8_t *s = src[0];
-	float **d = (float **) dst;
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (j = 0; j < n_samples; j++) {
-		for (i = 0; i < n_channels; i++) {
-			d[i][j] = S24_TO_F32(read_s24(s));
-			s += 3;
-		}
-	}
-}
-
-void
-conv_s24s_to_f32d_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	const uint8_t *s = src[0];
-	float **d = (float **) dst;
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (j = 0; j < n_samples; j++) {
-		for (i = 0; i < n_channels; i++) {
-			d[i][j] = S24_TO_F32(read_s24s(s));
-			s += 3;
-		}
-	}
-}
-
-void
-conv_s24d_to_f32_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	const uint8_t **s = (const uint8_t **) src;
-	float *d = dst[0];
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (j = 0; j < n_samples; j++) {
-		for (i = 0; i < n_channels; i++) {
-			*d++ = S24_TO_F32(read_s24(&s[i][j*3]));
-		}
-	}
-}
-
-void
-conv_u24_32_to_f32_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	uint32_t i, n_channels = conv->n_channels;
-	const uint32_t *s = src[0];
-	float *d = dst[0];
-
-	n_samples *= n_channels;
-
-	for (i = 0; i < n_samples; i++) {
-		d[i] = U24_32_TO_F32(s[i]);
-	}
-}
-
-void
-conv_u24_32_to_f32d_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	const uint32_t *s = src[0];
-	float **d = (float **) dst;
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (j = 0; j < n_samples; j++) {
-		for (i = 0; i < n_channels; i++)
-			d[i][j] = U24_32_TO_F32(*s++);
-	}
-}
-
-void
-conv_s24_32d_to_f32d_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (i = 0; i < n_channels; i++) {
-		const int32_t *s = src[i];
-		float *d = dst[i];
-
-		for (j = 0; j < n_samples; j++)
-			d[j] = S24_32_TO_F32(s[j]);
-	}
-}
-
-void
-conv_s24_32_to_f32_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	uint32_t i, n_channels = conv->n_channels;
-	const int32_t *s = src[0];
-	float *d = dst[0];
-
-	n_samples *= n_channels;
-
-	for (i = 0; i < n_samples; i++) {
-		d[i] = S24_32_TO_F32(s[i]);
-	}
-}
-
-void
-conv_s24_32_to_f32d_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	const int32_t *s = src[0];
-	float **d = (float **) dst;
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (j = 0; j < n_samples; j++) {
-		for (i = 0; i < n_channels; i++)
-			d[i][j] = S24_32_TO_F32(*s++);
-	}
-}
-
-void
-conv_s24_32s_to_f32d_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	const int32_t *s = src[0];
-	float **d = (float **) dst;
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (j = 0; j < n_samples; j++) {
-		for (i = 0; i < n_channels; i++)
-			d[i][j] = S24_32S_TO_F32(*s++);
-	}
-}
-
-void
-conv_s24_32d_to_f32_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	const int32_t **s = (const int32_t **) src;
-	float *d = dst[0];
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (j = 0; j < n_samples; j++) {
-		for (i = 0; i < n_channels; i++)
-			*d++ = S24_32_TO_F32(s[i][j]);
-	}
-}
-
-void
-conv_f64d_to_f32d_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (i = 0; i < n_channels; i++) {
-		const double *s = src[i];
-		float *d = dst[i];
-
-		for (j = 0; j < n_samples; j++)
-			d[j] = s[j];
-	}
-}
-
-void
-conv_f64_to_f32_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	uint32_t i, n_channels = conv->n_channels;
-	const double *s = src[0];
-	float *d = dst[0];
-
-	n_samples *= n_channels;
-
-	for (i = 0; i < n_samples; i++)
-		d[i] = s[i];
-}
-
-void
-conv_f64_to_f32d_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	const double *s = src[0];
-	float **d = (float **) dst;
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (j = 0; j < n_samples; j++) {
-		for (i = 0; i < n_channels; i++)
-			d[i][j] = *s++;
-	}
-}
-
-void
-conv_f64s_to_f32d_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	const double *s = src[0];
-	float **d = (float **) dst;
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (j = 0; j < n_samples; j++) {
-		for (i = 0; i < n_channels; i++)
-			d[i][j] = bswap_64(*s++);
-	}
-}
-
-void
-conv_f64d_to_f32_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	const double **s = (const double **) src;
-	float *d = dst[0];
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (j = 0; j < n_samples; j++) {
-		for (i = 0; i < n_channels; i++)
-			*d++ = s[i][j];
-	}
-}
-
-void
-conv_f32d_to_u8d_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (i = 0; i < n_channels; i++) {
-		const float *s = src[i];
-		uint8_t *d = dst[i];
-
-		for (j = 0; j < n_samples; j++)
-			d[j] = F32_TO_U8(s[j]);
-	}
-}
-
-void
-conv_f32_to_u8_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	uint32_t i, n_channels = conv->n_channels;
-	const float *s = src[0];
-	uint8_t *d = dst[0];
-
-	n_samples *= n_channels;
-
-	for (i = 0; i < n_samples; i++)
-		d[i] = F32_TO_U8(s[i]);
-}
-
-void
-conv_f32_to_u8d_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	const float *s = src[0];
-	uint8_t **d = (uint8_t **) dst;
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (j = 0; j < n_samples; j++) {
-		for (i = 0; i < n_channels; i++)
-			d[i][j] = F32_TO_U8(*s++);
-	}
-}
-
-void
-conv_f32d_to_u8_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	const float **s = (const float **) src;
-	uint8_t *d = dst[0];
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (j = 0; j < n_samples; j++) {
-		for (i = 0; i < n_channels; i++)
-			*d++ = F32_TO_U8(s[i][j]);
-	}
-}
-
-void
-conv_f32d_to_s8d_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (i = 0; i < n_channels; i++) {
-		const float *s = src[i];
-		int8_t *d = dst[i];
-
-		for (j = 0; j < n_samples; j++)
-			d[j] = F32_TO_S8(s[j]);
-	}
-}
-
-void
-conv_f32_to_s8_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	uint32_t i, n_channels = conv->n_channels;
-	const float *s = src[0];
-	int8_t *d = dst[0];
-
-	n_samples *= n_channels;
-
-	for (i = 0; i < n_samples; i++)
-		d[i] = F32_TO_S8(s[i]);
-}
-
-void
-conv_f32_to_s8d_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	const float *s = src[0];
-	int8_t **d = (int8_t **) dst;
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (j = 0; j < n_samples; j++) {
-		for (i = 0; i < n_channels; i++)
-			d[i][j] = F32_TO_S8(*s++);
-	}
-}
-
-void
-conv_f32d_to_s8_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	const float **s = (const float **) src;
-	int8_t *d = dst[0];
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (j = 0; j < n_samples; j++) {
-		for (i = 0; i < n_channels; i++)
-			*d++ = F32_TO_S8(s[i][j]);
-	}
-}
-
-void
-conv_f32d_to_alaw_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	const float **s = (const float **) src;
-	int8_t *d = dst[0];
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (j = 0; j < n_samples; j++) {
-		for (i = 0; i < n_channels; i++)
-			*d++ = f32_to_alaw(s[i][j]);
-	}
-}
-
-void
-conv_f32d_to_ulaw_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	const float **s = (const float **) src;
-	int8_t *d = dst[0];
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (j = 0; j < n_samples; j++) {
-		for (i = 0; i < n_channels; i++)
-			*d++ = f32_to_ulaw(s[i][j]);
-	}
-}
-
-void
-conv_f32_to_u16_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	uint32_t i, n_channels = conv->n_channels;
-	const float *s = src[0];
-	uint16_t *d = dst[0];
-
-	n_samples *= n_channels;
-
-	for (i = 0; i < n_samples; i++)
-		d[i] = F32_TO_U16(s[i]);
-}
-void
-conv_f32d_to_u16_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	const float **s = (const float **) src;
-	uint16_t *d = dst[0];
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (j = 0; j < n_samples; j++) {
-		for (i = 0; i < n_channels; i++)
-			*d++ = F32_TO_U16(s[i][j]);
-	}
-}
-
-void
-conv_f32d_to_s16d_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (i = 0; i < n_channels; i++) {
-		const float *s = src[i];
-		int16_t *d = dst[i];
-
-		for (j = 0; j < n_samples; j++)
-			d[j] = F32_TO_S16(s[j]);
-	}
-}
-
-void
-conv_f32_to_s16_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	uint32_t i, n_channels = conv->n_channels;
-	const float *s = src[0];
-	int16_t *d = dst[0];
-
-	n_samples *= n_channels;
-
-	for (i = 0; i < n_samples; i++)
-		d[i] = F32_TO_S16(s[i]);
-}
-
-void
-conv_f32_to_s16d_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	const float *s = src[0];
-	int16_t **d = (int16_t **) dst;
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (j = 0; j < n_samples; j++) {
-		for (i = 0; i < n_channels; i++)
-			d[i][j] = F32_TO_S16(*s++);
-	}
-}
-
-void
-conv_f32d_to_s16_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	const float **s = (const float **) src;
-	int16_t *d = dst[0];
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (j = 0; j < n_samples; j++) {
-		for (i = 0; i < n_channels; i++)
-			*d++ = F32_TO_S16(s[i][j]);
-	}
-}
-
-void
-conv_f32d_to_s16s_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	const float **s = (const float **) src;
-	int16_t *d = dst[0];
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (j = 0; j < n_samples; j++) {
-		for (i = 0; i < n_channels; i++)
-			*d++ = F32_TO_S16S(s[i][j]);
-	}
-}
-
-void
-conv_f32_to_u32_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	uint32_t i, n_channels = conv->n_channels;
-	const float *s = src[0];
-	uint32_t *d = dst[0];
-
-	n_samples *= n_channels;
-
-	for (i = 0; i < n_samples; i++)
-		d[i] = F32_TO_U32(s[i]);
-}
-
-void
-conv_f32d_to_u32_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	const float **s = (const float **) src;
-	uint32_t *d = dst[0];
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (j = 0; j < n_samples; j++) {
-		for (i = 0; i < n_channels; i++)
-			*d++ = F32_TO_U32(s[i][j]);
-	}
-}
-
-void
-conv_f32d_to_s32d_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (i = 0; i < n_channels; i++) {
-		const float *s = src[i];
-		int32_t *d = dst[i];
-
-		for (j = 0; j < n_samples; j++)
-			d[j] = F32_TO_S32(s[j]);
-	}
-}
-
-void
-conv_f32_to_s32_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	uint32_t i, n_channels = conv->n_channels;
-	const float *s = src[0];
-	int32_t *d = dst[0];
-
-	n_samples *= n_channels;
-
-	for (i = 0; i < n_samples; i++)
-		d[i] = F32_TO_S32(s[i]);
-}
-
-void
-conv_f32_to_s32d_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	const float *s = src[0];
-	int32_t **d = (int32_t **) dst;
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (j = 0; j < n_samples; j++) {
-		for (i = 0; i < n_channels; i++)
-			d[i][j] = F32_TO_S32(*s++);
-	}
-}
-
-void
-conv_f32d_to_s32_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	const float **s = (const float **) src;
-	int32_t *d = dst[0];
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (j = 0; j < n_samples; j++) {
-		for (i = 0; i < n_channels; i++)
-			*d++ = F32_TO_S32(s[i][j]);
-	}
-}
-
-void
-conv_f32d_to_s32s_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	const float **s = (const float **) src;
-	int32_t *d = dst[0];
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (j = 0; j < n_samples; j++) {
-		for (i = 0; i < n_channels; i++)
-			*d++ = F32_TO_S32S(s[i][j]);
-	}
-}
-
-void
-conv_f32d_to_f64d_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (i = 0; i < n_channels; i++) {
-		const float *s = src[i];
-		double *d = dst[i];
-
-		for (j = 0; j < n_samples; j++)
-			d[j] = s[j];
-	}
-}
-
-void
-conv_f32_to_f64_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	uint32_t i, n_channels = conv->n_channels;
-	const float *s = src[0];
-	double *d = dst[0];
-
-	n_samples *= n_channels;
-
-	for (i = 0; i < n_samples; i++)
-		d[i] = s[i];
-}
-
-void
-conv_f32_to_f64d_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	const float *s = src[0];
-	double **d = (double **) dst;
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (j = 0; j < n_samples; j++) {
-		for (i = 0; i < n_channels; i++)
-			d[i][j] = *s++;
-	}
-}
-
-void
-conv_f32d_to_f64_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	const float **s = (const float **) src;
-	double *d = dst[0];
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (j = 0; j < n_samples; j++) {
-		for (i = 0; i < n_channels; i++)
-			*d++ = s[i][j];
-	}
-}
-
-void
-conv_f32d_to_f64s_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	const float **s = (const float **) src;
-	double *d = dst[0];
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (j = 0; j < n_samples; j++) {
-		for (i = 0; i < n_channels; i++)
-			*d++ = bswap_32(s[i][j]);
-	}
-}
-
-void
-conv_f32_to_u24_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	uint32_t i, n_channels = conv->n_channels;
-	const float *s = src[0];
-	uint8_t *d = dst[0];
-
-	n_samples *= n_channels;
-
-	for (i = 0; i < n_samples; i++) {
-		write_u24(d, F32_TO_U24(s[i]));
-		d += 3;
-	}
-}
-
-void
-conv_f32d_to_u24_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	const float **s = (const float **) src;
-	uint8_t *d = dst[0];
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (j = 0; j < n_samples; j++) {
-		for (i = 0; i < n_channels; i++) {
-			write_u24(d, F32_TO_U24(s[i][j]));
-			d += 3;
-		}
-	}
-}
-
-void
-conv_f32d_to_s24d_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (i = 0; i < n_channels; i++) {
-		const float *s = src[i];
-		uint8_t *d = dst[i];
-
-		for (j = 0; j < n_samples; j++) {
-			write_s24(d, F32_TO_S24(s[j]));
-			d += 3;
-		}
-	}
-}
-
-void
-conv_f32_to_s24_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	uint32_t i, n_channels = conv->n_channels;
-	const float *s = src[0];
-	uint8_t *d = dst[0];
-
-	n_samples *= n_channels;
-
-	for (i = 0; i < n_samples; i++) {
-		write_s24(d, F32_TO_S24(s[i]));
-		d += 3;
-	}
-}
-
-void
-conv_f32_to_s24d_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	const float *s = src[0];
-	uint8_t **d = (uint8_t **) dst;
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (j = 0; j < n_samples; j++) {
-		for (i = 0; i < n_channels; i++) {
-			write_s24(&d[i][j*3], F32_TO_S24(*s++));
-		}
-	}
-}
-
-void
-conv_f32d_to_s24_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	const float **s = (const float **) src;
-	uint8_t *d = dst[0];
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (j = 0; j < n_samples; j++) {
-		for (i = 0; i < n_channels; i++) {
-			write_s24(d, F32_TO_S24(s[i][j]));
-			d += 3;
-		}
-	}
-}
-
-void
-conv_f32d_to_s24s_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	const float **s = (const float **) src;
-	uint8_t *d = dst[0];
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (j = 0; j < n_samples; j++) {
-		for (i = 0; i < n_channels; i++) {
-			write_s24s(d, F32_TO_S24(s[i][j]));
-			d += 3;
-		}
-	}
-}
-
-
-void
-conv_f32d_to_s24_32d_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (i = 0; i < n_channels; i++) {
-		const float *s = src[i];
-		int32_t *d = dst[i];
-
-		for (j = 0; j < n_samples; j++)
-			d[j] = F32_TO_S24_32(s[j]);
-	}
-}
-
-void
-conv_f32_to_u24_32_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	uint32_t i, n_channels = conv->n_channels;
-	const float *s = src[0];
-	uint32_t *d = dst[0];
-
-	n_samples *= n_channels;
-
-	for (i = 0; i < n_samples; i++)
-		d[i] = F32_TO_U24_32(s[i]);
-}
-
-void
-conv_f32d_to_u24_32_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	const float **s = (const float **) src;
-	uint32_t *d = dst[0];
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (j = 0; j < n_samples; j++) {
-		for (i = 0; i < n_channels; i++)
-			*d++ = F32_TO_U24_32(s[i][j]);
-	}
-}
-
-void
-conv_f32_to_s24_32_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	uint32_t i, n_channels = conv->n_channels;
-	const float *s = src[0];
-	int32_t *d = dst[0];
-
-	n_samples *= n_channels;
-
-	for (i = 0; i < n_samples; i++)
-		d[i] = F32_TO_S24_32(s[i]);
-}
-
-void
-conv_f32_to_s24_32d_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	const float *s = src[0];
-	int32_t **d = (int32_t **) dst;
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (j = 0; j < n_samples; j++) {
-		for (i = 0; i < n_channels; i++)
-			d[i][j] = F32_TO_S24_32(*s++);
-	}
-}
-
-void
-conv_f32d_to_s24_32_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	const float **s = (const float **) src;
-	int32_t *d = dst[0];
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (j = 0; j < n_samples; j++) {
-		for (i = 0; i < n_channels; i++)
-			*d++ = F32_TO_S24_32(s[i][j]);
-	}
-}
-
-void
-conv_f32d_to_s24_32s_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	const float **s = (const float **) src;
-	int32_t *d = dst[0];
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (j = 0; j < n_samples; j++) {
-		for (i = 0; i < n_channels; i++)
-			*d++ = F32_TO_S24_32S(s[i][j]);
-	}
-}
-
-void
-conv_deinterleave_8_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	const uint8_t *s = src[0];
-	uint8_t **d = (uint8_t **) dst;
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (j = 0; j < n_samples; j++) {
-		for (i = 0; i < n_channels; i++)
-			d[i][j] = *s++;
-	}
-}
-
-void
-conv_deinterleave_16_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	const uint16_t *s = src[0];
-	uint16_t **d = (uint16_t **) dst;
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (j = 0; j < n_samples; j++) {
-		for (i = 0; i < n_channels; i++)
-			d[i][j] = *s++;
-	}
-}
-
-void
-conv_deinterleave_24_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	const uint8_t *s = src[0];
-	uint8_t **d = (uint8_t **) dst;
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (j = 0; j < n_samples; j++) {
-		for (i = 0; i < n_channels; i++) {
-			write_s24(&d[i][j*3], read_s24(s));
-			s += 3;
-		}
-	}
-}
-
-void
-conv_deinterleave_32_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	const uint32_t *s = src[0];
-	uint32_t **d = (uint32_t **) dst;
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (j = 0; j < n_samples; j++) {
-		for (i = 0; i < n_channels; i++)
-			d[i][j] = *s++;
-	}
-}
-
-void
-conv_deinterleave_32s_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	const uint32_t *s = src[0];
-	uint32_t **d = (uint32_t **) dst;
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (j = 0; j < n_samples; j++) {
-		for (i = 0; i < n_channels; i++)
-			d[i][j] = bswap_32(*s++);
-	}
-}
-
-void
-conv_deinterleave_64_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	const uint64_t *s = src[0];
-	uint64_t **d = (uint64_t **) dst;
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (j = 0; j < n_samples; j++) {
-		for (i = 0; i < n_channels; i++)
-			d[i][j] = *s++;
-	}
-}
-
-void
-conv_interleave_8_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	const int8_t **s = (const int8_t **) src;
-	uint8_t *d = dst[0];
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (j = 0; j < n_samples; j++) {
-		for (i = 0; i < n_channels; i++)
-			*d++ = s[i][j];
-	}
-}
-
-void
-conv_interleave_16_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	const int16_t **s = (const int16_t **) src;
-	uint16_t *d = dst[0];
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (j = 0; j < n_samples; j++) {
-		for (i = 0; i < n_channels; i++)
-			*d++ = s[i][j];
-	}
-}
-
-void
-conv_interleave_24_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	const int8_t **s = (const int8_t **) src;
-	uint8_t *d = dst[0];
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (j = 0; j < n_samples; j++) {
-		for (i = 0; i < n_channels; i++) {
-			write_s24(d, read_s24(&s[i][j*3]));
-			d += 3;
+#define MAKE_COPY(size)								\
+void conv_copy ##size## d_c(struct convert *conv,				\
+		void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],	\
+		uint32_t n_samples)						\
+{										\
+	uint32_t i, n_channels = conv->n_channels;				\
+	for (i = 0; i < n_channels; i++)					\
+		spa_memcpy(dst[i], src[i], n_samples * (size>>3));		\
+}										\
+void conv_copy ##size## _c(struct convert *conv,				\
+		void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],	\
+		uint32_t n_samples)						\
+{										\
+	spa_memcpy(dst[0], src[0], n_samples * conv->n_channels * (size>>3));	\
+}
+
+MAKE_COPY(8);
+MAKE_COPY(16);
+MAKE_COPY(24);
+MAKE_COPY(32);
+MAKE_COPY(64);
+
+#define MAKE_D_TO_D(sname,stype,dname,dtype,func)				\
+void conv_ ##sname## d_to_ ##dname## d_c(struct convert *conv,			\
+		void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],	\
+                uint32_t n_samples)						\
+{										\
+	uint32_t i, j, n_channels = conv->n_channels;				\
+	for (i = 0; i < n_channels; i++) {					\
+		const stype *s = src[i];					\
+		dtype *d = dst[i];						\
+		for (j = 0; j < n_samples; j++)					\
+			d[j] = func (s[j]);					\
+	}									\
+}
+
+#define MAKE_I_TO_I(sname,stype,dname,dtype,func)				\
+void conv_ ##sname## _to_ ##dname## _c(struct convert *conv,			\
+		void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],	\
+                uint32_t n_samples)						\
+{										\
+	uint32_t j;								\
+	const stype *s = src[0];						\
+	dtype *d = dst[0];							\
+	n_samples *= conv->n_channels;						\
+	for (j = 0; j < n_samples; j++)						\
+		d[j] = func (s[j]);						\
+}
+
+#define MAKE_I_TO_D(sname,stype,dname,dtype,func)				\
+void conv_ ##sname## _to_ ##dname## d_c(struct convert *conv,			\
+		void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],	\
+                uint32_t n_samples)						\
+{										\
+	const stype *s = src[0];						\
+	dtype **d = (dtype**)dst;						\
+	uint32_t i, j, n_channels = conv->n_channels;				\
+	for (j = 0; j < n_samples; j++) {					\
+		for (i = 0; i < n_channels; i++)				\
+			d[i][j] = func (*s++);					\
+	}									\
+}
+
+#define MAKE_D_TO_I(sname,stype,dname,dtype,func)				\
+void conv_ ##sname## d_to_ ##dname## _c(struct convert *conv,			\
+		void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],	\
+                uint32_t n_samples)						\
+{										\
+	const stype **s = (const stype **)src;					\
+	dtype *d = dst[0];							\
+	uint32_t i, j, n_channels = conv->n_channels;				\
+	for (j = 0; j < n_samples; j++) {					\
+		for (i = 0; i < n_channels; i++)				\
+			*d++ = func (s[i][j]);					\
+	}									\
+}
+
+/* to f32 */
+MAKE_D_TO_D(u8, uint8_t, f32, float, U8_TO_F32);
+MAKE_I_TO_I(u8, uint8_t, f32, float, U8_TO_F32);
+MAKE_I_TO_D(u8, uint8_t, f32, float, U8_TO_F32);
+MAKE_D_TO_I(u8, uint8_t, f32, float, U8_TO_F32);
+
+MAKE_D_TO_D(s8, int8_t, f32, float, S8_TO_F32);
+MAKE_I_TO_I(s8, int8_t, f32, float, S8_TO_F32);
+MAKE_I_TO_D(s8, int8_t, f32, float, S8_TO_F32);
+MAKE_D_TO_I(s8, int8_t, f32, float, S8_TO_F32);
+
+MAKE_I_TO_D(alaw, uint8_t, f32, float, alaw_to_f32);
+MAKE_I_TO_D(ulaw, uint8_t, f32, float, ulaw_to_f32);
+
+MAKE_I_TO_I(u16, uint16_t, f32, float, U16_TO_F32);
+MAKE_I_TO_D(u16, uint16_t, f32, float, U16_TO_F32);
+
+MAKE_D_TO_D(s16, int16_t, f32, float, S16_TO_F32);
+MAKE_I_TO_I(s16, int16_t, f32, float, S16_TO_F32);
+MAKE_I_TO_D(s16, int16_t, f32, float, S16_TO_F32);
+MAKE_D_TO_I(s16, int16_t, f32, float, S16_TO_F32);
+MAKE_I_TO_D(s16s, uint16_t, f32, float, S16S_TO_F32);
+
+MAKE_I_TO_I(u32, uint32_t, f32, float, U32_TO_F32);
+MAKE_I_TO_D(u32, uint32_t, f32, float, U32_TO_F32);
+
+MAKE_D_TO_D(s32, int32_t, f32, float, S32_TO_F32);
+MAKE_I_TO_I(s32, int32_t, f32, float, S32_TO_F32);
+MAKE_I_TO_D(s32, int32_t, f32, float, S32_TO_F32);
+MAKE_D_TO_I(s32, int32_t, f32, float, S32_TO_F32);
+MAKE_I_TO_D(s32s, uint32_t, f32, float, S32S_TO_F32);
+
+MAKE_I_TO_I(u24, uint24_t, f32, float, U24_TO_F32);
+MAKE_I_TO_D(u24, uint24_t, f32, float, U24_TO_F32);
+
+MAKE_D_TO_D(s24, int24_t, f32, float, S24_TO_F32);
+MAKE_I_TO_I(s24, int24_t, f32, float, S24_TO_F32);
+MAKE_I_TO_D(s24, int24_t, f32, float, S24_TO_F32);
+MAKE_D_TO_I(s24, int24_t, f32, float, S24_TO_F32);
+MAKE_I_TO_D(s24s, int24_t, f32, float, S24S_TO_F32);
+
+MAKE_I_TO_I(u24_32, uint32_t, f32, float, U24_32_TO_F32);
+MAKE_I_TO_D(u24_32, uint32_t, f32, float, U24_32_TO_F32);
+
+MAKE_D_TO_D(s24_32, int32_t, f32, float, S24_32_TO_F32);
+MAKE_I_TO_I(s24_32, int32_t, f32, float, S24_32_TO_F32);
+MAKE_I_TO_D(s24_32, int32_t, f32, float, S24_32_TO_F32);
+MAKE_D_TO_I(s24_32, int32_t, f32, float, S24_32_TO_F32);
+MAKE_I_TO_D(s24_32s, uint32_t, f32, float, S24_32S_TO_F32);
+
+MAKE_D_TO_D(f64, double, f32, float, (float));
+MAKE_I_TO_I(f64, double, f32, float, (float));
+MAKE_I_TO_D(f64, double, f32, float, (float));
+MAKE_D_TO_I(f64, double, f32, float, (float));
+MAKE_I_TO_D(f64s, uint64_t, f32, float, (float)F64S_TO_F64);
+
+/* from f32 */
+MAKE_D_TO_D(f32, float, u8, uint8_t, F32_TO_U8);
+MAKE_I_TO_I(f32, float, u8, uint8_t, F32_TO_U8);
+MAKE_I_TO_D(f32, float, u8, uint8_t, F32_TO_U8);
+MAKE_D_TO_I(f32, float, u8, uint8_t, F32_TO_U8);
+
+MAKE_D_TO_D(f32, float, s8, int8_t, F32_TO_S8);
+MAKE_I_TO_I(f32, float, s8, int8_t, F32_TO_S8);
+MAKE_I_TO_D(f32, float, s8, int8_t, F32_TO_S8);
+MAKE_D_TO_I(f32, float, s8, int8_t, F32_TO_S8);
+
+MAKE_D_TO_I(f32, float, alaw, uint8_t, f32_to_alaw);
+MAKE_D_TO_I(f32, float, ulaw, uint8_t, f32_to_ulaw);
+
+MAKE_I_TO_I(f32, float, u16, uint16_t, F32_TO_U16);
+MAKE_D_TO_I(f32, float, u16, uint16_t, F32_TO_U16);
+
+MAKE_D_TO_D(f32, float, s16, int16_t, F32_TO_S16);
+MAKE_I_TO_I(f32, float, s16, int16_t, F32_TO_S16);
+MAKE_I_TO_D(f32, float, s16, int16_t, F32_TO_S16);
+MAKE_D_TO_I(f32, float, s16, int16_t, F32_TO_S16);
+MAKE_D_TO_I(f32, float, s16s, uint16_t, F32_TO_S16S);
+
+MAKE_I_TO_I(f32, float, u32, uint32_t, F32_TO_U32);
+MAKE_D_TO_I(f32, float, u32, uint32_t, F32_TO_U32);
+
+MAKE_D_TO_D(f32, float, s32, int32_t, F32_TO_S32);
+MAKE_I_TO_I(f32, float, s32, int32_t, F32_TO_S32);
+MAKE_I_TO_D(f32, float, s32, int32_t, F32_TO_S32);
+MAKE_D_TO_I(f32, float, s32, int32_t, F32_TO_S32);
+MAKE_D_TO_I(f32, float, s32s, uint32_t, F32_TO_S32S);
+
+MAKE_I_TO_I(f32, float, u24, uint24_t, F32_TO_U24);
+MAKE_D_TO_I(f32, float, u24, uint24_t, F32_TO_U24);
+
+MAKE_D_TO_D(f32, float, s24, int24_t, F32_TO_S24);
+MAKE_I_TO_I(f32, float, s24, int24_t, F32_TO_S24);
+MAKE_I_TO_D(f32, float, s24, int24_t, F32_TO_S24);
+MAKE_D_TO_I(f32, float, s24, int24_t, F32_TO_S24);
+MAKE_D_TO_I(f32, float, s24s, int24_t, F32_TO_S24S);
+
+MAKE_I_TO_I(f32, float, u24_32, uint32_t, F32_TO_U24_32);
+MAKE_D_TO_I(f32, float, u24_32, uint32_t, F32_TO_U24_32);
+
+MAKE_D_TO_D(f32, float, s24_32, int32_t, F32_TO_S24_32);
+MAKE_I_TO_I(f32, float, s24_32, int32_t, F32_TO_S24_32);
+MAKE_I_TO_D(f32, float, s24_32, int32_t, F32_TO_S24_32);
+MAKE_D_TO_I(f32, float, s24_32, int32_t, F32_TO_S24_32);
+MAKE_D_TO_I(f32, float, s24_32s, uint32_t, F32_TO_S24_32S);
+
+MAKE_D_TO_D(f32, float, f64, double, (double));
+MAKE_I_TO_I(f32, float, f64, double, (double));
+MAKE_I_TO_D(f32, float, f64, double, (double));
+MAKE_D_TO_I(f32, float, f64, double, (double));
+MAKE_D_TO_I(f32, float, f64s, uint64_t, F64_TO_F64S);
+
+
+static inline int32_t
+lcnoise(uint32_t *state)
+{
+        *state = (*state * 96314165) + 907633515;
+        return (int32_t)(*state);
+}
+
+static inline void update_noise_c(struct convert *conv, uint32_t n_samples)
+{
+	uint32_t n;
+	float *noise = conv->noise, scale = conv->scale;
+	uint32_t *state = &conv->random[0];
+	int32_t *prev = &conv->prev[0], old, new;
+
+	switch (conv->noise_method) {
+	case NOISE_METHOD_RECTANGULAR:
+		for (n = 0; n < n_samples; n++)
+			noise[n] = lcnoise(state) * scale;
+		break;
+	case NOISE_METHOD_TRIANGULAR:
+		for (n = 0; n < n_samples; n++)
+			noise[n] = (lcnoise(state) - lcnoise(state)) * scale;
+		break;
+	case NOISE_METHOD_TRIANGULAR_HF:
+		old = *prev;
+		for (n = 0; n < n_samples; n++) {
+			new = lcnoise(state);
+			noise[n] = (new - old) * scale;
+			old = new;
 		}
-	}
-}
-
-void
-conv_interleave_32_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	const int32_t **s = (const int32_t **) src;
-	uint32_t *d = dst[0];
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (j = 0; j < n_samples; j++) {
-		for (i = 0; i < n_channels; i++)
-			*d++ = s[i][j];
-	}
-}
-
-void
-conv_interleave_32s_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	const int32_t **s = (const int32_t **) src;
-	uint32_t *d = dst[0];
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (j = 0; j < n_samples; j++) {
-		for (i = 0; i < n_channels; i++)
-			*d++ = bswap_32(s[i][j]);
-	}
-}
-
-void
-conv_interleave_64_c(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
-		uint32_t n_samples)
-{
-	const int64_t **s = (const int64_t **) src;
-	uint64_t *d = dst[0];
-	uint32_t i, j, n_channels = conv->n_channels;
-
-	for (j = 0; j < n_samples; j++) {
-		for (i = 0; i < n_channels; i++)
-			*d++ = s[i][j];
-	}
-}
+		*prev = old;
+		break;
+	case NOISE_METHOD_PATTERN:
+		old = *prev;
+		for (n = 0; n < n_samples; n++)
+			noise[n] = conv->scale * (1-((old++>>10)&1));
+		*prev = old;
+		break;
+	}
+}
+
+#define MAKE_D_noise(dname,dtype,func)						\
+void conv_f32d_to_ ##dname## d_noise_c(struct convert *conv,			\
+		void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],	\
+                uint32_t n_samples)						\
+{										\
+	uint32_t i, j, k, chunk, n_channels = conv->n_channels, noise_size = conv->noise_size;	\
+	float *noise = conv->noise;						\
+	update_noise_c(conv, SPA_MIN(n_samples, noise_size));			\
+	for (i = 0; i < n_channels; i++) {					\
+		const float *s = src[i];					\
+		dtype *d = dst[i];						\
+		for (j = 0; j < n_samples;) {					\
+			chunk = SPA_MIN(n_samples - j, noise_size);		\
+			for (k = 0; k < chunk; k++, j++)			\
+				d[j] = func (s[j], noise[k]);			\
+		}								\
+	}									\
+}
+
+#define MAKE_I_noise(dname,dtype,func)						\
+void conv_f32d_to_ ##dname## _noise_c(struct convert *conv,			\
+		void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],	\
+                uint32_t n_samples)						\
+{										\
+	const float **s = (const float **) src;					\
+	dtype *d = dst[0];							\
+	uint32_t i, j, k, chunk, n_channels = conv->n_channels, noise_size = conv->noise_size;	\
+	float *noise = conv->noise;						\
+	update_noise_c(conv, SPA_MIN(n_samples, noise_size));			\
+	for (j = 0; j < n_samples;) {						\
+		chunk = SPA_MIN(n_samples - j, noise_size);			\
+		for (k = 0; k < chunk; k++, j++) {				\
+			for (i = 0; i < n_channels; i++)			\
+				*d++ = func (s[i][j], noise[k]);		\
+		}								\
+	}									\
+}
+
+MAKE_D_noise(u8, uint8_t, F32_TO_U8_D);
+MAKE_I_noise(u8, uint8_t, F32_TO_U8_D);
+MAKE_D_noise(s8, int8_t, F32_TO_S8_D);
+MAKE_I_noise(s8, int8_t, F32_TO_S8_D);
+MAKE_D_noise(s16, int16_t, F32_TO_S16_D);
+MAKE_I_noise(s16, int16_t, F32_TO_S16_D);
+MAKE_I_noise(s16s, uint16_t, F32_TO_S16S_D);
+MAKE_D_noise(s32, int32_t, F32_TO_S32_D);
+MAKE_I_noise(s32, int32_t, F32_TO_S32_D);
+MAKE_I_noise(s32s, uint32_t, F32_TO_S32S_D);
+MAKE_D_noise(s24, int24_t, F32_TO_S24_D);
+MAKE_I_noise(s24, int24_t, F32_TO_S24_D);
+MAKE_I_noise(s24s, int24_t, F32_TO_S24_D);
+MAKE_D_noise(s24_32, int32_t, F32_TO_S24_32_D);
+MAKE_I_noise(s24_32, int32_t, F32_TO_S24_32_D);
+MAKE_I_noise(s24_32s, int32_t, F32_TO_S24_32S_D);
+
+#define SHAPER(type,s,scale,offs,sh,min,max,d)			\
+({								\
+	type t;							\
+	float v = s * scale + offs;				\
+	for (n = 0; n < n_ns; n++)				\
+		v += sh->e[idx + n] * ns[n];			\
+	t = FTOI(type, v, 1.0f, 0.0f, d, min, max);		\
+	idx = (idx - 1) & NS_MASK;				\
+	sh->e[idx] = sh->e[idx + NS_MAX] = v - t;		\
+	t;							\
+})
+
+#define F32_TO_U8_SH(s,sh,d)	SHAPER(uint8_t, s, U8_SCALE, U8_OFFS, sh, U8_MIN, U8_MAX, d)
+#define F32_TO_S8_SH(s,sh,d)	SHAPER(int8_t, s, S8_SCALE, 0, sh, S8_MIN, S8_MAX, d)
+#define F32_TO_S16_SH(s,sh,d)	SHAPER(int16_t, s, S16_SCALE, 0, sh, S16_MIN, S16_MAX, d)
+#define F32_TO_S16S_SH(s,sh,d)	bswap_16(F32_TO_S16_SH(s,sh,d))
+
+#define MAKE_D_shaped(dname,dtype,func)						\
+void conv_f32d_to_ ##dname## d_shaped_c(struct convert *conv,			\
+		void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],	\
+                uint32_t n_samples)						\
+{										\
+	uint32_t i, j, k, chunk, n_channels = conv->n_channels, noise_size = conv->noise_size;	\
+	const float *noise = conv->noise, *ns = conv->ns;			\
+	uint32_t n, n_ns = conv->n_ns;						\
+	update_noise_c(conv, SPA_MIN(n_samples, noise_size));			\
+	for (i = 0; i < n_channels; i++) {					\
+		const float *s = src[i];					\
+		dtype *d = dst[i];						\
+		struct shaper *sh = &conv->shaper[i];				\
+		uint32_t idx = sh->idx;						\
+		for (j = 0; j < n_samples;) {					\
+			chunk = SPA_MIN(n_samples - j, noise_size);		\
+			for (k = 0; k < chunk; k++, j++)			\
+				d[j] = func (s[j], sh, noise[k]);		\
+		}								\
+		sh->idx = idx;							\
+	}									\
+}
+
+#define MAKE_I_shaped(dname,dtype,func)						\
+void conv_f32d_to_ ##dname## _shaped_c(struct convert *conv,			\
+		void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],	\
+                uint32_t n_samples)						\
+{										\
+	dtype *d0 = dst[0];							\
+	uint32_t i, j, k, chunk, n_channels = conv->n_channels, noise_size = conv->noise_size;	\
+	const float *noise = conv->noise, *ns = conv->ns;			\
+	uint32_t n, n_ns = conv->n_ns;						\
+	update_noise_c(conv, SPA_MIN(n_samples, noise_size));			\
+	for (i = 0; i < n_channels; i++) {					\
+		const float *s = src[i];					\
+		dtype *d = &d0[i];						\
+		struct shaper *sh = &conv->shaper[i];				\
+		uint32_t idx = sh->idx;						\
+		for (j = 0; j < n_samples;) {					\
+			chunk = SPA_MIN(n_samples - j, noise_size);		\
+			for (k = 0; k < chunk; k++, j++)			\
+				d[j*n_channels] = func (s[j], sh, noise[k]);	\
+		}								\
+		sh->idx = idx;							\
+	}									\
+}
+
+MAKE_D_shaped(u8, uint8_t, F32_TO_U8_SH);
+MAKE_I_shaped(u8, uint8_t, F32_TO_U8_SH);
+MAKE_D_shaped(s8, int8_t, F32_TO_S8_SH);
+MAKE_I_shaped(s8, int8_t, F32_TO_S8_SH);
+MAKE_D_shaped(s16, int16_t, F32_TO_S16_SH);
+MAKE_I_shaped(s16, int16_t, F32_TO_S16_SH);
+MAKE_I_shaped(s16s, uint16_t, F32_TO_S16S_SH);
+
+#define MAKE_DEINTERLEAVE(size1,size2, type,func)					\
+	MAKE_I_TO_D(size1,type,size2,type,func)
+
+MAKE_DEINTERLEAVE(8, 8, uint8_t, (uint8_t));
+MAKE_DEINTERLEAVE(16, 16, uint16_t, (uint16_t));
+MAKE_DEINTERLEAVE(24, 24, uint24_t, (uint24_t));
+MAKE_DEINTERLEAVE(32, 32, uint32_t, (uint32_t));
+MAKE_DEINTERLEAVE(32s, 32, uint32_t, bswap_32);
+MAKE_DEINTERLEAVE(64, 64, uint64_t, (uint64_t));
+
+#define MAKE_INTERLEAVE(size1,size2,type,func)						\
+	MAKE_D_TO_I(size1,type,size2,type,func)
+
+MAKE_INTERLEAVE(8, 8, uint8_t, (uint8_t));
+MAKE_INTERLEAVE(16, 16, uint16_t, (uint16_t));
+MAKE_INTERLEAVE(24, 24, uint24_t, (uint24_t));
+MAKE_INTERLEAVE(32, 32, uint32_t, (uint32_t));
+MAKE_INTERLEAVE(32, 32s, uint32_t, bswap_32);
+MAKE_INTERLEAVE(64, 64, uint64_t, (uint64_t));
diff --git a/spa/plugins/audioconvert/fmt-ops-neon.c b/spa/plugins/audioconvert/fmt-ops-neon.c
index 56c6b628b3de159065e1878759ef35015caadb9d..e6c8b844bb4cce84d1a58bb5aff0354b36e7a5ef 100644
--- a/spa/plugins/audioconvert/fmt-ops-neon.c
+++ b/spa/plugins/audioconvert/fmt-ops-neon.c
@@ -26,6 +26,8 @@
 #include <stdio.h>
 #include <math.h>
 
+#include <arm_neon.h>
+
 #include "fmt-ops.h"
 
 void
@@ -289,16 +291,19 @@ conv_f32d_to_s16_2s_neon(void *data, void * SPA_RESTRICT dst, const void * SPA_R
 
 #ifdef __aarch64__
 	asm volatile(
+		"      dup v2.4s, %w[scale]\n"
 		"      cmp %[n_samples], #0\n"
 		"      beq 2f\n"
 		"1:"
 		"      ld1 { v0.4s }, [%[s0]], #16\n"
 		"      ld1 { v1.4s }, [%[s1]], #16\n"
 		"      subs %[n_samples], %[n_samples], #4\n"
-		"      fcvtzs v0.4s, v0.4s, #31\n"
-		"      fcvtzs v1.4s, v1.4s, #31\n"
-		"      sqrshrn v0.4h, v0.4s, #16\n"
-		"      sqrshrn v1.4h, v1.4s, #16\n"
+		"      sqadd  v0.4s, v0.4s, v2.4s\n"
+		"      sqadd  v1.4s, v1.4s, v2.4s\n"
+		"      fcvtns v0.4s, v0.4s\n"
+		"      fcvtns v1.4s, v1.4s\n"
+		"      sqxtn  v0.4h, v0.4s\n"
+		"      sqxtn  v1.4h, v1.4s\n"
 		"      st2 { v0.h, v1.h }[0], [%[d]], %[stride]\n"
 		"      st2 { v0.h, v1.h }[1], [%[d]], %[stride]\n"
 		"      st2 { v0.h, v1.h }[2], [%[d]], %[stride]\n"
@@ -311,29 +316,42 @@ conv_f32d_to_s16_2s_neon(void *data, void * SPA_RESTRICT dst, const void * SPA_R
 		"      ld1 { v0.s }[0], [%[s0]], #4\n"
 		"      ld1 { v2.s }[0], [%[s1]], #4\n"
 		"      subs %[remainder], %[remainder], #1\n"
-		"      fcvtzs v0.4s, v0.4s, #31\n"
-		"      fcvtzs v1.4s, v1.4s, #31\n"
-		"      sqrshrn v0.4h, v0.4s, #16\n"
-		"      sqrshrn v1.4h, v1.4s, #16\n"
+		"      sqadd  v0.4s, v0.4s, v2.4s\n"
+		"      sqadd  v1.4s, v1.4s, v2.4s\n"
+		"      fcvtns v0.4s, v0.4s\n"
+		"      fcvtns v1.4s, v1.4s\n"
+		"      sqxtn  v0.4h, v0.4s\n"
+		"      sqxtn  v1.4h, v1.4s\n"
 		"      st2 { v0.h, v1.h }[0], [%[d]], %[stride]\n"
 		"      bne 3b\n"
 		"4:"
 		: [d] "+r" (d), [s0] "+r" (s0), [s1] "+r" (s1), [n_samples] "+r" (n_samples),
 		  [remainder] "+r" (remainder)
-		: [stride] "r" (stride)
+		: [stride] "r" (stride),
+		  [scale] "r" (15 << 23)
 		: "cc", "v0", "v1");
 #else
+	float32x4_t pos = vdupq_n_f32(0.4999999f / S16_SCALE);
+	float32x4_t neg = vdupq_n_f32(-0.4999999f / S16_SCALE);
+
 	asm volatile(
+		"      veor q2, q2, q2\n"
 		"      cmp %[n_samples], #0\n"
 		"      beq 2f\n"
 		"1:"
 		"      vld1.32 { q0 }, [%[s0]]!\n"
 		"      vld1.32 { q1 }, [%[s1]]!\n"
 		"      subs %[n_samples], %[n_samples], #4\n"
-		"      vcvt.s32.f32 q0, q0, #31\n"
-		"      vcvt.s32.f32 q1, q1, #31\n"
-		"      vqrshrn.s32 d0, q0, #16\n"
-		"      vqrshrn.s32 d1, q1, #16\n"
+		"      vcgt.f32 q3, q0, q2\n"
+		"      vcgt.f32 q4, q0, q2\n"
+		"      vbsl q3, %q[pos], %q[neg]\n"
+		"      vbsl q4, %q[pos], %q[neg]\n"
+		"      vadd.f32 q0, q0, q3\n"
+		"      vadd.f32 q1, q1, q4\n"
+		"      vcvt.s32.f32 q0, q0, #15\n"
+		"      vcvt.s32.f32 q1, q1, #15\n"
+		"      vqmovn.s32 d0, q0\n"
+		"      vqmovn.s32 d1, q1\n"
 		"      vst2.16 { d0[0], d1[0] }, [%[d]], %[stride]\n"
 		"      vst2.16 { d0[1], d1[1] }, [%[d]], %[stride]\n"
 		"      vst2.16 { d0[2], d1[2] }, [%[d]], %[stride]\n"
@@ -346,17 +364,25 @@ conv_f32d_to_s16_2s_neon(void *data, void * SPA_RESTRICT dst, const void * SPA_R
 		"      vld1.32 { d0[0] }, [%[s0]]!\n"
 		"      vld1.32 { d2[0] }, [%[s1]]!\n"
 		"      subs %[remainder], %[remainder], #1\n"
-		"      vcvt.s32.f32 q0, q0, #31\n"
-		"      vcvt.s32.f32 q1, q1, #31\n"
-		"      vqrshrn.s32 d0, q0, #16\n"
-		"      vqrshrn.s32 d1, q1, #16\n"
+		"      vcgt.f32 q3, q0, q2\n"
+		"      vcgt.f32 q4, q0, q2\n"
+		"      vbsl q3, %q[pos], %q[neg]\n"
+		"      vbsl q4, %q[pos], %q[neg]\n"
+		"      vadd.f32 q0, q0, q3\n"
+		"      vadd.f32 q1, q1, q4\n"
+		"      vcvt.s32.f32 q0, q0, #15\n"
+		"      vcvt.s32.f32 q1, q1, #15\n"
+		"      vqmovn.s32 d0, q0\n"
+		"      vqmovn.s32 d1, q1\n"
 		"      vst2.16 { d0[0], d1[0] }, [%[d]], %[stride]\n"
 		"      bne 3b\n"
 		"4:"
 		: [d] "+r" (d), [s0] "+r" (s0), [s1] "+r" (s1), [n_samples] "+r" (n_samples),
 		  [remainder] "+r" (remainder)
-		: [stride] "r" (stride)
-		: "cc", "q0", "q1");
+		: [stride] "r" (stride),
+		  [pos]"w"(pos),
+		  [neg]"w"(neg)
+		: "cc", "q0", "q1", "q2", "q3", "q4");
 #endif
 }
 
@@ -372,13 +398,15 @@ conv_f32d_to_s16_1s_neon(void *data, void * SPA_RESTRICT dst, const void * SPA_R
 
 #ifdef __aarch64__
 	asm volatile(
+		"      dup v2.4s, %w[scale]\n"
 		"      cmp %[n_samples], #0\n"
 		"      beq 2f\n"
 		"1:"
 		"      ld1 { v0.4s }, [%[s]], #16\n"
 		"      subs %[n_samples], %[n_samples], #4\n"
-		"      fcvtzs v0.4s, v0.4s, #31\n"
-		"      sqrshrn v0.4h, v0.4s, #16\n"
+		"      sqadd  v0.4s, v0.4s, v2.4s\n"
+		"      fcvtns v0.4s, v0.4s\n"
+		"      sqxtn  v0.4h, v0.4s\n"
 		"      st1 { v0.h }[0], [%[d]], %[stride]\n"
 		"      st1 { v0.h }[1], [%[d]], %[stride]\n"
 		"      st1 { v0.h }[2], [%[d]], %[stride]\n"
@@ -390,24 +418,33 @@ conv_f32d_to_s16_1s_neon(void *data, void * SPA_RESTRICT dst, const void * SPA_R
 		"3:"
 		"      ld1 { v0.s }[0], [%[s]], #4\n"
 		"      subs %[remainder], %[remainder], #1\n"
-		"      fcvtzs v0.4s, v0.4s, #31\n"
-		"      sqrshrn v0.4h, v0.4s, #16\n"
+		"      sqadd  v0.4s, v0.4s, v2.4s\n"
+		"      fcvtns v0.4s, v0.4s\n"
+		"      sqxtn  v0.4h, v0.4s\n"
 		"      st1 { v0.h }[0], [%[d]], %[stride]\n"
 		"      bne 3b\n"
 		"4:"
 		: [d] "+r" (d), [s] "+r" (s), [n_samples] "+r" (n_samples),
 		  [remainder] "+r" (remainder)
-		: [stride] "r" (stride)
+		: [stride] "r" (stride),
+		  [scale] "r" (15 << 23)
 		: "cc", "v0");
 #else
+	float32x4_t pos = vdupq_n_f32(0.4999999f / S16_SCALE);
+	float32x4_t neg = vdupq_n_f32(-0.4999999f / S16_SCALE);
+
 	asm volatile(
+		"      veor q1, q1, q1\n"
 		"      cmp %[n_samples], #0\n"
 		"      beq 2f\n"
 		"1:"
 		"      vld1.32 { q0 }, [%[s]]!\n"
 		"      subs %[n_samples], %[n_samples], #4\n"
-		"      vcvt.s32.f32 q0, q0, #31\n"
-		"      vqrshrn.s32 d0, q0, #16\n"
+		"      vcgt.f32 q2, q0, q1\n"
+		"      vbsl q2, %q[pos], %q[neg]\n"
+		"      vadd.f32 q0, q0, q2\n"
+		"      vcvt.s32.f32 q0, q0, #15\n"
+		"      vqmovn.s32 d0, q0\n"
 		"      vst1.16 { d0[0] }, [%[d]], %[stride]\n"
 		"      vst1.16 { d0[1] }, [%[d]], %[stride]\n"
 		"      vst1.16 { d0[2] }, [%[d]], %[stride]\n"
@@ -419,15 +456,20 @@ conv_f32d_to_s16_1s_neon(void *data, void * SPA_RESTRICT dst, const void * SPA_R
 		"3:"
 		"      vld1.32 { d0[0] }, [%[s]]!\n"
 		"      subs %[remainder], %[remainder], #1\n"
-		"      vcvt.s32.f32 q0, q0, #31\n"
-		"      vqrshrn.s32 d0, q0, #16\n"
+		"      vcgt.f32 q2, q0, q1\n"
+		"      vbsl q2, %q[pos], %q[neg]\n"
+		"      vadd.f32 q0, q0, q2\n"
+		"      vcvt.s32.f32 q0, q0, #15\n"
+		"      vqmovn.s32 d0, q0\n"
 		"      vst1.16 { d0[0] }, [%[d]], %[stride]\n"
 		"      bne 3b\n"
 		"4:"
 		: [d] "+r" (d), [s] "+r" (s), [n_samples] "+r" (n_samples),
 		  [remainder] "+r" (remainder)
-		: [stride] "r" (stride)
-		: "cc", "q0");
+		: [stride] "r" (stride),
+		  [pos]"w"(pos),
+		  [neg]"w"(neg)
+		: "cc", "q0", "q1", "q2");
 #endif
 }
 
diff --git a/spa/plugins/audioconvert/fmt-ops-sse2.c b/spa/plugins/audioconvert/fmt-ops-sse2.c
index 81524d2fd2e8501a44debbe623b5582f4acb0fde..917bfa0dc80c0a36e68d0b9b5179913d846c5ff4 100644
--- a/spa/plugins/audioconvert/fmt-ops-sse2.c
+++ b/spa/plugins/audioconvert/fmt-ops-sse2.c
@@ -26,6 +26,12 @@
 
 #include <emmintrin.h>
 
+#define _MM_CLAMP_PS(r,min,max)				\
+	_mm_min_ps(_mm_max_ps(r, min), max)
+
+#define _MM_CLAMP_SS(r,min,max)				\
+	_mm_min_ss(_mm_max_ss(r, min), max)
+
 static void
 conv_s16_to_f32d_1s_sse2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src,
 		uint32_t n_channels, uint32_t n_samples)
@@ -33,7 +39,7 @@ conv_s16_to_f32d_1s_sse2(void *data, void * SPA_RESTRICT dst[], const void * SPA
 	const int16_t *s = src;
 	float *d0 = dst[0];
 	uint32_t n, unrolled;
-	__m128i in;
+	__m128i in = _mm_setzero_si128();
 	__m128 out, factor = _mm_set1_ps(1.0f / S16_SCALE);
 
 	if (SPA_LIKELY(SPA_IS_ALIGNED(d0, 16)))
@@ -53,7 +59,7 @@ conv_s16_to_f32d_1s_sse2(void *data, void * SPA_RESTRICT dst[], const void * SPA
 		s += 4*n_channels;
 	}
 	for(; n < n_samples; n++) {
-		out = _mm_cvtsi32_ss(out, s[0]);
+		out = _mm_cvtsi32_ss(factor, s[0]);
 		out = _mm_mul_ss(out, factor);
 		_mm_store_ss(&d0[n], out);
 		s += n_channels;
@@ -118,9 +124,9 @@ conv_s16_to_f32d_2_sse2(struct convert *conv, void * SPA_RESTRICT dst[], const v
 		s += 16;
 	}
 	for(; n < n_samples; n++) {
-		out[0] = _mm_cvtsi32_ss(out[0], s[0]);
+		out[0] = _mm_cvtsi32_ss(factor, s[0]);
 		out[0] = _mm_mul_ss(out[0], factor);
-		out[1] = _mm_cvtsi32_ss(out[1], s[1]);
+		out[1] = _mm_cvtsi32_ss(factor, s[1]);
 		out[1] = _mm_mul_ss(out[1], factor);
 		_mm_store_ss(&d0[n], out[0]);
 		_mm_store_ss(&d1[n], out[1]);
@@ -132,7 +138,7 @@ void
 conv_s24_to_f32d_1s_sse2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src,
 		uint32_t n_channels, uint32_t n_samples)
 {
-	const uint8_t *s = src;
+	const int24_t *s = src;
 	float *d0 = dst[0];
 	uint32_t n, unrolled;
 	__m128i in;
@@ -149,21 +155,21 @@ conv_s24_to_f32d_1s_sse2(void *data, void * SPA_RESTRICT dst[], const void * SPA
 	for(n = 0; n < unrolled; n += 4) {
 		in = _mm_setr_epi32(
 			*((uint32_t*)&s[0 * n_channels]),
-			*((uint32_t*)&s[3 * n_channels]),
-			*((uint32_t*)&s[6 * n_channels]),
-			*((uint32_t*)&s[9 * n_channels]));
+			*((uint32_t*)&s[1 * n_channels]),
+			*((uint32_t*)&s[2 * n_channels]),
+			*((uint32_t*)&s[3 * n_channels]));
 		in = _mm_slli_epi32(in, 8);
 		in = _mm_srai_epi32(in, 8);
 		out = _mm_cvtepi32_ps(in);
 		out = _mm_mul_ps(out, factor);
 		_mm_store_ps(&d0[n], out);
-		s += 12 * n_channels;
+		s += 4 * n_channels;
 	}
 	for(; n < n_samples; n++) {
-		out = _mm_cvtsi32_ss(out, read_s24(s));
+		out = _mm_cvtsi32_ss(factor, s24_to_s32(*s));
 		out = _mm_mul_ss(out, factor);
 		_mm_store_ss(&d0[n], out);
-		s += 3 * n_channels;
+		s += n_channels;
 	}
 }
 
@@ -171,7 +177,7 @@ static void
 conv_s24_to_f32d_2s_sse2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src,
 		uint32_t n_channels, uint32_t n_samples)
 {
-	const uint8_t *s = src;
+	const int24_t *s = src;
 	float *d0 = dst[0], *d1 = dst[1];
 	uint32_t n, unrolled;
 	__m128i in[2];
@@ -190,14 +196,14 @@ conv_s24_to_f32d_2s_sse2(void *data, void * SPA_RESTRICT dst[], const void * SPA
 	for(n = 0; n < unrolled; n += 4) {
 		in[0] = _mm_setr_epi32(
 			*((uint32_t*)&s[0 + 0*n_channels]),
-			*((uint32_t*)&s[0 + 3*n_channels]),
-			*((uint32_t*)&s[0 + 6*n_channels]),
-			*((uint32_t*)&s[0 + 9*n_channels]));
+			*((uint32_t*)&s[0 + 1*n_channels]),
+			*((uint32_t*)&s[0 + 2*n_channels]),
+			*((uint32_t*)&s[0 + 3*n_channels]));
 		in[1] = _mm_setr_epi32(
-			*((uint32_t*)&s[3 + 0*n_channels]),
-			*((uint32_t*)&s[3 + 3*n_channels]),
-			*((uint32_t*)&s[3 + 6*n_channels]),
-			*((uint32_t*)&s[3 + 9*n_channels]));
+			*((uint32_t*)&s[1 + 0*n_channels]),
+			*((uint32_t*)&s[1 + 1*n_channels]),
+			*((uint32_t*)&s[1 + 2*n_channels]),
+			*((uint32_t*)&s[1 + 3*n_channels]));
 
 		in[0] = _mm_slli_epi32(in[0], 8);
 		in[1] = _mm_slli_epi32(in[1], 8);
@@ -214,23 +220,23 @@ conv_s24_to_f32d_2s_sse2(void *data, void * SPA_RESTRICT dst[], const void * SPA
 		_mm_store_ps(&d0[n], out[0]);
 		_mm_store_ps(&d1[n], out[1]);
 
-		s += 12 * n_channels;
+		s += 4 * n_channels;
 	}
 	for(; n < n_samples; n++) {
-		out[0] = _mm_cvtsi32_ss(out[0], read_s24(s));
-		out[1] = _mm_cvtsi32_ss(out[1], read_s24(s+3));
+		out[0] = _mm_cvtsi32_ss(factor, s24_to_s32(*s));
+		out[1] = _mm_cvtsi32_ss(factor, s24_to_s32(*(s+1)));
 		out[0] = _mm_mul_ss(out[0], factor);
 		out[1] = _mm_mul_ss(out[1], factor);
 		_mm_store_ss(&d0[n], out[0]);
 		_mm_store_ss(&d1[n], out[1]);
-		s += 3 * n_channels;
+		s += n_channels;
 	}
 }
 static void
 conv_s24_to_f32d_4s_sse2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src,
 		uint32_t n_channels, uint32_t n_samples)
 {
-	const uint8_t *s = src;
+	const int24_t *s = src;
 	float *d0 = dst[0], *d1 = dst[1], *d2 = dst[2], *d3 = dst[3];
 	uint32_t n, unrolled;
 	__m128i in[4];
@@ -251,24 +257,24 @@ conv_s24_to_f32d_4s_sse2(void *data, void * SPA_RESTRICT dst[], const void * SPA
 	for(n = 0; n < unrolled; n += 4) {
 		in[0] = _mm_setr_epi32(
 			*((uint32_t*)&s[0 + 0*n_channels]),
-			*((uint32_t*)&s[0 + 3*n_channels]),
-			*((uint32_t*)&s[0 + 6*n_channels]),
-			*((uint32_t*)&s[0 + 9*n_channels]));
+			*((uint32_t*)&s[0 + 1*n_channels]),
+			*((uint32_t*)&s[0 + 2*n_channels]),
+			*((uint32_t*)&s[0 + 3*n_channels]));
 		in[1] = _mm_setr_epi32(
-			*((uint32_t*)&s[3 + 0*n_channels]),
-			*((uint32_t*)&s[3 + 3*n_channels]),
-			*((uint32_t*)&s[3 + 6*n_channels]),
-			*((uint32_t*)&s[3 + 9*n_channels]));
+			*((uint32_t*)&s[1 + 0*n_channels]),
+			*((uint32_t*)&s[1 + 1*n_channels]),
+			*((uint32_t*)&s[1 + 2*n_channels]),
+			*((uint32_t*)&s[1 + 3*n_channels]));
 		in[2] = _mm_setr_epi32(
-			*((uint32_t*)&s[6 + 0*n_channels]),
-			*((uint32_t*)&s[6 + 3*n_channels]),
-			*((uint32_t*)&s[6 + 6*n_channels]),
-			*((uint32_t*)&s[6 + 9*n_channels]));
+			*((uint32_t*)&s[2 + 0*n_channels]),
+			*((uint32_t*)&s[2 + 1*n_channels]),
+			*((uint32_t*)&s[2 + 2*n_channels]),
+			*((uint32_t*)&s[2 + 3*n_channels]));
 		in[3] = _mm_setr_epi32(
-			*((uint32_t*)&s[9 + 0*n_channels]),
-			*((uint32_t*)&s[9 + 3*n_channels]),
-			*((uint32_t*)&s[9 + 6*n_channels]),
-			*((uint32_t*)&s[9 + 9*n_channels]));
+			*((uint32_t*)&s[3 + 0*n_channels]),
+			*((uint32_t*)&s[3 + 1*n_channels]),
+			*((uint32_t*)&s[3 + 2*n_channels]),
+			*((uint32_t*)&s[3 + 3*n_channels]));
 
 		in[0] = _mm_slli_epi32(in[0], 8);
 		in[1] = _mm_slli_epi32(in[1], 8);
@@ -295,13 +301,13 @@ conv_s24_to_f32d_4s_sse2(void *data, void * SPA_RESTRICT dst[], const void * SPA
 		_mm_store_ps(&d2[n], out[2]);
 		_mm_store_ps(&d3[n], out[3]);
 
-		s += 12 * n_channels;
+		s += 4 * n_channels;
 	}
 	for(; n < n_samples; n++) {
-		out[0] = _mm_cvtsi32_ss(out[0], read_s24(s));
-		out[1] = _mm_cvtsi32_ss(out[1], read_s24(s+3));
-		out[2] = _mm_cvtsi32_ss(out[2], read_s24(s+6));
-		out[3] = _mm_cvtsi32_ss(out[3], read_s24(s+9));
+		out[0] = _mm_cvtsi32_ss(factor, s24_to_s32(*s));
+		out[1] = _mm_cvtsi32_ss(factor, s24_to_s32(*(s+1)));
+		out[2] = _mm_cvtsi32_ss(factor, s24_to_s32(*(s+2)));
+		out[3] = _mm_cvtsi32_ss(factor, s24_to_s32(*(s+3)));
 		out[0] = _mm_mul_ss(out[0], factor);
 		out[1] = _mm_mul_ss(out[1], factor);
 		out[2] = _mm_mul_ss(out[2], factor);
@@ -310,7 +316,7 @@ conv_s24_to_f32d_4s_sse2(void *data, void * SPA_RESTRICT dst[], const void * SPA
 		_mm_store_ss(&d1[n], out[1]);
 		_mm_store_ss(&d2[n], out[2]);
 		_mm_store_ss(&d3[n], out[3]);
-		s += 3 * n_channels;
+		s += n_channels;
 	}
 }
 
@@ -357,7 +363,7 @@ conv_s32_to_f32d_1s_sse2(void *data, void * SPA_RESTRICT dst[], const void * SPA
 		s += 4*n_channels;
 	}
 	for(; n < n_samples; n++) {
-		out = _mm_cvtsi32_ss(out, s[0]>>8);
+		out = _mm_cvtsi32_ss(factor, s[0]>>8);
 		out = _mm_mul_ss(out, factor);
 		_mm_store_ss(&d0[n], out);
 		s += n_channels;
@@ -384,8 +390,9 @@ conv_f32d_to_s32_1s_sse2(void *data, void * SPA_RESTRICT dst, const void * SPA_R
 	uint32_t n, unrolled;
 	__m128 in[1];
 	__m128i out[4];
-	__m128 scale = _mm_set1_ps(S32_SCALE);
-	__m128 int_min = _mm_set1_ps(S32_MIN);
+	__m128 scale = _mm_set1_ps(S24_SCALE);
+	__m128 int_min = _mm_set1_ps(S24_MIN);
+	__m128 int_max = _mm_set1_ps(S24_MAX);
 
 	if (SPA_IS_ALIGNED(s0, 16))
 		unrolled = n_samples & ~3;
@@ -394,8 +401,9 @@ conv_f32d_to_s32_1s_sse2(void *data, void * SPA_RESTRICT dst, const void * SPA_R
 
 	for(n = 0; n < unrolled; n += 4) {
 		in[0] = _mm_mul_ps(_mm_load_ps(&s0[n]), scale);
-		in[0] = _mm_min_ps(in[0], int_min);
+		in[0] = _MM_CLAMP_PS(in[0], int_min, int_max);
 		out[0] = _mm_cvtps_epi32(in[0]);
+		out[0] = _mm_slli_epi32(out[0], 8);
 		out[1] = _mm_shuffle_epi32(out[0], _MM_SHUFFLE(0, 3, 2, 1));
 		out[2] = _mm_shuffle_epi32(out[0], _MM_SHUFFLE(1, 0, 3, 2));
 		out[3] = _mm_shuffle_epi32(out[0], _MM_SHUFFLE(2, 1, 0, 3));
@@ -409,8 +417,8 @@ conv_f32d_to_s32_1s_sse2(void *data, void * SPA_RESTRICT dst, const void * SPA_R
 	for(; n < n_samples; n++) {
 		in[0] = _mm_load_ss(&s0[n]);
 		in[0] = _mm_mul_ss(in[0], scale);
-		in[0] = _mm_min_ss(in[0], int_min);
-		*d = _mm_cvtss_si32(in[0]);
+		in[0] = _MM_CLAMP_SS(in[0], int_min, int_max);
+		*d = _mm_cvtss_si32(in[0]) << 8;
 		d += n_channels;
 	}
 }
@@ -424,8 +432,9 @@ conv_f32d_to_s32_2s_sse2(void *data, void * SPA_RESTRICT dst, const void * SPA_R
 	uint32_t n, unrolled;
 	__m128 in[2];
 	__m128i out[2], t[2];
-	__m128 scale = _mm_set1_ps(S32_SCALE);
-	__m128 int_min = _mm_set1_ps(S32_MIN);
+	__m128 scale = _mm_set1_ps(S24_SCALE);
+	__m128 int_min = _mm_set1_ps(S24_MIN);
+	__m128 int_max = _mm_set1_ps(S24_MAX);
 
 	if (SPA_IS_ALIGNED(s0, 16) &&
 	    SPA_IS_ALIGNED(s1, 16))
@@ -437,11 +446,13 @@ conv_f32d_to_s32_2s_sse2(void *data, void * SPA_RESTRICT dst, const void * SPA_R
 		in[0] = _mm_mul_ps(_mm_load_ps(&s0[n]), scale);
 		in[1] = _mm_mul_ps(_mm_load_ps(&s1[n]), scale);
 
-		in[0] = _mm_min_ps(in[0], int_min);
-		in[1] = _mm_min_ps(in[1], int_min);
+		in[0] = _MM_CLAMP_PS(in[0], int_min, int_max);
+		in[1] = _MM_CLAMP_PS(in[1], int_min, int_max);
 
 		out[0] = _mm_cvtps_epi32(in[0]);
 		out[1] = _mm_cvtps_epi32(in[1]);
+		out[0] = _mm_slli_epi32(out[0], 8);
+		out[1] = _mm_slli_epi32(out[1], 8);
 
 		t[0] = _mm_unpacklo_epi32(out[0], out[1]);
 		t[1] = _mm_unpackhi_epi32(out[0], out[1]);
@@ -459,8 +470,9 @@ conv_f32d_to_s32_2s_sse2(void *data, void * SPA_RESTRICT dst, const void * SPA_R
 		in[0] = _mm_unpacklo_ps(in[0], in[1]);
 
 		in[0] = _mm_mul_ps(in[0], scale);
-		in[0] = _mm_min_ps(in[0], int_min);
+		in[0] = _MM_CLAMP_PS(in[0], int_min, int_max);
 		out[0] = _mm_cvtps_epi32(in[0]);
+		out[0] = _mm_slli_epi32(out[0], 8);
 		_mm_storel_epi64((__m128i*)d, out[0]);
 		d += n_channels;
 	}
@@ -475,8 +487,9 @@ conv_f32d_to_s32_4s_sse2(void *data, void * SPA_RESTRICT dst, const void * SPA_R
 	uint32_t n, unrolled;
 	__m128 in[4];
 	__m128i out[4];
-	__m128 scale = _mm_set1_ps(S32_SCALE);
-	__m128 int_min = _mm_set1_ps(S32_MIN);
+	__m128 scale = _mm_set1_ps(S24_SCALE);
+	__m128 int_min = _mm_set1_ps(S24_MIN);
+	__m128 int_max = _mm_set1_ps(S24_MAX);
 
 	if (SPA_IS_ALIGNED(s0, 16) &&
 	    SPA_IS_ALIGNED(s1, 16) &&
@@ -492,10 +505,10 @@ conv_f32d_to_s32_4s_sse2(void *data, void * SPA_RESTRICT dst, const void * SPA_R
 		in[2] = _mm_mul_ps(_mm_load_ps(&s2[n]), scale);
 		in[3] = _mm_mul_ps(_mm_load_ps(&s3[n]), scale);
 
-		in[0] = _mm_min_ps(in[0], int_min);
-		in[1] = _mm_min_ps(in[1], int_min);
-		in[2] = _mm_min_ps(in[2], int_min);
-		in[3] = _mm_min_ps(in[3], int_min);
+		in[0] = _MM_CLAMP_PS(in[0], int_min, int_max);
+		in[1] = _MM_CLAMP_PS(in[1], int_min, int_max);
+		in[2] = _MM_CLAMP_PS(in[2], int_min, int_max);
+		in[3] = _MM_CLAMP_PS(in[3], int_min, int_max);
 
 		_MM_TRANSPOSE4_PS(in[0], in[1], in[2], in[3]);
 
@@ -503,6 +516,10 @@ conv_f32d_to_s32_4s_sse2(void *data, void * SPA_RESTRICT dst, const void * SPA_R
 		out[1] = _mm_cvtps_epi32(in[1]);
 		out[2] = _mm_cvtps_epi32(in[2]);
 		out[3] = _mm_cvtps_epi32(in[3]);
+		out[0] = _mm_slli_epi32(out[0], 8);
+		out[1] = _mm_slli_epi32(out[1], 8);
+		out[2] = _mm_slli_epi32(out[2], 8);
+		out[3] = _mm_slli_epi32(out[3], 8);
 
 		_mm_storeu_si128((__m128i*)(d + 0*n_channels), out[0]);
 		_mm_storeu_si128((__m128i*)(d + 1*n_channels), out[1]);
@@ -521,8 +538,9 @@ conv_f32d_to_s32_4s_sse2(void *data, void * SPA_RESTRICT dst, const void * SPA_R
 		in[0] = _mm_unpacklo_ps(in[0], in[1]);
 
 		in[0] = _mm_mul_ps(in[0], scale);
-		in[0] = _mm_min_ps(in[0], int_min);
+		in[0] = _MM_CLAMP_PS(in[0], int_min, int_max);
 		out[0] = _mm_cvtps_epi32(in[0]);
+		out[0] = _mm_slli_epi32(out[0], 8);
 		_mm_storeu_si128((__m128i*)d, out[0]);
 		d += n_channels;
 	}
@@ -543,6 +561,133 @@ conv_f32d_to_s32_sse2(struct convert *conv, void * SPA_RESTRICT dst[], const voi
 		conv_f32d_to_s32_1s_sse2(conv, &d[i], &src[i], n_channels, n_samples);
 }
 
+/* 32 bit xorshift PRNG, see https://en.wikipedia.org/wiki/Xorshift */
+#define _MM_XORSHIFT_EPI32(r)				\
+({							\
+	__m128i i, t;					\
+	i = _mm_load_si128((__m128i*)r);		\
+	t = _mm_slli_epi32(i, 13);			\
+	i = _mm_xor_si128(i, t);			\
+	t = _mm_srli_epi32(i, 17);			\
+	i = _mm_xor_si128(i, t);			\
+	t = _mm_slli_epi32(i, 5);			\
+	i = _mm_xor_si128(i, t);			\
+	_mm_store_si128((__m128i*)r, i);		\
+	i;						\
+})
+
+
+static inline void update_noise_sse2(struct convert *conv, uint32_t n_samples)
+{
+	uint32_t n;
+	const uint32_t *r = SPA_PTR_ALIGN(conv->random, 16, uint32_t);
+	int32_t *p = SPA_PTR_ALIGN(conv->prev, 16, int32_t), op;
+	__m128 scale = _mm_set1_ps(conv->scale);
+	__m128 out[1];
+	float *noise = SPA_PTR_ALIGN(conv->noise, 16, float);
+	__m128i in[1], old[1], new[1];
+
+	switch (conv->noise_method) {
+	case DITHER_METHOD_RECTANGULAR:
+		for (n = 0; n < n_samples; n += 4) {
+			in[0] = _MM_XORSHIFT_EPI32(r);
+			out[0] = _mm_cvtepi32_ps(_MM_XORSHIFT_EPI32(r));
+			out[0] = _mm_mul_ps(out[0], scale);
+			_mm_store_ps(&noise[n], out[0]);
+		}
+		break;
+	case DITHER_METHOD_TRIANGULAR:
+		for (n = 0; n < n_samples; n += 4) {
+			in[0] = _mm_sub_epi32( _MM_XORSHIFT_EPI32(r), _MM_XORSHIFT_EPI32(r));
+			out[0] = _mm_cvtepi32_ps(in[0]);
+			out[0] = _mm_mul_ps(out[0], scale);
+			_mm_store_ps(&noise[n], out[0]);
+		}
+		break;
+	case DITHER_METHOD_TRIANGULAR_HF:
+		old[0] = _mm_load_si128((__m128i*)p);
+		for (n = 0; n < n_samples; n += 4) {
+			new[0] = _MM_XORSHIFT_EPI32(r);
+			in[0] = _mm_sub_epi32(old[0], new[0]);
+			old[0] = new[0];
+			out[0] = _mm_cvtepi32_ps(in[0]);
+			out[0] = _mm_mul_ps(out[0], scale);
+			_mm_store_ps(&noise[n], out[0]);
+		}
+		_mm_store_si128((__m128i*)p, old[0]);
+		break;
+	case NOISE_METHOD_PATTERN:
+		op = *p;
+		for (n = 0; n < n_samples; n++)
+			noise[n] = conv->scale * (1-((op++>>10)&1));
+		*p = op;
+		break;
+	}
+}
+
+static void
+conv_f32d_to_s32_1s_noise_sse2(struct convert *conv, void * SPA_RESTRICT dst, const void * SPA_RESTRICT src,
+		uint32_t n_channels, uint32_t n_samples)
+{
+	const float *s = src;
+	float *noise = SPA_PTR_ALIGN(conv->noise, 16, float);
+	int32_t *d = dst;
+	uint32_t n, unrolled;
+	__m128 in[1];
+	__m128i out[4];
+	__m128 scale = _mm_set1_ps(S24_SCALE);
+	__m128 int_min = _mm_set1_ps(S24_MIN);
+	__m128 int_max = _mm_set1_ps(S24_MAX);
+
+	if (SPA_IS_ALIGNED(s, 16))
+		unrolled = n_samples & ~3;
+	else
+		unrolled = 0;
+
+	for(n = 0; n < unrolled; n += 4) {
+		in[0] = _mm_mul_ps(_mm_load_ps(&s[n]), scale);
+		in[0] = _mm_add_ps(in[0], _mm_load_ps(&noise[n]));
+		in[0] = _MM_CLAMP_PS(in[0], int_min, int_max);
+		out[0] = _mm_cvtps_epi32(in[0]);
+		out[0] = _mm_slli_epi32(out[0], 8);
+		out[1] = _mm_shuffle_epi32(out[0], _MM_SHUFFLE(0, 3, 2, 1));
+		out[2] = _mm_shuffle_epi32(out[0], _MM_SHUFFLE(1, 0, 3, 2));
+		out[3] = _mm_shuffle_epi32(out[0], _MM_SHUFFLE(2, 1, 0, 3));
+
+		d[0*n_channels] = _mm_cvtsi128_si32(out[0]);
+		d[1*n_channels] = _mm_cvtsi128_si32(out[1]);
+		d[2*n_channels] = _mm_cvtsi128_si32(out[2]);
+		d[3*n_channels] = _mm_cvtsi128_si32(out[3]);
+		d += 4*n_channels;
+	}
+	for(; n < n_samples; n++) {
+		in[0] = _mm_load_ss(&s[n]);
+		in[0] = _mm_mul_ss(in[0], scale);
+		in[0] = _mm_add_ss(in[0], _mm_load_ss(&noise[n]));
+		in[0] = _MM_CLAMP_SS(in[0], int_min, int_max);
+		*d = _mm_cvtss_si32(in[0]) << 8;
+		d += n_channels;
+	}
+}
+
+void
+conv_f32d_to_s32_noise_sse2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
+		uint32_t n_samples)
+{
+	int32_t *d = dst[0];
+	uint32_t i, k, chunk, n_channels = conv->n_channels;
+
+	update_noise_sse2(conv, SPA_MIN(n_samples, conv->noise_size));
+
+	for(i = 0; i < n_channels; i++) {
+		const float *s = src[i];
+		for(k = 0; k < n_samples; k += chunk) {
+			chunk = SPA_MIN(n_samples - k, conv->noise_size);
+			conv_f32d_to_s32_1s_noise_sse2(conv, &d[i + k*n_channels], &s[k], n_channels, chunk);
+		}
+	}
+}
+
 static void
 conv_interleave_32_1s_sse2(void *data, void * SPA_RESTRICT dst, const void * SPA_RESTRICT src[],
 		uint32_t n_channels, uint32_t n_samples)
@@ -613,7 +758,7 @@ conv_interleave_32_4s_sse2(void *data, void * SPA_RESTRICT dst, const void * SPA
 }
 
 void
-conv_interleave_32_sse2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
+conv_32d_to_32_sse2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
 		uint32_t n_samples)
 {
 	int32_t *d = dst[0];
@@ -711,7 +856,7 @@ conv_interleave_32s_4s_sse2(void *data, void * SPA_RESTRICT dst, const void * SP
 }
 
 void
-conv_interleave_32s_sse2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
+conv_32d_to_32s_sse2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
 		uint32_t n_samples)
 {
 	int32_t *d = dst[0];
@@ -792,7 +937,7 @@ conv_deinterleave_32_4s_sse2(void *data, void * SPA_RESTRICT dst[], const void *
 }
 
 void
-conv_deinterleave_32_sse2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
+conv_32_to_32d_sse2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
 		uint32_t n_samples)
 {
 	const float *s = src[0];
@@ -879,7 +1024,7 @@ conv_deinterleave_32s_4s_sse2(void *data, void * SPA_RESTRICT dst[], const void
 }
 
 void
-conv_deinterleave_32s_sse2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
+conv_32s_to_32d_sse2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
 		uint32_t n_samples)
 {
 	const float *s = src[0];
@@ -900,8 +1045,9 @@ conv_f32_to_s16_1_sse2(void *data, void * SPA_RESTRICT dst, const void * SPA_RES
 	uint32_t n, unrolled;
 	__m128 in[2];
 	__m128i out[2];
-	__m128 int_max = _mm_set1_ps(S16_MAX_F);
-        __m128 int_min = _mm_sub_ps(_mm_setzero_ps(), int_max);
+	__m128 int_scale = _mm_set1_ps(S16_SCALE);
+	__m128 int_max = _mm_set1_ps(S16_MAX);
+        __m128 int_min = _mm_set1_ps(S16_MIN);
 
 	if (SPA_IS_ALIGNED(s, 16))
 		unrolled = n_samples & ~7;
@@ -909,8 +1055,8 @@ conv_f32_to_s16_1_sse2(void *data, void * SPA_RESTRICT dst, const void * SPA_RES
 		unrolled = 0;
 
 	for(n = 0; n < unrolled; n += 8) {
-		in[0] = _mm_mul_ps(_mm_load_ps(&s[n]), int_max);
-		in[1] = _mm_mul_ps(_mm_load_ps(&s[n+4]), int_max);
+		in[0] = _mm_mul_ps(_mm_load_ps(&s[n]), int_scale);
+		in[1] = _mm_mul_ps(_mm_load_ps(&s[n+4]), int_scale);
 		out[0] = _mm_cvtps_epi32(in[0]);
 		out[1] = _mm_cvtps_epi32(in[1]);
 		out[0] = _mm_packs_epi32(out[0], out[1]);
@@ -918,8 +1064,8 @@ conv_f32_to_s16_1_sse2(void *data, void * SPA_RESTRICT dst, const void * SPA_RES
 		d += 8;
 	}
 	for(; n < n_samples; n++) {
-		in[0] = _mm_mul_ss(_mm_load_ss(&s[n]), int_max);
-		in[0] = _mm_min_ss(int_max, _mm_max_ss(in[0], int_min));
+		in[0] = _mm_mul_ss(_mm_load_ss(&s[n]), int_scale);
+		in[0] = _MM_CLAMP_SS(in[0], int_min, int_max);
 		*d++ = _mm_cvtss_si32(in[0]);
 	}
 }
@@ -949,8 +1095,9 @@ conv_f32d_to_s16_1s_sse2(void *data, void * SPA_RESTRICT dst, const void * SPA_R
 	uint32_t n, unrolled;
 	__m128 in[2];
 	__m128i out[2];
-	__m128 int_max = _mm_set1_ps(S16_MAX_F);
-        __m128 int_min = _mm_sub_ps(_mm_setzero_ps(), int_max);
+	__m128 int_scale = _mm_set1_ps(S16_SCALE);
+	__m128 int_max = _mm_set1_ps(S16_MAX);
+        __m128 int_min = _mm_set1_ps(S16_MIN);
 
 	if (SPA_IS_ALIGNED(s0, 16))
 		unrolled = n_samples & ~7;
@@ -958,8 +1105,8 @@ conv_f32d_to_s16_1s_sse2(void *data, void * SPA_RESTRICT dst, const void * SPA_R
 		unrolled = 0;
 
 	for(n = 0; n < unrolled; n += 8) {
-		in[0] = _mm_mul_ps(_mm_load_ps(&s0[n]), int_max);
-		in[1] = _mm_mul_ps(_mm_load_ps(&s0[n+4]), int_max);
+		in[0] = _mm_mul_ps(_mm_load_ps(&s0[n]), int_scale);
+		in[1] = _mm_mul_ps(_mm_load_ps(&s0[n+4]), int_scale);
 		out[0] = _mm_cvtps_epi32(in[0]);
 		out[1] = _mm_cvtps_epi32(in[1]);
 		out[0] = _mm_packs_epi32(out[0], out[1]);
@@ -975,8 +1122,8 @@ conv_f32d_to_s16_1s_sse2(void *data, void * SPA_RESTRICT dst, const void * SPA_R
 		d += 8*n_channels;
 	}
 	for(; n < n_samples; n++) {
-		in[0] = _mm_mul_ss(_mm_load_ss(&s0[n]), int_max);
-		in[0] = _mm_min_ss(int_max, _mm_max_ss(in[0], int_min));
+		in[0] = _mm_mul_ss(_mm_load_ss(&s0[n]), int_scale);
+		in[0] = _MM_CLAMP_SS(in[0], int_min, int_max);
 		*d = _mm_cvtss_si32(in[0]);
 		d += n_channels;
 	}
@@ -991,8 +1138,9 @@ conv_f32d_to_s16_2s_sse2(void *data, void * SPA_RESTRICT dst, const void * SPA_R
 	uint32_t n, unrolled;
 	__m128 in[2];
 	__m128i out[4], t[2];
-	__m128 int_max = _mm_set1_ps(S16_MAX_F);
-        __m128 int_min = _mm_sub_ps(_mm_setzero_ps(), int_max);
+	__m128 int_scale = _mm_set1_ps(S16_SCALE);
+	__m128 int_max = _mm_set1_ps(S16_MAX);
+        __m128 int_min = _mm_set1_ps(S16_MIN);
 
 	if (SPA_IS_ALIGNED(s0, 16) &&
 	    SPA_IS_ALIGNED(s1, 16))
@@ -1001,8 +1149,8 @@ conv_f32d_to_s16_2s_sse2(void *data, void * SPA_RESTRICT dst, const void * SPA_R
 		unrolled = 0;
 
 	for(n = 0; n < unrolled; n += 4) {
-		in[0] = _mm_mul_ps(_mm_load_ps(&s0[n]), int_max);
-		in[1] = _mm_mul_ps(_mm_load_ps(&s1[n]), int_max);
+		in[0] = _mm_mul_ps(_mm_load_ps(&s0[n]), int_scale);
+		in[1] = _mm_mul_ps(_mm_load_ps(&s1[n]), int_scale);
 
 		t[0] = _mm_cvtps_epi32(in[0]);
 		t[1] = _mm_cvtps_epi32(in[1]);
@@ -1022,10 +1170,10 @@ conv_f32d_to_s16_2s_sse2(void *data, void * SPA_RESTRICT dst, const void * SPA_R
 		d += 4*n_channels;
 	}
 	for(; n < n_samples; n++) {
-		in[0] = _mm_mul_ss(_mm_load_ss(&s0[n]), int_max);
-		in[1] = _mm_mul_ss(_mm_load_ss(&s1[n]), int_max);
-		in[0] = _mm_min_ss(int_max, _mm_max_ss(in[0], int_min));
-		in[1] = _mm_min_ss(int_max, _mm_max_ss(in[1], int_min));
+		in[0] = _mm_mul_ss(_mm_load_ss(&s0[n]), int_scale);
+		in[1] = _mm_mul_ss(_mm_load_ss(&s1[n]), int_scale);
+		in[0] = _MM_CLAMP_SS(in[0], int_min, int_max);
+		in[1] = _MM_CLAMP_SS(in[1], int_min, int_max);
 		d[0] = _mm_cvtss_si32(in[0]);
 		d[1] = _mm_cvtss_si32(in[1]);
 		d += n_channels;
@@ -1041,8 +1189,9 @@ conv_f32d_to_s16_4s_sse2(void *data, void * SPA_RESTRICT dst, const void * SPA_R
 	uint32_t n, unrolled;
 	__m128 in[4];
 	__m128i out[4], t[4];
-	__m128 int_max = _mm_set1_ps(S16_MAX_F);
-        __m128 int_min = _mm_sub_ps(_mm_setzero_ps(), int_max);
+	__m128 int_scale = _mm_set1_ps(S16_SCALE);
+	__m128 int_max = _mm_set1_ps(S16_MAX);
+        __m128 int_min = _mm_set1_ps(S16_MIN);
 
 	if (SPA_IS_ALIGNED(s0, 16) &&
 	    SPA_IS_ALIGNED(s1, 16) &&
@@ -1053,10 +1202,10 @@ conv_f32d_to_s16_4s_sse2(void *data, void * SPA_RESTRICT dst, const void * SPA_R
 		unrolled = 0;
 
 	for(n = 0; n < unrolled; n += 4) {
-		in[0] = _mm_mul_ps(_mm_load_ps(&s0[n]), int_max);
-		in[1] = _mm_mul_ps(_mm_load_ps(&s1[n]), int_max);
-		in[2] = _mm_mul_ps(_mm_load_ps(&s2[n]), int_max);
-		in[3] = _mm_mul_ps(_mm_load_ps(&s3[n]), int_max);
+		in[0] = _mm_mul_ps(_mm_load_ps(&s0[n]), int_scale);
+		in[1] = _mm_mul_ps(_mm_load_ps(&s1[n]), int_scale);
+		in[2] = _mm_mul_ps(_mm_load_ps(&s2[n]), int_scale);
+		in[3] = _mm_mul_ps(_mm_load_ps(&s3[n]), int_scale);
 
 		t[0] = _mm_cvtps_epi32(in[0]);
 		t[1] = _mm_cvtps_epi32(in[1]);
@@ -1079,14 +1228,14 @@ conv_f32d_to_s16_4s_sse2(void *data, void * SPA_RESTRICT dst, const void * SPA_R
 		d += 4*n_channels;
 	}
 	for(; n < n_samples; n++) {
-		in[0] = _mm_mul_ss(_mm_load_ss(&s0[n]), int_max);
-		in[1] = _mm_mul_ss(_mm_load_ss(&s1[n]), int_max);
-		in[2] = _mm_mul_ss(_mm_load_ss(&s2[n]), int_max);
-		in[3] = _mm_mul_ss(_mm_load_ss(&s3[n]), int_max);
-		in[0] = _mm_min_ss(int_max, _mm_max_ss(in[0], int_min));
-		in[1] = _mm_min_ss(int_max, _mm_max_ss(in[1], int_min));
-		in[2] = _mm_min_ss(int_max, _mm_max_ss(in[2], int_min));
-		in[3] = _mm_min_ss(int_max, _mm_max_ss(in[3], int_min));
+		in[0] = _mm_mul_ss(_mm_load_ss(&s0[n]), int_scale);
+		in[1] = _mm_mul_ss(_mm_load_ss(&s1[n]), int_scale);
+		in[2] = _mm_mul_ss(_mm_load_ss(&s2[n]), int_scale);
+		in[3] = _mm_mul_ss(_mm_load_ss(&s3[n]), int_scale);
+		in[0] = _MM_CLAMP_SS(in[0], int_min, int_max);
+		in[1] = _MM_CLAMP_SS(in[1], int_min, int_max);
+		in[2] = _MM_CLAMP_SS(in[2], int_min, int_max);
+		in[3] = _MM_CLAMP_SS(in[3], int_min, int_max);
 		d[0] = _mm_cvtss_si32(in[0]);
 		d[1] = _mm_cvtss_si32(in[1]);
 		d[2] = _mm_cvtss_si32(in[2]);
@@ -1110,6 +1259,126 @@ conv_f32d_to_s16_sse2(struct convert *conv, void * SPA_RESTRICT dst[], const voi
 		conv_f32d_to_s16_1s_sse2(conv, &d[i], &src[i], n_channels, n_samples);
 }
 
+static void
+conv_f32d_to_s16_1s_noise_sse2(struct convert *conv, void * SPA_RESTRICT dst, const void * SPA_RESTRICT src,
+		uint32_t n_channels, uint32_t n_samples)
+{
+	const float *s0 = src;
+	int16_t *d = dst;
+	float *noise = SPA_PTR_ALIGN(conv->noise, 16, float);
+	uint32_t n, unrolled;
+	__m128 in[2];
+	__m128i out[2];
+	__m128 int_scale = _mm_set1_ps(S16_SCALE);
+	__m128 int_max = _mm_set1_ps(S16_MAX);
+        __m128 int_min = _mm_set1_ps(S16_MIN);
+
+	if (SPA_IS_ALIGNED(s0, 16))
+		unrolled = n_samples & ~7;
+	else
+		unrolled = 0;
+
+	for(n = 0; n < unrolled; n += 8) {
+		in[0] = _mm_mul_ps(_mm_load_ps(&s0[n]), int_scale);
+		in[1] = _mm_mul_ps(_mm_load_ps(&s0[n+4]), int_scale);
+		in[0] = _mm_add_ps(in[0], _mm_load_ps(&noise[n]));
+		in[1] = _mm_add_ps(in[1], _mm_load_ps(&noise[n+4]));
+		out[0] = _mm_cvtps_epi32(in[0]);
+		out[1] = _mm_cvtps_epi32(in[1]);
+		out[0] = _mm_packs_epi32(out[0], out[1]);
+
+		d[0*n_channels] = _mm_extract_epi16(out[0], 0);
+		d[1*n_channels] = _mm_extract_epi16(out[0], 1);
+		d[2*n_channels] = _mm_extract_epi16(out[0], 2);
+		d[3*n_channels] = _mm_extract_epi16(out[0], 3);
+		d[4*n_channels] = _mm_extract_epi16(out[0], 4);
+		d[5*n_channels] = _mm_extract_epi16(out[0], 5);
+		d[6*n_channels] = _mm_extract_epi16(out[0], 6);
+		d[7*n_channels] = _mm_extract_epi16(out[0], 7);
+		d += 8*n_channels;
+	}
+	for(; n < n_samples; n++) {
+		in[0] = _mm_mul_ss(_mm_load_ss(&s0[n]), int_scale);
+		in[0] = _mm_add_ss(in[0], _mm_load_ss(&noise[n]));
+		in[0] = _MM_CLAMP_SS(in[0], int_min, int_max);
+		*d = _mm_cvtss_si32(in[0]);
+		d += n_channels;
+	}
+}
+
+void
+conv_f32d_to_s16_noise_sse2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
+		uint32_t n_samples)
+{
+	int16_t *d = dst[0];
+	uint32_t i, k, chunk, n_channels = conv->n_channels;
+
+	update_noise_sse2(conv, SPA_MIN(n_samples, conv->noise_size));
+
+	for(i = 0; i < n_channels; i++) {
+		const float *s = src[i];
+		for(k = 0; k < n_samples; k += chunk) {
+			chunk = SPA_MIN(n_samples - k, conv->noise_size);
+			conv_f32d_to_s16_1s_noise_sse2(conv, &d[i + k*n_channels], &s[k], n_channels, chunk);
+		}
+	}
+}
+
+static void
+conv_f32_to_s16_1_noise_sse2(struct convert *conv, void * SPA_RESTRICT dst, const void * SPA_RESTRICT src,
+		uint32_t n_samples)
+{
+	const float *s = src;
+	int16_t *d = dst;
+	float *noise = SPA_PTR_ALIGN(conv->noise, 16, float);
+	uint32_t n, unrolled;
+	__m128 in[2];
+	__m128i out[2];
+	__m128 int_scale = _mm_set1_ps(S16_SCALE);
+	__m128 int_max = _mm_set1_ps(S16_MAX);
+        __m128 int_min = _mm_set1_ps(S16_MIN);
+
+	if (SPA_IS_ALIGNED(s, 16))
+		unrolled = n_samples & ~7;
+	else
+		unrolled = 0;
+
+	for(n = 0; n < unrolled; n += 8) {
+		in[0] = _mm_mul_ps(_mm_load_ps(&s[n]), int_scale);
+		in[1] = _mm_mul_ps(_mm_load_ps(&s[n+4]), int_scale);
+		in[0] = _mm_add_ps(in[0], _mm_load_ps(&noise[n]));
+		in[1] = _mm_add_ps(in[1], _mm_load_ps(&noise[n+4]));
+		out[0] = _mm_cvtps_epi32(in[0]);
+		out[1] = _mm_cvtps_epi32(in[1]);
+		out[0] = _mm_packs_epi32(out[0], out[1]);
+		_mm_storeu_si128((__m128i*)(&d[n]), out[0]);
+	}
+	for(; n < n_samples; n++) {
+		in[0] = _mm_mul_ss(_mm_load_ss(&s[n]), int_scale);
+		in[0] = _mm_add_ss(in[0], _mm_load_ss(&noise[n]));
+		in[0] = _MM_CLAMP_SS(in[0], int_min, int_max);
+		d[n] = _mm_cvtss_si32(in[0]);
+	}
+}
+
+void
+conv_f32d_to_s16d_noise_sse2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
+		uint32_t n_samples)
+{
+	uint32_t i, k, chunk, n_channels = conv->n_channels;
+
+	update_noise_sse2(conv, SPA_MIN(n_samples, conv->noise_size));
+
+	for(i = 0; i < n_channels; i++) {
+		const float *s = src[i];
+		int16_t *d = dst[i];
+		for(k = 0; k < n_samples; k += chunk) {
+			chunk = SPA_MIN(n_samples - k, conv->noise_size);
+			conv_f32_to_s16_1_noise_sse2(conv, &d[k], &s[k], chunk);
+		}
+	}
+}
+
 void
 conv_f32d_to_s16_2_sse2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
 		uint32_t n_samples)
@@ -1119,8 +1388,9 @@ conv_f32d_to_s16_2_sse2(struct convert *conv, void * SPA_RESTRICT dst[], const v
 	uint32_t n, unrolled;
 	__m128 in[4];
 	__m128i out[4];
-	__m128 int_max = _mm_set1_ps(S16_MAX_F);
-        __m128 int_min = _mm_sub_ps(_mm_setzero_ps(), int_max);
+	__m128 int_scale = _mm_set1_ps(S16_SCALE);
+	__m128 int_max = _mm_set1_ps(S16_MAX);
+        __m128 int_min = _mm_set1_ps(S16_MIN);
 
 	if (SPA_IS_ALIGNED(s0, 16) &&
 	    SPA_IS_ALIGNED(s1, 16))
@@ -1129,10 +1399,10 @@ conv_f32d_to_s16_2_sse2(struct convert *conv, void * SPA_RESTRICT dst[], const v
 		unrolled = 0;
 
 	for(n = 0; n < unrolled; n += 8) {
-		in[0] = _mm_mul_ps(_mm_load_ps(&s0[n+0]), int_max);
-		in[1] = _mm_mul_ps(_mm_load_ps(&s1[n+0]), int_max);
-		in[2] = _mm_mul_ps(_mm_load_ps(&s0[n+4]), int_max);
-		in[3] = _mm_mul_ps(_mm_load_ps(&s1[n+4]), int_max);
+		in[0] = _mm_mul_ps(_mm_load_ps(&s0[n+0]), int_scale);
+		in[1] = _mm_mul_ps(_mm_load_ps(&s1[n+0]), int_scale);
+		in[2] = _mm_mul_ps(_mm_load_ps(&s0[n+4]), int_scale);
+		in[3] = _mm_mul_ps(_mm_load_ps(&s1[n+4]), int_scale);
 
 		out[0] = _mm_cvtps_epi32(in[0]);
 		out[1] = _mm_cvtps_epi32(in[1]);
@@ -1151,10 +1421,10 @@ conv_f32d_to_s16_2_sse2(struct convert *conv, void * SPA_RESTRICT dst[], const v
 		d += 16;
 	}
 	for(; n < n_samples; n++) {
-		in[0] = _mm_mul_ss(_mm_load_ss(&s0[n]), int_max);
-		in[1] = _mm_mul_ss(_mm_load_ss(&s1[n]), int_max);
-		in[0] = _mm_min_ss(int_max, _mm_max_ss(in[0], int_min));
-		in[1] = _mm_min_ss(int_max, _mm_max_ss(in[1], int_min));
+		in[0] = _mm_mul_ss(_mm_load_ss(&s0[n]), int_scale);
+		in[1] = _mm_mul_ss(_mm_load_ss(&s1[n]), int_scale);
+		in[0] = _MM_CLAMP_SS(in[0], int_min, int_max);
+		in[1] = _MM_CLAMP_SS(in[1], int_min, int_max);
 		d[0] = _mm_cvtss_si32(in[0]);
 		d[1] = _mm_cvtss_si32(in[1]);
 		d += 2;
diff --git a/spa/plugins/audioconvert/fmt-ops-sse41.c b/spa/plugins/audioconvert/fmt-ops-sse41.c
index 0478555e8517a0676b8292665a48198cba998942..042294d6b317c7a0d43c17d00f2632a28562ae16 100644
--- a/spa/plugins/audioconvert/fmt-ops-sse41.c
+++ b/spa/plugins/audioconvert/fmt-ops-sse41.c
@@ -30,10 +30,10 @@ static void
 conv_s24_to_f32d_1s_sse41(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src,
 		uint32_t n_channels, uint32_t n_samples)
 {
-	const uint8_t *s = src;
+	const int24_t *s = src;
 	float *d0 = dst[0];
 	uint32_t n, unrolled;
-	__m128i in;
+	__m128i in = _mm_setzero_si128();
 	__m128 out, factor = _mm_set1_ps(1.0f / S24_SCALE);
 
 	if (SPA_IS_ALIGNED(d0, 16))
@@ -43,21 +43,21 @@ conv_s24_to_f32d_1s_sse41(void *data, void * SPA_RESTRICT dst[], const void * SP
 
 	for(n = 0; n < unrolled; n += 4) {
 		in = _mm_insert_epi32(in, *((uint32_t*)&s[0 * n_channels]), 0);
-		in = _mm_insert_epi32(in, *((uint32_t*)&s[3 * n_channels]), 1);
-		in = _mm_insert_epi32(in, *((uint32_t*)&s[6 * n_channels]), 2);
-		in = _mm_insert_epi32(in, *((uint32_t*)&s[9 * n_channels]), 3);
+		in = _mm_insert_epi32(in, *((uint32_t*)&s[1 * n_channels]), 1);
+		in = _mm_insert_epi32(in, *((uint32_t*)&s[2 * n_channels]), 2);
+		in = _mm_insert_epi32(in, *((uint32_t*)&s[3 * n_channels]), 3);
 		in = _mm_slli_epi32(in, 8);
 		in = _mm_srai_epi32(in, 8);
 		out = _mm_cvtepi32_ps(in);
 		out = _mm_mul_ps(out, factor);
 		_mm_store_ps(&d0[n], out);
-		s += 12 * n_channels;
+		s += 4 * n_channels;
 	}
 	for(; n < n_samples; n++) {
-		out = _mm_cvtsi32_ss(out, read_s24(s));
+		out = _mm_cvtsi32_ss(factor, s24_to_s32(*s));
 		out = _mm_mul_ss(out, factor);
 		_mm_store_ss(&d0[n], out);
-		s += 3 * n_channels;
+		s += n_channels;
 	}
 }
 
diff --git a/spa/plugins/audioconvert/fmt-ops-ssse3.c b/spa/plugins/audioconvert/fmt-ops-ssse3.c
index 6a7fc2e05399fdfc74d07b1483c0d9c97cb9befa..c35c7c9f3db7af5db69e5e684fc3cb3177cd0323 100644
--- a/spa/plugins/audioconvert/fmt-ops-ssse3.c
+++ b/spa/plugins/audioconvert/fmt-ops-ssse3.c
@@ -30,7 +30,7 @@ static void
 conv_s24_to_f32d_4s_ssse3(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src,
 		uint32_t n_channels, uint32_t n_samples)
 {
-	const uint8_t *s = src;
+	const int24_t *s = src;
 	float *d0 = dst[0], *d1 = dst[1], *d2 = dst[2], *d3 = dst[3];
 	uint32_t n, unrolled;
 	__m128i in[4];
@@ -48,9 +48,9 @@ conv_s24_to_f32d_4s_ssse3(void *data, void * SPA_RESTRICT dst[], const void * SP
 
 	for(n = 0; n < unrolled; n += 4) {
                 in[0] = _mm_loadu_si128((__m128i*)(s + 0*n_channels));
-                in[1] = _mm_loadu_si128((__m128i*)(s + 3*n_channels));
-                in[2] = _mm_loadu_si128((__m128i*)(s + 6*n_channels));
-                in[3] = _mm_loadu_si128((__m128i*)(s + 9*n_channels));
+                in[1] = _mm_loadu_si128((__m128i*)(s + 1*n_channels));
+                in[2] = _mm_loadu_si128((__m128i*)(s + 2*n_channels));
+                in[3] = _mm_loadu_si128((__m128i*)(s + 3*n_channels));
 		in[0] = _mm_shuffle_epi8(in[0], mask);
 		in[1] = _mm_shuffle_epi8(in[1], mask);
 		in[2] = _mm_shuffle_epi8(in[2], mask);
@@ -74,13 +74,13 @@ conv_s24_to_f32d_4s_ssse3(void *data, void * SPA_RESTRICT dst[], const void * SP
 		_mm_store_ps(&d1[n], out[1]);
 		_mm_store_ps(&d2[n], out[2]);
 		_mm_store_ps(&d3[n], out[3]);
-		s += 12 * n_channels;
+		s += 4 * n_channels;
 	}
 	for(; n < n_samples; n++) {
-		out[0] = _mm_cvtsi32_ss(out[0], read_s24(s));
-		out[1] = _mm_cvtsi32_ss(out[1], read_s24(s+3));
-		out[2] = _mm_cvtsi32_ss(out[2], read_s24(s+6));
-		out[3] = _mm_cvtsi32_ss(out[3], read_s24(s+9));
+		out[0] = _mm_cvtsi32_ss(factor, s24_to_s32(*s));
+		out[1] = _mm_cvtsi32_ss(factor, s24_to_s32(*(s+1)));
+		out[2] = _mm_cvtsi32_ss(factor, s24_to_s32(*(s+2)));
+		out[3] = _mm_cvtsi32_ss(factor, s24_to_s32(*(s+3)));
 		out[0] = _mm_mul_ss(out[0], factor);
 		out[1] = _mm_mul_ss(out[1], factor);
 		out[2] = _mm_mul_ss(out[2], factor);
@@ -89,7 +89,7 @@ conv_s24_to_f32d_4s_ssse3(void *data, void * SPA_RESTRICT dst[], const void * SP
 		_mm_store_ss(&d1[n], out[1]);
 		_mm_store_ss(&d2[n], out[2]);
 		_mm_store_ss(&d3[n], out[3]);
-		s += 3 * n_channels;
+		s += n_channels;
 	}
 }
 
diff --git a/spa/plugins/audioconvert/fmt-ops.c b/spa/plugins/audioconvert/fmt-ops.c
index 617793971a1efdc0da155194ee724da172f5f5a9..443d59eec729b7b0ffb61ad2c554bddde9e1b918 100644
--- a/spa/plugins/audioconvert/fmt-ops.c
+++ b/spa/plugins/audioconvert/fmt-ops.c
@@ -32,6 +32,8 @@
 
 #include "fmt-ops.h"
 
+#define DITHER_SIZE	(1<<10)
+
 typedef void (*convert_func_t) (struct convert *conv, void * SPA_RESTRICT dst[],
 		const void * SPA_RESTRICT src[], uint32_t n_samples);
 
@@ -39,277 +41,322 @@ struct conv_info {
 	uint32_t src_fmt;
 	uint32_t dst_fmt;
 	uint32_t n_channels;
-	uint32_t cpu_flags;
 
 	convert_func_t process;
+	const char *name;
+
+	uint32_t cpu_flags;
+#define CONV_NOISE	(1<<0)
+#define CONV_SHAPE	(1<<1)
+	uint32_t conv_flags;
 };
 
+#define MAKE(fmt1,fmt2,chan,func,...) \
+	{  SPA_AUDIO_FORMAT_ ##fmt1, SPA_AUDIO_FORMAT_ ##fmt2, chan, func, #func , __VA_ARGS__ }
+
 static struct conv_info conv_table[] =
 {
 	/* to f32 */
-	{ SPA_AUDIO_FORMAT_U8, SPA_AUDIO_FORMAT_F32, 0, 0, conv_u8_to_f32_c },
-	{ SPA_AUDIO_FORMAT_U8P, SPA_AUDIO_FORMAT_F32P, 0, 0, conv_u8d_to_f32d_c },
-	{ SPA_AUDIO_FORMAT_U8, SPA_AUDIO_FORMAT_F32P, 0, 0, conv_u8_to_f32d_c },
-	{ SPA_AUDIO_FORMAT_U8P, SPA_AUDIO_FORMAT_F32, 0, 0, conv_u8d_to_f32_c },
+	MAKE(U8, F32, 0, conv_u8_to_f32_c),
+	MAKE(U8, F32, 0, conv_u8_to_f32_c),
+	MAKE(U8P, F32P, 0, conv_u8d_to_f32d_c),
+	MAKE(U8, F32P, 0, conv_u8_to_f32d_c),
+	MAKE(U8P, F32, 0, conv_u8d_to_f32_c),
 
-	{ SPA_AUDIO_FORMAT_S8, SPA_AUDIO_FORMAT_F32, 0, 0, conv_s8_to_f32_c },
-	{ SPA_AUDIO_FORMAT_S8P, SPA_AUDIO_FORMAT_F32P, 0, 0, conv_s8d_to_f32d_c },
-	{ SPA_AUDIO_FORMAT_S8, SPA_AUDIO_FORMAT_F32P, 0, 0, conv_s8_to_f32d_c },
-	{ SPA_AUDIO_FORMAT_S8P, SPA_AUDIO_FORMAT_F32, 0, 0, conv_s8d_to_f32_c },
+	MAKE(S8, F32, 0, conv_s8_to_f32_c),
+	MAKE(S8P, F32P, 0, conv_s8d_to_f32d_c),
+	MAKE(S8, F32P, 0, conv_s8_to_f32d_c),
+	MAKE(S8P, F32, 0, conv_s8d_to_f32_c),
 
-	{ SPA_AUDIO_FORMAT_ALAW, SPA_AUDIO_FORMAT_F32P, 0, 0, conv_alaw_to_f32d_c },
-	{ SPA_AUDIO_FORMAT_ULAW, SPA_AUDIO_FORMAT_F32P, 0, 0, conv_ulaw_to_f32d_c },
+	MAKE(ALAW, F32P, 0, conv_alaw_to_f32d_c),
+	MAKE(ULAW, F32P, 0, conv_ulaw_to_f32d_c),
 
-	{ SPA_AUDIO_FORMAT_U16, SPA_AUDIO_FORMAT_F32, 0, 0, conv_u16_to_f32_c },
-	{ SPA_AUDIO_FORMAT_U16, SPA_AUDIO_FORMAT_F32P, 0, 0, conv_u16_to_f32d_c },
+	MAKE(U16, F32, 0, conv_u16_to_f32_c),
+	MAKE(U16, F32P, 0, conv_u16_to_f32d_c),
 
-	{ SPA_AUDIO_FORMAT_S16, SPA_AUDIO_FORMAT_F32, 0, 0, conv_s16_to_f32_c },
-	{ SPA_AUDIO_FORMAT_S16P, SPA_AUDIO_FORMAT_F32P, 0, 0, conv_s16d_to_f32d_c },
+	MAKE(S16, F32, 0, conv_s16_to_f32_c),
+	MAKE(S16P, F32P, 0, conv_s16d_to_f32d_c),
 #if defined (HAVE_NEON)
-	{ SPA_AUDIO_FORMAT_S16, SPA_AUDIO_FORMAT_F32P, 2, SPA_CPU_FLAG_NEON, conv_s16_to_f32d_2_neon },
-	{ SPA_AUDIO_FORMAT_S16, SPA_AUDIO_FORMAT_F32P, 0, SPA_CPU_FLAG_NEON, conv_s16_to_f32d_neon },
+	MAKE(S16, F32P, 2, conv_s16_to_f32d_2_neon, SPA_CPU_FLAG_NEON),
+	MAKE(S16, F32P, 0, conv_s16_to_f32d_neon, SPA_CPU_FLAG_NEON),
 #endif
 #if defined (HAVE_AVX2)
-	{ SPA_AUDIO_FORMAT_S16, SPA_AUDIO_FORMAT_F32P, 2, SPA_CPU_FLAG_AVX2, conv_s16_to_f32d_2_avx2 },
-	{ SPA_AUDIO_FORMAT_S16, SPA_AUDIO_FORMAT_F32P, 0, SPA_CPU_FLAG_AVX2, conv_s16_to_f32d_avx2 },
+	MAKE(S16, F32P, 2, conv_s16_to_f32d_2_avx2, SPA_CPU_FLAG_AVX2),
+	MAKE(S16, F32P, 0, conv_s16_to_f32d_avx2, SPA_CPU_FLAG_AVX2),
 #endif
 #if defined (HAVE_SSE2)
-	{ SPA_AUDIO_FORMAT_S16, SPA_AUDIO_FORMAT_F32P, 2, SPA_CPU_FLAG_SSE2, conv_s16_to_f32d_2_sse2 },
-	{ SPA_AUDIO_FORMAT_S16, SPA_AUDIO_FORMAT_F32P, 0, SPA_CPU_FLAG_SSE2, conv_s16_to_f32d_sse2 },
+	MAKE(S16, F32P, 2, conv_s16_to_f32d_2_sse2, SPA_CPU_FLAG_SSE2),
+	MAKE(S16, F32P, 0, conv_s16_to_f32d_sse2, SPA_CPU_FLAG_SSE2),
 #endif
-	{ SPA_AUDIO_FORMAT_S16, SPA_AUDIO_FORMAT_F32P, 0, 0, conv_s16_to_f32d_c },
-	{ SPA_AUDIO_FORMAT_S16P, SPA_AUDIO_FORMAT_F32, 0, 0, conv_s16d_to_f32_c },
+	MAKE(S16, F32P, 0, conv_s16_to_f32d_c),
+	MAKE(S16P, F32, 0, conv_s16d_to_f32_c),
 
-	{ SPA_AUDIO_FORMAT_S16_OE, SPA_AUDIO_FORMAT_F32P, 0, 0, conv_s16s_to_f32d_c },
+	MAKE(S16_OE, F32P, 0, conv_s16s_to_f32d_c),
 
-	{ SPA_AUDIO_FORMAT_F32, SPA_AUDIO_FORMAT_F32, 0, 0, conv_copy32_c },
-	{ SPA_AUDIO_FORMAT_F32P, SPA_AUDIO_FORMAT_F32P, 0, 0, conv_copy32d_c },
+	MAKE(F32, F32, 0, conv_copy32_c),
+	MAKE(F32P, F32P, 0, conv_copy32d_c),
 #if defined (HAVE_SSE2)
-	{ SPA_AUDIO_FORMAT_F32, SPA_AUDIO_FORMAT_F32P, 0, 0, conv_deinterleave_32_sse2 },
+	MAKE(F32, F32P, 0, conv_32_to_32d_sse2, SPA_CPU_FLAG_SSE2),
 #endif
-	{ SPA_AUDIO_FORMAT_F32, SPA_AUDIO_FORMAT_F32P, 0, 0, conv_deinterleave_32_c },
+	MAKE(F32, F32P, 0, conv_32_to_32d_c),
 #if defined (HAVE_SSE2)
-	{ SPA_AUDIO_FORMAT_F32P, SPA_AUDIO_FORMAT_F32, 0, 0, conv_interleave_32_sse2 },
+	MAKE(F32P, F32, 0, conv_32d_to_32_sse2, SPA_CPU_FLAG_SSE2),
 #endif
-	{ SPA_AUDIO_FORMAT_F32P, SPA_AUDIO_FORMAT_F32, 0, 0, conv_interleave_32_c },
+	MAKE(F32P, F32, 0, conv_32d_to_32_c),
 
 #if defined (HAVE_SSE2)
-	{ SPA_AUDIO_FORMAT_F32_OE, SPA_AUDIO_FORMAT_F32P, 0, 0, conv_deinterleave_32s_sse2 },
+	MAKE(F32_OE, F32P, 0, conv_32s_to_32d_sse2, SPA_CPU_FLAG_SSE2),
 #endif
-	{ SPA_AUDIO_FORMAT_F32_OE, SPA_AUDIO_FORMAT_F32P, 0, 0, conv_deinterleave_32s_c },
+	MAKE(F32_OE, F32P, 0, conv_32s_to_32d_c),
 #if defined (HAVE_SSE2)
-	{ SPA_AUDIO_FORMAT_F32P, SPA_AUDIO_FORMAT_F32_OE, 0, 0, conv_interleave_32s_sse2 },
+	MAKE(F32P, F32_OE, 0, conv_32d_to_32s_sse2, SPA_CPU_FLAG_SSE2),
 #endif
-	{ SPA_AUDIO_FORMAT_F32P, SPA_AUDIO_FORMAT_F32_OE, 0, 0, conv_interleave_32s_c },
+	MAKE(F32P, F32_OE, 0, conv_32d_to_32s_c),
 
-	{ SPA_AUDIO_FORMAT_U32, SPA_AUDIO_FORMAT_F32, 0, 0, conv_u32_to_f32_c },
-	{ SPA_AUDIO_FORMAT_U32, SPA_AUDIO_FORMAT_F32P, 0, 0, conv_u32_to_f32d_c },
+	MAKE(U32, F32, 0, conv_u32_to_f32_c),
+	MAKE(U32, F32P, 0, conv_u32_to_f32d_c),
 
 #if defined (HAVE_AVX2)
-	{ SPA_AUDIO_FORMAT_S32, SPA_AUDIO_FORMAT_F32P, 0, SPA_CPU_FLAG_AVX2, conv_s32_to_f32d_avx2 },
+	MAKE(S32, F32P, 0, conv_s32_to_f32d_avx2, SPA_CPU_FLAG_AVX2),
 #endif
 #if defined (HAVE_SSE2)
-	{ SPA_AUDIO_FORMAT_S32, SPA_AUDIO_FORMAT_F32P, 0, SPA_CPU_FLAG_SSE2, conv_s32_to_f32d_sse2 },
+	MAKE(S32, F32P, 0, conv_s32_to_f32d_sse2, SPA_CPU_FLAG_SSE2),
 #endif
-	{ SPA_AUDIO_FORMAT_S32, SPA_AUDIO_FORMAT_F32, 0, 0, conv_s32_to_f32_c },
-	{ SPA_AUDIO_FORMAT_S32P, SPA_AUDIO_FORMAT_F32P, 0, 0, conv_s32d_to_f32d_c },
-	{ SPA_AUDIO_FORMAT_S32, SPA_AUDIO_FORMAT_F32P, 0, 0, conv_s32_to_f32d_c },
-	{ SPA_AUDIO_FORMAT_S32P, SPA_AUDIO_FORMAT_F32, 0, 0, conv_s32d_to_f32_c },
+	MAKE(S32, F32, 0, conv_s32_to_f32_c),
+	MAKE(S32P, F32P, 0, conv_s32d_to_f32d_c),
+	MAKE(S32, F32P, 0, conv_s32_to_f32d_c),
+	MAKE(S32P, F32, 0, conv_s32d_to_f32_c),
 
-	{ SPA_AUDIO_FORMAT_S32_OE, SPA_AUDIO_FORMAT_F32P, 0, 0, conv_s32s_to_f32d_c },
+	MAKE(S32_OE, F32P, 0, conv_s32s_to_f32d_c),
 
-	{ SPA_AUDIO_FORMAT_U24, SPA_AUDIO_FORMAT_F32, 0, 0, conv_u24_to_f32_c },
-	{ SPA_AUDIO_FORMAT_U24, SPA_AUDIO_FORMAT_F32P, 0, 0, conv_u24_to_f32d_c },
+	MAKE(U24, F32, 0, conv_u24_to_f32_c),
+	MAKE(U24, F32P, 0, conv_u24_to_f32d_c),
 
-	{ SPA_AUDIO_FORMAT_S24, SPA_AUDIO_FORMAT_F32, 0, 0, conv_s24_to_f32_c },
-	{ SPA_AUDIO_FORMAT_S24P, SPA_AUDIO_FORMAT_F32P, 0, 0, conv_s24d_to_f32d_c },
+	MAKE(S24, F32, 0, conv_s24_to_f32_c),
+	MAKE(S24P, F32P, 0, conv_s24d_to_f32d_c),
 #if defined (HAVE_AVX2)
-	{ SPA_AUDIO_FORMAT_S24, SPA_AUDIO_FORMAT_F32P, 0, SPA_CPU_FLAG_AVX2, conv_s24_to_f32d_avx2 },
+	MAKE(S24, F32P, 0, conv_s24_to_f32d_avx2, SPA_CPU_FLAG_AVX2),
 #endif
 #if defined (HAVE_SSSE3)
-//	{ SPA_AUDIO_FORMAT_S24, SPA_AUDIO_FORMAT_F32P, 0, SPA_CPU_FLAG_SSSE3, conv_s24_to_f32d_ssse3 },
+//	MAKE(S24, F32P, 0, conv_s24_to_f32d_ssse3, SPA_CPU_FLAG_SSSE3),
 #endif
 #if defined (HAVE_SSE41)
-	{ SPA_AUDIO_FORMAT_S24, SPA_AUDIO_FORMAT_F32P, 0, SPA_CPU_FLAG_SSE41, conv_s24_to_f32d_sse41 },
+	MAKE(S24, F32P, 0, conv_s24_to_f32d_sse41, SPA_CPU_FLAG_SSE41),
 #endif
 #if defined (HAVE_SSE2)
-	{ SPA_AUDIO_FORMAT_S24, SPA_AUDIO_FORMAT_F32P, 0, SPA_CPU_FLAG_SSE2, conv_s24_to_f32d_sse2 },
+	MAKE(S24, F32P, 0, conv_s24_to_f32d_sse2, SPA_CPU_FLAG_SSE2),
 #endif
-	{ SPA_AUDIO_FORMAT_S24, SPA_AUDIO_FORMAT_F32P, 0, 0, conv_s24_to_f32d_c },
-	{ SPA_AUDIO_FORMAT_S24P, SPA_AUDIO_FORMAT_F32, 0, 0, conv_s24d_to_f32_c },
+	MAKE(S24, F32P, 0, conv_s24_to_f32d_c),
+	MAKE(S24P, F32, 0, conv_s24d_to_f32_c),
 
-	{ SPA_AUDIO_FORMAT_S24_OE, SPA_AUDIO_FORMAT_F32P, 0, 0, conv_s24s_to_f32d_c },
+	MAKE(S24_OE, F32P, 0, conv_s24s_to_f32d_c),
 
-	{ SPA_AUDIO_FORMAT_U24_32, SPA_AUDIO_FORMAT_F32, 0, 0, conv_u24_32_to_f32_c },
-	{ SPA_AUDIO_FORMAT_U24_32, SPA_AUDIO_FORMAT_F32P, 0, 0, conv_u24_32_to_f32d_c },
+	MAKE(U24_32, F32, 0, conv_u24_32_to_f32_c),
+	MAKE(U24_32, F32P, 0, conv_u24_32_to_f32d_c),
 
-	{ SPA_AUDIO_FORMAT_S24_32, SPA_AUDIO_FORMAT_F32, 0, 0, conv_s24_32_to_f32_c },
-	{ SPA_AUDIO_FORMAT_S24_32P, SPA_AUDIO_FORMAT_F32P, 0, 0, conv_s24_32d_to_f32d_c },
-	{ SPA_AUDIO_FORMAT_S24_32, SPA_AUDIO_FORMAT_F32P, 0, 0, conv_s24_32_to_f32d_c },
-	{ SPA_AUDIO_FORMAT_S24_32P, SPA_AUDIO_FORMAT_F32, 0, 0, conv_s24_32d_to_f32_c },
+	MAKE(S24_32, F32, 0, conv_s24_32_to_f32_c),
+	MAKE(S24_32P, F32P, 0, conv_s24_32d_to_f32d_c),
+	MAKE(S24_32, F32P, 0, conv_s24_32_to_f32d_c),
+	MAKE(S24_32P, F32, 0, conv_s24_32d_to_f32_c),
 
-	{ SPA_AUDIO_FORMAT_S24_32_OE, SPA_AUDIO_FORMAT_F32P, 0, 0, conv_s24_32s_to_f32d_c },
+	MAKE(S24_32_OE, F32P, 0, conv_s24_32s_to_f32d_c),
 
-	{ SPA_AUDIO_FORMAT_F64, SPA_AUDIO_FORMAT_F32, 0, 0, conv_f64_to_f32_c },
-	{ SPA_AUDIO_FORMAT_F64P, SPA_AUDIO_FORMAT_F32P, 0, 0, conv_f64d_to_f32d_c },
-	{ SPA_AUDIO_FORMAT_F64, SPA_AUDIO_FORMAT_F32P, 0, 0, conv_f64_to_f32d_c },
-	{ SPA_AUDIO_FORMAT_F64P, SPA_AUDIO_FORMAT_F32, 0, 0, conv_f64d_to_f32_c },
+	MAKE(F64, F32, 0, conv_f64_to_f32_c),
+	MAKE(F64P, F32P, 0, conv_f64d_to_f32d_c),
+	MAKE(F64, F32P, 0, conv_f64_to_f32d_c),
+	MAKE(F64P, F32, 0, conv_f64d_to_f32_c),
 
-	{ SPA_AUDIO_FORMAT_F64_OE, SPA_AUDIO_FORMAT_F32P, 0, 0, conv_f64s_to_f32d_c },
+	MAKE(F64_OE, F32P, 0, conv_f64s_to_f32d_c),
 
 	/* from f32 */
-	{ SPA_AUDIO_FORMAT_F32, SPA_AUDIO_FORMAT_U8, 0, 0, conv_f32_to_u8_c },
-	{ SPA_AUDIO_FORMAT_F32P, SPA_AUDIO_FORMAT_U8P, 0, 0, conv_f32d_to_u8d_c },
-	{ SPA_AUDIO_FORMAT_F32, SPA_AUDIO_FORMAT_U8P, 0, 0, conv_f32_to_u8d_c },
-	{ SPA_AUDIO_FORMAT_F32P, SPA_AUDIO_FORMAT_U8, 0, 0, conv_f32d_to_u8_c },
-
-	{ SPA_AUDIO_FORMAT_F32, SPA_AUDIO_FORMAT_S8, 0, 0, conv_f32_to_s8_c },
-	{ SPA_AUDIO_FORMAT_F32P, SPA_AUDIO_FORMAT_S8P, 0, 0, conv_f32d_to_s8d_c },
-	{ SPA_AUDIO_FORMAT_F32, SPA_AUDIO_FORMAT_S8P, 0, 0, conv_f32_to_s8d_c },
-	{ SPA_AUDIO_FORMAT_F32P, SPA_AUDIO_FORMAT_S8, 0, 0, conv_f32d_to_s8_c },
-
-	{ SPA_AUDIO_FORMAT_F32P, SPA_AUDIO_FORMAT_ALAW, 0, 0, conv_f32d_to_alaw_c },
-	{ SPA_AUDIO_FORMAT_F32P, SPA_AUDIO_FORMAT_ULAW, 0, 0, conv_f32d_to_ulaw_c },
-
-	{ SPA_AUDIO_FORMAT_F32, SPA_AUDIO_FORMAT_U16, 0, 0, conv_f32_to_u16_c },
-	{ SPA_AUDIO_FORMAT_F32P, SPA_AUDIO_FORMAT_U16, 0, 0, conv_f32d_to_u16_c },
+	MAKE(F32, U8, 0, conv_f32_to_u8_c),
+	MAKE(F32P, U8P, 0, conv_f32d_to_u8d_shaped_c, 0, CONV_SHAPE),
+	MAKE(F32P, U8P, 0, conv_f32d_to_u8d_noise_c, 0, CONV_NOISE),
+	MAKE(F32P, U8P, 0, conv_f32d_to_u8d_c),
+	MAKE(F32, U8P, 0, conv_f32_to_u8d_c),
+	MAKE(F32P, U8, 0, conv_f32d_to_u8_shaped_c, 0, CONV_SHAPE),
+	MAKE(F32P, U8, 0, conv_f32d_to_u8_noise_c, 0, CONV_NOISE),
+	MAKE(F32P, U8, 0, conv_f32d_to_u8_c),
+
+	MAKE(F32, S8, 0, conv_f32_to_s8_c),
+	MAKE(F32P, S8P, 0, conv_f32d_to_s8d_shaped_c, 0, CONV_SHAPE),
+	MAKE(F32P, S8P, 0, conv_f32d_to_s8d_noise_c, 0, CONV_NOISE),
+	MAKE(F32P, S8P, 0, conv_f32d_to_s8d_c),
+	MAKE(F32, S8P, 0, conv_f32_to_s8d_c),
+	MAKE(F32P, S8, 0, conv_f32d_to_s8_shaped_c, 0, CONV_SHAPE),
+	MAKE(F32P, S8, 0, conv_f32d_to_s8_noise_c, 0, CONV_NOISE),
+	MAKE(F32P, S8, 0, conv_f32d_to_s8_c),
+
+	MAKE(F32P, ALAW, 0, conv_f32d_to_alaw_c),
+	MAKE(F32P, ULAW, 0, conv_f32d_to_ulaw_c),
+
+	MAKE(F32, U16, 0, conv_f32_to_u16_c),
+	MAKE(F32P, U16, 0, conv_f32d_to_u16_c),
 
 #if defined (HAVE_SSE2)
-	{ SPA_AUDIO_FORMAT_F32, SPA_AUDIO_FORMAT_S16, 0, SPA_CPU_FLAG_SSE2, conv_f32_to_s16_sse2 },
+	MAKE(F32, S16, 0, conv_f32_to_s16_sse2, SPA_CPU_FLAG_SSE2),
 #endif
-	{ SPA_AUDIO_FORMAT_F32, SPA_AUDIO_FORMAT_S16, 0, 0, conv_f32_to_s16_c },
+	MAKE(F32, S16, 0, conv_f32_to_s16_c),
 
+	MAKE(F32P, S16P, 0, conv_f32d_to_s16d_shaped_c, 0, CONV_SHAPE),
+#if defined (HAVE_SSE2)
+	MAKE(F32P, S16P, 0, conv_f32d_to_s16d_noise_sse2, SPA_CPU_FLAG_SSE2, CONV_NOISE),
+#endif
+	MAKE(F32P, S16P, 0, conv_f32d_to_s16d_noise_c, 0, CONV_NOISE),
 #if defined (HAVE_SSE2)
-	{ SPA_AUDIO_FORMAT_F32P, SPA_AUDIO_FORMAT_S16P, 0, SPA_CPU_FLAG_SSE2, conv_f32d_to_s16d_sse2 },
+	MAKE(F32P, S16P, 0, conv_f32d_to_s16d_sse2, SPA_CPU_FLAG_SSE2),
 #endif
-	{ SPA_AUDIO_FORMAT_F32P, SPA_AUDIO_FORMAT_S16P, 0, 0, conv_f32d_to_s16d_c },
+	MAKE(F32P, S16P, 0, conv_f32d_to_s16d_c),
 
-	{ SPA_AUDIO_FORMAT_F32, SPA_AUDIO_FORMAT_S16P, 0, 0, conv_f32_to_s16d_c },
+	MAKE(F32, S16P, 0, conv_f32_to_s16d_c),
 
+	MAKE(F32P, S16, 0, conv_f32d_to_s16_shaped_c, 0, CONV_SHAPE),
+#if defined (HAVE_SSE2)
+	MAKE(F32P, S16, 0, conv_f32d_to_s16_noise_sse2, SPA_CPU_FLAG_SSE2, CONV_NOISE),
+#endif
+	MAKE(F32P, S16, 0, conv_f32d_to_s16_noise_c, 0, CONV_NOISE),
 #if defined (HAVE_NEON)
-	{ SPA_AUDIO_FORMAT_F32P, SPA_AUDIO_FORMAT_S16, 0, SPA_CPU_FLAG_NEON, conv_f32d_to_s16_neon },
+	MAKE(F32P, S16, 0, conv_f32d_to_s16_neon, SPA_CPU_FLAG_NEON),
 #endif
 #if defined (HAVE_AVX2)
-	{ SPA_AUDIO_FORMAT_F32P, SPA_AUDIO_FORMAT_S16, 4, SPA_CPU_FLAG_AVX2, conv_f32d_to_s16_4_avx2 },
-	{ SPA_AUDIO_FORMAT_F32P, SPA_AUDIO_FORMAT_S16, 2, SPA_CPU_FLAG_AVX2, conv_f32d_to_s16_2_avx2 },
-	{ SPA_AUDIO_FORMAT_F32P, SPA_AUDIO_FORMAT_S16, 0, SPA_CPU_FLAG_AVX2, conv_f32d_to_s16_avx2 },
+	MAKE(F32P, S16, 4, conv_f32d_to_s16_4_avx2, SPA_CPU_FLAG_AVX2),
+	MAKE(F32P, S16, 2, conv_f32d_to_s16_2_avx2, SPA_CPU_FLAG_AVX2),
+	MAKE(F32P, S16, 0, conv_f32d_to_s16_avx2, SPA_CPU_FLAG_AVX2),
 #endif
 #if defined (HAVE_SSE2)
-	{ SPA_AUDIO_FORMAT_F32P, SPA_AUDIO_FORMAT_S16, 2, SPA_CPU_FLAG_SSE2, conv_f32d_to_s16_2_sse2 },
-	{ SPA_AUDIO_FORMAT_F32P, SPA_AUDIO_FORMAT_S16, 0, SPA_CPU_FLAG_SSE2, conv_f32d_to_s16_sse2 },
+	MAKE(F32P, S16, 2, conv_f32d_to_s16_2_sse2, SPA_CPU_FLAG_SSE2),
+	MAKE(F32P, S16, 0, conv_f32d_to_s16_sse2, SPA_CPU_FLAG_SSE2),
 #endif
-	{ SPA_AUDIO_FORMAT_F32P, SPA_AUDIO_FORMAT_S16, 0, 0, conv_f32d_to_s16_c },
+	MAKE(F32P, S16, 0, conv_f32d_to_s16_c),
+
+	MAKE(F32P, S16_OE, 0, conv_f32d_to_s16s_shaped_c, 0, CONV_SHAPE),
+	MAKE(F32P, S16_OE, 0, conv_f32d_to_s16s_noise_c, 0, CONV_NOISE),
+	MAKE(F32P, S16_OE, 0, conv_f32d_to_s16s_c),
 
-	{ SPA_AUDIO_FORMAT_F32P, SPA_AUDIO_FORMAT_S16_OE, 0, 0, conv_f32d_to_s16s_c },
+	MAKE(F32, U32, 0, conv_f32_to_u32_c),
+	MAKE(F32P, U32, 0, conv_f32d_to_u32_c),
 
-	{ SPA_AUDIO_FORMAT_F32, SPA_AUDIO_FORMAT_U32, 0, 0, conv_f32_to_u32_c },
-	{ SPA_AUDIO_FORMAT_F32P, SPA_AUDIO_FORMAT_U32, 0, 0, conv_f32d_to_u32_c },
+	MAKE(F32, S32, 0, conv_f32_to_s32_c),
+	MAKE(F32P, S32P, 0, conv_f32d_to_s32d_noise_c, 0, CONV_NOISE),
+	MAKE(F32P, S32P, 0, conv_f32d_to_s32d_c),
+	MAKE(F32, S32P, 0, conv_f32_to_s32d_c),
+
+#if defined (HAVE_SSE2)
+	MAKE(F32P, S32, 0, conv_f32d_to_s32_noise_sse2, SPA_CPU_FLAG_SSE2, CONV_NOISE),
+#endif
+	MAKE(F32P, S32, 0, conv_f32d_to_s32_noise_c, 0, CONV_NOISE),
 
-	{ SPA_AUDIO_FORMAT_F32, SPA_AUDIO_FORMAT_S32, 0, 0, conv_f32_to_s32_c },
-	{ SPA_AUDIO_FORMAT_F32P, SPA_AUDIO_FORMAT_S32P, 0, 0, conv_f32d_to_s32d_c },
-	{ SPA_AUDIO_FORMAT_F32, SPA_AUDIO_FORMAT_S32P, 0, 0, conv_f32_to_s32d_c },
 #if defined (HAVE_AVX2)
-	{ SPA_AUDIO_FORMAT_F32P, SPA_AUDIO_FORMAT_S32, 0, SPA_CPU_FLAG_AVX2, conv_f32d_to_s32_avx2 },
+	MAKE(F32P, S32, 0, conv_f32d_to_s32_avx2, SPA_CPU_FLAG_AVX2),
 #endif
 #if defined (HAVE_SSE2)
-	{ SPA_AUDIO_FORMAT_F32P, SPA_AUDIO_FORMAT_S32, 0, SPA_CPU_FLAG_SSE2, conv_f32d_to_s32_sse2 },
+	MAKE(F32P, S32, 0, conv_f32d_to_s32_sse2, SPA_CPU_FLAG_SSE2),
 #endif
-	{ SPA_AUDIO_FORMAT_F32P, SPA_AUDIO_FORMAT_S32, 0, 0, conv_f32d_to_s32_c },
+	MAKE(F32P, S32, 0, conv_f32d_to_s32_c),
 
-	{ SPA_AUDIO_FORMAT_F32P, SPA_AUDIO_FORMAT_S32_OE, 0, 0, conv_f32d_to_s32s_c },
+	MAKE(F32P, S32_OE, 0, conv_f32d_to_s32s_noise_c, 0, CONV_NOISE),
+	MAKE(F32P, S32_OE, 0, conv_f32d_to_s32s_c),
 
-	{ SPA_AUDIO_FORMAT_F32, SPA_AUDIO_FORMAT_U24, 0, 0, conv_f32_to_u24_c },
-	{ SPA_AUDIO_FORMAT_F32P, SPA_AUDIO_FORMAT_U24, 0, 0, conv_f32d_to_u24_c },
+	MAKE(F32, U24, 0, conv_f32_to_u24_c),
+	MAKE(F32P, U24, 0, conv_f32d_to_u24_c),
 
-	{ SPA_AUDIO_FORMAT_F32, SPA_AUDIO_FORMAT_S24, 0, 0, conv_f32_to_s24_c },
-	{ SPA_AUDIO_FORMAT_F32P, SPA_AUDIO_FORMAT_S24P, 0, 0, conv_f32d_to_s24d_c },
-	{ SPA_AUDIO_FORMAT_F32, SPA_AUDIO_FORMAT_S24P, 0, 0, conv_f32_to_s24d_c },
-	{ SPA_AUDIO_FORMAT_F32P, SPA_AUDIO_FORMAT_S24, 0, 0, conv_f32d_to_s24_c },
+	MAKE(F32, S24, 0, conv_f32_to_s24_c),
+	MAKE(F32P, S24P, 0, conv_f32d_to_s24d_noise_c, 0, CONV_NOISE),
+	MAKE(F32P, S24P, 0, conv_f32d_to_s24d_c),
+	MAKE(F32, S24P, 0, conv_f32_to_s24d_c),
+	MAKE(F32P, S24, 0, conv_f32d_to_s24_noise_c, 0, CONV_NOISE),
+	MAKE(F32P, S24, 0, conv_f32d_to_s24_c),
 
-	{ SPA_AUDIO_FORMAT_F32P, SPA_AUDIO_FORMAT_S24_OE, 0, 0, conv_f32d_to_s24s_c },
+	MAKE(F32P, S24_OE, 0, conv_f32d_to_s24s_noise_c, 0, CONV_NOISE),
+	MAKE(F32P, S24_OE, 0, conv_f32d_to_s24s_c),
 
-	{ SPA_AUDIO_FORMAT_F32, SPA_AUDIO_FORMAT_U24_32, 0, 0, conv_f32_to_u24_32_c },
-	{ SPA_AUDIO_FORMAT_F32P, SPA_AUDIO_FORMAT_U24_32, 0, 0, conv_f32d_to_u24_32_c },
+	MAKE(F32, U24_32, 0, conv_f32_to_u24_32_c),
+	MAKE(F32P, U24_32, 0, conv_f32d_to_u24_32_c),
 
-	{ SPA_AUDIO_FORMAT_F32, SPA_AUDIO_FORMAT_S24_32, 0, 0, conv_f32_to_s24_32_c },
-	{ SPA_AUDIO_FORMAT_F32P, SPA_AUDIO_FORMAT_S24_32P, 0, 0, conv_f32d_to_s24_32d_c },
-	{ SPA_AUDIO_FORMAT_F32, SPA_AUDIO_FORMAT_S24_32P, 0, 0, conv_f32_to_s24_32d_c },
-	{ SPA_AUDIO_FORMAT_F32P, SPA_AUDIO_FORMAT_S24_32, 0, 0, conv_f32d_to_s24_32_c },
+	MAKE(F32, S24_32, 0, conv_f32_to_s24_32_c),
+	MAKE(F32P, S24_32P, 0, conv_f32d_to_s24_32d_noise_c, 0, CONV_NOISE),
+	MAKE(F32P, S24_32P, 0, conv_f32d_to_s24_32d_c),
+	MAKE(F32, S24_32P, 0, conv_f32_to_s24_32d_c),
+	MAKE(F32P, S24_32, 0, conv_f32d_to_s24_32_noise_c, 0, CONV_NOISE),
+	MAKE(F32P, S24_32, 0, conv_f32d_to_s24_32_c),
 
-	{ SPA_AUDIO_FORMAT_F32P, SPA_AUDIO_FORMAT_S24_32_OE, 0, 0, conv_f32d_to_s24_32s_c },
+	MAKE(F32P, S24_32_OE, 0, conv_f32d_to_s24_32s_noise_c, 0, CONV_NOISE),
+	MAKE(F32P, S24_32_OE, 0, conv_f32d_to_s24_32s_c),
 
-	{ SPA_AUDIO_FORMAT_F32, SPA_AUDIO_FORMAT_F64, 0, 0, conv_f32_to_f64_c },
-	{ SPA_AUDIO_FORMAT_F32P, SPA_AUDIO_FORMAT_F64P, 0, 0, conv_f32d_to_f64d_c },
-	{ SPA_AUDIO_FORMAT_F32, SPA_AUDIO_FORMAT_F64P, 0, 0, conv_f32_to_f64d_c },
-	{ SPA_AUDIO_FORMAT_F32P, SPA_AUDIO_FORMAT_F64, 0, 0, conv_f32d_to_f64_c },
+	MAKE(F32, F64, 0, conv_f32_to_f64_c),
+	MAKE(F32P, F64P, 0, conv_f32d_to_f64d_c),
+	MAKE(F32, F64P, 0, conv_f32_to_f64d_c),
+	MAKE(F32P, F64, 0, conv_f32d_to_f64_c),
 
-	{ SPA_AUDIO_FORMAT_F32P, SPA_AUDIO_FORMAT_F64_OE, 0, 0, conv_f32d_to_f64s_c },
+	MAKE(F32P, F64_OE, 0, conv_f32d_to_f64s_c),
 
 	/* u8 */
-	{ SPA_AUDIO_FORMAT_U8, SPA_AUDIO_FORMAT_U8, 0, 0, conv_copy8_c },
-	{ SPA_AUDIO_FORMAT_U8P, SPA_AUDIO_FORMAT_U8P, 0, 0, conv_copy8d_c },
-	{ SPA_AUDIO_FORMAT_U8, SPA_AUDIO_FORMAT_U8P, 0, 0, conv_deinterleave_8_c },
-	{ SPA_AUDIO_FORMAT_U8P, SPA_AUDIO_FORMAT_U8, 0, 0, conv_interleave_8_c },
+	MAKE(U8, U8, 0, conv_copy8_c),
+	MAKE(U8P, U8P, 0, conv_copy8d_c),
+	MAKE(U8, U8P, 0, conv_8_to_8d_c),
+	MAKE(U8P, U8, 0, conv_8d_to_8_c),
 
 	/* s8 */
-	{ SPA_AUDIO_FORMAT_S8, SPA_AUDIO_FORMAT_S8, 0, 0, conv_copy8_c },
-	{ SPA_AUDIO_FORMAT_S8P, SPA_AUDIO_FORMAT_S8P, 0, 0, conv_copy8d_c },
-	{ SPA_AUDIO_FORMAT_S8, SPA_AUDIO_FORMAT_S8P, 0, 0, conv_deinterleave_8_c },
-	{ SPA_AUDIO_FORMAT_S8P, SPA_AUDIO_FORMAT_S8, 0, 0, conv_interleave_8_c },
+	MAKE(S8, S8, 0, conv_copy8_c),
+	MAKE(S8P, S8P, 0, conv_copy8d_c),
+	MAKE(S8, S8P, 0, conv_8_to_8d_c),
+	MAKE(S8P, S8, 0, conv_8d_to_8_c),
 
 	/* alaw */
-	{ SPA_AUDIO_FORMAT_ALAW, SPA_AUDIO_FORMAT_ALAW, 0, 0, conv_copy8_c },
+	MAKE(ALAW, ALAW, 0, conv_copy8_c),
 	/* ulaw */
-	{ SPA_AUDIO_FORMAT_ULAW, SPA_AUDIO_FORMAT_ULAW, 0, 0, conv_copy8_c },
+	MAKE(ULAW, ULAW, 0, conv_copy8_c),
 
 	/* s16 */
-	{ SPA_AUDIO_FORMAT_S16, SPA_AUDIO_FORMAT_S16, 0, 0, conv_copy16_c },
-	{ SPA_AUDIO_FORMAT_S16P, SPA_AUDIO_FORMAT_S16P, 0, 0, conv_copy16d_c },
-	{ SPA_AUDIO_FORMAT_S16, SPA_AUDIO_FORMAT_S16P, 0, 0, conv_deinterleave_16_c },
-	{ SPA_AUDIO_FORMAT_S16P, SPA_AUDIO_FORMAT_S16, 0, 0, conv_interleave_16_c },
+	MAKE(S16, S16, 0, conv_copy16_c),
+	MAKE(S16P, S16P, 0, conv_copy16d_c),
+	MAKE(S16, S16P, 0, conv_16_to_16d_c),
+	MAKE(S16P, S16, 0, conv_16d_to_16_c),
 
 	/* s32 */
-	{ SPA_AUDIO_FORMAT_S32, SPA_AUDIO_FORMAT_S32, 0, 0, conv_copy32_c },
-	{ SPA_AUDIO_FORMAT_S32P, SPA_AUDIO_FORMAT_S32P, 0, 0, conv_copy32d_c },
+	MAKE(S32, S32, 0, conv_copy32_c),
+	MAKE(S32P, S32P, 0, conv_copy32d_c),
 #if defined (HAVE_SSE2)
-	{ SPA_AUDIO_FORMAT_S32, SPA_AUDIO_FORMAT_S32P, 0, 0, conv_deinterleave_32_sse2 },
+	MAKE(S32, S32P, 0, conv_32_to_32d_sse2, SPA_CPU_FLAG_SSE2),
 #endif
-	{ SPA_AUDIO_FORMAT_S32, SPA_AUDIO_FORMAT_S32P, 0, 0, conv_deinterleave_32_c },
+	MAKE(S32, S32P, 0, conv_32_to_32d_c),
 #if defined (HAVE_SSE2)
-	{ SPA_AUDIO_FORMAT_S32P, SPA_AUDIO_FORMAT_S32, 0, 0, conv_interleave_32_sse2 },
+	MAKE(S32P, S32, 0, conv_32d_to_32_sse2, SPA_CPU_FLAG_SSE2),
 #endif
-	{ SPA_AUDIO_FORMAT_S32P, SPA_AUDIO_FORMAT_S32, 0, 0, conv_interleave_32_c },
+	MAKE(S32P, S32, 0, conv_32d_to_32_c),
 
 	/* s24 */
-	{ SPA_AUDIO_FORMAT_S24, SPA_AUDIO_FORMAT_S24, 0, 0, conv_copy24_c },
-	{ SPA_AUDIO_FORMAT_S24P, SPA_AUDIO_FORMAT_S24P, 0, 0, conv_copy24d_c },
-	{ SPA_AUDIO_FORMAT_S24, SPA_AUDIO_FORMAT_S24P, 0, 0, conv_deinterleave_24_c },
-	{ SPA_AUDIO_FORMAT_S24P, SPA_AUDIO_FORMAT_S24, 0, 0, conv_interleave_24_c },
+	MAKE(S24, S24, 0, conv_copy24_c),
+	MAKE(S24P, S24P, 0, conv_copy24d_c),
+	MAKE(S24, S24P, 0, conv_24_to_24d_c),
+	MAKE(S24P, S24, 0, conv_24d_to_24_c),
 
 	/* s24_32 */
-	{ SPA_AUDIO_FORMAT_S24_32, SPA_AUDIO_FORMAT_S24_32, 0, 0, conv_copy32_c },
-	{ SPA_AUDIO_FORMAT_S24_32P, SPA_AUDIO_FORMAT_S24_32P, 0, 0, conv_copy32d_c },
+	MAKE(S24_32, S24_32, 0, conv_copy32_c),
+	MAKE(S24_32P, S24_32P, 0, conv_copy32d_c),
 #if defined (HAVE_SSE2)
-	{ SPA_AUDIO_FORMAT_S24_32, SPA_AUDIO_FORMAT_S24_32P, 0, 0, conv_deinterleave_32_sse2 },
+	MAKE(S24_32, S24_32P, 0, conv_32_to_32d_sse2, SPA_CPU_FLAG_SSE2),
 #endif
-	{ SPA_AUDIO_FORMAT_S24_32, SPA_AUDIO_FORMAT_S24_32P, 0, 0, conv_deinterleave_32_c },
+	MAKE(S24_32, S24_32P, 0, conv_32_to_32d_c),
 #if defined (HAVE_SSE2)
-	{ SPA_AUDIO_FORMAT_S24_32P, SPA_AUDIO_FORMAT_S24_32, 0, 0, conv_interleave_32_sse2 },
+	MAKE(S24_32P, S24_32, 0, conv_32d_to_32_sse2, SPA_CPU_FLAG_SSE2),
 #endif
-	{ SPA_AUDIO_FORMAT_S24_32P, SPA_AUDIO_FORMAT_S24_32, 0, 0, conv_interleave_32_c },
+	MAKE(S24_32P, S24_32, 0, conv_32d_to_32_c),
 
 	/* F64 */
-	{ SPA_AUDIO_FORMAT_F64, SPA_AUDIO_FORMAT_F64, 0, 0, conv_copy64_c },
-	{ SPA_AUDIO_FORMAT_F64P, SPA_AUDIO_FORMAT_F64P, 0, 0, conv_copy64d_c },
-	{ SPA_AUDIO_FORMAT_F64, SPA_AUDIO_FORMAT_F64P, 0, 0, conv_deinterleave_64_c },
-	{ SPA_AUDIO_FORMAT_F64P, SPA_AUDIO_FORMAT_F64, 0, 0, conv_interleave_64_c },
+	MAKE(F64, F64, 0, conv_copy64_c),
+	MAKE(F64P, F64P, 0, conv_copy64d_c),
+	MAKE(F64, F64P, 0, conv_64_to_64d_c),
+	MAKE(F64P, F64, 0, conv_64d_to_64_c),
 };
+#undef MAKE
 
 #define MATCH_CHAN(a,b)		((a) == 0 || (a) == (b))
 #define MATCH_CPU_FLAGS(a,b)	((a) == 0 || ((a) & (b)) == a)
+#define MATCH_DITHER(a,b)	((a) == 0 || ((a) & (b)) == a)
 
 static const struct conv_info *find_conv_info(uint32_t src_fmt, uint32_t dst_fmt,
-		uint32_t n_channels, uint32_t cpu_flags)
+		uint32_t n_channels, uint32_t cpu_flags, uint32_t conv_flags)
 {
 	size_t i;
 
@@ -317,7 +364,8 @@ static const struct conv_info *find_conv_info(uint32_t src_fmt, uint32_t dst_fmt
 		if (conv_table[i].src_fmt == src_fmt &&
 		    conv_table[i].dst_fmt == dst_fmt &&
 		    MATCH_CHAN(conv_table[i].n_channels, n_channels) &&
-		    MATCH_CPU_FLAGS(conv_table[i].cpu_flags, cpu_flags))
+		    MATCH_CPU_FLAGS(conv_table[i].cpu_flags, cpu_flags) &&
+		    MATCH_DITHER(conv_table[i].conv_flags, conv_flags))
 			return &conv_table[i];
 	}
 	return NULL;
@@ -326,20 +374,140 @@ static const struct conv_info *find_conv_info(uint32_t src_fmt, uint32_t dst_fmt
 static void impl_convert_free(struct convert *conv)
 {
 	conv->process = NULL;
+	free(conv->noise);
+	conv->noise = NULL;
+}
+
+static bool need_dither(uint32_t format)
+{
+	switch (format) {
+	case SPA_AUDIO_FORMAT_U8:
+	case SPA_AUDIO_FORMAT_U8P:
+	case SPA_AUDIO_FORMAT_S8:
+	case SPA_AUDIO_FORMAT_S8P:
+	case SPA_AUDIO_FORMAT_ULAW:
+	case SPA_AUDIO_FORMAT_ALAW:
+	case SPA_AUDIO_FORMAT_S16P:
+	case SPA_AUDIO_FORMAT_S16:
+	case SPA_AUDIO_FORMAT_S16_OE:
+		return true;
+	}
+	return false;
+}
+
+/* filters based on F-weighted curves
+ * from 'Psychoacoustically Optimal Noise Shaping' (**)
+ * this filter is the "F-Weighted" noise filter described by Wannamaker
+ * It is designed to produce minimum audibility: */
+static const float wan3[] = { /* Table 3; 3 Coefficients */
+	1.623f, -0.982f, 0.109f
+};
+/* Noise shaping coefficients from[1], moves most power of the
+ * error noise into inaudible frequency ranges.
+ *
+ * [1]
+ * "Minimally Audible Noise Shaping", Stanley P. Lipshitz,
+ * John Vanderkooy, and Robert A. Wannamaker,
+ * J. Audio Eng. Soc., Vol. 39, No. 11, November 1991. */
+static const float lips44[] = { /* improved E-weighted (appendix: 5) */
+	2.033f, -2.165f, 1.959f, -1.590f, 0.6149f
+};
+
+static const struct dither_info {
+	uint32_t method;
+	uint32_t noise_method;
+	uint32_t rate;
+	const float *ns;
+	uint32_t n_ns;
+} dither_info[] = {
+	{ DITHER_METHOD_NONE, NOISE_METHOD_NONE, },
+	{ DITHER_METHOD_RECTANGULAR, NOISE_METHOD_RECTANGULAR, },
+	{ DITHER_METHOD_TRIANGULAR, NOISE_METHOD_TRIANGULAR, },
+	{ DITHER_METHOD_TRIANGULAR_HF, NOISE_METHOD_TRIANGULAR_HF, },
+	{ DITHER_METHOD_WANNAMAKER_3, NOISE_METHOD_TRIANGULAR_HF, 44100, wan3, SPA_N_ELEMENTS(wan3) },
+	{ DITHER_METHOD_LIPSHITZ, NOISE_METHOD_TRIANGULAR, 44100, lips44, SPA_N_ELEMENTS(lips44) }
+};
+
+static const struct dither_info *find_dither_info(uint32_t method, uint32_t rate)
+{
+	size_t i;
+
+	for (i = 0; i < SPA_N_ELEMENTS(dither_info); i++) {
+		const struct dither_info *di = &dither_info[i];
+		if (di->method != method)
+			continue;
+		/* don't use shaped for too low rates, it moves the noise to
+		 * audible ranges */
+		if (di->ns != NULL && rate < di->rate * 3 / 4)
+			return find_dither_info(DITHER_METHOD_TRIANGULAR_HF, rate);
+		return &dither_info[i];
+	}
+	return NULL;
 }
 
 int convert_init(struct convert *conv)
 {
 	const struct conv_info *info;
+	const struct dither_info *dinfo;
+	uint32_t i, conv_flags;
+
+	conv->scale = 1.0f / (float)(INT32_MAX);
+
+	/* disable dither if not needed */
+	if (!need_dither(conv->dst_fmt))
+		conv->method = DITHER_METHOD_NONE;
+
+	dinfo = find_dither_info(conv->method, conv->rate);
+	if (dinfo == NULL)
+		return -EINVAL;
+
+	conv->noise_method = dinfo->noise_method;
+	if (conv->noise_bits > 0) {
+		switch (conv->noise_method) {
+		case NOISE_METHOD_NONE:
+			conv->noise_method = NOISE_METHOD_PATTERN;
+			conv->scale = -1.0f * (1 << (conv->noise_bits-1));
+			break;
+		case NOISE_METHOD_RECTANGULAR:
+			conv->noise_method = NOISE_METHOD_TRIANGULAR;
+			SPA_FALLTHROUGH;
+		case NOISE_METHOD_TRIANGULAR:
+		case NOISE_METHOD_TRIANGULAR_HF:
+			conv->scale *= (1 << (conv->noise_bits-1));
+			break;
+		}
+	}
+	if (conv->noise_method < NOISE_METHOD_TRIANGULAR)
+		conv->scale *= 0.5f;
+
+	conv_flags = 0;
+	if (conv->noise_method != NOISE_METHOD_NONE)
+		conv_flags |= CONV_NOISE;
+	if (dinfo->n_ns > 0) {
+		conv_flags |= CONV_SHAPE;
+		conv->n_ns = dinfo->n_ns;
+		conv->ns = dinfo->ns;
+	}
 
-	info = find_conv_info(conv->src_fmt, conv->dst_fmt, conv->n_channels, conv->cpu_flags);
+	info = find_conv_info(conv->src_fmt, conv->dst_fmt, conv->n_channels,
+			conv->cpu_flags, conv_flags);
 	if (info == NULL)
 		return -ENOTSUP;
 
+	conv->noise_size = DITHER_SIZE;
+	conv->noise = calloc(conv->noise_size + 16 +
+			FMT_OPS_MAX_ALIGN / sizeof(float), sizeof(float));
+	if (conv->noise == NULL)
+		return -errno;
+
+	for (i = 0; i < SPA_N_ELEMENTS(conv->random); i++)
+		conv->random[i] = random();
+
 	conv->is_passthrough = conv->src_fmt == conv->dst_fmt;
 	conv->cpu_flags = info->cpu_flags;
 	conv->process = info->process;
 	conv->free = impl_convert_free;
+	conv->func_name = info->name;
 
 	return 0;
 }
diff --git a/spa/plugins/audioconvert/fmt-ops.h b/spa/plugins/audioconvert/fmt-ops.h
index 3f31e1a4762bc51842beada35f832279ef8bcef4..a4cd1de1125e0ea06b6cc777b03421a3fb6d508d 100644
--- a/spa/plugins/audioconvert/fmt-ops.h
+++ b/spa/plugins/audioconvert/fmt-ops.h
@@ -23,7 +23,7 @@
  */
 
 #include <math.h>
-#ifdef __FreeBSD__
+#if defined(__FreeBSD__) || defined(__MidnightBSD__)
 #include <sys/endian.h>
 #define bswap_16 bswap16
 #define bswap_32 bswap32
@@ -33,158 +33,212 @@
 #endif
 
 #include <spa/utils/defs.h>
+#include <spa/utils/string.h>
 
-#define U8_MIN		0
-#define U8_MAX		255
-#define U8_SCALE	127.5f
-#define U8_OFFS		128
-#define U8_TO_F32(v)	((((uint8_t)(v)) * (1.0f / U8_OFFS)) - 1.0)
-#define F32_TO_U8(v)	(uint8_t)((SPA_CLAMP(v, -1.0f, 1.0f) * U8_SCALE) + U8_OFFS)
-
-#define S8_MIN		-127
-#define S8_MAX		127
-#define S8_MAX_F	127.0f
-#define S8_SCALE	127.0f
-#define S8_TO_F32(v)	(((int8_t)(v)) * (1.0f / S8_SCALE))
-#define F32_TO_S8(v)	(int8_t)(SPA_CLAMP(v, -1.0f, 1.0f) * S8_SCALE)
-
-#define U16_MIN		0
-#define U16_MAX		65535
-#define U16_SCALE	32767.5f
-#define U16_OFFS	32768
-#define U16_TO_F32(v)	((((uint16_t)(v)) * (1.0f / U16_OFFS)) - 1.0)
-#define U16S_TO_F32(v)	(((uint16_t)bswap_16((uint16_t)(v)) * (1.0f / U16_OFFS)) - 1.0)
-#define F32_TO_U16(v)	(uint16_t)((SPA_CLAMP(v, -1.0f, 1.0f) * U16_SCALE) + U16_OFFS)
-#define F32_TO_U16S(v)	((uint16_t)bswap_16((uint16_t)((SPA_CLAMP(v, -1.0f, 1.0f) * U16_SCALE) + U16_OFFS)))
-
-#define S16_MIN		-32767
-#define S16_MAX		32767
-#define S16_MAX_F	32767.0f
-#define S16_SCALE	32767.0f
-#define S16_TO_F32(v)	(((int16_t)(v)) * (1.0f / S16_SCALE))
-#define S16S_TO_F32(v)	(((int16_t)bswap_16((uint16_t)v)) * (1.0f / S16_SCALE))
-#define F32_TO_S16(v)	(int16_t)(SPA_CLAMP(v, -1.0f, 1.0f) * S16_SCALE)
-#define F32_TO_S16S(v)	((int16_t)bswap_16((uint16_t)(SPA_CLAMP(v, -1.0f, 1.0f) * S16_SCALE)))
-
-#define U24_MIN		0
-#define U24_MAX		16777215
-#define U24_SCALE	8388607.5f
-#define U24_OFFS	8388608
-#define U24_TO_F32(v)	((((uint32_t)(v)) * (1.0f / U24_OFFS)) - 1.0)
-#define F32_TO_U24(v)	(uint32_t)((SPA_CLAMP(v, -1.0f, 1.0f) * U24_SCALE) + U24_OFFS)
-
-#define S24_MIN		-8388607
-#define S24_MAX		8388607
-#define S24_MAX_F	8388607.0f
-#define S24_SCALE	8388607.0f
-#define S24_TO_F32(v)	(((int32_t)(v)) * (1.0f / S24_SCALE))
-#define F32_TO_S24(v)	(int32_t)(SPA_CLAMP(v, -1.0f, 1.0f) * S24_SCALE)
-
-#define U32_TO_F32(v)	U24_TO_F32(((uint32_t)(v)) >> 8)
-#define F32_TO_U32(v)	(F32_TO_U24(v) << 8)
-
-#define S32_SCALE	2147483648.0f
-#define S32_MIN		2147483520.0f
-
-#define S32_TO_F32(v)	S24_TO_F32(((int32_t)(v)) >> 8)
-#define S32S_TO_F32(v)	S24_TO_F32(((int32_t)bswap_32(v)) >> 8)
-#define F32_TO_S32(v)	(F32_TO_S24(v) << 8)
-#define F32_TO_S32S(v)	bswap_32((F32_TO_S24(v) << 8))
+#define f32_round(a)	lrintf(a)
+
+#define ITOF(type,v,scale,offs) \
+	(((type)(v)) * (1.0f / (scale)) - (offs))
+#define FTOI(type,v,scale,offs,noise,min,max) \
+	(type)f32_round(SPA_CLAMPF((v) * (scale) + (offs) + (noise), min, max))
+
+#define FMT_OPS_MAX_ALIGN	32
+
+#define U8_MIN			0u
+#define U8_MAX			255u
+#define U8_SCALE		128.f
+#define U8_OFFS			128.f
+#define U8_TO_F32(v)		ITOF(uint8_t, v, U8_SCALE, 1.0f)
+#define F32_TO_U8_D(v,d)	FTOI(uint8_t, v, U8_SCALE, U8_OFFS, d, U8_MIN, U8_MAX)
+#define F32_TO_U8(v)		F32_TO_U8_D(v, 0.0f)
+
+#define S8_MIN			-128
+#define S8_MAX			127
+#define S8_SCALE		128.0f
+#define S8_TO_F32(v)		ITOF(int8_t, v, S8_SCALE, 0.0f)
+#define F32_TO_S8_D(v,d)	FTOI(int8_t, v, S8_SCALE, 0.0f, d, S8_MIN, S8_MAX)
+#define F32_TO_S8(v)		F32_TO_S8_D(v, 0.0f);
+
+#define U16_MIN			0u
+#define U16_MAX			65535u
+#define U16_SCALE		32768.f
+#define U16_OFFS		32768.f
+#define U16_TO_F32(v)		ITOF(uint16_t, v, U16_SCALE, 1.0f)
+#define U16S_TO_F32(v)		U16_TO_F32(bswap_16(v))
+#define F32_TO_U16_D(v,d)	FTOI(uint16_t, v, U16_SCALE, U16_OFFS, d, U16_MIN, U16_MAX)
+#define F32_TO_U16(v)		F32_TO_U16_D(v, 0.0f);
+#define F32_TO_U16S_D(v,d)	bswap_16(F32_TO_U16_D(v,d))
+#define F32_TO_U16S(v)		bswap_16(F32_TO_U16(v))
+
+#define S16_MIN			-32768
+#define S16_MAX			32767
+#define S16_SCALE		32768.0f
+#define S16_TO_F32(v)		ITOF(int16_t, v, S16_SCALE, 0.0f)
+#define S16S_TO_F32(v)		S16_TO_F32(bswap_16(v))
+#define F32_TO_S16_D(v,d)	FTOI(int16_t, v, S16_SCALE, 0.0f, d, S16_MIN, S16_MAX)
+#define F32_TO_S16(v)		F32_TO_S16_D(v, 0.0f)
+#define F32_TO_S16S_D(v,d)	bswap_16(F32_TO_S16_D(v,d))
+#define F32_TO_S16S(v)		bswap_16(F32_TO_S16(v))
+
+#define U24_MIN			0u
+#define U24_MAX			16777215u
+#define U24_SCALE		8388608.f
+#define U24_OFFS		8388608.f
+#define U24_TO_F32(v)		ITOF(uint32_t, u24_to_u32(v), U24_SCALE, 1.0f)
+#define F32_TO_U24_D(v,d)	u32_to_u24(FTOI(uint32_t, v, U24_SCALE, U24_OFFS, d, U24_MIN, U24_MAX))
+#define F32_TO_U24(v)		F32_TO_U24_D(v, 0.0f)
+
+#define S24_MIN			-8388608
+#define S24_MAX			8388607
+#define S24_SCALE		8388608.0f
+#define S24_TO_F32(v)		ITOF(int32_t, s24_to_s32(v), S24_SCALE, 0.0f)
+#define S24S_TO_F32(v)		S24_TO_F32(bswap_s24(v))
+#define F32_TO_S24_D(v,d)	s32_to_s24(FTOI(int32_t, v, S24_SCALE, 0.0f, d, S24_MIN, S24_MAX))
+#define F32_TO_S24(v)		F32_TO_S24_D(v, 0.0f)
+#define F32_TO_S24S(v)		bswap_s24(F32_TO_S24(v))
 
 #define U24_32_TO_F32(v)	U32_TO_F32((v)<<8)
-#define U24_32S_TO_F32(v)	U32_TO_F32(((int32_t)bswap_32(v))<<8)
-#define F32_TO_U24_32(v)	F32_TO_U24(v)
-#define F32_TO_U24_32S(v)	bswap_32(F32_TO_U24(v))
+#define U24_32S_TO_F32(v)	U24_32_TO_F32(bswap_32(v))
+#define F32_TO_U24_32_D(v,d)	FTOI(uint32_t, v, U24_SCALE, U24_OFFS, d, U24_MIN, U24_MAX)
+#define F32_TO_U24_32(v)	F32_TO_U24_32_D(v, 0.0f)
+#define F32_TO_U24_32S(v)	bswap_32(F32_TO_U24_32(v))
+#define F32_TO_U24_32S_D(v,d)	bswap_32(F32_TO_U24_32_D(v,d))
+
+#define U32_MIN			0u
+#define U32_MAX			4294967295u
+#define U32_SCALE		2147483648.f
+#define U32_OFFS		2147483648.f
+#define U32_TO_F32(v)		ITOF(uint32_t, (v) >> 8, U24_SCALE, 1.0f)
+#define F32_TO_U32(v)		(F32_TO_U24_32(v) << 8)
+#define F32_TO_U32_D(v,d)	(F32_TO_U24_32_D(v,d) << 8)
 
 #define S24_32_TO_F32(v)	S32_TO_F32((v)<<8)
-#define S24_32S_TO_F32(v)	S32_TO_F32(((int32_t)bswap_32(v))<<8)
-#define F32_TO_S24_32(v)	F32_TO_S24(v)
-#define F32_TO_S24_32S(v)	bswap_32(F32_TO_S24(v))
+#define S24_32S_TO_F32(v)	S24_32_TO_F32(bswap_32(v))
+#define F32_TO_S24_32_D(v,d)	FTOI(int32_t, v, S24_SCALE, 0.0f, d, S24_MIN, S24_MAX)
+#define F32_TO_S24_32(v)	F32_TO_S24_32_D(v, 0.0f)
+#define F32_TO_S24_32S(v)	bswap_32(F32_TO_S24_32(v))
+#define F32_TO_S24_32S_D(v,d)	bswap_32(F32_TO_S24_32_D(v,d))
 
-static inline uint32_t read_u24(const void *src)
-{
-	const uint8_t *s = src;
+#define S32_MIN			(S24_MIN * 256)
+#define S32_MAX			(S24_MAX * 256)
+#define S32_TO_F32(v)		ITOF(int32_t, (v) >> 8, S24_SCALE, 0.0f)
+#define S32S_TO_F32(v)		S32_TO_F32(bswap_32(v))
+#define F32_TO_S32(v)		(F32_TO_S24_32(v) << 8)
+#define F32_TO_S32_D(v,d)	(F32_TO_S24_32_D(v,d) << 8)
+#define F32_TO_S32S(v)		bswap_32(F32_TO_S32(v))
+#define F32_TO_S32S_D(v,d)	bswap_32(F32_TO_S32_D(v,d))
+
+typedef struct {
 #if __BYTE_ORDER == __LITTLE_ENDIAN
-	return (((uint32_t)s[2] << 16) | ((uint32_t)(uint8_t)s[1] << 8) | (uint32_t)(uint8_t)s[0]);
+	uint8_t v3;
+	uint8_t v2;
+	uint8_t v1;
 #else
-	return (((uint32_t)s[0] << 16) | ((uint32_t)(uint8_t)s[1] << 8) | (uint32_t)(uint8_t)s[2]);
+	uint8_t v1;
+	uint8_t v2;
+	uint8_t v3;
 #endif
-}
+} __attribute__ ((packed)) uint24_t;
 
-static inline int32_t read_s24(const void *src)
-{
-	const int8_t *s = src;
+typedef struct {
 #if __BYTE_ORDER == __LITTLE_ENDIAN
-	return (((int32_t)s[2] << 16) | ((uint32_t)(uint8_t)s[1] << 8) | (uint32_t)(uint8_t)s[0]);
+	uint8_t v3;
+	uint8_t v2;
+	int8_t v1;
 #else
-	return (((int32_t)s[0] << 16) | ((uint32_t)(uint8_t)s[1] << 8) | (uint32_t)(uint8_t)s[2]);
+	int8_t v1;
+	uint8_t v2;
+	uint8_t v3;
 #endif
+} __attribute__ ((packed)) int24_t;
+
+static inline uint32_t u24_to_u32(uint24_t src)
+{
+	return ((uint32_t)src.v1 << 16) | ((uint32_t)src.v2 << 8) | (uint32_t)src.v3;
 }
 
-static inline int32_t read_s24s(const void *src)
+#define U32_TO_U24(s) (uint24_t) { .v1 = (uint8_t)(((uint32_t)s) >> 16), \
+	.v2 = (uint8_t)(((uint32_t)s) >> 8), .v3 = (uint8_t)((uint32_t)s) }
+
+static inline uint24_t u32_to_u24(uint32_t src)
 {
-	const int8_t *s = src;
-#if __BYTE_ORDER == __LITTLE_ENDIAN
-	return (((int32_t)s[0] << 16) | ((uint32_t)(uint8_t)s[1] << 8) | (uint32_t)(uint8_t)s[2]);
-#else
-	return (((int32_t)s[2] << 16) | ((uint32_t)(uint8_t)s[1] << 8) | (uint32_t)(uint8_t)s[0]);
-#endif
+	return U32_TO_U24(src);
 }
 
-static inline void write_u24(void *dst, uint32_t val)
+static inline int32_t s24_to_s32(int24_t src)
 {
-	uint8_t *d = dst;
-#if __BYTE_ORDER == __LITTLE_ENDIAN
-	d[0] = (uint8_t) (val);
-	d[1] = (uint8_t) (val >> 8);
-	d[2] = (uint8_t) (val >> 16);
-#else
-	d[0] = (uint8_t) (val >> 16);
-	d[1] = (uint8_t) (val >> 8);
-	d[2] = (uint8_t) (val);
-#endif
+	return ((int32_t)src.v1 << 16) | ((uint32_t)src.v2 << 8) | (uint32_t)src.v3;
 }
 
-static inline void write_s24(void *dst, int32_t val)
+#define S32_TO_S24(s) (int24_t) { .v1 = (int8_t)(((int32_t)s) >> 16), \
+	.v2 = (uint8_t)(((uint32_t)s) >> 8), .v3 = (uint8_t)((uint32_t)s) }
+
+static inline int24_t s32_to_s24(int32_t src)
 {
-	uint8_t *d = dst;
-#if __BYTE_ORDER == __LITTLE_ENDIAN
-	d[0] = (uint8_t) (val);
-	d[1] = (uint8_t) (val >> 8);
-	d[2] = (uint8_t) (val >> 16);
-#else
-	d[0] = (uint8_t) (val >> 16);
-	d[1] = (uint8_t) (val >> 8);
-	d[2] = (uint8_t) (val);
-#endif
+	return S32_TO_S24(src);
 }
 
-static inline void write_s24s(void *dst, int32_t val)
+static inline uint24_t bswap_u24(uint24_t src)
 {
-	uint8_t *d = dst;
-#if __BYTE_ORDER == __LITTLE_ENDIAN
-	d[0] = (uint8_t) (val >> 16);
-	d[1] = (uint8_t) (val >> 8);
-	d[2] = (uint8_t) (val);
-#else
-	d[0] = (uint8_t) (val);
-	d[1] = (uint8_t) (val >> 8);
-	d[2] = (uint8_t) (val >> 16);
-#endif
+	return (uint24_t) { .v1 = src.v3, .v2 = src.v2, .v3 = src.v1 };
+}
+static inline int24_t bswap_s24(int24_t src)
+{
+	return (int24_t) { .v1 = src.v3, .v2 = src.v2, .v3 = src.v1 };
 }
 
-#define MAX_NS	64
+#define F32_TO_F32S(v) \
+	bswap_32((union { uint32_t i; float f; }){ .f = (v) }.i)
+#define F32S_TO_F32(v) \
+	((union { uint32_t i; float f; }){ .i = bswap_32(v) }.f)
+
+#define F64_TO_F64S(v) \
+	bswap_32((union { uint64_t i; double d; }){ .d = (v) }.i)
+#define F64S_TO_F64(v) \
+	((union { uint64_t i; double d; }){ .i = bswap_32(v) }.d)
+
+#define NS_MAX	8
+#define NS_MASK	(NS_MAX-1)
+
+struct shaper {
+	float e[NS_MAX * 2];
+	uint32_t idx;
+	float r;
+};
 
 struct convert {
+	uint32_t noise_bits;
+#define DITHER_METHOD_NONE		0
+#define DITHER_METHOD_RECTANGULAR	1
+#define DITHER_METHOD_TRIANGULAR	2
+#define DITHER_METHOD_TRIANGULAR_HF	3
+#define DITHER_METHOD_WANNAMAKER_3	4
+#define DITHER_METHOD_LIPSHITZ		5
+	uint32_t method;
+
 	uint32_t src_fmt;
 	uint32_t dst_fmt;
 	uint32_t n_channels;
+	uint32_t rate;
 	uint32_t cpu_flags;
+	const char *func_name;
 
 	unsigned int is_passthrough:1;
-	float ns_data[MAX_NS];
-	uint32_t ns_idx;
-	uint32_t ns_size;
+
+	float scale;
+	uint32_t random[16 + FMT_OPS_MAX_ALIGN/4];
+	int32_t prev[16 + FMT_OPS_MAX_ALIGN/4];
+#define NOISE_METHOD_NONE		0
+#define NOISE_METHOD_RECTANGULAR	1
+#define NOISE_METHOD_TRIANGULAR		2
+#define NOISE_METHOD_TRIANGULAR_HF	3
+#define NOISE_METHOD_PATTERN		4
+	uint32_t noise_method;
+	float *noise;
+	uint32_t noise_size;
+	const float *ns;
+	uint32_t n_ns;
+	struct shaper shaper[64];
 
 	void (*process) (struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
 			uint32_t n_samples);
@@ -193,6 +247,35 @@ struct convert {
 
 int convert_init(struct convert *conv);
 
+static const struct dither_method_info {
+	uint32_t method;
+	const char *label;
+	const char *description;
+} dither_method_info[] = {
+	[DITHER_METHOD_NONE] = { DITHER_METHOD_NONE,
+		"none", "Disabled", },
+	[DITHER_METHOD_RECTANGULAR] = { DITHER_METHOD_RECTANGULAR,
+		"rectangular", "Rectangular dithering", },
+	[DITHER_METHOD_TRIANGULAR] = { DITHER_METHOD_TRIANGULAR,
+		"triangular", "Triangular dithering", },
+	[DITHER_METHOD_TRIANGULAR_HF] = { DITHER_METHOD_TRIANGULAR_HF,
+		"triangular-hf", "Sloped Triangular dithering", },
+	[DITHER_METHOD_WANNAMAKER_3] = { DITHER_METHOD_WANNAMAKER_3,
+		"wannamaker3", "Wannamaker 3 dithering", },
+	[DITHER_METHOD_LIPSHITZ] = { DITHER_METHOD_LIPSHITZ,
+		"shaped5", "Lipshitz 5 dithering", },
+};
+
+static inline uint32_t dither_method_from_label(const char *label)
+{
+	uint32_t i;
+	for (i = 0; i < SPA_N_ELEMENTS(dither_method_info); i++) {
+		if (spa_streq(dither_method_info[i].label, label))
+			return dither_method_info[i].method;
+	}
+	return DITHER_METHOD_NONE;
+}
+
 #define convert_process(conv,...)	(conv)->process(conv, __VA_ARGS__)
 #define convert_free(conv)		(conv)->free(conv)
 
@@ -200,8 +283,6 @@ int convert_init(struct convert *conv);
 void conv_##name##_##arch(struct convert *conv, void * SPA_RESTRICT dst[],	\
 		const void * SPA_RESTRICT src[], uint32_t n_samples)		\
 
-#define FMT_OPS_MAX_ALIGN	32
-
 DEFINE_FUNCTION(copy8d, c);
 DEFINE_FUNCTION(copy8, c);
 DEFINE_FUNCTION(copy16d, c);
@@ -256,62 +337,85 @@ DEFINE_FUNCTION(f64_to_f32d, c);
 DEFINE_FUNCTION(f64s_to_f32d, c);
 DEFINE_FUNCTION(f64d_to_f32, c);
 DEFINE_FUNCTION(f32d_to_u8d, c);
+DEFINE_FUNCTION(f32d_to_u8d_noise, c);
+DEFINE_FUNCTION(f32d_to_u8d_shaped, c);
 DEFINE_FUNCTION(f32_to_u8, c);
 DEFINE_FUNCTION(f32_to_u8d, c);
 DEFINE_FUNCTION(f32d_to_u8, c);
+DEFINE_FUNCTION(f32d_to_u8_noise, c);
+DEFINE_FUNCTION(f32d_to_u8_shaped, c);
 DEFINE_FUNCTION(f32d_to_s8d, c);
+DEFINE_FUNCTION(f32d_to_s8d_noise, c);
+DEFINE_FUNCTION(f32d_to_s8d_shaped, c);
 DEFINE_FUNCTION(f32_to_s8, c);
 DEFINE_FUNCTION(f32_to_s8d, c);
 DEFINE_FUNCTION(f32d_to_s8, c);
+DEFINE_FUNCTION(f32d_to_s8_noise, c);
+DEFINE_FUNCTION(f32d_to_s8_shaped, c);
 DEFINE_FUNCTION(f32d_to_alaw, c);
 DEFINE_FUNCTION(f32d_to_ulaw, c);
 DEFINE_FUNCTION(f32_to_u16, c);
 DEFINE_FUNCTION(f32d_to_u16, c);
 DEFINE_FUNCTION(f32d_to_s16d, c);
+DEFINE_FUNCTION(f32d_to_s16d_noise, c);
+DEFINE_FUNCTION(f32d_to_s16d_shaped, c);
 DEFINE_FUNCTION(f32_to_s16, c);
 DEFINE_FUNCTION(f32_to_s16d, c);
 DEFINE_FUNCTION(f32d_to_s16, c);
+DEFINE_FUNCTION(f32d_to_s16_noise, c);
+DEFINE_FUNCTION(f32d_to_s16_shaped, c);
 DEFINE_FUNCTION(f32d_to_s16s, c);
+DEFINE_FUNCTION(f32d_to_s16s_noise, c);
+DEFINE_FUNCTION(f32d_to_s16s_shaped, c);
 DEFINE_FUNCTION(f32_to_u32, c);
 DEFINE_FUNCTION(f32d_to_u32, c);
 DEFINE_FUNCTION(f32d_to_s32d, c);
+DEFINE_FUNCTION(f32d_to_s32d_noise, c);
 DEFINE_FUNCTION(f32_to_s32, c);
 DEFINE_FUNCTION(f32_to_s32d, c);
 DEFINE_FUNCTION(f32d_to_s32, c);
+DEFINE_FUNCTION(f32d_to_s32_noise, c);
 DEFINE_FUNCTION(f32d_to_s32s, c);
+DEFINE_FUNCTION(f32d_to_s32s_noise, c);
 DEFINE_FUNCTION(f32_to_u24, c);
 DEFINE_FUNCTION(f32d_to_u24, c);
 DEFINE_FUNCTION(f32d_to_s24d, c);
+DEFINE_FUNCTION(f32d_to_s24d_noise, c);
 DEFINE_FUNCTION(f32_to_s24, c);
 DEFINE_FUNCTION(f32_to_s24d, c);
 DEFINE_FUNCTION(f32d_to_s24, c);
+DEFINE_FUNCTION(f32d_to_s24_noise, c);
 DEFINE_FUNCTION(f32d_to_s24s, c);
+DEFINE_FUNCTION(f32d_to_s24s_noise, c);
 DEFINE_FUNCTION(f32_to_u24_32, c);
 DEFINE_FUNCTION(f32d_to_u24_32, c);
 DEFINE_FUNCTION(f32d_to_s24_32d, c);
+DEFINE_FUNCTION(f32d_to_s24_32d_noise, c);
 DEFINE_FUNCTION(f32_to_s24_32, c);
 DEFINE_FUNCTION(f32_to_s24_32d, c);
 DEFINE_FUNCTION(f32d_to_s24_32, c);
+DEFINE_FUNCTION(f32d_to_s24_32_noise, c);
 DEFINE_FUNCTION(f32d_to_s24_32s, c);
+DEFINE_FUNCTION(f32d_to_s24_32s_noise, c);
 DEFINE_FUNCTION(f32d_to_f64d, c);
 DEFINE_FUNCTION(f32_to_f64, c);
 DEFINE_FUNCTION(f32_to_f64d, c);
 DEFINE_FUNCTION(f32d_to_f64, c);
 DEFINE_FUNCTION(f32d_to_f64s, c);
-DEFINE_FUNCTION(deinterleave_8, c);
-DEFINE_FUNCTION(deinterleave_16, c);
-DEFINE_FUNCTION(deinterleave_24, c);
-DEFINE_FUNCTION(deinterleave_32, c);
-DEFINE_FUNCTION(deinterleave_32s, c);
-DEFINE_FUNCTION(deinterleave_64, c);
-DEFINE_FUNCTION(deinterleave_64s, c);
-DEFINE_FUNCTION(interleave_8, c);
-DEFINE_FUNCTION(interleave_16, c);
-DEFINE_FUNCTION(interleave_24, c);
-DEFINE_FUNCTION(interleave_32, c);
-DEFINE_FUNCTION(interleave_32s, c);
-DEFINE_FUNCTION(interleave_64, c);
-DEFINE_FUNCTION(interleave_64s, c);
+DEFINE_FUNCTION(8_to_8d, c);
+DEFINE_FUNCTION(16_to_16d, c);
+DEFINE_FUNCTION(24_to_24d, c);
+DEFINE_FUNCTION(32_to_32d, c);
+DEFINE_FUNCTION(32s_to_32d, c);
+DEFINE_FUNCTION(64_to_64d, c);
+DEFINE_FUNCTION(64s_to_64sd, c);
+DEFINE_FUNCTION(8d_to_8, c);
+DEFINE_FUNCTION(16d_to_16, c);
+DEFINE_FUNCTION(24d_to_24, c);
+DEFINE_FUNCTION(32d_to_32, c);
+DEFINE_FUNCTION(32d_to_32s, c);
+DEFINE_FUNCTION(64d_to_64, c);
+DEFINE_FUNCTION(64sd_to_64s, c);
 
 #if defined(HAVE_NEON)
 DEFINE_FUNCTION(s16_to_f32d_2, neon);
@@ -324,14 +428,17 @@ DEFINE_FUNCTION(s16_to_f32d, sse2);
 DEFINE_FUNCTION(s24_to_f32d, sse2);
 DEFINE_FUNCTION(s32_to_f32d, sse2);
 DEFINE_FUNCTION(f32d_to_s32, sse2);
+DEFINE_FUNCTION(f32d_to_s32_noise, sse2);
 DEFINE_FUNCTION(f32_to_s16, sse2);
 DEFINE_FUNCTION(f32d_to_s16_2, sse2);
 DEFINE_FUNCTION(f32d_to_s16, sse2);
+DEFINE_FUNCTION(f32d_to_s16_noise, sse2);
 DEFINE_FUNCTION(f32d_to_s16d, sse2);
-DEFINE_FUNCTION(deinterleave_32, sse2);
-DEFINE_FUNCTION(deinterleave_32s, sse2);
-DEFINE_FUNCTION(interleave_32, sse2);
-DEFINE_FUNCTION(interleave_32s, sse2);
+DEFINE_FUNCTION(f32d_to_s16d_noise, sse2);
+DEFINE_FUNCTION(32_to_32d, sse2);
+DEFINE_FUNCTION(32s_to_32d, sse2);
+DEFINE_FUNCTION(32d_to_32, sse2);
+DEFINE_FUNCTION(32d_to_32s, sse2);
 #endif
 #if defined(HAVE_SSSE3)
 DEFINE_FUNCTION(s24_to_f32d, ssse3);
diff --git a/spa/plugins/audioconvert/fmtconvert.c b/spa/plugins/audioconvert/fmtconvert.c
deleted file mode 100644
index d1d52ad48d5f9d61a50980514b6c9f0de174097f..0000000000000000000000000000000000000000
--- a/spa/plugins/audioconvert/fmtconvert.c
+++ /dev/null
@@ -1,1161 +0,0 @@
-/* Spa
- *
- * Copyright © 2018 Wim Taymans
- *
- * Permission is hereby granted, free of charge, to any person obtaining a
- * copy of this software and associated documentation files (the "Software"),
- * to deal in the Software without restriction, including without limitation
- * the rights to use, copy, modify, merge, publish, distribute, sublicense,
- * and/or sell copies of the Software, and to permit persons to whom the
- * Software is furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice (including the next
- * paragraph) shall be included in all copies or substantial portions of the
- * Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
- * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
- * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
- * DEALINGS IN THE SOFTWARE.
- */
-
-#include <errno.h>
-#include <string.h>
-#include <stdio.h>
-#include <limits.h>
-
-#include <spa/support/plugin.h>
-#include <spa/support/log.h>
-#include <spa/support/cpu.h>
-#include <spa/utils/list.h>
-#include <spa/utils/names.h>
-#include <spa/utils/string.h>
-#include <spa/node/node.h>
-#include <spa/node/io.h>
-#include <spa/node/utils.h>
-#include <spa/param/audio/format-utils.h>
-#include <spa/param/latency-utils.h>
-#include <spa/param/param.h>
-#include <spa/pod/filter.h>
-#include <spa/debug/types.h>
-#include <spa/debug/format.h>
-
-#include "fmt-ops.h"
-
-#undef SPA_LOG_TOPIC_DEFAULT
-#define SPA_LOG_TOPIC_DEFAULT log_topic
-static struct spa_log_topic *log_topic = &SPA_LOG_TOPIC(0, "spa.fmtconvert");
-
-#define DEFAULT_RATE		48000
-#define DEFAULT_CHANNELS	2
-
-#define MAX_BUFFERS	32
-#define MAX_ALIGN	FMT_OPS_MAX_ALIGN
-#define MAX_DATAS	SPA_AUDIO_MAX_CHANNELS
-
-#define PROP_DEFAULT_TRUNCATE	false
-#define PROP_DEFAULT_DITHER	0
-
-struct impl;
-
-struct props {
-	bool truncate;
-	uint32_t dither;
-};
-
-static void props_reset(struct props *props)
-{
-	props->truncate = PROP_DEFAULT_TRUNCATE;
-	props->dither = PROP_DEFAULT_DITHER;
-}
-
-struct buffer {
-	uint32_t id;
-#define BUFFER_FLAG_OUT		(1 << 0)
-	uint32_t flags;
-	struct spa_list link;
-	struct spa_buffer *outbuf;
-	struct spa_meta_header *h;
-	void *datas[MAX_DATAS];
-};
-
-struct port {
-	uint32_t direction;
-	uint32_t id;
-
-	struct spa_io_buffers *io;
-
-	uint64_t info_all;
-	struct spa_port_info info;
-#define PORT_EnumFormat		0
-#define PORT_Meta		1
-#define PORT_IO			2
-#define PORT_Format		3
-#define PORT_Buffers		4
-#define PORT_Latency		5
-#define N_PORT_PARAMS		6
-	struct spa_param_info params[N_PORT_PARAMS];
-
-	struct spa_audio_info format;
-	uint32_t stride;
-	uint32_t blocks;
-	uint32_t size;
-	unsigned int have_format:1;
-
-	struct buffer buffers[MAX_BUFFERS];
-	uint32_t n_buffers;
-
-	struct spa_list queue;
-};
-
-struct impl {
-	struct spa_handle handle;
-	struct spa_node node;
-
-	struct spa_log *log;
-	struct spa_cpu *cpu;
-	uint32_t cpu_flags;
-	uint32_t max_align;
-	uint32_t quantum_limit;
-
-	struct spa_io_position *io_position;
-
-	uint64_t info_all;
-	struct spa_node_info info;
-	struct props props;
-#define N_NODE_PARAMS 0
-	struct spa_param_info params[1];
-
-	struct spa_hook_list hooks;
-
-	struct port ports[2][1];
-
-	uint32_t src_remap[SPA_AUDIO_MAX_CHANNELS];
-	uint32_t dst_remap[SPA_AUDIO_MAX_CHANNELS];
-
-	struct spa_latency_info latency[2];
-
-	struct convert conv;
-	unsigned int started:1;
-	unsigned int is_passthrough:1;
-};
-
-#define CHECK_PORT(this,d,id)		(id == 0)
-#define GET_PORT(this,d,id)		(&this->ports[d][id])
-#define GET_IN_PORT(this,id)		GET_PORT(this,SPA_DIRECTION_INPUT,id)
-#define GET_OUT_PORT(this,id)		GET_PORT(this,SPA_DIRECTION_OUTPUT,id)
-
-static int can_convert(const struct spa_audio_info *info1, const struct spa_audio_info *info2)
-{
-	if (info1->info.raw.channels != info2->info.raw.channels ||
-	    info1->info.raw.rate != info2->info.raw.rate) {
-		return 0;
-	}
-	return 1;
-}
-
-static int setup_convert(struct impl *this)
-{
-	uint32_t src_fmt, dst_fmt;
-	struct spa_audio_info informat, outformat;
-	struct port *inport, *outport;
-	uint32_t i, j;
-	int res;
-
-	inport = GET_IN_PORT(this, 0);
-	outport = GET_OUT_PORT(this, 0);
-
-	if (!inport->have_format || !outport->have_format)
-		return -EIO;
-
-	informat = inport->format;
-	outformat = outport->format;
-
-	src_fmt = informat.info.raw.format;
-	dst_fmt = outformat.info.raw.format;
-
-	spa_log_info(this->log, "%p: %s/%d@%d->%s/%d@%d", this,
-			spa_debug_type_find_name(spa_type_audio_format, src_fmt),
-			informat.info.raw.channels,
-			informat.info.raw.rate,
-			spa_debug_type_find_name(spa_type_audio_format, dst_fmt),
-			outformat.info.raw.channels,
-			outformat.info.raw.rate);
-
-	if (!can_convert(&informat, &outformat))
-		return -EINVAL;
-
-	for (i = 0; i < informat.info.raw.channels; i++) {
-		for (j = 0; j < outformat.info.raw.channels; j++) {
-			if (informat.info.raw.position[i] !=
-			    outformat.info.raw.position[j])
-				continue;
-			if (inport->blocks > 1) {
-				this->src_remap[j] = i;
-				if (outport->blocks > 1)
-					this->dst_remap[j] = j;
-				else
-					this->dst_remap[j] = 0;
-			} else {
-				this->src_remap[j] = 0;
-				if (outport->blocks > 1)
-					this->dst_remap[i] = j;
-				else
-					this->dst_remap[j] = 0;
-			}
-			spa_log_debug(this->log, "%p: channel %d -> %d (%s -> %s)", this,
-					i, j,
-					spa_debug_type_find_short_name(spa_type_audio_channel,
-						informat.info.raw.position[i]),
-					spa_debug_type_find_short_name(spa_type_audio_channel,
-						outformat.info.raw.position[j]));
-			outformat.info.raw.position[j] = -1;
-			break;
-		}
-	}
-	this->conv.src_fmt = src_fmt;
-	this->conv.dst_fmt = dst_fmt;
-	this->conv.n_channels = outformat.info.raw.channels;
-	this->conv.cpu_flags = this->cpu_flags;
-
-	if ((res = convert_init(&this->conv)) < 0)
-		return res;
-
-	this->is_passthrough = this->conv.is_passthrough;
-
-	spa_log_debug(this->log, "%p: got converter features %08x:%08x passthrough:%d", this,
-			this->cpu_flags, this->conv.cpu_flags, this->is_passthrough);
-
-	return 0;
-}
-
-static int impl_node_enum_params(void *object, int seq,
-				 uint32_t id, uint32_t start, uint32_t num,
-				 const struct spa_pod *filter)
-{
-	return -ENOTSUP;
-}
-
-static int impl_node_set_param(void *object, uint32_t id, uint32_t flags,
-			       const struct spa_pod *param)
-{
-	return -ENOTSUP;
-}
-
-static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size)
-{
-	struct impl *this = object;
-
-	spa_return_val_if_fail(this != NULL, -EINVAL);
-
-	spa_log_debug(this->log, "%p: io %d %p/%zd", this, id, data, size);
-
-	switch (id) {
-	case SPA_IO_Position:
-		this->io_position = data;
-		break;
-	default:
-		return -ENOENT;
-	}
-	return 0;
-}
-
-static int impl_node_send_command(void *object, const struct spa_command *command)
-{
-	struct impl *this = object;
-
-	spa_return_val_if_fail(this != NULL, -EINVAL);
-	spa_return_val_if_fail(command != NULL, -EINVAL);
-
-	switch (SPA_NODE_COMMAND_ID(command)) {
-	case SPA_NODE_COMMAND_Start:
-		this->started = true;
-		break;
-	case SPA_NODE_COMMAND_Suspend:
-	case SPA_NODE_COMMAND_Flush:
-	case SPA_NODE_COMMAND_Pause:
-		this->started = false;
-		break;
-	default:
-		return -ENOTSUP;
-	}
-	return 0;
-}
-
-static void emit_info(struct impl *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) {
-		spa_node_emit_info(&this->hooks, &this->info);
-		this->info.change_mask = old;
-	}
-}
-
-static void emit_port_info(struct impl *this, struct port *port, bool full)
-{
-	uint64_t old = full ? port->info.change_mask : 0;
-	if (full)
-		port->info.change_mask = port->info_all;
-	if (port->info.change_mask) {
-		spa_node_emit_port_info(&this->hooks,
-				port->direction, port->id, &port->info);
-		port->info.change_mask = old;
-	}
-}
-
-static int
-impl_node_add_listener(void *object,
-		struct spa_hook *listener,
-		const struct spa_node_events *events,
-		void *data)
-{
-	struct impl *this = object;
-	struct spa_hook_list save;
-
-	spa_return_val_if_fail(this != NULL, -EINVAL);
-
-	spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
-
-	emit_info(this, true);
-	emit_port_info(this, GET_IN_PORT(this, 0), true);
-	emit_port_info(this, GET_OUT_PORT(this, 0), true);
-
-	spa_hook_list_join(&this->hooks, &save);
-
-	return 0;
-}
-
-static int
-impl_node_set_callbacks(void *object,
-			const struct spa_node_callbacks *callbacks,
-			void *user_data)
-{
-	return 0;
-}
-
-static int impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id,
-		const struct spa_dict *props)
-{
-        return -ENOTSUP;
-}
-
-static int
-impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id)
-{
-        return -ENOTSUP;
-}
-
-static int int32_cmp(const void *v1, const void *v2)
-{
-	int32_t a1 = *(int32_t*)v1;
-	int32_t a2 = *(int32_t*)v2;
-	if (a1 == 0 && a2 != 0)
-		return 1;
-	if (a2 == 0 && a1 != 0)
-		return -1;
-	return a1 - a2;
-}
-
-static int port_enum_formats(void *object,
-			     enum spa_direction direction, uint32_t port_id,
-			     uint32_t index,
-			     struct spa_pod **param,
-			     struct spa_pod_builder *builder)
-{
-	struct impl *this = object;
-	struct port *port, *other;
-
-	port = GET_PORT(this, direction, port_id);
-	other = GET_PORT(this, SPA_DIRECTION_REVERSE(direction), 0);
-
-	spa_log_debug(this->log, "%p: enum %p %d %d", this, other, port->have_format, other->have_format);
-	switch (index) {
-	case 0:
-		if (port->have_format) {
-			*param = spa_format_audio_raw_build(builder,
-					SPA_PARAM_EnumFormat, &port->format.info.raw);
-		}
-		else {
-			struct spa_pod_frame f;
-			struct spa_audio_info info;
-
-			spa_pod_builder_push_object(builder, &f,
-				SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat);
-
-			spa_pod_builder_add(builder,
-				SPA_FORMAT_mediaType,      SPA_POD_Id(SPA_MEDIA_TYPE_audio),
-				SPA_FORMAT_mediaSubtype,   SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw),
-				0);
-
-			if (other->have_format)
-				info = other->format;
-			else
-				info.info.raw.format = SPA_AUDIO_FORMAT_F32P;
-
-			if (!other->have_format ||
-			    info.info.raw.format == SPA_AUDIO_FORMAT_F32P ||
-			    info.info.raw.format == SPA_AUDIO_FORMAT_F32) {
-				spa_pod_builder_add(builder,
-					SPA_FORMAT_AUDIO_format,   SPA_POD_CHOICE_ENUM_Id(29,
-								info.info.raw.format,
-								SPA_AUDIO_FORMAT_F32P,
-								SPA_AUDIO_FORMAT_F32,
-								SPA_AUDIO_FORMAT_F32_OE,
-								SPA_AUDIO_FORMAT_F64P,
-								SPA_AUDIO_FORMAT_F64,
-								SPA_AUDIO_FORMAT_F64_OE,
-								SPA_AUDIO_FORMAT_S32P,
-								SPA_AUDIO_FORMAT_S32,
-								SPA_AUDIO_FORMAT_S32_OE,
-								SPA_AUDIO_FORMAT_U32,
-								SPA_AUDIO_FORMAT_S24_32P,
-								SPA_AUDIO_FORMAT_S24_32,
-								SPA_AUDIO_FORMAT_S24_32_OE,
-								SPA_AUDIO_FORMAT_U24_32,
-								SPA_AUDIO_FORMAT_S24P,
-								SPA_AUDIO_FORMAT_S24,
-								SPA_AUDIO_FORMAT_S24_OE,
-								SPA_AUDIO_FORMAT_U24,
-								SPA_AUDIO_FORMAT_S16P,
-								SPA_AUDIO_FORMAT_S16,
-								SPA_AUDIO_FORMAT_S16_OE,
-								SPA_AUDIO_FORMAT_U16,
-								SPA_AUDIO_FORMAT_S8P,
-								SPA_AUDIO_FORMAT_S8,
-								SPA_AUDIO_FORMAT_U8P,
-								SPA_AUDIO_FORMAT_U8,
-								SPA_AUDIO_FORMAT_ULAW,
-								SPA_AUDIO_FORMAT_ALAW),
-					0);
-			} else {
-				spa_pod_builder_add(builder,
-					SPA_FORMAT_AUDIO_format,   SPA_POD_CHOICE_ENUM_Id(5,
-								info.info.raw.format,
-								info.info.raw.format,
-								SPA_AUDIO_FORMAT_F32,
-								SPA_AUDIO_FORMAT_F32P,
-								SPA_AUDIO_FORMAT_F32_OE),
-					0);
-			}
-			if (other->have_format) {
-				spa_pod_builder_add(builder,
-					SPA_FORMAT_AUDIO_rate,     SPA_POD_Int(info.info.raw.rate),
-					SPA_FORMAT_AUDIO_channels, SPA_POD_Int(info.info.raw.channels),
-					0);
-				if (!SPA_FLAG_IS_SET(info.info.raw.flags, SPA_AUDIO_FLAG_UNPOSITIONED)) {
-					qsort(info.info.raw.position, info.info.raw.channels,
-							sizeof(uint32_t), int32_cmp);
-					spa_pod_builder_prop(builder, SPA_FORMAT_AUDIO_position, 0);
-			                spa_pod_builder_array(builder, sizeof(uint32_t), SPA_TYPE_Id,
-							info.info.raw.channels, info.info.raw.position);
-				}
-			} else {
-				uint32_t rate = this->io_position ?
-					this->io_position->clock.rate.denom : DEFAULT_RATE;
-
-				spa_pod_builder_add(builder,
-					SPA_FORMAT_AUDIO_rate,     SPA_POD_CHOICE_RANGE_Int(
-									rate, 1, INT32_MAX),
-					SPA_FORMAT_AUDIO_channels, SPA_POD_CHOICE_RANGE_Int(
-									DEFAULT_CHANNELS, 1, INT32_MAX),
-					0);
-			}
-			*param = spa_pod_builder_pop(builder, &f);
-		}
-		break;
-	default:
-		return 0;
-	}
-
-	return 1;
-}
-
-static int
-impl_node_port_enum_params(void *object, int seq,
-			   enum spa_direction direction, uint32_t port_id,
-			   uint32_t id, uint32_t start, uint32_t num,
-			   const struct spa_pod *filter)
-{
-	struct impl *this = object;
-	struct port *port, *other;
-	struct spa_pod *param;
-	struct spa_pod_builder b = { 0 };
-	uint8_t buffer[4096];
-	struct spa_result_node_params result;
-	uint32_t count = 0;
-	int res;
-
-	spa_return_val_if_fail(this != NULL, -EINVAL);
-	spa_return_val_if_fail(num != 0, -EINVAL);
-
-	spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
-
-	port = GET_PORT(this, direction, port_id);
-	other = GET_PORT(this, SPA_DIRECTION_REVERSE(direction), port_id);
-
-	spa_log_debug(this->log, "%p: enum params port %d.%d %d %u",
-			this, direction, port_id, seq, id);
-
-	result.id = id;
-	result.next = start;
-      next:
-	result.index = result.next++;
-
-	spa_pod_builder_init(&b, buffer, sizeof(buffer));
-
-	switch (id) {
-	case SPA_PARAM_EnumFormat:
-		if ((res = port_enum_formats(this, direction, port_id,
-						result.index, &param, &b)) <= 0)
-			return res;
-		break;
-
-	case SPA_PARAM_Format:
-		if (!port->have_format)
-			return -EIO;
-		if (result.index > 0)
-			return 0;
-
-		param = spa_format_audio_raw_build(&b, id, &port->format.info.raw);
-		break;
-
-	case SPA_PARAM_Buffers:
-	{
-		if (!port->have_format)
-			return -EIO;
-		if (result.index > 0)
-			return 0;
-
-		if (other->n_buffers > 0) {
-			param = spa_pod_builder_add_object(&b,
-				SPA_TYPE_OBJECT_ParamBuffers, id,
-				SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(2, 1, MAX_BUFFERS),
-				SPA_PARAM_BUFFERS_blocks,  SPA_POD_Int(port->blocks),
-				SPA_PARAM_BUFFERS_size,    SPA_POD_Int(other->size / other->stride * port->stride),
-				SPA_PARAM_BUFFERS_stride,  SPA_POD_Int(port->stride));
-
-
-		} else {
-			param = spa_pod_builder_add_object(&b,
-				SPA_TYPE_OBJECT_ParamBuffers, id,
-				SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(2, 1, MAX_BUFFERS),
-				SPA_PARAM_BUFFERS_blocks,  SPA_POD_Int(port->blocks),
-				SPA_PARAM_BUFFERS_size,    SPA_POD_CHOICE_RANGE_Int(
-								this->quantum_limit * 2 * port->stride,
-								16 * port->stride,
-								INT32_MAX),
-				SPA_PARAM_BUFFERS_stride,  SPA_POD_Int(port->stride));
-		}
-		break;
-	}
-	case SPA_PARAM_Meta:
-		switch (result.index) {
-		case 0:
-			param = spa_pod_builder_add_object(&b,
-				SPA_TYPE_OBJECT_ParamMeta, id,
-				SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header),
-				SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header)));
-			break;
-		default:
-			return 0;
-		}
-		break;
-
-	case SPA_PARAM_IO:
-		switch (result.index) {
-		case 0:
-			param = spa_pod_builder_add_object(&b,
-				SPA_TYPE_OBJECT_ParamIO, id,
-				SPA_PARAM_IO_id,   SPA_POD_Id(SPA_IO_Buffers),
-				SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers)));
-			break;
-		default:
-			return 0;
-		}
-		break;
-
-	case SPA_PARAM_Latency:
-		switch (result.index) {
-		case 0: case 1:
-			param = spa_latency_build(&b, id, &this->latency[result.index]);
-			break;
-		default:
-			return 0;
-		}
-		break;
-
-	default:
-		return -ENOENT;
-	}
-
-	if (spa_pod_filter(&b, &result.param, param, filter) < 0)
-		goto next;
-
-	spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
-
-	if (++count != num)
-		goto next;
-
-	return 0;
-}
-
-static int calc_width(struct spa_audio_info *info)
-{
-	switch (info->info.raw.format) {
-	case SPA_AUDIO_FORMAT_U8P:
-	case SPA_AUDIO_FORMAT_U8:
-	case SPA_AUDIO_FORMAT_S8P:
-	case SPA_AUDIO_FORMAT_S8:
-	case SPA_AUDIO_FORMAT_ALAW:
-	case SPA_AUDIO_FORMAT_ULAW:
-		return 1;
-	case SPA_AUDIO_FORMAT_S16P:
-	case SPA_AUDIO_FORMAT_S16:
-	case SPA_AUDIO_FORMAT_S16_OE:
-	case SPA_AUDIO_FORMAT_U16:
-		return 2;
-	case SPA_AUDIO_FORMAT_S24P:
-	case SPA_AUDIO_FORMAT_S24:
-	case SPA_AUDIO_FORMAT_S24_OE:
-	case SPA_AUDIO_FORMAT_U24:
-		return 3;
-	case SPA_AUDIO_FORMAT_F64P:
-	case SPA_AUDIO_FORMAT_F64:
-	case SPA_AUDIO_FORMAT_F64_OE:
-		return 8;
-	default:
-		return 4;
-	}
-}
-
-static int clear_buffers(struct impl *this, struct port *port)
-{
-	if (port->n_buffers > 0) {
-		spa_log_debug(this->log, "%p: clear buffers %p", this, port);
-		port->n_buffers = 0;
-		spa_list_init(&port->queue);
-	}
-	return 0;
-}
-static int port_set_format(void *object,
-			   enum spa_direction direction,
-			   uint32_t port_id,
-			   uint32_t flags,
-			   const struct spa_pod *format)
-{
-	struct impl *this = object;
-	struct port *port, *other;
-	int res;
-
-	port = GET_PORT(this, direction, port_id);
-	other = GET_PORT(this, SPA_DIRECTION_REVERSE(direction), port_id);
-
-	if (format == NULL) {
-		if (port->have_format) {
-			port->have_format = false;
-			clear_buffers(this, port);
-			if (this->conv.process)
-				convert_free(&this->conv);
-		}
-	} else {
-		struct spa_audio_info info = { 0 };
-
-		if ((res = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0)
-			return res;
-
-		if (info.media_type != SPA_MEDIA_TYPE_audio ||
-		    info.media_subtype != SPA_MEDIA_SUBTYPE_raw)
-			return -EINVAL;
-
-		if (spa_format_audio_raw_parse(format, &info.info.raw) < 0)
-			return -EINVAL;
-
-		if (other->have_format) {
-			spa_log_debug(this->log, "%p: channels:%d<>%d rate:%d<>%d format:%d<>%d", this,
-				info.info.raw.channels, other->format.info.raw.channels,
-				info.info.raw.rate, other->format.info.raw.rate,
-				info.info.raw.format, other->format.info.raw.format);
-			if (!can_convert(&info, &other->format))
-				return -ENOTSUP;
-		}
-
-		port->stride = calc_width(&info);
-
-		if (SPA_AUDIO_FORMAT_IS_PLANAR(info.info.raw.format)) {
-			port->blocks = info.info.raw.channels;
-		} else {
-			port->stride *= info.info.raw.channels;
-			port->blocks = 1;
-		}
-
-		port->have_format = true;
-		port->format = info;
-
-		if (other->have_format && port->have_format)
-			if ((res = setup_convert(this)) < 0)
-				return res;
-
-		spa_log_debug(this->log, "%p: set format on port %d:%d res:%d stride:%d",
-				this, direction, port_id, res, port->stride);
-	}
-	if (port->have_format) {
-		port->params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE);
-		port->params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ);
-	} else {
-		port->params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
-		port->params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
-	}
-	port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
-	emit_port_info(this, port, false);
-	return 0;
-}
-
-static int
-impl_node_port_set_param(void *object,
-			 enum spa_direction direction, uint32_t port_id,
-			 uint32_t id, uint32_t flags,
-			 const struct spa_pod *param)
-{
-	struct impl *this = object;
-	struct port *port;
-	int res;
-
-	spa_return_val_if_fail(object != NULL, -EINVAL);
-	spa_return_val_if_fail(CHECK_PORT(object, direction, port_id), -EINVAL);
-
-	port = GET_PORT(this, direction, port_id);
-
-	spa_log_debug(this->log, "%p: set param %u on port %d:%d %p",
-				this, id, direction, port_id, param);
-
-	switch (id) {
-	case SPA_PARAM_Latency:
-	{
-		struct spa_latency_info info;
-		if (param == NULL)
-			return 0;
-		if ((res = spa_latency_parse(param, &info)) < 0)
-			return res;
-		if (direction == info.direction)
-			return -EINVAL;
-
-		this->latency[info.direction] = info;
-		port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
-		port->params[PORT_Latency].flags ^= SPA_PARAM_INFO_SERIAL;
-		emit_port_info(this, port, false);
-		break;
-	}
-	case SPA_PARAM_Format:
-		res = port_set_format(object, direction, port_id, flags, param);
-		break;
-	default:
-		res = -ENOENT;
-	}
-	return res;
-}
-
-static int
-impl_node_port_use_buffers(void *object,
-			   enum spa_direction direction,
-			   uint32_t port_id,
-			   uint32_t flags,
-			   struct spa_buffer **buffers,
-			   uint32_t n_buffers)
-{
-	struct impl *this = object;
-	struct port *port;
-	uint32_t i, size = SPA_ID_INVALID, j;
-
-	spa_return_val_if_fail(this != NULL, -EINVAL);
-
-	spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
-
-	port = GET_PORT(this, direction, port_id);
-
-	spa_return_val_if_fail(port->have_format, -EIO);
-
-	spa_log_debug(this->log, "%p: use buffers %d on port %d", this, n_buffers, port_id);
-
-	clear_buffers(this, port);
-
-	for (i = 0; i < n_buffers; i++) {
-		struct buffer *b;
-		uint32_t n_datas = buffers[i]->n_datas;
-		struct spa_data *d = buffers[i]->datas;
-
-		b = &port->buffers[i];
-		b->id = i;
-		b->flags = 0;
-		b->outbuf = buffers[i];
-		b->h = spa_buffer_find_meta_data(buffers[i], SPA_META_Header, sizeof(*b->h));
-
-		if (n_datas != port->blocks) {
-			spa_log_error(this->log, "%p: expected %d blocks on buffer %d", this,
-				      port->blocks, i);
-			return -EINVAL;
-		}
-
-		for (j = 0; j < n_datas; j++) {
-			if (size == SPA_ID_INVALID)
-				size = d[j].maxsize;
-			else if (size != d[j].maxsize) {
-				spa_log_error(this->log, "%p: expected size %d on buffer %d",
-						this, size, i);
-				return -EINVAL;
-			}
-
-			if (d[j].data == NULL) {
-				spa_log_error(this->log, "%p: invalid memory %d on buffer %d",
-						this, j, i);
-				return -EINVAL;
-			}
-			if (!SPA_IS_ALIGNED(d[j].data, this->max_align)) {
-				spa_log_warn(this->log, "%p: memory %d on buffer %d not aligned",
-						this, j, i);
-			}
-			b->datas[j] = d[j].data;
-			if (direction == SPA_DIRECTION_OUTPUT &&
-			    !SPA_FLAG_IS_SET(d[j].flags, SPA_DATA_FLAG_DYNAMIC))
-				this->is_passthrough = false;
-		}
-
-		if (direction == SPA_DIRECTION_OUTPUT)
-			spa_list_append(&port->queue, &b->link);
-		else
-			SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUT);
-	}
-	port->n_buffers = n_buffers;
-	port->size = size;
-
-	spa_log_debug(this->log, "%p: buffer size %d", this, size);
-
-	return 0;
-}
-
-static int
-impl_node_port_set_io(void *object,
-		      enum spa_direction direction, uint32_t port_id,
-		      uint32_t id, void *data, size_t size)
-{
-	struct impl *this = object;
-	struct port *port;
-
-	spa_return_val_if_fail(this != NULL, -EINVAL);
-	spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
-
-	port = GET_PORT(this, direction, port_id);
-
-	spa_log_debug(this->log, "%p: port %d:%d update io %d %p",
-			this, direction, port_id, id, data);
-
-	switch (id) {
-	case SPA_IO_Buffers:
-		port->io = data;
-		break;
-	default:
-		return -ENOENT;
-	}
-	return 0;
-}
-
-static void recycle_buffer(struct impl *this, struct port *port, uint32_t id)
-{
-	struct buffer *b = &port->buffers[id];
-
-	if (SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_OUT)) {
-		spa_list_append(&port->queue, &b->link);
-		SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_OUT);
-		spa_log_trace_fp(this->log, "%p: recycle buffer %d", this, id);
-	}
-}
-
-static inline struct buffer *dequeue_buffer(struct impl *this, struct port *port)
-{
-	struct buffer *b;
-
-	if (spa_list_is_empty(&port->queue))
-		return NULL;
-	b = spa_list_first(&port->queue, struct buffer, link);
-	spa_list_remove(&b->link);
-	SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUT);
-	return b;
-}
-
-static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id)
-{
-	struct impl *this = object;
-	struct port *port;
-
-	spa_return_val_if_fail(this != NULL, -EINVAL);
-	spa_return_val_if_fail(CHECK_PORT(this, SPA_DIRECTION_OUTPUT, port_id), -EINVAL);
-
-	port = GET_OUT_PORT(this, port_id);
-
-	recycle_buffer(this, port, buffer_id);
-
-	return 0;
-}
-
-static int impl_node_process(void *object)
-{
-	struct impl *this = object;
-	struct port *inport, *outport;
-	struct spa_io_buffers *inio, *outio;
-	struct buffer *inbuf, *outbuf;
-	struct spa_buffer *inb, *outb;
-	const void **src_datas;
-	void **dst_datas;
-	uint32_t i, n_src_datas, n_dst_datas;
-	uint32_t n_samples, size, maxsize, offs;
-
-	spa_return_val_if_fail(this != NULL, -EINVAL);
-
-	outport = GET_OUT_PORT(this, 0);
-	inport = GET_IN_PORT(this, 0);
-
-	outio = outport->io;
-	inio = inport->io;
-
-	spa_log_trace_fp(this->log, "%p: io %p %p", this, inio, outio);
-
-	spa_return_val_if_fail(outio != NULL, -EIO);
-	spa_return_val_if_fail(inio != NULL, -EIO);
-
-	spa_log_trace_fp(this->log, "%p: status %p %d %d -> %p %d %d", this,
-			inio, inio->status, inio->buffer_id,
-			outio, outio->status, outio->buffer_id);
-
-	if (SPA_UNLIKELY(outio->status == SPA_STATUS_HAVE_DATA))
-		return inio->status | outio->status;
-
-	if (SPA_LIKELY(outio->buffer_id < outport->n_buffers)) {
-		recycle_buffer(this, outport, outio->buffer_id);
-		outio->buffer_id = SPA_ID_INVALID;
-	}
-	if (SPA_UNLIKELY(inio->status != SPA_STATUS_HAVE_DATA))
-		return outio->status = inio->status;
-
-	if (SPA_UNLIKELY(inio->buffer_id >= inport->n_buffers))
-		return inio->status = -EINVAL;
-
-	if (SPA_UNLIKELY((outbuf = dequeue_buffer(this, outport)) == NULL))
-		return outio->status = -EPIPE;
-
-	inbuf = &inport->buffers[inio->buffer_id];
-	inb = inbuf->outbuf;
-
-	n_src_datas = inb->n_datas;
-	src_datas = alloca(sizeof(void*) * n_src_datas);
-
-	outb = outbuf->outbuf;
-
-	n_dst_datas = outb->n_datas;
-	dst_datas = alloca(sizeof(void*) * n_dst_datas);
-
-	size = UINT32_MAX;
-	for (i = 0; i < n_src_datas; i++) {
-		uint32_t src_remap = this->src_remap[i];
-		struct spa_data *sd = &inb->datas[src_remap];
-		offs = SPA_MIN(sd->chunk->offset, sd->maxsize);
-		size = SPA_MIN(size, SPA_MIN(sd->maxsize - offs, sd->chunk->size));
-		src_datas[i] = SPA_PTROFF(sd->data, offs, void);
-	}
-	n_samples = size / inport->stride;
-
-	maxsize = outb->datas[0].maxsize;
-	n_samples = SPA_MIN(n_samples, maxsize / outport->stride);
-
-	spa_log_trace_fp(this->log, "%p: n_src:%d n_dst:%d size:%d maxsize:%d n_samples:%d p:%d",
-			this, n_src_datas, n_dst_datas, size, maxsize, n_samples,
-			this->is_passthrough);
-
-	for (i = 0; i < n_dst_datas; i++) {
-		uint32_t dst_remap = this->dst_remap[i];
-		struct spa_data *dd = outb->datas;
-
-		if (this->is_passthrough)
-			dd[i].data = (void *)src_datas[i];
-		else
-			dst_datas[i] = dd[dst_remap].data = outbuf->datas[dst_remap];
-
-		dd[i].chunk->offset = 0;
-		dd[i].chunk->size = n_samples * outport->stride;
-	}
-	if (!this->is_passthrough)
-		convert_process(&this->conv, dst_datas, src_datas, n_samples);
-
-	inio->status = SPA_STATUS_NEED_DATA;
-
-	outio->status = SPA_STATUS_HAVE_DATA;
-	outio->buffer_id = outbuf->id;
-
-	return SPA_STATUS_NEED_DATA | SPA_STATUS_HAVE_DATA;
-}
-
-static const struct spa_node_methods impl_node = {
-	SPA_VERSION_NODE_METHODS,
-	.add_listener = impl_node_add_listener,
-	.set_callbacks = impl_node_set_callbacks,
-	.enum_params = impl_node_enum_params,
-	.set_param = impl_node_set_param,
-	.set_io = impl_node_set_io,
-	.send_command = impl_node_send_command,
-	.add_port = impl_node_add_port,
-	.remove_port = impl_node_remove_port,
-	.port_enum_params = impl_node_port_enum_params,
-	.port_set_param = impl_node_port_set_param,
-	.port_use_buffers = impl_node_port_use_buffers,
-	.port_set_io = impl_node_port_set_io,
-	.port_reuse_buffer = impl_node_port_reuse_buffer,
-	.process = impl_node_process,
-};
-
-static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface)
-{
-	struct impl *this;
-
-	spa_return_val_if_fail(handle != NULL, -EINVAL);
-	spa_return_val_if_fail(interface != NULL, -EINVAL);
-
-	this = (struct impl *) handle;
-
-	if (spa_streq(type, SPA_TYPE_INTERFACE_Node))
-		*interface = &this->node;
-	else
-		return -ENOENT;
-
-	return 0;
-}
-
-static int impl_clear(struct spa_handle *handle)
-{
-	return 0;
-}
-
-static int init_port(struct impl *this, enum spa_direction direction, uint32_t port_id)
-{
-	struct port *port;
-
-	port = GET_PORT(this, direction, port_id);
-	port->direction = direction;
-	port->id = port_id;
-
-	spa_list_init(&port->queue);
-	port->info_all = SPA_PORT_CHANGE_MASK_FLAGS |
-		SPA_PORT_CHANGE_MASK_PARAMS;
-	port->info = SPA_PORT_INFO_INIT();
-	port->info.flags = SPA_PORT_FLAG_NO_REF |
-		SPA_PORT_FLAG_DYNAMIC_DATA;
-	port->params[PORT_EnumFormat] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ);
-	port->params[PORT_Meta] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ);
-	port->params[PORT_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
-	port->params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
-	port->params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
-	port->params[PORT_Latency] = SPA_PARAM_INFO(SPA_PARAM_Latency, SPA_PARAM_INFO_READWRITE);
-	port->info.params = port->params;
-	port->info.n_params = N_PORT_PARAMS;
-	port->have_format = false;
-
-	return 0;
-}
-
-static size_t
-impl_get_size(const struct spa_handle_factory *factory,
-	      const struct spa_dict *params)
-{
-	return sizeof(struct impl);
-}
-
-static int
-impl_init(const struct spa_handle_factory *factory,
-	  struct spa_handle *handle,
-	  const struct spa_dict *info,
-	  const struct spa_support *support,
-	  uint32_t n_support)
-{
-	struct impl *this;
-	uint32_t i;
-
-	spa_return_val_if_fail(factory != NULL, -EINVAL);
-	spa_return_val_if_fail(handle != NULL, -EINVAL);
-
-	handle->get_interface = impl_get_interface;
-	handle->clear = impl_clear;
-
-	this = (struct impl *) handle;
-
-	this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
-	spa_log_topic_init(this->log, log_topic);
-
-	this->cpu = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_CPU);
-	if (this->cpu) {
-		this->cpu_flags = spa_cpu_get_flags(this->cpu);
-		this->max_align = SPA_MIN(MAX_ALIGN, spa_cpu_get_max_align(this->cpu));
-	}
-
-	for (i = 0; info && i < info->n_items; i++) {
-		const char *k = info->items[i].key;
-		const char *s = info->items[i].value;
-		if (spa_streq(k, "clock.quantum-limit"))
-			spa_atou32(s, &this->quantum_limit, 0);
-	}
-
-	this->node.iface = SPA_INTERFACE_INIT(
-			SPA_TYPE_INTERFACE_Node,
-			SPA_VERSION_NODE,
-			&impl_node, this);
-	spa_hook_list_init(&this->hooks);
-
-	this->info_all = SPA_PORT_CHANGE_MASK_FLAGS;
-	this->info = SPA_NODE_INFO_INIT();
-	this->info.flags = SPA_NODE_FLAG_RT;
-	this->info.params = this->params;
-	this->info.n_params = N_NODE_PARAMS;
-	props_reset(&this->props);
-
-	this->latency[SPA_DIRECTION_INPUT] = SPA_LATENCY_INFO(SPA_DIRECTION_INPUT);
-	this->latency[SPA_DIRECTION_OUTPUT] = SPA_LATENCY_INFO(SPA_DIRECTION_OUTPUT);
-
-	init_port(this, SPA_DIRECTION_OUTPUT, 0);
-	init_port(this, SPA_DIRECTION_INPUT, 0);
-
-	return 0;
-}
-
-static const struct spa_interface_info impl_interfaces[] = {
-	{SPA_TYPE_INTERFACE_Node,},
-};
-
-static int
-impl_enum_interface_info(const struct spa_handle_factory *factory,
-			 const struct spa_interface_info **info,
-			 uint32_t *index)
-{
-	spa_return_val_if_fail(factory != NULL, -EINVAL);
-	spa_return_val_if_fail(info != NULL, -EINVAL);
-	spa_return_val_if_fail(index != NULL, -EINVAL);
-
-	switch (*index) {
-	case 0:
-		*info = &impl_interfaces[*index];
-		break;
-	default:
-		return 0;
-	}
-	(*index)++;
-	return 1;
-}
-
-const struct spa_handle_factory spa_fmtconvert_factory = {
-	SPA_VERSION_HANDLE_FACTORY,
-	SPA_NAME_AUDIO_PROCESS_FORMAT,
-	NULL,
-	impl_get_size,
-	impl_init,
-	impl_enum_interface_info,
-};
diff --git a/spa/plugins/audioconvert/merger.c b/spa/plugins/audioconvert/merger.c
deleted file mode 100644
index 3310855865aa2e439a3b667665669888322c5b39..0000000000000000000000000000000000000000
--- a/spa/plugins/audioconvert/merger.c
+++ /dev/null
@@ -1,1550 +0,0 @@
-/* Spa
- *
- * Copyright © 2018 Wim Taymans
- *
- * Permission is hereby granted, free of charge, to any person obtaining a
- * copy of this software and associated documentation files (the "Software"),
- * to deal in the Software without restriction, including without limitation
- * the rights to use, copy, modify, merge, publish, distribute, sublicense,
- * and/or sell copies of the Software, and to permit persons to whom the
- * Software is furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice (including the next
- * paragraph) shall be included in all copies or substantial portions of the
- * Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
- * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
- * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
- * DEALINGS IN THE SOFTWARE.
- */
-
-#include <errno.h>
-#include <string.h>
-#include <stdio.h>
-#include <limits.h>
-
-#include <spa/support/plugin.h>
-#include <spa/support/cpu.h>
-#include <spa/support/log.h>
-#include <spa/utils/result.h>
-#include <spa/utils/list.h>
-#include <spa/utils/names.h>
-#include <spa/utils/string.h>
-#include <spa/node/node.h>
-#include <spa/node/io.h>
-#include <spa/node/utils.h>
-#include <spa/node/keys.h>
-#include <spa/param/audio/format-utils.h>
-#include <spa/param/param.h>
-#include <spa/param/latency-utils.h>
-#include <spa/pod/filter.h>
-#include <spa/debug/types.h>
-#include <spa/debug/pod.h>
-
-#include "volume-ops.h"
-#include "fmt-ops.h"
-
-#undef SPA_LOG_TOPIC_DEFAULT
-#define SPA_LOG_TOPIC_DEFAULT log_topic
-static struct spa_log_topic *log_topic = &SPA_LOG_TOPIC(0, "spa.merger");
-
-#define DEFAULT_RATE		48000
-#define DEFAULT_CHANNELS	2
-
-#define MAX_ALIGN	FMT_OPS_MAX_ALIGN
-#define MAX_BUFFERS	32
-#define MAX_DATAS	SPA_AUDIO_MAX_CHANNELS
-#define MAX_PORTS	SPA_AUDIO_MAX_CHANNELS
-
-#define DEFAULT_MUTE	false
-#define DEFAULT_VOLUME	VOLUME_NORM
-
-struct volumes {
-	bool mute;
-	uint32_t n_volumes;
-	float volumes[SPA_AUDIO_MAX_CHANNELS];
-};
-
-static void init_volumes(struct volumes *vol)
-{
-	uint32_t i;
-	vol->mute = DEFAULT_MUTE;
-	vol->n_volumes = 0;
-	for (i = 0; i < SPA_AUDIO_MAX_CHANNELS; i++)
-		vol->volumes[i] = DEFAULT_VOLUME;
-}
-
-struct props {
-	float volume;
-	uint32_t n_channels;
-	uint32_t channel_map[SPA_AUDIO_MAX_CHANNELS];
-	struct volumes channel;
-	struct volumes soft;
-	struct volumes monitor;
-};
-
-static void props_reset(struct props *props)
-{
-	uint32_t i;
-	props->volume = DEFAULT_VOLUME;
-	props->n_channels = 0;
-	for (i = 0; i < SPA_AUDIO_MAX_CHANNELS; i++)
-		props->channel_map[i] = SPA_AUDIO_CHANNEL_UNKNOWN;
-	init_volumes(&props->channel);
-	init_volumes(&props->soft);
-	init_volumes(&props->monitor);
-}
-
-struct buffer {
-	uint32_t id;
-#define BUFFER_FLAG_QUEUED	(1<<0)
-	uint32_t flags;
-	struct spa_list link;
-	struct spa_buffer *buf;
-	void *datas[MAX_DATAS];
-};
-
-struct port {
-	uint32_t direction;
-	uint32_t id;
-
-	struct spa_io_buffers *io;
-
-	uint64_t info_all;
-	struct spa_port_info info;
-#define IDX_EnumFormat	0
-#define IDX_Meta	1
-#define IDX_IO		2
-#define IDX_Format	3
-#define IDX_Buffers	4
-#define IDX_Latency	5
-#define N_PORT_PARAMS	6
-	struct spa_param_info params[N_PORT_PARAMS];
-	char position[16];
-
-	struct spa_audio_info format;
-	uint32_t blocks;
-	uint32_t stride;
-
-	struct buffer buffers[MAX_BUFFERS];
-	uint32_t n_buffers;
-
-	struct spa_list queue;
-
-	unsigned int have_format:1;
-};
-
-struct impl {
-	struct spa_handle handle;
-	struct spa_node node;
-
-	struct spa_log *log;
-	struct spa_cpu *cpu;
-
-	uint32_t cpu_flags;
-	uint32_t max_align;
-	uint32_t quantum_limit;
-
-	struct spa_io_position *io_position;
-
-	uint64_t info_all;
-	struct spa_node_info info;
-#define IDX_PortConfig	0
-#define IDX_PropInfo	1
-#define IDX_Props	2
-#define N_NODE_PARAMS	3
-	struct spa_param_info params[N_NODE_PARAMS];
-
-	struct spa_hook_list hooks;
-
-	uint32_t port_count;
-	uint32_t monitor_count;
-	struct port *in_ports[MAX_PORTS];
-	struct port *out_ports[MAX_PORTS + 1];
-
-	struct spa_audio_info format;
-	unsigned int have_profile:1;
-
-	struct convert conv;
-	unsigned int is_passthrough:1;
-	unsigned int started:1;
-	unsigned int monitor:1;
-	unsigned int monitor_channel_volumes:1;
-
-	struct volume volume;
-	struct props props;
-
-	uint32_t src_remap[SPA_AUDIO_MAX_CHANNELS];
-	uint32_t dst_remap[SPA_AUDIO_MAX_CHANNELS];
-
-	struct spa_latency_info latency[2];
-
-	uint32_t empty_size;
-	float *empty;
-};
-
-#define CHECK_IN_PORT(this,d,p)		((d) == SPA_DIRECTION_INPUT && (p) < this->port_count)
-#define CHECK_OUT_PORT(this,d,p)	((d) == SPA_DIRECTION_OUTPUT && (p) <= this->monitor_count)
-#define CHECK_PORT(this,d,p)		(CHECK_OUT_PORT(this,d,p) || CHECK_IN_PORT (this,d,p))
-#define GET_IN_PORT(this,p)		(this->in_ports[p])
-#define GET_OUT_PORT(this,p)		(this->out_ports[p])
-#define GET_PORT(this,d,p)		(d == SPA_DIRECTION_INPUT ? GET_IN_PORT(this,p) : GET_OUT_PORT(this,p))
-
-#define PORT_IS_DSP(d,p) (p != 0 || d != SPA_DIRECTION_OUTPUT)
-
-static void emit_node_info(struct impl *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) {
-		spa_node_emit_info(&this->hooks, &this->info);
-		this->info.change_mask = old;
-	}
-}
-
-static void emit_port_info(struct impl *this, struct port *port, bool full)
-{
-	uint64_t old = full ? port->info.change_mask : 0;
-	if (full)
-		port->info.change_mask = port->info_all;
-	if (port->info.change_mask) {
-		struct spa_dict_item items[3];
-		uint32_t n_items = 0;
-
-		if (PORT_IS_DSP(port->direction, port->id)) {
-			items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_FORMAT_DSP, "32 bit float mono audio");
-			items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_AUDIO_CHANNEL, port->position);
-			if (port->direction == SPA_DIRECTION_OUTPUT)
-				items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_PORT_MONITOR, "true");
-		}
-		port->info.props = &SPA_DICT_INIT(items, n_items);
-
-		spa_node_emit_port_info(&this->hooks, port->direction, port->id, &port->info);
-		port->info.change_mask = old;
-	}
-}
-
-static int init_port(struct impl *this, enum spa_direction direction, uint32_t port_id,
-		uint32_t position)
-{
-	struct port *port = GET_PORT(this, direction, port_id);
-	const char *name;
-
-	if (port == NULL) {
-		port = calloc(1, sizeof(struct port));
-		if (port == NULL)
-			return -errno;
-		if (direction == SPA_DIRECTION_INPUT)
-			this->in_ports[port_id] = port;
-		else
-			this->out_ports[port_id] = port;
-	}
-	port->direction = direction;
-	port->id = port_id;
-
-	name = spa_debug_type_find_short_name(spa_type_audio_channel, position);
-	snprintf(port->position, sizeof(port->position), "%s", name ? name : "UNK");
-
-	port->info_all = SPA_PORT_CHANGE_MASK_FLAGS |
-			SPA_PORT_CHANGE_MASK_PROPS |
-			SPA_PORT_CHANGE_MASK_PARAMS;
-	port->info = SPA_PORT_INFO_INIT();
-	port->info.flags = SPA_PORT_FLAG_NO_REF |
-		SPA_PORT_FLAG_DYNAMIC_DATA;
-	port->params[IDX_EnumFormat] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ);
-	port->params[IDX_Meta] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ);
-	port->params[IDX_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
-	port->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
-	port->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
-	port->params[IDX_Latency] = SPA_PARAM_INFO(SPA_PARAM_Latency, SPA_PARAM_INFO_READWRITE);
-	port->info.params = port->params;
-	port->info.n_params = N_PORT_PARAMS;
-
-	port->n_buffers = 0;
-	port->have_format = false;
-	port->format.media_type = SPA_MEDIA_TYPE_audio;
-	port->format.media_subtype = SPA_MEDIA_SUBTYPE_dsp;
-	port->format.info.dsp.format = SPA_AUDIO_FORMAT_DSP_F32;
-	spa_list_init(&port->queue);
-
-	spa_log_debug(this->log, "%p: add port %d:%d position:%s",
-			this, direction, port_id, port->position);
-	emit_port_info(this, port, true);
-
-	return 0;
-}
-
-static int impl_node_enum_params(void *object, int seq,
-				 uint32_t id, uint32_t start, uint32_t num,
-				 const struct spa_pod *filter)
-{
-	struct impl *this = object;
-	struct spa_pod *param;
-	struct spa_pod_builder b = { 0 };
-	uint8_t buffer[4096];
-	struct spa_result_node_params result;
-	uint32_t count = 0;
-
-	spa_return_val_if_fail(this != NULL, -EINVAL);
-	spa_return_val_if_fail(num != 0, -EINVAL);
-
-	result.id = id;
-	result.next = start;
-      next:
-	result.index = result.next++;
-
-	spa_pod_builder_init(&b, buffer, sizeof(buffer));
-
-	switch (id) {
-	case SPA_PARAM_PortConfig:
-		return -ENOTSUP;
-
-	case SPA_PARAM_PropInfo:
-	{
-		switch (result.index) {
-		default:
-			return 0;
-		}
-		break;
-	}
-
-	case SPA_PARAM_Props:
-	{
-		switch (result.index) {
-		default:
-			return 0;
-		}
-		break;
-	}
-	default:
-		return 0;
-	}
-
-	if (spa_pod_filter(&b, &result.param, param, filter) < 0)
-		goto next;
-
-	spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
-
-	if (++count != num)
-		goto next;
-
-	return 0;
-}
-
-static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size)
-{
-	struct impl *this = object;
-
-	spa_return_val_if_fail(this != NULL, -EINVAL);
-
-	spa_log_debug(this->log, "%p: io %d %p/%zd", this, id, data, size);
-
-	switch (id) {
-	case SPA_IO_Position:
-		this->io_position = data;
-		break;
-	default:
-		return -ENOENT;
-	}
-	return 0;
-}
-
-static int merger_set_param(struct impl *this, const char *k, const char *s)
-{
-	if (spa_streq(k, "monitor.channel-volumes"))
-		this->monitor_channel_volumes = spa_atob(s);
-	return 0;
-}
-
-static int parse_prop_params(struct impl *this, struct spa_pod *params)
-{
-	struct spa_pod_parser prs;
-	struct spa_pod_frame f;
-
-	spa_pod_parser_pod(&prs, params);
-	if (spa_pod_parser_push_struct(&prs, &f) < 0)
-		return 0;
-
-	while (true) {
-		const char *name;
-		struct spa_pod *pod;
-		char value[512];
-
-		if (spa_pod_parser_get_string(&prs, &name) < 0)
-			break;
-
-		if (spa_pod_parser_get_pod(&prs, &pod) < 0)
-			break;
-
-		if (spa_pod_is_bool(pod)) {
-			snprintf(value, sizeof(value), "%s",
-					SPA_POD_VALUE(struct spa_pod_bool, pod) ?
-					"true" : "false");
-		} else
-			continue;
-
-		spa_log_info(this->log, "key:'%s' val:'%s'", name, value);
-		merger_set_param(this, name, value);
-	}
-	return 0;
-}
-
-static int apply_props(struct impl *this, const struct spa_pod *param)
-{
-	struct spa_pod_prop *prop;
-	struct spa_pod_object *obj = (struct spa_pod_object *) param;
-	struct props *p = &this->props;
-	int changed = 0;
-	uint32_t n;
-
-	SPA_POD_OBJECT_FOREACH(obj, prop) {
-		switch (prop->key) {
-		case SPA_PROP_volume:
-			if (spa_pod_get_float(&prop->value, &p->volume) == 0)
-				changed++;
-			break;
-		case SPA_PROP_mute:
-			if (spa_pod_get_bool(&prop->value, &p->channel.mute) == 0)
-				changed++;
-			break;
-		case SPA_PROP_channelVolumes:
-			if ((n = spa_pod_copy_array(&prop->value, SPA_TYPE_Float,
-					p->channel.volumes, SPA_AUDIO_MAX_CHANNELS)) > 0) {
-				p->channel.n_volumes = n;
-				changed++;
-			}
-			break;
-		case SPA_PROP_channelMap:
-			if ((n = spa_pod_copy_array(&prop->value, SPA_TYPE_Id,
-					p->channel_map, SPA_AUDIO_MAX_CHANNELS)) > 0) {
-				p->n_channels = n;
-				changed++;
-			}
-			break;
-		case SPA_PROP_softMute:
-			if (spa_pod_get_bool(&prop->value, &p->soft.mute) == 0)
-				changed++;
-			break;
-		case SPA_PROP_softVolumes:
-			if ((n = spa_pod_copy_array(&prop->value, SPA_TYPE_Float,
-					p->soft.volumes, SPA_AUDIO_MAX_CHANNELS)) > 0) {
-				p->soft.n_volumes = n;
-				changed++;
-			}
-			break;
-		case SPA_PROP_monitorMute:
-			if (spa_pod_get_bool(&prop->value, &p->monitor.mute) == 0)
-				changed++;
-			break;
-		case SPA_PROP_monitorVolumes:
-			if ((n = spa_pod_copy_array(&prop->value, SPA_TYPE_Float,
-					p->monitor.volumes, SPA_AUDIO_MAX_CHANNELS)) > 0) {
-				p->monitor.n_volumes = n;
-				changed++;
-			}
-			break;
-		case SPA_PROP_params:
-			parse_prop_params(this, &prop->value);
-			break;
-		default:
-			break;
-		}
-	}
-	return changed;
-}
-
-static int int32_cmp(const void *v1, const void *v2)
-{
-	int32_t a1 = *(int32_t*)v1;
-	int32_t a2 = *(int32_t*)v2;
-	if (a1 == 0 && a2 != 0)
-		return 1;
-	if (a2 == 0 && a1 != 0)
-		return -1;
-	return a1 - a2;
-}
-
-static int impl_node_set_param(void *object, uint32_t id, uint32_t flags,
-			       const struct spa_pod *param)
-{
-	struct impl *this = object;
-	int res;
-
-	spa_return_val_if_fail(this != NULL, -EINVAL);
-
-	if (param == NULL)
-		return 0;
-
-	switch (id) {
-	case SPA_PARAM_PortConfig:
-	{
-		struct spa_audio_info info = { 0, };
-		struct port *port;
-		struct spa_pod *format;
-		enum spa_direction direction;
-		enum spa_param_port_config_mode mode;
-		bool monitor = false;
-		uint32_t i;
-
-		if (spa_pod_parse_object(param,
-				SPA_TYPE_OBJECT_ParamPortConfig, NULL,
-				SPA_PARAM_PORT_CONFIG_direction,	SPA_POD_Id(&direction),
-				SPA_PARAM_PORT_CONFIG_mode,		SPA_POD_Id(&mode),
-				SPA_PARAM_PORT_CONFIG_monitor,		SPA_POD_OPT_Bool(&monitor),
-				SPA_PARAM_PORT_CONFIG_format,		SPA_POD_Pod(&format)) < 0)
-			return -EINVAL;
-
-		if (!spa_pod_is_object_type(format, SPA_TYPE_OBJECT_Format))
-			return -EINVAL;
-
-		if (mode != SPA_PARAM_PORT_CONFIG_MODE_dsp)
-			return -ENOTSUP;
-		if (direction != SPA_DIRECTION_INPUT)
-			return -EINVAL;
-
-		if ((res = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0)
-			return res;
-
-		if (info.media_type != SPA_MEDIA_TYPE_audio ||
-		    info.media_subtype != SPA_MEDIA_SUBTYPE_raw)
-			return -EINVAL;
-
-		if (spa_format_audio_raw_parse(format, &info.info.raw) < 0)
-			return -EINVAL;
-
-		info.info.raw.rate = 0;
-
-		if (this->have_profile &&
-		    memcmp(&this->format, &info, sizeof(info)) == 0 &&
-		    this->monitor == monitor)
-			return 0;
-
-		spa_log_debug(this->log, "%p: port config %d/%d %d", this,
-				info.info.raw.rate, info.info.raw.channels, monitor);
-
-		for (i = 0; i < this->port_count; i++) {
-			spa_node_emit_port_info(&this->hooks,
-					SPA_DIRECTION_INPUT, i, NULL);
-			if (this->monitor)
-				spa_node_emit_port_info(&this->hooks,
-						SPA_DIRECTION_OUTPUT, i+1, NULL);
-		}
-
-		this->monitor = monitor;
-		this->format = info;
-		this->have_profile = true;
-		this->port_count = info.info.raw.channels;
-		this->monitor_count = this->monitor ? this->port_count : 0;
-		for (i = 0; i < this->port_count; i++)
-			this->props.channel_map[i] = info.info.raw.position[i];
-		this->props.channel.n_volumes = this->port_count;
-		this->props.monitor.n_volumes = this->port_count;
-		this->props.soft.n_volumes = this->port_count;
-		this->props.n_channels = this->port_count;
-
-		for (i = 0; i < this->port_count; i++) {
-			init_port(this, SPA_DIRECTION_INPUT, i, info.info.raw.position[i]);
-			if (this->monitor)
-				init_port(this, SPA_DIRECTION_OUTPUT, i+1,
-					info.info.raw.position[i]);
-		}
-
-		port = GET_OUT_PORT(this, 0);
-		qsort(info.info.raw.position, info.info.raw.channels,
-					sizeof(uint32_t), int32_cmp);
-		port->format = info;
-		port->have_format = true;
-
-		this->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS;
-		this->params[IDX_Props].flags ^= SPA_PARAM_INFO_SERIAL;
-		emit_node_info(this, false);
-		return 0;
-	}
-	case SPA_PARAM_Props:
-		if (apply_props(this, param) > 0) {
-			this->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS;
-			this->params[IDX_Props].flags ^= SPA_PARAM_INFO_SERIAL;
-			emit_node_info(this, false);
-		}
-		break;
-	default:
-		return -ENOENT;
-	}
-	return 0;
-}
-
-static int impl_node_send_command(void *object, const struct spa_command *command)
-{
-	struct impl *this = object;
-
-	spa_return_val_if_fail(this != NULL, -EINVAL);
-	spa_return_val_if_fail(command != NULL, -EINVAL);
-
-	switch (SPA_NODE_COMMAND_ID(command)) {
-	case SPA_NODE_COMMAND_Start:
-		this->started = true;
-		break;
-	case SPA_NODE_COMMAND_Suspend:
-	case SPA_NODE_COMMAND_Flush:
-	case SPA_NODE_COMMAND_Pause:
-		this->started = false;
-		break;
-	default:
-		return -ENOTSUP;
-	}
-	return 0;
-}
-
-static int
-impl_node_add_listener(void *object,
-		struct spa_hook *listener,
-		const struct spa_node_events *events,
-		void *data)
-{
-	struct impl *this = object;
-	uint32_t i;
-	struct spa_hook_list save;
-
-	spa_return_val_if_fail(this != NULL, -EINVAL);
-
-	spa_log_trace(this->log, "%p: add listener %p", this, listener);
-	spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
-
-	emit_node_info(this, true);
-	emit_port_info(this, GET_OUT_PORT(this, 0), true);
-	for (i = 0; i < this->port_count; i++) {
-		emit_port_info(this, GET_IN_PORT(this, i), true);
-		if (this->monitor)
-			emit_port_info(this, GET_OUT_PORT(this, i+1), true);
-	}
-
-	spa_hook_list_join(&this->hooks, &save);
-
-	return 0;
-}
-
-static int
-impl_node_set_callbacks(void *object,
-			const struct spa_node_callbacks *callbacks,
-			void *user_data)
-{
-	return 0;
-}
-
-static int impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id,
-		const struct spa_dict *props)
-{
-	return -ENOTSUP;
-}
-
-static int
-impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id)
-{
-	return -ENOTSUP;
-}
-
-static int port_enum_formats(void *object,
-			     enum spa_direction direction, uint32_t port_id,
-			     uint32_t index,
-			     struct spa_pod **param,
-			     struct spa_pod_builder *builder)
-{
-	struct impl *this = object;
-	struct port *port = GET_PORT(this, direction, port_id);
-
-	switch (index) {
-	case 0:
-		if (PORT_IS_DSP(direction, port_id)) {
-			*param = spa_format_audio_dsp_build(builder,
-				SPA_PARAM_EnumFormat, &port->format.info.dsp);
-		} else if (port->have_format) {
-			*param = spa_format_audio_raw_build(builder,
-				SPA_PARAM_EnumFormat, &port->format.info.raw);
-		}
-		else {
-			uint32_t rate = this->io_position ?
-				this->io_position->clock.rate.denom : DEFAULT_RATE;
-
-			*param = spa_pod_builder_add_object(builder,
-				SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat,
-				SPA_FORMAT_mediaType,      SPA_POD_Id(SPA_MEDIA_TYPE_audio),
-				SPA_FORMAT_mediaSubtype,   SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw),
-				SPA_FORMAT_AUDIO_format,   SPA_POD_CHOICE_ENUM_Id(25,
-							SPA_AUDIO_FORMAT_F32P,
-							SPA_AUDIO_FORMAT_F32P,
-							SPA_AUDIO_FORMAT_F32,
-							SPA_AUDIO_FORMAT_F32_OE,
-							SPA_AUDIO_FORMAT_F64P,
-							SPA_AUDIO_FORMAT_F64,
-							SPA_AUDIO_FORMAT_F64_OE,
-							SPA_AUDIO_FORMAT_S32P,
-							SPA_AUDIO_FORMAT_S32,
-							SPA_AUDIO_FORMAT_S32_OE,
-							SPA_AUDIO_FORMAT_S24_32P,
-							SPA_AUDIO_FORMAT_S24_32,
-							SPA_AUDIO_FORMAT_S24_32_OE,
-							SPA_AUDIO_FORMAT_S24P,
-							SPA_AUDIO_FORMAT_S24,
-							SPA_AUDIO_FORMAT_S24_OE,
-							SPA_AUDIO_FORMAT_S16P,
-							SPA_AUDIO_FORMAT_S16,
-							SPA_AUDIO_FORMAT_S16_OE,
-							SPA_AUDIO_FORMAT_S8P,
-							SPA_AUDIO_FORMAT_S8,
-							SPA_AUDIO_FORMAT_U8P,
-							SPA_AUDIO_FORMAT_U8,
-							SPA_AUDIO_FORMAT_ULAW,
-							SPA_AUDIO_FORMAT_ALAW),
-				SPA_FORMAT_AUDIO_rate,     SPA_POD_CHOICE_RANGE_Int(
-					rate, 1, INT32_MAX),
-				SPA_FORMAT_AUDIO_channels, SPA_POD_CHOICE_RANGE_Int(
-					DEFAULT_CHANNELS, 1, MAX_PORTS));
-		}
-		break;
-	default:
-		return 0;
-	}
-	return 1;
-}
-
-static int
-impl_node_port_enum_params(void *object, int seq,
-			   enum spa_direction direction, uint32_t port_id,
-			   uint32_t id, uint32_t start, uint32_t num,
-			   const struct spa_pod *filter)
-{
-	struct impl *this = object;
-	struct port *port;
-	struct spa_pod *param;
-	struct spa_pod_builder b = { 0 };
-	uint8_t buffer[2048];
-	struct spa_result_node_params result;
-	uint32_t count = 0;
-	int res;
-
-	spa_return_val_if_fail(this != NULL, -EINVAL);
-	spa_return_val_if_fail(num != 0, -EINVAL);
-
-	spa_log_debug(this->log, "%p: enum params port %d.%d %d %u",
-			this, direction, port_id, seq, id);
-
-	spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
-
-	port = GET_PORT(this, direction, port_id);
-
-	result.id = id;
-	result.next = start;
-      next:
-	result.index = result.next++;
-
-	spa_pod_builder_init(&b, buffer, sizeof(buffer));
-
-	switch (id) {
-	case SPA_PARAM_EnumFormat:
-		if ((res = port_enum_formats(object, direction, port_id, result.index, &param, &b)) <= 0)
-			return res;
-		break;
-	case SPA_PARAM_Format:
-		if (!port->have_format)
-			return -EIO;
-		if (result.index > 0)
-			return 0;
-
-		if (PORT_IS_DSP(direction, port_id))
-			param = spa_format_audio_dsp_build(&b, id, &port->format.info.dsp);
-		else
-			param = spa_format_audio_raw_build(&b, id, &port->format.info.raw);
-		break;
-	case SPA_PARAM_Buffers:
-		if (!port->have_format)
-			return -EIO;
-		if (result.index > 0)
-			return 0;
-
-		param = spa_pod_builder_add_object(&b,
-			SPA_TYPE_OBJECT_ParamBuffers, id,
-			SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(1, 1, MAX_BUFFERS),
-			SPA_PARAM_BUFFERS_blocks,  SPA_POD_Int(port->blocks),
-			SPA_PARAM_BUFFERS_size,    SPA_POD_CHOICE_RANGE_Int(
-								this->quantum_limit * port->stride,
-								16 * port->stride,
-								INT32_MAX),
-			SPA_PARAM_BUFFERS_stride,  SPA_POD_Int(port->stride));
-		break;
-	case SPA_PARAM_Meta:
-		switch (result.index) {
-		case 0:
-			param = spa_pod_builder_add_object(&b,
-				SPA_TYPE_OBJECT_ParamMeta, id,
-				SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header),
-				SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header)));
-			break;
-		default:
-			return 0;
-		}
-		break;
-	case SPA_PARAM_IO:
-		switch (result.index) {
-		case 0:
-			param = spa_pod_builder_add_object(&b,
-				SPA_TYPE_OBJECT_ParamIO, id,
-				SPA_PARAM_IO_id,   SPA_POD_Id(SPA_IO_Buffers),
-				SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers)));
-			break;
-		default:
-			return 0;
-		}
-		break;
-	case SPA_PARAM_Latency:
-		switch (result.index) {
-		case 0: case 1:
-			param = spa_latency_build(&b, id, &this->latency[result.index]);
-			break;
-		default:
-			return 0;
-		}
-		break;
-	default:
-		return -ENOENT;
-	}
-
-	if (spa_pod_filter(&b, &result.param, param, filter) < 0)
-		goto next;
-
-	spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
-
-	if (++count != num)
-		goto next;
-
-	return 0;
-}
-
-static int clear_buffers(struct impl *this, struct port *port)
-{
-	if (port->n_buffers > 0) {
-		spa_log_debug(this->log, "%p: clear buffers %p", this, port);
-		port->n_buffers = 0;
-		spa_list_init(&port->queue);
-	}
-	return 0;
-}
-
-static int setup_convert(struct impl *this)
-{
-	struct port *outport;
-	struct spa_audio_info informat, outformat;
-	uint32_t i, j, src_fmt, dst_fmt;
-	int res;
-
-	outport = GET_OUT_PORT(this, 0);
-
-	informat = this->format;
-	outformat = outport->format;
-
-	src_fmt = SPA_AUDIO_FORMAT_DSP_F32;
-	dst_fmt = outformat.info.raw.format;
-
-	spa_log_info(this->log, "%p: %s/%d@%dx%d->%s/%d@%d", this,
-			spa_debug_type_find_name(spa_type_audio_format, src_fmt),
-			1,
-			informat.info.raw.rate,
-			informat.info.raw.channels,
-			spa_debug_type_find_name(spa_type_audio_format, dst_fmt),
-			outformat.info.raw.channels,
-			outformat.info.raw.rate);
-
-	for (i = 0; i < informat.info.raw.channels; i++) {
-		for (j = 0; j < outformat.info.raw.channels; j++) {
-			if (informat.info.raw.position[i] !=
-			    outformat.info.raw.position[j])
-				continue;
-			this->src_remap[j] = i;
-			this->dst_remap[i] = j;
-			spa_log_debug(this->log, "%p: channel %d -> %d (%s -> %s)", this,
-					i, j,
-					spa_debug_type_find_short_name(spa_type_audio_channel,
-						informat.info.raw.position[i]),
-					spa_debug_type_find_short_name(spa_type_audio_channel,
-						outformat.info.raw.position[j]));
-			outformat.info.raw.position[j] = -1;
-			break;
-		}
-	}
-
-	this->conv.src_fmt = src_fmt;
-	this->conv.dst_fmt = dst_fmt;
-	this->conv.n_channels = outformat.info.raw.channels;
-	this->conv.cpu_flags = this->cpu_flags;
-
-	if ((res = convert_init(&this->conv)) < 0)
-		return res;
-
-	this->is_passthrough = this->conv.is_passthrough;
-
-	spa_log_debug(this->log, "%p: got converter features %08x:%08x passthrough:%d", this,
-			this->cpu_flags, this->conv.cpu_flags, this->is_passthrough);
-
-	return 0;
-}
-
-static int calc_width(struct spa_audio_info *info)
-{
-	switch (info->info.raw.format) {
-	case SPA_AUDIO_FORMAT_U8:
-	case SPA_AUDIO_FORMAT_U8P:
-	case SPA_AUDIO_FORMAT_S8:
-	case SPA_AUDIO_FORMAT_S8P:
-	case SPA_AUDIO_FORMAT_ULAW:
-	case SPA_AUDIO_FORMAT_ALAW:
-		return 1;
-	case SPA_AUDIO_FORMAT_S16P:
-	case SPA_AUDIO_FORMAT_S16:
-	case SPA_AUDIO_FORMAT_S16_OE:
-		return 2;
-	case SPA_AUDIO_FORMAT_S24P:
-	case SPA_AUDIO_FORMAT_S24:
-	case SPA_AUDIO_FORMAT_S24_OE:
-		return 3;
-	case SPA_AUDIO_FORMAT_F64P:
-	case SPA_AUDIO_FORMAT_F64:
-	case SPA_AUDIO_FORMAT_F64_OE:
-		return 8;
-	default:
-		return 4;
-	}
-}
-
-static int port_set_latency(void *object,
-			   enum spa_direction direction,
-			   uint32_t port_id,
-			   uint32_t flags,
-			   const struct spa_pod *latency)
-{
-	struct impl *this = object;
-	struct port *port;
-	enum spa_direction other = SPA_DIRECTION_REVERSE(direction);
-	uint32_t i;
-
-	spa_log_debug(this->log, "%p: set latency direction:%d id:%d",
-			this, direction, port_id);
-
-	if (direction == SPA_DIRECTION_OUTPUT && port_id != 0)
-		return 0;
-
-	if (latency == NULL) {
-		this->latency[other] = SPA_LATENCY_INFO(other);
-	} else {
-		struct spa_latency_info info;
-		if (spa_latency_parse(latency, &info) < 0 ||
-		    info.direction != other)
-			return -EINVAL;
-		this->latency[other] = info;
-	}
-	for (i = 0; i < this->port_count; i++) {
-		port = GET_IN_PORT(this, i);
-		port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
-		port->params[IDX_Latency].flags ^= SPA_PARAM_INFO_SERIAL;
-		emit_port_info(this, port, false);
-	}
-	port = GET_OUT_PORT(this, 0);
-	port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
-	port->params[IDX_Latency].flags ^= SPA_PARAM_INFO_SERIAL;
-	emit_port_info(this, port, false);
-	return 0;
-}
-
-static int port_set_format(void *object,
-			   enum spa_direction direction,
-			   uint32_t port_id,
-			   uint32_t flags,
-			   const struct spa_pod *format)
-{
-	struct impl *this = object;
-	struct port *port;
-	int res;
-
-	port = GET_PORT(this, direction, port_id);
-
-	spa_log_debug(this->log, "%p: set format", this);
-
-	if (format == NULL) {
-		if (port->have_format) {
-			if (PORT_IS_DSP(direction, port_id))
-				port->have_format = false;
-			else
-				port->have_format = this->have_profile;
-			port->format.info.raw.rate = 0;
-			clear_buffers(this, port);
-		}
-	} else {
-		struct spa_audio_info info = { 0 };
-
-		if ((res = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0) {
-			spa_log_error(this->log, "can't parse format %s", spa_strerror(res));
-			return res;
-		}
-		if (PORT_IS_DSP(direction, port_id)) {
-			if (info.media_type != SPA_MEDIA_TYPE_audio ||
-			    info.media_subtype != SPA_MEDIA_SUBTYPE_dsp) {
-				spa_log_error(this->log, "unexpected types %d/%d",
-						info.media_type, info.media_subtype);
-				return -EINVAL;
-			}
-			if ((res = spa_format_audio_dsp_parse(format, &info.info.dsp)) < 0) {
-				spa_log_error(this->log, "can't parse format %s", spa_strerror(res));
-				return res;
-			}
-			if (info.info.dsp.format != SPA_AUDIO_FORMAT_DSP_F32) {
-				spa_log_error(this->log, "unexpected format %d<->%d",
-					info.info.dsp.format, SPA_AUDIO_FORMAT_DSP_F32);
-				return -EINVAL;
-			}
-			port->blocks = 1;
-			port->stride = 4;
-		}
-		else {
-			if (info.media_type != SPA_MEDIA_TYPE_audio ||
-			    info.media_subtype != SPA_MEDIA_SUBTYPE_raw) {
-				spa_log_error(this->log, "unexpected types %d/%d",
-						info.media_type, info.media_subtype);
-				return -EINVAL;
-			}
-			if ((res = spa_format_audio_raw_parse(format, &info.info.raw)) < 0) {
-				spa_log_error(this->log, "can't parse format %s", spa_strerror(res));
-				return res;
-			}
-			if (info.info.raw.channels != this->port_count) {
-				spa_log_error(this->log, "unexpected channels %d<->%d",
-					info.info.raw.channels, this->port_count);
-				return -EINVAL;
-			}
-			port->stride = calc_width(&info);
-			if (SPA_AUDIO_FORMAT_IS_PLANAR(info.info.raw.format)) {
-				port->blocks = info.info.raw.channels;
-			}
-			else {
-				port->stride *= info.info.raw.channels;
-				port->blocks = 1;
-			}
-		}
-		port->format = info;
-
-		spa_log_debug(this->log, "%p: %d %d %d", this,
-				port_id, port->stride, port->blocks);
-
-		if (!PORT_IS_DSP(direction, port_id))
-			if ((res = setup_convert(this)) < 0)
-				return res;
-
-		port->have_format = true;
-	}
-
-	port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
-	if (port->have_format) {
-		port->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE);
-		port->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ);
-	} else {
-		port->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
-		port->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
-	}
-	emit_port_info(this, port, false);
-
-	return 0;
-}
-
-
-static int
-impl_node_port_set_param(void *object,
-			 enum spa_direction direction, uint32_t port_id,
-			 uint32_t id, uint32_t flags,
-			 const struct spa_pod *param)
-{
-	struct impl *this = object;
-
-	spa_return_val_if_fail(this != NULL, -EINVAL);
-
-	spa_log_debug(this->log, "%p: set param port %d.%d %u",
-			this, direction, port_id, id);
-
-	spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
-
-	switch (id) {
-	case SPA_PARAM_Latency:
-		return port_set_latency(this, direction, port_id, flags, param);
-	case SPA_PARAM_Format:
-		return port_set_format(this, direction, port_id, flags, param);
-	default:
-		return -ENOENT;
-	}
-}
-
-static void queue_buffer(struct impl *this, struct port *port, uint32_t id)
-{
-	struct buffer *b = &port->buffers[id];
-
-	spa_log_trace_fp(this->log, "%p: queue buffer %d on port %d %d",
-			this, id, port->id, b->flags);
-	if (SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_QUEUED))
-		return;
-
-	spa_list_append(&port->queue, &b->link);
-	SPA_FLAG_SET(b->flags, BUFFER_FLAG_QUEUED);
-}
-
-static struct buffer *dequeue_buffer(struct impl *this, struct port *port)
-{
-	struct buffer *b;
-
-	if (spa_list_is_empty(&port->queue))
-		return NULL;
-
-	b = spa_list_first(&port->queue, struct buffer, link);
-	spa_list_remove(&b->link);
-	SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_QUEUED);
-	spa_log_trace_fp(this->log, "%p: dequeue buffer %d on port %d %u",
-			this, b->id, port->id, b->flags);
-
-	return b;
-}
-
-static int
-impl_node_port_use_buffers(void *object,
-			   enum spa_direction direction,
-			   uint32_t port_id,
-			   uint32_t flags,
-			   struct spa_buffer **buffers,
-			   uint32_t n_buffers)
-{
-	struct impl *this = object;
-	struct port *port;
-	uint32_t i, j, maxsize;
-
-	spa_return_val_if_fail(this != NULL, -EINVAL);
-
-	spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
-
-	port = GET_PORT(this, direction, port_id);
-
-	spa_return_val_if_fail(port->have_format, -EIO);
-
-	spa_log_debug(this->log, "%p: use buffers %d on port %d:%d",
-			this, n_buffers, direction, port_id);
-
-	clear_buffers(this, port);
-
-	maxsize = 0;
-	for (i = 0; i < n_buffers; i++) {
-		struct buffer *b;
-		uint32_t n_datas = buffers[i]->n_datas;
-		struct spa_data *d = buffers[i]->datas;
-
-		b = &port->buffers[i];
-		b->id = i;
-		b->flags = 0;
-		b->buf = buffers[i];
-
-		if (n_datas != port->blocks) {
-			spa_log_error(this->log, "%p: invalid blocks %d on buffer %d",
-					this, n_datas, i);
-			return -EINVAL;
-		}
-
-		for (j = 0; j < n_datas; j++) {
-			if (d[j].data == NULL) {
-				spa_log_error(this->log, "%p: invalid memory %d on buffer %d %d %p",
-						this, j, i, d[j].type, d[j].data);
-				return -EINVAL;
-			}
-			if (!SPA_IS_ALIGNED(d[j].data, this->max_align)) {
-				spa_log_warn(this->log, "%p: memory %d on buffer %d not aligned",
-						this, j, i);
-			}
-			b->datas[j] = d[j].data;
-			if (direction == SPA_DIRECTION_OUTPUT &&
-			    !SPA_FLAG_IS_SET(d[j].flags, SPA_DATA_FLAG_DYNAMIC))
-				this->is_passthrough = false;
-
-			maxsize = SPA_MAX(maxsize, d[j].maxsize);
-		}
-		if (direction == SPA_DIRECTION_OUTPUT)
-			queue_buffer(this, port, i);
-	}
-	if (maxsize > this->empty_size) {
-		this->empty = realloc(this->empty, maxsize + MAX_ALIGN);
-		if (this->empty == NULL)
-			return -errno;
-		memset(this->empty, 0, maxsize + MAX_ALIGN);
-		this->empty_size = maxsize;
-	}
-	port->n_buffers = n_buffers;
-
-	return 0;
-}
-
-static int
-impl_node_port_set_io(void *object,
-		      enum spa_direction direction, uint32_t port_id,
-		      uint32_t id, void *data, size_t size)
-{
-	struct impl *this = object;
-	struct port *port;
-
-	spa_return_val_if_fail(this != NULL, -EINVAL);
-
-	spa_log_debug(this->log, "%p: set io %d on port %d:%d %p",
-			this, id, direction, port_id, data);
-
-	spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
-
-	port = GET_PORT(this, direction, port_id);
-
-	switch (id) {
-	case SPA_IO_Buffers:
-		port->io = data;
-		break;
-	default:
-		return -ENOENT;
-	}
-	return 0;
-}
-
-static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id)
-{
-	struct impl *this = object;
-	struct port *port;
-
-	spa_return_val_if_fail(this != NULL, -EINVAL);
-
-	spa_return_val_if_fail(CHECK_PORT(this, SPA_DIRECTION_OUTPUT, port_id), -EINVAL);
-
-	port = GET_OUT_PORT(this, port_id);
-	queue_buffer(this, port, buffer_id);
-
-	return 0;
-}
-
-static inline int get_in_buffer(struct impl *this, struct port *port, struct buffer **buf)
-{
-	struct spa_io_buffers *io;
-
-	if ((io = port->io) == NULL) {
-		spa_log_trace_fp(this->log, "%p: no io on port %d",
-				this, port->id);
-		return -EIO;
-	}
-	if (io->status != SPA_STATUS_HAVE_DATA ||
-	    io->buffer_id >= port->n_buffers) {
-		spa_log_trace_fp(this->log, "%p: empty port %d %p %d %d %d",
-				this, port->id, io, io->status, io->buffer_id,
-				port->n_buffers);
-		return -EPIPE;
-	}
-
-	*buf = &port->buffers[io->buffer_id];
-	io->status = SPA_STATUS_NEED_DATA;
-
-	return 0;
-}
-
-static inline int get_out_buffer(struct impl *this, struct port *port, struct buffer **buf)
-{
-	struct spa_io_buffers *io;
-
-	if (SPA_UNLIKELY((io = port->io) == NULL ||
-	    io->status == SPA_STATUS_HAVE_DATA))
-		return SPA_STATUS_HAVE_DATA;
-
-	if (SPA_LIKELY(io->buffer_id < port->n_buffers))
-		queue_buffer(this, port, io->buffer_id);
-
-	if (SPA_UNLIKELY((*buf = dequeue_buffer(this, port)) == NULL))
-		return -EPIPE;
-
-	io->status = SPA_STATUS_HAVE_DATA;
-	io->buffer_id = (*buf)->id;
-
-	return 0;
-}
-
-static inline int handle_monitor(struct impl *this, const void *data, float volume, int n_samples, struct port *outport)
-{
-	struct buffer *dbuf;
-        struct spa_data *dd;
-	int res, size;
-
-	if (SPA_UNLIKELY((res = get_out_buffer(this, outport, &dbuf)) != 0))
-		return res;
-
-	dd = &dbuf->buf->datas[0];
-	size = SPA_MIN(dd->maxsize, n_samples * outport->stride);
-	dd->chunk->offset = 0;
-	dd->chunk->size = size;
-
-	spa_log_trace(this->log, "%p: io %p %08x", this, outport->io, dd->flags);
-
-	if (SPA_FLAG_IS_SET(dd->flags, SPA_DATA_FLAG_DYNAMIC) && volume == VOLUME_NORM)
-		dd->data = (void*)data;
-	else
-		volume_process(&this->volume, dd->data, data, volume, size / outport->stride);
-
-	return res;
-}
-
-static int impl_node_process(void *object)
-{
-	struct impl *this = object;
-	struct port *outport;
-	struct spa_io_buffers *outio;
-	uint32_t i, maxsize, n_samples;
-	struct spa_data *sd, *dd;
-	struct buffer *sbuf, *dbuf;
-	uint32_t n_src_datas, n_dst_datas;
-	const void **src_datas;
-	void **dst_datas;
-	int res;
-
-	spa_return_val_if_fail(this != NULL, -EINVAL);
-
-	outport = GET_OUT_PORT(this, 0);
-	outio = outport->io;
-	spa_return_val_if_fail(outio != NULL, -EIO);
-	spa_return_val_if_fail(this->conv.process != NULL, -EIO);
-
-	spa_log_trace_fp(this->log, "%p: status %p %d %d", this,
-			outio, outio->status, outio->buffer_id);
-
-	if (SPA_UNLIKELY((res = get_out_buffer(this, outport, &dbuf)) != 0))
-		return res;
-
-	dd = &dbuf->buf->datas[0];
-
-	maxsize = dd->maxsize;
-
-	if (SPA_LIKELY(this->io_position))
-		n_samples = this->io_position->clock.duration;
-	else
-		n_samples = maxsize / outport->stride;
-
-
-	n_dst_datas = dbuf->buf->n_datas;
-	dst_datas = alloca(sizeof(void*) * n_dst_datas);
-
-	n_src_datas = this->port_count;
-	src_datas = alloca(sizeof(void*) * this->port_count);
-
-	/* produce more output if possible */
-	for (i = 0; i < n_src_datas; i++) {
-		struct port *inport = GET_IN_PORT(this, i);
-
-		if (SPA_UNLIKELY(get_in_buffer(this, inport, &sbuf) < 0)) {
-			src_datas[i] = SPA_PTR_ALIGN(this->empty, MAX_ALIGN, void);
-			continue;
-		}
-
-		sd = &sbuf->buf->datas[0];
-
-		src_datas[i] = SPA_PTROFF(sd->data, sd->chunk->offset, void);
-
-		n_samples = SPA_MIN(n_samples, sd->chunk->size / inport->stride);
-
-		spa_log_trace_fp(this->log, "%p: %d %d %d %p", this,
-				sd->chunk->size, maxsize, n_samples, src_datas[i]);
-	}
-
-	for (i = 0; i < this->monitor_count; i++) {
-		float volume;
-
-		volume = this->props.monitor.mute ? 0.0f : this->props.monitor.volumes[i];
-		if (this->monitor_channel_volumes)
-			volume *= this->props.channel.mute ? 0.0f : this->props.channel.volumes[i];
-
-		handle_monitor(this, src_datas[i], volume, n_samples,
-				GET_OUT_PORT(this, i + 1));
-	}
-
-	for (i = 0; i < n_dst_datas; i++) {
-		uint32_t dst_remap = this->dst_remap[i];
-		uint32_t src_remap = this->src_remap[i];
-		struct spa_data *dd = dbuf->buf->datas;
-
-		if (this->is_passthrough)
-			dd[i].data = (void *)src_datas[src_remap];
-		else
-			dst_datas[dst_remap] = dd[i].data = dbuf->datas[i];
-
-		dd[i].chunk->offset = 0;
-		dd[i].chunk->size = n_samples * outport->stride;
-	}
-
-	spa_log_trace_fp(this->log, "%p: n_src:%d n_dst:%d n_samples:%d max:%d p:%d", this,
-			n_src_datas, n_dst_datas, n_samples, maxsize, this->is_passthrough);
-
-	if (!this->is_passthrough)
-		convert_process(&this->conv, dst_datas, src_datas, n_samples);
-
-	return SPA_STATUS_NEED_DATA | SPA_STATUS_HAVE_DATA;
-}
-
-static const struct spa_node_methods impl_node = {
-	SPA_VERSION_NODE_METHODS,
-	.add_listener = impl_node_add_listener,
-	.set_callbacks = impl_node_set_callbacks,
-	.enum_params = impl_node_enum_params,
-	.set_param = impl_node_set_param,
-	.set_io = impl_node_set_io,
-	.send_command = impl_node_send_command,
-	.add_port = impl_node_add_port,
-	.remove_port = impl_node_remove_port,
-	.port_enum_params = impl_node_port_enum_params,
-	.port_set_param = impl_node_port_set_param,
-	.port_use_buffers = impl_node_port_use_buffers,
-	.port_set_io = impl_node_port_set_io,
-	.port_reuse_buffer = impl_node_port_reuse_buffer,
-	.process = impl_node_process,
-};
-
-static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface)
-{
-	struct impl *this;
-
-	spa_return_val_if_fail(handle != NULL, -EINVAL);
-	spa_return_val_if_fail(interface != NULL, -EINVAL);
-
-	this = (struct impl *) handle;
-
-	if (spa_streq(type, SPA_TYPE_INTERFACE_Node))
-		*interface = &this->node;
-	else
-		return -ENOENT;
-
-	return 0;
-}
-
-static int impl_clear(struct spa_handle *handle)
-{
-	struct impl *this;
-	uint32_t i;
-
-	spa_return_val_if_fail(handle != NULL, -EINVAL);
-
-	this = (struct impl *) handle;
-
-	for (i = 0; i < MAX_PORTS; i++)
-		free(this->in_ports[i]);
-	for (i = 0; i < MAX_PORTS+1; i++)
-		free(this->out_ports[i]);
-	free(this->empty);
-	return 0;
-}
-
-static size_t
-impl_get_size(const struct spa_handle_factory *factory,
-	      const struct spa_dict *params)
-{
-	return sizeof(struct impl);
-}
-
-static int
-impl_init(const struct spa_handle_factory *factory,
-	  struct spa_handle *handle,
-	  const struct spa_dict *info,
-	  const struct spa_support *support,
-	  uint32_t n_support)
-{
-	struct impl *this;
-	uint32_t i;
-
-	spa_return_val_if_fail(factory != NULL, -EINVAL);
-	spa_return_val_if_fail(handle != NULL, -EINVAL);
-
-	handle->get_interface = impl_get_interface;
-	handle->clear = impl_clear;
-
-	this = (struct impl *) handle;
-
-	this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
-	spa_log_topic_init(this->log, log_topic);
-
-	this->cpu = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_CPU);
-	if (this->cpu) {
-		this->cpu_flags = spa_cpu_get_flags(this->cpu);
-		this->max_align = SPA_MIN(MAX_ALIGN, spa_cpu_get_max_align(this->cpu));
-	}
-
-	for (i = 0; info && i < info->n_items; i++) {
-		const char *k = info->items[i].key;
-		const char *s = info->items[i].value;
-		if (spa_streq(k, "clock.quantum-limit"))
-			spa_atou32(s, &this->quantum_limit, 0);
-		else
-			merger_set_param(this, k, s);
-	}
-
-	this->latency[SPA_DIRECTION_INPUT] = SPA_LATENCY_INFO(SPA_DIRECTION_INPUT);
-	this->latency[SPA_DIRECTION_OUTPUT] = SPA_LATENCY_INFO(SPA_DIRECTION_OUTPUT);
-
-	this->node.iface = SPA_INTERFACE_INIT(
-			SPA_TYPE_INTERFACE_Node,
-			SPA_VERSION_NODE,
-			&impl_node, this);
-	spa_hook_list_init(&this->hooks);
-
-	this->info_all = SPA_NODE_CHANGE_MASK_FLAGS |
-			SPA_NODE_CHANGE_MASK_PARAMS;
-	this->info = SPA_NODE_INFO_INIT();
-	this->info.max_input_ports = MAX_PORTS;
-	this->info.max_output_ports = MAX_PORTS+1;
-	this->info.flags = SPA_NODE_FLAG_RT |
-		SPA_NODE_FLAG_IN_PORT_CONFIG;
-	this->params[IDX_PortConfig] = SPA_PARAM_INFO(SPA_PARAM_PortConfig, SPA_PARAM_INFO_WRITE);
-	this->params[IDX_PropInfo] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ);
-	this->params[IDX_Props] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE);
-	this->info.params = this->params;
-	this->info.n_params = N_NODE_PARAMS;
-
-	init_port(this, SPA_DIRECTION_OUTPUT, 0, 0);
-
-	this->volume.cpu_flags = this->cpu_flags;
-	volume_init(&this->volume);
-	props_reset(&this->props);
-
-	return 0;
-}
-
-static const struct spa_interface_info impl_interfaces[] = {
-	{SPA_TYPE_INTERFACE_Node,},
-};
-
-static int
-impl_enum_interface_info(const struct spa_handle_factory *factory,
-			 const struct spa_interface_info **info,
-			 uint32_t *index)
-{
-	spa_return_val_if_fail(factory != NULL, -EINVAL);
-	spa_return_val_if_fail(info != NULL, -EINVAL);
-	spa_return_val_if_fail(index != NULL, -EINVAL);
-
-	switch (*index) {
-	case 0:
-		*info = &impl_interfaces[*index];
-		break;
-	default:
-		return 0;
-	}
-	(*index)++;
-	return 1;
-}
-
-const struct spa_handle_factory spa_merger_factory = {
-	SPA_VERSION_HANDLE_FACTORY,
-	SPA_NAME_AUDIO_PROCESS_INTERLEAVE,
-	NULL,
-	impl_get_size,
-	impl_init,
-	impl_enum_interface_info,
-};
diff --git a/spa/plugins/audioconvert/meson.build b/spa/plugins/audioconvert/meson.build
index 89af1267133b4a34a94a29fe58ac3aaf8a11518a..d84699242dcff1f665c3c0423c27871f34c16cca 100644
--- a/spa/plugins/audioconvert/meson.build
+++ b/spa/plugins/audioconvert/meson.build
@@ -1,15 +1,26 @@
-audioconvert_sources = ['audioadapter.c',
+audioconvert_sources = [
+  'audioadapter.c',
   'audioconvert.c',
-  'fmtconvert.c',
-  'channelmix.c',
-  'merger.c',
-  'plugin.c',
-  'resample.c',
-  'splitter.c']
+  'plugin.c'
+]
 
 simd_cargs = []
 simd_dependencies = []
 
+audioconvert_c = static_library('audioconvert_c',
+  [ 'channelmix-ops-c.c',
+    'biquad.c',
+    'crossover.c',
+    'volume-ops-c.c',
+    'resample-native-c.c',
+    'resample-peaks-c.c',
+    'fmt-ops-c.c' ],
+  c_args : ['-Ofast', '-ffast-math'],
+  dependencies : [ spa_dep ],
+  install : false
+  )
+simd_dependencies += audioconvert_c
+
 if have_sse
   audioconvert_sse = static_library('audioconvert_sse',
     ['resample-native-sse.c',
@@ -89,15 +100,10 @@ endif
 
 audioconvert_lib = static_library('audioconvert',
   ['fmt-ops.c',
-    'biquad.c',
-    'crossover.c',
     'channelmix-ops.c',
-    'channelmix-ops-c.c',
     'resample-native.c',
     'resample-peaks.c',
-    'fmt-ops-c.c',
-    'volume-ops.c',
-    'volume-ops-c.c' ],
+    'volume-ops.c' ],
   c_args : [ simd_cargs, '-O3'],
   link_with : simd_dependencies,
   include_directories : [configinc],
diff --git a/spa/plugins/audioconvert/plugin.c b/spa/plugins/audioconvert/plugin.c
index fba14c2059549fe0be1e74b768bd21b8bead1b36..03c206f89ccfb13a86c8e4ac8e147ad5900bbd0e 100644
--- a/spa/plugins/audioconvert/plugin.c
+++ b/spa/plugins/audioconvert/plugin.c
@@ -27,11 +27,6 @@
 #include <spa/support/plugin.h>
 
 extern const struct spa_handle_factory spa_audioconvert_factory;
-extern const struct spa_handle_factory spa_fmtconvert_factory;
-extern const struct spa_handle_factory spa_channelmix_factory;
-extern const struct spa_handle_factory spa_resample_factory;
-extern const struct spa_handle_factory spa_splitter_factory;
-extern const struct spa_handle_factory spa_merger_factory;
 extern const struct spa_handle_factory spa_audioadapter_factory;
 
 SPA_EXPORT
@@ -45,21 +40,6 @@ int spa_handle_factory_enum(const struct spa_handle_factory **factory, uint32_t
 		*factory = &spa_audioconvert_factory;
 		break;
 	case 1:
-		*factory = &spa_fmtconvert_factory;
-		break;
-	case 2:
-		*factory = &spa_channelmix_factory;
-		break;
-	case 3:
-		*factory = &spa_resample_factory;
-		break;
-	case 4:
-		*factory = &spa_splitter_factory;
-		break;
-	case 5:
-		*factory = &spa_merger_factory;
-		break;
-	case 6:
 		*factory = &spa_audioadapter_factory;
 		break;
 	default:
diff --git a/spa/plugins/audioconvert/resample-native-c.c b/spa/plugins/audioconvert/resample-native-c.c
new file mode 100644
index 0000000000000000000000000000000000000000..3fe50b769d1e6a4b4adad7a38a5586e5d1522691
--- /dev/null
+++ b/spa/plugins/audioconvert/resample-native-c.c
@@ -0,0 +1,65 @@
+/* Spa
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "resample-native-impl.h"
+
+static void inner_product_c(float *d, const float * SPA_RESTRICT s,
+		const float * SPA_RESTRICT taps, uint32_t n_taps)
+{
+	float sum = 0.0f;
+#if 1
+	uint32_t i, j, nt2 = n_taps/2;
+	for (i = 0, j = n_taps-1; i < nt2; i++, j--)
+		sum += s[i] * taps[i] + s[j] * taps[j];
+#else
+	uint32_t i;
+	for (i = 0; i < n_taps; i++)
+		sum += s[i] * taps[i];
+#endif
+	*d = sum;
+}
+
+static void inner_product_ip_c(float *d, const float * SPA_RESTRICT s,
+	const float * SPA_RESTRICT t0, const float * SPA_RESTRICT t1, float x,
+	uint32_t n_taps)
+{
+	float sum[2] = { 0.0f, 0.0f };
+	uint32_t i;
+#if 1
+	uint32_t j, nt2 = n_taps/2;
+	for (i = 0, j = n_taps-1; i < nt2; i++, j--) {
+		sum[0] += s[i] * t0[i] + s[j] * t0[j];
+		sum[1] += s[i] * t1[i] + s[j] * t1[j];
+	}
+#else
+	for (i = 0; i < n_taps; i++) {
+		sum[0] += s[i] * t0[i];
+		sum[1] += s[i] * t1[i];
+	}
+#endif
+	*d = (sum[1] - sum[0]) * x + sum[0];
+}
+
+MAKE_RESAMPLER_FULL(c);
+MAKE_RESAMPLER_INTER(c);
diff --git a/spa/plugins/audioconvert/resample-native-impl.h b/spa/plugins/audioconvert/resample-native-impl.h
index beb7bd6f955cb33ce0a8d6816ec789d6b0966363..a6c87fc3640ea8c6cd0b096ff656be912d1db387 100644
--- a/spa/plugins/audioconvert/resample-native-impl.h
+++ b/spa/plugins/audioconvert/resample-native-impl.h
@@ -34,10 +34,13 @@ typedef void (*resample_func_t)(struct resample *r,
 
 struct resample_info {
 	uint32_t format;
-	uint32_t cpu_flags;
 	resample_func_t process_copy;
+	const char *copy_name;
 	resample_func_t process_full;
+	const char *full_name;
 	resample_func_t process_inter;
+	const char *inter_name;
+	uint32_t cpu_flags;
 };
 
 struct native_data {
diff --git a/spa/plugins/audioconvert/resample-native.c b/spa/plugins/audioconvert/resample-native.c
index f522d67e2cf85b607a829db749b909b76faeae58..b46a09fc1f0cf0e223b0a70083e107da4824bb4f 100644
--- a/spa/plugins/audioconvert/resample-native.c
+++ b/spa/plugins/audioconvert/resample-native.c
@@ -33,22 +33,22 @@ struct quality {
 	double cutoff;
 };
 
-static const struct quality blackman_qualities[] = {
-	{ 8, 0.5, },
-	{ 16, 0.70, },
-	{ 24, 0.76, },
-	{ 32, 0.8, },
+static const struct quality window_qualities[] = {
+	{ 8, 0.53, },
+	{ 16, 0.67, },
+	{ 24, 0.75, },
+	{ 32, 0.80, },
 	{ 48, 0.85, },                  /* default */
-	{ 64, 0.90, },
-	{ 80, 0.92, },
-	{ 96, 0.933, },
-	{ 128, 0.950, },
-	{ 144, 0.955, },
-	{ 160, 0.958, },
-	{ 192, 0.965, },
-	{ 256, 0.975, },
-	{ 896, 0.997, },
-	{ 1024, 0.998, },
+	{ 64, 0.88, },
+	{ 80, 0.895, },
+	{ 96, 0.910, },
+	{ 128, 0.936, },
+	{ 144, 0.945, },
+	{ 160, 0.950, },
+	{ 192, 0.960, },
+	{ 256, 0.970, },
+	{ 896, 0.990, },
+	{ 1024, 0.995, },
 };
 
 static inline double sinc(double x)
@@ -58,14 +58,29 @@ static inline double sinc(double x)
 	return sin(x) / x;
 }
 
-static inline double blackman(double x, double n_taps)
+static inline double window_blackman(double x, double n_taps)
 {
-	double alpha = 0.232;
+	double alpha = 0.232, r;
 	x =  2.0 * M_PI * x / n_taps;
-	return (1.0 - alpha) / 2.0 + (1.0 / 2.0) * cos(x) +
-		(alpha / 2.0) * cos(2 * x);
+	r = (1.0 - alpha) / 2.0 + (1.0 / 2.0) * cos(x) +
+		(alpha / 2.0) * cos(2.0 * x);
+	return r;
+}
+static inline double window_cosh(double x, double n_taps)
+{
+	double r;
+	double A = 16.97789;
+	double x2;
+	x =  2.0 * x / n_taps;
+	x2 = x * x;
+	if (x2 >= 1.0)
+		return 0.0;
+	/* doi:10.1109/RME.2008.4595727 with tweak */
+	r = (exp(A * sqrt(1 - x2)) - 1) / (exp(A) - 1);
+	return r;
 }
 
+#define window window_cosh
 
 static int build_filter(float *taps, uint32_t stride, uint32_t n_taps, uint32_t n_phases, double cutoff)
 {
@@ -77,74 +92,35 @@ static int build_filter(float *taps, uint32_t stride, uint32_t n_taps, uint32_t
 			/* exploit symmetry in filter taps */
 			taps[(n_phases - i) * stride + n_taps12 + j] =
 				taps[i * stride + (n_taps12 - j - 1)] =
-					cutoff * sinc(t * cutoff) * blackman(t, n_taps);
+					cutoff * sinc(t * cutoff) * window(t, n_taps);
 		}
 	}
 	return 0;
 }
 
-static void inner_product_c(float *d, const float * SPA_RESTRICT s,
-		const float * SPA_RESTRICT taps, uint32_t n_taps)
-{
-	float sum = 0.0f;
-#if 1
-	uint32_t i, j, nt2 = n_taps/2;
-	for (i = 0, j = n_taps-1; i < nt2; i++, j--)
-		sum += s[i] * taps[i] + s[j] * taps[j];
-#else
-	uint32_t i;
-	for (i = 0; i < n_taps; i++)
-		sum += s[i] * taps[i];
-#endif
-	*d = sum;
-}
-
-static void inner_product_ip_c(float *d, const float * SPA_RESTRICT s,
-	const float * SPA_RESTRICT t0, const float * SPA_RESTRICT t1, float x,
-	uint32_t n_taps)
-{
-	float sum[2] = { 0.0f, 0.0f };
-	uint32_t i;
-#if 1
-	uint32_t j, nt2 = n_taps/2;
-	for (i = 0, j = n_taps-1; i < nt2; i++, j--) {
-		sum[0] += s[i] * t0[i] + s[j] * t0[j];
-		sum[1] += s[i] * t1[i] + s[j] * t1[j];
-	}
-#else
-	for (i = 0; i < n_taps; i++) {
-		sum[0] += s[i] * t0[i];
-		sum[1] += s[i] * t1[i];
-	}
-#endif
-	*d = (sum[1] - sum[0]) * x + sum[0];
-}
-
 MAKE_RESAMPLER_COPY(c);
-MAKE_RESAMPLER_FULL(c);
-MAKE_RESAMPLER_INTER(c);
+
+#define MAKE(fmt,copy,full,inter,...) \
+	{ SPA_AUDIO_FORMAT_ ##fmt, do_resample_ ##copy, #copy, \
+		do_resample_ ##full, #full, do_resample_ ##inter, #inter, __VA_ARGS__ }
 
 static struct resample_info resample_table[] =
 {
 #if defined (HAVE_NEON)
-	{ SPA_AUDIO_FORMAT_F32, SPA_CPU_FLAG_NEON,
-		do_resample_copy_c, do_resample_full_neon, do_resample_inter_neon },
+	MAKE(F32, copy_c, full_neon, inter_neon, SPA_CPU_FLAG_NEON),
 #endif
 #if defined(HAVE_AVX) && defined(HAVE_FMA)
-	{ SPA_AUDIO_FORMAT_F32, SPA_CPU_FLAG_AVX | SPA_CPU_FLAG_FMA3,
-		do_resample_copy_c, do_resample_full_avx, do_resample_inter_avx },
+	MAKE(F32, copy_c, full_avx, inter_avx, SPA_CPU_FLAG_AVX | SPA_CPU_FLAG_FMA3),
 #endif
 #if defined (HAVE_SSSE3)
-	{ SPA_AUDIO_FORMAT_F32, SPA_CPU_FLAG_SSSE3 | SPA_CPU_FLAG_SLOW_UNALIGNED,
-		do_resample_copy_c, do_resample_full_ssse3, do_resample_inter_ssse3 },
+	MAKE(F32, copy_c, full_ssse3, inter_ssse3, SPA_CPU_FLAG_SSSE3 | SPA_CPU_FLAG_SLOW_UNALIGNED),
 #endif
 #if defined (HAVE_SSE)
-	{ SPA_AUDIO_FORMAT_F32, SPA_CPU_FLAG_SSE,
-		do_resample_copy_c, do_resample_full_sse, do_resample_inter_sse },
+	MAKE(F32, copy_c, full_sse, inter_sse, SPA_CPU_FLAG_SSE),
 #endif
-	{ SPA_AUDIO_FORMAT_F32, 0,
-		do_resample_copy_c, do_resample_full_c, do_resample_inter_c },
+	MAKE(F32, copy_c, full_c, inter_c),
 };
+#undef MAKE
 
 #define MATCH_CPU_FLAGS(a,b)	((a) == 0 || ((a) & (b)) == a)
 static const struct resample_info *find_resample_info(uint32_t format, uint32_t cpu_flags)
@@ -200,12 +176,18 @@ static void impl_native_update_rate(struct resample *r, double rate)
 	data->inc = data->in_rate / data->out_rate;
 	data->frac = data->in_rate % data->out_rate;
 
-	if (data->in_rate == data->out_rate)
+	if (data->in_rate == data->out_rate) {
 		data->func = data->info->process_copy;
-	else if (rate == 1.0)
+		r->func_name = data->info->copy_name;
+	}
+	else if (rate == 1.0) {
 		data->func = data->info->process_full;
-	else
+		r->func_name = data->info->full_name;
+	}
+	else {
 		data->func = data->info->process_inter;
+		r->func_name = data->info->inter_name;
+	}
 
 	spa_log_trace_fp(r->log, "native %p: rate:%f in:%d out:%d phase:%d inc:%d frac:%d", r,
 			rate, data->in_rate, data->out_rate, data->phase, data->inc, data->frac);
@@ -340,7 +322,7 @@ int resample_native_init(struct resample *r)
 	uint32_t c, n_taps, n_phases, filter_size, in_rate, out_rate, gcd, filter_stride;
 	uint32_t history_stride, history_size, oversample;
 
-	r->quality = SPA_CLAMP(r->quality, 0, (int) SPA_N_ELEMENTS(blackman_qualities) - 1);
+	r->quality = SPA_CLAMP(r->quality, 0, (int) SPA_N_ELEMENTS(window_qualities) - 1);
 	r->free = impl_native_free;
 	r->update_rate = impl_native_update_rate;
 	r->in_len = impl_native_in_len;
@@ -348,14 +330,15 @@ int resample_native_init(struct resample *r)
 	r->reset = impl_native_reset;
 	r->delay = impl_native_delay;
 
-	q = &blackman_qualities[r->quality];
+	q = &window_qualities[r->quality];
 
 	gcd = calc_gcd(r->i_rate, r->o_rate);
 
 	in_rate = r->i_rate / gcd;
 	out_rate = r->o_rate / gcd;
 
-	scale = SPA_MIN(q->cutoff * out_rate / in_rate, 1.0);
+	scale = SPA_MIN(q->cutoff * out_rate / in_rate, q->cutoff);
+
 	/* multiple of 8 taps to ease simd optimizations */
 	n_taps = SPA_ROUND_UP_N((uint32_t)ceil(q->n_taps / scale), 8);
 	n_taps = SPA_MIN(n_taps, 1u << 18);
@@ -396,10 +379,9 @@ int resample_native_init(struct resample *r)
 	build_filter(d->filter, d->filter_stride, n_taps, n_phases, scale);
 
 	d->info = find_resample_info(SPA_AUDIO_FORMAT_F32, r->cpu_flags);
-	if (SPA_UNLIKELY(!d->info))
-	{
+	if (SPA_UNLIKELY(d->info == NULL)) {
 	    spa_log_error(r->log, "failed to find suitable resample format!");
-	    return -1;
+	    return -ENOTSUP;
 	}
 
 	spa_log_debug(r->log, "native %p: q:%d in:%d out:%d n_taps:%d n_phases:%d features:%08x:%08x",
diff --git a/spa/plugins/audioconvert/resample-peaks-c.c b/spa/plugins/audioconvert/resample-peaks-c.c
new file mode 100644
index 0000000000000000000000000000000000000000..3d27016e573e84978131751c869fd83680edcbbf
--- /dev/null
+++ b/spa/plugins/audioconvert/resample-peaks-c.c
@@ -0,0 +1,73 @@
+/* Spa
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <math.h>
+
+#include "resample-peaks-impl.h"
+
+void resample_peaks_process_c(struct resample *r,
+			const void * SPA_RESTRICT src[], uint32_t *in_len,
+			void * SPA_RESTRICT dst[], uint32_t *out_len)
+{
+	struct peaks_data *pd = r->data;
+	uint32_t c, i, o, end, chunk, o_count, i_count;
+
+	if (SPA_UNLIKELY(r->channels == 0))
+		return;
+
+	for (c = 0; c < r->channels; c++) {
+		const float *s = src[c];
+		float *d = dst[c], m = pd->max_f[c];
+
+		o_count = pd->o_count;
+		i_count = pd->i_count;
+		o = i = 0;
+
+		while (i < *in_len && o < *out_len) {
+			end = ((uint64_t) (o_count + 1) * r->i_rate) / r->o_rate;
+			end = end > i_count ? end - i_count : 0;
+			chunk = SPA_MIN(end, *in_len);
+
+			for (; i < chunk; i++)
+				m = SPA_MAX(fabsf(s[i]), m);
+
+			if (i == end) {
+				d[o++] = m;
+				m = 0.0f;
+				o_count++;
+			}
+		}
+		pd->max_f[c] = m;
+	}
+
+	*out_len = o;
+	*in_len = i;
+	pd->o_count = o_count;
+	pd->i_count = i_count + i;
+
+	while (pd->i_count >= r->i_rate) {
+		pd->i_count -= r->i_rate;
+		pd->o_count -= r->o_rate;
+	}
+}
diff --git a/spa/plugins/audioconvert/resample-peaks-impl.h b/spa/plugins/audioconvert/resample-peaks-impl.h
index d8b28fb2bab9dc9628f08b831620543845a3fc30..7a39af078a4ed6d433ae8d1114d53a3f53ccc47e 100644
--- a/spa/plugins/audioconvert/resample-peaks-impl.h
+++ b/spa/plugins/audioconvert/resample-peaks-impl.h
@@ -34,6 +34,9 @@ struct peaks_data {
 	float max_f[];
 };
 
+void resample_peaks_process_c(struct resample *r,
+	const void * SPA_RESTRICT src[], uint32_t *in_len,
+	void * SPA_RESTRICT dst[], uint32_t *out_len);
 #if defined (HAVE_SSE)
 void resample_peaks_process_sse(struct resample *r,
 	const void * SPA_RESTRICT src[], uint32_t *in_len,
diff --git a/spa/plugins/audioconvert/resample-peaks.c b/spa/plugins/audioconvert/resample-peaks.c
index 9e4c4223dc1546a61119a1cd6bf9c3492f04c93e..2fa52e875e4a65a55b89842f16759a15fb2a92ff 100644
--- a/spa/plugins/audioconvert/resample-peaks.c
+++ b/spa/plugins/audioconvert/resample-peaks.c
@@ -29,52 +29,6 @@
 
 #include "resample-peaks-impl.h"
 
-static void resample_peaks_process_c(struct resample *r,
-			const void * SPA_RESTRICT src[], uint32_t *in_len,
-			void * SPA_RESTRICT dst[], uint32_t *out_len)
-{
-	struct peaks_data *pd = r->data;
-	uint32_t c, i, o, end, chunk, o_count, i_count;
-
-	if (SPA_UNLIKELY(r->channels == 0))
-		return;
-
-	for (c = 0; c < r->channels; c++) {
-		const float *s = src[c];
-		float *d = dst[c], m = pd->max_f[c];
-
-		o_count = pd->o_count;
-		i_count = pd->i_count;
-		o = i = 0;
-
-		while (i < *in_len && o < *out_len) {
-			end = ((uint64_t) (o_count + 1) * r->i_rate) / r->o_rate;
-			end = end > i_count ? end - i_count : 0;
-			chunk = SPA_MIN(end, *in_len);
-
-			for (; i < chunk; i++)
-				m = SPA_MAX(fabsf(s[i]), m);
-
-			if (i == end) {
-				d[o++] = m;
-				m = 0.0f;
-				o_count++;
-			}
-		}
-		pd->max_f[c] = m;
-	}
-
-	*out_len = o;
-	*in_len = i;
-	pd->o_count = o_count;
-	pd->i_count = i_count + i;
-
-	while (pd->i_count >= r->i_rate) {
-		pd->i_count -= r->i_rate;
-		pd->o_count -= r->o_rate;
-	}
-}
-
 struct resample_info {
 	uint32_t format;
 	uint32_t cpu_flags;
diff --git a/spa/plugins/audioconvert/resample.c b/spa/plugins/audioconvert/resample.c
deleted file mode 100644
index 91dabb6cc36b89d10aaee24af75bd636b9e47605..0000000000000000000000000000000000000000
--- a/spa/plugins/audioconvert/resample.c
+++ /dev/null
@@ -1,1307 +0,0 @@
-/* Spa
- *
- * Copyright © 2018 Wim Taymans
- *
- * Permission is hereby granted, free of charge, to any person obtaining a
- * copy of this software and associated documentation files (the "Software"),
- * to deal in the Software without restriction, including without limitation
- * the rights to use, copy, modify, merge, publish, distribute, sublicense,
- * and/or sell copies of the Software, and to permit persons to whom the
- * Software is furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice (including the next
- * paragraph) shall be included in all copies or substantial portions of the
- * Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
- * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
- * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
- * DEALINGS IN THE SOFTWARE.
- */
-
-#include <errno.h>
-#include <string.h>
-#include <stdio.h>
-
-#include <spa/support/plugin.h>
-#include <spa/support/log.h>
-#include <spa/utils/list.h>
-#include <spa/utils/names.h>
-#include <spa/utils/string.h>
-#include <spa/node/node.h>
-#include <spa/node/io.h>
-#include <spa/node/utils.h>
-#include <spa/param/audio/format-utils.h>
-#include <spa/param/param.h>
-#include <spa/pod/filter.h>
-#include <spa/debug/types.h>
-
-#include "resample.h"
-
-#undef SPA_LOG_TOPIC_DEFAULT
-#define SPA_LOG_TOPIC_DEFAULT log_topic
-static struct spa_log_topic *log_topic = &SPA_LOG_TOPIC(0, "spa.resample");
-
-#define DEFAULT_RATE		48000
-#define DEFAULT_CHANNELS	2
-
-#define MAX_BUFFERS	32
-
-struct impl;
-
-struct props {
-	double rate;
-	int quality;
-	bool disabled;
-};
-
-static void props_reset(struct props *props)
-{
-	props->rate = 1.0;
-	props->quality = RESAMPLE_DEFAULT_QUALITY;
-	props->disabled = false;
-}
-
-struct buffer {
-	uint32_t id;
-#define BUFFER_FLAG_OUT		(1 << 0)
-	uint32_t flags;
-	struct spa_list link;
-	struct spa_buffer *outbuf;
-	struct spa_meta_header *h;
-};
-
-struct port {
-	uint32_t direction;
-	uint32_t id;
-
-	uint64_t info_all;
-	struct spa_port_info info;
-	struct spa_param_info params[8];
-
-	struct spa_io_buffers *io;
-
-	struct spa_audio_info format;
-	uint32_t stride;
-	uint32_t blocks;
-	uint32_t size;
-	unsigned int have_format:1;
-
-	struct buffer buffers[MAX_BUFFERS];
-	uint32_t n_buffers;
-
-	uint32_t offset;
-	struct spa_list queue;
-};
-
-struct impl {
-	struct spa_handle handle;
-	struct spa_node node;
-
-	struct spa_log *log;
-	struct spa_cpu *cpu;
-
-	uint32_t quantum_limit;
-
-	struct spa_io_position *io_position;
-	struct spa_io_rate_match *io_rate_match;
-
-	uint64_t info_all;
-	struct spa_node_info info;
-	struct props props;
-
-	struct spa_hook_list hooks;
-
-	struct port in_port;
-	struct port out_port;
-
-#define MODE_SPLIT	0
-#define MODE_MERGE	1
-#define MODE_CONVERT	2
-	int mode;
-	unsigned int started:1;
-	unsigned int peaks:1;
-	unsigned int drained:1;
-
-	struct resample resample;
-
-	double rate_scale;
-};
-
-#define CHECK_PORT(this,d,id)		(id == 0)
-#define GET_IN_PORT(this,id)		(&this->in_port)
-#define GET_OUT_PORT(this,id)		(&this->out_port)
-#define GET_PORT(this,d,id)		(d == SPA_DIRECTION_INPUT ? GET_IN_PORT(this,id) : GET_OUT_PORT(this,id))
-
-static int setup_convert(struct impl *this,
-		enum spa_direction direction,
-		const struct spa_audio_info *info)
-{
-	const struct spa_audio_info *src_info, *dst_info;
-	int err;
-
-	if (direction == SPA_DIRECTION_INPUT) {
-		src_info = info;
-		dst_info = &GET_OUT_PORT(this, 0)->format;
-	} else {
-		src_info = &GET_IN_PORT(this, 0)->format;
-		dst_info = info;
-	}
-
-	spa_log_info(this->log, "%p: %s/%d@%d->%s/%d@%d", this,
-			spa_debug_type_find_name(spa_type_audio_format, src_info->info.raw.format),
-			src_info->info.raw.channels,
-			src_info->info.raw.rate,
-			spa_debug_type_find_name(spa_type_audio_format, dst_info->info.raw.format),
-			dst_info->info.raw.channels,
-			dst_info->info.raw.rate);
-
-	if (src_info->info.raw.channels != dst_info->info.raw.channels)
-		return -EINVAL;
-
-	if (this->resample.free)
-		resample_free(&this->resample);
-
-	this->resample.channels = src_info->info.raw.channels;
-	this->resample.i_rate = src_info->info.raw.rate;
-	this->resample.o_rate = dst_info->info.raw.rate;
-	this->resample.log = this->log;
-	this->resample.quality = this->props.quality;
-
-	if (this->peaks)
-		err = resample_peaks_init(&this->resample);
-	else
-		err = resample_native_init(&this->resample);
-
-	return err;
-}
-
-static int impl_node_enum_params(void *object, int seq,
-				 uint32_t id, uint32_t start, uint32_t num,
-				 const struct spa_pod *filter)
-{
-	struct impl *this = object;
-	struct spa_pod *param;
-	struct spa_pod_builder b = { 0 };
-	uint8_t buffer[4096];
-	struct spa_result_node_params result;
-	uint32_t count = 0;
-
-	spa_return_val_if_fail(this != NULL, -EINVAL);
-	spa_return_val_if_fail(num != 0, -EINVAL);
-
-	result.id = id;
-	result.next = start;
-      next:
-	result.index = result.next++;
-
-	spa_pod_builder_init(&b, buffer, sizeof(buffer));
-
-	switch (id) {
-	case SPA_PARAM_PropInfo:
-	{
-		struct props *p = &this->props;
-
-		switch (result.index) {
-		case 0:
-			param = spa_pod_builder_add_object(&b,
-				SPA_TYPE_OBJECT_PropInfo, id,
-				SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_rate),
-				SPA_PROP_INFO_description, SPA_POD_String("Rate scaler"),
-				SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Double(p->rate, 0.0, 10.0));
-			break;
-		case 1:
-			param = spa_pod_builder_add_object(&b,
-				SPA_TYPE_OBJECT_PropInfo, id,
-				SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_quality),
-				SPA_PROP_INFO_name, SPA_POD_String("resample.quality"),
-				SPA_PROP_INFO_description, SPA_POD_String("Resample Quality"),
-				SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Int(p->quality, 0, 14),
-				SPA_PROP_INFO_params, SPA_POD_Bool(true));
-			break;
-		case 2:
-			param = spa_pod_builder_add_object(&b,
-				SPA_TYPE_OBJECT_PropInfo, id,
-				SPA_PROP_INFO_name, SPA_POD_String("resample.disable"),
-				SPA_PROP_INFO_description, SPA_POD_String("Disable Resampling"),
-				SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(p->disabled),
-				SPA_PROP_INFO_params, SPA_POD_Bool(true));
-			break;
-		default:
-			return 0;
-		}
-		break;
-	}
-	case SPA_PARAM_Props:
-	{
-		struct props *p = &this->props;
-		struct spa_pod_frame f[2];
-
-		switch (result.index) {
-		case 0:
-			spa_pod_builder_push_object(&b, &f[0], SPA_TYPE_OBJECT_Props, id);
-			spa_pod_builder_add(&b,
-				SPA_PROP_rate,			SPA_POD_Double(p->rate),
-				SPA_PROP_quality,		SPA_POD_Int(p->quality),
-				0);
-			spa_pod_builder_prop(&b, SPA_PROP_params, 0);
-			spa_pod_builder_push_struct(&b, &f[1]);
-			spa_pod_builder_string(&b, "resample.quality");
-			spa_pod_builder_int(&b, p->quality);
-			spa_pod_builder_string(&b, "resample.disable");
-			spa_pod_builder_bool(&b, p->disabled);
-			spa_pod_builder_pop(&b, &f[1]);
-			param = spa_pod_builder_pop(&b, &f[0]);
-			break;
-		default:
-			return 0;
-		}
-		break;
-	}
-	default:
-		return -ENOENT;
-	}
-
-	if (spa_pod_filter(&b, &result.param, param, filter) < 0)
-		goto next;
-
-	spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
-
-	if (++count != num)
-		goto next;
-
-	return 0;
-}
-
-static int resample_set_param(struct impl *this, const char *k, const char *s)
-{
-	if (spa_streq(k, "resample.quality"))
-		this->props.quality = atoi(s);
-	else if (spa_streq(k, "resample.disable"))
-		this->props.disabled = spa_atob(s);
-	return 0;
-}
-
-static int parse_prop_params(struct impl *this, struct spa_pod *params)
-{
-	struct spa_pod_parser prs;
-	struct spa_pod_frame f;
-
-	spa_pod_parser_pod(&prs, params);
-	if (spa_pod_parser_push_struct(&prs, &f) < 0)
-		return 0;
-
-	while (true) {
-		const char *name;
-		struct spa_pod *pod;
-		char value[512];
-
-		if (spa_pod_parser_get_string(&prs, &name) < 0)
-			break;
-
-		if (spa_pod_parser_get_pod(&prs, &pod) < 0)
-			break;
-
-		if (spa_pod_is_string(pod)) {
-			spa_pod_copy_string(pod, sizeof(value), value);
-		} else if (spa_pod_is_int(pod)) {
-			snprintf(value, sizeof(value), "%d",
-					SPA_POD_VALUE(struct spa_pod_int, pod));
-		} else if (spa_pod_is_bool(pod)) {
-			snprintf(value, sizeof(value), "%s",
-					SPA_POD_VALUE(struct spa_pod_bool, pod) ?
-					"true" : "false");
-		} else
-			continue;
-
-		spa_log_info(this->log, "key:'%s' val:'%s'", name, value);
-		resample_set_param(this, name, value);
-	}
-	return 0;
-}
-
-static int apply_props(struct impl *this, const struct spa_pod *param)
-{
-	struct spa_pod_prop *prop;
-	struct spa_pod_object *obj = (struct spa_pod_object *) param;
-	struct props *p = &this->props;
-	int changed = 0;
-
-	SPA_POD_OBJECT_FOREACH(obj, prop) {
-		switch (prop->key) {
-		case SPA_PROP_rate:
-			if (spa_pod_get_double(&prop->value, &p->rate) == 0) {
-				resample_update_rate(&this->resample, p->rate);
-				changed++;
-			}
-			break;
-		case SPA_PROP_quality:
-			if (spa_pod_get_int(&prop->value, &p->quality) == 0)
-				changed++;
-			break;
-		case SPA_PROP_params:
-			changed += parse_prop_params(this, &prop->value);
-			break;
-		default:
-			break;
-		}
-	}
-	return changed;
-}
-
-static int impl_node_set_param(void *object, uint32_t id, uint32_t flags,
-			       const struct spa_pod *param)
-{
-	struct impl *this = object;
-	int res = 0;
-
-	spa_return_val_if_fail(this != NULL, -EINVAL);
-
-	switch (id) {
-	case SPA_PARAM_Props:
-		apply_props(this, param);
-		break;
-	default:
-		return -ENOTSUP;
-	}
-
-	return res;
-}
-
-static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size)
-{
-	struct impl *this = object;
-
-	spa_return_val_if_fail(this != NULL, -EINVAL);
-
-	spa_log_debug(this->log, "%p: io %d %p/%zd", this, id, data, size);
-
-	switch (id) {
-	case SPA_IO_Position:
-		this->io_position = data;
-		break;
-	default:
-		return -ENOENT;
-	}
-	return 0;
-}
-
-static void update_rate_match(struct impl *this, bool passthrough, uint32_t out_size, uint32_t in_queued)
-{
-	double r = this->rate_scale / this->props.rate;
-
-	if (this->io_rate_match) {
-		uint32_t delay, match_size;
-
-		if (passthrough) {
-			delay = in_queued;
-			match_size = out_size;
-		} else {
-			if (SPA_FLAG_IS_SET(this->io_rate_match->flags, SPA_IO_RATE_MATCH_FLAG_ACTIVE))
-				resample_update_rate(&this->resample, r * this->io_rate_match->rate);
-			else
-				resample_update_rate(&this->resample, r);
-
-			delay = resample_delay(&this->resample) + in_queued;
-			match_size = resample_in_len(&this->resample, out_size);
-		}
-		match_size -= SPA_MIN(match_size, in_queued);
-		this->io_rate_match->size = match_size;
-		this->io_rate_match->delay = delay;
-		spa_log_trace_fp(this->log, "%p: next match:%u queued:%u delay:%u", this, match_size,
-				in_queued, delay);
-	} else {
-		resample_update_rate(&this->resample, r);
-	}
-}
-static inline bool is_passthrough(struct impl *this)
-{
-	return this->resample.i_rate == this->resample.o_rate &&
-		this->rate_scale == 1.0 && this->props.rate == 1.0 &&
-		(this->io_rate_match == NULL || this->props.disabled ||
-		 !SPA_FLAG_IS_SET(this->io_rate_match->flags, SPA_IO_RATE_MATCH_FLAG_ACTIVE));
-}
-
-static void recalc_rate_match(struct impl *this)
-{
-	bool passthrough = is_passthrough(this);
-	uint32_t out_size = this->io_position ? this->io_position->clock.duration : 1024;
-	update_rate_match(this, passthrough, out_size, 0);
-}
-
-static void reset_node(struct impl *this)
-{
-	struct port *outport, *inport;
-	outport = GET_OUT_PORT(this, 0);
-	inport = GET_IN_PORT(this, 0);
-
-	if (this->resample.reset)
-		resample_reset(&this->resample);
-	outport->offset = 0;
-	inport->offset = 0;
-}
-
-static int impl_node_send_command(void *object, const struct spa_command *command)
-{
-	struct impl *this = object;
-
-	spa_return_val_if_fail(this != NULL, -EINVAL);
-	spa_return_val_if_fail(command != NULL, -EINVAL);
-
-	switch (SPA_NODE_COMMAND_ID(command)) {
-	case SPA_NODE_COMMAND_Start:
-		recalc_rate_match(this);
-		this->started = true;
-		break;
-	case SPA_NODE_COMMAND_Suspend:
-	case SPA_NODE_COMMAND_Flush:
-		reset_node(this);
-		SPA_FALLTHROUGH;
-	case SPA_NODE_COMMAND_Pause:
-		this->started = false;
-		break;
-	default:
-		return -ENOTSUP;
-	}
-	return 0;
-}
-
-static void emit_node_info(struct impl *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) {
-		spa_node_emit_info(&this->hooks, &this->info);
-		this->info.change_mask = old;
-	}
-}
-
-static void emit_port_info(struct impl *this, struct port *port, bool full)
-{
-	uint64_t old = full ? port->info.change_mask : 0;
-	if (full)
-		port->info.change_mask = port->info_all;
-	if (port->info.change_mask) {
-		spa_node_emit_port_info(&this->hooks,
-				port->direction, port->id, &port->info);
-		port->info.change_mask = old;
-	}
-}
-
-static int
-impl_node_add_listener(void *object,
-		struct spa_hook *listener,
-		const struct spa_node_events *events,
-		void *data)
-{
-	struct impl *this = object;
-	struct spa_hook_list save;
-
-	spa_return_val_if_fail(this != NULL, -EINVAL);
-
-	spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
-
-	emit_node_info(this, true);
-	emit_port_info(this, GET_IN_PORT(this, 0), true);
-	emit_port_info(this, GET_OUT_PORT(this, 0), true);
-
-	spa_hook_list_join(&this->hooks, &save);
-
-	return 0;
-}
-
-static int
-impl_node_set_callbacks(void *object,
-			const struct spa_node_callbacks *callbacks,
-			void *user_data)
-{
-	return 0;
-}
-
-static int impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id,
-		const struct spa_dict *props)
-{
-	return -ENOTSUP;
-}
-
-static int
-impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id)
-{
-	return -ENOTSUP;
-}
-
-static int port_enum_formats(void *object,
-			     enum spa_direction direction, uint32_t port_id,
-			     uint32_t index,
-			     struct spa_pod **param,
-			     struct spa_pod_builder *builder)
-{
-	struct impl *this = object;
-	struct port *other;
-	struct spa_pod_frame f;
-	uint32_t rate, min = 1, max = INT32_MAX;
-
-	other = GET_PORT(this, SPA_DIRECTION_REVERSE(direction), 0);
-
-	switch (index) {
-	case 0:
-		if (other->have_format) {
-			rate = other->format.info.raw.rate;
-			if (this->props.disabled)
-				min = max = rate;
-
-			spa_pod_builder_push_object(builder, &f,
-				SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat);
-			spa_pod_builder_add(builder,
-				SPA_FORMAT_mediaType,      SPA_POD_Id(SPA_MEDIA_TYPE_audio),
-				SPA_FORMAT_mediaSubtype,   SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw),
-				SPA_FORMAT_AUDIO_format,   SPA_POD_Id(SPA_AUDIO_FORMAT_F32P),
-				SPA_FORMAT_AUDIO_rate,     SPA_POD_CHOICE_RANGE_Int(rate, min, max),
-				SPA_FORMAT_AUDIO_channels, SPA_POD_Int(other->format.info.raw.channels),
-				0);
-			spa_pod_builder_prop(builder, SPA_FORMAT_AUDIO_position, 0);
-			spa_pod_builder_array(builder, sizeof(uint32_t), SPA_TYPE_Id,
-					other->format.info.raw.channels, other->format.info.raw.position);
-			*param = spa_pod_builder_pop(builder, &f);
-		} else {
-			rate = this->io_position ?
-				this->io_position->clock.rate.denom : DEFAULT_RATE;
-			if (this->props.disabled)
-				min = max = rate;
-
-			*param = spa_pod_builder_add_object(builder,
-				SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat,
-				SPA_FORMAT_mediaType,      SPA_POD_Id(SPA_MEDIA_TYPE_audio),
-				SPA_FORMAT_mediaSubtype,   SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw),
-				SPA_FORMAT_AUDIO_format,   SPA_POD_Id(SPA_AUDIO_FORMAT_F32P),
-				SPA_FORMAT_AUDIO_rate,     SPA_POD_CHOICE_RANGE_Int(rate, min, max),
-				SPA_FORMAT_AUDIO_channels, SPA_POD_CHOICE_RANGE_Int(DEFAULT_CHANNELS, 1, INT32_MAX));
-		}
-		break;
-	default:
-		return 0;
-	}
-	return 1;
-}
-
-static int
-impl_node_port_enum_params(void *object, int seq,
-			   enum spa_direction direction, uint32_t port_id,
-			   uint32_t id, uint32_t start, uint32_t num,
-			   const struct spa_pod *filter)
-{
-	struct impl *this = object;
-	struct port *port, *other;
-	struct spa_pod *param;
-	struct spa_pod_builder b = { 0 };
-	uint8_t buffer[1024];
-	struct spa_result_node_params result;
-	uint32_t count = 0;
-	int res;
-
-	spa_return_val_if_fail(this != NULL, -EINVAL);
-	spa_return_val_if_fail(num != 0, -EINVAL);
-
-	spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
-
-	port = GET_PORT(this, direction, port_id);
-	other = GET_PORT(this, SPA_DIRECTION_REVERSE(direction), port_id);
-
-	spa_log_debug(this->log, "%p: enum params port %d.%d %d %u",
-			this, direction, port_id, seq, id);
-
-	result.id = id;
-	result.next = start;
-      next:
-	result.index = result.next++;
-
-	spa_pod_builder_init(&b, buffer, sizeof(buffer));
-
-	switch (id) {
-	case SPA_PARAM_EnumFormat:
-		if ((res = port_enum_formats(this, direction, port_id,
-						result.index, &param, &b)) <= 0)
-			return res;
-		break;
-	case SPA_PARAM_Format:
-		if (!port->have_format)
-			return -EIO;
-		if (result.index > 0)
-			return 0;
-
-		param = spa_format_audio_raw_build(&b, id, &port->format.info.raw);
-		break;
-	case SPA_PARAM_Buffers:
-	{
-		uint32_t buffers, size;
-		uint32_t rate;
-
-		if (!port->have_format || !other->have_format)
-			return -EIO;
-		if (result.index > 0)
-			return 0;
-
-		if (direction == SPA_DIRECTION_OUTPUT) {
-			rate = (this->resample.o_rate + this->resample.i_rate - 1) / this->resample.i_rate;
-		} else {
-			rate = (this->resample.i_rate + this->resample.o_rate - 1) / this->resample.o_rate;
-		}
-		if (other->n_buffers > 0) {
-			buffers = other->n_buffers;
-			size = (other->size / other->stride) * rate;
-		} else {
-			buffers = 1;
-			size = this->quantum_limit * rate;
-		}
-		size = SPA_MAX(size, this->quantum_limit) * 2;
-
-		param = spa_pod_builder_add_object(&b,
-			SPA_TYPE_OBJECT_ParamBuffers, id,
-			SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(buffers, 1, MAX_BUFFERS),
-			SPA_PARAM_BUFFERS_blocks,  SPA_POD_Int(port->blocks),
-			SPA_PARAM_BUFFERS_size,    SPA_POD_CHOICE_RANGE_Int(
-							size * port->stride,
-							16 * port->stride,
-							INT32_MAX),
-			SPA_PARAM_BUFFERS_stride,  SPA_POD_Int(port->stride));
-		break;
-	}
-	case SPA_PARAM_Meta:
-		switch (result.index) {
-		case 0:
-			param = spa_pod_builder_add_object(&b,
-				SPA_TYPE_OBJECT_ParamMeta, id,
-				SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header),
-				SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header)));
-			break;
-		default:
-			return 0;
-		}
-		break;
-	case SPA_PARAM_IO:
-		switch (result.index) {
-		case 0:
-			param = spa_pod_builder_add_object(&b,
-				SPA_TYPE_OBJECT_ParamIO, id,
-				SPA_PARAM_IO_id,   SPA_POD_Id(SPA_IO_Buffers),
-				SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers)));
-			break;
-		default:
-			return 0;
-		}
-		break;
-	default:
-		return -ENOENT;
-	}
-
-	if (spa_pod_filter(&b, &result.param, param, filter) < 0)
-		goto next;
-
-	spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
-
-	if (++count != num)
-		goto next;
-
-	return 0;
-}
-
-static int clear_buffers(struct impl *this, struct port *port)
-{
-	if (port->n_buffers > 0) {
-		spa_log_debug(this->log, "%p: clear buffers %p", this, port);
-		port->n_buffers = 0;
-		spa_list_init(&port->queue);
-	}
-	return 0;
-}
-
-static int port_set_format(void *object,
-			   enum spa_direction direction,
-			   uint32_t port_id,
-			   uint32_t flags,
-			   const struct spa_pod *format)
-{
-	struct impl *this = object;
-	struct port *port, *other;
-	int res = 0;
-
-	port = GET_PORT(this, direction, port_id);
-	other = GET_PORT(this, SPA_DIRECTION_REVERSE(direction), port_id);
-
-	if (format == NULL) {
-		if (port->have_format) {
-			port->have_format = false;
-			clear_buffers(this, port);
-		}
-	} else {
-		struct spa_audio_info info = { 0 };
-
-		if ((res = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0)
-			return res;
-
-		if (info.media_type != SPA_MEDIA_TYPE_audio ||
-		    info.media_subtype != SPA_MEDIA_SUBTYPE_raw)
-			return -EINVAL;
-
-		if (spa_format_audio_raw_parse(format, &info.info.raw) < 0)
-			return -EINVAL;
-
-		if (info.info.raw.format != SPA_AUDIO_FORMAT_F32P)
-			return -EINVAL;
-
-		port->stride = sizeof(float);
-		port->blocks = info.info.raw.channels;
-
-		if (other->have_format) {
-			if ((res = setup_convert(this, direction, &info)) < 0)
-				return res;
-		}
-		port->format = info;
-		port->have_format = true;
-
-		spa_log_debug(this->log, "%p: set format on port %d %d", this, port_id, res);
-	}
-
-	port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
-	if (port->have_format) {
-		port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
-		port->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
-	} else {
-		port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE);
-		port->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ);
-	}
-	emit_port_info(this, port, false);
-
-	return res;
-}
-
-static int
-impl_node_port_set_param(void *object,
-			 enum spa_direction direction, uint32_t port_id,
-			 uint32_t id, uint32_t flags,
-			 const struct spa_pod *param)
-{
-	spa_return_val_if_fail(object != NULL, -EINVAL);
-
-	spa_return_val_if_fail(CHECK_PORT(object, direction, port_id), -EINVAL);
-
-	if (id == SPA_PARAM_Format) {
-		return port_set_format(object, direction, port_id, flags, param);
-	}
-	else
-		return -ENOENT;
-}
-
-static int
-impl_node_port_use_buffers(void *object,
-			   enum spa_direction direction,
-			   uint32_t port_id,
-			   uint32_t flags,
-			   struct spa_buffer **buffers,
-			   uint32_t n_buffers)
-{
-	struct impl *this = object;
-	struct port *port;
-	uint32_t i, j, size = SPA_ID_INVALID;
-
-	spa_return_val_if_fail(this != NULL, -EINVAL);
-
-	spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
-
-	port = GET_PORT(this, direction, port_id);
-
-	spa_return_val_if_fail(port->have_format, -EIO);
-
-	spa_log_debug(this->log, "%p: use buffers %d on port %d:%d", this,
-			n_buffers, direction, port_id);
-
-	clear_buffers(this, port);
-
-	for (i = 0; i < n_buffers; i++) {
-		struct buffer *b;
-		struct spa_data *d = buffers[i]->datas;
-
-		b = &port->buffers[i];
-		b->id = i;
-		b->flags = 0;
-		b->outbuf = buffers[i];
-		b->h = spa_buffer_find_meta_data(buffers[i], SPA_META_Header, sizeof(*b->h));
-
-		for (j = 0; j < buffers[i]->n_datas; j++) {
-			if (size == SPA_ID_INVALID)
-				size = d[j].maxsize;
-			else
-				if (size != d[j].maxsize) {
-					spa_log_error(this->log, "%p: invalid size %d on buffer %p", this,
-						      size, buffers[i]);
-					return -EINVAL;
-				}
-
-			if (d[j].data == NULL) {
-				spa_log_error(this->log, "%p: invalid memory on buffer %p", this,
-					      buffers[i]);
-				return -EINVAL;
-			}
-		}
-
-		if (direction == SPA_DIRECTION_OUTPUT)
-			spa_list_append(&port->queue, &b->link);
-		else
-			SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUT);
-
-		port->offset = 0;
-	}
-	port->n_buffers = n_buffers;
-	port->size = size;
-
-	return 0;
-}
-
-static int
-impl_node_port_set_io(void *object,
-		      enum spa_direction direction, uint32_t port_id,
-		      uint32_t id, void *data, size_t size)
-{
-	struct impl *this = object;
-	struct port *port;
-
-	spa_return_val_if_fail(this != NULL, -EINVAL);
-
-	spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
-
-	spa_log_trace_fp(this->log, "%p: %d:%d io %d", this, direction, port_id, id);
-
-	port = GET_PORT(this, direction, port_id);
-
-	switch (id) {
-	case SPA_IO_Buffers:
-		port->io = data;
-		break;
-	case SPA_IO_RateMatch:
-		this->io_rate_match = data;
-		break;
-	default:
-		return -ENOENT;
-	}
-	return 0;
-}
-
-static void recycle_buffer(struct impl *this, uint32_t id)
-{
-	struct port *port = GET_OUT_PORT(this, 0);
-	struct buffer *b = &port->buffers[id];
-
-	if (SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_OUT)) {
-		spa_list_append(&port->queue, &b->link);
-		SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_OUT);
-		spa_log_trace_fp(this->log, "%p: recycle buffer %d", this, id);
-	}
-}
-
-static struct buffer *peek_buffer(struct impl *this, struct port *port)
-{
-	struct buffer *b;
-
-	if (spa_list_is_empty(&port->queue))
-		return NULL;
-
-	b = spa_list_first(&port->queue, struct buffer, link);
-	return b;
-}
-
-static void dequeue_buffer(struct impl *this, struct buffer *b)
-{
-	spa_list_remove(&b->link);
-	SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUT);
-}
-
-static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id)
-{
-	struct impl *this = object;
-
-	spa_return_val_if_fail(this != NULL, -EINVAL);
-
-	spa_return_val_if_fail(CHECK_PORT(this, SPA_DIRECTION_OUTPUT, port_id), -EINVAL);
-
-	recycle_buffer(this, buffer_id);
-
-	return 0;
-}
-
-static int impl_node_process(void *object)
-{
-	struct impl *this = object;
-	struct port *outport, *inport;
-	struct spa_io_buffers *outio, *inio;
-	struct buffer *sbuf, *dbuf;
-	struct spa_buffer *sb, *db;
-	uint32_t i, size, in_len, out_len, maxsize, max;
-#ifndef FASTPATH
-	uint32_t pin_len, pout_len;
-#endif
-	int res = 0;
-	const void **src_datas;
-	void **dst_datas;
-	bool flush_out = false;
-	bool flush_in = false;
-	bool draining = false;
-	bool passthrough;
-
-	spa_return_val_if_fail(this != NULL, -EINVAL);
-
-	outport = GET_OUT_PORT(this, 0);
-	inport = GET_IN_PORT(this, 0);
-
-	outio = outport->io;
-	inio = inport->io;
-
-	spa_return_val_if_fail(outio != NULL, -EIO);
-	spa_return_val_if_fail(inio != NULL, -EIO);
-
-	spa_log_trace_fp(this->log, "%p: status %p %d %d -> %p %d %d", this,
-			inio, inio->status, inio->buffer_id,
-			outio, outio->status, outio->buffer_id);
-
-	if (SPA_UNLIKELY(outio->status == SPA_STATUS_HAVE_DATA))
-		return SPA_STATUS_HAVE_DATA;
-	/* recycle */
-	if (SPA_LIKELY(outio->buffer_id < outport->n_buffers)) {
-		recycle_buffer(this, outio->buffer_id);
-		outio->buffer_id = SPA_ID_INVALID;
-	}
-	if (SPA_UNLIKELY(inio->status != SPA_STATUS_HAVE_DATA)) {
-		if (inio->status != SPA_STATUS_DRAINED || this->drained) {
-			recalc_rate_match(this);
-			return outio->status = inio->status;
-		}
-		inio->buffer_id = 0;
-		inport->buffers[0].outbuf->datas[0].chunk->size = -1;
-	}
-
-	if (SPA_UNLIKELY(inio->buffer_id >= inport->n_buffers))
-		return inio->status = -EINVAL;
-
-	if (SPA_UNLIKELY((dbuf = peek_buffer(this, outport)) == NULL))
-		return outio->status = -EPIPE;
-
-	sbuf = &inport->buffers[inio->buffer_id];
-
-	sb = sbuf->outbuf;
-	db = dbuf->outbuf;
-
-	maxsize = db->datas[0].maxsize;
-
-	if (SPA_LIKELY(this->io_position)) {
-		double r =  this->rate_scale;
-
-		max = this->io_position->clock.duration * sizeof(float);
-		if (this->mode == MODE_SPLIT) {
-			if (this->io_position->clock.rate.denom != this->resample.o_rate)
-				r = (double) this->io_position->clock.rate.denom / this->resample.o_rate;
-			else
-				r = 1.0;
-		} else {
-			if (this->io_position->clock.rate.denom != this->resample.i_rate)
-				r = (double) this->resample.i_rate / this->io_position->clock.rate.denom;
-			else
-				r = 1.0;
-		}
-		if (this->rate_scale != r) {
-			spa_log_info(this->log, "scale %f->%f", this->rate_scale, r);
-			this->rate_scale = r;
-		}
-	}
-	else
-		max = maxsize;
-
-	switch (this->mode) {
-	case MODE_SPLIT:
-		/* in split mode we need to output exactly the size of the
-		 * duration so we don't try to flush early */
-		maxsize = SPA_MIN(maxsize, max);
-		flush_out = false;
-		break;
-	case MODE_MERGE:
-	default:
-		/* in merge mode we consume one duration of samples and
-		 * always output the resulting data */
-		flush_out = true;
-		break;
-	}
-	src_datas = alloca(sizeof(void*) * this->resample.channels);
-	dst_datas = alloca(sizeof(void*) * this->resample.channels);
-
-	if (outport->offset > maxsize)
-		outport->offset = maxsize;
-
-	size = sb->datas[0].chunk->size;
-	if (size == (uint32_t)-1) {
-		size = sb->datas[0].maxsize;
-		memset(sb->datas[0].data, 0, size);
-		for (i = 0; i < sb->n_datas; i++)
-			src_datas[i] = sb->datas[0].data;
-		inport->offset = 0;
-		flush_in = draining = true;
-	} else {
-		size = SPA_MIN(size, sb->datas[0].maxsize);
-		if (inport->offset > size)
-			inport->offset = size;
-		for (i = 0; i < sb->n_datas; i++)
-			src_datas[i] = SPA_PTROFF(sb->datas[i].data, inport->offset, void);
-	}
-	for (i = 0; i < db->n_datas; i++)
-		dst_datas[i] = SPA_PTROFF(db->datas[i].data, outport->offset, void);
-
-	in_len = (size - inport->offset) / sizeof(float);
-	out_len = (maxsize - outport->offset) / sizeof(float);
-
-#ifndef FASTPATH
-	pin_len = in_len;
-	pout_len = out_len;
-#endif
-	passthrough = is_passthrough(this);
-
-	if (passthrough) {
-		uint32_t len = SPA_MIN(in_len, out_len);
-		for (i = 0; i < sb->n_datas; i++)
-			spa_memcpy(dst_datas[i], src_datas[i], len * sizeof(float));
-		out_len = in_len = len;
-	} else {
-		resample_process(&this->resample, src_datas, &in_len, dst_datas, &out_len);
-	}
-
-#ifndef FASTPATH
-	spa_log_trace_fp(this->log, "%p: in %d/%d %zd %d out %d/%d %zd %d max:%d",
-			this, pin_len, in_len, size / sizeof(float), inport->offset,
-			pout_len, out_len, maxsize / sizeof(float), outport->offset,
-			max);
-#endif
-
-	for (i = 0; i < db->n_datas; i++) {
-		db->datas[i].chunk->size = outport->offset + (out_len * sizeof(float));
-		db->datas[i].chunk->offset = 0;
-	}
-
-	inport->offset += in_len * sizeof(float);
-	if (inport->offset >= size || flush_in) {
-		inio->status = SPA_STATUS_NEED_DATA;
-		spa_log_trace_fp(this->log, "%p: return input buffer of %zd samples",
-				this, size / sizeof(float));
-		inport->offset = 0;
-		size = 0;
-		SPA_FLAG_SET(res, inio->status);
-	}
-
-	outport->offset += out_len * sizeof(float);
-	if (outport->offset > 0 && (outport->offset >= maxsize || flush_out)) {
-		outio->status = SPA_STATUS_HAVE_DATA;
-		outio->buffer_id = dbuf->id;
-		spa_log_trace_fp(this->log, "%p: have output buffer of %zd samples",
-				this, outport->offset / sizeof(float));
-		dequeue_buffer(this, dbuf);
-		outport->offset = 0;
-		this->drained = draining;
-		SPA_FLAG_SET(res, SPA_STATUS_HAVE_DATA);
-	}
-	if (out_len == 0 && this->peaks) {
-		outio->status = SPA_STATUS_HAVE_DATA;
-		outio->buffer_id = SPA_ID_INVALID;
-		SPA_FLAG_SET(res, SPA_STATUS_HAVE_DATA);
-		spa_log_trace_fp(this->log, "%p: no output buffer", this);
-	}
-
-	update_rate_match(this, passthrough, (max - outport->offset) / sizeof(float),
-			(size - inport->offset) / sizeof(float));
-	return res;
-}
-
-static const struct spa_node_methods impl_node = {
-	SPA_VERSION_NODE_METHODS,
-	.add_listener = impl_node_add_listener,
-	.set_callbacks = impl_node_set_callbacks,
-	.enum_params = impl_node_enum_params,
-	.set_param = impl_node_set_param,
-	.set_io = impl_node_set_io,
-	.send_command = impl_node_send_command,
-	.add_port = impl_node_add_port,
-	.remove_port = impl_node_remove_port,
-	.port_enum_params = impl_node_port_enum_params,
-	.port_set_param = impl_node_port_set_param,
-	.port_use_buffers = impl_node_port_use_buffers,
-	.port_set_io = impl_node_port_set_io,
-	.port_reuse_buffer = impl_node_port_reuse_buffer,
-	.process = impl_node_process,
-};
-
-static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface)
-{
-	struct impl *this;
-
-	spa_return_val_if_fail(handle != NULL, -EINVAL);
-	spa_return_val_if_fail(interface != NULL, -EINVAL);
-
-	this = (struct impl *) handle;
-
-	if (spa_streq(type, SPA_TYPE_INTERFACE_Node))
-		*interface = &this->node;
-	else
-		return -ENOENT;
-
-	return 0;
-}
-
-static int impl_clear(struct spa_handle *handle)
-{
-	struct impl *this;
-
-	spa_return_val_if_fail(handle != NULL, -EINVAL);
-
-	this = (struct impl *) handle;
-
-	if (this->resample.free)
-		resample_free(&this->resample);
-	return 0;
-}
-
-static size_t
-impl_get_size(const struct spa_handle_factory *factory,
-	      const struct spa_dict *params)
-{
-	return sizeof(struct impl);
-}
-
-static int
-impl_init(const struct spa_handle_factory *factory,
-	  struct spa_handle *handle,
-	  const struct spa_dict *info,
-	  const struct spa_support *support,
-	  uint32_t n_support)
-{
-	struct impl *this;
-	struct port *port;
-	uint32_t i;
-
-	spa_return_val_if_fail(factory != NULL, -EINVAL);
-	spa_return_val_if_fail(handle != NULL, -EINVAL);
-
-	handle->get_interface = impl_get_interface;
-	handle->clear = impl_clear;
-
-	this = (struct impl *) handle;
-
-	this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
-	spa_log_topic_init(this->log, log_topic);
-
-	this->cpu = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_CPU);
-	if (this->cpu)
-		this->resample.cpu_flags = spa_cpu_get_flags(this->cpu);
-
-	props_reset(&this->props);
-
-	for (i = 0; info && i < info->n_items; i++) {
-		const char *k = info->items[i].key;
-		const char *s = info->items[i].value;
-		if (spa_streq(k, "clock.quantum-limit"))
-			spa_atou32(s, &this->quantum_limit, 0);
-		else if (spa_streq(k, "resample.peaks"))
-			this->peaks = spa_atob(s);
-		else if (spa_streq(k, "factory.mode")) {
-			if (spa_streq(s, "split"))
-				this->mode = MODE_SPLIT;
-			else if (spa_streq(s, "merge"))
-				this->mode = MODE_MERGE;
-			else
-				this->mode = MODE_CONVERT;
-		} else
-			resample_set_param(this, k, s);
-
-	}
-
-	spa_log_debug(this->log, "mode:%d", this->mode);
-
-	this->node.iface = SPA_INTERFACE_INIT(
-			SPA_TYPE_INTERFACE_Node,
-			SPA_VERSION_NODE,
-			&impl_node, this);
-
-	spa_hook_list_init(&this->hooks);
-
-	this->rate_scale = 1.0;
-
-	this->info = SPA_NODE_INFO_INIT();
-	this->info_all = SPA_NODE_CHANGE_MASK_FLAGS;
-	this->info.max_input_ports = 1;
-	this->info.max_output_ports = 1;
-	this->info.flags = SPA_NODE_FLAG_RT;
-
-	port = GET_OUT_PORT(this, 0);
-	port->direction = SPA_DIRECTION_OUTPUT;
-	port->id = 0;
-	port->info_all = SPA_PORT_CHANGE_MASK_FLAGS |
-		SPA_PORT_CHANGE_MASK_PARAMS;
-	port->info = SPA_PORT_INFO_INIT();
-	port->info.flags = 0;
-	port->params[0] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ);
-	port->params[1] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ);
-	port->params[2] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
-	port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
-	port->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
-	port->info.params = port->params;
-	port->info.n_params = 5;
-	spa_list_init(&port->queue);
-
-	port = GET_IN_PORT(this, 0);
-	port->direction = SPA_DIRECTION_INPUT;
-	port->id = 0;
-	port->info_all = SPA_PORT_CHANGE_MASK_FLAGS |
-		SPA_PORT_CHANGE_MASK_PARAMS;
-	port->info = SPA_PORT_INFO_INIT();
-	port->info.flags = SPA_PORT_FLAG_DYNAMIC_DATA;
-	port->params[0] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ);
-	port->params[1] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ);
-	port->params[2] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
-	port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
-	port->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
-	port->info.params = port->params;
-	port->info.n_params = 5;
-	spa_list_init(&port->queue);
-
-	return 0;
-}
-
-static const struct spa_interface_info impl_interfaces[] = {
-	{SPA_TYPE_INTERFACE_Node,},
-};
-
-static int
-impl_enum_interface_info(const struct spa_handle_factory *factory,
-			 const struct spa_interface_info **info,
-			 uint32_t *index)
-{
-	spa_return_val_if_fail(factory != NULL, -EINVAL);
-	spa_return_val_if_fail(info != NULL, -EINVAL);
-	spa_return_val_if_fail(index != NULL, -EINVAL);
-
-	switch (*index) {
-	case 0:
-		*info = &impl_interfaces[*index];
-		break;
-	default:
-		return 0;
-	}
-	(*index)++;
-	return 1;
-}
-
-const struct spa_handle_factory spa_resample_factory = {
-	SPA_VERSION_HANDLE_FACTORY,
-	SPA_NAME_AUDIO_PROCESS_RESAMPLE,
-	NULL,
-	impl_get_size,
-	impl_init,
-	impl_enum_interface_info,
-};
diff --git a/spa/plugins/audioconvert/resample.h b/spa/plugins/audioconvert/resample.h
index caf55977f88375e88322b2546fb9118a5dee4dab..0b9180ccb37cdb736d8b32a0edd81fa184d9f27e 100644
--- a/spa/plugins/audioconvert/resample.h
+++ b/spa/plugins/audioconvert/resample.h
@@ -31,11 +31,13 @@
 #define RESAMPLE_DEFAULT_QUALITY	4
 
 struct resample {
+	struct spa_log *log;
 	uint32_t cpu_flags;
+	const char *func_name;
+
 	uint32_t channels;
 	uint32_t i_rate;
 	uint32_t o_rate;
-	struct spa_log *log;
 	double rate;
 	int quality;
 
diff --git a/spa/plugins/audioconvert/spa-resample.c b/spa/plugins/audioconvert/spa-resample.c
index 8d93697c5951b162e86c6b36046fdb2274a270da..4efb718decc8a86ab90f79020f47c2a91cc50e52 100644
--- a/spa/plugins/audioconvert/spa-resample.c
+++ b/spa/plugins/audioconvert/spa-resample.c
@@ -33,6 +33,7 @@
 #include <spa/support/log-impl.h>
 #include <spa/debug/mem.h>
 #include <spa/utils/string.h>
+#include <spa/utils/result.h>
 
 #include <sndfile.h>
 
@@ -184,14 +185,14 @@ static int do_conversion(struct data *d)
 	float out[MAX_SAMPLES * channels];
 	float ibuf[MAX_SAMPLES * channels];
 	float obuf[MAX_SAMPLES * channels];
-	uint32_t in_len, out_len;
-        uint32_t pin_len, pout_len;
+	uint32_t in_len, out_len, queued;
+	uint32_t pin_len, pout_len;
 	size_t read, written;
 	const void *src[channels];
 	void *dst[channels];
 	uint32_t i;
-	int j, k, queued;
-	bool flushing = false;
+	int res, j, k;
+	uint32_t flushing = UINT32_MAX;
 
 	spa_zero(r);
 	r.cpu_flags = d->cpu_flags;
@@ -200,7 +201,10 @@ static int do_conversion(struct data *d)
 	r.i_rate = d->iinfo.samplerate;
 	r.o_rate = d->oinfo.samplerate;
 	r.quality = d->quality < 0 ? DEFAULT_QUALITY : d->quality;
-	resample_native_init(&r);
+	if ((res = resample_native_init(&r)) < 0) {
+		fprintf(stderr, "can't init converter: %s\n", spa_strerror(res));
+		return res;
+	}
 
 	for (j = 0; j < channels; j++)
 		src[j] = &in[MAX_SAMPLES * j];
@@ -210,25 +214,29 @@ static int do_conversion(struct data *d)
 	read = written = queued = 0;
 	while (true) {
 		pout_len = out_len = MAX_SAMPLES;
-                in_len = SPA_MIN(MAX_SAMPLES, resample_in_len(&r, out_len)) - queued;
+		in_len = SPA_MIN(MAX_SAMPLES, resample_in_len(&r, out_len));
+		in_len -= SPA_MIN(queued, in_len);
 
-		pin_len = in_len = sf_readf_float(d->ifile, &ibuf[queued * channels], in_len);
+		if (in_len > 0) {
+			pin_len = in_len = sf_readf_float(d->ifile, &ibuf[queued * channels], in_len);
 
-		read += pin_len;
+			read += pin_len;
 
-		if (pin_len == 0) {
-			if (flushing)
-				break;
+			if (pin_len == 0) {
+				if (flushing == 0)
+					break;
+				if (flushing == UINT32_MAX)
+					flushing = resample_delay(&r);
 
-			flushing = true;
-			pin_len = in_len = resample_delay(&r);
+				pin_len = in_len = SPA_MIN(MAX_SAMPLES, flushing);
+				flushing -= in_len;
 
-			for (k = 0, i = 0; i < pin_len; i++) {
-				for (j = 0; j < channels; j++)
-					ibuf[k++] = 0.0;
+				for (k = 0, i = 0; i < pin_len; i++) {
+					for (j = 0; j < channels; j++)
+						ibuf[k++] = 0.0;
+				}
 			}
 		}
-
 		in_len += queued;
 		pin_len = in_len;
 
@@ -243,18 +251,20 @@ static int do_conversion(struct data *d)
 		if (queued)
 			memmove(ibuf, &ibuf[pin_len * channels], queued * channels * sizeof(float));
 
-		for (k = 0, i = 0; i < pout_len; i++) {
-			for (j = 0; j < channels; j++) {
-				obuf[k++] = out[MAX_SAMPLES * j + i];
+		if (pout_len > 0) {
+			for (k = 0, i = 0; i < pout_len; i++) {
+				for (j = 0; j < channels; j++) {
+					obuf[k++] = out[MAX_SAMPLES * j + i];
+				}
 			}
-		}
-		pout_len = sf_writef_float(d->ofile, obuf, pout_len);
+			pout_len = sf_writef_float(d->ofile, obuf, pout_len);
 
-		written += pout_len;
+			written += pout_len;
+		}
 	}
-	if (d->verbose) {
+	if (d->verbose)
 		fprintf(stdout, "read %zu samples, wrote %zu samples\n", read, written);
-	}
+
 	return 0;
 }
 
diff --git a/spa/plugins/audioconvert/splitter.c b/spa/plugins/audioconvert/splitter.c
deleted file mode 100644
index bcacb8c3cae0c0837ba4a2458cadf8f0bd4754cd..0000000000000000000000000000000000000000
--- a/spa/plugins/audioconvert/splitter.c
+++ /dev/null
@@ -1,1251 +0,0 @@
-/* Spa
- *
- * Copyright © 2018 Wim Taymans
- *
- * Permission is hereby granted, free of charge, to any person obtaining a
- * copy of this software and associated documentation files (the "Software"),
- * to deal in the Software without restriction, including without limitation
- * the rights to use, copy, modify, merge, publish, distribute, sublicense,
- * and/or sell copies of the Software, and to permit persons to whom the
- * Software is furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice (including the next
- * paragraph) shall be included in all copies or substantial portions of the
- * Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
- * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
- * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
- * DEALINGS IN THE SOFTWARE.
- */
-
-#include <errno.h>
-#include <string.h>
-#include <stdio.h>
-#include <limits.h>
-
-#include <spa/support/plugin.h>
-#include <spa/support/cpu.h>
-#include <spa/support/log.h>
-#include <spa/utils/list.h>
-#include <spa/utils/names.h>
-#include <spa/utils/string.h>
-#include <spa/node/node.h>
-#include <spa/node/utils.h>
-#include <spa/node/io.h>
-#include <spa/param/audio/format-utils.h>
-#include <spa/param/latency-utils.h>
-#include <spa/param/param.h>
-#include <spa/pod/filter.h>
-#include <spa/debug/types.h>
-#include <spa/debug/pod.h>
-
-#include "fmt-ops.h"
-
-#undef SPA_LOG_TOPIC_DEFAULT
-#define SPA_LOG_TOPIC_DEFAULT log_topic
-static struct spa_log_topic *log_topic = &SPA_LOG_TOPIC(0, "spa.splitter");
-
-#define DEFAULT_RATE		48000
-#define DEFAULT_CHANNELS	2
-#define DEFAULT_MASK		(1LL << SPA_AUDIO_CHANNEL_FL) | (1LL << SPA_AUDIO_CHANNEL_FR)
-
-#define MAX_ALIGN	FMT_OPS_MAX_ALIGN
-#define MAX_BUFFERS	32
-#define MAX_DATAS	SPA_AUDIO_MAX_CHANNELS
-#define MAX_PORTS	SPA_AUDIO_MAX_CHANNELS
-
-struct buffer {
-	uint32_t id;
-#define BUFFER_FLAG_QUEUED	(1<<0)
-	uint32_t flags;
-	struct spa_list link;
-	struct spa_buffer *buf;
-	void *datas[MAX_DATAS];
-};
-
-struct port {
-	uint32_t direction;
-	uint32_t id;
-
-	struct spa_io_buffers *io;
-
-	uint64_t info_all;
-	struct spa_port_info info;
-#define IDX_EnumFormat	0
-#define IDX_Meta	1
-#define IDX_IO		2
-#define IDX_Format	3
-#define IDX_Buffers	4
-#define IDX_Latency	5
-#define N_PORT_PARAMS	6
-	struct spa_param_info params[N_PORT_PARAMS];
-
-	struct spa_dict info_props;
-	struct spa_dict_item info_props_items[2];
-	char position[16];
-
-	bool have_format;
-	struct spa_audio_info format;
-	uint32_t blocks;
-	uint32_t stride;
-
-	struct buffer buffers[MAX_BUFFERS];
-	uint32_t n_buffers;
-
-	struct spa_list queue;
-};
-
-struct impl {
-	struct spa_handle handle;
-	struct spa_node node;
-
-	struct spa_log *log;
-	struct spa_cpu *cpu;
-
-	uint32_t cpu_flags;
-	uint32_t max_align;
-	uint32_t quantum_limit;
-
-	struct spa_io_position *io_position;
-
-	uint64_t info_all;
-	struct spa_node_info info;
-#define IDX_PortConfig	0
-#define N_NODE_PARAMS	1
-	struct spa_param_info params[N_NODE_PARAMS];
-
-	struct spa_hook_list hooks;
-
-	struct port in_ports[1];
-	struct port *out_ports[MAX_PORTS];
-	uint32_t port_count;
-
-	struct spa_audio_info format;
-	unsigned int have_profile:1;
-
-	struct convert conv;
-	unsigned int is_passthrough:1;
-	unsigned int started:1;
-
-	struct spa_latency_info latency[2];
-
-	uint32_t src_remap[SPA_AUDIO_MAX_CHANNELS];
-	uint32_t dst_remap[SPA_AUDIO_MAX_CHANNELS];
-
-	uint32_t empty_size;
-	float *empty;
-};
-
-#define CHECK_OUT_PORT(this,d,p)	((d) == SPA_DIRECTION_OUTPUT && (p) < this->port_count)
-#define CHECK_IN_PORT(this,d,p)		((d) == SPA_DIRECTION_INPUT && (p) == 0)
-#define CHECK_PORT(this,d,p)		(CHECK_OUT_PORT(this,d,p) || CHECK_IN_PORT (this,d,p))
-#define GET_IN_PORT(this,p)		(&this->in_ports[p])
-#define GET_OUT_PORT(this,p)		(this->out_ports[p])
-#define GET_PORT(this,d,p)		(d == SPA_DIRECTION_INPUT ? GET_IN_PORT(this,p) : GET_OUT_PORT(this,p))
-
-static void emit_node_info(struct impl *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) {
-		spa_node_emit_info(&this->hooks, &this->info);
-		this->info.change_mask = old;
-	}
-}
-static void emit_port_info(struct impl *this, struct port *port, bool full)
-{
-	uint64_t old = full ? port->info.change_mask : 0;
-	if (full)
-		port->info.change_mask = port->info_all;
-	if (port->info.change_mask) {
-		spa_node_emit_port_info(&this->hooks,
-				port->direction, port->id, &port->info);
-		port->info.change_mask = old;
-	}
-}
-
-static int init_port(struct impl *this, enum spa_direction direction,
-		uint32_t port_id, uint32_t position)
-{
-	struct port *port = GET_OUT_PORT(this, port_id);
-	const char *name;
-
-	if (port == NULL) {
-		port = calloc(1, sizeof(struct port));
-		if (port == NULL)
-			return -errno;
-		this->out_ports[port_id] = port;
-	}
-	port->direction = direction;
-	port->id = port_id;
-
-	name = spa_debug_type_find_short_name(spa_type_audio_channel, position);
-	snprintf(port->position, sizeof(port->position), "%s", name ? name : "UNK");
-
-	port->info_all = SPA_PORT_CHANGE_MASK_FLAGS |
-			SPA_PORT_CHANGE_MASK_PROPS |
-			SPA_PORT_CHANGE_MASK_PARAMS;
-
-	port->info = SPA_PORT_INFO_INIT();
-	port->info.flags = SPA_PORT_FLAG_DYNAMIC_DATA;
-	port->info_props_items[0] = SPA_DICT_ITEM_INIT(SPA_KEY_FORMAT_DSP, "32 bit float mono audio");
-	port->info_props_items[1] = SPA_DICT_ITEM_INIT(SPA_KEY_AUDIO_CHANNEL, port->position);
-	port->info_props = SPA_DICT_INIT(port->info_props_items, 2);
-	port->info.props = &port->info_props;
-	port->params[IDX_EnumFormat] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ);
-	port->params[IDX_Meta] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ);
-	port->params[IDX_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
-	port->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
-	port->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
-	port->params[IDX_Latency] = SPA_PARAM_INFO(SPA_PARAM_Latency, SPA_PARAM_INFO_READWRITE);
-	port->info.params = port->params;
-	port->info.n_params = N_PORT_PARAMS;
-
-	spa_list_init(&port->queue);
-
-	port->n_buffers = 0;
-	port->have_format = false;
-	port->format.media_type = SPA_MEDIA_TYPE_audio;
-	port->format.media_subtype = SPA_MEDIA_SUBTYPE_dsp;
-	port->format.info.dsp.format = SPA_AUDIO_FORMAT_DSP_F32;
-
-	spa_log_debug(this->log, "%p: init port %d:%d position:%s",
-			this, direction, port_id, port->position);
-	emit_port_info(this, port, true);
-
-	return 0;
-}
-
-static int impl_node_enum_params(void *object, int seq,
-				 uint32_t id, uint32_t start, uint32_t num,
-				 const struct spa_pod *filter)
-{
-	struct impl *this = object;
-	struct spa_pod *param;
-	struct spa_pod_builder b = { 0 };
-	uint8_t buffer[4096];
-	struct spa_result_node_params result;
-	uint32_t count = 0;
-
-	spa_return_val_if_fail(this != NULL, -EINVAL);
-	spa_return_val_if_fail(num != 0, -EINVAL);
-
-	result.id = id;
-	result.next = start;
-      next:
-	result.index = result.next++;
-
-	spa_pod_builder_init(&b, buffer, sizeof(buffer));
-
-	switch (id) {
-	default:
-		return 0;
-	}
-
-	if (spa_pod_filter(&b, &result.param, param, filter) < 0)
-		goto next;
-
-	spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
-
-	if (++count != num)
-		goto next;
-
-	return 0;
-}
-
-static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size)
-{
-	struct impl *this = object;
-
-	spa_return_val_if_fail(this != NULL, -EINVAL);
-
-	spa_log_debug(this->log, "%p: io %d %p/%zd", this, id, data, size);
-
-	switch (id) {
-	case SPA_IO_Position:
-		this->io_position = data;
-		break;
-	default:
-		return -ENOENT;
-	}
-	return 0;
-}
-
-static int int32_cmp(const void *v1, const void *v2)
-{
-	int32_t a1 = *(int32_t*)v1;
-	int32_t a2 = *(int32_t*)v2;
-	if (a1 == 0 && a2 != 0)
-		return 1;
-	if (a2 == 0 && a1 != 0)
-		return -1;
-	return a1 - a2;
-}
-
-static int impl_node_set_param(void *object, uint32_t id, uint32_t flags,
-			       const struct spa_pod *param)
-{
-	struct impl *this = object;
-	int res;
-
-	spa_return_val_if_fail(this != NULL, -EINVAL);
-
-	if (param == NULL)
-		return 0;
-
-	switch (id) {
-	case SPA_PARAM_PortConfig:
-	{
-		struct port *port;
-		struct spa_audio_info info = { 0, };
-		struct spa_pod *format;
-		enum spa_direction direction;
-		enum spa_param_port_config_mode mode;
-		uint32_t i;
-
-		if (spa_pod_parse_object(param,
-				SPA_TYPE_OBJECT_ParamPortConfig, NULL,
-				SPA_PARAM_PORT_CONFIG_direction,	SPA_POD_Id(&direction),
-				SPA_PARAM_PORT_CONFIG_mode,		SPA_POD_Id(&mode),
-				SPA_PARAM_PORT_CONFIG_format,		SPA_POD_Pod(&format)) < 0)
-			return -EINVAL;
-
-		if (!spa_pod_is_object_type(format, SPA_TYPE_OBJECT_Format))
-			return -EINVAL;
-
-		if (mode != SPA_PARAM_PORT_CONFIG_MODE_dsp)
-			return -ENOTSUP;
-		if (direction != SPA_DIRECTION_OUTPUT)
-			return -EINVAL;
-
-		if ((res = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0)
-			return res;
-
-		if (info.media_type != SPA_MEDIA_TYPE_audio ||
-		    info.media_subtype != SPA_MEDIA_SUBTYPE_raw)
-			return -ENOTSUP;
-
-		if (spa_format_audio_raw_parse(format, &info.info.raw) < 0)
-			return -EINVAL;
-
-		info.info.raw.rate = 0;
-
-		if (this->have_profile && memcmp(&this->format, &info, sizeof(info)) == 0)
-			return 0;
-
-		spa_log_debug(this->log, "%p: port config %d/%d", this,
-				info.info.raw.rate, info.info.raw.channels);
-
-		for (i = 0; i < this->port_count; i++)
-			spa_node_emit_port_info(&this->hooks,
-					SPA_DIRECTION_OUTPUT, i, NULL);
-
-		this->have_profile = true;
-		this->is_passthrough = true;
-		this->format = info;
-
-		this->port_count = info.info.raw.channels;
-		for (i = 0; i < this->port_count; i++) {
-			init_port(this, SPA_DIRECTION_OUTPUT, i,
-					info.info.raw.position[i]);
-		}
-		port = GET_IN_PORT(this, 0);
-		qsort(info.info.raw.position, info.info.raw.channels,
-					sizeof(uint32_t), int32_cmp);
-		port->format = info;
-		port->have_format = true;
-		return 0;
-	}
-	default:
-		return -ENOENT;
-	}
-	return 0;
-}
-
-static int impl_node_send_command(void *object, const struct spa_command *command)
-{
-	struct impl *this = object;
-
-	spa_return_val_if_fail(this != NULL, -EINVAL);
-	spa_return_val_if_fail(command != NULL, -EINVAL);
-
-	switch (SPA_NODE_COMMAND_ID(command)) {
-	case SPA_NODE_COMMAND_Start:
-		this->started = true;
-		break;
-	case SPA_NODE_COMMAND_Suspend:
-	case SPA_NODE_COMMAND_Flush:
-	case SPA_NODE_COMMAND_Pause:
-		this->started = false;
-		break;
-	default:
-		return -ENOTSUP;
-	}
-	return 0;
-}
-
-static int
-impl_node_add_listener(void *object,
-		struct spa_hook *listener,
-		const struct spa_node_events *events,
-		void *data)
-{
-	struct impl *this = object;
-	struct spa_hook_list save;
-	uint32_t i;
-
-	spa_return_val_if_fail(this != NULL, -EINVAL);
-
-	spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
-
-	emit_node_info(this, true);
-	emit_port_info(this, GET_IN_PORT(this, 0), true);
-	for (i = 0; i < this->port_count; i++)
-		emit_port_info(this, GET_OUT_PORT(this, i), true);
-
-	spa_hook_list_join(&this->hooks, &save);
-
-	return 0;
-}
-
-static int
-impl_node_set_callbacks(void *object,
-			const struct spa_node_callbacks *callbacks,
-			void *user_data)
-{
-	return 0;
-}
-
-static int impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id,
-		const struct spa_dict *props)
-{
-	return -ENOTSUP;
-}
-
-static int
-impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id)
-{
-	return -ENOTSUP;
-}
-
-static int port_enum_formats(void *object,
-			     enum spa_direction direction, uint32_t port_id,
-			     uint32_t index,
-			     struct spa_pod **param,
-			     struct spa_pod_builder *builder)
-{
-	struct impl *this = object;
-	struct port *port = GET_PORT(this, direction, port_id);
-
-	switch (index) {
-	case 0:
-		if (direction == SPA_DIRECTION_OUTPUT) {
-			*param = spa_format_audio_dsp_build(builder,
-					SPA_PARAM_EnumFormat, &port->format.info.dsp);
-		} else if (port->have_format) {
-			*param = spa_format_audio_raw_build(builder,
-					SPA_PARAM_EnumFormat, &port->format.info.raw);
-		}
-		else {
-			uint32_t rate = this->io_position ?
-				this->io_position->clock.rate.denom : DEFAULT_RATE;
-
-			*param = spa_pod_builder_add_object(builder,
-				SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat,
-				SPA_FORMAT_mediaType,      SPA_POD_Id(SPA_MEDIA_TYPE_audio),
-				SPA_FORMAT_mediaSubtype,   SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw),
-				SPA_FORMAT_AUDIO_format,   SPA_POD_CHOICE_ENUM_Id(25,
-							SPA_AUDIO_FORMAT_F32P,
-							SPA_AUDIO_FORMAT_F32P,
-							SPA_AUDIO_FORMAT_F32,
-							SPA_AUDIO_FORMAT_F32_OE,
-							SPA_AUDIO_FORMAT_F64P,
-							SPA_AUDIO_FORMAT_F64,
-							SPA_AUDIO_FORMAT_F64_OE,
-							SPA_AUDIO_FORMAT_S32P,
-							SPA_AUDIO_FORMAT_S32,
-							SPA_AUDIO_FORMAT_S32_OE,
-							SPA_AUDIO_FORMAT_S24_32P,
-							SPA_AUDIO_FORMAT_S24_32,
-							SPA_AUDIO_FORMAT_S24_32_OE,
-							SPA_AUDIO_FORMAT_S24P,
-							SPA_AUDIO_FORMAT_S24,
-							SPA_AUDIO_FORMAT_S24_OE,
-							SPA_AUDIO_FORMAT_S16P,
-							SPA_AUDIO_FORMAT_S16,
-							SPA_AUDIO_FORMAT_S16_OE,
-							SPA_AUDIO_FORMAT_S8P,
-							SPA_AUDIO_FORMAT_S8,
-							SPA_AUDIO_FORMAT_U8P,
-							SPA_AUDIO_FORMAT_U8,
-							SPA_AUDIO_FORMAT_ULAW,
-							SPA_AUDIO_FORMAT_ALAW),
-				SPA_FORMAT_AUDIO_rate,     SPA_POD_CHOICE_RANGE_Int(
-						rate, 1, INT32_MAX),
-				SPA_FORMAT_AUDIO_channels, SPA_POD_CHOICE_RANGE_Int(
-						DEFAULT_CHANNELS, 1, MAX_PORTS));
-		}
-		break;
-	default:
-		return 0;
-	}
-	return 1;
-}
-
-static int
-impl_node_port_enum_params(void *object, int seq,
-			   enum spa_direction direction, uint32_t port_id,
-			   uint32_t id, uint32_t start, uint32_t num,
-			   const struct spa_pod *filter)
-{
-	struct impl *this = object;
-	struct port *port;
-	struct spa_pod *param;
-	struct spa_pod_builder b = { 0 };
-	uint8_t buffer[2048];
-	struct spa_result_node_params result;
-	uint32_t count = 0;
-	int res;
-
-	spa_return_val_if_fail(this != NULL, -EINVAL);
-	spa_return_val_if_fail(num != 0, -EINVAL);
-	spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
-
-	port = GET_PORT(this, direction, port_id);
-
-	spa_log_debug(this->log, "%p: enum params port %d.%d %d %u",
-			this, direction, port_id, seq, id);
-
-	result.id = id;
-	result.next = start;
-      next:
-	result.index = result.next++;
-
-	spa_pod_builder_init(&b, buffer, sizeof(buffer));
-
-	switch (id) {
-	case SPA_PARAM_EnumFormat:
-		if ((res = port_enum_formats(this, direction, port_id,
-						result.index, &param, &b)) <= 0)
-			return res;
-		break;
-	case SPA_PARAM_Format:
-		if (!port->have_format)
-			return -EIO;
-		if (result.index > 0)
-			return 0;
-
-		if (direction == SPA_DIRECTION_OUTPUT)
-			param = spa_format_audio_dsp_build(&b, id, &port->format.info.dsp);
-		else
-			param = spa_format_audio_raw_build(&b, id, &port->format.info.raw);
-		break;
-	case SPA_PARAM_Buffers:
-		if (!port->have_format)
-			return -EIO;
-		if (result.index > 0)
-			return 0;
-
-		param = spa_pod_builder_add_object(&b,
-			SPA_TYPE_OBJECT_ParamBuffers, id,
-			SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(1, 1, MAX_BUFFERS),
-			SPA_PARAM_BUFFERS_blocks,  SPA_POD_Int(port->blocks),
-			SPA_PARAM_BUFFERS_size,    SPA_POD_CHOICE_RANGE_Int(
-							this->quantum_limit * port->stride,
-							16 * port->stride,
-							INT32_MAX),
-			SPA_PARAM_BUFFERS_stride,  SPA_POD_Int(port->stride));
-		break;
-
-	case SPA_PARAM_Meta:
-		switch (result.index) {
-		case 0:
-			param = spa_pod_builder_add_object(&b,
-				SPA_TYPE_OBJECT_ParamMeta, id,
-				SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header),
-				SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header)));
-			break;
-		default:
-			return 0;
-		}
-		break;
-	case SPA_PARAM_IO:
-		switch (result.index) {
-		case 0:
-			param = spa_pod_builder_add_object(&b,
-				SPA_TYPE_OBJECT_ParamIO, id,
-				SPA_PARAM_IO_id,   SPA_POD_Id(SPA_IO_Buffers),
-				SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers)));
-			break;
-		default:
-			return 0;
-		}
-		break;
-	case SPA_PARAM_Latency:
-		switch (result.index) {
-		case 0: case 1:
-			param = spa_latency_build(&b, id, &this->latency[result.index]);
-			break;
-		default:
-			return 0;
-		}
-		break;
-	default:
-		return -ENOENT;
-	}
-
-	if (spa_pod_filter(&b, &result.param, param, filter) < 0)
-		goto next;
-
-	spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
-
-	if (++count != num)
-		goto next;
-
-	return 0;
-}
-
-static int clear_buffers(struct impl *this, struct port *port)
-{
-	if (port->n_buffers > 0) {
-		spa_log_debug(this->log, "%p: clear buffers %p", this, port);
-		port->n_buffers = 0;
-		spa_list_init(&port->queue);
-	}
-	return 0;
-}
-
-static int setup_convert(struct impl *this)
-{
-	struct port *inport;
-	struct spa_audio_info informat, outformat;
-	uint32_t i, j, src_fmt, dst_fmt;
-	int res;
-
-	inport = GET_IN_PORT(this, 0);
-
-	informat = inport->format;
-	outformat = this->format;
-
-	src_fmt = informat.info.raw.format;
-	dst_fmt = SPA_AUDIO_FORMAT_DSP_F32;
-
-	spa_log_info(this->log, "%p: %s/%d@%d->%s/%d@%dx%d", this,
-			spa_debug_type_find_name(spa_type_audio_format, src_fmt),
-			informat.info.raw.channels,
-			informat.info.raw.rate,
-			spa_debug_type_find_name(spa_type_audio_format, dst_fmt),
-			1,
-			outformat.info.raw.rate,
-			outformat.info.raw.channels);
-
-	for (i = 0; i < informat.info.raw.channels; i++) {
-		for (j = 0; j < outformat.info.raw.channels; j++) {
-			if (informat.info.raw.position[i] !=
-			    outformat.info.raw.position[j])
-				continue;
-			this->src_remap[i] = j;
-			this->dst_remap[j] = i;
-			spa_log_debug(this->log, "%p: channel %d -> %d (%s -> %s)", this,
-					i, j,
-					spa_debug_type_find_short_name(spa_type_audio_channel,
-						informat.info.raw.position[i]),
-					spa_debug_type_find_short_name(spa_type_audio_channel,
-						outformat.info.raw.position[j]));
-			outformat.info.raw.position[j] = -1;
-			break;
-		}
-	}
-
-	this->conv.src_fmt = src_fmt;
-	this->conv.dst_fmt = dst_fmt;
-	this->conv.n_channels = informat.info.raw.channels;
-	this->conv.cpu_flags = this->cpu_flags;
-
-	if ((res = convert_init(&this->conv)) < 0)
-		return res;
-
-	this->is_passthrough &= this->conv.is_passthrough;
-
-	spa_log_debug(this->log, "%p: got converter features %08x:%08x passthrough:%d", this,
-			this->cpu_flags, this->conv.cpu_flags, this->is_passthrough);
-
-	return 0;
-}
-
-static int calc_width(struct spa_audio_info *info)
-{
-	switch (info->info.raw.format) {
-	case SPA_AUDIO_FORMAT_U8:
-	case SPA_AUDIO_FORMAT_U8P:
-	case SPA_AUDIO_FORMAT_S8:
-	case SPA_AUDIO_FORMAT_S8P:
-	case SPA_AUDIO_FORMAT_ULAW:
-	case SPA_AUDIO_FORMAT_ALAW:
-		return 1;
-	case SPA_AUDIO_FORMAT_S16P:
-	case SPA_AUDIO_FORMAT_S16:
-	case SPA_AUDIO_FORMAT_S16_OE:
-		return 2;
-	case SPA_AUDIO_FORMAT_S24P:
-	case SPA_AUDIO_FORMAT_S24:
-	case SPA_AUDIO_FORMAT_S24_OE:
-		return 3;
-	case SPA_AUDIO_FORMAT_F64P:
-	case SPA_AUDIO_FORMAT_F64:
-	case SPA_AUDIO_FORMAT_F64_OE:
-		return 8;
-	default:
-		return 4;
-	}
-}
-
-static int port_set_latency(void *object,
-			   enum spa_direction direction,
-			   uint32_t port_id,
-			   uint32_t flags,
-			   const struct spa_pod *latency)
-{
-	struct impl *this = object;
-	struct port *port;
-	enum spa_direction other = SPA_DIRECTION_REVERSE(direction);
-	uint32_t i;
-
-	spa_log_debug(this->log, "%p: set latency direction:%d", this, direction);
-
-	if (latency == NULL) {
-		this->latency[other] = SPA_LATENCY_INFO(other);
-	} else {
-		struct spa_latency_info info;
-		if (spa_latency_parse(latency, &info) < 0 ||
-		    info.direction != other)
-			return -EINVAL;
-		this->latency[other] = info;
-	}
-	for (i = 0; i < this->port_count; i++) {
-		port = GET_OUT_PORT(this, i);
-		port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
-		port->params[IDX_Latency].flags ^= SPA_PARAM_INFO_SERIAL;
-		emit_port_info(this, port, false);
-	}
-	port = GET_IN_PORT(this, 0);
-	port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
-	port->params[IDX_Latency].flags ^= SPA_PARAM_INFO_SERIAL;
-	emit_port_info(this, port, false);
-	return 0;
-}
-
-static int port_set_format(void *object,
-			   enum spa_direction direction,
-			   uint32_t port_id,
-			   uint32_t flags,
-			   const struct spa_pod *format)
-{
-	struct impl *this = object;
-	struct port *port;
-	int res;
-
-	port = GET_PORT(this, direction, port_id);
-
-	spa_log_debug(this->log, "%p: set format", this);
-
-	if (format == NULL) {
-		if (port->have_format) {
-			if (direction == SPA_DIRECTION_INPUT)
-				port->have_format = this->have_profile;
-			else
-				port->have_format = false;
-			port->format.info.raw.rate = 0;
-			clear_buffers(this, port);
-		}
-	} else {
-		struct spa_audio_info info = { 0 };
-
-		if ((res = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0)
-			return res;
-
-		if (direction == SPA_DIRECTION_OUTPUT) {
-			if (info.media_type != SPA_MEDIA_TYPE_audio ||
-			    info.media_subtype != SPA_MEDIA_SUBTYPE_dsp)
-				return -EINVAL;
-			if (spa_format_audio_dsp_parse(format, &info.info.dsp) < 0)
-				return -EINVAL;
-			if (info.info.dsp.format != SPA_AUDIO_FORMAT_DSP_F32)
-				return -EINVAL;
-
-			port->stride = 4;
-			port->blocks = 1;
-		}
-		else {
-			if (info.media_type != SPA_MEDIA_TYPE_audio ||
-			    info.media_subtype != SPA_MEDIA_SUBTYPE_raw)
-				return -EINVAL;
-			if (spa_format_audio_raw_parse(format, &info.info.raw) < 0)
-				return -EINVAL;
-			if (info.info.raw.channels != this->port_count)
-				return -EINVAL;
-
-			port->stride = calc_width(&info);
-			if (SPA_AUDIO_FORMAT_IS_PLANAR(info.info.raw.format)) {
-				port->blocks = info.info.raw.channels;
-			} else {
-				port->stride *= info.info.raw.channels;
-				port->blocks = 1;
-			}
-		}
-
-		port->format = info;
-
-		spa_log_debug(this->log, "%p: %d %d %d", this, port_id, port->stride, port->blocks);
-
-		if (direction == SPA_DIRECTION_INPUT)
-			if ((res = setup_convert(this)) < 0)
-				return res;
-
-		port->have_format = true;
-	}
-	port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
-	if (port->have_format) {
-		port->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE);
-		port->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ);
-	} else {
-		port->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
-		port->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
-	}
-	emit_port_info(this, port, false);
-
-	return 0;
-}
-
-
-static int
-impl_node_port_set_param(void *object,
-			 enum spa_direction direction, uint32_t port_id,
-			 uint32_t id, uint32_t flags,
-			 const struct spa_pod *param)
-{
-	struct impl *this = object;
-
-	spa_return_val_if_fail(this != NULL, -EINVAL);
-
-	spa_log_debug(this->log, "%p: set param port %d.%d %u",
-			this, direction, port_id, id);
-
-	spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
-
-	switch (id) {
-	case SPA_PARAM_Latency:
-		return port_set_latency(this, direction, port_id, flags, param);
-	case SPA_PARAM_Format:
-		return port_set_format(this, direction, port_id, flags, param);
-	default:
-		return -ENOENT;
-	}
-}
-
-static void queue_buffer(struct impl *this, struct port *port, uint32_t id)
-{
-	struct buffer *b = &port->buffers[id];
-
-	spa_log_trace_fp(this->log, "%p: queue buffer %d on port %d %d",
-			this, id, port->id, b->flags);
-	if (SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_QUEUED))
-		return;
-
-	spa_list_append(&port->queue, &b->link);
-	SPA_FLAG_SET(b->flags, BUFFER_FLAG_QUEUED);
-}
-
-static struct buffer *dequeue_buffer(struct impl *this, struct port *port)
-{
-	struct buffer *b;
-
-	if (spa_list_is_empty(&port->queue))
-		return NULL;
-
-	b = spa_list_first(&port->queue, struct buffer, link);
-	spa_list_remove(&b->link);
-	SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_QUEUED);
-	spa_log_trace_fp(this->log, "%p: dequeue buffer %d on port %d %u",
-			this, b->id, port->id, b->flags);
-
-	return b;
-}
-
-static int
-impl_node_port_use_buffers(void *object,
-			   enum spa_direction direction,
-			   uint32_t port_id,
-			   uint32_t flags,
-			   struct spa_buffer **buffers,
-			   uint32_t n_buffers)
-{
-	struct impl *this = object;
-	struct port *port;
-	uint32_t i, j, maxsize;
-
-	spa_return_val_if_fail(this != NULL, -EINVAL);
-	spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
-
-	port = GET_PORT(this, direction, port_id);
-
-	spa_return_val_if_fail(port->have_format, -EIO);
-
-	spa_log_debug(this->log, "%p: use buffers %d on port %d", this, n_buffers, port_id);
-
-	clear_buffers(this, port);
-
-	maxsize = 0;
-	for (i = 0; i < n_buffers; i++) {
-		struct buffer *b;
-		uint32_t n_datas = buffers[i]->n_datas;
-		struct spa_data *d = buffers[i]->datas;
-
-		b = &port->buffers[i];
-		b->id = i;
-		b->buf = buffers[i];
-		b->flags = 0;
-
-		for (j = 0; j < n_datas; j++) {
-			if (d[j].data == NULL) {
-				spa_log_error(this->log, "%p: invalid memory %d on buffer %d %d %p",
-						this, j, i, d[j].type, d[j].data);
-				return -EINVAL;
-			}
-			if (!SPA_IS_ALIGNED(d[j].data, this->max_align)) {
-				spa_log_warn(this->log, "%p: memory %d on buffer %d not aligned",
-						this, j, i);
-			}
-			b->datas[j] = d[j].data;
-			if (direction == SPA_DIRECTION_OUTPUT &&
-			    !SPA_FLAG_IS_SET(d[j].flags, SPA_DATA_FLAG_DYNAMIC))
-				this->is_passthrough = false;
-
-			spa_log_debug(this->log, "%p: buffer %d data %d flags:%08x %p",
-					this, i, j, d[j].flags, b->datas[j]);
-
-			maxsize = SPA_MAX(maxsize, d[j].maxsize);
-		}
-		if (direction == SPA_DIRECTION_OUTPUT)
-			queue_buffer(this, port, i);
-	}
-	if (maxsize > this->empty_size) {
-		this->empty = realloc(this->empty, maxsize + MAX_ALIGN);
-		if (this->empty == NULL)
-			return -errno;
-		memset(this->empty, 0, maxsize + MAX_ALIGN);
-		this->empty_size = maxsize;
-	}
-	port->n_buffers = n_buffers;
-
-	return 0;
-}
-
-static int
-impl_node_port_set_io(void *object,
-		      enum spa_direction direction, uint32_t port_id,
-		      uint32_t id, void *data, size_t size)
-{
-	struct impl *this = object;
-	struct port *port;
-
-	spa_return_val_if_fail(this != NULL, -EINVAL);
-	spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
-
-	port = GET_PORT(this, direction, port_id);
-
-	switch (id) {
-	case SPA_IO_Buffers:
-		port->io = data;
-		break;
-	default:
-		return -ENOENT;
-	}
-	return 0;
-}
-
-static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id)
-{
-	struct impl *this = object;
-	struct port *port;
-
-	spa_return_val_if_fail(this != NULL, -EINVAL);
-	spa_return_val_if_fail(CHECK_PORT(this, SPA_DIRECTION_OUTPUT, port_id), -EINVAL);
-
-	port = GET_OUT_PORT(this, port_id);
-	queue_buffer(this, port, buffer_id);
-
-	return 0;
-}
-
-static int impl_node_process(void *object)
-{
-	struct impl *this = object;
-	struct port *inport;
-	struct spa_io_buffers *inio;
-	uint32_t i, maxsize, n_samples;
-	struct spa_data *sd, *dd;
-	struct buffer *sbuf, *dbuf;
-	uint32_t n_src_datas, n_dst_datas;
-	const void **src_datas;
-	void **dst_datas;
-
-	spa_return_val_if_fail(this != NULL, -EINVAL);
-
-	inport = GET_IN_PORT(this, 0);
-	inio = inport->io;
-	spa_return_val_if_fail(inio != NULL, -EIO);
-	spa_return_val_if_fail(this->conv.process != NULL, -EIO);
-
-	spa_log_trace_fp(this->log, "%p: status %p %d %d", this,
-			inio, inio->status, inio->buffer_id);
-
-	if (SPA_UNLIKELY(inio->status != SPA_STATUS_HAVE_DATA))
-		return inio->status;
-
-	if (SPA_UNLIKELY(inio->buffer_id >= inport->n_buffers))
-		return inio->status = -EINVAL;
-
-	sbuf = &inport->buffers[inio->buffer_id];
-	sd = sbuf->buf->datas;
-
-	n_src_datas = sbuf->buf->n_datas;
-	src_datas = alloca(sizeof(void*) * n_src_datas);
-
-	maxsize = INT_MAX;
-	for (i = 0; i < n_src_datas; i++) {
-		src_datas[i] = SPA_PTROFF(sd[i].data,
-				sd[i].chunk->offset, void);
-		maxsize = SPA_MIN(sd[i].chunk->size, maxsize);
-	}
-	n_samples = maxsize / inport->stride;
-
-	n_dst_datas = this->port_count;
-	dst_datas = alloca(sizeof(void*) * n_dst_datas);
-
-	for (i = 0; i < n_dst_datas; i++) {
-		struct port *outport = GET_OUT_PORT(this, i);
-		struct spa_io_buffers *outio;
-		uint32_t src_remap = this->src_remap[i];
-		uint32_t dst_remap = this->dst_remap[i];
-
-		if (SPA_UNLIKELY((outio = outport->io) == NULL))
-			goto empty;
-
-		spa_log_trace_fp(this->log, "%p: %d %p %d %d %d", this, i,
-				outio, outio->status, outio->buffer_id, outport->stride);
-
-		if (SPA_UNLIKELY(outio->status == SPA_STATUS_HAVE_DATA))
-			goto empty;
-
-		if (SPA_LIKELY(outio->buffer_id < outport->n_buffers)) {
-			queue_buffer(this, outport, outio->buffer_id);
-			outio->buffer_id = SPA_ID_INVALID;
-		}
-
-		if (SPA_UNLIKELY((dbuf = dequeue_buffer(this, outport)) == NULL)) {
-			outio->status = -EPIPE;
-          empty:
-			spa_log_trace_fp(this->log, "%p: %d skip output", this, i);
-			dst_datas[dst_remap] = SPA_PTR_ALIGN(this->empty, MAX_ALIGN, void);
-			continue;
-		}
-
-		dd = dbuf->buf->datas;
-
-		maxsize = dd->maxsize;
-		n_samples = SPA_MIN(n_samples, maxsize / outport->stride);
-
-		if (this->is_passthrough)
-			dd[0].data = (void *)src_datas[src_remap];
-		else
-			dst_datas[dst_remap] = dd[0].data = dbuf->datas[0];
-
-		dd[0].chunk->offset = 0;
-		dd[0].chunk->size = n_samples * outport->stride;
-
-		outio->status = SPA_STATUS_HAVE_DATA;
-		outio->buffer_id = dbuf->id;
-	}
-
-	spa_log_trace_fp(this->log, "%p: n_src:%d n_dst:%d n_samples:%d max:%d stride:%d p:%d", this,
-			n_src_datas, n_dst_datas, n_samples, maxsize, inport->stride,
-			this->is_passthrough);
-
-	if (!this->is_passthrough)
-		convert_process(&this->conv, dst_datas, src_datas, n_samples);
-
-	inio->status = SPA_STATUS_NEED_DATA;
-
-	return SPA_STATUS_NEED_DATA | SPA_STATUS_HAVE_DATA;
-}
-
-static const struct spa_node_methods impl_node = {
-	SPA_VERSION_NODE_METHODS,
-	.add_listener = impl_node_add_listener,
-	.set_callbacks = impl_node_set_callbacks,
-	.enum_params = impl_node_enum_params,
-	.set_param = impl_node_set_param,
-	.set_io = impl_node_set_io,
-	.send_command = impl_node_send_command,
-	.add_port = impl_node_add_port,
-	.remove_port = impl_node_remove_port,
-	.port_enum_params = impl_node_port_enum_params,
-	.port_set_param = impl_node_port_set_param,
-	.port_use_buffers = impl_node_port_use_buffers,
-	.port_set_io = impl_node_port_set_io,
-	.port_reuse_buffer = impl_node_port_reuse_buffer,
-	.process = impl_node_process,
-};
-
-static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface)
-{
-	struct impl *this;
-
-	spa_return_val_if_fail(handle != NULL, -EINVAL);
-	spa_return_val_if_fail(interface != NULL, -EINVAL);
-
-	this = (struct impl *) handle;
-
-	if (spa_streq(type, SPA_TYPE_INTERFACE_Node))
-		*interface = &this->node;
-	else
-		return -ENOENT;
-
-	return 0;
-}
-
-static int impl_clear(struct spa_handle *handle)
-{
-	struct impl *this;
-	uint32_t i;
-
-	spa_return_val_if_fail(handle != NULL, -EINVAL);
-
-	this = (struct impl *) handle;
-
-	for (i = 0; i < MAX_PORTS; i++)
-		free(this->out_ports[i]);
-	free(this->empty);
-	return 0;
-}
-
-static size_t
-impl_get_size(const struct spa_handle_factory *factory,
-	      const struct spa_dict *params)
-{
-	return sizeof(struct impl);
-}
-
-static int
-impl_init(const struct spa_handle_factory *factory,
-	  struct spa_handle *handle,
-	  const struct spa_dict *info,
-	  const struct spa_support *support,
-	  uint32_t n_support)
-{
-	struct impl *this;
-	struct port *port;
-	uint32_t i;
-
-	spa_return_val_if_fail(factory != NULL, -EINVAL);
-	spa_return_val_if_fail(handle != NULL, -EINVAL);
-
-	handle->get_interface = impl_get_interface;
-	handle->clear = impl_clear;
-
-	this = (struct impl *) handle;
-
-	this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
-	spa_log_topic_init(this->log, log_topic);
-
-	this->cpu = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_CPU);
-	if (this->cpu) {
-		this->cpu_flags = spa_cpu_get_flags(this->cpu);
-		this->max_align = SPA_MIN(MAX_ALIGN, spa_cpu_get_max_align(this->cpu));
-	}
-
-	for (i = 0; info && i < info->n_items; i++) {
-		const char *k = info->items[i].key;
-		const char *s = info->items[i].value;
-		if (spa_streq(k, "clock.quantum-limit"))
-			spa_atou32(s, &this->quantum_limit, 0);
-	}
-
-	spa_hook_list_init(&this->hooks);
-
-	this->latency[SPA_DIRECTION_INPUT] = SPA_LATENCY_INFO(SPA_DIRECTION_INPUT);
-	this->latency[SPA_DIRECTION_OUTPUT] = SPA_LATENCY_INFO(SPA_DIRECTION_OUTPUT);
-
-	this->node.iface = SPA_INTERFACE_INIT(
-			SPA_TYPE_INTERFACE_Node,
-			SPA_VERSION_NODE,
-			&impl_node, this);
-	this->info_all = SPA_NODE_CHANGE_MASK_FLAGS |
-			SPA_NODE_CHANGE_MASK_PARAMS;
-	this->info = SPA_NODE_INFO_INIT();
-	this->info.max_input_ports = 1;
-	this->info.max_output_ports = MAX_PORTS;
-	this->info.flags = SPA_NODE_FLAG_RT |
-		SPA_NODE_FLAG_OUT_PORT_CONFIG;
-	this->params[IDX_PortConfig] = SPA_PARAM_INFO(SPA_PARAM_PortConfig, SPA_PARAM_INFO_WRITE);
-	this->info.params = this->params;
-	this->info.n_params = N_NODE_PARAMS;
-
-	port = GET_IN_PORT(this, 0);
-	port->info_all = SPA_PORT_CHANGE_MASK_FLAGS |
-			SPA_PORT_CHANGE_MASK_PARAMS;
-	port->direction = SPA_DIRECTION_INPUT;
-	port->id = 0;
-	port->info = SPA_PORT_INFO_INIT();
-	port->info.flags = SPA_PORT_FLAG_NO_REF |
-			SPA_PORT_FLAG_DYNAMIC_DATA;
-	port->params[IDX_EnumFormat] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ);
-	port->params[IDX_Meta] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ);
-	port->params[IDX_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
-	port->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
-	port->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
-	port->params[IDX_Latency] = SPA_PARAM_INFO(SPA_PARAM_Latency, SPA_PARAM_INFO_READWRITE);
-	port->info.params = port->params;
-	port->info.n_params = N_PORT_PARAMS;
-
-	return 0;
-}
-
-static const struct spa_interface_info impl_interfaces[] = {
-	{SPA_TYPE_INTERFACE_Node,},
-};
-
-static int
-impl_enum_interface_info(const struct spa_handle_factory *factory,
-			 const struct spa_interface_info **info,
-			 uint32_t *index)
-{
-	spa_return_val_if_fail(factory != NULL, -EINVAL);
-	spa_return_val_if_fail(info != NULL, -EINVAL);
-	spa_return_val_if_fail(index != NULL, -EINVAL);
-
-	switch (*index) {
-	case 0:
-		*info = &impl_interfaces[*index];
-		break;
-	default:
-		return 0;
-	}
-	(*index)++;
-	return 1;
-}
-
-const struct spa_handle_factory spa_splitter_factory = {
-	SPA_VERSION_HANDLE_FACTORY,
-	SPA_NAME_AUDIO_PROCESS_DEINTERLEAVE,
-	NULL,
-	impl_get_size,
-	impl_init,
-	impl_enum_interface_info,
-};
diff --git a/spa/plugins/audioconvert/test-audioconvert.c b/spa/plugins/audioconvert/test-audioconvert.c
index 72ae39d1fa99cc5e3e622147b1c83925868e5e81..883803d06c30d1e562d4b60076984c404748325c 100644
--- a/spa/plugins/audioconvert/test-audioconvert.c
+++ b/spa/plugins/audioconvert/test-audioconvert.c
@@ -36,6 +36,7 @@
 #include <spa/param/audio/format.h>
 #include <spa/param/audio/format-utils.h>
 #include <spa/node/node.h>
+#include <spa/node/io.h>
 #include <spa/debug/mem.h>
 #include <spa/support/log-impl.h>
 
@@ -43,7 +44,7 @@ SPA_LOG_IMPL(logger);
 
 extern const struct spa_handle_factory test_source_factory;
 
-#define MAX_PORTS SPA_AUDIO_MAX_CHANNELS
+#define MAX_PORTS (SPA_AUDIO_MAX_CHANNELS+1)
 
 struct context {
 	struct spa_handle *convert_handle;
@@ -71,6 +72,7 @@ static int setup_context(struct context *ctx)
 	size_t size;
 	int res;
 	struct spa_support support[1];
+	struct spa_dict_item items[2];
 	const struct spa_handle_factory *factory;
 	void *iface;
 
@@ -86,9 +88,11 @@ static int setup_context(struct context *ctx)
 	ctx->convert_handle = calloc(1, size);
 	spa_assert_se(ctx->convert_handle != NULL);
 
+	items[0] = SPA_DICT_ITEM_INIT("clock.quantum-limit", "8192");
+
 	res = spa_handle_factory_init(factory,
 			ctx->convert_handle,
-			NULL,
+			&SPA_DICT_INIT(items, 1),
 			support, 1);
 	spa_assert_se(res >= 0);
 
@@ -510,6 +514,431 @@ static int test_set_in_format2(struct context *ctx)
 	return 0;
 }
 
+static int setup_direction(struct context *ctx, enum spa_direction direction, uint32_t mode,
+		struct spa_audio_info_raw *info)
+{
+	struct spa_pod_builder b = { 0 };
+	uint8_t buffer[1024];
+	struct spa_pod *param, *format;
+	int res;
+	uint32_t i;
+
+	spa_pod_builder_init(&b, buffer, sizeof(buffer));
+	format = spa_format_audio_raw_build(&b, SPA_PARAM_Format, info);
+
+	switch (mode) {
+	case SPA_PARAM_PORT_CONFIG_MODE_dsp:
+		param = spa_pod_builder_add_object(&b,
+			SPA_TYPE_OBJECT_ParamPortConfig, SPA_PARAM_PortConfig,
+			SPA_PARAM_PORT_CONFIG_direction,	SPA_POD_Id(direction),
+			SPA_PARAM_PORT_CONFIG_mode,		SPA_POD_Id(mode),
+			SPA_PARAM_PORT_CONFIG_format,		SPA_POD_Pod(format));
+		break;
+
+	case SPA_PARAM_PORT_CONFIG_MODE_convert:
+		param = spa_pod_builder_add_object(&b,
+			SPA_TYPE_OBJECT_ParamPortConfig, SPA_PARAM_PortConfig,
+			SPA_PARAM_PORT_CONFIG_direction,	SPA_POD_Id(direction),
+			SPA_PARAM_PORT_CONFIG_mode,		SPA_POD_Id(mode));
+		break;
+	default:
+		return -EINVAL;
+	}
+	res = spa_node_set_param(ctx->convert_node, SPA_PARAM_PortConfig, 0, param);
+	spa_assert_se(res == 0);
+
+	switch (mode) {
+	case SPA_PARAM_PORT_CONFIG_MODE_convert:
+		res = spa_node_port_set_param(ctx->convert_node, direction, 0,
+			SPA_PARAM_Format, 0, format);
+		spa_assert_se(res == 0);
+		break;
+	case SPA_PARAM_PORT_CONFIG_MODE_dsp:
+		spa_pod_builder_init(&b, buffer, sizeof(buffer));
+		format = spa_format_audio_dsp_build(&b, SPA_PARAM_Format,
+	                &SPA_AUDIO_INFO_DSP_INIT(
+				.format = SPA_AUDIO_FORMAT_F32P));
+		for (i = 0; i < info->channels; i++) {
+			res = spa_node_port_set_param(ctx->convert_node, direction, i,
+				SPA_PARAM_Format, 0, format);
+			spa_assert_se(res == 0);
+		}
+		break;
+	default:
+		return -EINVAL;
+	}
+	return 0;
+}
+
+struct buffer {
+	struct spa_buffer buffer;
+        struct spa_data datas[MAX_PORTS];
+        struct spa_chunk chunks[MAX_PORTS];
+};
+
+struct data {
+	uint32_t mode;
+	struct spa_audio_info_raw info;
+	uint32_t ports;
+	uint32_t planes;
+	const void *data[MAX_PORTS];
+	uint32_t size;
+};
+
+static int run_convert(struct context *ctx, struct data *in_data,
+		struct data *out_data)
+{
+	struct spa_command cmd;
+	int res;
+	uint32_t i, j, k;
+	struct buffer in_buffers[in_data->ports];
+	struct buffer out_buffers[out_data->ports];
+	struct spa_io_buffers in_io[in_data->ports];
+	struct spa_io_buffers out_io[out_data->ports];
+
+	setup_direction(ctx, SPA_DIRECTION_INPUT, in_data->mode, &in_data->info);
+	setup_direction(ctx, SPA_DIRECTION_OUTPUT, out_data->mode, &out_data->info);
+
+	cmd = SPA_NODE_COMMAND_INIT(SPA_NODE_COMMAND_Start);
+	res = spa_node_send_command(ctx->convert_node, &cmd);
+	spa_assert_se(res == 0);
+
+	for (i = 0, k = 0; i < in_data->ports; i++) {
+		struct buffer *b = &in_buffers[i];
+		struct spa_buffer *buffers[1];
+		spa_zero(*b);
+		b->buffer.datas = b->datas;
+                b->buffer.n_datas = in_data->planes;
+
+		for (j = 0; j < in_data->planes; j++, k++) {
+			b->datas[j].type = SPA_DATA_MemPtr;
+			b->datas[j].flags = 0;
+			b->datas[j].fd = -1;
+			b->datas[j].mapoffset = 0;
+			b->datas[j].maxsize = in_data->size;
+			b->datas[j].data = (void *)in_data->data[k];
+			b->datas[j].chunk = &b->chunks[j];
+			b->datas[j].chunk->offset = 0;
+			b->datas[j].chunk->size = in_data->size;
+			b->datas[j].chunk->stride = 0;
+		}
+		buffers[0] = &b->buffer;
+		res = spa_node_port_use_buffers(ctx->convert_node, SPA_DIRECTION_INPUT, i,
+				0, buffers, 1);
+		spa_assert_se(res == 0);
+
+		in_io[i].status = SPA_STATUS_HAVE_DATA;
+		in_io[i].buffer_id = 0;
+
+		res = spa_node_port_set_io(ctx->convert_node, SPA_DIRECTION_INPUT, i,
+				SPA_IO_Buffers, &in_io[i], sizeof(in_io[i]));
+		spa_assert_se(res == 0);
+	}
+	for (i = 0; i < out_data->ports; i++) {
+		struct buffer *b = &out_buffers[i];
+		struct spa_buffer *buffers[1];
+		spa_zero(*b);
+		b->buffer.datas = b->datas;
+                b->buffer.n_datas = out_data->planes;
+
+		for (j = 0; j < out_data->planes; j++) {
+			b->datas[j].type = SPA_DATA_MemPtr;
+			b->datas[j].flags = 0;
+			b->datas[j].fd = -1;
+			b->datas[j].mapoffset = 0;
+			b->datas[j].maxsize = out_data->size;
+			b->datas[j].data = calloc(1, out_data->size);
+			b->datas[j].chunk = &b->chunks[j];
+			b->datas[j].chunk->offset = 0;
+			b->datas[j].chunk->size = 0;
+			b->datas[j].chunk->stride = 0;
+		}
+		buffers[0] = &b->buffer;
+		res = spa_node_port_use_buffers(ctx->convert_node,
+				SPA_DIRECTION_OUTPUT, i, 0, buffers, 1);
+		spa_assert_se(res == 0);
+
+		out_io[i].status = SPA_STATUS_NEED_DATA;
+		out_io[i].buffer_id = -1;
+
+		res = spa_node_port_set_io(ctx->convert_node, SPA_DIRECTION_OUTPUT, i,
+				SPA_IO_Buffers, &out_io[i], sizeof(out_io[i]));
+		spa_assert_se(res == 0);
+	}
+
+	res = spa_node_process(ctx->convert_node);
+	spa_assert_se(res == (SPA_STATUS_NEED_DATA | SPA_STATUS_HAVE_DATA));
+
+	for (i = 0, k = 0; i < out_data->ports; i++) {
+		struct buffer *b = &out_buffers[i];
+
+		spa_assert_se(out_io[i].status == SPA_STATUS_HAVE_DATA);
+		spa_assert_se(out_io[i].buffer_id == 0);
+
+		for (j = 0; j < out_data->planes; j++, k++) {
+			spa_assert_se(b->datas[j].chunk->offset == 0);
+			spa_assert_se(b->datas[j].chunk->size == out_data->size);
+
+			res = memcmp(b->datas[j].data, out_data->data[k], out_data->size);
+			if (res != 0) {
+				fprintf(stderr, "error plane %d\n", j);
+				spa_debug_mem(0, b->datas[j].data, out_data->size);
+				spa_debug_mem(0, out_data->data[j], out_data->size);
+			}
+			spa_assert_se(res == 0);
+
+			free(b->datas[j].data);
+		}
+	}
+	cmd = SPA_NODE_COMMAND_INIT(SPA_NODE_COMMAND_Suspend);
+	res = spa_node_send_command(ctx->convert_node, &cmd);
+	spa_assert_se(res == 0);
+
+	return 0;
+}
+
+static const float data_f32p_1[] = { 0.1f, 0.1f, 0.1f, 0.1f };
+static const float data_f32p_2[] = { 0.2f, 0.2f, 0.2f, 0.2f };
+static const float data_f32p_3[] = { 0.3f, 0.3f, 0.3f, 0.3f };
+static const float data_f32p_4[] = { 0.4f, 0.4f, 0.4f, 0.4f };
+static const float data_f32p_5[] = { 0.5f, 0.5f, 0.5f, 0.5f };
+static const float data_f32p_6[] = { 0.6f, 0.6f, 0.6f, 0.6f };
+static const float data_f32p_7[] = { 0.7f, 0.7f, 0.7f, 0.7f };
+static const float data_f32p_8[] = { 0.8f, 0.8f, 0.8f, 0.8f };
+
+static const float data_f32_5p1[] = { 0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f,
+				      0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f,
+				      0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f,
+				      0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f };
+
+static const float data_f32_7p1_remapped[] = { 0.1f, 0.2f, 0.5f, 0.6f, 0.7f, 0.8f, 0.3f, 0.4f,
+				      0.1f, 0.2f, 0.5f, 0.6f, 0.7f, 0.8f, 0.3f, 0.4f,
+				      0.1f, 0.2f, 0.5f, 0.6f, 0.7f, 0.8f, 0.3f, 0.4f,
+				      0.1f, 0.2f, 0.5f, 0.6f, 0.7f, 0.8f, 0.3f, 0.4f };
+static const float data_f32_5p1_remapped[] = { 0.1f, 0.2f, 0.5f, 0.6f, 0.3f, 0.4f,
+				      0.1f, 0.2f, 0.5f, 0.6f, 0.3f, 0.4f,
+				      0.1f, 0.2f, 0.5f, 0.6f, 0.3f, 0.4f,
+				      0.1f, 0.2f, 0.5f, 0.6f, 0.3f, 0.4f };
+
+struct data dsp_5p1 = {
+	.mode = SPA_PARAM_PORT_CONFIG_MODE_dsp,
+	.info = SPA_AUDIO_INFO_RAW_INIT(
+		.format = SPA_AUDIO_FORMAT_F32,
+		.rate = 48000,
+		.channels = 6,
+		.position = {
+			SPA_AUDIO_CHANNEL_FL,
+			SPA_AUDIO_CHANNEL_FR,
+			SPA_AUDIO_CHANNEL_FC,
+			SPA_AUDIO_CHANNEL_LFE,
+			SPA_AUDIO_CHANNEL_RL,
+			SPA_AUDIO_CHANNEL_RR,
+		}),
+	.ports = 6,
+	.planes = 1,
+	.data = { data_f32p_1, data_f32p_2, data_f32p_3, data_f32p_4, data_f32p_5, data_f32p_6, },
+	.size = sizeof(float) * 4
+};
+
+struct data dsp_5p1_remapped = {
+	.mode = SPA_PARAM_PORT_CONFIG_MODE_dsp,
+	.info = SPA_AUDIO_INFO_RAW_INIT(
+		.format = SPA_AUDIO_FORMAT_F32,
+		.rate = 48000,
+		.channels = 6,
+		.position = {
+			SPA_AUDIO_CHANNEL_FL,
+			SPA_AUDIO_CHANNEL_FR,
+			SPA_AUDIO_CHANNEL_RL,
+			SPA_AUDIO_CHANNEL_RR,
+			SPA_AUDIO_CHANNEL_FC,
+			SPA_AUDIO_CHANNEL_LFE,
+		}),
+	.ports = 6,
+	.planes = 1,
+	.data = { data_f32p_1, data_f32p_2, data_f32p_5, data_f32p_6, data_f32p_3, data_f32p_4, },
+	.size = sizeof(float) * 4
+};
+
+struct data dsp_7p1_remapped = {
+	.mode = SPA_PARAM_PORT_CONFIG_MODE_dsp,
+	.info = SPA_AUDIO_INFO_RAW_INIT(
+		.format = SPA_AUDIO_FORMAT_F32,
+		.rate = 48000,
+		.channels = 8,
+		.position = {
+			SPA_AUDIO_CHANNEL_FL,
+			SPA_AUDIO_CHANNEL_FR,
+			SPA_AUDIO_CHANNEL_FC,
+			SPA_AUDIO_CHANNEL_LFE,
+			SPA_AUDIO_CHANNEL_RL,
+			SPA_AUDIO_CHANNEL_RR,
+			SPA_AUDIO_CHANNEL_SL,
+			SPA_AUDIO_CHANNEL_SR,
+		}),
+	.ports = 8,
+	.planes = 1,
+	.data = { data_f32p_1, data_f32p_2, data_f32p_3, data_f32p_4, data_f32p_7, data_f32p_8, data_f32p_5, data_f32p_6 },
+	.size = sizeof(data_f32p_1)
+};
+
+struct data dsp_5p1_remapped_2 = {
+	.mode = SPA_PARAM_PORT_CONFIG_MODE_dsp,
+	.info = SPA_AUDIO_INFO_RAW_INIT(
+		.format = SPA_AUDIO_FORMAT_F32,
+		.rate = 48000,
+		.channels = 6,
+		.position = {
+			SPA_AUDIO_CHANNEL_FC,
+			SPA_AUDIO_CHANNEL_LFE,
+			SPA_AUDIO_CHANNEL_RL,
+			SPA_AUDIO_CHANNEL_RR,
+			SPA_AUDIO_CHANNEL_FR,
+			SPA_AUDIO_CHANNEL_FL,
+		}),
+	.ports = 6,
+	.planes = 1,
+	.data = { data_f32p_3, data_f32p_4, data_f32p_5, data_f32p_6, data_f32p_2, data_f32p_1, },
+	.size = sizeof(float) * 4
+};
+
+struct data conv_f32_48000_5p1 = {
+	.mode = SPA_PARAM_PORT_CONFIG_MODE_convert,
+	.info = SPA_AUDIO_INFO_RAW_INIT(
+		.format = SPA_AUDIO_FORMAT_F32,
+		.rate = 48000,
+		.channels = 6,
+		.position = {
+			SPA_AUDIO_CHANNEL_FL,
+			SPA_AUDIO_CHANNEL_FR,
+			SPA_AUDIO_CHANNEL_FC,
+			SPA_AUDIO_CHANNEL_LFE,
+			SPA_AUDIO_CHANNEL_RL,
+			SPA_AUDIO_CHANNEL_RR,
+		}),
+	.ports = 1,
+	.planes = 1,
+	.data = { data_f32_5p1 },
+	.size = sizeof(data_f32_5p1)
+};
+
+struct data conv_f32_48000_5p1_remapped = {
+	.mode = SPA_PARAM_PORT_CONFIG_MODE_convert,
+	.info = SPA_AUDIO_INFO_RAW_INIT(
+		.format = SPA_AUDIO_FORMAT_F32,
+		.rate = 48000,
+		.channels = 6,
+		.position = {
+			SPA_AUDIO_CHANNEL_FL,
+			SPA_AUDIO_CHANNEL_FR,
+			SPA_AUDIO_CHANNEL_RL,
+			SPA_AUDIO_CHANNEL_RR,
+			SPA_AUDIO_CHANNEL_FC,
+			SPA_AUDIO_CHANNEL_LFE,
+		}),
+	.ports = 1,
+	.planes = 1,
+	.data = { data_f32_5p1_remapped },
+	.size = sizeof(data_f32_5p1_remapped)
+};
+
+struct data conv_f32p_48000_5p1 = {
+	.mode = SPA_PARAM_PORT_CONFIG_MODE_convert,
+	.info = SPA_AUDIO_INFO_RAW_INIT(
+		.format = SPA_AUDIO_FORMAT_F32P,
+		.rate = 48000,
+		.channels = 6,
+		.position = {
+			SPA_AUDIO_CHANNEL_FL,
+			SPA_AUDIO_CHANNEL_FR,
+			SPA_AUDIO_CHANNEL_FC,
+			SPA_AUDIO_CHANNEL_LFE,
+			SPA_AUDIO_CHANNEL_RL,
+			SPA_AUDIO_CHANNEL_RR,
+		}),
+	.ports = 1,
+	.planes = 6,
+	.data = { data_f32p_1, data_f32p_2, data_f32p_3, data_f32p_4, data_f32p_5, data_f32p_6, },
+	.size = sizeof(float) * 4
+};
+
+struct data conv_f32p_48000_5p1_remapped = {
+	.mode = SPA_PARAM_PORT_CONFIG_MODE_convert,
+	.info = SPA_AUDIO_INFO_RAW_INIT(
+		.format = SPA_AUDIO_FORMAT_F32P,
+		.rate = 48000,
+		.channels = 6,
+		.position = {
+			SPA_AUDIO_CHANNEL_FL,
+			SPA_AUDIO_CHANNEL_FR,
+			SPA_AUDIO_CHANNEL_RL,
+			SPA_AUDIO_CHANNEL_RR,
+			SPA_AUDIO_CHANNEL_FC,
+			SPA_AUDIO_CHANNEL_LFE,
+		}),
+	.ports = 1,
+	.planes = 6,
+	.data = { data_f32p_1, data_f32p_2, data_f32p_5, data_f32p_6, data_f32p_3, data_f32p_4, },
+	.size = sizeof(float) * 4
+};
+
+struct data conv_f32_48000_7p1_remapped = {
+	.mode = SPA_PARAM_PORT_CONFIG_MODE_convert,
+	.info = SPA_AUDIO_INFO_RAW_INIT(
+		.format = SPA_AUDIO_FORMAT_F32,
+		.rate = 48000,
+		.channels = 8,
+		.position = {
+			SPA_AUDIO_CHANNEL_FL,
+			SPA_AUDIO_CHANNEL_FR,
+			SPA_AUDIO_CHANNEL_SL,
+			SPA_AUDIO_CHANNEL_SR,
+			SPA_AUDIO_CHANNEL_RL,
+			SPA_AUDIO_CHANNEL_RR,
+			SPA_AUDIO_CHANNEL_FC,
+			SPA_AUDIO_CHANNEL_LFE,
+		}),
+	.ports = 1,
+	.planes = 1,
+	.data = { data_f32_7p1_remapped, },
+	.size = sizeof(data_f32_7p1_remapped)
+};
+
+static int test_convert_remap_dsp(struct context *ctx)
+{
+	run_convert(ctx, &dsp_5p1, &conv_f32_48000_5p1);
+	run_convert(ctx, &dsp_5p1, &conv_f32p_48000_5p1);
+	run_convert(ctx, &dsp_5p1, &conv_f32_48000_5p1_remapped);
+	run_convert(ctx, &dsp_5p1, &conv_f32p_48000_5p1_remapped);
+	run_convert(ctx, &dsp_5p1_remapped, &conv_f32_48000_5p1);
+	run_convert(ctx, &dsp_5p1_remapped, &conv_f32p_48000_5p1);
+	run_convert(ctx, &dsp_5p1_remapped, &conv_f32_48000_5p1_remapped);
+	run_convert(ctx, &dsp_5p1_remapped, &conv_f32p_48000_5p1_remapped);
+	run_convert(ctx, &dsp_5p1_remapped_2, &conv_f32_48000_5p1);
+	run_convert(ctx, &dsp_5p1_remapped_2, &conv_f32p_48000_5p1);
+	run_convert(ctx, &dsp_5p1_remapped_2, &conv_f32_48000_5p1_remapped);
+	run_convert(ctx, &dsp_5p1_remapped_2, &conv_f32p_48000_5p1_remapped);
+	return 0;
+}
+
+static int test_convert_remap_conv(struct context *ctx)
+{
+	run_convert(ctx, &conv_f32_48000_5p1, &dsp_5p1);
+	run_convert(ctx, &conv_f32_48000_5p1, &dsp_5p1_remapped);
+	run_convert(ctx, &conv_f32_48000_5p1, &dsp_5p1_remapped_2);
+	run_convert(ctx, &conv_f32p_48000_5p1, &dsp_5p1);
+	run_convert(ctx, &conv_f32p_48000_5p1, &dsp_5p1_remapped);
+	run_convert(ctx, &conv_f32p_48000_5p1, &dsp_5p1_remapped_2);
+	run_convert(ctx, &conv_f32_48000_5p1_remapped, &dsp_5p1);
+	run_convert(ctx, &conv_f32_48000_5p1_remapped, &dsp_5p1_remapped);
+	run_convert(ctx, &conv_f32_48000_5p1_remapped, &dsp_5p1_remapped_2);
+	run_convert(ctx, &conv_f32p_48000_5p1_remapped, &dsp_5p1);
+	run_convert(ctx, &conv_f32p_48000_5p1_remapped, &dsp_5p1_remapped);
+	run_convert(ctx, &conv_f32_48000_7p1_remapped, &dsp_7p1_remapped);
+	run_convert(ctx, &conv_f32p_48000_5p1_remapped, &dsp_5p1_remapped_2);
+	return 0;
+}
+
 int main(int argc, char *argv[])
 {
 	struct context ctx;
@@ -531,6 +960,9 @@ int main(int argc, char *argv[])
 	test_set_in_format2(&ctx);
 	test_set_out_format(&ctx);
 
+	test_convert_remap_dsp(&ctx);
+	test_convert_remap_conv(&ctx);
+
 	clean_context(&ctx);
 
 	return 0;
diff --git a/spa/plugins/audioconvert/test-fmt-ops.c b/spa/plugins/audioconvert/test-fmt-ops.c
index 48253b4a23659496943af431bdaf2b91ff8e59b4..8c8d4cdaca2579101231324f2fb6e7f06bac3a66 100644
--- a/spa/plugins/audioconvert/test-fmt-ops.c
+++ b/spa/plugins/audioconvert/test-fmt-ops.c
@@ -50,11 +50,11 @@ static void compare_mem(int i, int j, const void *m1, const void *m2, size_t siz
 {
 	int res = memcmp(m1, m2, size);
 	if (res != 0) {
-		fprintf(stderr, "%d %d:\n", i, j);
+		fprintf(stderr, "%d %d %zd:\n", i, j, size);
 		spa_debug_mem(0, m1, size);
 		spa_debug_mem(0, m2, size);
 	}
-//	spa_assert_se(res == 0);
+	spa_assert_se(res == 0);
 }
 
 static void run_test(const char *name,
@@ -81,19 +81,19 @@ static void run_test(const char *name,
 		tp[0] = temp_in;
 		switch(in_size) {
 		case 1:
-			conv_interleave_8_c(&conv, tp, ip, N_SAMPLES);
+			conv_8d_to_8_c(&conv, tp, ip, N_SAMPLES);
 			break;
 		case 2:
-			conv_interleave_16_c(&conv, tp, ip, N_SAMPLES);
+			conv_16d_to_16_c(&conv, tp, ip, N_SAMPLES);
 			break;
 		case 3:
-			conv_interleave_24_c(&conv, tp, ip, N_SAMPLES);
+			conv_24d_to_24_c(&conv, tp, ip, N_SAMPLES);
 			break;
 		case 4:
-			conv_interleave_32_c(&conv, tp, ip, N_SAMPLES);
+			conv_32d_to_32_c(&conv, tp, ip, N_SAMPLES);
 			break;
 		case 8:
-			conv_interleave_64_c(&conv, tp, ip, N_SAMPLES);
+			conv_64d_to_64_c(&conv, tp, ip, N_SAMPLES);
 			break;
 		default:
 			fprintf(stderr, "unknown size %zd\n", in_size);
@@ -125,10 +125,42 @@ static void run_test(const char *name,
 	}
 }
 
+static void test_f32_s8(void)
+{
+	static const float in[] = { 0.0f, 1.0f, -1.0f, 0.5f, -0.5f, 1.1f, -1.1f,
+		1.0f/160.f, 1.0f/256.f, -1.0f/160.f, -1.0f/256.f };
+	static const int8_t out[] = { 0, 127, -128, 64, 192, 127, -128, 1, 0, -1, 0 };
+
+	run_test("test_f32_s8", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+			true, true, conv_f32_to_s8_c);
+	run_test("test_f32d_s8", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+			false, true, conv_f32d_to_s8_c);
+	run_test("test_f32_s8d", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+			true, false, conv_f32_to_s8d_c);
+	run_test("test_f32d_s8d", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+			false, false, conv_f32d_to_s8d_c);
+}
+
+static void test_s8_f32(void)
+{
+	static const int8_t in[] = { 0, 127, -128, 64, 192, };
+	static const float out[] = { 0.0f, 0.9921875f, -1.0f, 0.5f, -0.5f, };
+
+	run_test("test_s8_f32", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+			true, true, conv_s8_to_f32_c);
+	run_test("test_s8d_f32", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+			false, true, conv_s8d_to_f32_c);
+	run_test("test_s8_f32d", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+			true, false, conv_s8_to_f32d_c);
+	run_test("test_s8d_f32d", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+			false, false, conv_s8d_to_f32d_c);
+}
+
 static void test_f32_u8(void)
 {
-	static const float in[] = { 0.0f, 1.0f, -1.0f, 0.5f, -0.5f, 1.1f, -1.1f };
-	static const uint8_t out[] = { 128, 255, 0, 191, 64, 255, 0, };
+	static const float in[] = { 0.0f, 1.0f, -1.0f, 0.5f, -0.5f, 1.1f, -1.1f,
+		1.0f/160.f, 1.0f/256.f, -1.0f/160.f, -1.0f/256.f };
+	static const uint8_t out[] = { 128, 255, 0, 192, 64, 255, 0, 129, 128, 127, 128 };
 
 	run_test("test_f32_u8", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
 			true, true, conv_f32_to_u8_c);
@@ -157,8 +189,10 @@ static void test_u8_f32(void)
 
 static void test_f32_u16(void)
 {
-	static const float in[] = { 0.0f, 1.0f, -1.0f, 0.5f, -0.5f, 1.1f, -1.1f };
-	static const uint16_t out[] = { 32767, 65535, 0, 49150, 16383, 65535, 0 };
+	static const float in[] = { 0.0f, 1.0f, -1.0f, 0.5f, -0.5f, 1.1f, -1.1f,
+		1.0f/49152.f, 1.0f/65536.f, -1.0f/49152.f, -1.0f/65536.f };
+	static const uint16_t out[] = { 32768, 65535, 0, 49152, 16384, 65535, 0,
+		32769, 32768, 32767, 32768 };
 
 	run_test("test_f32_u16", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
 			true, true, conv_f32_to_u16_c);
@@ -168,8 +202,8 @@ static void test_f32_u16(void)
 
 static void test_u16_f32(void)
 {
-	static const uint16_t in[] = { 32767, 65535, 0, 49150, 16383, };
-	static const float out[] = { 0.0f, 1.0f, -1.0f, 0.4999847412f, -0.4999847412f };
+	static const uint16_t in[] = { 32768, 65535, 0, 49152, 16384, };
+	static const float out[] = { 0.0f, 0.999969482422f, -1.0f, 0.5f, -0.5f };
 
 	run_test("test_u16_f32d", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
 			true, false, conv_u16_to_f32d_c);
@@ -179,8 +213,10 @@ static void test_u16_f32(void)
 
 static void test_f32_s16(void)
 {
-	static const float in[] = { 0.0f, 1.0f, -1.0f, 0.5f, -0.5f, 1.1f, -1.1f };
-	static const int16_t out[] = { 0, 32767, -32767, 16383, -16383, 32767, -32767 };
+	static const float in[] = { 0.0f, 1.0f, -1.0f, 0.5f, -0.5f, 1.1f, -1.1f,
+		1.0f/49152.f, 1.0f/65536.f, -1.0f/49152.f, -1.0f/65536.f };
+	static const int16_t out[] = { 0, 32767, -32768, 16384, -16384, 32767, -32768,
+		1, 0, -1, 0 };
 
 	run_test("test_f32_s16", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
 			true, true, conv_f32_to_s16_c);
@@ -192,16 +228,32 @@ static void test_f32_s16(void)
 			false, false, conv_f32d_to_s16d_c);
 #if defined(HAVE_SSE2)
 	if (cpu_flags & SPA_CPU_FLAG_SSE2) {
+		run_test("test_f32_s16_sse2", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+			true, true, conv_f32_to_s16_sse2);
 		run_test("test_f32d_s16_sse2", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
 			false, true, conv_f32d_to_s16_sse2);
+		run_test("test_f32d_s16d_sse2", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+			false, false, conv_f32d_to_s16d_sse2);
+	}
+#endif
+#if defined(HAVE_AVX2)
+	if (cpu_flags & SPA_CPU_FLAG_AVX2) {
+		run_test("test_f32d_s16_avx2", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+			false, true, conv_f32d_to_s16_avx2);
+	}
+#endif
+#if defined(HAVE_NEON)
+	if (cpu_flags & SPA_CPU_FLAG_NEON) {
+		run_test("test_f32d_s16_neon", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+			false, true, conv_f32d_to_s16_neon);
 	}
 #endif
 }
 
 static void test_s16_f32(void)
 {
-	static const int16_t in[] = { 0, 32767, -32767, 16383, -16383, };
-	static const float out[] = { 0.0f, 1.0f, -1.0f, 0.4999847412f, -0.4999847412f };
+	static const int16_t in[] = { 0, 32767, -32768, 16384, -16384, };
+	static const float out[] = { 0.0f, 0.999969482422f, -1.0f, 0.5f, -0.5f };
 
 	run_test("test_s16_f32d", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
 			true, false, conv_s16_to_f32d_c);
@@ -217,13 +269,27 @@ static void test_s16_f32(void)
 			true, false, conv_s16_to_f32d_sse2);
 	}
 #endif
+#if defined(HAVE_AVX2)
+	if (cpu_flags & SPA_CPU_FLAG_AVX2) {
+		run_test("test_s16_f32d_avx2", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+			true, false, conv_s16_to_f32d_avx2);
+	}
+#endif
+#if defined(HAVE_NEON)
+	if (cpu_flags & SPA_CPU_FLAG_NEON) {
+		run_test("test_s16_f32d_neon", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+			true, false, conv_s16_to_f32d_neon);
+	}
+#endif
 }
 
 static void test_f32_u32(void)
 {
-	static const float in[] = { 0.0f, 1.0f, -1.0f, 0.5f, -0.5f, 1.1f, -1.1f };
-	static const uint32_t out[] = { 0, 0x7fffff00, 0x80000100, 0x3fffff00, 0xc0000100,
-					0x7fffff00, 0x80000100 };
+	static const float in[] = { 0.0f, 1.0f, -1.0f, 0.5f, -0.5f, 1.1f, -1.1f,
+		1.0f/0xa00000, 1.0f/0x1000000, -1.0f/0xa00000, -1.0f/0x1000000 };
+	static const uint32_t out[] = { 0x80000000, 0xffffff00, 0x0, 0xc0000000, 0x40000000,
+					0xffffff00, 0x0,
+		0x80000100, 0x80000000, 0x7fffff00, 0x80000000 };
 
 	run_test("test_f32_u32", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
 			true, true, conv_f32_to_u32_c);
@@ -233,8 +299,8 @@ static void test_f32_u32(void)
 
 static void test_u32_f32(void)
 {
-	static const uint32_t in[] = { 0, 0x7fffff00, 0x80000100, 0x3fffff00, 0xc0000100 };
-	static const float out[] = { 0.0f, 1.0f, -1.0f, 0.4999999404f, -0.4999999404f, };
+	static const uint32_t in[] = { 0x80000000, 0xffffff00, 0x0, 0xc0000000, 0x40000000 };
+	static const float out[] = { 0.0f, 0.999999880791f, -1.0f, 0.5f, -0.5f, };
 
 	run_test("test_u32_f32d", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
 			true, false, conv_u32_to_f32d_c);
@@ -244,9 +310,11 @@ static void test_u32_f32(void)
 
 static void test_f32_s32(void)
 {
-	static const float in[] = { 0.0f, 1.0f, -1.0f, 0.5f, -0.5f, 1.1f, -1.1f };
-	static const int32_t out[] = { 0, 0x7fffff00, 0x80000100, 0x3fffff00, 0xc0000100,
-					0x7fffff00, 0x80000100 };
+	static const float in[] = { 0.0f, 1.0f, -1.0f, 0.5f, -0.5f, 1.1f, -1.1f,
+		1.0f/0xa00000, 1.0f/0x1000000, -1.0f/0xa00000, -1.0f/0x1000000 };
+	static const int32_t out[] = { 0, 0x7fffff00, 0x80000000, 0x40000000, 0xc0000000,
+					0x7fffff00, 0x80000000,
+		0x00000100, 0x00000000, 0xffffff00, 0x00000000 };
 
 	run_test("test_f32_s32", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
 			true, true, conv_f32_to_s32_c);
@@ -262,12 +330,18 @@ static void test_f32_s32(void)
 			false, true, conv_f32d_to_s32_sse2);
 	}
 #endif
+#if defined(HAVE_AVX2)
+	if (cpu_flags & SPA_CPU_FLAG_AVX2) {
+		run_test("test_f32d_s32_avx2", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+			false, true, conv_f32d_to_s32_avx2);
+	}
+#endif
 }
 
 static void test_s32_f32(void)
 {
-	static const int32_t in[] = { 0, 0x7fffff00, 0x80000100, 0x3fffff00, 0xc0000100 };
-	static const float out[] = { 0.0f, 1.0f, -1.0f, 0.4999999404f, -0.4999999404f, };
+	static const int32_t in[] = { 0, 0x7fffff00, 0x80000000, 0x40000000, 0xc0000000 };
+	static const float out[] = { 0.0f, 0.999999880791, -1.0f, 0.5, -0.5, };
 
 	run_test("test_s32_f32d", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
 			true, false, conv_s32_to_f32d_c);
@@ -283,18 +357,23 @@ static void test_s32_f32(void)
 			true, false, conv_s32_to_f32d_sse2);
 	}
 #endif
+#if defined(HAVE_AVX2)
+	if (cpu_flags & SPA_CPU_FLAG_AVX2) {
+		run_test("test_s32_f32d_avx2", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+			true, false, conv_s32_to_f32d_avx2);
+	}
+#endif
 }
 
 static void test_f32_u24(void)
 {
-	static const float in[] = { 0.0f, 1.0f, -1.0f, 0.5f, -0.5f, 1.1f, -1.1f };
-#if __BYTE_ORDER == __LITTLE_ENDIAN
-	static const uint8_t out[] = { 0x00, 0x00, 0x00, 0xff, 0xff, 0x7f, 0x01, 0x00, 0x80,
-		0xff, 0xff, 0x3f, 0x01, 0x00, 0xc0, 0xff, 0xff, 0x7f, 0x01, 0x00, 0x80 };
-#else
-	static const uint8_t out[] = { 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0x80, 0x00, 0x01,
-		0x3f, 0xff, 0xff, 0xc0, 0x00, 0x01, 0x7f, 0xff, 0xff, 0x80, 0x00, 0x01 };
-#endif
+	static const float in[] = { 0.0f, 1.0f, -1.0f, 0.5f, -0.5f, 1.1f, -1.1f,
+		1.0f/0xa00000, 1.0f/0x1000000, -1.0f/0xa00000, -1.0f/0x1000000 };
+	static const uint24_t out[] = { U32_TO_U24(0x00800000), U32_TO_U24(0xffffff),
+		U32_TO_U24(0x000000), U32_TO_U24(0xc00000), U32_TO_U24(0x400000),
+		U32_TO_U24(0xffffff), U32_TO_U24(0x000000),
+		U32_TO_U24(0x800001), U32_TO_U24(0x800000), U32_TO_U24(0x7fffff),
+		U32_TO_U24(0x800000) };
 
 	run_test("test_f32_u24", in, sizeof(in[0]), out, 3, SPA_N_ELEMENTS(in),
 			true, true, conv_f32_to_u24_c);
@@ -304,14 +383,9 @@ static void test_f32_u24(void)
 
 static void test_u24_f32(void)
 {
-#if __BYTE_ORDER == __LITTLE_ENDIAN
-	static const uint8_t in[] = { 0x00, 0x00, 0x00, 0xff, 0xff, 0x7f, 0x01, 0x00, 0x80,
-		0xff, 0xff, 0x3f, 0x01, 0x00, 0xc0,  };
-#else
-	static const uint8_t in[] = { 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0x80, 0x00, 0x01,
-		0x3f, 0xff, 0xff, 0xc0, 0x00, 0x01,  };
-#endif
-	static const float out[] = { 0.0f, 1.0f, -1.0f, 0.4999999404f, -0.4999999404f, };
+	static const uint24_t in[] = { U32_TO_U24(0x00800000), U32_TO_U24(0xffffff),
+		U32_TO_U24(0x000000), U32_TO_U24(0xc00000), U32_TO_U24(0x400000) };
+	static const float out[] = { 0.0f, 0.999999880791f, -1.0f, 0.5, -0.5, };
 
 	run_test("test_u24_f32d", in, 3, out, sizeof(out[0]), SPA_N_ELEMENTS(out),
 			true, false, conv_u24_to_f32d_c);
@@ -321,14 +395,13 @@ static void test_u24_f32(void)
 
 static void test_f32_s24(void)
 {
-	static const float in[] = { 0.0f, 1.0f, -1.0f, 0.5f, -0.5f, 1.1f, -1.1f };
-#if __BYTE_ORDER == __LITTLE_ENDIAN
-	static const uint8_t out[] = { 0x00, 0x00, 0x00, 0xff, 0xff, 0x7f, 0x01, 0x00, 0x80,
-		0xff, 0xff, 0x3f, 0x01, 0x00, 0xc0, 0xff, 0xff, 0x7f, 0x01, 0x00, 0x80 };
-#else
-	static const uint8_t out[] = { 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0x80, 0x00, 0x01,
-		0x3f, 0xff, 0xff, 0xc0, 0x00, 0x01, 0x7f, 0xff, 0xff, 0x80, 0x00, 0x01 };
-#endif
+	static const float in[] = { 0.0f, 1.0f, -1.0f, 0.5f, -0.5f, 1.1f, -1.1f,
+		1.0f/0xa00000, 1.0f/0x1000000, -1.0f/0xa00000, -1.0f/0x1000000 };
+	static const int24_t out[] = { S32_TO_S24(0), S32_TO_S24(0x7fffff),
+		S32_TO_S24(0xff800000), S32_TO_S24(0x400000), S32_TO_S24(0xc00000),
+		S32_TO_S24(0x7fffff), S32_TO_S24(0xff800000),
+		S32_TO_S24(0x000001), S32_TO_S24(0x000000), S32_TO_S24(0xffffffff),
+		S32_TO_S24(0x000000) };
 
 	run_test("test_f32_s24", in, sizeof(in[0]), out, 3, SPA_N_ELEMENTS(in),
 			true, true, conv_f32_to_s24_c);
@@ -342,14 +415,9 @@ static void test_f32_s24(void)
 
 static void test_s24_f32(void)
 {
-#if __BYTE_ORDER == __LITTLE_ENDIAN
-	static const uint8_t in[] = { 0x00, 0x00, 0x00, 0xff, 0xff, 0x7f, 0x01, 0x00, 0x80,
-		0xff, 0xff, 0x3f, 0x01, 0x00, 0xc0,  };
-#else
-	static const uint8_t in[] = { 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0x80, 0x00, 0x01,
-		0x3f, 0xff, 0xff, 0xc0, 0x00, 0x01,  };
-#endif
-	static const float out[] = { 0.0f, 1.0f, -1.0f, 0.4999999404f, -0.4999999404f, };
+	static const int24_t in[] = { S32_TO_S24(0), S32_TO_S24(0x7fffff),
+		S32_TO_S24(0xff800000), S32_TO_S24(0x400000), S32_TO_S24(0xc00000) };
+	static const float out[] = { 0.0f, 0.999999880791f, -1.0f, 0.5f, -0.5f, };
 
 	run_test("test_s24_f32d", in, 3, out, sizeof(out[0]), SPA_N_ELEMENTS(out),
 			true, false, conv_s24_to_f32d_c);
@@ -377,13 +445,21 @@ static void test_s24_f32(void)
 			true, false, conv_s24_to_f32d_sse41);
 	}
 #endif
+#if defined(HAVE_AVX2)
+	if (cpu_flags & SPA_CPU_FLAG_AVX2) {
+		run_test("test_s24_f32d_avx2", in, 3, out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+			true, false, conv_s24_to_f32d_avx2);
+	}
+#endif
 }
 
 static void test_f32_u24_32(void)
 {
-	static const float in[] = { 0.0f, 1.0f, -1.0f, 0.5f, -0.5f, 1.1f, -1.1f };
-	static const uint32_t out[] = { 0, 0x7fffff, 0xff800001, 0x3fffff, 0xffc00001,
-					0x7fffff, 0xff800001 };
+	static const float in[] = { 0.0f, 1.0f, -1.0f, 0.5f, -0.5f, 1.1f, -1.1f,
+		1.0f/0xa00000, 1.0f/0x1000000, -1.0f/0xa00000, -1.0f/0x1000000 };
+	static const uint32_t out[] = { 0x800000, 0xffffff, 0x0, 0xc00000, 0x400000,
+					0xffffff, 0x000000,
+		0x800001, 0x800000, 0x7fffff, 0x800000 };
 
 	run_test("test_f32_u24_32", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
 			true, true, conv_f32_to_u24_32_c);
@@ -393,8 +469,8 @@ static void test_f32_u24_32(void)
 
 static void test_u24_32_f32(void)
 {
-	static const uint32_t in[] = { 0, 0x7fffff, 0xff800001, 0x3fffff, 0xffc00001 };
-	static const float out[] = { 0.0f, 1.0f, -1.0f, 0.4999999404f, -0.4999999404f, };
+	static const uint32_t in[] = { 0x800000, 0xffffff, 0x0, 0xc00000, 0x400000, 0x11000000 };
+	static const float out[] = { 0.0f, 0.999999880791f, -1.0f, 0.5f, -0.5f, -1.0f };
 
 	run_test("test_u24_32_f32d", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
 			true, false, conv_u24_32_to_f32d_c);
@@ -404,9 +480,11 @@ static void test_u24_32_f32(void)
 
 static void test_f32_s24_32(void)
 {
-	static const float in[] = { 0.0f, 1.0f, -1.0f, 0.5f, -0.5f, 1.1f, -1.1f };
-	static const int32_t out[] = { 0, 0x7fffff, 0xff800001, 0x3fffff, 0xffc00001,
-					0x7fffff, 0xff800001 };
+	static const float in[] = { 0.0f, 1.0f, -1.0f, 0.5f, -0.5f, 1.1f, -1.1f,
+		1.0f/0xa00000, 1.0f/0x1000000, -1.0f/0xa00000, -1.0f/0x1000000 };
+	static const int32_t out[] = { 0, 0x7fffff, 0xff800000, 0x400000, 0xffc00000,
+					0x7fffff, 0xff800000,
+		0x000001, 0x000000, 0xffffffff, 0x000000 };
 
 	run_test("test_f32_s24_32", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
 			true, true, conv_f32_to_s24_32_c);
@@ -420,8 +498,8 @@ static void test_f32_s24_32(void)
 
 static void test_s24_32_f32(void)
 {
-	static const int32_t in[] = { 0, 0x7fffff, 0xff800001, 0x3fffff, 0xffc00001 };
-	static const float out[] = { 0.0f, 1.0f, -1.0f, 0.4999999404f, -0.4999999404f, };
+	static const int32_t in[] = { 0, 0x7fffff, 0xff800000, 0x400000, 0xffc00000, 0x66800000 };
+	static const float out[] = { 0.0f, 0.999999880791f, -1.0f, 0.5f, -0.5f, -1.0f };
 
 	run_test("test_s24_32_f32d", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
 			true, false, conv_s24_32_to_f32d_c);
@@ -435,8 +513,8 @@ static void test_s24_32_f32(void)
 
 static void test_f64_f32(void)
 {
-	static const double in[] = { 0.0, 1.0, -1.0, 0.4999999404, -0.4999999404, };
-	static const float out[] = { 0.0f, 1.0f, -1.0f, 0.4999999404f, -0.4999999404f, };
+	static const double in[] = { 0.0, 1.0, -1.0, 0.5, -0.5, };
+	static const float out[] = { 0.0, 1.0, -1.0, 0.5, -0.5, };
 
 	run_test("test_f64_f32d", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
 			true, false, conv_f64_to_f32d_c);
@@ -451,7 +529,7 @@ static void test_f64_f32(void)
 static void test_f32_f64(void)
 {
 	static const float in[] = { 0.0f, 1.0f, -1.0f, 0.5f, -0.5f, 1.1f, -1.1f };
-	static const double out[] = { 0.0, 1.0, -1.0, 0.5, -0.5, 1.1, -1.1 };
+	static const double out[] = { 0.0f, 1.0f, -1.0f, 0.5f, -0.5f, 1.1f, -1.1f };
 
 	run_test("test_f32_f64", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
 			true, true, conv_f32_to_f64_c);
@@ -462,11 +540,226 @@ static void test_f32_f64(void)
 	run_test("test_f32d_f64d", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
 			false, false, conv_f32d_to_f64d_c);
 }
+
+static void test_lossless_s8(void)
+{
+	int8_t i;
+
+	fprintf(stderr, "test %s:\n", __func__);
+	for (i = S8_MIN; i < S8_MAX; i+=1) {
+		float v = S8_TO_F32(i);
+		int8_t t = F32_TO_S8(v);
+		spa_assert_se(i == t);
+	}
+}
+
+static void test_lossless_u8(void)
+{
+	uint8_t i;
+
+	fprintf(stderr, "test %s:\n", __func__);
+	for (i = U8_MIN; i < U8_MAX; i+=1) {
+		float v = U8_TO_F32(i);
+		uint8_t t = F32_TO_U8(v);
+		spa_assert_se(i == t);
+	}
+}
+static void test_lossless_s16(void)
+{
+	int16_t i;
+
+	fprintf(stderr, "test %s:\n", __func__);
+	for (i = S16_MIN; i < S16_MAX; i+=3) {
+		float v = S16_TO_F32(i);
+		int16_t t = F32_TO_S16(v);
+		spa_assert_se(i == t);
+	}
+}
+
+static void test_lossless_u16(void)
+{
+	uint32_t i;
+
+	fprintf(stderr, "test %s:\n", __func__);
+	for (i = U16_MIN; i < U16_MAX; i+=3) {
+		float v = U16_TO_F32(i);
+		uint16_t t = F32_TO_U16(v);
+		spa_assert_se(i == t);
+	}
+}
+
+static void test_lossless_s24(void)
+{
+	int32_t i;
+
+	fprintf(stderr, "test %s:\n", __func__);
+	for (i = S24_MIN; i < S24_MAX; i+=13) {
+		float v = S24_TO_F32(s32_to_s24(i));
+		int32_t t = s24_to_s32(F32_TO_S24(v));
+		spa_assert_se(i == t);
+	}
+}
+
+static void test_lossless_u24(void)
+{
+	uint32_t i;
+
+	fprintf(stderr, "test %s:\n", __func__);
+	for (i = U24_MIN; i < U24_MAX; i+=11) {
+		float v = U24_TO_F32(u32_to_u24(i));
+		uint32_t t = u24_to_u32(F32_TO_U24(v));
+		spa_assert_se(i == t);
+	}
+}
+
+static void test_lossless_s32(void)
+{
+	int32_t i;
+
+	fprintf(stderr, "test %s:\n", __func__);
+	for (i = S32_MIN; i < S32_MAX; i+=255) {
+		float v = S32_TO_F32(i);
+		int32_t t = F32_TO_S32(v);
+		spa_assert_se(SPA_ABS(i - t) <= 256);
+	}
+}
+
+static void test_lossless_u32(void)
+{
+	uint32_t i;
+
+	fprintf(stderr, "test %s:\n", __func__);
+	for (i = U32_MIN; i < U32_MAX; i+=255) {
+		float v = U32_TO_F32(i);
+		uint32_t t = F32_TO_U32(v);
+		spa_assert_se(i > t ? (i - t) <= 256 : (t - i) <= 256);
+	}
+}
+
+static void test_swaps(void)
+{
+	{
+		uint24_t v = U32_TO_U24(0x123456);
+		uint24_t t = U32_TO_U24(0x563412);
+		uint24_t s = bswap_u24(v);
+		spa_assert_se(memcmp(&s, &t, sizeof(t)) == 0);
+	}
+	{
+		int24_t v = S32_TO_S24(0xfffe1dc0);
+		int24_t t = S32_TO_S24(0xffc01dfe);
+		int24_t s = bswap_s24(v);
+		spa_assert_se(memcmp(&s, &t, sizeof(t)) == 0);
+	}
+	{
+		int24_t v = S32_TO_S24(0x123456);
+		int24_t t = S32_TO_S24(0x563412);
+		int24_t s = bswap_s24(v);
+		spa_assert_se(memcmp(&s, &t, sizeof(t)) == 0);
+	}
+}
+
+static void run_test_noise(uint32_t fmt, uint32_t noise, uint32_t flags)
+{
+	struct convert conv;
+	const void *ip[N_CHANNELS];
+	void *op[N_CHANNELS];
+	uint32_t i, range;
+	bool all_zero;
+
+	spa_zero(conv);
+
+	conv.noise_bits = noise;
+	conv.src_fmt = SPA_AUDIO_FORMAT_F32P;
+	conv.dst_fmt = fmt;
+	conv.n_channels = 2;
+	conv.rate = 44100;
+	conv.cpu_flags = flags;
+	spa_assert_se(convert_init(&conv) == 0);
+	fprintf(stderr, "test noise %s:\n", conv.func_name);
+
+	memset(samp_in, 0, sizeof(samp_in));
+	for (i = 0; i < conv.n_channels; i++) {
+		ip[i] = samp_in;
+		op[i] = samp_out;
+	}
+	convert_process(&conv, op, ip, N_SAMPLES);
+
+	range = 1 << conv.noise_bits;
+
+	all_zero = true;
+	for (i = 0; i < conv.n_channels * N_SAMPLES; i++) {
+		switch (fmt) {
+		case SPA_AUDIO_FORMAT_S8:
+		{
+			int8_t *d = (int8_t *)samp_out;
+			if (d[i] != 0)
+				all_zero = false;
+			spa_assert_se(SPA_ABS(d[i] - 0) <= (int8_t)range);
+			break;
+		}
+		case SPA_AUDIO_FORMAT_U8:
+		{
+			uint8_t *d = (uint8_t *)samp_out;
+			if (d[i] != 0x80)
+				all_zero = false;
+			spa_assert_se((int8_t)SPA_ABS(d[i] - 0x80) <= (int8_t)(range<<1));
+			break;
+		}
+		case SPA_AUDIO_FORMAT_S16:
+		{
+			int16_t *d = (int16_t *)samp_out;
+			if (d[i] != 0)
+				all_zero = false;
+			spa_assert_se(SPA_ABS(d[i] - 0) <= (int16_t)range);
+			break;
+		}
+		case SPA_AUDIO_FORMAT_S24:
+		{
+			int24_t *d = (int24_t *)samp_out;
+			int32_t t = s24_to_s32(d[i]);
+			if (t != 0)
+				all_zero = false;
+			spa_assert_se(SPA_ABS(t - 0) <= (int32_t)range);
+			break;
+		}
+		case SPA_AUDIO_FORMAT_S32:
+		{
+			int32_t *d = (int32_t *)samp_out;
+			if (d[i] != 0)
+				all_zero = false;
+			spa_assert_se(SPA_ABS(d[i] - 0) <= (int32_t)(range << 8));
+			break;
+		}
+		default:
+			spa_assert_not_reached();
+			break;
+		}
+	}
+	spa_assert_se(all_zero == false);
+	convert_free(&conv);
+}
+
+static void test_noise(void)
+{
+	run_test_noise(SPA_AUDIO_FORMAT_S8, 1, 0);
+	run_test_noise(SPA_AUDIO_FORMAT_S8, 2, 0);
+	run_test_noise(SPA_AUDIO_FORMAT_U8, 1, 0);
+	run_test_noise(SPA_AUDIO_FORMAT_U8, 2, 0);
+	run_test_noise(SPA_AUDIO_FORMAT_S16, 1, 0);
+	run_test_noise(SPA_AUDIO_FORMAT_S16, 2, 0);
+	run_test_noise(SPA_AUDIO_FORMAT_S24, 1, 0);
+	run_test_noise(SPA_AUDIO_FORMAT_S24, 2, 0);
+	run_test_noise(SPA_AUDIO_FORMAT_S32, 1, 0);
+	run_test_noise(SPA_AUDIO_FORMAT_S32, 2, 0);
+}
+
 int main(int argc, char *argv[])
 {
 	cpu_flags = get_cpu_flags();
-	printf("got get CPU flags %d\n", cpu_flags);
+	printf("got CPU flags %d\n", cpu_flags);
 
+	test_f32_s8();
+	test_s8_f32();
 	test_f32_u8();
 	test_u8_f32();
 	test_f32_u16();
@@ -487,5 +780,19 @@ int main(int argc, char *argv[])
 	test_s24_32_f32();
 	test_f32_f64();
 	test_f64_f32();
+
+	test_lossless_s8();
+	test_lossless_u8();
+	test_lossless_s16();
+	test_lossless_u16();
+	test_lossless_s24();
+	test_lossless_u24();
+	test_lossless_s32();
+	test_lossless_u32();
+
+	test_swaps();
+
+	test_noise();
+
 	return 0;
 }
diff --git a/spa/plugins/audioconvert/test-source.c b/spa/plugins/audioconvert/test-source.c
index 3687ddf19c53973cc6567ac5170ccf6b4d471210..55a65c5d554ab88736174358a579d543efdb293d 100644
--- a/spa/plugins/audioconvert/test-source.c
+++ b/spa/plugins/audioconvert/test-source.c
@@ -749,9 +749,8 @@ static int impl_node_process(void *object)
 	spa_return_val_if_fail(this != NULL, -EINVAL);
 
 	port = GET_OUT_PORT(this, 0);
-
-	io = port->io;
-	spa_return_val_if_fail(io != NULL, -EIO);
+	if ((io = port->io) == NULL)
+		return -EIO;
 
 	spa_log_trace_fp(this->log, NAME " %p: status %d", this, io->status);
 
diff --git a/spa/plugins/audioconvert/volume-ops.c b/spa/plugins/audioconvert/volume-ops.c
index 32a0d22cb2bc34debdeef01a5181f1e225cc86c8..98887e46f5217b708bc2f76cf0b09fada094ba1d 100644
--- a/spa/plugins/audioconvert/volume-ops.c
+++ b/spa/plugins/audioconvert/volume-ops.c
@@ -36,16 +36,21 @@
 typedef void (*volume_func_t) (struct volume *vol, void * SPA_RESTRICT dst,
 			const void * SPA_RESTRICT src, float volume, uint32_t n_samples);
 
+#define MAKE(func,...) \
+	{ func, #func , __VA_ARGS__ }
+
 static const struct volume_info {
 	volume_func_t process;
+	const char *name;
 	uint32_t cpu_flags;
 } volume_table[] =
 {
 #if defined (HAVE_SSE)
-	{ volume_f32_sse, SPA_CPU_FLAG_SSE },
+	MAKE(volume_f32_sse, SPA_CPU_FLAG_SSE),
 #endif
-	{ volume_f32_c, 0 },
+	MAKE(volume_f32_c),
 };
+#undef MAKE
 
 #define MATCH_CPU_FLAGS(a,b)	((a) == 0 || ((a) & (b)) == a)
 
@@ -73,6 +78,8 @@ int volume_init(struct volume *vol)
 	if (info == NULL)
 		return -ENOTSUP;
 
+	vol->cpu_flags = info->cpu_flags;
+	vol->func_name = info->name;
 	vol->free = impl_volume_free;
 	vol->process = info->process;
 	return 0;
diff --git a/spa/plugins/audioconvert/volume-ops.h b/spa/plugins/audioconvert/volume-ops.h
index 4b3f763e44c60ce3049a8a5928bb4dac672155ce..0825712903a15167d93e771bd3e213f313e2bebe 100644
--- a/spa/plugins/audioconvert/volume-ops.h
+++ b/spa/plugins/audioconvert/volume-ops.h
@@ -33,6 +33,7 @@
 
 struct volume {
 	uint32_t cpu_flags;
+	const char *func_name;
 
 	struct spa_log *log;
 
diff --git a/spa/plugins/audiomixer/audiomixer.c b/spa/plugins/audiomixer/audiomixer.c
index 1dbf51b28ae8c3d4fae0acda3b14394dd428ac71..5e682bba5d8c666fc46beacda2eb896a67f6904f 100644
--- a/spa/plugins/audiomixer/audiomixer.c
+++ b/spa/plugins/audiomixer/audiomixer.c
@@ -730,8 +730,8 @@ static int impl_node_process(void *object)
 	spa_return_val_if_fail(this != NULL, -EINVAL);
 
 	outport = GET_OUT_PORT(this, 0);
-	outio = outport->io;
-	spa_return_val_if_fail(outio != NULL, -EIO);
+	if ((outio = outport->io) == NULL)
+		return -EIO;
 
 	spa_log_trace_fp(this->log, "%p: status %p %d %d",
 			this, outio, outio->status, outio->buffer_id);
@@ -755,6 +755,8 @@ static int impl_node_process(void *object)
 		struct port *inport = GET_IN_PORT(this, i);
 		struct spa_io_buffers *inio = NULL;
 		struct buffer *inb;
+		struct spa_data *bd;
+		uint32_t size, offs;
 
 		if (SPA_UNLIKELY(!PORT_VALID(inport) ||
 		    (inio = inport->io) == NULL ||
@@ -770,13 +772,20 @@ static int impl_node_process(void *object)
 		}
 
 		inb = &inport->buffers[inio->buffer_id];
-		maxsize = SPA_MIN(inb->buffer->datas[0].chunk->size, maxsize);
+		bd = &inb->buffer->datas[0];
 
-		spa_log_trace_fp(this->log, "%p: mix input %d %p->%p %d %d %d", this,
-				i, inio, outio, inio->status, inio->buffer_id, maxsize);
+		offs = SPA_MIN(bd->chunk->offset, bd->maxsize);
+		size = SPA_MIN(bd->maxsize - offs, bd->chunk->size);
+		maxsize = SPA_MIN(size, maxsize);
 
-		datas[n_buffers] = inb->buffer->datas[0].data;
-		buffers[n_buffers++] = inb;
+		spa_log_trace_fp(this->log, "%p: mix input %d %p->%p %d %d %d:%d", this,
+				i, inio, outio, inio->status, inio->buffer_id,
+				offs, size);
+
+		if (!SPA_FLAG_IS_SET(bd->chunk->flags, SPA_CHUNK_FLAG_EMPTY)) {
+			datas[n_buffers] = SPA_PTROFF(bd->data, offs, void);
+			buffers[n_buffers++] = inb;
+		}
 		inio->status = SPA_STATUS_NEED_DATA;
 	}
 
@@ -788,8 +797,7 @@ static int impl_node_process(void *object)
 
 	if (n_buffers == 1) {
 		*outb->buffer = *buffers[0]->buffer;
-	}
-	else {
+	} else {
 		struct spa_data *d = outb->buf.datas;
 
 		*outb->buffer = outb->buf;
@@ -799,6 +807,7 @@ static int impl_node_process(void *object)
 		d[0].chunk->offset = 0;
 		d[0].chunk->size = maxsize;
 		d[0].chunk->stride = this->stride;
+		SPA_FLAG_UPDATE(d[0].chunk->flags, SPA_CHUNK_FLAG_EMPTY, n_buffers == 0);
 
 		mix_ops_process(&this->ops, d[0].data,
 				datas, n_buffers, maxsize / this->stride);
diff --git a/spa/plugins/audiomixer/benchmark-mix-ops.c b/spa/plugins/audiomixer/benchmark-mix-ops.c
new file mode 100644
index 0000000000000000000000000000000000000000..e698417b616d171ecca9675a65ecfcf5aa8b2882
--- /dev/null
+++ b/spa/plugins/audiomixer/benchmark-mix-ops.c
@@ -0,0 +1,222 @@
+/* Spa
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <string.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <errno.h>
+#include <time.h>
+
+#include "test-helper.h"
+#include "mix-ops.h"
+
+static uint32_t cpu_flags;
+
+typedef void (*mix_func_t) (struct mix_ops *ops, void * SPA_RESTRICT dst,
+		const void * SPA_RESTRICT src[], uint32_t n_src, uint32_t n_samples);
+struct stats {
+	uint32_t n_samples;
+	uint32_t n_src;
+	uint64_t perf;
+	const char *name;
+	const char *impl;
+};
+
+#define MAX_SAMPLES	4096
+#define MAX_SRC		11
+
+#define MAX_COUNT 100
+
+static uint8_t samp_in[MAX_SAMPLES * MAX_SRC * 8];
+static uint8_t samp_out[MAX_SAMPLES * 8];
+
+static const int sample_sizes[] = { 0, 1, 128, 513, 4096 };
+static const int src_counts[] = { 1, 2, 4, 6, 8, 11 };
+
+#define MAX_RESULTS	SPA_N_ELEMENTS(sample_sizes) * SPA_N_ELEMENTS(src_counts) * 70
+
+static uint32_t n_results = 0;
+static struct stats results[MAX_RESULTS];
+
+static void run_test1(const char *name, const char *impl, mix_func_t func, int n_src, int n_samples)
+{
+	int i, j;
+	const void *ip[n_src];
+	void *op;
+	struct timespec ts;
+	uint64_t count, t1, t2;
+	struct mix_ops mix;
+
+	mix.n_channels = 1;
+
+	for (j = 0; j < n_src; j++)
+		ip[j] = SPA_PTR_ALIGN(&samp_in[j * n_samples * 4], 32, void);
+	op = SPA_PTR_ALIGN(samp_out, 32, void);
+
+	clock_gettime(CLOCK_MONOTONIC, &ts);
+	t1 = SPA_TIMESPEC_TO_NSEC(&ts);
+
+	count = 0;
+	for (i = 0; i < MAX_COUNT; i++) {
+		func(&mix, op, ip, n_src, n_samples);
+		count++;
+	}
+	clock_gettime(CLOCK_MONOTONIC, &ts);
+	t2 = SPA_TIMESPEC_TO_NSEC(&ts);
+
+	spa_assert(n_results < MAX_RESULTS);
+
+	results[n_results++] = (struct stats) {
+		.n_samples = n_samples,
+		.n_src = n_src,
+		.perf = count * (uint64_t)SPA_NSEC_PER_SEC / (t2 - t1),
+		.name = name,
+		.impl = impl
+	};
+}
+
+static void run_test(const char *name, const char *impl, mix_func_t func)
+{
+	size_t i, j;
+
+	for (i = 0; i < SPA_N_ELEMENTS(sample_sizes); i++) {
+		for (j = 0; j < SPA_N_ELEMENTS(src_counts); j++) {
+			run_test1(name, impl, func, src_counts[j],
+				(sample_sizes[i] + (src_counts[j] -1)) / src_counts[j]);
+		}
+	}
+}
+
+static void test_s8(void)
+{
+	run_test("test_s8", "c", mix_s8_c);
+}
+static void test_u8(void)
+{
+	run_test("test_u8", "c", mix_u8_c);
+}
+
+static void test_s16(void)
+{
+	run_test("test_s16", "c", mix_s16_c);
+}
+static void test_u16(void)
+{
+	run_test("test_u8", "c", mix_u16_c);
+}
+
+static void test_s24(void)
+{
+	run_test("test_s24", "c", mix_s24_c);
+}
+static void test_u24(void)
+{
+	run_test("test_u24", "c", mix_u24_c);
+}
+static void test_s24_32(void)
+{
+	run_test("test_s24_32", "c", mix_s24_32_c);
+}
+static void test_u24_32(void)
+{
+	run_test("test_u24_32", "c", mix_u24_32_c);
+}
+
+static void test_s32(void)
+{
+	run_test("test_s32", "c", mix_s32_c);
+}
+static void test_u32(void)
+{
+	run_test("test_u32", "c", mix_u32_c);
+}
+
+static void test_f32(void)
+{
+	run_test("test_f32", "c", mix_f32_c);
+#if defined (HAVE_SSE)
+	if (cpu_flags & SPA_CPU_FLAG_SSE) {
+		run_test("test_f32", "sse", mix_f32_sse);
+	}
+#endif
+#if defined (HAVE_AVX)
+	if (cpu_flags & SPA_CPU_FLAG_AVX) {
+		run_test("test_f32", "avx", mix_f32_avx);
+	}
+#endif
+}
+
+static void test_f64(void)
+{
+	run_test("test_f64", "c", mix_f64_c);
+#if defined (HAVE_SSE2)
+	if (cpu_flags & SPA_CPU_FLAG_SSE2) {
+		run_test("test_f64", "sse2", mix_f64_sse2);
+	}
+#endif
+}
+
+static int compare_func(const void *_a, const void *_b)
+{
+	const struct stats *a = _a, *b = _b;
+	int diff;
+	if ((diff = strcmp(a->name, b->name)) != 0) return diff;
+	if ((diff = a->n_samples - b->n_samples) != 0) return diff;
+	if ((diff = a->n_src - b->n_src) != 0) return diff;
+	if ((diff = b->perf - a->perf) != 0) return diff;
+	return 0;
+}
+
+int main(int argc, char *argv[])
+{
+	uint32_t i;
+
+	cpu_flags = get_cpu_flags();
+	printf("got get CPU flags %d\n", cpu_flags);
+
+	test_s8();
+	test_u8();
+	test_s16();
+	test_u16();
+	test_s24();
+	test_u24();
+	test_s32();
+	test_u32();
+	test_s24_32();
+	test_u24_32();
+	test_f32();
+	test_f64();
+
+	qsort(results, n_results, sizeof(struct stats), compare_func);
+
+	for (i = 0; i < n_results; i++) {
+		struct stats *s = &results[i];
+		fprintf(stderr, "%-12."PRIu64" \t%-32.32s %s \t samples %d, src %d\n",
+				s->perf, s->name, s->impl, s->n_samples, s->n_src);
+	}
+	return 0;
+}
diff --git a/spa/plugins/audiomixer/meson.build b/spa/plugins/audiomixer/meson.build
index 9e1d12d599334655eee10c5c133481a2953f8db1..5a4a1fbfe2fe9c18a701a5479fd89fb6394d363b 100644
--- a/spa/plugins/audiomixer/meson.build
+++ b/spa/plugins/audiomixer/meson.build
@@ -1,6 +1,5 @@
 audiomixer_sources = [
   'audiomixer.c',
-  'mix-ops.c',
   'mixer-dsp.c',
   'plugin.c'
 ]
@@ -47,11 +46,81 @@ if have_avx and have_fma
   simd_dependencies += audiomixer_avx
 endif
 
-audiomixerlib = shared_library('spa-audiomixer',
+audiomixer_lib = static_library('audiomixer',
+  ['mix-ops.c' ],
+  c_args : [ simd_cargs, '-O3'],
+  link_with : simd_dependencies,
+  include_directories : [configinc],
+  dependencies : [ spa_dep ],
+  install : false
+  )
+audiomixer_dep = declare_dependency(link_with: audiomixer_lib)
+
+spa_audiomixer_lib = shared_library('spa-audiomixer',
   audiomixer_sources,
   c_args : simd_cargs,
   link_with : simd_dependencies,
-  dependencies : [ spa_dep, mathlib ],
+  dependencies : [ spa_dep, mathlib, audiomixer_dep ],
   install : true,
   install_dir : spa_plugindir / 'audiomixer'
 )
+spa_audiomixer_dep = declare_dependency(link_with: spa_audiomixer_lib)
+
+test_apps = [
+  'test-mix-ops',
+  ]
+
+foreach a : test_apps
+  test(a,
+    executable(a, a + '.c',
+      dependencies : [ spa_dep, dl_lib, pthread_lib, mathlib, audiomixer_dep ],
+      include_directories : [ configinc ],
+      link_with : [ test_lib ],
+      install_rpath : spa_plugindir / 'audiomixer',
+      c_args : [ simd_cargs ],
+      install : installed_tests_enabled,
+      install_dir : installed_tests_execdir / 'audiomixer'),
+      env : [
+        'SPA_PLUGIN_DIR=@0@'.format(spa_dep.get_variable('plugindir')),
+        ])
+
+    if installed_tests_enabled
+      test_conf = configuration_data()
+      test_conf.set('exec', installed_tests_execdir / 'audiomixer' / a)
+      configure_file(
+        input: installed_tests_template,
+        output: a + '.test',
+        install_dir: installed_tests_metadir / 'audiomixer',
+        configuration: test_conf
+        )
+  endif
+endforeach
+
+benchmark_apps = [
+  'benchmark-mix-ops',
+  ]
+
+foreach a : benchmark_apps
+  benchmark(a,
+    executable(a, a + '.c',
+      dependencies : [ spa_dep, dl_lib, pthread_lib, mathlib, audiomixer_dep ],
+      include_directories : [ configinc ],
+      c_args : [ simd_cargs ],
+      install_rpath : spa_plugindir / 'audiomixer',
+      install : installed_tests_enabled,
+      install_dir : installed_tests_execdir / 'audiomixer'),
+      env : [
+        'SPA_PLUGIN_DIR=@0@'.format(spa_dep.get_variable('plugindir')),
+        ])
+
+    if installed_tests_enabled
+      test_conf = configuration_data()
+      test_conf.set('exec', installed_tests_execdir / 'audiomixer' / a)
+      configure_file(
+        input: installed_tests_template,
+        output: a + '.test',
+        install_dir: installed_tests_metadir / 'audiomixer',
+        configuration: test_conf
+        )
+  endif
+endforeach
diff --git a/spa/plugins/audiomixer/mix-ops-avx.c b/spa/plugins/audiomixer/mix-ops-avx.c
index b3884246334b54ff216588dc1fe36862a6ee5867..a5e3b5b11763874aa123a3dd05e22c82c9ca4691 100644
--- a/spa/plugins/audiomixer/mix-ops-avx.c
+++ b/spa/plugins/audiomixer/mix-ops-avx.c
@@ -86,50 +86,59 @@ static inline void mix_4(float * dst,
 
 static inline void mix_2(float * dst, const float * SPA_RESTRICT src, uint32_t n_samples)
 {
-	uint32_t n, unrolled;
-
-	if (SPA_IS_ALIGNED(src, 32) &&
-	    SPA_IS_ALIGNED(dst, 32))
-		unrolled = n_samples & ~15;
-	else
-		unrolled = 0;
-
-	for (n = 0; n < unrolled; n += 16) {
-		__m256 in1[2], in2[2];
-
-		in1[0] = _mm256_load_ps(&dst[n + 0]);
-		in1[1] = _mm256_load_ps(&dst[n + 8]);
-		in2[0] = _mm256_load_ps(&src[n + 0]);
-		in2[1] = _mm256_load_ps(&src[n + 8]);
-
-		in1[0] = _mm256_add_ps(in1[0], in2[0]);
-		in1[1] = _mm256_add_ps(in1[1], in2[1]);
-
-		_mm256_store_ps(&dst[n + 0], in1[0]);
-		_mm256_store_ps(&dst[n + 8], in1[1]);
-	}
-	for (; n < n_samples; n++) {
-		__m128 in1[1], in2[1];
-		in1[0] = _mm_load_ss(&dst[n]),
-		in2[0] = _mm_load_ss(&src[n]),
-		in1[0] = _mm_add_ss(in1[0], in2[0]);
-		_mm_store_ss(&dst[n], in1[0]);
-	}
 }
 
 void
 mix_f32_avx(struct mix_ops *ops, void * SPA_RESTRICT dst, const void * SPA_RESTRICT src[],
 		uint32_t n_src, uint32_t n_samples)
 {
-	uint32_t i;
+	n_samples *= ops->n_channels;
 
 	if (n_src == 0)
 		memset(dst, 0, n_samples * ops->n_channels * sizeof(float));
-	else if (dst != src[0])
-		spa_memcpy(dst, src[0], n_samples * ops->n_channels * sizeof(float));
-
-	for (i = 1; i + 2 < n_src; i += 3)
-		mix_4(dst, src[i], src[i + 1], src[i + 2], n_samples);
-	for (; i < n_src; i++)
-		mix_2(dst, src[i], n_samples * ops->n_channels);
+	else if (n_src == 1) {
+		if (dst != src[0])
+			spa_memcpy(dst, src[0], n_samples * sizeof(float));
+	} else {
+		uint32_t i, n, unrolled;
+		const float **s = (const float **)src;
+		float *d = dst;
+
+		if (SPA_LIKELY(SPA_IS_ALIGNED(dst, 32))) {
+			unrolled = n_samples & ~31;
+			for (i = 0; i < n_src; i++) {
+				if (SPA_UNLIKELY(!SPA_IS_ALIGNED(src[i], 32))) {
+					unrolled = 0;
+					break;
+				}
+			}
+		} else
+			unrolled = 0;
+
+		for (n = 0; n < unrolled; n += 32) {
+			__m256 in[4];
+
+			in[0] = _mm256_load_ps(&s[0][n +  0]);
+			in[1] = _mm256_load_ps(&s[0][n +  8]);
+			in[2] = _mm256_load_ps(&s[0][n + 16]);
+			in[3] = _mm256_load_ps(&s[0][n + 24]);
+			for (i = 1; i < n_src; i++) {
+				in[0] = _mm256_add_ps(in[0], _mm256_load_ps(&s[i][n +  0]));
+				in[1] = _mm256_add_ps(in[1], _mm256_load_ps(&s[i][n +  8]));
+				in[2] = _mm256_add_ps(in[2], _mm256_load_ps(&s[i][n + 16]));
+				in[3] = _mm256_add_ps(in[3], _mm256_load_ps(&s[i][n + 24]));
+			}
+			_mm256_store_ps(&d[n +  0], in[0]);
+			_mm256_store_ps(&d[n +  8], in[1]);
+			_mm256_store_ps(&d[n + 16], in[2]);
+			_mm256_store_ps(&d[n + 24], in[3]);
+		}
+		for (; n < n_samples; n++) {
+			__m128 in[1];
+			in[0] = _mm_load_ss(&s[0][n]);
+			for (i = 1; i < n_src; i++)
+				in[0] = _mm_add_ss(in[0], _mm_load_ss(&s[i][n]));
+			_mm_store_ss(&d[n], in[0]);
+		}
+	}
 }
diff --git a/spa/plugins/audiomixer/mix-ops-c.c b/spa/plugins/audiomixer/mix-ops-c.c
index e2d82e8c1d679a78c4133b83e5c0379fda0653a3..2f79cd80f748f1c533a413c9f710bdf008bab9b8 100644
--- a/spa/plugins/audiomixer/mix-ops-c.c
+++ b/spa/plugins/audiomixer/mix-ops-c.c
@@ -30,236 +30,39 @@
 
 #include "mix-ops.h"
 
-void
-mix_s8_c(struct mix_ops *ops, void * SPA_RESTRICT dst, const void * SPA_RESTRICT src[],
-		uint32_t n_src, uint32_t n_samples)
-{
-	uint32_t i, n;
-	int8_t *d = dst;
-
-	if (n_src == 0)
-		memset(dst, 0, n_samples * ops->n_channels * sizeof(int8_t));
-	else if (dst != src[0])
-		spa_memcpy(dst, src[0], n_samples * ops->n_channels * sizeof(int8_t));
-
-	for (i = 1; i < n_src; i++) {
-		const int8_t *s = src[i];
-		for (n = 0; n < n_samples * ops->n_channels; n++)
-			d[n] = S8_MIX(d[n], s[n]);
-	}
-}
-
-void
-mix_u8_c(struct mix_ops *ops, void * SPA_RESTRICT dst, const void * SPA_RESTRICT src[],
-		uint32_t n_src, uint32_t n_samples)
-{
-	uint32_t i, n;
-	uint8_t *d = dst;
-
-	if (n_src == 0)
-		memset(dst, 0, n_samples * ops->n_channels * sizeof(uint8_t));
-	else if (dst != src[0])
-		spa_memcpy(dst, src[0], n_samples * ops->n_channels * sizeof(uint8_t));
-
-	for (i = 1; i < n_src; i++) {
-		const uint8_t *s = src[i];
-		for (n = 0; n < n_samples * ops->n_channels; n++)
-			d[n] = U8_MIX(d[n], s[n]);
-	}
+#define MAKE_FUNC(name,type,atype,accum,clamp,zero)				\
+void mix_ ##name## _c(struct mix_ops *ops,					\
+		void * SPA_RESTRICT dst, const void * SPA_RESTRICT src[],	\
+                uint32_t n_src, uint32_t n_samples)				\
+{										\
+	uint32_t i, n;								\
+	type *d = dst;								\
+	const type **s = (const type **)src;					\
+	n_samples *= ops->n_channels;						\
+	if (n_src == 0 && zero)							\
+		memset(dst, 0, n_samples * sizeof(type));			\
+	else if (n_src == 1) {							\
+		if (dst != src[0])						\
+			spa_memcpy(dst, src[0], n_samples * sizeof(type));	\
+	} else {								\
+		for (n = 0; n < n_samples; n++) {				\
+			atype ac = 0;						\
+			for (i = 0; i < n_src; i++)				\
+				ac = accum (ac, s[i][n]);			\
+			d[n] = clamp (ac);					\
+		}								\
+	}									\
 }
 
-void
-mix_s16_c(struct mix_ops *ops, void * SPA_RESTRICT dst, const void * SPA_RESTRICT src[],
-		uint32_t n_src, uint32_t n_samples)
-{
-	uint32_t i, n;
-	int16_t *d = dst;
-
-	if (n_src == 0)
-		memset(dst, 0, n_samples * ops->n_channels * sizeof(int16_t));
-	else if (dst != src[0])
-		spa_memcpy(dst, src[0], n_samples * ops->n_channels * sizeof(int16_t));
-
-	for (i = 1; i < n_src; i++) {
-		const int16_t *s = src[i];
-		for (n = 0; n < n_samples * ops->n_channels; n++)
-			d[n] = S16_MIX(d[n], s[n]);
-	}
-}
-
-void
-mix_u16_c(struct mix_ops *ops, void * SPA_RESTRICT dst, const void * SPA_RESTRICT src[],
-		uint32_t n_src, uint32_t n_samples)
-{
-	uint32_t i, n;
-	uint16_t *d = dst;
-
-	if (n_src == 0)
-		memset(dst, 0, n_samples * ops->n_channels * sizeof(uint16_t));
-	else if (dst != src[0])
-		spa_memcpy(dst, src[0], n_samples * ops->n_channels * sizeof(uint16_t));
-
-	for (i = 1; i < n_src; i++) {
-		const uint16_t *s = src[i];
-		for (n = 0; n < n_samples * ops->n_channels; n++)
-			d[n] = U16_MIX(d[n], s[n]);
-	}
-}
-
-void
-mix_s24_c(struct mix_ops *ops, void * SPA_RESTRICT dst, const void * SPA_RESTRICT src[],
-		uint32_t n_src, uint32_t n_samples)
-{
-	uint32_t i, n;
-	uint8_t *d = dst;
-
-	if (n_src == 0)
-		memset(dst, 0, n_samples * ops->n_channels * sizeof(uint8_t) * 3);
-	else if (dst != src[0])
-		spa_memcpy(dst, src[0], n_samples * ops->n_channels * sizeof(uint8_t) * 3);
-
-	for (i = 1; i < n_src; i++) {
-		const uint8_t *s = src[i];
-		for (n = 0; n < n_samples * ops->n_channels; n++) {
-			write_s24(d, S24_MIX(read_s24(d), read_s24(s)));
-			d += 3;
-			s += 3;
-		}
-	}
-}
-
-void
-mix_u24_c(struct mix_ops *ops, void * SPA_RESTRICT dst, const void * SPA_RESTRICT src[],
-		uint32_t n_src, uint32_t n_samples)
-{
-	uint32_t i, n;
-	uint8_t *d = dst;
-
-	if (n_src == 0)
-		memset(dst, 0, n_samples * ops->n_channels * sizeof(uint8_t) * 3);
-	else if (dst != src[0])
-		spa_memcpy(dst, src[0], n_samples * ops->n_channels * sizeof(uint8_t) * 3);
-
-	for (i = 1; i < n_src; i++) {
-		const uint8_t *s = src[i];
-		for (n = 0; n < n_samples * ops->n_channels; n++) {
-			write_u24(d, U24_MIX(read_u24(d), read_u24(s)));
-			d += 3;
-			s += 3;
-		}
-	}
-}
-
-void
-mix_s32_c(struct mix_ops *ops, void * SPA_RESTRICT dst, const void * SPA_RESTRICT src[],
-		uint32_t n_src, uint32_t n_samples)
-{
-	uint32_t i, n;
-	int32_t *d = dst;
-
-	if (n_src == 0)
-		memset(dst, 0, n_samples * ops->n_channels * sizeof(int32_t));
-	else if (dst != src[0])
-		spa_memcpy(dst, src[0], n_samples * ops->n_channels * sizeof(int32_t));
-
-	for (i = 1; i < n_src; i++) {
-		const int32_t *s = src[i];
-		for (n = 0; n < n_samples * ops->n_channels; n++)
-			d[n] = S32_MIX(d[n], s[n]);
-	}
-}
-
-void
-mix_u32_c(struct mix_ops *ops, void * SPA_RESTRICT dst, const void * SPA_RESTRICT src[],
-		uint32_t n_src, uint32_t n_samples)
-{
-	uint32_t i, n;
-	uint32_t *d = dst;
-
-	if (n_src == 0)
-		memset(dst, 0, n_samples * ops->n_channels * sizeof(uint32_t));
-	else if (dst != src[0])
-		spa_memcpy(dst, src[0], n_samples * ops->n_channels * sizeof(uint32_t));
-
-	for (i = 1; i < n_src; i++) {
-		const uint32_t *s = src[i];
-		for (n = 0; n < n_samples * ops->n_channels; n++)
-			d[n] = U32_MIX(d[n], s[n]);
-	}
-}
-
-void
-mix_s24_32_c(struct mix_ops *ops, void * SPA_RESTRICT dst, const void * SPA_RESTRICT src[],
-		uint32_t n_src, uint32_t n_samples)
-{
-	uint32_t i, n;
-	int32_t *d = dst;
-
-	if (n_src == 0)
-		memset(dst, 0, n_samples * ops->n_channels * sizeof(int32_t));
-	else if (dst != src[0])
-		spa_memcpy(dst, src[0], n_samples * ops->n_channels * sizeof(int32_t));
-
-	for (i = 1; i < n_src; i++) {
-		const int32_t *s = src[i];
-		for (n = 0; n < n_samples * ops->n_channels; n++)
-			d[n] = S24_32_MIX(d[n], s[n]);
-	}
-}
-
-void
-mix_u24_32_c(struct mix_ops *ops, void * SPA_RESTRICT dst, const void * SPA_RESTRICT src[],
-		uint32_t n_src, uint32_t n_samples)
-{
-	uint32_t i, n;
-	uint32_t *d = dst;
-
-	if (n_src == 0)
-		memset(dst, 0, n_samples * ops->n_channels * sizeof(uint32_t));
-	else if (dst != src[0])
-		spa_memcpy(dst, src[0], n_samples * ops->n_channels * sizeof(uint32_t));
-
-	for (i = 1; i < n_src; i++) {
-		const uint32_t *s = src[i];
-		for (n = 0; n < n_samples * ops->n_channels; n++)
-			d[n] = U24_32_MIX(d[n], s[n]);
-	}
-}
-
-void
-mix_f32_c(struct mix_ops *ops, void * SPA_RESTRICT dst, const void * SPA_RESTRICT src[],
-		uint32_t n_src, uint32_t n_samples)
-{
-	uint32_t i, n;
-	float *d = dst;
-
-	if (n_src == 0)
-		memset(dst, 0, n_samples * ops->n_channels * sizeof(float));
-	else if (dst != src[0])
-		spa_memcpy(dst, src[0], n_samples * ops->n_channels * sizeof(float));
-
-	for (i = 1; i < n_src; i++) {
-		const float *s = src[i];
-		for (n = 0; n < n_samples * ops->n_channels; n++)
-			d[n] = F32_MIX(d[n], s[n]);
-	}
-}
-
-void
-mix_f64_c(struct mix_ops *ops, void * SPA_RESTRICT dst, const void * SPA_RESTRICT src[],
-		uint32_t n_src, uint32_t n_samples)
-{
-	uint32_t i, n;
-	double *d = dst;
-
-	if (n_src == 0)
-		memset(dst, 0, n_samples * ops->n_channels * sizeof(double));
-	else if (dst != src[0])
-		spa_memcpy(dst, src[0], n_samples * ops->n_channels * sizeof(double));
-
-	for (i = 1; i < n_src; i++) {
-		const double *s = src[i];
-		for (n = 0; n < n_samples * ops->n_channels; n++)
-			d[n] = F64_MIX(d[n], s[n]);
-	}
-}
+MAKE_FUNC(s8, int8_t, int16_t, S8_ACCUM, S8_CLAMP, true);
+MAKE_FUNC(u8, uint8_t, int16_t, U8_ACCUM, U8_CLAMP, false);
+MAKE_FUNC(s16, int16_t, int32_t, S16_ACCUM, S16_CLAMP, true);
+MAKE_FUNC(u16, uint16_t, int16_t, U16_ACCUM, U16_CLAMP, false);
+MAKE_FUNC(s24, int24_t, int32_t, S24_ACCUM, S24_CLAMP, false);
+MAKE_FUNC(u24, uint24_t, int32_t, U24_ACCUM, U24_CLAMP, false);
+MAKE_FUNC(s32, int32_t, int64_t, S32_ACCUM, S32_CLAMP, true);
+MAKE_FUNC(u32, uint32_t, int64_t, U32_ACCUM, U32_CLAMP, false);
+MAKE_FUNC(s24_32, int32_t, int32_t, S24_32_ACCUM, S24_32_CLAMP, true);
+MAKE_FUNC(u24_32, uint32_t, int32_t, U24_32_ACCUM, U24_32_CLAMP, false);
+MAKE_FUNC(f32, float, float, F32_ACCUM, F32_CLAMP, true);
+MAKE_FUNC(f64, double, double, F64_ACCUM, F64_CLAMP, true);
diff --git a/spa/plugins/audiomixer/mix-ops-sse.c b/spa/plugins/audiomixer/mix-ops-sse.c
index cd6e05fc106a41f3fb6523ad1e099150665a0eb0..bae619bad19326e76dcd6138185d5fddd7c10404 100644
--- a/spa/plugins/audiomixer/mix-ops-sse.c
+++ b/spa/plugins/audiomixer/mix-ops-sse.c
@@ -32,58 +32,56 @@
 
 #include <xmmintrin.h>
 
-static inline void mix_2(float * dst, const float * SPA_RESTRICT src, uint32_t n_samples)
-{
-	uint32_t n, unrolled;
-	__m128 in1[4], in2[4];
-
-	if (SPA_LIKELY(SPA_IS_ALIGNED(src, 16) &&
-	    SPA_IS_ALIGNED(dst, 16)))
-		unrolled = n_samples & ~15;
-	else
-		unrolled = 0;
-
-	for (n = 0; n < unrolled; n += 16) {
-		in1[0] = _mm_load_ps(&dst[n+ 0]);
-		in1[1] = _mm_load_ps(&dst[n+ 4]);
-		in1[2] = _mm_load_ps(&dst[n+ 8]);
-		in1[3] = _mm_load_ps(&dst[n+12]);
-
-		in2[0] = _mm_load_ps(&src[n+ 0]);
-		in2[1] = _mm_load_ps(&src[n+ 4]);
-		in2[2] = _mm_load_ps(&src[n+ 8]);
-		in2[3] = _mm_load_ps(&src[n+12]);
-
-		in1[0] = _mm_add_ps(in1[0], in2[0]);
-		in1[1] = _mm_add_ps(in1[1], in2[1]);
-		in1[2] = _mm_add_ps(in1[2], in2[2]);
-		in1[3] = _mm_add_ps(in1[3], in2[3]);
-
-		_mm_store_ps(&dst[n+ 0], in1[0]);
-		_mm_store_ps(&dst[n+ 4], in1[1]);
-		_mm_store_ps(&dst[n+ 8], in1[2]);
-		_mm_store_ps(&dst[n+12], in1[3]);
-	}
-	for (; n < n_samples; n++) {
-		in1[0] = _mm_load_ss(&dst[n]),
-		in2[0] = _mm_load_ss(&src[n]),
-		in1[0] = _mm_add_ss(in1[0], in2[0]);
-		_mm_store_ss(&dst[n], in1[0]);
-	}
-}
-
 void
 mix_f32_sse(struct mix_ops *ops, void * SPA_RESTRICT dst, const void * SPA_RESTRICT src[],
 		uint32_t n_src, uint32_t n_samples)
 {
-	uint32_t i;
+	n_samples *= ops->n_channels;
+
+	if (n_src == 0) {
+		memset(dst, 0, n_samples * sizeof(float));
+	} else if (n_src == 1) {
+		if (dst != src[0])
+			spa_memcpy(dst, src[0], n_samples * sizeof(float));
+	} else {
+		uint32_t n, i, unrolled;
+		__m128 in[4];
+		const float **s = (const float **)src;
+		float *d = dst;
+
+		if (SPA_LIKELY(SPA_IS_ALIGNED(dst, 16))) {
+			unrolled = n_samples & ~15;
+			for (i = 0; i < n_src; i++) {
+				if (SPA_UNLIKELY(!SPA_IS_ALIGNED(src[i], 16))) {
+					unrolled = 0;
+					break;
+				}
+			}
+		} else
+			unrolled = 0;
 
-	if (n_src == 0)
-		memset(dst, 0, n_samples * ops->n_channels * sizeof(float));
-	else if (dst != src[0])
-		spa_memcpy(dst, src[0], n_samples * ops->n_channels * sizeof(float));
+		for (n = 0; n < unrolled; n += 16) {
+			in[0] = _mm_load_ps(&s[0][n+ 0]);
+			in[1] = _mm_load_ps(&s[0][n+ 4]);
+			in[2] = _mm_load_ps(&s[0][n+ 8]);
+			in[3] = _mm_load_ps(&s[0][n+12]);
 
-	for (i = 1; i < n_src; i++) {
-		mix_2(dst, src[i], n_samples * ops->n_channels);
+			for (i = 1; i < n_src; i++) {
+				in[0] = _mm_add_ps(in[0], _mm_load_ps(&s[i][n+ 0]));
+				in[1] = _mm_add_ps(in[1], _mm_load_ps(&s[i][n+ 4]));
+				in[2] = _mm_add_ps(in[2], _mm_load_ps(&s[i][n+ 8]));
+				in[3] = _mm_add_ps(in[3], _mm_load_ps(&s[i][n+12]));
+			}
+			_mm_store_ps(&d[n+ 0], in[0]);
+			_mm_store_ps(&d[n+ 4], in[1]);
+			_mm_store_ps(&d[n+ 8], in[2]);
+			_mm_store_ps(&d[n+12], in[3]);
+		}
+		for (; n < n_samples; n++) {
+			in[0] = _mm_load_ss(&s[0][n]);
+			for (i = 1; i < n_src; i++)
+				in[0] = _mm_add_ss(in[0], _mm_load_ss(&s[i][n]));
+			_mm_store_ss(&d[n], in[0]);
+		}
 	}
 }
diff --git a/spa/plugins/audiomixer/mix-ops-sse2.c b/spa/plugins/audiomixer/mix-ops-sse2.c
index 51ad0dbcc72e586f14ce0215c302db68e0f5ceaf..e2f632d44788e42a2033e018bb630f415181ec6b 100644
--- a/spa/plugins/audiomixer/mix-ops-sse2.c
+++ b/spa/plugins/audiomixer/mix-ops-sse2.c
@@ -32,58 +32,56 @@
 
 #include <emmintrin.h>
 
-static inline void mix_2(double * dst, const double * SPA_RESTRICT src, uint32_t n_samples)
-{
-	uint32_t n, unrolled;
-	__m128d in1[4], in2[4];
-
-	if (SPA_IS_ALIGNED(src, 16) &&
-	    SPA_IS_ALIGNED(dst, 16))
-		unrolled = n_samples & ~7;
-	else
-		unrolled = 0;
-
-	for (n = 0; n < unrolled; n += 8) {
-		in1[0] = _mm_load_pd(&dst[n+ 0]);
-		in1[1] = _mm_load_pd(&dst[n+ 2]);
-		in1[2] = _mm_load_pd(&dst[n+ 4]);
-		in1[3] = _mm_load_pd(&dst[n+ 6]);
-
-		in2[0] = _mm_load_pd(&src[n+ 0]);
-		in2[1] = _mm_load_pd(&src[n+ 2]);
-		in2[2] = _mm_load_pd(&src[n+ 4]);
-		in2[3] = _mm_load_pd(&src[n+ 6]);
-
-		in1[0] = _mm_add_pd(in1[0], in2[0]);
-		in1[1] = _mm_add_pd(in1[1], in2[1]);
-		in1[2] = _mm_add_pd(in1[2], in2[2]);
-		in1[3] = _mm_add_pd(in1[3], in2[3]);
-
-		_mm_store_pd(&dst[n+ 0], in1[0]);
-		_mm_store_pd(&dst[n+ 2], in1[1]);
-		_mm_store_pd(&dst[n+ 4], in1[2]);
-		_mm_store_pd(&dst[n+ 6], in1[3]);
-	}
-	for (; n < n_samples; n++) {
-		in1[0] = _mm_load_sd(&dst[n]),
-		in2[0] = _mm_load_sd(&src[n]),
-		in1[0] = _mm_add_sd(in1[0], in2[0]);
-		_mm_store_sd(&dst[n], in1[0]);
-	}
-}
-
 void
 mix_f64_sse2(struct mix_ops *ops, void * SPA_RESTRICT dst, const void * SPA_RESTRICT src[],
 		uint32_t n_src, uint32_t n_samples)
 {
-	uint32_t i;
+	n_samples *= ops->n_channels;
+
+	if (n_src == 0) {
+		memset(dst, 0, n_samples * sizeof(double));
+	} else if (n_src == 1) {
+		if (dst != src[0])
+			spa_memcpy(dst, src[0], n_samples * sizeof(double));
+	} else {
+		uint32_t n, i, unrolled;
+		__m128d in[4];
+		const double **s = (const double **)src;
+		double *d = dst;
+
+		if (SPA_LIKELY(SPA_IS_ALIGNED(dst, 16))) {
+			unrolled = n_samples & ~15;
+			for (i = 0; i < n_src; i++) {
+				if (SPA_UNLIKELY(!SPA_IS_ALIGNED(src[i], 16))) {
+					unrolled = 0;
+					break;
+				}
+			}
+		} else
+			unrolled = 0;
 
-	if (n_src == 0)
-		memset(dst, 0, n_samples * ops->n_channels * sizeof(double));
-	else if (dst != src[0])
-		spa_memcpy(dst, src[0], n_samples * ops->n_channels * sizeof(double));
+		for (n = 0; n < unrolled; n += 8) {
+			in[0] = _mm_load_pd(&s[0][n+0]);
+			in[1] = _mm_load_pd(&s[0][n+2]);
+			in[2] = _mm_load_pd(&s[0][n+4]);
+			in[3] = _mm_load_pd(&s[0][n+6]);
 
-	for (i = 1; i < n_src; i++) {
-		mix_2(dst, src[i], n_samples * ops->n_channels);
+			for (i = 1; i < n_src; i++) {
+				in[0] = _mm_add_pd(in[0], _mm_load_pd(&s[i][n+0]));
+				in[1] = _mm_add_pd(in[1], _mm_load_pd(&s[i][n+2]));
+				in[2] = _mm_add_pd(in[2], _mm_load_pd(&s[i][n+4]));
+				in[3] = _mm_add_pd(in[3], _mm_load_pd(&s[i][n+6]));
+			}
+			_mm_store_pd(&d[n+0], in[0]);
+			_mm_store_pd(&d[n+2], in[1]);
+			_mm_store_pd(&d[n+4], in[2]);
+			_mm_store_pd(&d[n+6], in[3]);
+		}
+		for (; n < n_samples; n++) {
+			in[0] = _mm_load_sd(&s[0][n]);
+			for (i = 1; i < n_src; i++)
+				in[0] = _mm_add_sd(in[0], _mm_load_sd(&s[i][n]));
+			_mm_store_sd(&d[n], in[0]);
+		}
 	}
 }
diff --git a/spa/plugins/audiomixer/mix-ops.h b/spa/plugins/audiomixer/mix-ops.h
index a75c9b40b422696a4f7d6570b1ccb6322f371267..11e88dcdf0d3b84d794fd1cad41089189c3e8211 100644
--- a/spa/plugins/audiomixer/mix-ops.h
+++ b/spa/plugins/audiomixer/mix-ops.h
@@ -24,80 +24,98 @@
 
 #include <spa/utils/defs.h>
 
-static inline uint32_t read_u24(const void *src)
-{
-	const uint8_t *s = src;
+typedef struct {
 #if __BYTE_ORDER == __LITTLE_ENDIAN
-	return (((uint32_t)s[2] << 16) | ((uint32_t)(uint8_t)s[1] << 8) | (uint32_t)(uint8_t)s[0]);
+	uint8_t v3;
+	uint8_t v2;
+	uint8_t v1;
 #else
-	return (((uint32_t)s[0] << 16) | ((uint32_t)(uint8_t)s[1] << 8) | (uint32_t)(uint8_t)s[2]);
+	uint8_t v1;
+	uint8_t v2;
+	uint8_t v3;
 #endif
-}
+} __attribute__ ((packed)) uint24_t;
 
-static inline int32_t read_s24(const void *src)
-{
-	const int8_t *s = src;
+typedef struct {
 #if __BYTE_ORDER == __LITTLE_ENDIAN
-	return (((int32_t)s[2] << 16) | ((uint32_t)(uint8_t)s[1] << 8) | (uint32_t)(uint8_t)s[0]);
+	uint8_t v3;
+	uint8_t v2;
+	int8_t v1;
 #else
-	return (((int32_t)s[0] << 16) | ((uint32_t)(uint8_t)s[1] << 8) | (uint32_t)(uint8_t)s[2]);
+	int8_t v1;
+	uint8_t v2;
+	uint8_t v3;
 #endif
-}
+} __attribute__ ((packed)) int24_t;
 
-static inline void write_u24(void *dst, uint32_t val)
+static inline uint32_t u24_to_u32(uint24_t src)
 {
-	uint8_t *d = dst;
-#if __BYTE_ORDER == __LITTLE_ENDIAN
-	d[0] = (uint8_t) (val);
-	d[1] = (uint8_t) (val >> 8);
-	d[2] = (uint8_t) (val >> 16);
-#else
-	d[0] = (uint8_t) (val >> 16);
-	d[1] = (uint8_t) (val >> 8);
-	d[2] = (uint8_t) (val);
-#endif
+	return ((uint32_t)src.v1 << 16) | ((uint32_t)src.v2 << 8) | (uint32_t)src.v3;
 }
 
-static inline void write_s24(void *dst, int32_t val)
+#define U32_TO_U24(s) (uint24_t) { .v1 = (uint8_t)(((uint32_t)s) >> 16), \
+	.v2 = (uint8_t)(((uint32_t)s) >> 8), .v3 = (uint8_t)((uint32_t)s) }
+
+static inline uint24_t u32_to_u24(uint32_t src)
 {
-	uint8_t *d = dst;
-#if __BYTE_ORDER == __LITTLE_ENDIAN
-	d[0] = (uint8_t) (val);
-	d[1] = (uint8_t) (val >> 8);
-	d[2] = (uint8_t) (val >> 16);
-#else
-	d[0] = (uint8_t) (val >> 16);
-	d[1] = (uint8_t) (val >> 8);
-	d[2] = (uint8_t) (val);
-#endif
+	return U32_TO_U24(src);
 }
 
-#define S8_MIN		-127
-#define S8_MAX		127
-#define S8_MIX(a, b)    (int8_t)(SPA_CLAMP((int16_t)(a) + (int16_t)(b), S8_MIN, S8_MAX))
-#define U8_MIX(a, b)    (uint8_t)((int16_t)S8_MIX((int16_t)(a) - S8_MAX, (int16_t)(b) - S8_MAX) + S8_MAX)
-
-#define S16_MIN		-32767
-#define S16_MAX		32767
-#define S16_MIX(a, b)   (int16_t)(SPA_CLAMP((int32_t)(a) + (int32_t)(b), S16_MIN, S16_MAX))
-#define U16_MIX(a, b)   (uint16_t)((int32_t)S16_MIX((int32_t)(a) - S16_MAX, (int32_t)(b) - S16_MAX) + S16_MAX)
-
-#define S24_MIN		-8388607
-#define S24_MAX		8388607
-#define S24_MIX(a, b)   (int32_t)(SPA_CLAMP((int32_t)(a) + (int32_t)(b), S24_MIN, S24_MAX))
-#define U24_MIX(a, b)   (uint32_t)((int32_t)S24_MIX((int32_t)(a) - S24_MAX, (int32_t)(b) - S24_MAX) + S24_MAX)
+static inline int32_t s24_to_s32(int24_t src)
+{
+	return ((int32_t)src.v1 << 16) | ((uint32_t)src.v2 << 8) | (uint32_t)src.v3;
+}
 
-#define S32_MIN		-2147483647
-#define S32_MAX		2147483647
-#define S32_MIX(a, b)   (int32_t)(SPA_CLAMP((int64_t)(a) + (int64_t)(b), S32_MIN, S32_MAX))
-#define U32_MIX(a, b)   (uint32_t)((int64_t)S32_MIX((int64_t)(a) - S32_MAX, (int64_t)(b) - S32_MAX) + S32_MAX)
+#define S32_TO_S24(s) (int24_t) { .v1 = (int8_t)(((int32_t)s) >> 16), \
+	.v2 = (uint8_t)(((uint32_t)s) >> 8), .v3 = (uint8_t)((uint32_t)s) }
 
-#define S24_32_MIX(a, b) S24_MIX (a, b)
-#define U24_32_MIX(a, b) U24_MIX (a, b)
+static inline int24_t s32_to_s24(int32_t src)
+{
+	return S32_TO_S24(src);
+}
 
-#define F32_MIX(a, b)   (float)((float)(a) + (float)(b))
 
-#define F64_MIX(a, b)   (double)((double)(a) + (double)(b))
+#define S8_MIN			-128
+#define S8_MAX			127
+#define S8_ACCUM(a,b)		((a) + (int16_t)(b))
+#define S8_CLAMP(a)		(int8_t)(SPA_CLAMP((a), S8_MIN, S8_MAX))
+#define U8_OFFS			128
+#define U8_ACCUM(a,b)		((a) + ((int16_t)(b) - U8_OFFS))
+#define U8_CLAMP(a)		(uint8_t)(SPA_CLAMP((a), S8_MIN, S8_MAX) + U8_OFFS)
+
+#define S16_MIN			-32768
+#define S16_MAX			32767
+#define S16_ACCUM(a,b)		((a) + (int32_t)(b))
+#define S16_CLAMP(a)		(int16_t)(SPA_CLAMP((a), S16_MIN, S16_MAX))
+#define U16_OFFS		32768
+#define U16_ACCUM(a,b)		((a) + ((int32_t)(b) - U16_OFFS))
+#define U16_CLAMP(a)		(uint16_t)(SPA_CLAMP((a), S16_MIN, S16_MAX) + U16_OFFS)
+
+#define S24_32_MIN		-8388608
+#define S24_32_MAX		8388607
+#define S24_32_ACCUM(a,b)	((a) + (int32_t)(b))
+#define S24_32_CLAMP(a)		(int32_t)(SPA_CLAMP((a), S24_32_MIN, S24_32_MAX))
+#define U24_32_OFFS		8388608
+#define U24_32_ACCUM(a,b)	((a) + ((int32_t)(b) - U24_32_OFFS))
+#define U24_32_CLAMP(a)		(uint32_t)(SPA_CLAMP((a), S24_32_MIN, S24_32_MAX) + U24_32_OFFS)
+
+#define S24_ACCUM(a,b)		S24_32_ACCUM(a, s24_to_s32(b))
+#define S24_CLAMP(a)		s32_to_s24(S24_32_CLAMP(a))
+#define U24_ACCUM(a,b)		U24_32_ACCUM(a, u24_to_u32(b))
+#define U24_CLAMP(a)		u32_to_u24(U24_32_CLAMP(a))
+
+#define S32_MIN			-2147483648
+#define S32_MAX			2147483647
+#define S32_ACCUM(a,b)		((a) + (int64_t)(b))
+#define S32_CLAMP(a)		(int32_t)(SPA_CLAMP((a), S32_MIN, S32_MAX))
+#define U32_OFFS		2147483648
+#define U32_ACCUM(a,b)		((a) + ((int64_t)(b) - U32_OFFS))
+#define U32_CLAMP(a)		(uint32_t)(SPA_CLAMP((a), S32_MIN, S32_MAX) + U32_OFFS)
+
+#define F32_ACCUM(a,b)		((a) + (b))
+#define F32_CLAMP(a)		(a)
+#define F64_ACCUM(a,b)		((a) + (b))
+#define F64_CLAMP(a)		(a)
 
 struct mix_ops {
 	uint32_t fmt;
diff --git a/spa/plugins/audiomixer/mixer-dsp.c b/spa/plugins/audiomixer/mixer-dsp.c
index cf926244cca39984ed9dcbc251cce55e1ece1acc..f93796b971bed0b4c3645c8993910f965271b4a7 100644
--- a/spa/plugins/audiomixer/mixer-dsp.c
+++ b/spa/plugins/audiomixer/mixer-dsp.c
@@ -675,8 +675,8 @@ static int impl_node_process(void *object)
 	spa_return_val_if_fail(this != NULL, -EINVAL);
 
 	outport = GET_OUT_PORT(this, 0);
-	outio = outport->io;
-	spa_return_val_if_fail(outio != NULL, -EIO);
+	if ((outio = outport->io) == NULL)
+		return -EIO;
 
 	spa_log_trace_fp(this->log, "%p: status %p %d %d",
 			this, outio, outio->status, outio->buffer_id);
@@ -727,8 +727,10 @@ static int impl_node_process(void *object)
 				i, inio, outio, inio->status, inio->buffer_id,
 				offs, size);
 
-		datas[n_buffers] = SPA_PTROFF(bd->data, offs, void);
-		buffers[n_buffers++] = inb;
+		if (!SPA_FLAG_IS_SET(bd->chunk->flags, SPA_CHUNK_FLAG_EMPTY)) {
+			datas[n_buffers] = SPA_PTROFF(bd->data, offs, void);
+			buffers[n_buffers++] = inb;
+		}
 		inio->status = SPA_STATUS_NEED_DATA;
 	}
 
@@ -742,6 +744,7 @@ static int impl_node_process(void *object)
 		*outb->buffer = *buffers[0]->buffer;
 	} else {
 		struct spa_data *d = outb->buf.datas;
+
 		*outb->buffer = outb->buf;
 
 		maxsize = SPA_MIN(maxsize, d[0].maxsize);
@@ -749,6 +752,7 @@ static int impl_node_process(void *object)
 		d[0].chunk->offset = 0;
 		d[0].chunk->size = maxsize;
 		d[0].chunk->stride = sizeof(float);
+		SPA_FLAG_UPDATE(d[0].chunk->flags, SPA_CHUNK_FLAG_EMPTY, n_buffers == 0);
 
 		spa_log_trace_fp(this->log, "%p: %d mix %d", this, n_buffers, maxsize);
 
diff --git a/spa/plugins/audiomixer/test-helper.h b/spa/plugins/audiomixer/test-helper.h
new file mode 100644
index 0000000000000000000000000000000000000000..8c789bd45930280a89e80bcf4de9e012bf88594f
--- /dev/null
+++ b/spa/plugins/audiomixer/test-helper.h
@@ -0,0 +1,97 @@
+#include <dlfcn.h>
+
+#include <spa/support/plugin.h>
+#include <spa/utils/type.h>
+#include <spa/utils/result.h>
+#include <spa/support/cpu.h>
+#include <spa/utils/names.h>
+
+static inline const struct spa_handle_factory *get_factory(spa_handle_factory_enum_func_t enum_func,
+		const char *name, uint32_t version)
+{
+	uint32_t i;
+	int res;
+	const struct spa_handle_factory *factory;
+
+	for (i = 0;;) {
+		if ((res = enum_func(&factory, &i)) <= 0) {
+			if (res < 0)
+				errno = -res;
+			break;
+		}
+		if (factory->version >= version &&
+		    !strcmp(factory->name, name))
+			return factory;
+	}
+	return NULL;
+}
+
+static inline struct spa_handle *load_handle(const struct spa_support *support,
+		uint32_t n_support, const char *lib, const char *name)
+{
+	int res, len;
+	void *hnd;
+	spa_handle_factory_enum_func_t enum_func;
+	const struct spa_handle_factory *factory;
+	struct spa_handle *handle;
+	const char *str;
+	char *path;
+
+	if ((str = getenv("SPA_PLUGIN_DIR")) == NULL)
+		str = PLUGINDIR;
+
+	len = strlen(str) + strlen(lib) + 2;
+	path = alloca(len);
+	snprintf(path, len, "%s/%s", str, lib);
+
+	if ((hnd = dlopen(path, RTLD_NOW)) == NULL) {
+		fprintf(stderr, "can't load %s: %s\n", lib, dlerror());
+		res = -ENOENT;
+		goto error;
+	}
+	if ((enum_func = dlsym(hnd, SPA_HANDLE_FACTORY_ENUM_FUNC_NAME)) == NULL) {
+		fprintf(stderr, "can't find enum function\n");
+		res = -ENXIO;
+		goto error_close;
+	}
+
+	if ((factory = get_factory(enum_func, name, SPA_VERSION_HANDLE_FACTORY)) == NULL) {
+		fprintf(stderr, "can't find factory\n");
+		res = -ENOENT;
+		goto error_close;
+	}
+	handle = calloc(1, spa_handle_factory_get_size(factory, NULL));
+	if ((res = spa_handle_factory_init(factory, handle,
+					NULL, support, n_support)) < 0) {
+		fprintf(stderr, "can't make factory instance: %d\n", res);
+		goto error_close;
+	}
+	return handle;
+
+error_close:
+	dlclose(hnd);
+error:
+	errno = -res;
+	return NULL;
+}
+
+static inline uint32_t get_cpu_flags(void)
+{
+	struct spa_handle *handle;
+	uint32_t flags;
+	void *iface;
+	int res;
+
+	handle = load_handle(NULL, 0, "support/libspa-support.so", SPA_NAME_SUPPORT_CPU);
+	if (handle == NULL)
+		return 0;
+	if ((res = spa_handle_get_interface(handle, SPA_TYPE_INTERFACE_CPU, &iface)) < 0) {
+		fprintf(stderr, "can't get CPU interface %s\n", spa_strerror(res));
+		return 0;
+	}
+	flags = spa_cpu_get_flags((struct spa_cpu*)iface);
+
+	free(handle);
+
+	return flags;
+}
diff --git a/spa/plugins/audiomixer/test-mix-ops.c b/spa/plugins/audiomixer/test-mix-ops.c
new file mode 100644
index 0000000000000000000000000000000000000000..a20f7a4313d43f7c2df985842c807363f0a81ab8
--- /dev/null
+++ b/spa/plugins/audiomixer/test-mix-ops.c
@@ -0,0 +1,293 @@
+/* Spa
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <string.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <errno.h>
+#include <time.h>
+
+#include <spa/debug/mem.h>
+
+#include "test-helper.h"
+#include "mix-ops.c"
+
+static uint32_t cpu_flags;
+
+#define N_SAMPLES 1024
+
+static uint8_t samp_out[N_SAMPLES * 8];
+
+static void compare_mem(int i, int j, const void *m1, const void *m2, size_t size)
+{
+	int res = memcmp(m1, m2, size);
+	if (res != 0) {
+		fprintf(stderr, "%d %d %zd:\n", i, j, size);
+		spa_debug_mem(0, m1, size);
+		spa_debug_mem(0, m2, size);
+	}
+	spa_assert_se(res == 0);
+}
+
+static int run_test(const char *name, const void *src[], uint32_t n_src, const void *dst,
+		size_t dst_size, uint32_t n_samples, mix_func_t mix)
+{
+	struct mix_ops ops;
+
+	ops.fmt = SPA_AUDIO_FORMAT_F32;
+	ops.n_channels = 1;
+	ops.cpu_flags = cpu_flags;
+	mix_ops_init(&ops);
+
+	fprintf(stderr, "%s\n", name);
+
+	mix(&ops, (void *)samp_out, src, n_src, n_samples);
+	compare_mem(0, 0, samp_out, dst, dst_size);
+	return 0;
+}
+
+static void test_s8(void)
+{
+	int8_t out[] = { 0x00, 0x00, 0x00, 0x00 };
+	int8_t in_1[] = { 0x00, 0x00, 0x00, 0x00 };
+	int8_t in_2[] = { 0x7f, 0x80, 0x40, 0xc0 };
+	int8_t in_3[] = { 0x40, 0xc0, 0xc0, 0x40 };
+	int8_t in_4[] = { 0xc0, 0x40, 0x40, 0xc0 };
+	int8_t out_4[] = { 0x7f, 0x80, 0x40, 0xc0 };
+	const void *src[6] = { in_1, in_2, in_3, in_4 };
+
+	run_test("test_s8_0", NULL, 0, out, sizeof(out), SPA_N_ELEMENTS(out), mix_s8_c);
+	run_test("test_s8_1", src, 1, in_1, sizeof(in_1), SPA_N_ELEMENTS(in_1), mix_s8_c);
+	run_test("test_s8_4", src, 4, out_4, sizeof(out_4), SPA_N_ELEMENTS(out_4), mix_s8_c);
+}
+
+static void test_u8(void)
+{
+	uint8_t out[] = { 0x80, 0x80, 0x80, 0x80 };
+	uint8_t in_1[] = { 0x80, 0x80, 0x80, 0x80 };
+	uint8_t in_2[] = { 0xff, 0x00, 0xc0, 0x40 };
+	uint8_t in_3[] = { 0xc0, 0x40, 0x40, 0xc0 };
+	uint8_t in_4[] = { 0x40, 0xc0, 0xc0, 0x40 };
+	uint8_t out_4[] = { 0xff, 0x00, 0xc0, 0x40 };
+	const void *src[6] = { in_1, in_2, in_3, in_4 };
+
+	run_test("test_u8_0", NULL, 0, out, sizeof(out), SPA_N_ELEMENTS(out), mix_u8_c);
+	run_test("test_u8_1", src, 1, in_1, sizeof(in_1), SPA_N_ELEMENTS(in_1), mix_u8_c);
+	run_test("test_u8_4", src, 4, out_4, sizeof(out_4), SPA_N_ELEMENTS(out_4), mix_u8_c);
+}
+
+static void test_s16(void)
+{
+	int16_t out[] = { 0x0000, 0x0000, 0x0000, 0x0000 };
+	int16_t in_1[] = { 0x0000, 0x0000, 0x0000, 0x0000 };
+	int16_t in_2[] = { 0x7fff, 0x8000, 0x4000, 0xc000 };
+	int16_t in_3[] = { 0x4000, 0xc000, 0xc000, 0x4000 };
+	int16_t in_4[] = { 0xc000, 0x4000, 0x4000, 0xc000 };
+	int16_t out_4[] = { 0x7fff, 0x8000, 0x4000, 0xc000 };
+	const void *src[6] = { in_1, in_2, in_3, in_4 };
+
+	run_test("test_s16_0", NULL, 0, out, sizeof(out), SPA_N_ELEMENTS(out), mix_s16_c);
+	run_test("test_s16_1", src, 1, in_1, sizeof(in_1), SPA_N_ELEMENTS(in_1), mix_s16_c);
+	run_test("test_s16_4", src, 4, out_4, sizeof(out_4), SPA_N_ELEMENTS(out_4), mix_s16_c);
+}
+
+static void test_u16(void)
+{
+	uint16_t out[] = { 0x8000, 0x8000, 0x8000, 0x8000 };
+	uint16_t in_1[] = { 0x8000, 0x8000, 0x8000 , 0x8000};
+	uint16_t in_2[] = { 0xffff, 0x0000, 0xc000, 0x4000 };
+	uint16_t in_3[] = { 0xc000, 0x4000, 0x4000, 0xc000 };
+	uint16_t in_4[] = { 0x4000, 0xc000, 0xc000, 0x4000 };
+	uint16_t out_4[] = { 0xffff, 0x0000, 0xc000, 0x4000 };
+	const void *src[6] = { in_1, in_2, in_3, in_4 };
+
+	run_test("test_u16_0", NULL, 0, out, sizeof(out), SPA_N_ELEMENTS(out), mix_u16_c);
+	run_test("test_u16_1", src, 1, in_1, sizeof(in_1), SPA_N_ELEMENTS(in_1), mix_u16_c);
+	run_test("test_u16_4", src, 4, out_4, sizeof(out_4), SPA_N_ELEMENTS(out_4), mix_u16_c);
+}
+
+static void test_s24(void)
+{
+	int24_t out[] = { S32_TO_S24(0x000000), S32_TO_S24(0x000000), S32_TO_S24(0x000000) };
+	int24_t in_1[] = { S32_TO_S24(0x000000), S32_TO_S24(0x000000), S32_TO_S24(0x000000) };
+	int24_t in_2[] = { S32_TO_S24(0x7fffff), S32_TO_S24(0xff800000), S32_TO_S24(0x400000) };
+	int24_t in_3[] = { S32_TO_S24(0x400000), S32_TO_S24(0xffc00000), S32_TO_S24(0xffc00000) };
+	int24_t in_4[] = { S32_TO_S24(0xffc00000), S32_TO_S24(0x400000), S32_TO_S24(0x400000) };
+	int24_t out_4[] = { S32_TO_S24(0x7fffff), S32_TO_S24(0xff800000), S32_TO_S24(0x400000) };
+	const void *src[6] = { in_1, in_2, in_3, in_4 };
+
+	run_test("test_s24_0", NULL, 0, out, sizeof(out), SPA_N_ELEMENTS(out), mix_s24_c);
+	run_test("test_s24_1", src, 1, in_1, sizeof(in_1), SPA_N_ELEMENTS(in_1), mix_s24_c);
+	run_test("test_s24_4", src, 4, out_4, sizeof(out_4), SPA_N_ELEMENTS(out_4), mix_s24_c);
+}
+
+static void test_u24(void)
+{
+	uint24_t out[] = { U32_TO_U24(0x800000), U32_TO_U24(0x800000), U32_TO_U24(0x800000) };
+	uint24_t in_1[] = { U32_TO_U24(0x800000), U32_TO_U24(0x800000), U32_TO_U24(0x800000) };
+	uint24_t in_2[] = { U32_TO_U24(0xffffffff), U32_TO_U24(0x000000), U32_TO_U24(0xffc00000) };
+	uint24_t in_3[] = { U32_TO_U24(0xffc00000), U32_TO_U24(0x400000), U32_TO_U24(0x400000) };
+	uint24_t in_4[] = { U32_TO_U24(0x400000), U32_TO_U24(0xffc00000), U32_TO_U24(0xffc00000) };
+	uint24_t out_4[] = { U32_TO_U24(0xffffffff), U32_TO_U24(0x000000), U32_TO_U24(0xffc00000) };
+	const void *src[6] = { in_1, in_2, in_3, in_4 };
+
+	run_test("test_u24_0", NULL, 0, out, sizeof(out), SPA_N_ELEMENTS(out), mix_u24_c);
+	run_test("test_u24_1", src, 1, in_1, sizeof(in_1), SPA_N_ELEMENTS(in_1), mix_u24_c);
+	run_test("test_u24_4", src, 4, out_4, sizeof(out_4), SPA_N_ELEMENTS(out_4), mix_u24_c);
+}
+
+static void test_s32(void)
+{
+	int32_t out[] = { 0x00000000, 0x00000000, 0x00000000, 0x00000000 };
+	int32_t in_1[] = { 0x00000000, 0x00000000, 0x00000000, 0x00000000 };
+	int32_t in_2[] = { 0x7fffffff, 0x80000000, 0x40000000, 0xc0000000 };
+	int32_t in_3[] = { 0x40000000, 0xc0000000, 0xc0000000, 0x40000000 };
+	int32_t in_4[] = { 0xc0000000, 0x40000000, 0x40000000, 0xc0000000 };
+	int32_t out_4[] = { 0x7fffffff, 0x80000000, 0x40000000, 0xc0000000 };
+	const void *src[6] = { in_1, in_2, in_3, in_4 };
+
+	run_test("test_s32_0", NULL, 0, out, sizeof(out), SPA_N_ELEMENTS(out), mix_s32_c);
+	run_test("test_s32_1", src, 1, in_1, sizeof(in_1), SPA_N_ELEMENTS(in_1), mix_s32_c);
+	run_test("test_s32_4", src, 4, out_4, sizeof(out_4), SPA_N_ELEMENTS(out_4), mix_s32_c);
+}
+
+static void test_u32(void)
+{
+	uint32_t out[] = { 0x80000000, 0x80000000, 0x80000000, 0x80000000 };
+	uint32_t in_1[] = { 0x80000000, 0x80000000, 0x80000000, 0x80000000 };
+	uint32_t in_2[] = { 0xffffffff, 0x00000000, 0xc0000000, 0x40000000 };
+	uint32_t in_3[] = { 0xc0000000, 0x40000000, 0x40000000, 0xc0000000 };
+	uint32_t in_4[] = { 0x40000000, 0xc0000000, 0xc0000000, 0x40000000 };
+	uint32_t out_4[] = { 0xffffffff, 0x00000000, 0xc0000000, 0x40000000 };
+	const void *src[6] = { in_1, in_2, in_3, in_4 };
+
+	run_test("test_u32_0", NULL, 0, out, sizeof(out), SPA_N_ELEMENTS(out), mix_u32_c);
+	run_test("test_u32_1", src, 1, in_1, sizeof(in_1), SPA_N_ELEMENTS(in_1), mix_u32_c);
+	run_test("test_u32_4", src, 4, out_4, sizeof(out_4), SPA_N_ELEMENTS(out_4), mix_u32_c);
+}
+
+static void test_s24_32(void)
+{
+	int32_t out[] = { 0x000000, 0x000000, 0x000000, 0x000000 };
+	int32_t in_1[] = { 0x000000, 0x000000, 0x000000, 0x000000 };
+	int32_t in_2[] = { 0x7fffff, 0xff800000, 0x400000, 0xffc00000 };
+	int32_t in_3[] = { 0x400000, 0xffc00000, 0xffc00000, 0x400000 };
+	int32_t in_4[] = { 0xffc00000, 0x400000, 0x400000, 0xffc00000 };
+	int32_t out_4[] = { 0x7fffff, 0xff800000, 0x400000, 0xffc00000 };
+	const void *src[6] = { in_1, in_2, in_3, in_4 };
+
+	run_test("test_s24_32_0", NULL, 0, out, sizeof(out), SPA_N_ELEMENTS(out), mix_s24_32_c);
+	run_test("test_s24_32_1", src, 1, in_1, sizeof(in_1), SPA_N_ELEMENTS(in_1), mix_s24_32_c);
+	run_test("test_s24_32_4", src, 4, out_4, sizeof(out_4), SPA_N_ELEMENTS(out_4), mix_s24_32_c);
+}
+
+static void test_u24_32(void)
+{
+	uint32_t out[] = { 0x800000, 0x800000, 0x800000, 0x800000 };
+	uint32_t in_1[] = { 0x800000, 0x800000, 0x800000, 0x800000 };
+	uint32_t in_2[] = { 0xffffff, 0x000000, 0xc00000, 0x400000 };
+	uint32_t in_3[] = { 0xc00000, 0x400000, 0x400000, 0xc00000 };
+	uint32_t in_4[] = { 0x400000, 0xc00000, 0xc00000, 0x400000 };
+	uint32_t out_4[] = { 0xffffff, 0x000000, 0xc00000, 0x400000 };
+	const void *src[6] = { in_1, in_2, in_3, in_4 };
+
+	run_test("test_u24_32_0", NULL, 0, out, sizeof(out), SPA_N_ELEMENTS(out), mix_u24_32_c);
+	run_test("test_u24_32_1", src, 1, in_1, sizeof(in_1), SPA_N_ELEMENTS(in_1), mix_u24_32_c);
+	run_test("test_u24_32_4", src, 4, out_4, sizeof(out_4), SPA_N_ELEMENTS(out_4), mix_u24_32_c);
+}
+
+static void test_f32(void)
+{
+	float out[] = { 0.0f, 0.0f, 0.0f, 0.0f };
+	float in_1[] = { 0.0f, 0.0f, 0.0f, 0.0f };
+	float in_2[] = { 1.0f, -1.0f, 0.5f, -0.5f };
+	float in_3[] = { 0.5f, -0.5f, -0.5f, 0.5f };
+	float in_4[] = { -0.5f, 1.0f, 0.5f, -0.5f };
+	float out_4[] = { 1.0f, -0.5f, 0.5f, -0.5f };
+	const void *src[6] = { in_1, in_2, in_3, in_4 };
+
+	run_test("test_f32_0", NULL, 0, out, sizeof(out), SPA_N_ELEMENTS(out), mix_f32_c);
+	run_test("test_f32_1", src, 1, in_1, sizeof(in_1), SPA_N_ELEMENTS(in_1), mix_f32_c);
+	run_test("test_f32_4", src, 4, out_4, sizeof(out_4), SPA_N_ELEMENTS(out_4), mix_f32_c);
+#if defined(HAVE_SSE)
+	if (cpu_flags & SPA_CPU_FLAG_SSE) {
+		run_test("test_f32_0_sse", NULL, 0, out, sizeof(out), SPA_N_ELEMENTS(out), mix_f32_sse);
+		run_test("test_f32_1_sse", src, 1, in_1, sizeof(in_1), SPA_N_ELEMENTS(in_1), mix_f32_sse);
+		run_test("test_f32_4_sse", src, 4, out_4, sizeof(out_4), SPA_N_ELEMENTS(out_4), mix_f32_sse);
+	}
+#endif
+#if defined(HAVE_AVX)
+	if (cpu_flags & SPA_CPU_FLAG_AVX) {
+		run_test("test_f32_0_avx", NULL, 0, out, sizeof(out), SPA_N_ELEMENTS(out), mix_f32_avx);
+		run_test("test_f32_1_avx", src, 1, in_1, sizeof(in_1), SPA_N_ELEMENTS(in_1), mix_f32_avx);
+		run_test("test_f32_4_avx", src, 4, out_4, sizeof(out_4), SPA_N_ELEMENTS(out_4), mix_f32_avx);
+	}
+#endif
+}
+
+static void test_f64(void)
+{
+	double out[] = { 0.0, 0.0, 0.0, 0.0 };
+	double in_1[] = { 0.0, 0.0, 0.0, 0.0 };
+	double in_2[] = { 1.0, -1.0, 0.5, -0.5 };
+	double in_3[] = { 0.5, -0.5, -0.5, 0.5 };
+	double in_4[] = { -0.5, 1.0, 0.5, -0.5 };
+	double out_4[] = { 1.0, -0.5, 0.5, -0.5 };
+	const void *src[6] = { in_1, in_2, in_3, in_4 };
+
+	run_test("test_f64_0", NULL, 0, out, sizeof(out), SPA_N_ELEMENTS(out), mix_f64_c);
+	run_test("test_f64_1", src, 1, in_1, sizeof(in_1), SPA_N_ELEMENTS(in_1), mix_f64_c);
+	run_test("test_f64_4", src, 4, out_4, sizeof(out_4), SPA_N_ELEMENTS(out_4), mix_f64_c);
+#if defined(HAVE_SSE2)
+	if (cpu_flags & SPA_CPU_FLAG_SSE2) {
+		run_test("test_f64_0_sse2", NULL, 0, out, sizeof(out), SPA_N_ELEMENTS(out), mix_f64_sse2);
+		run_test("test_f64_1_sse2", src, 1, in_1, sizeof(in_1), SPA_N_ELEMENTS(in_1), mix_f64_sse2);
+		run_test("test_f64_4_sse2", src, 4, out_4, sizeof(out_4), SPA_N_ELEMENTS(out_4), mix_f64_sse2);
+	}
+#endif
+}
+
+int main(int argc, char *argv[])
+{
+	cpu_flags = get_cpu_flags();
+	printf("got get CPU flags %d\n", cpu_flags);
+
+	test_s8();
+	test_u8();
+	test_s16();
+	test_u16();
+	test_s24();
+	test_u24();
+	test_s32();
+	test_u32();
+	test_s24_32();
+	test_u24_32();
+	test_f32();
+	test_f64();
+
+	return 0;
+}
diff --git a/spa/plugins/audiotestsrc/audiotestsrc.c b/spa/plugins/audiotestsrc/audiotestsrc.c
index 2a7174b4c9363e48c4a274b43e113e6577409aa4..d02b735803a1d7cb0fb2d9c1d9b87be1cd79135a 100644
--- a/spa/plugins/audiotestsrc/audiotestsrc.c
+++ b/spa/plugins/audiotestsrc/audiotestsrc.c
@@ -931,7 +931,8 @@ static int impl_node_process(void *object)
 	port = &this->port;
 
 	io = port->io;
-	spa_return_val_if_fail(io != NULL, -EIO);
+	if ((io = port->io) == NULL)
+		return -EIO;
 
 	if (port->io_control)
 		process_control(this, &port->io_control->sequence);
diff --git a/spa/plugins/avb/avb-pcm-sink.c b/spa/plugins/avb/avb-pcm-sink.c
new file mode 100644
index 0000000000000000000000000000000000000000..00f4e9593433022d88684e7a581af5da9987d7fb
--- /dev/null
+++ b/spa/plugins/avb/avb-pcm-sink.c
@@ -0,0 +1,911 @@
+/* Spa AVB PCM Sink
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stddef.h>
+
+#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>
+#include <spa/param/audio/format.h>
+#include <spa/pod/filter.h>
+#include <spa/debug/pod.h>
+
+#include "avb-pcm.h"
+
+#define CHECK_PORT(this,d,p)    ((d) == SPA_DIRECTION_INPUT && (p) == 0)
+#define GET_PORT(this,d,p)	(&this->ports[p])
+
+static void reset_props(struct props *props)
+{
+	snprintf(props->ifname, sizeof(props->ifname), "%s", DEFAULT_IFNAME);
+	parse_addr(props->addr, DEFAULT_ADDR);
+	props->prio = DEFAULT_PRIO;
+	parse_streamid(&props->streamid, DEFAULT_STREAMID);
+	props->mtt = DEFAULT_MTT;
+	props->t_uncertainty = DEFAULT_TU;
+	props->frames_per_pdu = DEFAULT_FRAMES_PER_PDU;
+}
+
+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[4];
+		uint32_t i, n_items = 0;
+
+		items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_API, "avb");
+		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");
+		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, struct port *port, bool full)
+{
+	uint64_t old = full ? port->info.change_mask : 0;
+
+	if (full)
+		port->info.change_mask = port->info_all;
+	if (port->info.change_mask) {
+		uint32_t i;
+
+		if (port->info.change_mask & SPA_PORT_CHANGE_MASK_PARAMS) {
+			for (i = 0; i < port->info.n_params; i++) {
+				if (port->params[i].user > 0) {
+					port->params[i].flags ^= SPA_PARAM_INFO_SERIAL;
+					port->params[i].user = 0;
+				}
+			}
+		}
+		spa_node_emit_port_info(&this->hooks,
+				port->direction, port->id, &port->info);
+		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)
+{
+	struct state *this = object;
+	struct spa_pod *param;
+	struct spa_pod_builder b = { 0 };
+	uint8_t buffer[4096];
+	struct spa_result_node_params result;
+	uint32_t count = 0;
+
+	spa_return_val_if_fail(this != NULL, -EINVAL);
+	spa_return_val_if_fail(num != 0, -EINVAL);
+
+	result.id = id;
+	result.next = start;
+      next:
+	result.index = result.next++;
+
+	spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+	switch (id) {
+	case SPA_PARAM_PropInfo:
+	{
+		switch (result.index) {
+		default:
+			param = spa_avb_enum_propinfo(this, result.index, &b);
+			if (param == NULL)
+				return 0;
+		}
+		break;
+	}
+	case SPA_PARAM_Props:
+	{
+		struct spa_pod_frame f;
+
+		switch (result.index) {
+		case 0:
+			spa_pod_builder_push_object(&b, &f,
+                                SPA_TYPE_OBJECT_Props, id);
+			spa_pod_builder_add(&b,
+				SPA_PROP_latencyOffsetNsec,   SPA_POD_Long(this->process_latency.ns),
+				0);
+			spa_avb_add_prop_params(this, &b);
+			param = spa_pod_builder_pop(&b, &f);
+			break;
+		default:
+			return 0;
+		}
+		break;
+	}
+	case SPA_PARAM_IO:
+		switch (result.index) {
+		case 0:
+			param = spa_pod_builder_add_object(&b,
+				SPA_TYPE_OBJECT_ParamIO, id,
+				SPA_PARAM_IO_id,   SPA_POD_Id(SPA_IO_Clock),
+				SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_clock)));
+			break;
+		case 1:
+			param = spa_pod_builder_add_object(&b,
+				SPA_TYPE_OBJECT_ParamIO, id,
+				SPA_PARAM_IO_id,   SPA_POD_Id(SPA_IO_Position),
+				SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_position)));
+			break;
+		default:
+			return 0;
+		}
+		break;
+
+	case SPA_PARAM_ProcessLatency:
+		switch (result.index) {
+		case 0:
+			param = spa_process_latency_build(&b, id, &this->process_latency);
+			break;
+		default:
+			return 0;
+		}
+		break;
+
+	default:
+		return -ENOENT;
+	}
+
+	if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+		goto next;
+
+	spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+	if (++count != num)
+		goto next;
+
+	return 0;
+}
+
+static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size)
+{
+	struct state *this = object;
+
+	spa_return_val_if_fail(this != NULL, -EINVAL);
+
+	switch (id) {
+	case SPA_IO_Clock:
+		this->clock = data;
+		break;
+	case SPA_IO_Position:
+		this->position = data;
+		break;
+	default:
+		return -ENOENT;
+	}
+	spa_avb_reassign_follower(this);
+
+	return 0;
+}
+
+static void handle_process_latency(struct state *this,
+		const struct spa_process_latency_info *info)
+{
+	bool ns_changed = this->process_latency.ns != info->ns;
+	struct port *port = &this->ports[0];
+
+	if (this->process_latency.quantum == info->quantum &&
+	    this->process_latency.rate == info->rate &&
+	    !ns_changed)
+		return;
+
+	this->process_latency = *info;
+
+	this->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS;
+	if (ns_changed)
+		this->params[NODE_Props].user++;
+	this->params[NODE_ProcessLatency].user++;
+
+	port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+	port->params[PORT_Latency].user++;
+}
+
+static int impl_node_set_param(void *object, uint32_t id, uint32_t flags,
+			       const struct spa_pod *param)
+{
+	struct state *this = object;
+	int res;
+
+	spa_return_val_if_fail(this != NULL, -EINVAL);
+
+	switch (id) {
+	case SPA_PARAM_Props:
+	{
+		struct props *p = &this->props;
+		struct spa_pod *params = NULL;
+		int64_t lat_ns = -1;
+
+		if (param == NULL) {
+			reset_props(p);
+			return 0;
+		}
+
+		spa_pod_parse_object(param,
+			SPA_TYPE_OBJECT_Props, NULL,
+			SPA_PROP_latencyOffsetNsec,   SPA_POD_OPT_Long(&lat_ns),
+			SPA_PROP_params,       SPA_POD_OPT_Pod(&params));
+
+		spa_avb_parse_prop_params(this, params);
+		if (lat_ns != -1) {
+			struct spa_process_latency_info info;
+			info = this->process_latency;
+			info.ns = lat_ns;
+			handle_process_latency(this, &info);
+		}
+		emit_node_info(this, false);
+		emit_port_info(this, &this->ports[0], false);
+		break;
+	}
+	case SPA_PARAM_ProcessLatency:
+	{
+		struct spa_process_latency_info info;
+		if ((res = spa_process_latency_parse(param, &info)) < 0)
+			return res;
+
+		handle_process_latency(this, &info);
+
+		emit_node_info(this, false);
+		emit_port_info(this, &this->ports[0], false);
+		break;
+	}
+	default:
+		return -ENOENT;
+	}
+	return 0;
+}
+
+static int impl_node_send_command(void *object, const struct spa_command *command)
+{
+	struct state *this = object;
+	int res;
+
+	spa_return_val_if_fail(this != NULL, -EINVAL);
+	spa_return_val_if_fail(command != NULL, -EINVAL);
+
+	switch (SPA_NODE_COMMAND_ID(command)) {
+	case SPA_NODE_COMMAND_ParamBegin:
+		break;
+	case SPA_NODE_COMMAND_ParamEnd:
+		break;
+	case SPA_NODE_COMMAND_Start:
+		if (!this->ports[0].have_format)
+			return -EIO;
+		if (this->ports[0].n_buffers == 0)
+			return -EIO;
+		if ((res = spa_avb_start(this)) < 0)
+			return res;
+		break;
+	case SPA_NODE_COMMAND_Suspend:
+	case SPA_NODE_COMMAND_Pause:
+		if ((res = spa_avb_pause(this)) < 0)
+			return res;
+		break;
+	default:
+		return -ENOTSUP;
+	}
+	return 0;
+}
+
+
+static int
+impl_node_add_listener(void *object,
+		struct spa_hook *listener,
+		const struct spa_node_events *events,
+		void *data)
+{
+	struct state *this = object;
+	struct spa_hook_list save;
+
+	spa_return_val_if_fail(this != NULL, -EINVAL);
+
+	spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
+
+	emit_node_info(this, true);
+	emit_port_info(this, &this->ports[0], true);
+
+	spa_hook_list_join(&this->hooks, &save);
+
+	return 0;
+}
+
+static int
+impl_node_set_callbacks(void *object,
+			const struct spa_node_callbacks *callbacks,
+			void *data)
+{
+	struct state *this = object;
+
+	spa_return_val_if_fail(this != NULL, -EINVAL);
+
+	this->callbacks = SPA_CALLBACKS_INIT(callbacks, data);
+
+	return 0;
+}
+
+static int
+impl_node_sync(void *object, int seq)
+{
+	struct state *this = object;
+
+	spa_return_val_if_fail(this != NULL, -EINVAL);
+
+	spa_node_emit_result(&this->hooks, seq, 0, 0, NULL);
+
+	return 0;
+}
+
+static int impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id,
+		const struct spa_dict *props)
+{
+	return -ENOTSUP;
+}
+
+static int impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id)
+{
+	return -ENOTSUP;
+}
+
+static int
+impl_node_port_enum_params(void *object, int seq,
+			   enum spa_direction direction, uint32_t port_id,
+			   uint32_t id, uint32_t start, uint32_t num,
+			   const struct spa_pod *filter)
+{
+
+	struct state *this = object;
+	struct spa_pod *param;
+	struct spa_pod_builder b = { 0 };
+	uint8_t buffer[1024];
+	struct spa_result_node_params result;
+	uint32_t count = 0;
+	struct port *port;
+
+	spa_return_val_if_fail(this != NULL, -EINVAL);
+	spa_return_val_if_fail(num != 0, -EINVAL);
+
+	spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+	port = GET_PORT(this, direction, port_id);
+
+	result.id = id;
+	result.next = start;
+      next:
+	result.index = result.next++;
+
+	spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+	switch (id) {
+	case SPA_PARAM_EnumFormat:
+		return spa_avb_enum_format(this, seq, start, num, filter);
+
+	case SPA_PARAM_Format:
+		if (!port->have_format)
+			return -EIO;
+		if (result.index > 0)
+			return 0;
+
+		param = spa_format_audio_raw_build(&b, id,
+					&port->current_format.info.raw);
+		break;
+
+	case SPA_PARAM_Buffers:
+		if (!port->have_format)
+			return -EIO;
+		if (result.index > 0)
+			return 0;
+
+		param = spa_pod_builder_add_object(&b,
+			SPA_TYPE_OBJECT_ParamBuffers, id,
+			SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(2, 1, MAX_BUFFERS),
+			SPA_PARAM_BUFFERS_blocks,  SPA_POD_Int(this->blocks),
+			SPA_PARAM_BUFFERS_size,    SPA_POD_CHOICE_RANGE_Int(
+							this->quantum_limit * this->stride,
+							16 * this->stride,
+							INT32_MAX),
+			SPA_PARAM_BUFFERS_stride,  SPA_POD_Int(this->stride));
+		break;
+
+	case SPA_PARAM_Meta:
+		switch (result.index) {
+		case 0:
+			param = spa_pod_builder_add_object(&b,
+				SPA_TYPE_OBJECT_ParamMeta, id,
+				SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header),
+				SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header)));
+			break;
+		default:
+			return 0;
+		}
+		break;
+
+	case SPA_PARAM_IO:
+		switch (result.index) {
+		case 0:
+			param = spa_pod_builder_add_object(&b,
+				SPA_TYPE_OBJECT_ParamIO, id,
+				SPA_PARAM_IO_id,   SPA_POD_Id(SPA_IO_Buffers),
+				SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers)));
+			break;
+		case 1:
+			param = spa_pod_builder_add_object(&b,
+				SPA_TYPE_OBJECT_ParamIO, id,
+				SPA_PARAM_IO_id,   SPA_POD_Id(SPA_IO_RateMatch),
+				SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_rate_match)));
+			break;
+		default:
+			return 0;
+		}
+		break;
+
+	case SPA_PARAM_Latency:
+		switch (result.index) {
+		case 0: case 1:
+		{
+			struct spa_latency_info latency = this->latency[result.index];
+			if (latency.direction == SPA_DIRECTION_INPUT)
+				spa_process_latency_info_add(&this->process_latency, &latency);
+			param = spa_latency_build(&b, id, &latency);
+			break;
+		}
+		default:
+			return 0;
+		}
+		break;
+
+	default:
+		return -ENOENT;
+	}
+
+	if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+		goto next;
+
+	spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+	if (++count != num)
+		goto next;
+
+	return 0;
+}
+
+static int clear_buffers(struct state *this, struct port *port)
+{
+	if (port->n_buffers > 0) {
+		spa_list_init(&port->ready);
+		port->n_buffers = 0;
+	}
+	return 0;
+}
+
+static int port_set_format(void *object, struct port *port,
+			   uint32_t flags, const struct spa_pod *format)
+{
+	struct state *this = object;
+	int err;
+
+	if (format == NULL) {
+		if (!port->have_format)
+			return 0;
+
+		spa_log_debug(this->log, "clear format");
+		port->have_format = false;
+		spa_avb_clear_format(this);
+		clear_buffers(this, port);
+	} else {
+		struct spa_audio_info info = { 0 };
+
+		if ((err = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0)
+			return err;
+
+		if (info.media_type != SPA_MEDIA_TYPE_audio ||
+		    info.media_subtype != SPA_MEDIA_SUBTYPE_raw)
+			return -EINVAL;
+
+		if (spa_format_audio_raw_parse(format, &info.info.raw) < 0)
+			return -EINVAL;
+
+		if ((err = spa_avb_set_format(this, &info, flags)) < 0)
+			return err;
+
+		port->current_format = info;
+		port->have_format = true;
+	}
+
+	this->info.change_mask |= SPA_NODE_CHANGE_MASK_PROPS;
+	emit_node_info(this, false);
+
+	port->info.change_mask |= SPA_PORT_CHANGE_MASK_RATE;
+	port->info.rate = SPA_FRACTION(1, this->rate);
+	port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+	if (port->have_format) {
+		port->params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE);
+		port->params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ);
+		port->params[PORT_Latency].user++;
+	} else {
+		port->params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+		port->params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+	}
+	emit_port_info(this, port, false);
+
+	return 0;
+}
+
+static int
+impl_node_port_set_param(void *object,
+			 enum spa_direction direction, uint32_t port_id,
+			 uint32_t id, uint32_t flags,
+			 const struct spa_pod *param)
+{
+	struct state *this = object;
+	struct port *port;
+	int res;
+
+	spa_return_val_if_fail(this != NULL, -EINVAL);
+
+	spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+	port = GET_PORT(this, direction, port_id);
+
+	switch (id) {
+	case SPA_PARAM_Format:
+		res = port_set_format(this, port, flags, param);
+		break;
+	case SPA_PARAM_Latency:
+	{
+		struct spa_latency_info info;
+		if ((res = spa_latency_parse(param, &info)) < 0)
+			return res;
+		if (direction == info.direction)
+			return -EINVAL;
+
+		this->latency[info.direction] = info;
+		port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+		port->params[PORT_Latency].user++;
+		emit_port_info(this, port, false);
+		break;
+	}
+	default:
+		res = -ENOENT;
+		break;
+	}
+	return res;
+}
+
+static int
+impl_node_port_use_buffers(void *object,
+			   enum spa_direction direction, uint32_t port_id,
+			   uint32_t flags,
+			   struct spa_buffer **buffers, uint32_t n_buffers)
+{
+	struct state *this = object;
+	struct port *port;
+	uint32_t i;
+
+	spa_return_val_if_fail(this != NULL, -EINVAL);
+
+	spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+	port = GET_PORT(this, direction, port_id);
+
+	spa_log_debug(this->log, "%p: use %d buffers", this, n_buffers);
+
+	if (!port->have_format)
+		return -EIO;
+
+	if (n_buffers == 0) {
+		spa_avb_pause(this);
+		clear_buffers(this, port);
+		return 0;
+	}
+
+	for (i = 0; i < n_buffers; i++) {
+		struct buffer *b = &port->buffers[i];
+		struct spa_data *d = buffers[i]->datas;
+
+		b->buf = buffers[i];
+		b->id = i;
+		b->flags = BUFFER_FLAG_OUT;
+
+		b->h = spa_buffer_find_meta_data(b->buf, SPA_META_Header, sizeof(*b->h));
+
+		if (d[0].data == NULL) {
+			spa_log_error(this->log, "%p: need mapped memory", this);
+			return -EINVAL;
+		}
+		spa_log_debug(this->log, "%p: %d %p data:%p", this, i, b->buf, d[0].data);
+	}
+	port->n_buffers = n_buffers;
+
+	return 0;
+}
+
+static int
+impl_node_port_set_io(void *object,
+		      enum spa_direction direction,
+		      uint32_t port_id,
+		      uint32_t id,
+		      void *data, size_t size)
+{
+	struct state *this = object;
+	struct port *port;
+
+	spa_return_val_if_fail(this != NULL, -EINVAL);
+
+	spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+	port = GET_PORT(this, direction, port_id);
+
+	spa_log_debug(this->log, "%p: io %d %p %zd", this, id, data, size);
+
+	switch (id) {
+	case SPA_IO_Buffers:
+		port->io = data;
+		break;
+	case SPA_IO_RateMatch:
+		port->rate_match = data;
+		break;
+	default:
+		return -ENOENT;
+	}
+	return 0;
+}
+
+static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id)
+{
+	return -ENOTSUP;
+}
+
+static int impl_node_process(void *object)
+{
+	struct state *this = object;
+	struct port *port;
+	struct spa_io_buffers *io;
+
+	spa_return_val_if_fail(this != NULL, -EINVAL);
+
+	port = GET_PORT(this, SPA_DIRECTION_INPUT, 0);
+	if ((io = port->io) == NULL)
+		return -EIO;
+
+	spa_log_trace_fp(this->log, "%p: process %d %d/%d", this, io->status,
+			io->buffer_id, port->n_buffers);
+
+	if (this->position && this->position->clock.flags & SPA_IO_CLOCK_FLAG_FREEWHEEL) {
+		io->status = SPA_STATUS_NEED_DATA;
+		return SPA_STATUS_HAVE_DATA;
+	}
+	if (io->status == SPA_STATUS_HAVE_DATA &&
+	    io->buffer_id < port->n_buffers) {
+		struct buffer *b = &port->buffers[io->buffer_id];
+
+		if (!SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_OUT)) {
+			spa_log_warn(this->log, "%p: buffer %u in use",
+					this, io->buffer_id);
+			io->status = -EINVAL;
+			return -EINVAL;
+		}
+		spa_log_trace_fp(this->log, "%p: queue buffer %u", this, io->buffer_id);
+		spa_list_append(&port->ready, &b->link);
+		SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_OUT);
+		io->buffer_id = SPA_ID_INVALID;
+
+		spa_avb_write(this);
+
+		io->status = SPA_STATUS_OK;
+	}
+	return SPA_STATUS_HAVE_DATA;
+}
+
+static const struct spa_node_methods impl_node = {
+	SPA_VERSION_NODE_METHODS,
+	.add_listener = impl_node_add_listener,
+	.set_callbacks = impl_node_set_callbacks,
+	.sync = impl_node_sync,
+	.enum_params = impl_node_enum_params,
+	.set_param = impl_node_set_param,
+	.set_io = impl_node_set_io,
+	.send_command = impl_node_send_command,
+	.add_port = impl_node_add_port,
+	.remove_port = impl_node_remove_port,
+	.port_enum_params = impl_node_port_enum_params,
+	.port_set_param = impl_node_port_set_param,
+	.port_use_buffers = impl_node_port_use_buffers,
+	.port_set_io = impl_node_port_set_io,
+	.port_reuse_buffer = impl_node_port_reuse_buffer,
+	.process = impl_node_process,
+};
+
+static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface)
+{
+	struct state *this;
+
+	spa_return_val_if_fail(handle != NULL, -EINVAL);
+	spa_return_val_if_fail(interface != NULL, -EINVAL);
+
+	this = (struct state *) handle;
+
+	if (spa_streq(type, SPA_TYPE_INTERFACE_Node))
+		*interface = &this->node;
+	else
+		return -ENOENT;
+
+	return 0;
+}
+
+static int impl_clear(struct spa_handle *handle)
+{
+	struct state *this;
+	spa_return_val_if_fail(handle != NULL, -EINVAL);
+	this = (struct state *) handle;
+	spa_avb_clear(this);
+	return 0;
+}
+
+static size_t
+impl_get_size(const struct spa_handle_factory *factory,
+	      const struct spa_dict *params)
+{
+	return sizeof(struct state);
+}
+
+static int
+impl_init(const struct spa_handle_factory *factory,
+	  struct spa_handle *handle, const struct spa_dict *info, const struct spa_support *support, uint32_t n_support)
+{
+	struct state *this;
+	struct port *port;
+
+	spa_return_val_if_fail(factory != NULL, -EINVAL);
+	spa_return_val_if_fail(handle != NULL, -EINVAL);
+
+	handle->get_interface = impl_get_interface;
+	handle->clear = impl_clear;
+
+	this = (struct state *) handle;
+
+	this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
+	avb_log_topic_init(this->log);
+
+	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);
+
+	if (this->data_loop == NULL) {
+		spa_log_error(this->log, "a data loop is needed");
+		return -EINVAL;
+	}
+	if (this->data_system == NULL) {
+		spa_log_error(this->log, "a data system is needed");
+		return -EINVAL;
+	}
+
+	this->node.iface = SPA_INTERFACE_INIT(
+			SPA_TYPE_INTERFACE_Node,
+			SPA_VERSION_NODE,
+			&impl_node, this);
+
+	spa_hook_list_init(&this->hooks);
+
+
+	this->info_all = SPA_NODE_CHANGE_MASK_FLAGS |
+			SPA_NODE_CHANGE_MASK_PROPS |
+			SPA_NODE_CHANGE_MASK_PARAMS;
+	this->info = SPA_NODE_INFO_INIT();
+	this->info.max_input_ports = 1;
+	this->info.flags = SPA_NODE_FLAG_RT;
+	this->params[NODE_PropInfo] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ);
+	this->params[NODE_Props] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE);
+	this->params[NODE_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
+	this->params[NODE_ProcessLatency] = SPA_PARAM_INFO(SPA_PARAM_ProcessLatency, SPA_PARAM_INFO_READWRITE);
+	this->info.params = this->params;
+	this->info.n_params = N_NODE_PARAMS;
+
+	reset_props(&this->props);
+
+	port = GET_PORT(this, SPA_DIRECTION_INPUT, 0);
+	port->direction = SPA_DIRECTION_INPUT;
+
+	port->info_all = SPA_PORT_CHANGE_MASK_FLAGS |
+				 SPA_PORT_CHANGE_MASK_PARAMS;
+	port->info = SPA_PORT_INFO_INIT();
+	port->info.flags = SPA_PORT_FLAG_LIVE |
+			   SPA_PORT_FLAG_PHYSICAL |
+			   SPA_PORT_FLAG_TERMINAL;
+	port->params[PORT_EnumFormat] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ);
+	port->params[PORT_Meta] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ);
+	port->params[PORT_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
+	port->params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+	port->params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+	port->params[PORT_Latency] = SPA_PARAM_INFO(SPA_PARAM_Latency, SPA_PARAM_INFO_READWRITE);
+	port->info.params = port->params;
+	port->info.n_params = N_PORT_PARAMS;
+
+	spa_list_init(&port->ready);
+
+	this->latency[port->direction] = SPA_LATENCY_INFO(
+			port->direction,
+			.min_quantum = 1.0f,
+			.max_quantum = 1.0f);
+	this->latency[SPA_DIRECTION_OUTPUT] = SPA_LATENCY_INFO(SPA_DIRECTION_OUTPUT);
+
+	return spa_avb_init(this, info);
+}
+
+static const struct spa_interface_info impl_interfaces[] = {
+	{SPA_TYPE_INTERFACE_Node,},
+};
+
+static int
+impl_enum_interface_info(const struct spa_handle_factory *factory,
+			 const struct spa_interface_info **info, uint32_t *index)
+{
+	spa_return_val_if_fail(factory != NULL, -EINVAL);
+	spa_return_val_if_fail(info != NULL, -EINVAL);
+	spa_return_val_if_fail(index != NULL, -EINVAL);
+
+	switch (*index) {
+	case 0:
+		*info = &impl_interfaces[*index];
+		break;
+	default:
+		return 0;
+	}
+	(*index)++;
+	return 1;
+}
+
+static const struct spa_dict_item info_items[] = {
+	{ SPA_KEY_FACTORY_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+	{ SPA_KEY_FACTORY_DESCRIPTION, "Play audio with AVB" },
+	{ SPA_KEY_FACTORY_USAGE, "[]" },
+};
+
+static const struct spa_dict info = SPA_DICT_INIT_ARRAY(info_items);
+
+const struct spa_handle_factory spa_avb_sink_factory = {
+	SPA_VERSION_HANDLE_FACTORY,
+	"avb.pcm.sink",
+	&info,
+	impl_get_size,
+	impl_init,
+	impl_enum_interface_info,
+};
diff --git a/spa/plugins/avb/avb-pcm-source.c b/spa/plugins/avb/avb-pcm-source.c
new file mode 100644
index 0000000000000000000000000000000000000000..5bf0a6e51bb64adaaa91bcb3d51e87019ba90e13
--- /dev/null
+++ b/spa/plugins/avb/avb-pcm-source.c
@@ -0,0 +1,911 @@
+/* Spa AVB PCM Source
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stddef.h>
+
+#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>
+#include <spa/param/audio/format.h>
+#include <spa/pod/filter.h>
+#include <spa/debug/pod.h>
+
+#include "avb-pcm.h"
+
+#define CHECK_PORT(this,d,p)    ((d) == SPA_DIRECTION_OUTPUT && (p) == 0)
+#define GET_PORT(this,d,p)	(&this->ports[p])
+
+static void reset_props(struct props *props)
+{
+	snprintf(props->ifname, sizeof(props->ifname), "%s", DEFAULT_IFNAME);
+	parse_addr(props->addr, DEFAULT_ADDR);
+	props->prio = DEFAULT_PRIO;
+	parse_streamid(&props->streamid, DEFAULT_STREAMID);
+	props->mtt = DEFAULT_MTT;
+	props->t_uncertainty = DEFAULT_TU;
+	props->frames_per_pdu = DEFAULT_FRAMES_PER_PDU;
+}
+
+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[4];
+		uint32_t i, n_items = 0;
+
+		items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_API, "avb");
+		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");
+		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, struct port *port, bool full)
+{
+	uint64_t old = full ? port->info.change_mask : 0;
+
+	if (full)
+		port->info.change_mask = port->info_all;
+	if (port->info.change_mask) {
+		uint32_t i;
+
+		if (port->info.change_mask & SPA_PORT_CHANGE_MASK_PARAMS) {
+			for (i = 0; i < port->info.n_params; i++) {
+				if (port->params[i].user > 0) {
+					port->params[i].flags ^= SPA_PARAM_INFO_SERIAL;
+					port->params[i].user = 0;
+				}
+			}
+		}
+		spa_node_emit_port_info(&this->hooks,
+				port->direction, port->id, &port->info);
+		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)
+{
+	struct state *this = object;
+	struct spa_pod *param;
+	struct spa_pod_builder b = { 0 };
+	uint8_t buffer[4096];
+	struct spa_result_node_params result;
+	uint32_t count = 0;
+
+	spa_return_val_if_fail(this != NULL, -EINVAL);
+	spa_return_val_if_fail(num != 0, -EINVAL);
+
+	result.id = id;
+	result.next = start;
+      next:
+	result.index = result.next++;
+
+	spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+	switch (id) {
+	case SPA_PARAM_PropInfo:
+	{
+		switch (result.index) {
+		default:
+			param = spa_avb_enum_propinfo(this, result.index, &b);
+			if (param == NULL)
+				return 0;
+		}
+		break;
+	}
+	case SPA_PARAM_Props:
+	{
+		struct spa_pod_frame f;
+
+		switch (result.index) {
+		case 0:
+			spa_pod_builder_push_object(&b, &f,
+                                SPA_TYPE_OBJECT_Props, id);
+			spa_pod_builder_add(&b,
+				SPA_PROP_latencyOffsetNsec,   SPA_POD_Long(this->process_latency.ns),
+				0);
+			spa_avb_add_prop_params(this, &b);
+			param = spa_pod_builder_pop(&b, &f);
+			break;
+		default:
+			return 0;
+		}
+		break;
+	}
+	case SPA_PARAM_IO:
+		switch (result.index) {
+		case 0:
+			param = spa_pod_builder_add_object(&b,
+				SPA_TYPE_OBJECT_ParamIO, id,
+				SPA_PARAM_IO_id,   SPA_POD_Id(SPA_IO_Clock),
+				SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_clock)));
+			break;
+		case 1:
+			param = spa_pod_builder_add_object(&b,
+				SPA_TYPE_OBJECT_ParamIO, id,
+				SPA_PARAM_IO_id,   SPA_POD_Id(SPA_IO_Position),
+				SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_position)));
+			break;
+		default:
+			return 0;
+		}
+		break;
+
+	case SPA_PARAM_ProcessLatency:
+		switch (result.index) {
+		case 0:
+			param = spa_process_latency_build(&b, id, &this->process_latency);
+			break;
+		default:
+			return 0;
+		}
+		break;
+
+	default:
+		return -ENOENT;
+	}
+
+	if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+		goto next;
+
+	spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+	if (++count != num)
+		goto next;
+
+	return 0;
+}
+
+static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size)
+{
+	struct state *this = object;
+
+	spa_return_val_if_fail(this != NULL, -EINVAL);
+
+	switch (id) {
+	case SPA_IO_Clock:
+		this->clock = data;
+		break;
+	case SPA_IO_Position:
+		this->position = data;
+		break;
+	default:
+		return -ENOENT;
+	}
+	spa_avb_reassign_follower(this);
+
+	return 0;
+}
+
+static void handle_process_latency(struct state *this,
+		const struct spa_process_latency_info *info)
+{
+	bool ns_changed = this->process_latency.ns != info->ns;
+	struct port *port = &this->ports[0];
+
+	if (this->process_latency.quantum == info->quantum &&
+	    this->process_latency.rate == info->rate &&
+	    !ns_changed)
+		return;
+
+	this->process_latency = *info;
+
+	this->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS;
+	if (ns_changed)
+		this->params[NODE_Props].user++;
+	this->params[NODE_ProcessLatency].user++;
+
+	port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+	port->params[PORT_Latency].user++;
+}
+
+static int impl_node_set_param(void *object, uint32_t id, uint32_t flags,
+			       const struct spa_pod *param)
+{
+	struct state *this = object;
+	int res;
+
+	spa_return_val_if_fail(this != NULL, -EINVAL);
+
+	switch (id) {
+	case SPA_PARAM_Props:
+	{
+		struct props *p = &this->props;
+		struct spa_pod *params = NULL;
+		int64_t lat_ns = -1;
+
+		if (param == NULL) {
+			reset_props(p);
+			return 0;
+		}
+
+		spa_pod_parse_object(param,
+			SPA_TYPE_OBJECT_Props, NULL,
+			SPA_PROP_latencyOffsetNsec,   SPA_POD_OPT_Long(&lat_ns),
+			SPA_PROP_params,       SPA_POD_OPT_Pod(&params));
+
+		spa_avb_parse_prop_params(this, params);
+		if (lat_ns != -1) {
+			struct spa_process_latency_info info;
+			info = this->process_latency;
+			info.ns = lat_ns;
+			handle_process_latency(this, &info);
+		}
+		emit_node_info(this, false);
+		emit_port_info(this, &this->ports[0], false);
+		break;
+	}
+	case SPA_PARAM_ProcessLatency:
+	{
+		struct spa_process_latency_info info;
+		if ((res = spa_process_latency_parse(param, &info)) < 0)
+			return res;
+
+		handle_process_latency(this, &info);
+
+		emit_node_info(this, false);
+		emit_port_info(this, &this->ports[0], false);
+		break;
+	}
+	default:
+		return -ENOENT;
+	}
+	return 0;
+}
+
+static int impl_node_send_command(void *object, const struct spa_command *command)
+{
+	struct state *this = object;
+	int res;
+
+	spa_return_val_if_fail(this != NULL, -EINVAL);
+	spa_return_val_if_fail(command != NULL, -EINVAL);
+
+	switch (SPA_NODE_COMMAND_ID(command)) {
+	case SPA_NODE_COMMAND_ParamBegin:
+		break;
+	case SPA_NODE_COMMAND_ParamEnd:
+		break;
+	case SPA_NODE_COMMAND_Start:
+		if (!this->ports[0].have_format)
+			return -EIO;
+		if (this->ports[0].n_buffers == 0)
+			return -EIO;
+		if ((res = spa_avb_start(this)) < 0)
+			return res;
+		break;
+	case SPA_NODE_COMMAND_Suspend:
+	case SPA_NODE_COMMAND_Pause:
+		if ((res = spa_avb_pause(this)) < 0)
+			return res;
+		break;
+	default:
+		return -ENOTSUP;
+	}
+	return 0;
+}
+
+
+static int
+impl_node_add_listener(void *object,
+		struct spa_hook *listener,
+		const struct spa_node_events *events,
+		void *data)
+{
+	struct state *this = object;
+	struct spa_hook_list save;
+
+	spa_return_val_if_fail(this != NULL, -EINVAL);
+
+	spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
+
+	emit_node_info(this, true);
+	emit_port_info(this, &this->ports[0], true);
+
+	spa_hook_list_join(&this->hooks, &save);
+
+	return 0;
+}
+
+static int
+impl_node_set_callbacks(void *object,
+			const struct spa_node_callbacks *callbacks,
+			void *data)
+{
+	struct state *this = object;
+
+	spa_return_val_if_fail(this != NULL, -EINVAL);
+
+	this->callbacks = SPA_CALLBACKS_INIT(callbacks, data);
+
+	return 0;
+}
+
+static int
+impl_node_sync(void *object, int seq)
+{
+	struct state *this = object;
+
+	spa_return_val_if_fail(this != NULL, -EINVAL);
+
+	spa_node_emit_result(&this->hooks, seq, 0, 0, NULL);
+
+	return 0;
+}
+
+static int impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id,
+		const struct spa_dict *props)
+{
+	return -ENOTSUP;
+}
+
+static int impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id)
+{
+	return -ENOTSUP;
+}
+
+static int
+impl_node_port_enum_params(void *object, int seq,
+			   enum spa_direction direction, uint32_t port_id,
+			   uint32_t id, uint32_t start, uint32_t num,
+			   const struct spa_pod *filter)
+{
+
+	struct state *this = object;
+	struct spa_pod *param;
+	struct spa_pod_builder b = { 0 };
+	uint8_t buffer[1024];
+	struct spa_result_node_params result;
+	uint32_t count = 0;
+	struct port *port;
+
+	spa_return_val_if_fail(this != NULL, -EINVAL);
+	spa_return_val_if_fail(num != 0, -EINVAL);
+
+	spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+	port = GET_PORT(this, direction, port_id);
+
+	result.id = id;
+	result.next = start;
+      next:
+	result.index = result.next++;
+
+	spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+	switch (id) {
+	case SPA_PARAM_EnumFormat:
+		return spa_avb_enum_format(this, seq, start, num, filter);
+
+	case SPA_PARAM_Format:
+		if (!port->have_format)
+			return -EIO;
+		if (result.index > 0)
+			return 0;
+
+		param = spa_format_audio_raw_build(&b, id,
+					&port->current_format.info.raw);
+		break;
+
+	case SPA_PARAM_Buffers:
+		if (!port->have_format)
+			return -EIO;
+		if (result.index > 0)
+			return 0;
+
+		param = spa_pod_builder_add_object(&b,
+			SPA_TYPE_OBJECT_ParamBuffers, id,
+			SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(2, 1, MAX_BUFFERS),
+			SPA_PARAM_BUFFERS_blocks,  SPA_POD_Int(this->blocks),
+			SPA_PARAM_BUFFERS_size,    SPA_POD_CHOICE_RANGE_Int(
+							this->quantum_limit * this->stride,
+							16 * this->stride,
+							INT32_MAX),
+			SPA_PARAM_BUFFERS_stride,  SPA_POD_Int(this->stride));
+		break;
+
+	case SPA_PARAM_Meta:
+		switch (result.index) {
+		case 0:
+			param = spa_pod_builder_add_object(&b,
+				SPA_TYPE_OBJECT_ParamMeta, id,
+				SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header),
+				SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header)));
+			break;
+		default:
+			return 0;
+		}
+		break;
+
+	case SPA_PARAM_IO:
+		switch (result.index) {
+		case 0:
+			param = spa_pod_builder_add_object(&b,
+				SPA_TYPE_OBJECT_ParamIO, id,
+				SPA_PARAM_IO_id,   SPA_POD_Id(SPA_IO_Buffers),
+				SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers)));
+			break;
+		case 1:
+			param = spa_pod_builder_add_object(&b,
+				SPA_TYPE_OBJECT_ParamIO, id,
+				SPA_PARAM_IO_id,   SPA_POD_Id(SPA_IO_RateMatch),
+				SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_rate_match)));
+			break;
+		default:
+			return 0;
+		}
+		break;
+
+	case SPA_PARAM_Latency:
+		switch (result.index) {
+		case 0: case 1:
+		{
+			struct spa_latency_info latency = this->latency[result.index];
+			if (latency.direction == SPA_DIRECTION_OUTPUT)
+				spa_process_latency_info_add(&this->process_latency, &latency);
+			param = spa_latency_build(&b, id, &latency);
+			break;
+		}
+		default:
+			return 0;
+		}
+		break;
+
+	default:
+		return -ENOENT;
+	}
+
+	if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+		goto next;
+
+	spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+	if (++count != num)
+		goto next;
+
+	return 0;
+}
+
+static int clear_buffers(struct state *this, struct port *port)
+{
+	if (port->n_buffers > 0) {
+		spa_list_init(&port->ready);
+		port->n_buffers = 0;
+	}
+	return 0;
+}
+
+static int port_set_format(void *object, struct port *port,
+			   uint32_t flags, const struct spa_pod *format)
+{
+	struct state *this = object;
+	int err;
+
+	if (format == NULL) {
+		if (!port->have_format)
+			return 0;
+
+		spa_log_debug(this->log, "clear format");
+		port->have_format = false;
+		spa_avb_clear_format(this);
+		clear_buffers(this, port);
+	} else {
+		struct spa_audio_info info = { 0 };
+
+		if ((err = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0)
+			return err;
+
+		if (info.media_type != SPA_MEDIA_TYPE_audio ||
+		    info.media_subtype != SPA_MEDIA_SUBTYPE_raw)
+			return -EINVAL;
+
+		if (spa_format_audio_raw_parse(format, &info.info.raw) < 0)
+			return -EINVAL;
+
+		if ((err = spa_avb_set_format(this, &info, flags)) < 0)
+			return err;
+
+		port->current_format = info;
+		port->have_format = true;
+	}
+
+	this->info.change_mask |= SPA_NODE_CHANGE_MASK_PROPS;
+	emit_node_info(this, false);
+
+	port->info.change_mask |= SPA_PORT_CHANGE_MASK_RATE;
+	port->info.rate = SPA_FRACTION(1, this->rate);
+	port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+	if (port->have_format) {
+		port->params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE);
+		port->params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ);
+		port->params[PORT_Latency].user++;
+	} else {
+		port->params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+		port->params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+	}
+	emit_port_info(this, port, false);
+
+	return 0;
+}
+
+static int
+impl_node_port_set_param(void *object,
+			 enum spa_direction direction, uint32_t port_id,
+			 uint32_t id, uint32_t flags,
+			 const struct spa_pod *param)
+{
+	struct state *this = object;
+	struct port *port;
+	int res;
+
+	spa_return_val_if_fail(this != NULL, -EINVAL);
+
+	spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+	port = GET_PORT(this, direction, port_id);
+
+	switch (id) {
+	case SPA_PARAM_Format:
+		res = port_set_format(this, port, flags, param);
+		break;
+	case SPA_PARAM_Latency:
+	{
+		struct spa_latency_info info;
+		if ((res = spa_latency_parse(param, &info)) < 0)
+			return res;
+		if (direction == info.direction)
+			return -EINVAL;
+
+		this->latency[info.direction] = info;
+		port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+		port->params[PORT_Latency].user++;
+		emit_port_info(this, port, false);
+		break;
+	}
+	default:
+		res = -ENOENT;
+		break;
+	}
+	return res;
+}
+
+static int
+impl_node_port_use_buffers(void *object,
+			   enum spa_direction direction, uint32_t port_id,
+			   uint32_t flags,
+			   struct spa_buffer **buffers, uint32_t n_buffers)
+{
+	struct state *this = object;
+	struct port *port;
+	uint32_t i;
+
+	spa_return_val_if_fail(this != NULL, -EINVAL);
+
+	spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+	port = GET_PORT(this, direction, port_id);
+
+	spa_log_debug(this->log, "%p: use %d buffers", this, n_buffers);
+
+	if (!port->have_format)
+		return -EIO;
+
+	if (n_buffers == 0) {
+		spa_avb_pause(this);
+		clear_buffers(this, port);
+		return 0;
+	}
+
+	for (i = 0; i < n_buffers; i++) {
+		struct buffer *b = &port->buffers[i];
+		struct spa_data *d = buffers[i]->datas;
+
+		b->buf = buffers[i];
+		b->id = i;
+		b->flags = BUFFER_FLAG_OUT;
+
+		b->h = spa_buffer_find_meta_data(b->buf, SPA_META_Header, sizeof(*b->h));
+
+		if (d[0].data == NULL) {
+			spa_log_error(this->log, "%p: need mapped memory", this);
+			return -EINVAL;
+		}
+		spa_log_debug(this->log, "%p: %d %p data:%p", this, i, b->buf, d[0].data);
+	}
+	port->n_buffers = n_buffers;
+
+	return 0;
+}
+
+static int
+impl_node_port_set_io(void *object,
+		      enum spa_direction direction,
+		      uint32_t port_id,
+		      uint32_t id,
+		      void *data, size_t size)
+{
+	struct state *this = object;
+	struct port *port;
+
+	spa_return_val_if_fail(this != NULL, -EINVAL);
+
+	spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+	port = GET_PORT(this, direction, port_id);
+
+	spa_log_debug(this->log, "%p: io %d %p %zd", this, id, data, size);
+
+	switch (id) {
+	case SPA_IO_Buffers:
+		port->io = data;
+		break;
+	case SPA_IO_RateMatch:
+		port->rate_match = data;
+		break;
+	default:
+		return -ENOENT;
+	}
+	return 0;
+}
+
+static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id)
+{
+	return -ENOTSUP;
+}
+
+static int impl_node_process(void *object)
+{
+	struct state *this = object;
+	struct port *port;
+	struct spa_io_buffers *io;
+	struct buffer *b;
+
+	spa_return_val_if_fail(this != NULL, -EINVAL);
+
+	port = GET_PORT(this, SPA_DIRECTION_OUTPUT, 0);
+	if ((io = port->io) == NULL)
+		return -EIO;
+
+	spa_log_trace_fp(this->log, "%p: process %d %d/%d %d", this, io->status,
+			io->buffer_id, port->n_buffers, this->following);
+
+	if (io->status == SPA_STATUS_HAVE_DATA)
+		return SPA_STATUS_HAVE_DATA;
+
+	if (io->buffer_id < port->n_buffers) {
+		spa_avb_recycle_buffer(this, port, io->buffer_id);
+		io->buffer_id = SPA_ID_INVALID;
+	}
+
+	if (spa_list_is_empty(&port->ready) && this->following) {
+		spa_avb_read(this);
+	}
+	if (spa_list_is_empty(&port->ready) || !this->following)
+		return SPA_STATUS_OK;
+
+	b = spa_list_first(&port->ready, struct buffer, link);
+	spa_list_remove(&b->link);
+	SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUT);
+
+	spa_log_trace_fp(this->log, "%p: dequeue buffer %d", this, b->id);
+
+	io->buffer_id = b->id;
+	io->status = SPA_STATUS_HAVE_DATA;
+
+	return SPA_STATUS_HAVE_DATA;
+}
+
+static const struct spa_node_methods impl_node = {
+	SPA_VERSION_NODE_METHODS,
+	.add_listener = impl_node_add_listener,
+	.set_callbacks = impl_node_set_callbacks,
+	.sync = impl_node_sync,
+	.enum_params = impl_node_enum_params,
+	.set_param = impl_node_set_param,
+	.set_io = impl_node_set_io,
+	.send_command = impl_node_send_command,
+	.add_port = impl_node_add_port,
+	.remove_port = impl_node_remove_port,
+	.port_enum_params = impl_node_port_enum_params,
+	.port_set_param = impl_node_port_set_param,
+	.port_use_buffers = impl_node_port_use_buffers,
+	.port_set_io = impl_node_port_set_io,
+	.port_reuse_buffer = impl_node_port_reuse_buffer,
+	.process = impl_node_process,
+};
+
+static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface)
+{
+	struct state *this;
+
+	spa_return_val_if_fail(handle != NULL, -EINVAL);
+	spa_return_val_if_fail(interface != NULL, -EINVAL);
+
+	this = (struct state *) handle;
+
+	if (spa_streq(type, SPA_TYPE_INTERFACE_Node))
+		*interface = &this->node;
+	else
+		return -ENOENT;
+
+	return 0;
+}
+
+static int impl_clear(struct spa_handle *handle)
+{
+	struct state *this;
+	spa_return_val_if_fail(handle != NULL, -EINVAL);
+	this = (struct state *) handle;
+	spa_avb_clear(this);
+	return 0;
+}
+
+static size_t
+impl_get_size(const struct spa_handle_factory *factory,
+	      const struct spa_dict *params)
+{
+	return sizeof(struct state);
+}
+
+static int
+impl_init(const struct spa_handle_factory *factory,
+	  struct spa_handle *handle, const struct spa_dict *info, const struct spa_support *support, uint32_t n_support)
+{
+	struct state *this;
+	struct port *port;
+
+	spa_return_val_if_fail(factory != NULL, -EINVAL);
+	spa_return_val_if_fail(handle != NULL, -EINVAL);
+
+	handle->get_interface = impl_get_interface;
+	handle->clear = impl_clear;
+
+	this = (struct state *) handle;
+
+	this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
+	avb_log_topic_init(this->log);
+
+	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);
+
+	if (this->data_loop == NULL) {
+		spa_log_error(this->log, "a data loop is needed");
+		return -EINVAL;
+	}
+	if (this->data_system == NULL) {
+		spa_log_error(this->log, "a data system is needed");
+		return -EINVAL;
+	}
+
+	this->node.iface = SPA_INTERFACE_INIT(
+			SPA_TYPE_INTERFACE_Node,
+			SPA_VERSION_NODE,
+			&impl_node, this);
+
+	spa_hook_list_init(&this->hooks);
+
+	this->info_all = SPA_NODE_CHANGE_MASK_FLAGS |
+			SPA_NODE_CHANGE_MASK_PROPS |
+			SPA_NODE_CHANGE_MASK_PARAMS;
+	this->info = SPA_NODE_INFO_INIT();
+	this->info.max_output_ports = 1;
+	this->info.flags = SPA_NODE_FLAG_RT;
+	this->params[NODE_PropInfo] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ);
+	this->params[NODE_Props] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE);
+	this->params[NODE_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
+	this->params[NODE_ProcessLatency] = SPA_PARAM_INFO(SPA_PARAM_ProcessLatency, SPA_PARAM_INFO_READWRITE);
+	this->info.params = this->params;
+	this->info.n_params = N_NODE_PARAMS;
+
+	reset_props(&this->props);
+
+	port = GET_PORT(this, SPA_DIRECTION_OUTPUT, 0);
+	port->direction = SPA_DIRECTION_OUTPUT;
+
+	port->info_all = SPA_PORT_CHANGE_MASK_FLAGS |
+				 SPA_PORT_CHANGE_MASK_PARAMS;
+	port->info = SPA_PORT_INFO_INIT();
+	port->info.flags = SPA_PORT_FLAG_LIVE |
+			   SPA_PORT_FLAG_PHYSICAL |
+			   SPA_PORT_FLAG_TERMINAL;
+	port->params[PORT_EnumFormat] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ);
+	port->params[PORT_Meta] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ);
+	port->params[PORT_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
+	port->params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+	port->params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+	port->params[PORT_Latency] = SPA_PARAM_INFO(SPA_PARAM_Latency, SPA_PARAM_INFO_READWRITE);
+	port->info.params = port->params;
+	port->info.n_params = N_PORT_PARAMS;
+
+	spa_list_init(&port->ready);
+
+	this->latency[port->direction] = SPA_LATENCY_INFO(
+			port->direction,
+			.min_quantum = 1.0f,
+			.max_quantum = 1.0f);
+	this->latency[SPA_DIRECTION_INPUT] = SPA_LATENCY_INFO(SPA_DIRECTION_INPUT);
+
+	return spa_avb_init(this, info);
+}
+
+static const struct spa_interface_info impl_interfaces[] = {
+	{SPA_TYPE_INTERFACE_Node,},
+};
+
+static int
+impl_enum_interface_info(const struct spa_handle_factory *factory,
+			 const struct spa_interface_info **info, uint32_t *index)
+{
+	spa_return_val_if_fail(factory != NULL, -EINVAL);
+	spa_return_val_if_fail(info != NULL, -EINVAL);
+	spa_return_val_if_fail(index != NULL, -EINVAL);
+
+	switch (*index) {
+	case 0:
+		*info = &impl_interfaces[*index];
+		break;
+	default:
+		return 0;
+	}
+	(*index)++;
+	return 1;
+}
+
+static const struct spa_dict_item info_items[] = {
+	{ SPA_KEY_FACTORY_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+	{ SPA_KEY_FACTORY_DESCRIPTION, "Play audio with AVB" },
+	{ SPA_KEY_FACTORY_USAGE, "[]" },
+};
+
+static const struct spa_dict info = SPA_DICT_INIT_ARRAY(info_items);
+
+const struct spa_handle_factory spa_avb_source_factory = {
+	SPA_VERSION_HANDLE_FACTORY,
+	"avb.pcm.source",
+	&info,
+	impl_get_size,
+	impl_init,
+	impl_enum_interface_info,
+};
diff --git a/spa/plugins/avb/avb-pcm.c b/spa/plugins/avb/avb-pcm.c
new file mode 100644
index 0000000000000000000000000000000000000000..c7ccb05b666753640a1ceafe5f8d793747de81da
--- /dev/null
+++ b/spa/plugins/avb/avb-pcm.c
@@ -0,0 +1,1217 @@
+/* Spa AVB PCM
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sched.h>
+#include <errno.h>
+#include <getopt.h>
+#include <sys/time.h>
+#include <math.h>
+#include <limits.h>
+#include <unistd.h>
+#include <sys/ioctl.h>
+#include <arpa/inet.h>
+
+#include <spa/pod/filter.h>
+#include <spa/utils/string.h>
+#include <spa/support/system.h>
+#include <spa/utils/keys.h>
+
+#include "avb-pcm.h"
+
+#define TAI_OFFSET    (37ULL * SPA_NSEC_PER_SEC)
+#define TAI_TO_UTC(t) (t - TAI_OFFSET)
+
+static int avb_set_param(struct state *state, const char *k, const char *s)
+{
+	struct props *p = &state->props;
+	int fmt_change = 0;
+	if (spa_streq(k, SPA_KEY_AUDIO_CHANNELS)) {
+		state->default_channels = atoi(s);
+		fmt_change++;
+	} else if (spa_streq(k, SPA_KEY_AUDIO_RATE)) {
+		state->default_rate = atoi(s);
+		fmt_change++;
+	} else if (spa_streq(k, SPA_KEY_AUDIO_FORMAT)) {
+		state->default_format = spa_avb_format_from_name(s, strlen(s));
+		fmt_change++;
+	} else if (spa_streq(k, SPA_KEY_AUDIO_POSITION)) {
+		spa_avb_parse_position(&state->default_pos, s, strlen(s));
+		fmt_change++;
+	} else if (spa_streq(k, SPA_KEY_AUDIO_ALLOWED_RATES)) {
+		state->n_allowed_rates = spa_avb_parse_rates(state->allowed_rates,
+				MAX_RATES, s, strlen(s));
+		fmt_change++;
+	} else if (spa_streq(k, "avb.ifname")) {
+		snprintf(p->ifname, sizeof(p->ifname), "%s", s);
+	} else if (spa_streq(k, "avb.macaddr")) {
+		parse_addr(p->addr, s);
+	} else if (spa_streq(k, "avb.prio")) {
+		p->prio = atoi(s);
+	} else if (spa_streq(k, "avb.streamid")) {
+		parse_streamid(&p->streamid, s);
+	} else if (spa_streq(k, "avb.mtt")) {
+		p->mtt = atoi(s);
+	} else if (spa_streq(k, "avb.time-uncertainty")) {
+		p->t_uncertainty = atoi(s);
+	} else if (spa_streq(k, "avb.frames-per-pdu")) {
+		p->frames_per_pdu = atoi(s);
+	} else if (spa_streq(k, "avb.ptime-tolerance")) {
+		p->ptime_tolerance = atoi(s);
+	} else if (spa_streq(k, "latency.internal.rate")) {
+		state->process_latency.rate = atoi(s);
+	} else if (spa_streq(k, "latency.internal.ns")) {
+		state->process_latency.ns = atoi(s);
+	} else if (spa_streq(k, "clock.name")) {
+		spa_scnprintf(state->clock_name,
+				sizeof(state->clock_name), "%s", s);
+	} else
+		return 0;
+
+	if (fmt_change > 0) {
+		struct port *port = &state->ports[0];
+		port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+		port->params[PORT_EnumFormat].user++;
+	}
+	return 1;
+}
+
+static int position_to_string(struct channel_map *map, char *val, size_t len)
+{
+	uint32_t i, o = 0;
+	int r;
+	o += snprintf(val, len, "[ ");
+	for (i = 0; i < map->channels; i++) {
+		r = snprintf(val+o, len-o, "%s%s", i == 0 ? "" : ", ",
+				spa_debug_type_find_short_name(spa_type_audio_channel,
+					map->pos[i]));
+		if (r < 0 || o + r >= len)
+			return -ENOSPC;
+		o += r;
+	}
+	if (len > o)
+		o += snprintf(val+o, len-o, " ]");
+	return 0;
+}
+
+static int uint32_array_to_string(uint32_t *vals, uint32_t n_vals, char *val, size_t len)
+{
+	uint32_t i, o = 0;
+	int r;
+	o += snprintf(val, len, "[ ");
+	for (i = 0; i < n_vals; i++) {
+		r = snprintf(val+o, len-o, "%s%d", i == 0 ? "" : ", ", vals[i]);
+		if (r < 0 || o + r >= len)
+			return -ENOSPC;
+		o += r;
+	}
+	if (len > o)
+		o += snprintf(val+o, len-o, " ]");
+	return 0;
+}
+
+struct spa_pod *spa_avb_enum_propinfo(struct state *state,
+		uint32_t idx, struct spa_pod_builder *b)
+{
+	struct spa_pod *param;
+	struct props *p = &state->props;
+	char tmp[128];
+
+	switch (idx) {
+	case 0:
+		param = spa_pod_builder_add_object(b,
+			SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
+			SPA_PROP_INFO_name, SPA_POD_String(SPA_KEY_AUDIO_CHANNELS),
+			SPA_PROP_INFO_description, SPA_POD_String("Audio Channels"),
+			SPA_PROP_INFO_type, SPA_POD_Int(state->default_channels),
+			SPA_PROP_INFO_params, SPA_POD_Bool(true));
+		break;
+	case 1:
+		param = spa_pod_builder_add_object(b,
+			SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
+			SPA_PROP_INFO_name, SPA_POD_String(SPA_KEY_AUDIO_RATE),
+			SPA_PROP_INFO_description, SPA_POD_String("Audio Rate"),
+			SPA_PROP_INFO_type, SPA_POD_Int(state->default_rate),
+			SPA_PROP_INFO_params, SPA_POD_Bool(true));
+		break;
+	case 2:
+		param = spa_pod_builder_add_object(b,
+			SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
+			SPA_PROP_INFO_name, SPA_POD_String(SPA_KEY_AUDIO_FORMAT),
+			SPA_PROP_INFO_description, SPA_POD_String("Audio Format"),
+			SPA_PROP_INFO_type, SPA_POD_String(
+				spa_debug_type_find_short_name(spa_type_audio_format,
+					state->default_format)),
+			SPA_PROP_INFO_params, SPA_POD_Bool(true));
+		break;
+	case 3:
+	{
+		char buf[1024];
+		position_to_string(&state->default_pos, buf, sizeof(buf));
+		param = spa_pod_builder_add_object(b,
+			SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
+			SPA_PROP_INFO_name, SPA_POD_String(SPA_KEY_AUDIO_POSITION),
+			SPA_PROP_INFO_description, SPA_POD_String("Audio Position"),
+			SPA_PROP_INFO_type, SPA_POD_String(buf),
+			SPA_PROP_INFO_params, SPA_POD_Bool(true));
+		break;
+	}
+	case 4:
+	{
+		char buf[1024];
+		uint32_array_to_string(state->allowed_rates, state->n_allowed_rates, buf, sizeof(buf));
+		param = spa_pod_builder_add_object(b,
+			SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
+			SPA_PROP_INFO_name, SPA_POD_String(SPA_KEY_AUDIO_ALLOWED_RATES),
+			SPA_PROP_INFO_description, SPA_POD_String("Audio Allowed Rates"),
+			SPA_PROP_INFO_type, SPA_POD_String(buf),
+			SPA_PROP_INFO_params, SPA_POD_Bool(true));
+		break;
+	}
+	case 5:
+		param = spa_pod_builder_add_object(b,
+			SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
+			SPA_PROP_INFO_name, SPA_POD_String("avb.ifname"),
+			SPA_PROP_INFO_description, SPA_POD_String("The AVB interface name"),
+			SPA_PROP_INFO_type, SPA_POD_Stringn(p->ifname, sizeof(p->ifname)),
+			SPA_PROP_INFO_params, SPA_POD_Bool(true));
+		break;
+	case 6:
+		format_addr(tmp, sizeof(tmp), p->addr);
+		param = spa_pod_builder_add_object(b,
+			SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
+			SPA_PROP_INFO_name, SPA_POD_String("avb.macaddr"),
+			SPA_PROP_INFO_description, SPA_POD_String("The AVB MAC address"),
+			SPA_PROP_INFO_type, SPA_POD_String(tmp),
+			SPA_PROP_INFO_params, SPA_POD_Bool(true));
+		break;
+	case 7:
+		param = spa_pod_builder_add_object(b,
+			SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
+			SPA_PROP_INFO_name, SPA_POD_String("avb.prio"),
+			SPA_PROP_INFO_description, SPA_POD_String("The AVB stream priority"),
+			SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Int(p->prio, 0, INT32_MAX),
+			SPA_PROP_INFO_params, SPA_POD_Bool(true));
+		break;
+	case 8:
+		format_streamid(tmp, sizeof(tmp), p->streamid);
+		param = spa_pod_builder_add_object(b,
+			SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
+			SPA_PROP_INFO_name, SPA_POD_String("avb.streamid"),
+			SPA_PROP_INFO_description, SPA_POD_String("The AVB stream id"),
+			SPA_PROP_INFO_type, SPA_POD_String(tmp),
+			SPA_PROP_INFO_params, SPA_POD_Bool(true));
+		break;
+	case 9:
+		param = spa_pod_builder_add_object(b,
+			SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
+			SPA_PROP_INFO_name, SPA_POD_String("avb.mtt"),
+			SPA_PROP_INFO_description, SPA_POD_String("The AVB mtt"),
+			SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Int(p->mtt, 0, INT32_MAX),
+			SPA_PROP_INFO_params, SPA_POD_Bool(true));
+		break;
+	case 10:
+		param = spa_pod_builder_add_object(b,
+			SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
+			SPA_PROP_INFO_name, SPA_POD_String("avb.time-uncertainty"),
+			SPA_PROP_INFO_description, SPA_POD_String("The AVB time uncertainty"),
+			SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Int(p->t_uncertainty, 0, INT32_MAX),
+			SPA_PROP_INFO_params, SPA_POD_Bool(true));
+		break;
+	case 11:
+		param = spa_pod_builder_add_object(b,
+			SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
+			SPA_PROP_INFO_name, SPA_POD_String("avb.frames-per-pdu"),
+			SPA_PROP_INFO_description, SPA_POD_String("The AVB frames per packet"),
+			SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Int(p->frames_per_pdu, 0, INT32_MAX),
+			SPA_PROP_INFO_params, SPA_POD_Bool(true));
+		break;
+	case 12:
+		param = spa_pod_builder_add_object(b,
+			SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
+			SPA_PROP_INFO_name, SPA_POD_String("avb.ptime-tolerance"),
+			SPA_PROP_INFO_description, SPA_POD_String("The AVB packet tolerance"),
+			SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Int(p->ptime_tolerance, 0, INT32_MAX),
+			SPA_PROP_INFO_params, SPA_POD_Bool(true));
+		break;
+	case 13:
+		param = spa_pod_builder_add_object(b,
+			SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
+			SPA_PROP_INFO_name, SPA_POD_String("latency.internal.rate"),
+			SPA_PROP_INFO_description, SPA_POD_String("Internal latency in samples"),
+			SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Int(state->process_latency.rate,
+				0, 65536),
+			SPA_PROP_INFO_params, SPA_POD_Bool(true));
+		break;
+	case 14:
+		param = spa_pod_builder_add_object(b,
+			SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
+			SPA_PROP_INFO_name, SPA_POD_String("latency.internal.ns"),
+			SPA_PROP_INFO_description, SPA_POD_String("Internal latency in nanoseconds"),
+			SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Long(state->process_latency.ns,
+				0, 2 * SPA_NSEC_PER_SEC),
+			SPA_PROP_INFO_params, SPA_POD_Bool(true));
+		break;
+	case 15:
+		param = spa_pod_builder_add_object(b,
+			SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
+			SPA_PROP_INFO_name, SPA_POD_String("clock.name"),
+			SPA_PROP_INFO_description, SPA_POD_String("The name of the clock"),
+			SPA_PROP_INFO_type, SPA_POD_String(state->clock_name),
+			SPA_PROP_INFO_params, SPA_POD_Bool(true));
+		break;
+	default:
+		return NULL;
+	}
+	return param;
+}
+
+int spa_avb_add_prop_params(struct state *state, struct spa_pod_builder *b)
+{
+	struct props *p = &state->props;
+	struct spa_pod_frame f[1];
+	char buf[1024];
+
+	spa_pod_builder_prop(b, SPA_PROP_params, 0);
+	spa_pod_builder_push_struct(b, &f[0]);
+
+	spa_pod_builder_string(b, SPA_KEY_AUDIO_CHANNELS);
+	spa_pod_builder_int(b, state->default_channels);
+
+	spa_pod_builder_string(b, SPA_KEY_AUDIO_RATE);
+	spa_pod_builder_int(b, state->default_rate);
+
+	spa_pod_builder_string(b, SPA_KEY_AUDIO_FORMAT);
+	spa_pod_builder_string(b,
+			spa_debug_type_find_short_name(spa_type_audio_format,
+					state->default_format));
+
+	position_to_string(&state->default_pos, buf, sizeof(buf));
+	spa_pod_builder_string(b, SPA_KEY_AUDIO_POSITION);
+	spa_pod_builder_string(b, buf);
+
+	uint32_array_to_string(state->allowed_rates, state->n_allowed_rates,
+			buf, sizeof(buf));
+	spa_pod_builder_string(b, SPA_KEY_AUDIO_ALLOWED_RATES);
+	spa_pod_builder_string(b, buf);
+
+	spa_pod_builder_string(b, "avb.ifname");
+	spa_pod_builder_string(b, p->ifname);
+
+	format_addr(buf, sizeof(buf), p->addr);
+	spa_pod_builder_string(b, "avb.macadr");
+	spa_pod_builder_string(b, buf);
+
+	spa_pod_builder_string(b, "avb.prio");
+	spa_pod_builder_int(b, p->prio);
+
+	format_streamid(buf, sizeof(buf), p->streamid);
+	spa_pod_builder_string(b, "avb.streamid");
+	spa_pod_builder_string(b, buf);
+	spa_pod_builder_string(b, "avb.mtt");
+	spa_pod_builder_int(b, p->mtt);
+	spa_pod_builder_string(b, "avb.time-uncertainty");
+	spa_pod_builder_int(b, p->t_uncertainty);
+	spa_pod_builder_string(b, "avb.frames-per-pdu");
+	spa_pod_builder_int(b, p->frames_per_pdu);
+	spa_pod_builder_string(b, "avb.ptime-tolerance");
+	spa_pod_builder_int(b, p->ptime_tolerance);
+
+	spa_pod_builder_string(b, "latency.internal.rate");
+	spa_pod_builder_int(b, state->process_latency.rate);
+
+	spa_pod_builder_string(b, "latency.internal.ns");
+	spa_pod_builder_long(b, state->process_latency.ns);
+
+	spa_pod_builder_string(b, "clock.name");
+	spa_pod_builder_string(b, state->clock_name);
+
+	spa_pod_builder_pop(b, &f[0]);
+	return 0;
+}
+
+int spa_avb_parse_prop_params(struct state *state, struct spa_pod *params)
+{
+	struct spa_pod_parser prs;
+	struct spa_pod_frame f;
+	int changed = 0;
+
+	if (params == NULL)
+		return 0;
+
+	spa_pod_parser_pod(&prs, params);
+	if (spa_pod_parser_push_struct(&prs, &f) < 0)
+		return 0;
+
+	while (true) {
+		const char *name;
+		struct spa_pod *pod;
+		char value[512];
+
+		if (spa_pod_parser_get_string(&prs, &name) < 0)
+			break;
+
+		if (spa_pod_parser_get_pod(&prs, &pod) < 0)
+			break;
+		if (spa_pod_is_string(pod)) {
+			spa_pod_copy_string(pod, sizeof(value), value);
+		} else if (spa_pod_is_int(pod)) {
+			snprintf(value, sizeof(value), "%d",
+					SPA_POD_VALUE(struct spa_pod_int, pod));
+		} else if (spa_pod_is_long(pod)) {
+			snprintf(value, sizeof(value), "%"PRIi64,
+					SPA_POD_VALUE(struct spa_pod_long, pod));
+		} else if (spa_pod_is_bool(pod)) {
+			snprintf(value, sizeof(value), "%s",
+					SPA_POD_VALUE(struct spa_pod_bool, pod) ?
+					"true" : "false");
+		} else
+			continue;
+
+		spa_log_info(state->log, "key:'%s' val:'%s'", name, value);
+		avb_set_param(state, name, value);
+		changed++;
+	}
+	if (changed > 0) {
+		state->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS;
+		state->params[NODE_Props].user++;
+	}
+	return changed;
+}
+
+int spa_avb_init(struct state *state, const struct spa_dict *info)
+{
+	uint32_t i;
+
+	state->quantum_limit = 8192;
+	for (i = 0; info && i < info->n_items; i++) {
+		const char *k = info->items[i].key;
+		const char *s = info->items[i].value;
+		if (spa_streq(k, "clock.quantum-limit")) {
+			spa_atou32(s, &state->quantum_limit, 0);
+		} else {
+			avb_set_param(state, k, s);
+		}
+	}
+
+	state->ringbuffer_size = state->quantum_limit * 64;
+	state->ringbuffer_data = calloc(1, state->ringbuffer_size * 4);
+	spa_ringbuffer_init(&state->ring);
+	return 0;
+}
+
+int spa_avb_clear(struct state *state)
+{
+	return 0;
+}
+
+static int spa_format_to_aaf(uint32_t format)
+{
+	switch(format) {
+	case SPA_AUDIO_FORMAT_F32_BE: return SPA_AVBTP_AAF_FORMAT_FLOAT_32BIT;
+	case SPA_AUDIO_FORMAT_S32_BE: return SPA_AVBTP_AAF_FORMAT_INT_32BIT;
+	case SPA_AUDIO_FORMAT_S24_BE: return SPA_AVBTP_AAF_FORMAT_INT_24BIT;
+	case SPA_AUDIO_FORMAT_S16_BE: return SPA_AVBTP_AAF_FORMAT_INT_16BIT;
+	default: return SPA_AVBTP_AAF_FORMAT_USER;
+	}
+}
+
+static int frame_size(uint32_t format)
+{
+	switch(format) {
+	case SPA_AUDIO_FORMAT_F32_BE:
+	case SPA_AUDIO_FORMAT_S32_BE: return 4;
+	case SPA_AUDIO_FORMAT_S24_BE: return 3;
+	case SPA_AUDIO_FORMAT_S16_BE: return 2;
+	default: return 0;
+	}
+}
+
+static int spa_rate_to_aaf(uint32_t rate)
+{
+	switch(rate) {
+	case 8000: return SPA_AVBTP_AAF_PCM_NSR_8KHZ;
+	case 16000: return SPA_AVBTP_AAF_PCM_NSR_16KHZ;
+	case 24000: return SPA_AVBTP_AAF_PCM_NSR_24KHZ;
+	case 32000: return SPA_AVBTP_AAF_PCM_NSR_32KHZ;
+	case 44100: return SPA_AVBTP_AAF_PCM_NSR_44_1KHZ;
+	case 48000: return SPA_AVBTP_AAF_PCM_NSR_48KHZ;
+	case 88200: return SPA_AVBTP_AAF_PCM_NSR_88_2KHZ;
+	case 96000: return SPA_AVBTP_AAF_PCM_NSR_96KHZ;
+	case 176400: return SPA_AVBTP_AAF_PCM_NSR_176_4KHZ;
+	case 192000: return SPA_AVBTP_AAF_PCM_NSR_192KHZ;
+	default: return SPA_AVBTP_AAF_PCM_NSR_USER;
+	}
+}
+
+int
+spa_avb_enum_format(struct state *state, int seq, uint32_t start, uint32_t num,
+		     const struct spa_pod *filter)
+{
+	uint8_t buffer[4096];
+	struct spa_pod_builder b = { 0 };
+	struct spa_pod_frame f[2];
+	struct spa_pod *fmt;
+	int res = 0;
+	struct spa_result_node_params result;
+	uint32_t count = 0;
+
+	result.id = SPA_PARAM_EnumFormat;
+	result.next = start;
+
+next:
+	result.index = result.next++;
+
+	if (result.index > 0)
+		return 0;
+
+	spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+	spa_pod_builder_push_object(&b, &f[0], SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat);
+	spa_pod_builder_add(&b,
+			SPA_FORMAT_mediaType,    SPA_POD_Id(SPA_MEDIA_TYPE_audio),
+			SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw),
+			0);
+
+	spa_pod_builder_prop(&b, SPA_FORMAT_AUDIO_format, 0);
+	if (state->default_format != 0) {
+		spa_pod_builder_id(&b, state->default_format);
+	} else {
+		spa_pod_builder_push_choice(&b, &f[1], SPA_CHOICE_Enum, 0);
+		spa_pod_builder_id(&b, SPA_AUDIO_FORMAT_F32_BE);
+		spa_pod_builder_id(&b, SPA_AUDIO_FORMAT_F32_BE);
+		spa_pod_builder_id(&b, SPA_AUDIO_FORMAT_S32_BE);
+		spa_pod_builder_id(&b, SPA_AUDIO_FORMAT_S24_BE);
+		spa_pod_builder_id(&b, SPA_AUDIO_FORMAT_S16_BE);
+		spa_pod_builder_pop(&b, &f[1]);
+	}
+	spa_pod_builder_prop(&b, SPA_FORMAT_AUDIO_rate, 0);
+	if (state->default_rate != 0) {
+		spa_pod_builder_int(&b, state->default_rate);
+	} else {
+		spa_pod_builder_push_choice(&b, &f[1], SPA_CHOICE_Enum, 0);
+		spa_pod_builder_int(&b, 48000);
+		spa_pod_builder_int(&b, 8000);
+		spa_pod_builder_int(&b, 16000);
+		spa_pod_builder_int(&b, 24000);
+		spa_pod_builder_int(&b, 32000);
+		spa_pod_builder_int(&b, 44100);
+		spa_pod_builder_int(&b, 48000);
+		spa_pod_builder_int(&b, 88200);
+		spa_pod_builder_int(&b, 96000);
+		spa_pod_builder_int(&b, 176400);
+		spa_pod_builder_int(&b, 192000);
+		spa_pod_builder_pop(&b, &f[1]);
+	}
+	spa_pod_builder_prop(&b, SPA_FORMAT_AUDIO_channels, 0);
+	if (state->default_channels != 0) {
+		spa_pod_builder_int(&b, state->default_channels);
+	} else {
+		spa_pod_builder_push_choice(&b, &f[1], SPA_CHOICE_Range, 0);
+		spa_pod_builder_int(&b, 8);
+		spa_pod_builder_int(&b, 2);
+		spa_pod_builder_int(&b, 32);
+		spa_pod_builder_pop(&b, &f[1]);
+	}
+	fmt = spa_pod_builder_pop(&b, &f[0]);
+
+	if (spa_pod_filter(&b, &result.param, fmt, filter) < 0)
+		goto next;
+
+	spa_node_emit_result(&state->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+	if (++count != num)
+		goto next;
+
+	return res;
+}
+
+static int setup_socket(struct state *state)
+{
+	int fd, res;
+	struct ifreq req;
+	struct props *p = &state->props;
+
+	fd = socket(AF_PACKET, SOCK_DGRAM|SOCK_NONBLOCK, htons(ETH_P_TSN));
+	if (fd < 0) {
+		spa_log_error(state->log, "socket() failed: %m");
+		return -errno;
+	}
+
+	snprintf(req.ifr_name, sizeof(req.ifr_name), "%s", p->ifname);
+	res = ioctl(fd, SIOCGIFINDEX, &req);
+	if (res < 0) {
+		spa_log_error(state->log, "SIOCGIFINDEX %s failed: %m", p->ifname);
+		res = -errno;
+		goto error_close;
+	}
+
+	state->sock_addr.sll_family = AF_PACKET;
+	state->sock_addr.sll_protocol = htons(ETH_P_TSN);
+	state->sock_addr.sll_halen = ETH_ALEN;
+	state->sock_addr.sll_ifindex = req.ifr_ifindex;
+	memcpy(&state->sock_addr.sll_addr, p->addr, ETH_ALEN);
+
+	if (state->ports[0].direction == SPA_DIRECTION_INPUT) {
+		struct sock_txtime txtime_cfg;
+
+		res = setsockopt(fd, SOL_SOCKET, SO_PRIORITY, &p->prio,
+				sizeof(p->prio));
+		if (res < 0) {
+			spa_log_error(state->log, "setsockopt(SO_PRIORITY %d) failed: %m", p->prio);
+			res = -errno;
+			goto error_close;
+		}
+
+		txtime_cfg.clockid = CLOCK_TAI;
+		txtime_cfg.flags = 0;
+		res = setsockopt(fd, SOL_SOCKET, SO_TXTIME, &txtime_cfg,
+				sizeof(txtime_cfg));
+		if (res < 0) {
+			spa_log_error(state->log, "setsockopt(SO_TXTIME) failed: %m");
+			res = -errno;
+			goto error_close;
+		}
+	} else {
+		struct packet_mreq mreq = { 0 };
+
+		res = bind(fd, (struct sockaddr *) &state->sock_addr,
+				sizeof(state->sock_addr));
+		if (res < 0) {
+			spa_log_error(state->log, "bind() failed: %m");
+			res = -errno;
+			goto error_close;
+		}
+
+		mreq.mr_ifindex = req.ifr_ifindex;
+		mreq.mr_type = PACKET_MR_MULTICAST;
+		mreq.mr_alen = ETH_ALEN;
+		memcpy(&mreq.mr_address, p->addr, ETH_ALEN);
+		res = setsockopt(fd, SOL_PACKET, PACKET_ADD_MEMBERSHIP,
+				&mreq, sizeof(struct packet_mreq));
+		if (res < 0) {
+			spa_log_error(state->log, "setsockopt(ADD_MEMBERSHIP) failed: %m");
+			res = -errno;
+			goto error_close;
+		}
+	}
+	state->sockfd = fd;
+	return 0;
+
+error_close:
+	close(fd);
+	return res;
+}
+
+static int setup_packet(struct state *state, struct spa_audio_info *fmt)
+{
+	struct spa_avbtp_packet_aaf *pdu;
+	struct props *p = &state->props;
+	ssize_t payload_size, hdr_size, pdu_size;
+
+	hdr_size = sizeof(*pdu);
+	payload_size = state->stride * p->frames_per_pdu;
+	pdu_size = hdr_size + payload_size;
+	if ((pdu = calloc(1, pdu_size)) == NULL)
+		return -errno;
+
+	SPA_AVBTP_PACKET_AAF_SET_SUBTYPE(pdu, SPA_AVBTP_SUBTYPE_AAF);
+
+	if (state->ports[0].direction == SPA_DIRECTION_INPUT) {
+		SPA_AVBTP_PACKET_AAF_SET_SV(pdu, 1);
+		SPA_AVBTP_PACKET_AAF_SET_STREAM_ID(pdu, p->streamid);
+		SPA_AVBTP_PACKET_AAF_SET_TV(pdu, 1);
+		SPA_AVBTP_PACKET_AAF_SET_FORMAT(pdu, spa_format_to_aaf(state->format));
+		SPA_AVBTP_PACKET_AAF_SET_NSR(pdu, spa_rate_to_aaf(state->rate));
+		SPA_AVBTP_PACKET_AAF_SET_CHAN_PER_FRAME(pdu, state->channels);
+		SPA_AVBTP_PACKET_AAF_SET_BIT_DEPTH(pdu, frame_size(state->format)*8);
+		SPA_AVBTP_PACKET_AAF_SET_DATA_LEN(pdu, payload_size);
+		SPA_AVBTP_PACKET_AAF_SET_SP(pdu, SPA_AVBTP_AAF_PCM_SP_NORMAL);
+	}
+	state->pdu = pdu;
+	state->hdr_size = hdr_size;
+	state->payload_size = payload_size;
+	state->pdu_size = pdu_size;
+	return 0;
+}
+
+static int setup_msg(struct state *state)
+{
+	state->iov[0].iov_base = state->pdu;
+	state->iov[0].iov_len = state->hdr_size;
+	state->iov[1].iov_base = state->pdu->payload;
+	state->iov[1].iov_len = state->payload_size;
+	state->iov[2].iov_base = state->pdu->payload;
+	state->iov[2].iov_len = 0;
+	state->msg.msg_name = &state->sock_addr;
+	state->msg.msg_namelen = sizeof(state->sock_addr);
+	state->msg.msg_iov = state->iov;
+	state->msg.msg_iovlen = 3;
+	state->msg.msg_control = state->control;
+	state->msg.msg_controllen = sizeof(state->control);
+	state->cmsg = CMSG_FIRSTHDR(&state->msg);
+	state->cmsg->cmsg_level = SOL_SOCKET;
+	state->cmsg->cmsg_type = SCM_TXTIME;
+	state->cmsg->cmsg_len = CMSG_LEN(sizeof(__u64));
+	return 0;
+}
+
+int spa_avb_clear_format(struct state *state)
+{
+	close(state->sockfd);
+	close(state->timerfd);
+	free(state->pdu);
+
+	return 0;
+}
+
+int spa_avb_set_format(struct state *state, struct spa_audio_info *fmt, uint32_t flags)
+{
+	int res;
+	struct props *p = &state->props;
+
+	state->format = fmt->info.raw.format;
+	state->rate = fmt->info.raw.rate;
+	state->channels = fmt->info.raw.channels;
+	state->blocks = 1;
+	state->stride = state->channels * frame_size(state->format);
+
+	if ((res = setup_socket(state)) < 0)
+		return res;
+
+	if ((res = spa_system_timerfd_create(state->data_system,
+			CLOCK_REALTIME, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK)) < 0)
+		goto error_close_sockfd;
+
+	state->timerfd = res;
+
+	if ((res = setup_packet(state, fmt)) < 0)
+		return res;
+
+	if ((res = setup_msg(state)) < 0)
+		return res;
+
+	state->pdu_period = SPA_NSEC_PER_SEC * p->frames_per_pdu /
+                          state->rate;
+
+	return 0;
+
+error_close_sockfd:
+	close(state->sockfd);
+	return res;
+}
+
+void spa_avb_recycle_buffer(struct state *this, struct port *port, uint32_t buffer_id)
+{
+	struct buffer *b = &port->buffers[buffer_id];
+
+	if (SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_OUT)) {
+		spa_log_trace_fp(this->log, "%p: recycle buffer %u", this, buffer_id);
+		spa_list_append(&port->free, &b->link);
+		SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_OUT);
+	}
+}
+
+static void reset_buffers(struct state *this, struct port *port)
+{
+	uint32_t i;
+
+	spa_list_init(&port->free);
+	spa_list_init(&port->ready);
+
+	for (i = 0; i < port->n_buffers; i++) {
+		struct buffer *b = &port->buffers[i];
+		if (port->direction == SPA_DIRECTION_INPUT) {
+			SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUT);
+			spa_node_call_reuse_buffer(&this->callbacks, 0, b->id);
+		} else {
+			spa_list_append(&port->free, &b->link);
+			SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_OUT);
+		}
+	}
+}
+
+static inline bool is_pdu_valid(struct state *state)
+{
+	uint8_t seq_num;
+
+	seq_num = SPA_AVBTP_PACKET_AAF_GET_SEQ_NUM(state->pdu);
+
+	if (state->prev_seq != 0 && (uint8_t)(state->prev_seq + 1) != seq_num) {
+		spa_log_warn(state->log, "dropped packets %d != %d", state->prev_seq + 1, seq_num);
+	}
+	state->prev_seq = seq_num;
+	return true;
+}
+
+static inline void
+set_iovec(struct spa_ringbuffer *rbuf, void *buffer, uint32_t size,
+		uint32_t offset, struct iovec *iov, uint32_t len)
+{
+	iov[0].iov_len = SPA_MIN(len, size - offset);
+	iov[0].iov_base = SPA_PTROFF(buffer, offset, void);
+	iov[1].iov_len = len - iov[0].iov_len;
+	iov[1].iov_base = buffer;
+}
+
+static void avb_on_socket_event(struct spa_source *source)
+{
+	struct state *state = source->data;
+	ssize_t n;
+	int32_t filled;
+	uint32_t subtype, index;
+	struct spa_avbtp_packet_aaf *pdu = state->pdu;
+	bool overrun = false;
+
+	filled = spa_ringbuffer_get_write_index(&state->ring, &index);
+	overrun = filled > (int32_t) state->ringbuffer_size;
+	if (overrun) {
+		state->iov[1].iov_base = state->pdu->payload;
+		state->iov[1].iov_len = state->payload_size;
+		state->iov[2].iov_len = 0;
+	} else {
+		set_iovec(&state->ring,
+			state->ringbuffer_data,
+			state->ringbuffer_size,
+			index % state->ringbuffer_size,
+			&state->iov[1], state->payload_size);
+	}
+
+	n = recvmsg(state->sockfd, &state->msg, 0);
+	if (n < 0) {
+		spa_log_error(state->log, "recv() failed: %m");
+                return;
+        }
+	if (n != (ssize_t)state->pdu_size) {
+		spa_log_error(state->log, "AVB packet dropped: Invalid size");
+		return;
+	}
+
+	subtype = SPA_AVBTP_PACKET_AAF_GET_SUBTYPE(pdu);
+	if (subtype != SPA_AVBTP_SUBTYPE_AAF) {
+		spa_log_error(state->log, "non supported subtype %d", subtype);
+		return;
+	}
+	if (!is_pdu_valid(state)) {
+		spa_log_error(state->log, "AAF PDU invalid");
+		return;
+        }
+	if (overrun) {
+		spa_log_warn(state->log, "overrun %d", filled);
+		return;
+	}
+	index += state->payload_size;
+	spa_ringbuffer_write_update(&state->ring, index);
+}
+
+static void set_timeout(struct state *state, uint64_t next_time)
+{
+	struct itimerspec ts;
+	uint64_t time_utc;
+
+	spa_log_trace(state->log, "set timeout %"PRIu64, next_time);
+
+        time_utc = next_time > TAI_OFFSET ? TAI_TO_UTC(next_time) : 0;
+	ts.it_value.tv_sec = time_utc / SPA_NSEC_PER_SEC;
+	ts.it_value.tv_nsec = time_utc % SPA_NSEC_PER_SEC;
+        ts.it_interval.tv_sec = 0;
+        ts.it_interval.tv_nsec = 0;
+	spa_system_timerfd_settime(state->data_system,
+			state->timer_source.fd, SPA_FD_TIMER_ABSTIME, &ts, NULL);
+}
+
+static int flush_write(struct state *state, uint64_t current_time)
+{
+	int32_t avail, wanted;
+	uint32_t index;
+        uint64_t ptime, txtime;
+	int pdu_count;
+	struct props *p = &state->props;
+	struct spa_avbtp_packet_aaf *pdu = state->pdu;
+	ssize_t n;
+
+	avail = spa_ringbuffer_get_read_index(&state->ring, &index);
+	wanted = state->duration * state->stride;
+	if (avail < wanted) {
+		spa_log_warn(state->log, "underrun %d < %d", avail, wanted);
+		return -EPIPE;
+	}
+
+	pdu_count = state->duration / p->frames_per_pdu;
+
+	txtime = current_time + p->t_uncertainty;
+	ptime = txtime + p->mtt;
+
+	while (pdu_count--) {
+		*(__u64 *)CMSG_DATA(state->cmsg) = txtime;
+
+		set_iovec(&state->ring,
+			state->ringbuffer_data,
+			state->ringbuffer_size,
+			index % state->ringbuffer_size,
+			&state->iov[1], state->payload_size);
+
+		SPA_AVBTP_PACKET_AAF_SET_SEQ_NUM(pdu, state->pdu_seq++);
+		SPA_AVBTP_PACKET_AAF_SET_TIMESTAMP(pdu, ptime);
+
+		n = sendmsg(state->sockfd, &state->msg, 0);
+		if (n < 0 || n != (ssize_t)state->pdu_size) {
+			spa_log_error(state->log, "sendmdg() failed: %m");
+		}
+		txtime += state->pdu_period;
+		ptime += state->pdu_period;
+		index += state->payload_size;
+	}
+	spa_ringbuffer_read_update(&state->ring, index);
+	return 0;
+}
+
+int spa_avb_write(struct state *state)
+{
+	int32_t filled;
+	uint32_t index, to_write;
+	struct port *port = &state->ports[0];
+
+	filled = spa_ringbuffer_get_write_index(&state->ring, &index);
+	if (filled < 0) {
+		spa_log_warn(state->log, "underrun %d", filled);
+	} else if (filled > (int32_t)state->ringbuffer_size) {
+		spa_log_warn(state->log, "overrun %d", filled);
+	}
+	to_write = state->ringbuffer_size - filled;
+
+	while (!spa_list_is_empty(&port->ready) && to_write > 0) {
+		size_t n_bytes;
+		struct buffer *b;
+		struct spa_data *d;
+		uint32_t offs, avail, size;
+
+		b = spa_list_first(&port->ready, struct buffer, link);
+		d = b->buf->datas;
+
+		offs = SPA_MIN(d[0].chunk->offset + port->ready_offset, d[0].maxsize);
+		size = SPA_MIN(d[0].chunk->size, d[0].maxsize - offs);
+		avail = size - offs;
+
+		n_bytes = SPA_MIN(avail, to_write);
+		if (n_bytes == 0)
+			break;
+
+		spa_ringbuffer_write_data(&state->ring,
+				state->ringbuffer_data,
+				state->ringbuffer_size,
+				index % state->ringbuffer_size,
+				SPA_PTROFF(d[0].data, offs, void),
+				n_bytes);
+
+		port->ready_offset += n_bytes;
+
+		if (port->ready_offset >= size || avail == 0) {
+			spa_list_remove(&b->link);
+			SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUT);
+			port->io->buffer_id = b->id;
+			spa_log_trace_fp(state->log, "%p: reuse buffer %u", state, b->id);
+
+			spa_node_call_reuse_buffer(&state->callbacks, 0, b->id);
+
+			port->ready_offset = 0;
+		}
+		to_write -= n_bytes;
+		index += n_bytes;
+	}
+	spa_ringbuffer_write_update(&state->ring, index);
+
+	if (state->following) {
+		flush_write(state, state->position->clock.nsec);
+	}
+	return 0;
+}
+
+static int handle_play(struct state *state, uint64_t current_time)
+{
+	flush_write(state, current_time);
+	spa_node_call_ready(&state->callbacks, SPA_STATUS_NEED_DATA);
+	return 0;
+}
+
+int spa_avb_read(struct state *state)
+{
+	int32_t avail, wanted;
+	uint32_t index;
+	struct port *port = &state->ports[0];
+	struct buffer *b;
+	struct spa_data *d;
+	uint32_t n_bytes;
+
+	if (state->position)
+		state->duration = state->position->clock.duration;
+
+	avail = spa_ringbuffer_get_read_index(&state->ring, &index);
+	wanted = state->duration * state->stride;
+
+	if (spa_list_is_empty(&port->free)) {
+		spa_log_warn(state->log, "out of buffers");
+		return -EPIPE;
+	}
+
+	b = spa_list_first(&port->free, struct buffer, link);
+	d = b->buf->datas;
+
+	n_bytes = SPA_MIN(d[0].maxsize, (uint32_t)wanted);
+
+	if (avail < wanted) {
+		spa_log_warn(state->log, "capture underrun %d < %d", avail, wanted);
+		memset(d[0].data, 0, n_bytes);
+	} else {
+		spa_ringbuffer_read_data(&state->ring,
+				state->ringbuffer_data,
+				state->ringbuffer_size,
+				index % state->ringbuffer_size,
+				d[0].data, n_bytes);
+		index += n_bytes;
+		spa_ringbuffer_read_update(&state->ring, index);
+	}
+
+	d[0].chunk->offset = 0;
+	d[0].chunk->size = n_bytes;
+	d[0].chunk->stride = state->stride;
+	d[0].chunk->flags = 0;
+
+	spa_list_remove(&b->link);
+	spa_list_append(&port->ready, &b->link);
+
+	return 0;
+}
+
+static int handle_capture(struct state *state, uint64_t current_time)
+{
+	struct port *port = &state->ports[0];
+	struct spa_io_buffers *io;
+	struct buffer *b;
+
+	spa_avb_read(state);
+
+	if (spa_list_is_empty(&port->ready))
+		return 0;
+
+	io = port->io;
+	if (io != NULL &&
+	    (io->status != SPA_STATUS_HAVE_DATA || port->rate_match != NULL)) {
+		if (io->buffer_id < port->n_buffers)
+			spa_avb_recycle_buffer(state, port, io->buffer_id);
+
+		b = spa_list_first(&port->ready, struct buffer, link);
+		spa_list_remove(&b->link);
+		SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUT);
+
+		io->buffer_id = b->id;
+		io->status = SPA_STATUS_HAVE_DATA;
+		spa_log_trace_fp(state->log, "%p: output buffer:%d", state, b->id);
+	}
+	spa_node_call_ready(&state->callbacks, SPA_STATUS_HAVE_DATA);
+	return 0;
+}
+
+static void avb_on_timeout_event(struct spa_source *source)
+{
+	struct state *state = source->data;
+	uint64_t expirations, current_time, duration;
+	uint32_t rate;
+
+	spa_log_trace(state->log, "timeout");
+
+	if (spa_system_timerfd_read(state->data_system,
+				state->timer_source.fd, &expirations) < 0) {
+		if (errno == EAGAIN)
+			return;
+		spa_log_error(state->log, "read timerfd: %m");
+	}
+
+	current_time = state->next_time;
+	if (SPA_LIKELY(state->position)) {
+		duration = state->position->clock.duration;
+		rate = state->position->clock.rate.denom;
+	} else {
+		duration = 1024;
+		rate = 48000;
+	}
+	state->duration = duration;
+
+	if (state->ports[0].direction == SPA_DIRECTION_INPUT)
+		handle_play(state, current_time);
+	else
+		handle_capture(state, current_time);
+
+	state->next_time = current_time + duration * SPA_NSEC_PER_SEC / rate;
+
+	if (SPA_LIKELY(state->clock)) {
+		state->clock->nsec = current_time;
+		state->clock->position += duration;
+		state->clock->duration = duration;
+		state->clock->delay = 0;
+		state->clock->rate_diff = 1.0;
+		state->clock->next_nsec = state->next_time;
+	}
+
+	set_timeout(state, state->next_time);
+}
+
+static int set_timers(struct state *state)
+{
+	struct timespec now;
+	int res;
+
+	if ((res = spa_system_clock_gettime(state->data_system, CLOCK_TAI, &now)) < 0)
+	    return res;
+
+	state->next_time = SPA_TIMESPEC_TO_NSEC(&now);
+
+	if (state->following) {
+		set_timeout(state, 0);
+	} else {
+		set_timeout(state, state->next_time);
+	}
+	return 0;
+}
+
+static inline bool is_following(struct state *state)
+{
+	return state->position && state->clock && state->position->clock.id != state->clock->id;
+}
+
+static int do_reassign_follower(struct spa_loop *loop,
+			    bool async,
+			    uint32_t seq,
+			    const void *data,
+			    size_t size,
+			    void *user_data)
+{
+	struct state *state = user_data;
+	spa_dll_init(&state->dll);
+	set_timers(state);
+	return 0;
+}
+
+int spa_avb_reassign_follower(struct state *state)
+{
+	bool following, freewheel;
+
+	if (!state->started)
+		return 0;
+
+	following = is_following(state);
+	if (following != state->following) {
+		spa_log_debug(state->log, "%p: reassign follower %d->%d", state, state->following, following);
+		state->following = following;
+		spa_loop_invoke(state->data_loop, do_reassign_follower, 0, NULL, 0, true, state);
+	}
+
+	freewheel = state->position &&
+		SPA_FLAG_IS_SET(state->position->clock.flags, SPA_IO_CLOCK_FLAG_FREEWHEEL);
+
+	if (state->freewheel != freewheel) {
+		spa_log_debug(state->log, "%p: freewheel %d->%d", state, state->freewheel, freewheel);
+		state->freewheel = freewheel;
+	}
+	return 0;
+}
+
+int spa_avb_start(struct state *state)
+{
+	if (state->started)
+		return 0;
+
+	if (state->position) {
+		state->duration = state->position->clock.duration;
+		state->rate_denom = state->position->clock.rate.denom;
+	} else {
+		state->duration = 1024;
+		state->rate_denom = state->rate;
+	}
+
+	spa_dll_init(&state->dll);
+	state->max_error = (256.0 * state->rate) / state->rate_denom;
+
+	state->following = is_following(state);
+
+	state->timer_source.func = avb_on_timeout_event;
+	state->timer_source.data = state;
+	state->timer_source.fd = state->timerfd;
+	state->timer_source.mask = SPA_IO_IN;
+	state->timer_source.rmask = 0;
+	spa_loop_add_source(state->data_loop, &state->timer_source);
+
+	state->pdu_seq = 0;
+
+	if (state->ports[0].direction == SPA_DIRECTION_OUTPUT) {
+		state->sock_source.func = avb_on_socket_event;
+		state->sock_source.data = state;
+		state->sock_source.fd = state->sockfd;
+		state->sock_source.mask = SPA_IO_IN;
+		state->sock_source.rmask = 0;
+		spa_loop_add_source(state->data_loop, &state->sock_source);
+	}
+
+	reset_buffers(state, &state->ports[0]);
+
+	set_timers(state);
+
+	state->started = true;
+
+	return 0;
+}
+
+static int do_remove_source(struct spa_loop *loop,
+			    bool async,
+			    uint32_t seq,
+			    const void *data,
+			    size_t size,
+			    void *user_data)
+{
+	struct state *state = user_data;
+
+	spa_loop_remove_source(state->data_loop, &state->timer_source);
+
+	if (state->ports[0].direction == SPA_DIRECTION_OUTPUT) {
+		spa_loop_remove_source(state->data_loop, &state->sock_source);
+	}
+	return 0;
+}
+
+int spa_avb_pause(struct state *state)
+{
+	if (!state->started)
+		return 0;
+
+	spa_log_debug(state->log, "%p: pause", state);
+
+	spa_loop_invoke(state->data_loop, do_remove_source, 0, NULL, 0, true, state);
+
+	state->started = false;
+	set_timeout(state, 0);
+
+	return 0;
+}
diff --git a/spa/plugins/avb/avb-pcm.h b/spa/plugins/avb/avb-pcm.h
new file mode 100644
index 0000000000000000000000000000000000000000..bb3bce68472f0376b0529914b368b0d353f9884c
--- /dev/null
+++ b/spa/plugins/avb/avb-pcm.h
@@ -0,0 +1,343 @@
+/* Spa AVB PCM
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_AVB_PCM_H
+#define SPA_AVB_PCM_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <stddef.h>
+#include <math.h>
+#include <linux/if_ether.h>
+#include <linux/if_packet.h>
+#include <linux/net_tstamp.h>
+#include <limits.h>
+#include <net/if.h>
+
+#include <avbtp/packets.h>
+
+#include <spa/support/plugin.h>
+#include <spa/support/loop.h>
+#include <spa/utils/list.h>
+#include <spa/utils/json.h>
+#include <spa/utils/dll.h>
+
+#include <spa/node/node.h>
+#include <spa/node/utils.h>
+#include <spa/node/io.h>
+#include <spa/debug/types.h>
+#include <spa/utils/ringbuffer.h>
+#include <spa/param/param.h>
+#include <spa/param/latency-utils.h>
+#include <spa/param/audio/format-utils.h>
+
+#include "avb.h"
+
+#define MAX_RATES	16
+
+#define DEFAULT_IFNAME		"eth0"
+#define DEFAULT_ADDR		"01:AA:AA:AA:AA:AA"
+#define DEFAULT_PRIO		0
+#define DEFAULT_STREAMID	"AA:BB:CC:DD:EE:FF:0000"
+#define DEFAULT_MTT		5000000
+#define DEFAULT_TU		1000000
+#define DEFAULT_FRAMES_PER_PDU	8
+
+#define DEFAULT_PERIOD		1024u
+#define DEFAULT_RATE		48000u
+#define DEFAULT_CHANNELS	8u
+
+struct props {
+	char ifname[IFNAMSIZ];
+	unsigned char addr[ETH_ALEN];
+	int prio;
+	uint64_t streamid;
+	int mtt;
+	int t_uncertainty;
+	uint32_t frames_per_pdu;
+	int ptime_tolerance;
+};
+
+static inline int parse_addr(unsigned char addr[ETH_ALEN], const char *str)
+{
+	unsigned char ad[ETH_ALEN];
+	if (sscanf(str, "%hhx:%hhx:%hhx:%hhx:%hhx:%hhx",
+			&ad[0], &ad[1], &ad[2], &ad[3], &ad[4], &ad[5]) != 6)
+		return -EINVAL;
+	memcpy(addr, ad, sizeof(ad));
+	return 0;
+}
+static inline char *format_addr(char *str, size_t size, const unsigned char addr[ETH_ALEN])
+{
+	snprintf(str, size, "%02x:%02x:%02x:%02x:%02x:%02x",
+			addr[0], addr[1], addr[2],
+			addr[3], addr[4], addr[5]);
+	return str;
+}
+
+static inline int parse_streamid(uint64_t *streamid, const char *str)
+{
+	unsigned char addr[6];
+	unsigned short unique_id;
+	if (sscanf(str, "%hhx:%hhx:%hhx:%hhx:%hhx:%hhx:%hx",
+			&addr[0], &addr[1], &addr[2], &addr[3],
+			&addr[4], &addr[5], &unique_id) != 7)
+		return -EINVAL;
+	*streamid = (uint64_t) addr[0] << 56 |
+		    (uint64_t) addr[1] << 48 |
+		    (uint64_t) addr[2] << 40 |
+		    (uint64_t) addr[3] << 32 |
+		    (uint64_t) addr[4] << 24 |
+		    (uint64_t) addr[5] << 16 |
+		    unique_id;
+	return 0;
+}
+static inline char *format_streamid(char *str, size_t size, const uint64_t streamid)
+{
+	snprintf(str, size, "%02x:%02x:%02x:%02x:%02x:%02x:%04x",
+			(uint8_t)(streamid >> 56),
+			(uint8_t)(streamid >> 48),
+			(uint8_t)(streamid >> 40),
+			(uint8_t)(streamid >> 32),
+			(uint8_t)(streamid >> 24),
+			(uint8_t)(streamid >> 16),
+			(uint16_t)(streamid));
+	return str;
+}
+
+#define MAX_BUFFERS 32
+
+struct buffer {
+	uint32_t id;
+#define BUFFER_FLAG_OUT	(1<<0)
+	uint32_t flags;
+	struct spa_buffer *buf;
+	struct spa_meta_header *h;
+	struct spa_list link;
+};
+
+#define BW_MAX		0.128
+#define BW_MED		0.064
+#define BW_MIN		0.016
+#define BW_PERIOD	(3 * SPA_NSEC_PER_SEC)
+
+struct channel_map {
+	uint32_t channels;
+	uint32_t pos[SPA_AUDIO_MAX_CHANNELS];
+};
+
+struct port {
+	enum spa_direction direction;
+	uint32_t id;
+
+	uint64_t info_all;
+	struct spa_port_info info;
+#define PORT_EnumFormat		0
+#define PORT_Meta		1
+#define PORT_IO			2
+#define PORT_Format		3
+#define PORT_Buffers		4
+#define PORT_Latency		5
+#define N_PORT_PARAMS		6
+	struct spa_param_info params[N_PORT_PARAMS];
+
+	bool have_format;
+	struct spa_audio_info current_format;
+
+	struct spa_io_buffers *io;
+	struct spa_io_rate_match *rate_match;
+	struct buffer buffers[MAX_BUFFERS];
+	unsigned int n_buffers;
+
+	struct spa_list free;
+	struct spa_list ready;
+	uint32_t ready_offset;
+};
+
+struct state {
+	struct spa_handle handle;
+	struct spa_node node;
+
+	struct spa_log *log;
+	struct spa_system *data_system;
+	struct spa_loop *data_loop;
+
+	struct spa_hook_list hooks;
+	struct spa_callbacks callbacks;
+
+	uint64_t info_all;
+	struct spa_node_info info;
+#define NODE_PropInfo		0
+#define NODE_Props		1
+#define NODE_IO			2
+#define NODE_ProcessLatency	3
+#define N_NODE_PARAMS		4
+	struct spa_param_info params[N_NODE_PARAMS];
+	struct props props;
+
+	uint32_t default_period_size;
+	uint32_t default_format;
+	unsigned int default_channels;
+	unsigned int default_rate;
+	uint32_t allowed_rates[MAX_RATES];
+	uint32_t n_allowed_rates;
+	struct channel_map default_pos;
+	char clock_name[64];
+	uint32_t quantum_limit;
+
+	uint32_t format;
+	uint32_t rate;
+	uint32_t channels;
+	uint32_t stride;
+	uint32_t blocks;
+	uint32_t rate_denom;
+
+	struct spa_io_clock *clock;
+	struct spa_io_position *position;
+
+	struct port ports[1];
+
+	uint32_t duration;
+	unsigned int following:1;
+	unsigned int matching:1;
+	unsigned int resample:1;
+	unsigned int started:1;
+	unsigned int freewheel:1;
+
+	int timerfd;
+	struct spa_source timer_source;
+	uint64_t next_time;
+
+	int sockfd;
+	struct spa_source sock_source;
+	struct sockaddr_ll sock_addr;
+
+	struct spa_avbtp_packet_aaf *pdu;
+	size_t hdr_size;
+	size_t payload_size;
+	size_t pdu_size;
+	int64_t pdu_period;
+	uint8_t pdu_seq;
+	uint8_t prev_seq;
+
+	struct iovec iov[3];
+	struct msghdr msg;
+	char control[CMSG_SPACE(sizeof(__u64))];
+	struct cmsghdr *cmsg;
+
+	uint8_t *ringbuffer_data;
+	uint32_t ringbuffer_size;
+	struct spa_ringbuffer ring;
+
+	struct spa_dll dll;
+	double max_error;
+
+	struct spa_latency_info latency[2];
+	struct spa_process_latency_info process_latency;
+};
+
+struct spa_pod *spa_avb_enum_propinfo(struct state *state,
+		uint32_t idx, struct spa_pod_builder *b);
+int spa_avb_add_prop_params(struct state *state, struct spa_pod_builder *b);
+int spa_avb_parse_prop_params(struct state *state, struct spa_pod *params);
+
+int spa_avb_enum_format(struct state *state, int seq,
+		     uint32_t start, uint32_t num,
+		     const struct spa_pod *filter);
+
+int spa_avb_clear_format(struct state *state);
+int spa_avb_set_format(struct state *state, struct spa_audio_info *info, uint32_t flags);
+
+int spa_avb_init(struct state *state, const struct spa_dict *info);
+int spa_avb_clear(struct state *state);
+
+int spa_avb_start(struct state *state);
+int spa_avb_reassign_follower(struct state *state);
+int spa_avb_pause(struct state *state);
+
+int spa_avb_write(struct state *state);
+int spa_avb_read(struct state *state);
+int spa_avb_skip(struct state *state);
+
+void spa_avb_recycle_buffer(struct state *state, struct port *port, uint32_t buffer_id);
+
+static inline uint32_t spa_avb_format_from_name(const char *name, size_t len)
+{
+	int i;
+	for (i = 0; spa_type_audio_format[i].name; i++) {
+		if (strncmp(name, spa_debug_type_short_name(spa_type_audio_format[i].name), len) == 0)
+			return spa_type_audio_format[i].type;
+	}
+	return SPA_AUDIO_FORMAT_UNKNOWN;
+}
+
+static inline uint32_t spa_avb_channel_from_name(const char *name)
+{
+	int i;
+	for (i = 0; spa_type_audio_channel[i].name; i++) {
+		if (strcmp(name, spa_debug_type_short_name(spa_type_audio_channel[i].name)) == 0)
+			return spa_type_audio_channel[i].type;
+	}
+	return SPA_AUDIO_CHANNEL_UNKNOWN;
+}
+
+static inline void spa_avb_parse_position(struct channel_map *map, const char *val, size_t len)
+{
+	struct spa_json it[2];
+	char v[256];
+
+	spa_json_init(&it[0], val, len);
+        if (spa_json_enter_array(&it[0], &it[1]) <= 0)
+                spa_json_init(&it[1], val, len);
+
+	map->channels = 0;
+	while (spa_json_get_string(&it[1], v, sizeof(v)) > 0 &&
+	    map->channels < SPA_AUDIO_MAX_CHANNELS) {
+		map->pos[map->channels++] = spa_avb_channel_from_name(v);
+	}
+}
+
+static inline uint32_t spa_avb_parse_rates(uint32_t *rates, uint32_t max, const char *val, size_t len)
+{
+	struct spa_json it[2];
+	char v[256];
+	uint32_t count;
+
+	spa_json_init(&it[0], val, len);
+        if (spa_json_enter_array(&it[0], &it[1]) <= 0)
+                spa_json_init(&it[1], val, len);
+
+	count = 0;
+	while (spa_json_get_string(&it[1], v, sizeof(v)) > 0 && count < max)
+		rates[count++] = atoi(v);
+	return count;
+}
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_AVB_PCM_H */
diff --git a/spa/plugins/avb/avb.c b/spa/plugins/avb/avb.c
new file mode 100644
index 0000000000000000000000000000000000000000..8f6731025bc50cf7ac1afc927ca4c1d461fd0167
--- /dev/null
+++ b/spa/plugins/avb/avb.c
@@ -0,0 +1,54 @@
+/* Spa AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+
+#include <spa/support/plugin.h>
+#include <spa/support/log.h>
+
+extern const struct spa_handle_factory spa_avb_sink_factory;
+extern const struct spa_handle_factory spa_avb_source_factory;
+
+struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.avb");
+struct spa_log_topic *avb_log_topic = &log_topic;
+
+SPA_EXPORT
+int spa_handle_factory_enum(const struct spa_handle_factory **factory, uint32_t *index)
+{
+	spa_return_val_if_fail(factory != NULL, -EINVAL);
+	spa_return_val_if_fail(index != NULL, -EINVAL);
+
+	switch (*index) {
+	case 0:
+		*factory = &spa_avb_sink_factory;
+		break;
+	case 1:
+		*factory = &spa_avb_source_factory;
+		break;
+	default:
+		return 0;
+	}
+	(*index)++;
+	return 1;
+}
diff --git a/spa/plugins/avb/avb.h b/spa/plugins/avb/avb.h
new file mode 100644
index 0000000000000000000000000000000000000000..a99a0fed4e48e4ef0993083adf6261682dc5472c
--- /dev/null
+++ b/spa/plugins/avb/avb.h
@@ -0,0 +1,39 @@
+/* Spa AVB
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_AVB_H
+#define SPA_AVB_H
+
+#include <spa/support/log.h>
+
+#undef SPA_LOG_TOPIC_DEFAULT
+#define SPA_LOG_TOPIC_DEFAULT avb_log_topic
+extern struct spa_log_topic *avb_log_topic;
+
+static inline void avb_log_topic_init(struct spa_log *log)
+{
+	spa_log_topic_init(log, avb_log_topic);
+}
+
+#endif /* SPA_AVB_H */
diff --git a/spa/plugins/avb/avbtp/packets.h b/spa/plugins/avb/avbtp/packets.h
new file mode 100644
index 0000000000000000000000000000000000000000..3d4a652ee7a249eab3f37d4fbf51753605b0ef50
--- /dev/null
+++ b/spa/plugins/avb/avbtp/packets.h
@@ -0,0 +1,220 @@
+/* Spa AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_AVB_PACKETS_H
+#define SPA_AVB_PACKETS_H
+
+#define SPA_AVBTP_SUBTYPE_61883_IIDC		0x00
+#define SPA_AVBTP_SUBTYPE_MMA_STREAM		0x01
+#define SPA_AVBTP_SUBTYPE_AAF			0x02
+#define SPA_AVBTP_SUBTYPE_CVF			0x03
+#define SPA_AVBTP_SUBTYPE_CRF			0x04
+#define SPA_AVBTP_SUBTYPE_TSCF			0x05
+#define SPA_AVBTP_SUBTYPE_SVF			0x06
+#define SPA_AVBTP_SUBTYPE_RVF			0x07
+#define SPA_AVBTP_SUBTYPE_AEF_CONTINUOUS	0x6E
+#define SPA_AVBTP_SUBTYPE_VSF_STREAM		0x6F
+#define SPA_AVBTP_SUBTYPE_EF_STREAM		0x7F
+#define SPA_AVBTP_SUBTYPE_NTSCF			0x82
+#define SPA_AVBTP_SUBTYPE_ESCF			0xEC
+#define SPA_AVBTP_SUBTYPE_EECF			0xED
+#define SPA_AVBTP_SUBTYPE_AEF_DISCRETE		0xEE
+#define SPA_AVBTP_SUBTYPE_ADP			0xFA
+#define SPA_AVBTP_SUBTYPE_AECP			0xFB
+#define SPA_AVBTP_SUBTYPE_ACMP			0xFC
+#define SPA_AVBTP_SUBTYPE_MAAP			0xFE
+#define SPA_AVBTP_SUBTYPE_EF_CONTROL		0xFF
+
+struct spa_avbtp_packet_common {
+	uint8_t subtype;
+#if __BYTE_ORDER == __BIG_ENDIAN
+	unsigned sv:1;			/* stream_id valid */
+	unsigned version:3;
+	unsigned subtype_data1:4;
+#elif __BYTE_ORDER == __LITTLE_ENDIAN
+	unsigned subtype_data1:4;
+	unsigned version:3;
+	unsigned sv:1;
+#elif
+#error "Unknown byte order"
+#endif
+	uint16_t subtype_data2;
+	uint64_t stream_id;
+	uint8_t payload[0];
+} __attribute__ ((__packed__));
+
+#define SPA_AVBTP_PACKET_SET_SUBTYPE(p,v)	((p)->subtype = (v))
+#define SPA_AVBTP_PACKET_SET_SV(p,v)		((p)->sv = (v))
+#define SPA_AVBTP_PACKET_SET_VERSION(p,v)	((p)->version = (v))
+#define SPA_AVBTP_PACKET_SET_STREAM_ID(p,v)	((p)->stream_id = htobe64(v))
+
+#define SPA_AVBTP_PACKET_GET_SUBTYPE(p)		((p)->subtype)
+#define SPA_AVBTP_PACKET_GET_SV(p)		((p)->sv)
+#define SPA_AVBTP_PACKET_GET_VERSION(p)		((p)->version)
+#define SPA_AVBTP_PACKET_GET_STREAM_ID(p)	be64toh((p)->stream_id)
+
+struct spa_avbtp_packet_cc {
+	uint8_t subtype;
+#if __BYTE_ORDER == __BIG_ENDIAN
+	unsigned sv:1;
+	unsigned version:3;
+	unsigned control_data1:4;
+#elif __BYTE_ORDER == __LITTLE_ENDIAN
+	unsigned control_data1:4;
+	unsigned version:3;
+	unsigned sv:1;
+#endif
+	uint8_t status;
+	uint16_t control_frame_length;
+	uint64_t stream_id;
+	uint8_t payload[0];
+} __attribute__ ((__packed__));
+
+#define SPA_AVBTP_PACKET_CC_SET_SUBTYPE(p,v)		((p)->subtype = (v))
+#define SPA_AVBTP_PACKET_CC_SET_SV(p,v)			((p)->sv = (v))
+#define SPA_AVBTP_PACKET_CC_SET_VERSION(p,v)		((p)->version = (v))
+#define SPA_AVBTP_PACKET_CC_SET_STREAM_ID(p,v)		((p)->stream_id = htobe64(v))
+#define SPA_AVBTP_PACKET_CC_SET_STATUS(p,v)		((p)->status = (v))
+#define SPA_AVBTP_PACKET_CC_SET_LENGTH(p,v)		((p)->control_frame_length = htons(v))
+
+#define SPA_AVBTP_PACKET_CC_GET_SUBTYPE(p)		((p)->subtype)
+#define SPA_AVBTP_PACKET_CC_GET_SV(p)			((p)->sv)
+#define SPA_AVBTP_PACKET_CC_GET_VERSION(p)		((p)->version)
+#define SPA_AVBTP_PACKET_CC_GET_STREAM_ID(p)		be64toh((p)->stream_id)
+#define SPA_AVBTP_PACKET_CC_GET_STATUS(p)		((p)->status)
+#define SPA_AVBTP_PACKET_CC_GET_LENGTH(p)		ntohs((p)->control_frame_length)
+
+/* AAF */
+struct spa_avbtp_packet_aaf {
+	uint8_t subtype;
+#if __BYTE_ORDER == __BIG_ENDIAN
+	unsigned sv:1;
+	unsigned version:3;
+	unsigned mr:1;
+	unsigned _r1:1;
+	unsigned gv:1;
+	unsigned tv:1;
+
+	uint8_t seq_num;
+
+	unsigned _r2:7;
+	unsigned tu:1;
+#elif __BYTE_ORDER == __LITTLE_ENDIAN
+	unsigned tv:1;
+	unsigned gv:1;
+	unsigned _r1:1;
+	unsigned mr:1;
+	unsigned version:3;
+	unsigned sv:1;
+
+	uint8_t seq_num;
+
+	unsigned tu:1;
+	unsigned _r2:7;
+#endif
+	uint64_t stream_id;
+	uint32_t timestamp;
+#define SPA_AVBTP_AAF_FORMAT_USER		0x00
+#define SPA_AVBTP_AAF_FORMAT_FLOAT_32BIT	0x01
+#define SPA_AVBTP_AAF_FORMAT_INT_32BIT		0x02
+#define SPA_AVBTP_AAF_FORMAT_INT_24BIT		0x03
+#define SPA_AVBTP_AAF_FORMAT_INT_16BIT		0x04
+#define SPA_AVBTP_AAF_FORMAT_AES3_32BIT		0x05
+	uint8_t format;
+
+#define SPA_AVBTP_AAF_PCM_NSR_USER		0x00
+#define SPA_AVBTP_AAF_PCM_NSR_8KHZ		0x01
+#define SPA_AVBTP_AAF_PCM_NSR_16KHZ		0x02
+#define SPA_AVBTP_AAF_PCM_NSR_32KHZ		0x03
+#define SPA_AVBTP_AAF_PCM_NSR_44_1KHZ		0x04
+#define SPA_AVBTP_AAF_PCM_NSR_48KHZ		0x05
+#define SPA_AVBTP_AAF_PCM_NSR_88_2KHZ		0x06
+#define SPA_AVBTP_AAF_PCM_NSR_96KHZ		0x07
+#define SPA_AVBTP_AAF_PCM_NSR_176_4KHZ		0x08
+#define SPA_AVBTP_AAF_PCM_NSR_192KHZ		0x09
+#define SPA_AVBTP_AAF_PCM_NSR_24KHZ		0x0A
+#if __BYTE_ORDER == __BIG_ENDIAN
+	unsigned nsr:4;
+	unsigned _r3:4;
+#elif __BYTE_ORDER == __LITTLE_ENDIAN
+	unsigned _r3:4;
+	unsigned nsr:4;
+#endif
+	uint8_t chan_per_frame;
+	uint8_t bit_depth;
+	uint16_t data_len;
+
+#define SPA_AVBTP_AAF_PCM_SP_NORMAL		0x00
+#define SPA_AVBTP_AAF_PCM_SP_SPARSE		0x01
+#if __BYTE_ORDER == __BIG_ENDIAN
+	unsigned _r4:3;
+	unsigned sp:1;
+	unsigned event:4;
+#elif __BYTE_ORDER == __LITTLE_ENDIAN
+	unsigned event:4;
+	unsigned sp:1;
+	unsigned _r4:3;
+#endif
+	uint8_t _r5;
+	uint8_t payload[0];
+} __attribute__ ((__packed__));
+
+#define SPA_AVBTP_PACKET_AAF_SET_SUBTYPE(p,v)		((p)->subtype = (v))
+#define SPA_AVBTP_PACKET_AAF_SET_SV(p,v)		((p)->sv = (v))
+#define SPA_AVBTP_PACKET_AAF_SET_VERSION(p,v)		((p)->version = (v))
+#define SPA_AVBTP_PACKET_AAF_SET_MR(p,v)		((p)->mr = (v))
+#define SPA_AVBTP_PACKET_AAF_SET_GV(p,v)		((p)->gv = (v))
+#define SPA_AVBTP_PACKET_AAF_SET_TV(p,v)		((p)->tv = (v))
+#define SPA_AVBTP_PACKET_AAF_SET_SEQ_NUM(p,v)		((p)->seq_num = (v))
+#define SPA_AVBTP_PACKET_AAF_SET_TU(p,v)		((p)->tu = (v))
+#define SPA_AVBTP_PACKET_AAF_SET_STREAM_ID(p,v)		((p)->stream_id = htobe64(v))
+#define SPA_AVBTP_PACKET_AAF_SET_TIMESTAMP(p,v)		((p)->timestamp = htonl(v))
+#define SPA_AVBTP_PACKET_AAF_SET_DATA_LEN(p,v)		((p)->data_len = htons(v))
+#define SPA_AVBTP_PACKET_AAF_SET_FORMAT(p,v)		((p)->format = (v))
+#define SPA_AVBTP_PACKET_AAF_SET_NSR(p,v)		((p)->nsr = (v))
+#define SPA_AVBTP_PACKET_AAF_SET_CHAN_PER_FRAME(p,v)	((p)->chan_per_frame = (v))
+#define SPA_AVBTP_PACKET_AAF_SET_BIT_DEPTH(p,v)		((p)->bit_depth = (v))
+#define SPA_AVBTP_PACKET_AAF_SET_SP(p,v)		((p)->sp = (v))
+#define SPA_AVBTP_PACKET_AAF_SET_EVENT(p,v)		((p)->event = (v))
+
+#define SPA_AVBTP_PACKET_AAF_GET_SUBTYPE(p)		((p)->subtype)
+#define SPA_AVBTP_PACKET_AAF_GET_SV(p)			((p)->sv)
+#define SPA_AVBTP_PACKET_AAF_GET_VERSION(p)		((p)->version)
+#define SPA_AVBTP_PACKET_AAF_GET_MR(p)			((p)->mr)
+#define SPA_AVBTP_PACKET_AAF_GET_GV(p)			((p)->gv)
+#define SPA_AVBTP_PACKET_AAF_GET_TV(p)			((p)->tv)
+#define SPA_AVBTP_PACKET_AAF_GET_SEQ_NUM(p)		((p)->seq_num)
+#define SPA_AVBTP_PACKET_AAF_GET_TU(p)			((p)->tu)
+#define SPA_AVBTP_PACKET_AAF_GET_STREAM_ID(p)		be64toh((p)->stream_id)
+#define SPA_AVBTP_PACKET_AAF_GET_TIMESTAMP(p)		ntohl((p)->timestamp)
+#define SPA_AVBTP_PACKET_AAF_GET_DATA_LEN(p)		ntohs((p)->data_len)
+#define SPA_AVBTP_PACKET_AAF_GET_FORMAT(p)		((p)->format)
+#define SPA_AVBTP_PACKET_AAF_GET_NSR(p)			((p)->nsr)
+#define SPA_AVBTP_PACKET_AAF_GET_CHAN_PER_FRAME(p)	((p)->chan_per_frame)
+#define SPA_AVBTP_PACKET_AAF_GET_BIT_DEPTH(p)		((p)->bit_depth)
+#define SPA_AVBTP_PACKET_AAF_GET_SP(p)			((p)->sp)
+#define SPA_AVBTP_PACKET_AAF_GET_EVENT(p)		((p)->event)
+
+
+#endif /* SPA_AVB_PACKETS_H */
diff --git a/spa/plugins/avb/meson.build b/spa/plugins/avb/meson.build
new file mode 100644
index 0000000000000000000000000000000000000000..2d9759ec794986e86c63dabef9524537abc1dd1d
--- /dev/null
+++ b/spa/plugins/avb/meson.build
@@ -0,0 +1,14 @@
+spa_avb_sources = ['avb.c',
+                'avb.h',
+                'avb-pcm-sink.c',
+                'avb-pcm-source.c',
+                'avb-pcm.c' ]
+
+spa_avb = shared_library(
+  'spa-avb',
+  [ spa_avb_sources ],
+  include_directories : [configinc],
+  dependencies : [ spa_dep, mathlib, epoll_shim_dep ],
+  install : true,
+  install_dir : spa_plugindir / 'avb'
+)
diff --git a/spa/plugins/bluez5/README-OPUS-A2DP.md b/spa/plugins/bluez5/README-OPUS-A2DP.md
new file mode 100644
index 0000000000000000000000000000000000000000..e94623f7516f99caa5554c86495accca8a7168d7
--- /dev/null
+++ b/spa/plugins/bluez5/README-OPUS-A2DP.md
@@ -0,0 +1,321 @@
+---
+title: OPUS-A2DP-0.5 specification
+author: Pauli Virtanen <pav@iki.fi>
+date: Jun 4, 2022
+---
+
+# OPUS-A2DP-0.5 specification
+
+DRAFT
+
+In this file, we specify how to use Opus as an A2DP vendor codec. We
+will call this "OPUS-A2DP-0.5". There is no previous public
+specification for using Opus as an A2DP vendor codec (to my
+knowledge), which is why we need this one.
+
+[[_TOC_]]
+
+# A2DP Codec Capabilities
+
+The A2DP capability structure is as follows.
+
+Integer fields and multi-byte bitfields are laid out in **little
+endian** order.  All integer fields are unsigned.
+
+Each entry may have different meaning when present as a capability.
+Below, we indicate this by abbreviations CAP/SNK for sink capability,
+CAP/SRC for source capability, CAP for capability as either, and SEL
+for the selected value by SRC.
+
+Bits in fields marked RFA (Reserved For Additions) shall be set to
+zero.
+
+The capability and configuration structure is as follows:
+
+| Octet | Bits | Meaning                                       |
+|-------|------|-----------------------------------------------|
+| 0-5   | 0-7  | Vendor ID Part                                |
+| 6-7   | 0-7  | Channel Configuration                         |
+| 8-11  | 0-7  | Audio Location Configuration                  |
+| 12-14 | 0-7  | Limits Configuration                          |
+| 15-16 | 0-7  | Return Direction Channel Configuration        |
+| 17-20 | 0-7  | Return Direction Audio Location Configuration |
+| 21-23 | 0-7  | Return Direction Limits Configuration         |
+
+See `a2dp-codec-caps.h` for definition as C structs.
+
+## Vendor ID Part
+
+The fixed value
+
+| Octet | Bits | Meaning                       |
+|-------|------|-------------------------------|
+| 0-3   | 0-7  | A2DP Vendor ID (0x05F1)       |
+| 4-5   | 0-7  | A2DP Vendor Codec ID (0x1005) |
+
+The Vendor ID is that of the Linux Foundation, and we are using it
+here unofficially.
+
+## Channel Configuration
+
+The channel configuration consists of the channel count and a bitfield
+indicating which of them are encoded in coupled streams.
+
+| Octet | Bits | Meaning                                                    |
+|-------|------|------------------------------------------------------------|
+| 6     | 0-7  | Channel Count. CAP: maximum number supported. SEL: actual. |
+| 7     | 0-7  | Coupled Stream Count. CAP: 0. SEL: actual.                 |
+
+The Channel Count indicates the number of logical channels encoded in
+the data stream.
+
+The Coupled Stream Count indicates the number of streams that encode a
+coupled (left & right) channel pair.  The count shall satisfy
+`(Channel Count) >= 2*(Coupled Stream Count)`.
+The Stream Count is `(Channel Count) - (Coupled Stream Count)`.
+
+Streams and Coupled Streams have the same meaning as in Sec. 5.1.1 of
+Opus Multistream [RFC7845].
+
+The logical Channels are identified by a Channel Index *j* such that `0 <= j
+< (Channel Count)`. The channels `0 <= j < 2*(Coupled Stream Count)`
+are encoded in the *k*-th stream of the payload, where `k = floor(j/2)` and
+`j mod 2` determines which of the two channels of the stream the logical
+channel is. The channels `2*(Coupled Stream Count) <= j < (Channel Count)`
+are encoded in the *k*-th stream of the payload, where `k = j - (Coupled Stream Count)`.
+The prescription here is identical to [RFC7845] with channel mapping
+`mapping[j] = j`.
+
+The semantic meaning for each channel is determined by their Audio
+Location.
+
+## Audio Location Configuration
+
+The channel audio location specification is similar to the location
+bitfield of the `Audio_Channel_Allocation` LTV structure in Bluetooth
+SIG [Assigned Numbers, Generic Audio] used in the LE Audio.
+
+| Octet | Bits | Meaning                                              |
+|-------|------|------------------------------------------------------|
+| 8-11  | 0-7  | Audio Location bitfield. CAP: available. SEL: actual |
+
+The values specified in CAP are informative, and SEL may contain bits
+that were not set in CAP. SNK shall handle unsupported audio
+locations. It may do this for example by ignoring unsupported channels
+or via suitable up/downmixing.  Hence, SRC may transmit channels with
+audio locations that are not marked supported by SNK. The maximum
+Channel Count however shall not be exceeded.
+
+The audio location bitfield values defined in [Assigned Numbers,
+Generic Audio] are:
+
+| Channel Order | Bitmask    | Audio Location          |
+|---------------|------------|-------------------------|
+| 0             | 0x00000001 | Front Left              |
+| 1             | 0x00000002 | Front Right             |
+| 2             | 0x00000400 | Side Left               |
+| 3             | 0x00000800 | Side Right              |
+| 4             | 0x00000010 | Back Left               |
+| 5             | 0x00000020 | Back Right              |
+| 6             | 0x00000040 | Front Left of Center    |
+| 7             | 0x00000080 | Front Right of Center   |
+| 8             | 0x00001000 | Top Front Left          |
+| 9             | 0x00002000 | Top Front Right         |
+| 10            | 0x00040000 | Top Side Left           |
+| 11            | 0x00080000 | Top Side Right          |
+| 12            | 0x00010000 | Top Back Left           |
+| 13            | 0x00020000 | Top Back Right          |
+| 14            | 0x00400000 | Bottom Front Left       |
+| 15            | 0x00800000 | Bottom Front Right      |
+| 16            | 0x01000000 | Front Left Wide         |
+| 17            | 0x02000000 | Front Right Wide        |
+| 18            | 0x04000000 | Left Surround           |
+| 19            | 0x08000000 | Right Surround          |
+| 20            | 0x00000004 | Front Center            |
+| 21            | 0x00000100 | Back Center             |
+| 22            | 0x00004000 | Top Front Center        |
+| 23            | 0x00008000 | Top Center              |
+| 24            | 0x00100000 | Top Back Center         |
+| 25            | 0x00200000 | Bottom Front Center     |
+| 26            | 0x00000008 | Low Frequency Effects 1 |
+| 27            | 0x00000200 | Low Frequency Effects 2 |
+| 28            | 0x10000000 | RFA                     |
+| 29            | 0x20000000 | RFA                     |
+| 30            | 0x40000000 | RFA                     |
+| 31            | 0x80000000 | RFA                     |
+
+In addition, we define a specific Channel Order for each.  The bits
+set in the bitfield define audio locations for the streams present in the
+payload. The set bit with the smallest Channel Order value defines the
+audio location for the Channel Index *j=0*, the bit with the next
+lowest Channel Order value defines the audio location for the Channel
+Index *j=1*, and so forth.
+
+When the Channel Count is larger than the number of bits set in the
+Audio Location bitfield, the audio locations of the remaining channels
+are unspecified. Implementations may handle them as appropriate for
+their use case, considering them as AUX0-AUXN, or in the case of
+Channel Count = 1, as the single mono audio channel.
+
+When the Channel Count is smaller than the number of bits set in the
+Audio Location bitfield, the audio locations for the channels are
+assigned as above, and remaining excess bits shall be ignored.
+
+The channel ordering defined here is compatible with the internal
+stream ordering in the reference Opus Multistream surround encoder
+Mapping Family 0 and 1 output. This allows making use of its surround
+masking and LFE handling capabilities.  The stream ordering of the
+reference Opus surround encoder, although being unchanged since its
+addition in 2013, is an internal detail of the
+encoder. Implementations using the surround encoder shall check that
+the mapping table used by the encoder corresponds to the above channel
+ordering.
+
+For reference, we list the Audio Location bitfield values
+corresponding to the different channel counts in Opus Mapping Family 0
+and 1 surround encoder output, and the expected mapping table:
+
+| Mapping Family | Channel Count | Audio Location Value | Stream Ordering                 | Mapping Table            |
+|----------------|---------------|----------------------|---------------------------------|--------------------------|
+| 0              | 1             | 0x00000000           | mono                            | {0}                      |
+| 0              | 2             | 0x00000003           | FL, FR                          | {0, 1}                   |
+| 1              | 1             | 0x00000000           | mono                            | {0}                      |
+| 1              | 2             | 0x00000003           | FL, FR                          | {0, 1}                   |
+| 1              | 3             | 0x00000007           | FL, FR, FC                      | {0, 2, 1}                |
+| 1              | 4             | 0x00000033           | FL, FR, BL, BR                  | {0, 1, 2, 3}             |
+| 1              | 5             | 0x00000037           | FL, FR, BL, BR, FC              | {0, 4, 1, 2, 3}          |
+| 1              | 6             | 0x0000003f           | FL, FR, BL, BR, FC, LFE         | {0, 4, 1, 2, 3, 5}       |
+| 1              | 7             | 0x00000d0f           | FL, FR, SL, SR, FC, BC, LFE     | {0, 4, 1, 2, 3, 5, 6}    |
+| 1              | 8             | 0x00000c3f           | FL, FR, SL, SR, BL, BR, FC, LFE | {0, 6, 1, 2, 3, 4, 5, 7} |
+
+The Mapping Table in the table indicates the mapping table selected by
+`opus_multistream_surround_encoder_create` (Opus 1.3.1). If the
+encoder outputs a different mapping table in a future Opus encoder
+release, the channel ordering will be incorrect, and the surround
+encoder can not be used. We expect that the probability of the Opus
+encoder authors making such changes is negligible.
+
+## Limits Configuration
+
+The limits for allowed frame durations and maximum bitrate can also be
+configured.
+
+| Octet | Bits | Meaning                                             |
+|-------|------|-----------------------------------------------------|
+| 16    | 0    | Frame duration 2.5ms. CAP: supported, SEL: selected |
+| 16    | 1    | Frame duration 5ms. CAP: supported, SEL: selected   |
+| 16    | 2    | Frame duration 10ms. CAP: supported, SEL: selected  |
+| 16    | 3    | Frame duration 20ms. CAP: supported, SEL: selected  |
+| 16    | 4    | Frame duration 40ms. CAP: supported, SEL: selected  |
+| 16    | 5-7  | RFA                                                 |
+
+| Octet | Bits | Meaning                                        |
+|-------|------|------------------------------------------------|
+| 17-18 | 0-7  | Maximum bitrate. CAP: supported, SEL: selected |
+
+The maximum bitrate is given in units of 1024 bits per second.
+
+The maximum bitrate field in CAP may contain value 0 to indicate
+everything is supported.
+
+## Bidirectional Audio Configuration
+
+Bidirectional audio may be supported. Its Channel Configuration, Audio
+Location Configuration, and Limits Configuration have identical form
+to the forward direction, and represented by exactly similar
+structures.
+
+Namely:
+
+| Octet | Bits | Meaning                                            |
+|-------|------|----------------------------------------------------|
+| 19-20 | 0-7  | Channel Configuration fields, for return direction |
+| 21-28 | 0-7  | Audio Location fields, for return direction        |
+| 29-31 | 0-7  | Limits Configuration fields, for return direction  |
+
+If no return channel is supported or selected, the number of channels
+is set to 0 in CAP or SEL.
+
+
+# Packet Structure
+
+Each packet consists of an RTP header, an RTP payload header, and a
+payload containing Opus Multistream data.
+
+| Octet | Bits | Meaning                  |
+|-------|------|--------------------------|
+| 0-11  | 0-7  | RTP header               |
+| 12    | 0-7  | RTP payload header       |
+| 13-N  | 0-7  | Opus Multistream payload |
+
+For each Bluetooth packet, the payload shall contain exactly one Opus
+Multistream packet, or a fragment of one. The Opus Multistream packet
+may be fragmented to several consecutive Bluetooth packets.
+
+The format of the Multistream data is the same as in the audio packets
+of [RFC7845], or, as produced/consumed by the Opus Multistream API.
+
+(Note that we DO NOT follow [RFC7587], as we want fragmentation and
+multichannel support.)
+
+## RTP Header
+
+See [RFC3550].
+
+The RTP payload type is pt=96 (dynamic).
+
+## RTP Payload Header
+
+The RTP payload header is used to indicate if and how the Opus
+Multistream packet is fragmented across several consecutive Bluetooth
+packets.
+
+| Octet  | Bits | Meaning
+|--------|------|--------------------------------------------------------
+|   0    | 0-3  | Frame Count
+|   4    | 4    | RFA
+|   4    | 5    | Is Last Fragment
+|   4    | 6    | Is First Fragment
+|   4    | 7    | Is Fragmented
+
+In each packet, Frame Count indicates how many Bluetooth packets are
+still to be received (including the present packet) before the Opus
+Multistream packet is complete.
+
+The Is Fragment flag indicates whether the present packet contains
+fragmented payload.
+
+The Is Last Fragment flag indicates whether the present packet is the
+last part of fragmented payload.
+
+The Is First Fragment flag indicates whether the present packet is the
+first part of fragmented payload.
+
+In non-fragmented packets, Frame Count shall be (1), and the other bits
+in the header zero.
+
+## Opus Payload
+
+The Opus payload is a single Opus Multistream packet, or its fragment.
+
+In case of fragmentation, as indicated by the RTP payload header,
+concatenating the payloads of the fragment Bluetooth packets shall
+yield the total Opus Multistream packet.
+
+The SRC should choose encoder parameters such that Bluetooth bandwidth
+limitations are not exceeded.
+
+The SRC may include FEC data. The SNK may enable forward error
+correction instead of PLC.
+
+
+# References
+
+1. IETF RFC 3550: [RFC3550]
+2. IETF RFC 7587: [RFC7587]
+3. IETF RFC 7845: [RFC7845]
+
+[RFC3550]: https://datatracker.ietf.org/doc/html/rfc3550
+[RFC7587]: https://datatracker.ietf.org/doc/html/rfc7587
+[RFC7845]: https://datatracker.ietf.org/doc/html/rfc7845
+[Assigned Numbers, Generic Audio]: https://www.bluetooth.com/specifications/assigned-numbers/
diff --git a/spa/plugins/bluez5/a2dp-codec-aac.c b/spa/plugins/bluez5/a2dp-codec-aac.c
index 1195def30299ad0ae17e70c8365ecba1cee1d9b8..c14ff2af9af32e7fbc2f9f103b8dd4abe57e2535 100644
--- a/spa/plugins/bluez5/a2dp-codec-aac.c
+++ b/spa/plugins/bluez5/a2dp-codec-aac.c
@@ -31,10 +31,16 @@
 #include <spa/utils/dict.h>
 
 #include <fdk-aac/aacenc_lib.h>
+#include <fdk-aac/aacdecoder_lib.h>
 
 #include "rtp.h"
 #include "a2dp-codecs.h"
 
+static struct spa_log *log;
+static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.codecs.aac");
+#undef SPA_LOG_TOPIC_DEFAULT
+#define SPA_LOG_TOPIC_DEFAULT &log_topic
+
 #define DEFAULT_AAC_BITRATE	320000
 #define MIN_AAC_BITRATE		64000
 
@@ -44,6 +50,7 @@ struct props {
 
 struct impl {
 	HANDLE_AACENCODER aacenc;
+	HANDLE_AACDECODER aacdec;
 
 	struct rtp_header *header;
 
@@ -170,7 +177,7 @@ static int codec_select_config(const struct a2dp_codec *codec, uint32_t flags,
 	return sizeof(conf);
 }
 
-static int codec_enum_config(const struct a2dp_codec *codec,
+static int codec_enum_config(const struct a2dp_codec *codec, uint32_t flags,
 		const void *caps, size_t caps_size, uint32_t id, uint32_t idx,
 		struct spa_pod_builder *b, struct spa_pod **param)
 {
@@ -289,7 +296,7 @@ static int codec_validate_config(const struct a2dp_codec *codec, uint32_t flags,
 	return 0;
 }
 
-static void *codec_init_props(const struct a2dp_codec *codec, const struct spa_dict *settings)
+static void *codec_init_props(const struct a2dp_codec *codec, uint32_t flags, const struct spa_dict *settings)
 {
 	struct props *p = calloc(1, sizeof(struct props));
 	const char *str;
@@ -412,11 +419,39 @@ static void *codec_init(const struct a2dp_codec *codec, uint32_t flags,
 
 	this->codesize = enc_info.frameLength * this->channels * this->samplesize;
 
+	this->aacdec = aacDecoder_Open(TT_MP4_LATM_MCP1, 1);
+	if (!this->aacdec) {
+		res = -EINVAL;
+		goto error;
+	}
+
+#ifdef AACDECODER_LIB_VL0
+	res = aacDecoder_SetParam(this->aacdec, AAC_PCM_MIN_OUTPUT_CHANNELS, this->channels);
+	if (res != AAC_DEC_OK) {
+		spa_log_debug(log, "Couldn't set min output channels: 0x%04X", res);
+		goto error;
+	}
+
+	res = aacDecoder_SetParam(this->aacdec, AAC_PCM_MAX_OUTPUT_CHANNELS, this->channels);
+	if (res != AAC_DEC_OK) {
+		spa_log_debug(log, "Couldn't set max output channels: 0x%04X", res);
+		goto error;
+	}
+#else
+	res = aacDecoder_SetParam(this->aacdec, AAC_PCM_OUTPUT_CHANNELS, this->channels);
+	if (res != AAC_DEC_OK) {
+		spa_log_debug(log, "Couldn't set output channels: 0x%04X", res);
+		goto error;
+	}
+#endif
+
 	return this;
 
 error:
-	if (this->aacenc)
+	if (this && this->aacenc)
 		aacEncClose(&this->aacenc);
+	if (this && this->aacdec)
+		aacDecoder_Close(this->aacdec);
 	free(this);
 	errno = -res;
 	return NULL;
@@ -427,6 +462,8 @@ static void codec_deinit(void *data)
 	struct impl *this = data;
 	if (this->aacenc)
 		aacEncClose(&this->aacenc);
+	if (this->aacdec)
+		aacDecoder_Close(this->aacdec);
 	free(this);
 }
 
@@ -502,6 +539,55 @@ static int codec_encode(void *data,
 	return out_args.numInSamples * this->samplesize;
 }
 
+static int codec_start_decode (void *data,
+		const void *src, size_t src_size, uint16_t *seqnum, uint32_t *timestamp)
+{
+	const struct rtp_header *header = src;
+	size_t header_size = sizeof(struct rtp_header);
+
+	spa_return_val_if_fail (src_size > header_size, -EINVAL);
+
+	if (seqnum)
+		*seqnum = ntohs(header->sequence_number);
+	if (timestamp)
+		*timestamp = ntohl(header->timestamp);
+
+	return header_size;
+}
+
+static int codec_decode(void *data,
+		const void *src, size_t src_size,
+		void *dst, size_t dst_size,
+		size_t *dst_out)
+{
+	struct impl *this = data;
+	uint data_size = (uint)src_size;
+	uint bytes_valid = data_size;
+	CStreamInfo *aacinf;
+	int res;
+
+	res = aacDecoder_Fill(this->aacdec, (UCHAR **)&src, &data_size, &bytes_valid);
+	if (res != AAC_DEC_OK) {
+		spa_log_debug(log, "AAC buffer fill error: 0x%04X", res);
+		return -EINVAL;
+	}
+
+	res = aacDecoder_DecodeFrame(this->aacdec, dst, dst_size, 0);
+	if (res != AAC_DEC_OK) {
+		spa_log_debug(log, "AAC decode frame error: 0x%04X", res);
+		return -EINVAL;
+	}
+
+	aacinf = aacDecoder_GetStreamInfo(this->aacdec);
+	if (!aacinf) {
+		spa_log_debug(log, "AAC get stream info failed");
+		return -EINVAL;
+	}
+	*dst_out = aacinf->frameSize * aacinf->numChannels * this->samplesize;
+
+	return src_size - bytes_valid;
+}
+
 static int codec_abr_process (void *data, size_t unsent)
 {
 	return -ENOTSUP;
@@ -538,6 +624,12 @@ static int codec_increase_bitpool(void *data)
 	return codec_change_bitrate(this, (this->cur_bitrate * 4) / 3);
 }
 
+static void codec_set_log(struct spa_log *global_log)
+{
+	log = global_log;
+	spa_log_topic_init(log, &log_topic);
+}
+
 const struct a2dp_codec a2dp_codec_aac = {
 	.id = SPA_BLUETOOTH_AUDIO_CODEC_AAC,
 	.codec_id = A2DP_CODEC_MPEG24,
@@ -554,9 +646,12 @@ const struct a2dp_codec a2dp_codec_aac = {
 	.get_block_size = codec_get_block_size,
 	.start_encode = codec_start_encode,
 	.encode = codec_encode,
+	.start_decode = codec_start_decode,
+	.decode = codec_decode,
 	.abr_process = codec_abr_process,
 	.reduce_bitpool = codec_reduce_bitpool,
 	.increase_bitpool = codec_increase_bitpool,
+	.set_log = codec_set_log,
 };
 
 A2DP_CODEC_EXPORT_DEF(
diff --git a/spa/plugins/bluez5/a2dp-codec-aptx.c b/spa/plugins/bluez5/a2dp-codec-aptx.c
index 9a80134fd5c08c4d19a5411088c35d30c63c7e1b..721b90599adbae76bd627b8d964f93625cd63011 100644
--- a/spa/plugins/bluez5/a2dp-codec-aptx.c
+++ b/spa/plugins/bluez5/a2dp-codec-aptx.c
@@ -218,7 +218,7 @@ static int codec_select_config_ll(const struct a2dp_codec *codec, uint32_t flags
 	return actual_conf_size;
 }
 
-static int codec_enum_config(const struct a2dp_codec *codec,
+static int codec_enum_config(const struct a2dp_codec *codec, uint32_t flags,
 		const void *caps, size_t caps_size, uint32_t id, uint32_t idx,
 		struct spa_pod_builder *b, struct spa_pod **param)
 {
@@ -458,7 +458,7 @@ static int codec_decode(void *data,
  * When connected as SRC to SNK, aptX-LL sink may send back mSBC data.
  */
 
-static int msbc_enum_config(const struct a2dp_codec *codec,
+static int msbc_enum_config(const struct a2dp_codec *codec, uint32_t flags,
 		const void *caps, size_t caps_size, uint32_t id, uint32_t idx,
 		struct spa_pod_builder *b, struct spa_pod **param)
 {
@@ -710,6 +710,11 @@ static const struct a2dp_codec aptx_ll_msbc = {
 	.increase_bitpool = msbc_increase_bitpool,
 };
 
+static const struct spa_dict_item duplex_info_items[] = {
+	{ "duplex.boost", "true" },
+};
+static const struct spa_dict duplex_info = SPA_DICT_INIT_ARRAY(duplex_info_items);
+
 const struct a2dp_codec a2dp_codec_aptx_ll_duplex_0 = {
 	APTX_LL_COMMON_DEFS,
 	.id = SPA_BLUETOOTH_AUDIO_CODEC_APTX_LL_DUPLEX,
@@ -718,6 +723,7 @@ const struct a2dp_codec a2dp_codec_aptx_ll_duplex_0 = {
 	.name = "aptx_ll_duplex",
 	.endpoint_name = "aptx_ll_duplex_0",
 	.duplex_codec = &aptx_ll_msbc,
+	.info = &duplex_info,
 };
 
 const struct a2dp_codec a2dp_codec_aptx_ll_duplex_1 = {
@@ -728,6 +734,7 @@ const struct a2dp_codec a2dp_codec_aptx_ll_duplex_1 = {
 	.name = "aptx_ll_duplex",
 	.endpoint_name = "aptx_ll_duplex_1",
 	.duplex_codec = &aptx_ll_msbc,
+	.info = &duplex_info,
 };
 
 A2DP_CODEC_EXPORT_DEF(
diff --git a/spa/plugins/bluez5/a2dp-codec-caps.h b/spa/plugins/bluez5/a2dp-codec-caps.h
index 3aa06529e96159cf88a09e43c5aedd095e3f1e1a..9f7259296d3a5d0a2fa4e70586ae69209b86e39e 100644
--- a/spa/plugins/bluez5/a2dp-codec-caps.h
+++ b/spa/plugins/bluez5/a2dp-codec-caps.h
@@ -233,6 +233,54 @@
 #define LC3PLUS_HR_SAMPLING_FREQ_48000	(1 << 8)
 #define LC3PLUS_HR_SAMPLING_FREQ_96000	(1 << 7)
 
+#define OPUS_05_VENDOR_ID		0x000005f1
+#define OPUS_05_CODEC_ID		0x1005
+
+#define OPUS_05_MAPPING_FAMILY_0	(1 << 0)
+#define OPUS_05_MAPPING_FAMILY_1	(1 << 1)
+#define OPUS_05_MAPPING_FAMILY_255	(1 << 2)
+
+#define OPUS_05_FRAME_DURATION_25	(1 << 0)
+#define OPUS_05_FRAME_DURATION_50	(1 << 1)
+#define OPUS_05_FRAME_DURATION_100	(1 << 2)
+#define OPUS_05_FRAME_DURATION_200	(1 << 3)
+#define OPUS_05_FRAME_DURATION_400	(1 << 4)
+
+#define OPUS_05_GET_UINT16(a, field)			\
+	(((a).field ## 2 << 8) | (a).field ## 1)
+#define OPUS_05_INIT_UINT16(field, v)			\
+	.field ## 1 = ((v) & 0xff),			\
+	.field ## 2 = (((v) >> 8) & 0xff),
+#define OPUS_05_SET_UINT16(a, field, v)			\
+	do {						\
+		(a).field ## 1 = ((v) & 0xff);		\
+		(a).field ## 2 = (((v) >> 8) & 0xff);	\
+	} while (0)
+#define OPUS_05_GET_UINT32(a, field)				\
+	(((a).field ## 4 << 24) | ((a).field ## 3 << 16) |	\
+	((a).field ## 2 << 8) | (a).field ## 1)
+#define OPUS_05_INIT_UINT32(field, v)			\
+	.field ## 1 = ((v) & 0xff),			\
+	.field ## 2 = (((v) >> 8) & 0xff),		\
+	.field ## 3 = (((v) >> 16) & 0xff),		\
+	.field ## 4 = (((v) >> 24) & 0xff),
+#define OPUS_05_SET_UINT32(a, field, v)			\
+	do {						\
+		(a).field ## 1 = ((v) & 0xff);		\
+		(a).field ## 2 = (((v) >> 8) & 0xff);	\
+		(a).field ## 3 = (((v) >> 16) & 0xff);	\
+		(a).field ## 4 = (((v) >> 24) & 0xff);	\
+	} while (0)
+
+#define OPUS_05_GET_LOCATION(a) OPUS_05_GET_UINT32(a, location)
+#define OPUS_05_INIT_LOCATION(v) OPUS_05_INIT_UINT32(location, v)
+#define OPUS_05_SET_LOCATION(a, v) OPUS_05_SET_UINT32(a, location, v)
+
+#define OPUS_05_GET_BITRATE(a) OPUS_05_GET_UINT16(a, bitrate)
+#define OPUS_05_INIT_BITRATE(v) OPUS_05_INIT_UINT16(bitrate, v)
+#define OPUS_05_SET_BITRATE(a, v) OPUS_05_SET_UINT16(a, bitrate, v)
+
+
 typedef struct {
 	uint32_t vendor_id;
 	uint16_t codec_id;
@@ -391,4 +439,22 @@ typedef struct {
 	uint8_t frequency2;
 } __attribute__ ((packed)) a2dp_lc3plus_hr_t;
 
+typedef struct {
+	uint8_t channels;
+	uint8_t coupled_streams;
+	uint8_t location1;
+	uint8_t location2;
+	uint8_t location3;
+	uint8_t location4;
+	uint8_t frame_duration;
+	uint8_t bitrate1;
+	uint8_t bitrate2;
+} __attribute__ ((packed)) a2dp_opus_05_direction_t;
+
+typedef struct {
+	a2dp_vendor_codec_t info;
+	a2dp_opus_05_direction_t main;
+	a2dp_opus_05_direction_t bidi;
+} __attribute__ ((packed)) a2dp_opus_05_t;
+
 #endif
diff --git a/spa/plugins/bluez5/a2dp-codec-faststream.c b/spa/plugins/bluez5/a2dp-codec-faststream.c
index ddcf331d62d65db97b6a2a5b8fde7eb31c23e031..03911945c3b49d3ac5f08ecaf21f8356629e8220 100644
--- a/spa/plugins/bluez5/a2dp-codec-faststream.c
+++ b/spa/plugins/bluez5/a2dp-codec-faststream.c
@@ -129,7 +129,7 @@ static int codec_select_config(const struct a2dp_codec *codec, uint32_t flags,
 	return sizeof(conf);
 }
 
-static int codec_enum_config(const struct a2dp_codec *codec,
+static int codec_enum_config(const struct a2dp_codec *codec, uint32_t flags,
 		const void *caps, size_t caps_size, uint32_t id, uint32_t idx,
 		struct spa_pod_builder *b, struct spa_pod **param)
 {
@@ -372,7 +372,7 @@ static SPA_UNUSED int codec_decode(void *data,
  * When connected as SRC to SNK, FastStream sink may send back SBC data.
  */
 
-static int duplex_enum_config(const struct a2dp_codec *codec,
+static int duplex_enum_config(const struct a2dp_codec *codec, uint32_t flags,
 		const void *caps, size_t caps_size, uint32_t id, uint32_t idx,
 		struct spa_pod_builder *b, struct spa_pod **param)
 {
@@ -614,17 +614,23 @@ static const struct a2dp_codec duplex_codec = {
 	.reduce_bitpool = codec_reduce_bitpool,		\
 	.increase_bitpool = codec_increase_bitpool
 
-const struct a2dp_codec a2dp_codec_faststream = {
+static const struct a2dp_codec a2dp_codec_faststream = {
 	FASTSTREAM_COMMON_DEFS,
 	.id = SPA_BLUETOOTH_AUDIO_CODEC_FASTSTREAM,
 	.name = "faststream",
 };
 
+static const struct spa_dict_item duplex_info_items[] = {
+	{ "duplex.boost", "true" },
+};
+static const struct spa_dict duplex_info = SPA_DICT_INIT_ARRAY(duplex_info_items);
+
 const struct a2dp_codec a2dp_codec_faststream_duplex = {
 	FASTSTREAM_COMMON_DEFS,
 	.id = SPA_BLUETOOTH_AUDIO_CODEC_FASTSTREAM_DUPLEX,
 	.name = "faststream_duplex",
 	.duplex_codec = &duplex_codec,
+	.info = &duplex_info,
 };
 
 A2DP_CODEC_EXPORT_DEF(
diff --git a/spa/plugins/bluez5/a2dp-codec-lc3plus.c b/spa/plugins/bluez5/a2dp-codec-lc3plus.c
index b50c947468f479e1ad3915095530660519e140ab..40ca0e0db142fa809560a9599ecbc08d7ef2a1e3 100644
--- a/spa/plugins/bluez5/a2dp-codec-lc3plus.c
+++ b/spa/plugins/bluez5/a2dp-codec-lc3plus.c
@@ -150,8 +150,8 @@ static int codec_select_config(const struct a2dp_codec *codec, uint32_t flags,
 	return sizeof(conf);
 }
 
-static int codec_caps_preference_cmp(const struct a2dp_codec *codec, const void *caps1, size_t caps1_size,
-		const void *caps2, size_t caps2_size, const struct a2dp_codec_audio_info *info)
+static int codec_caps_preference_cmp(const struct a2dp_codec *codec, uint32_t flags, const void *caps1, size_t caps1_size,
+		const void *caps2, size_t caps2_size, const struct a2dp_codec_audio_info *info, const struct spa_dict *global_settings)
 {
 	a2dp_lc3plus_hr_t conf1, conf2;
 	a2dp_lc3plus_hr_t *conf;
@@ -160,7 +160,7 @@ static int codec_caps_preference_cmp(const struct a2dp_codec *codec, const void
 
 	/* Order selected configurations by preference */
 	res1 = codec->select_config(codec, 0, caps1, caps1_size, info, NULL, (uint8_t *)&conf1);
-	res2 = codec->select_config(codec, 0, caps2, caps2_size, info , NULL, (uint8_t *)&conf2);
+	res2 = codec->select_config(codec, 0, caps2, caps2_size, info, NULL, (uint8_t *)&conf2);
 
 #define PREFER_EXPR(expr)			\
 		do {				\
@@ -190,7 +190,7 @@ static int codec_caps_preference_cmp(const struct a2dp_codec *codec, const void
 #undef PREFER_BOOL
 }
 
-static int codec_enum_config(const struct a2dp_codec *codec,
+static int codec_enum_config(const struct a2dp_codec *codec, uint32_t flags,
 		const void *caps, size_t caps_size, uint32_t id, uint32_t idx,
 		struct spa_pod_builder *b, struct spa_pod **param)
 {
diff --git a/spa/plugins/bluez5/a2dp-codec-ldac.c b/spa/plugins/bluez5/a2dp-codec-ldac.c
index ee6bc2ebf48300578696ff568c9f8c069a8f3cf9..3906104e87ab4b03d7bfb7cfffceb5550c10c174 100644
--- a/spa/plugins/bluez5/a2dp-codec-ldac.c
+++ b/spa/plugins/bluez5/a2dp-codec-ldac.c
@@ -150,7 +150,7 @@ static int codec_select_config(const struct a2dp_codec *codec, uint32_t flags,
         return sizeof(conf);
 }
 
-static int codec_enum_config(const struct a2dp_codec *codec,
+static int codec_enum_config(const struct a2dp_codec *codec, uint32_t flags,
 		const void *caps, size_t caps_size, uint32_t id, uint32_t idx,
 		struct spa_pod_builder *b, struct spa_pod **param)
 {
@@ -284,7 +284,7 @@ static int string_to_eqmid(const char * eqmid)
 		return LDACBT_EQMID_AUTO;
 }
 
-static void *codec_init_props(const struct a2dp_codec *codec, const struct spa_dict *settings)
+static void *codec_init_props(const struct a2dp_codec *codec, uint32_t flags, const struct spa_dict *settings)
 {
 	struct props *p = calloc(1, sizeof(struct props));
 	const char *str;
@@ -469,10 +469,10 @@ static void *codec_init(const struct a2dp_codec *codec, uint32_t flags,
 error_errno:
 	res = -errno;
 error:
-	if (this->ldac)
+	if (this && this->ldac)
 		ldacBT_free_handle(this->ldac);
 #ifdef ENABLE_LDAC_ABR
-	if (this->ldac_abr)
+	if (this && this->ldac_abr)
 		ldac_ABR_free_handle(this->ldac_abr);
 #endif
 	free(this);
diff --git a/spa/plugins/bluez5/a2dp-codec-opus.c b/spa/plugins/bluez5/a2dp-codec-opus.c
new file mode 100644
index 0000000000000000000000000000000000000000..7f1df12df331ed17d565a558ebdc3b9a2b3cf8e5
--- /dev/null
+++ b/spa/plugins/bluez5/a2dp-codec-opus.c
@@ -0,0 +1,1437 @@
+/* Spa A2DP Opus Codec
+ *
+ * Copyright © 2020 Wim Taymans
+ * Copyright © 2022 Pauli Virtanen
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <unistd.h>
+#include <string.h>
+#include <stddef.h>
+#include <errno.h>
+#include <arpa/inet.h>
+#if __BYTE_ORDER != __LITTLE_ENDIAN
+#include <byteswap.h>
+#endif
+
+#include <spa/debug/types.h>
+#include <spa/param/audio/type-info.h>
+#include <spa/param/audio/raw.h>
+#include <spa/utils/string.h>
+#include <spa/utils/dict.h>
+#include <spa/param/audio/format.h>
+#include <spa/param/audio/format-utils.h>
+
+#include <opus.h>
+#include <opus_multistream.h>
+
+#include "rtp.h"
+#include "a2dp-codecs.h"
+
+static struct spa_log *log;
+static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.codecs.opus");
+#undef SPA_LOG_TOPIC_DEFAULT
+#define SPA_LOG_TOPIC_DEFAULT &log_topic
+
+#define BUFSIZE_FROM_BITRATE(frame_dms,bitrate)	((bitrate)/8 * (frame_dms) / 10000 * 5/4)  /* estimate */
+
+/*
+ * Opus CVBR target bitrate. When connecting, it is set to the INITIAL
+ * value, and after that adjusted according to link quality between the MIN and
+ * MAX values. The bitrate adjusts up to either MAX or the value at
+ * which the socket buffer starts filling up, whichever is lower.
+ *
+ * With perfect connection quality, the target bitrate converges to the MAX
+ * value. Under realistic conditions, the upper limit may often be as low as
+ * 300-500kbit/s, so the INITIAL values are not higher than this.
+ *
+ * The MAX is here set to 2-2.5x and INITIAL to 1.5x the upper Opus recommended
+ * values [1], to be safer quality-wise for CVBR, and MIN to the lower
+ * recommended value.
+ *
+ * [1] https://wiki.xiph.org/Opus_Recommended_Settings
+ */
+#define BITRATE_INITIAL			192000
+#define BITRATE_MAX			320000
+#define BITRATE_MIN			96000
+
+#define BITRATE_INITIAL_51		384000
+#define BITRATE_MAX_51			600000
+#define BITRATE_MIN_51			128000
+
+#define BITRATE_INITIAL_71		450000
+#define BITRATE_MAX_71			900000
+#define BITRATE_MIN_71			256000
+
+#define BITRATE_DUPLEX_BIDI		160000
+
+#define OPUS_05_MAX_BYTES	(15 * 1024)
+
+struct props {
+	uint32_t channels;
+	uint32_t coupled_streams;
+	uint32_t location;
+	uint32_t max_bitrate;
+	uint8_t frame_duration;
+	int application;
+
+	uint32_t bidi_channels;
+	uint32_t bidi_coupled_streams;
+	uint32_t bidi_location;
+	uint32_t bidi_max_bitrate;
+	uint32_t bidi_frame_duration;
+	int bidi_application;
+};
+
+struct dec_data {
+	int fragment_size;
+	int fragment_count;
+	uint8_t fragment[OPUS_05_MAX_BYTES];
+};
+
+struct abr {
+	uint64_t now;
+	uint64_t last_update;
+
+	uint32_t buffer_level;
+	uint32_t packet_size;
+	uint32_t total_size;
+	bool bad;
+
+	uint64_t last_change;
+	uint64_t retry_interval;
+
+	bool prev_bad;
+};
+
+struct enc_data {
+	struct rtp_header *header;
+	struct rtp_payload *payload;
+
+	struct abr abr;
+
+	int samples;
+	int codesize;
+
+	int packet_size;
+	int fragment_size;
+	int fragment_count;
+	void *fragment;
+
+	int bitrate_min;
+	int bitrate_max;
+
+	int bitrate;
+	int next_bitrate;
+
+	int frame_dms;
+	int application;
+};
+
+struct impl {
+	OpusMSEncoder *enc;
+	OpusMSDecoder *dec;
+
+	int mtu;
+	int samplerate;
+	int application;
+
+	uint8_t channels;
+	uint8_t streams;
+	uint8_t coupled_streams;
+
+	bool is_bidi;
+
+	struct dec_data d;
+	struct enc_data e;
+};
+
+struct audio_location {
+	uint32_t mask;
+	enum spa_audio_channel position;
+};
+
+struct surround_encoder_mapping {
+	uint8_t channels;
+	uint8_t coupled_streams;
+	uint32_t location;
+	uint8_t mapping[8];		/**< permutation streams -> vorbis order */
+	uint8_t inv_mapping[8];		/**< permutation vorbis order -> streams */
+};
+
+/* Bluetooth SIG, Assigned Numbers, Generic Audio, Audio Location Definitions */
+#define BT_AUDIO_LOCATION_FL	0x00000001  /* Front Left */
+#define BT_AUDIO_LOCATION_FR	0x00000002  /* Front Right */
+#define BT_AUDIO_LOCATION_FC	0x00000004  /* Front Center */
+#define BT_AUDIO_LOCATION_LFE	0x00000008  /* Low Frequency Effects 1 */
+#define BT_AUDIO_LOCATION_RL	0x00000010  /* Back Left */
+#define BT_AUDIO_LOCATION_RR	0x00000020  /* Back Right */
+#define BT_AUDIO_LOCATION_FLC	0x00000040  /* Front Left of Center */
+#define BT_AUDIO_LOCATION_FRC	0x00000080  /* Front Right of Center */
+#define BT_AUDIO_LOCATION_RC	0x00000100  /* Back Center */
+#define BT_AUDIO_LOCATION_LFE2	0x00000200  /* Low Frequency Effects 2 */
+#define BT_AUDIO_LOCATION_SL	0x00000400  /* Side Left */
+#define BT_AUDIO_LOCATION_SR	0x00000800  /* Side Right */
+#define BT_AUDIO_LOCATION_TFL	0x00001000  /* Top Front Left */
+#define BT_AUDIO_LOCATION_TFR	0x00002000  /* Top Front Right */
+#define BT_AUDIO_LOCATION_TFC	0x00004000  /* Top Front Center */
+#define BT_AUDIO_LOCATION_TC	0x00008000  /* Top Center */
+#define BT_AUDIO_LOCATION_TRL	0x00010000  /* Top Back Left */
+#define BT_AUDIO_LOCATION_TRR	0x00020000  /* Top Back Right */
+#define BT_AUDIO_LOCATION_TSL	0x00040000  /* Top Side Left */
+#define BT_AUDIO_LOCATION_TSR	0x00080000  /* Top Side Right */
+#define BT_AUDIO_LOCATION_TRC	0x00100000  /* Top Back Center */
+#define BT_AUDIO_LOCATION_BC	0x00200000  /* Bottom Front Center */
+#define BT_AUDIO_LOCATION_BLC	0x00400000  /* Bottom Front Left */
+#define BT_AUDIO_LOCATION_BRC	0x00800000  /* Bottom Front Right */
+#define BT_AUDIO_LOCATION_FLW	0x01000000  /* Fron Left Wide */
+#define BT_AUDIO_LOCATION_FRW	0x02000000  /* Front Right Wide */
+#define BT_AUDIO_LOCATION_SSL	0x04000000  /* Left Surround */
+#define BT_AUDIO_LOCATION_SSR	0x08000000  /* Right Surround */
+
+#define BT_AUDIO_LOCATION_ANY	0x0fffffff
+
+static const struct audio_location audio_locations[] = {
+	{ BT_AUDIO_LOCATION_FL, SPA_AUDIO_CHANNEL_FL },
+	{ BT_AUDIO_LOCATION_FR, SPA_AUDIO_CHANNEL_FR },
+	{ BT_AUDIO_LOCATION_SL, SPA_AUDIO_CHANNEL_SL },
+	{ BT_AUDIO_LOCATION_SR, SPA_AUDIO_CHANNEL_SR },
+	{ BT_AUDIO_LOCATION_RL, SPA_AUDIO_CHANNEL_RL },
+	{ BT_AUDIO_LOCATION_RR, SPA_AUDIO_CHANNEL_RR },
+	{ BT_AUDIO_LOCATION_FLC, SPA_AUDIO_CHANNEL_FLC },
+	{ BT_AUDIO_LOCATION_FRC, SPA_AUDIO_CHANNEL_FRC },
+	{ BT_AUDIO_LOCATION_TFL, SPA_AUDIO_CHANNEL_TFL },
+	{ BT_AUDIO_LOCATION_TFR, SPA_AUDIO_CHANNEL_TFR },
+	{ BT_AUDIO_LOCATION_TSL, SPA_AUDIO_CHANNEL_TSL },
+	{ BT_AUDIO_LOCATION_TSR, SPA_AUDIO_CHANNEL_TSR },
+	{ BT_AUDIO_LOCATION_TRL, SPA_AUDIO_CHANNEL_TRL },
+	{ BT_AUDIO_LOCATION_TRR, SPA_AUDIO_CHANNEL_TRR },
+	{ BT_AUDIO_LOCATION_BLC, SPA_AUDIO_CHANNEL_BLC },
+	{ BT_AUDIO_LOCATION_BRC, SPA_AUDIO_CHANNEL_BRC },
+	{ BT_AUDIO_LOCATION_FLW, SPA_AUDIO_CHANNEL_FLW },
+	{ BT_AUDIO_LOCATION_FRW, SPA_AUDIO_CHANNEL_FRW },
+	{ BT_AUDIO_LOCATION_SSL, SPA_AUDIO_CHANNEL_SL },  /* ~ Side Left */
+	{ BT_AUDIO_LOCATION_SSR, SPA_AUDIO_CHANNEL_SR },  /* ~ Side Right */
+	{ BT_AUDIO_LOCATION_FC, SPA_AUDIO_CHANNEL_FC },
+	{ BT_AUDIO_LOCATION_RC, SPA_AUDIO_CHANNEL_RC },
+	{ BT_AUDIO_LOCATION_TFC, SPA_AUDIO_CHANNEL_TFC },
+	{ BT_AUDIO_LOCATION_TC, SPA_AUDIO_CHANNEL_TC },
+	{ BT_AUDIO_LOCATION_TRC, SPA_AUDIO_CHANNEL_TRC },
+	{ BT_AUDIO_LOCATION_BC, SPA_AUDIO_CHANNEL_BC },
+	{ BT_AUDIO_LOCATION_LFE, SPA_AUDIO_CHANNEL_LFE },
+	{ BT_AUDIO_LOCATION_LFE2, SPA_AUDIO_CHANNEL_LFE2 },
+};
+
+/* Opus surround encoder mapping tables for the supported channel configurations */
+static const struct surround_encoder_mapping surround_encoders[] = {
+	{ 1, 0, (0x0),
+	  { 0 }, { 0 } },
+	{ 2, 1, (BT_AUDIO_LOCATION_FL | BT_AUDIO_LOCATION_FR),
+	  { 0, 1 }, { 0, 1 } },
+	{ 3, 1, (BT_AUDIO_LOCATION_FL | BT_AUDIO_LOCATION_FR | BT_AUDIO_LOCATION_FC),
+	  { 0, 2, 1 }, { 0, 2, 1 } },
+	{ 4, 2, (BT_AUDIO_LOCATION_FL | BT_AUDIO_LOCATION_FR | BT_AUDIO_LOCATION_RL |
+				BT_AUDIO_LOCATION_RR),
+	  { 0, 1, 2, 3 }, { 0, 1, 2, 3 } },
+	{ 5, 2, (BT_AUDIO_LOCATION_FL | BT_AUDIO_LOCATION_FR | BT_AUDIO_LOCATION_RL |
+				BT_AUDIO_LOCATION_RR | BT_AUDIO_LOCATION_FC),
+	  { 0, 4, 1, 2, 3 }, { 0, 2, 3, 4, 1 } },
+	{ 6, 2, (BT_AUDIO_LOCATION_FL | BT_AUDIO_LOCATION_FR | BT_AUDIO_LOCATION_RL |
+				BT_AUDIO_LOCATION_RR | BT_AUDIO_LOCATION_FC |
+				BT_AUDIO_LOCATION_LFE),
+	  { 0, 4, 1, 2, 3, 5 }, { 0, 2, 3, 4, 1, 5 } },
+	{ 7, 3, (BT_AUDIO_LOCATION_FL | BT_AUDIO_LOCATION_FR | BT_AUDIO_LOCATION_SL |
+				BT_AUDIO_LOCATION_SR | BT_AUDIO_LOCATION_FC |
+				BT_AUDIO_LOCATION_RC | BT_AUDIO_LOCATION_LFE),
+	  { 0, 4, 1, 2, 3, 5, 6 }, { 0, 2, 3, 4, 1, 5, 6 } },
+	{ 8, 3, (BT_AUDIO_LOCATION_FL | BT_AUDIO_LOCATION_FR | BT_AUDIO_LOCATION_SL |
+				BT_AUDIO_LOCATION_SR | BT_AUDIO_LOCATION_RL |
+				BT_AUDIO_LOCATION_RR | BT_AUDIO_LOCATION_FC |
+				BT_AUDIO_LOCATION_LFE),
+	  { 0, 6, 1, 2, 3, 4, 5, 7 }, { 0, 2, 3, 4, 5, 6, 1, 7 } },
+};
+
+static uint32_t bt_channel_from_name(const char *name)
+{
+	size_t i;
+	enum spa_audio_channel position = SPA_AUDIO_CHANNEL_UNKNOWN;
+
+	for (i = 0; spa_type_audio_channel[i].name; i++) {
+		if (spa_streq(name, spa_debug_type_short_name(spa_type_audio_channel[i].name))) {
+			position = spa_type_audio_channel[i].type;
+			break;
+		}
+	}
+	for (i = 0; i < SPA_N_ELEMENTS(audio_locations); i++) {
+		if (position == audio_locations[i].position)
+			return audio_locations[i].mask;
+	}
+	return 0;
+}
+
+static uint32_t parse_locations(const char *str)
+{
+	char *s, *p, *save = NULL;
+	uint32_t location = 0;
+
+	if (!str)
+		return 0;
+
+	s = strdup(str);
+	if (s == NULL)
+		return 0;
+
+	for (p = s; (p = strtok_r(p, ", ", &save)) != NULL; p = NULL) {
+		if (*p == '\0')
+			continue;
+		location |= bt_channel_from_name(p);
+	}
+	free(s);
+
+	return location;
+}
+
+static void parse_settings(struct props *props, const struct spa_dict *settings)
+{
+	const char *str;
+	uint32_t v;
+
+	/* Pro Audio settings */
+	spa_zero(*props);
+	props->channels = 8;
+	props->coupled_streams = 0;
+	props->location = 0;
+	props->max_bitrate = BITRATE_MAX;
+	props->frame_duration = OPUS_05_FRAME_DURATION_100;
+	props->application = OPUS_APPLICATION_AUDIO;
+
+	props->bidi_channels = 1;
+	props->bidi_coupled_streams = 0;
+	props->bidi_location = 0;
+	props->bidi_max_bitrate = BITRATE_DUPLEX_BIDI;
+	props->bidi_frame_duration = OPUS_05_FRAME_DURATION_400;
+	props->bidi_application = OPUS_APPLICATION_AUDIO;
+
+	if (settings == NULL)
+		return;
+
+	if (spa_atou32(spa_dict_lookup(settings, "bluez5.a2dp.opus.pro.channels"), &v, 0))
+		props->channels = SPA_CLAMP(v, 1u, SPA_AUDIO_MAX_CHANNELS);
+	if (spa_atou32(spa_dict_lookup(settings, "bluez5.a2dp.opus.pro.max-bitrate"), &v, 0))
+		props->max_bitrate = SPA_MAX(v, (uint32_t)BITRATE_MIN);
+	if (spa_atou32(spa_dict_lookup(settings, "bluez5.a2dp.opus.pro.coupled-streams"), &v, 0))
+		props->coupled_streams = SPA_CLAMP(v, 0u, props->channels / 2);
+
+	if (spa_atou32(spa_dict_lookup(settings, "bluez5.a2dp.opus.pro.bidi.channels"), &v, 0))
+		props->bidi_channels = SPA_CLAMP(v, 0u, SPA_AUDIO_MAX_CHANNELS);
+	if (spa_atou32(spa_dict_lookup(settings, "bluez5.a2dp.opus.pro.bidi.max-bitrate"), &v, 0))
+		props->bidi_max_bitrate = SPA_MAX(v, (uint32_t)BITRATE_MIN);
+	if (spa_atou32(spa_dict_lookup(settings, "bluez5.a2dp.opus.pro.bidi.coupled-streams"), &v, 0))
+		props->bidi_coupled_streams = SPA_CLAMP(v, 0u, props->bidi_channels / 2);
+
+	str = spa_dict_lookup(settings, "bluez5.a2dp.opus.pro.locations");
+	props->location = parse_locations(str);
+
+	str = spa_dict_lookup(settings, "bluez5.a2dp.opus.pro.bidi.locations");
+	props->bidi_location = parse_locations(str);
+
+	str = spa_dict_lookup(settings, "bluez5.a2dp.opus.pro.frame-dms");
+	if (spa_streq(str, "25"))
+		props->frame_duration = OPUS_05_FRAME_DURATION_25;
+	else if (spa_streq(str, "50"))
+		props->frame_duration = OPUS_05_FRAME_DURATION_50;
+	else if (spa_streq(str, "100"))
+		props->frame_duration = OPUS_05_FRAME_DURATION_100;
+	else if (spa_streq(str, "200"))
+		props->frame_duration = OPUS_05_FRAME_DURATION_200;
+	else if (spa_streq(str, "400"))
+		props->frame_duration = OPUS_05_FRAME_DURATION_400;
+
+	str = spa_dict_lookup(settings, "bluez5.a2dp.opus.pro.bidi.frame-dms");
+	if (spa_streq(str, "25"))
+		props->bidi_frame_duration = OPUS_05_FRAME_DURATION_25;
+	else if (spa_streq(str, "50"))
+		props->bidi_frame_duration = OPUS_05_FRAME_DURATION_50;
+	else if (spa_streq(str, "100"))
+		props->bidi_frame_duration = OPUS_05_FRAME_DURATION_100;
+	else if (spa_streq(str, "200"))
+		props->bidi_frame_duration = OPUS_05_FRAME_DURATION_200;
+	else if (spa_streq(str, "400"))
+		props->bidi_frame_duration = OPUS_05_FRAME_DURATION_400;
+
+	str = spa_dict_lookup(settings, "bluez5.a2dp.opus.pro.application");
+	if (spa_streq(str, "audio"))
+		props->application = OPUS_APPLICATION_AUDIO;
+	else if (spa_streq(str, "voip"))
+		props->application = OPUS_APPLICATION_VOIP;
+	else if (spa_streq(str, "lowdelay"))
+		props->application = OPUS_APPLICATION_RESTRICTED_LOWDELAY;
+
+
+	str = spa_dict_lookup(settings, "bluez5.a2dp.opus.pro.bidi.application");
+	if (spa_streq(str, "audio"))
+		props->bidi_application = OPUS_APPLICATION_AUDIO;
+	else if (spa_streq(str, "voip"))
+		props->bidi_application = OPUS_APPLICATION_VOIP;
+	else if (spa_streq(str, "lowdelay"))
+		props->bidi_application = OPUS_APPLICATION_RESTRICTED_LOWDELAY;
+}
+
+static int set_channel_conf(const struct a2dp_codec *codec, a2dp_opus_05_t *caps, const struct props *props)
+{
+	/*
+	 * Predefined codec profiles
+	 */
+	if (caps->main.channels < 1)
+		return -EINVAL;
+
+	caps->main.coupled_streams = 0;
+	OPUS_05_SET_LOCATION(caps->main, 0);
+
+	caps->bidi.coupled_streams = 0;
+	OPUS_05_SET_LOCATION(caps->bidi, 0);
+
+	switch (codec->id) {
+	case SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05:
+		caps->main.channels = SPA_MIN(2, caps->main.channels);
+		if (caps->main.channels == 2) {
+			caps->main.coupled_streams = surround_encoders[1].coupled_streams;
+			OPUS_05_SET_LOCATION(caps->main, surround_encoders[1].location);
+		}
+		caps->bidi.channels = 0;
+		break;
+	case SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_51:
+		if (caps->main.channels < 6)
+			return -EINVAL;
+		caps->main.channels = surround_encoders[5].channels;
+		caps->main.coupled_streams = surround_encoders[5].coupled_streams;
+		OPUS_05_SET_LOCATION(caps->main, surround_encoders[5].location);
+		caps->bidi.channels = 0;
+		break;
+	case SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_71:
+		if (caps->main.channels < 8)
+			return -EINVAL;
+		caps->main.channels = surround_encoders[7].channels;
+		caps->main.coupled_streams = surround_encoders[7].coupled_streams;
+		OPUS_05_SET_LOCATION(caps->main, surround_encoders[7].location);
+		caps->bidi.channels = 0;
+		break;
+	case SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_DUPLEX:
+		if (caps->bidi.channels < 1)
+			return -EINVAL;
+		caps->main.channels = SPA_MIN(2, caps->main.channels);
+		if (caps->main.channels == 2) {
+			caps->main.coupled_streams = surround_encoders[1].coupled_streams;
+			OPUS_05_SET_LOCATION(caps->main, surround_encoders[1].location);
+		}
+		caps->bidi.channels = SPA_MIN(2, caps->bidi.channels);
+		if (caps->bidi.channels == 2) {
+			caps->bidi.coupled_streams = surround_encoders[1].coupled_streams;
+			OPUS_05_SET_LOCATION(caps->bidi, surround_encoders[1].location);
+		}
+		break;
+	case SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_PRO:
+		if (caps->main.channels < props->channels)
+			return -EINVAL;
+		if (props->bidi_channels == 0 && caps->bidi.channels != 0)
+			return -EINVAL;
+		if (caps->bidi.channels < props->bidi_channels)
+			return -EINVAL;
+		caps->main.channels = props->channels;
+		caps->main.coupled_streams = props->coupled_streams;
+		OPUS_05_SET_LOCATION(caps->main, props->location);
+		caps->bidi.channels = props->bidi_channels;
+		caps->bidi.coupled_streams = props->bidi_coupled_streams;
+		OPUS_05_SET_LOCATION(caps->bidi, props->bidi_location);
+		break;
+	default:
+		spa_assert(false);
+	};
+
+	return 0;
+}
+
+static void get_default_bitrates(const struct a2dp_codec *codec, bool bidi, int *min, int *max, int *init)
+{
+	int tmp;
+
+	if (min == NULL)
+		min = &tmp;
+	if (max == NULL)
+		max = &tmp;
+	if (init == NULL)
+		init = &tmp;
+
+	if (bidi) {
+		*min = SPA_MIN(BITRATE_MIN, BITRATE_DUPLEX_BIDI);
+		*max = BITRATE_DUPLEX_BIDI;
+		*init = BITRATE_DUPLEX_BIDI;
+		return;
+	}
+
+	switch (codec->id) {
+	case SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05:
+		*min = BITRATE_MIN;
+		*max = BITRATE_MAX;
+		*init = BITRATE_INITIAL;
+		break;
+	case SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_51:
+		*min = BITRATE_MIN_51;
+		*max = BITRATE_MAX_51;
+		*init = BITRATE_INITIAL_51;
+		break;
+	case SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_71:
+		*min = BITRATE_MIN_71;
+		*max = BITRATE_MAX_71;
+		*init = BITRATE_INITIAL_71;
+		break;
+	case SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_DUPLEX:
+		*min = BITRATE_MIN;
+		*max = BITRATE_MAX;
+		*init = BITRATE_INITIAL;
+		break;
+	case SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_PRO:
+	default:
+		spa_assert_not_reached();
+	};
+}
+
+static int get_mapping(const struct a2dp_codec *codec, const a2dp_opus_05_direction_t *conf,
+		bool use_surround_encoder, uint8_t *streams_ret, uint8_t *coupled_streams_ret,
+		const uint8_t **surround_mapping, uint32_t *positions)
+{
+	const uint8_t channels = conf->channels;
+	const uint32_t location = OPUS_05_GET_LOCATION(*conf);
+	const uint8_t coupled_streams = conf->coupled_streams;
+	const uint8_t *permutation = NULL;
+	size_t i, j;
+
+	if (channels > SPA_AUDIO_MAX_CHANNELS)
+		return -EINVAL;
+	if (2 * coupled_streams > channels)
+		return -EINVAL;
+
+	if (streams_ret)
+		*streams_ret = channels - coupled_streams;
+	if (coupled_streams_ret)
+		*coupled_streams_ret = coupled_streams;
+
+	if (channels == 0)
+		return 0;
+
+	if (use_surround_encoder) {
+		/* Opus surround encoder supports only some channel configurations, and
+		 * needs a specific input channel ordering */
+		for (i = 0; i < SPA_N_ELEMENTS(surround_encoders); ++i) {
+			const struct surround_encoder_mapping *m = &surround_encoders[i];
+
+			if (m->channels == channels &&
+					m->coupled_streams == coupled_streams &&
+					m->location == location)
+			{
+				spa_assert(channels <= SPA_N_ELEMENTS(m->inv_mapping));
+				permutation = m->inv_mapping;
+				if (surround_mapping)
+					*surround_mapping = m->mapping;
+				break;
+			}
+		}
+		if (permutation == NULL && surround_mapping)
+			*surround_mapping = NULL;
+	}
+
+	if (positions) {
+		for (i = 0, j = 0; i < SPA_N_ELEMENTS(audio_locations) && j < channels; ++i) {
+			const struct audio_location loc = audio_locations[i];
+
+			if (location & loc.mask) {
+				if (permutation)
+					positions[permutation[j++]] = loc.position;
+				else
+					positions[j++] = loc.position;
+			}
+		}
+		for (i = SPA_AUDIO_CHANNEL_START_Aux; j < channels; ++i, ++j)
+			positions[j] = i;
+	}
+
+	return 0;
+}
+
+static int codec_fill_caps(const struct a2dp_codec *codec, uint32_t flags,
+		uint8_t caps[A2DP_MAX_CAPS_SIZE])
+{
+	a2dp_opus_05_t a2dp_opus_05 = {
+		.info = codec->vendor,
+		.main = {
+			.channels = SPA_AUDIO_MAX_CHANNELS,
+			.frame_duration = (OPUS_05_FRAME_DURATION_25 |
+					OPUS_05_FRAME_DURATION_50 |
+					OPUS_05_FRAME_DURATION_100 |
+					OPUS_05_FRAME_DURATION_200 |
+					OPUS_05_FRAME_DURATION_400),
+			OPUS_05_INIT_LOCATION(BT_AUDIO_LOCATION_ANY)
+			OPUS_05_INIT_BITRATE(0)
+		},
+		.bidi = {
+			.channels = SPA_AUDIO_MAX_CHANNELS,
+			.frame_duration = (OPUS_05_FRAME_DURATION_25 |
+					OPUS_05_FRAME_DURATION_50 |
+					OPUS_05_FRAME_DURATION_100 |
+					OPUS_05_FRAME_DURATION_200 |
+					OPUS_05_FRAME_DURATION_400),
+			OPUS_05_INIT_LOCATION(BT_AUDIO_LOCATION_ANY)
+			OPUS_05_INIT_BITRATE(0)
+		}
+	};
+
+	/* Only duplex/pro codec has bidi, since bluez5-device has to know early
+	 * whether to show nodes or not. */
+	if (codec->id != SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_DUPLEX &&
+			codec->id != SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_PRO)
+		spa_zero(a2dp_opus_05.bidi);
+
+	memcpy(caps, &a2dp_opus_05, sizeof(a2dp_opus_05));
+	return sizeof(a2dp_opus_05);
+}
+
+static int codec_select_config(const struct a2dp_codec *codec, uint32_t flags,
+		const void *caps, size_t caps_size,
+		const struct a2dp_codec_audio_info *info,
+		const struct spa_dict *global_settings, uint8_t config[A2DP_MAX_CAPS_SIZE])
+{
+	struct props props;
+	a2dp_opus_05_t conf;
+	int res;
+	int max;
+
+	if (caps_size < sizeof(conf))
+		return -EINVAL;
+
+	memcpy(&conf, caps, sizeof(conf));
+
+	if (codec->vendor.vendor_id != conf.info.vendor_id ||
+	    codec->vendor.codec_id != conf.info.codec_id)
+		return -ENOTSUP;
+
+	parse_settings(&props, global_settings);
+
+	/* Channel Configuration & Audio Location */
+	if ((res = set_channel_conf(codec, &conf, &props)) < 0)
+		return res;
+
+	/* Limits */
+	if (codec->id == SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_PRO) {
+		max = props.max_bitrate;
+		if (OPUS_05_GET_BITRATE(conf.main) != 0)
+			OPUS_05_SET_BITRATE(conf.main, SPA_MIN(OPUS_05_GET_BITRATE(conf.main), max / 1024));
+		else
+			OPUS_05_SET_BITRATE(conf.main, max / 1024);
+
+		max = props.bidi_max_bitrate;
+		if (OPUS_05_GET_BITRATE(conf.bidi) != 0)
+			OPUS_05_SET_BITRATE(conf.bidi, SPA_MIN(OPUS_05_GET_BITRATE(conf.bidi), max / 1024));
+		else
+			OPUS_05_SET_BITRATE(conf.bidi, max / 1024);
+
+		if (conf.main.frame_duration & props.frame_duration)
+			conf.main.frame_duration = props.frame_duration;
+		else
+			return -EINVAL;
+
+		if (conf.bidi.channels == 0)
+			true;
+		else if (conf.bidi.frame_duration & props.bidi_frame_duration)
+			conf.bidi.frame_duration = props.bidi_frame_duration;
+		else
+			return -EINVAL;
+	} else {
+		if (conf.main.frame_duration & OPUS_05_FRAME_DURATION_100)
+			conf.main.frame_duration = OPUS_05_FRAME_DURATION_100;
+		else if (conf.main.frame_duration & OPUS_05_FRAME_DURATION_200)
+			conf.main.frame_duration = OPUS_05_FRAME_DURATION_200;
+		else if (conf.main.frame_duration & OPUS_05_FRAME_DURATION_400)
+			conf.main.frame_duration = OPUS_05_FRAME_DURATION_400;
+		else if (conf.main.frame_duration & OPUS_05_FRAME_DURATION_50)
+			conf.main.frame_duration = OPUS_05_FRAME_DURATION_50;
+		else if (conf.main.frame_duration & OPUS_05_FRAME_DURATION_25)
+			conf.main.frame_duration = OPUS_05_FRAME_DURATION_25;
+		else
+			return -EINVAL;
+
+		get_default_bitrates(codec, false, NULL, &max, NULL);
+
+		if (OPUS_05_GET_BITRATE(conf.main) != 0)
+			OPUS_05_SET_BITRATE(conf.main, SPA_MIN(OPUS_05_GET_BITRATE(conf.main), max / 1024));
+		else
+			OPUS_05_SET_BITRATE(conf.main, max / 1024);
+
+		/* longer bidi frames appear to work better */
+		if (conf.bidi.channels == 0)
+			true;
+		else if (conf.bidi.frame_duration & OPUS_05_FRAME_DURATION_200)
+			conf.bidi.frame_duration = OPUS_05_FRAME_DURATION_200;
+		else if (conf.bidi.frame_duration & OPUS_05_FRAME_DURATION_100)
+			conf.bidi.frame_duration = OPUS_05_FRAME_DURATION_100;
+		else if (conf.bidi.frame_duration & OPUS_05_FRAME_DURATION_400)
+			conf.bidi.frame_duration = OPUS_05_FRAME_DURATION_400;
+		else if (conf.bidi.frame_duration & OPUS_05_FRAME_DURATION_50)
+			conf.bidi.frame_duration = OPUS_05_FRAME_DURATION_50;
+		else if (conf.bidi.frame_duration & OPUS_05_FRAME_DURATION_25)
+			conf.bidi.frame_duration = OPUS_05_FRAME_DURATION_25;
+		else
+			return -EINVAL;
+
+		get_default_bitrates(codec, true, NULL, &max, NULL);
+
+		if (conf.bidi.channels == 0)
+			true;
+		else if (OPUS_05_GET_BITRATE(conf.bidi) != 0)
+			OPUS_05_SET_BITRATE(conf.bidi, SPA_MIN(OPUS_05_GET_BITRATE(conf.bidi), max / 1024));
+		else
+			OPUS_05_SET_BITRATE(conf.bidi, max / 1024);
+	}
+
+	memcpy(config, &conf, sizeof(conf));
+
+	return sizeof(conf);
+}
+
+static int codec_caps_preference_cmp(const struct a2dp_codec *codec, uint32_t flags, const void *caps1, size_t caps1_size,
+		const void *caps2, size_t caps2_size, const struct a2dp_codec_audio_info *info,
+		const struct spa_dict *global_settings)
+{
+	a2dp_opus_05_t conf1, conf2, cap1, cap2;
+	a2dp_opus_05_t *conf;
+	int res1, res2;
+	int a, b;
+
+	/* Order selected configurations by preference */
+	res1 = codec->select_config(codec, flags, caps1, caps1_size, info, global_settings, (uint8_t *)&conf1);
+	res2 = codec->select_config(codec, flags, caps2, caps2_size, info, global_settings, (uint8_t *)&conf2);
+
+#define PREFER_EXPR(expr)			\
+		do {				\
+			conf = &conf1; 		\
+			a = (expr);		\
+			conf = &conf2;		\
+			b = (expr);		\
+			if (a != b)		\
+				return b - a;	\
+		} while (0)
+
+#define PREFER_BOOL(expr)	PREFER_EXPR((expr) ? 1 : 0)
+
+	/* Prefer valid */
+	a = (res1 > 0 && (size_t)res1 == sizeof(a2dp_opus_05_t)) ? 1 : 0;
+	b = (res2 > 0 && (size_t)res2 == sizeof(a2dp_opus_05_t)) ? 1 : 0;
+	if (!a || !b)
+		return b - a;
+
+	memcpy(&cap1, caps1, sizeof(cap1));
+	memcpy(&cap2, caps2, sizeof(cap2));
+
+	if (conf1.bidi.channels == 0 && conf2.bidi.channels == 0) {
+		/* If no bidi, prefer the SEP that has none */
+		a = (cap1.bidi.channels == 0);
+		b = (cap2.bidi.channels == 0);
+		if (a != b)
+			return b - a;
+	}
+
+	PREFER_EXPR(conf->main.channels);
+	PREFER_EXPR(conf->bidi.channels);
+	PREFER_EXPR(OPUS_05_GET_BITRATE(conf->main));
+	PREFER_EXPR(OPUS_05_GET_BITRATE(conf->bidi));
+
+	return 0;
+
+#undef PREFER_EXPR
+#undef PREFER_BOOL
+}
+
+static bool is_duplex_codec(const struct a2dp_codec *codec)
+{
+	return codec->id == 0;
+}
+
+static bool use_surround_encoder(const struct a2dp_codec *codec, bool is_sink)
+{
+	if (codec->id == SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_PRO)
+		return false;
+
+	if (is_duplex_codec(codec))
+		return is_sink;
+	else
+		return !is_sink;
+}
+
+static int codec_enum_config(const struct a2dp_codec *codec, uint32_t flags,
+		const void *caps, size_t caps_size, uint32_t id, uint32_t idx,
+		struct spa_pod_builder *b, struct spa_pod **param)
+{
+	const bool surround_encoder = use_surround_encoder(codec, flags & A2DP_CODEC_FLAG_SINK);
+	a2dp_opus_05_t conf;
+	a2dp_opus_05_direction_t *dir;
+	struct spa_pod_frame f[1];
+	uint32_t position[SPA_AUDIO_MAX_CHANNELS];
+
+	if (caps_size < sizeof(conf))
+		return -EINVAL;
+
+	memcpy(&conf, caps, sizeof(conf));
+
+	if (idx > 0)
+		return 0;
+
+	dir = !is_duplex_codec(codec) ? &conf.main : &conf.bidi;
+
+	if (get_mapping(codec, dir, surround_encoder, NULL, NULL, NULL, position) < 0)
+		return -EINVAL;
+
+	spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_Format, id);
+	spa_pod_builder_add(b,
+			SPA_FORMAT_mediaType,      SPA_POD_Id(SPA_MEDIA_TYPE_audio),
+			SPA_FORMAT_mediaSubtype,   SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw),
+			SPA_FORMAT_AUDIO_format,   SPA_POD_Id(SPA_AUDIO_FORMAT_F32),
+			SPA_FORMAT_AUDIO_rate,     SPA_POD_CHOICE_ENUM_Int(6,
+					48000, 48000, 24000, 16000, 12000, 8000),
+			SPA_FORMAT_AUDIO_channels, SPA_POD_Int(dir->channels),
+			SPA_FORMAT_AUDIO_position, SPA_POD_Array(sizeof(uint32_t),
+					SPA_TYPE_Id, dir->channels, position),
+			0);
+
+	*param = spa_pod_builder_pop(b, &f[0]);
+	return *param == NULL ? -EIO : 1;
+}
+
+static int codec_validate_config(const struct a2dp_codec *codec, uint32_t flags,
+			const void *caps, size_t caps_size,
+			struct spa_audio_info *info)
+{
+	const bool surround_encoder = use_surround_encoder(codec, flags & A2DP_CODEC_FLAG_SINK);
+	const a2dp_opus_05_direction_t *dir1, *dir2;
+	const a2dp_opus_05_t *conf;
+
+	if (caps == NULL || caps_size < sizeof(*conf))
+		return -EINVAL;
+
+	conf = caps;
+
+	spa_zero(*info);
+	info->media_type = SPA_MEDIA_TYPE_audio;
+	info->media_subtype = SPA_MEDIA_SUBTYPE_raw;
+	info->info.raw.format = SPA_AUDIO_FORMAT_F32;
+	info->info.raw.rate = 0;  /* not specified by config */
+
+	if (2 * conf->main.coupled_streams > conf->main.channels)
+		return -EINVAL;
+	if (2 * conf->bidi.coupled_streams > conf->bidi.channels)
+		return -EINVAL;
+
+	if (!is_duplex_codec(codec)) {
+		dir1 = &conf->main;
+		dir2 = &conf->bidi;
+	} else {
+		dir1 = &conf->bidi;
+		dir2 = &conf->main;
+	}
+
+	info->info.raw.channels = dir1->channels;
+	if (get_mapping(codec, dir1, surround_encoder, NULL, NULL, NULL, info->info.raw.position) < 0)
+		return -EINVAL;
+	if (get_mapping(codec, dir2, surround_encoder, NULL, NULL, NULL, NULL) < 0)
+		return -EINVAL;
+
+	return 0;
+}
+
+static size_t ceildiv(size_t v, size_t divisor)
+{
+	if (v % divisor == 0)
+		return v / divisor;
+	else
+		return v / divisor + 1;
+}
+
+static bool check_bitrate_vs_frame_dms(struct impl *this, size_t bitrate)
+{
+	size_t header_size = sizeof(struct rtp_header) + sizeof(struct rtp_payload);
+	size_t max_fragments = 0xf;
+	size_t payload_size = BUFSIZE_FROM_BITRATE(bitrate, this->e.frame_dms);
+	return (size_t)this->mtu >= header_size + ceildiv(payload_size, max_fragments);
+}
+
+static int parse_frame_dms(int bitfield)
+{
+	switch (bitfield) {
+	case OPUS_05_FRAME_DURATION_25:
+		return 25;
+	case OPUS_05_FRAME_DURATION_50:
+		return 50;
+	case OPUS_05_FRAME_DURATION_100:
+		return 100;
+	case OPUS_05_FRAME_DURATION_200:
+		return 200;
+	case OPUS_05_FRAME_DURATION_400:
+		return 400;
+	default:
+		return -EINVAL;
+	}
+}
+
+static void *codec_init_props(const struct a2dp_codec *codec, uint32_t flags, const struct spa_dict *settings)
+{
+	struct props *p;
+
+	if (codec->id != SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_PRO)
+		return NULL;
+
+	p = calloc(1, sizeof(struct props));
+	if (p == NULL)
+		return NULL;
+
+	parse_settings(p, settings);
+
+	return p;
+}
+
+static void codec_clear_props(void *props)
+{
+	free(props);
+}
+
+static void *codec_init(const struct a2dp_codec *codec, uint32_t flags,
+		void *config, size_t config_len, const struct spa_audio_info *info,
+		void *props, size_t mtu)
+{
+	const bool surround_encoder = use_surround_encoder(codec, flags & A2DP_CODEC_FLAG_SINK);
+	a2dp_opus_05_t *conf = config;
+	a2dp_opus_05_direction_t *dir;
+	struct impl *this = NULL;
+	struct spa_audio_info config_info;
+	const uint8_t *enc_mapping = NULL;
+	unsigned char mapping[256];
+	size_t i;
+	int res;
+
+	if (info->media_type != SPA_MEDIA_TYPE_audio ||
+	    info->media_subtype != SPA_MEDIA_SUBTYPE_raw ||
+	    info->info.raw.format != SPA_AUDIO_FORMAT_F32) {
+		res = -EINVAL;
+		goto error;
+	}
+
+	if ((this = calloc(1, sizeof(struct impl))) == NULL)
+		goto error_errno;
+
+	this->is_bidi = is_duplex_codec(codec);
+	dir = !this->is_bidi ? &conf->main : &conf->bidi;
+
+	if ((res = codec_validate_config(codec, flags, config, config_len, &config_info)) < 0)
+		goto error;
+	if ((res = get_mapping(codec, dir, surround_encoder, &this->streams, &this->coupled_streams,
+							&enc_mapping, NULL)) < 0)
+		goto error;
+	if (config_info.info.raw.channels != info->info.raw.channels) {
+		res = -EINVAL;
+		goto error;
+	}
+
+	this->mtu = mtu;
+	this->samplerate = info->info.raw.rate;
+	this->channels = config_info.info.raw.channels;
+	this->application = OPUS_APPLICATION_AUDIO;
+
+	if (codec->id == SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_PRO && props) {
+		struct props *p = props;
+		this->application = !this->is_bidi ? p->application :
+			p->bidi_application;
+	}
+
+	/*
+	 * Setup encoder
+	 */
+	if (enc_mapping) {
+		int streams, coupled_streams;
+		bool incompatible_opus_surround_encoder = false;
+
+		this->enc = opus_multistream_surround_encoder_create(
+				this->samplerate, this->channels, 1, &streams, &coupled_streams,
+				mapping, this->application, &res);
+
+		if (this->enc) {
+			/* Check surround encoder channel mapping is what we want */
+			if (streams != this->streams || coupled_streams != this->coupled_streams)
+				incompatible_opus_surround_encoder = true;
+			for (i = 0; i < this->channels; ++i)
+				if (enc_mapping[i] != mapping[i])
+					incompatible_opus_surround_encoder = true;
+		}
+
+		/* Assert: this should never happen */
+		spa_assert(!incompatible_opus_surround_encoder);
+		if (incompatible_opus_surround_encoder) {
+			res = -EINVAL;
+			goto error;
+		}
+	} else {
+		for (i = 0; i < this->channels; ++i)
+			mapping[i] = i;
+		this->enc = opus_multistream_encoder_create(
+				this->samplerate, this->channels, this->streams, this->coupled_streams,
+				mapping, this->application, &res);
+	}
+	if (this->enc == NULL) {
+		res = -EINVAL;
+		goto error;
+	}
+
+	if ((this->e.frame_dms = parse_frame_dms(dir->frame_duration)) < 0) {
+		res = -EINVAL;
+		goto error;
+	}
+
+	if (codec->id != SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_PRO) {
+		get_default_bitrates(codec, this->is_bidi, &this->e.bitrate_min,
+				&this->e.bitrate_max, &this->e.bitrate);
+		this->e.bitrate_max = SPA_MIN(this->e.bitrate_max,
+				OPUS_05_GET_BITRATE(*dir) * 1024);
+	} else {
+		this->e.bitrate_max = OPUS_05_GET_BITRATE(*dir) * 1024;
+		this->e.bitrate_min = BITRATE_MIN;
+		this->e.bitrate = BITRATE_INITIAL;
+	}
+
+	this->e.bitrate_min = SPA_MIN(this->e.bitrate_min, this->e.bitrate_max);
+	this->e.bitrate = SPA_CLAMP(this->e.bitrate, this->e.bitrate_min, this->e.bitrate_max);
+
+	this->e.next_bitrate = this->e.bitrate;
+	opus_multistream_encoder_ctl(this->enc, OPUS_SET_BITRATE(this->e.bitrate));
+
+	this->e.samples = this->e.frame_dms * this->samplerate / 10000;
+	this->e.codesize = this->e.samples * (int)this->channels * sizeof(float);
+
+
+	/*
+	 * Setup decoder
+	 */
+	for (i = 0; i < this->channels; ++i)
+		mapping[i] = i;
+	this->dec = opus_multistream_decoder_create(
+			this->samplerate, this->channels,
+			this->streams, this->coupled_streams,
+			mapping, &res);
+	if (this->dec == NULL) {
+		res = -EINVAL;
+		goto error;
+	}
+
+	return this;
+
+error_errno:
+	res = -errno;
+	goto error;
+
+error:
+	if (this && this->enc)
+		opus_multistream_encoder_destroy(this->enc);
+	if (this && this->dec)
+		opus_multistream_decoder_destroy(this->dec);
+	free(this);
+	errno = -res;
+	return NULL;
+}
+
+static void codec_deinit(void *data)
+{
+	struct impl *this = data;
+	opus_multistream_encoder_destroy(this->enc);
+	opus_multistream_decoder_destroy(this->dec);
+	free(this);
+}
+
+static int codec_get_block_size(void *data)
+{
+	struct impl *this = data;
+	return this->e.codesize;
+}
+
+static int codec_update_bitrate(struct impl *this)
+{
+	this->e.next_bitrate = SPA_CLAMP(this->e.next_bitrate,
+			this->e.bitrate_min, this->e.bitrate_max);
+
+	if (!check_bitrate_vs_frame_dms(this, this->e.next_bitrate)) {
+		this->e.next_bitrate = this->e.bitrate;
+		return 0;
+	}
+
+	this->e.bitrate = this->e.next_bitrate;
+	opus_multistream_encoder_ctl(this->enc, OPUS_SET_BITRATE(this->e.bitrate));
+	return 0;
+}
+
+static int codec_start_encode (void *data,
+		void *dst, size_t dst_size, uint16_t seqnum, uint32_t timestamp)
+{
+	struct impl *this = data;
+	size_t header_size = sizeof(struct rtp_header) + sizeof(struct rtp_payload);
+
+	if (dst_size <= header_size)
+		return -EINVAL;
+
+	codec_update_bitrate(this);
+
+	this->e.header = (struct rtp_header *)dst;
+	this->e.payload = SPA_PTROFF(dst, sizeof(struct rtp_header), struct rtp_payload);
+	memset(dst, 0, header_size);
+
+	this->e.payload->frame_count = 0;
+	this->e.header->v = 2;
+	this->e.header->pt = 96;
+	this->e.header->sequence_number = htons(seqnum);
+	this->e.header->timestamp = htonl(timestamp);
+	this->e.header->ssrc = htonl(1);
+
+	this->e.packet_size = header_size;
+	return this->e.packet_size;
+}
+
+static int codec_encode(void *data,
+		const void *src, size_t src_size,
+		void *dst, size_t dst_size,
+		size_t *dst_out, int *need_flush)
+{
+	struct impl *this = data;
+	const int header_size = sizeof(struct rtp_header) + sizeof(struct rtp_payload);
+	int size;
+	int res;
+
+	if (src == NULL) {
+		/* Produce fragment packets.
+		 *
+		 * We assume the caller gives the same buffer here as in the previous
+		 * calls to encode(), without changes in the buffer content.
+		 */
+		if (this->e.fragment == NULL ||
+				this->e.fragment_count <= 1 ||
+				this->e.fragment < dst ||
+				SPA_PTROFF(this->e.fragment, this->e.fragment_size, void) > SPA_PTROFF(dst, dst_size, void)) {
+			this->e.fragment = NULL;
+			return -EINVAL;
+		}
+
+		size = SPA_MIN(this->mtu - header_size, this->e.fragment_size);
+		memmove(dst, this->e.fragment, size);
+		*dst_out = size;
+
+		this->e.payload->is_fragmented = 1;
+		this->e.payload->frame_count = --this->e.fragment_count;
+		this->e.payload->is_last_fragment = (this->e.fragment_count == 1);
+
+		if (this->e.fragment_size > size && this->e.fragment_count > 1) {
+			this->e.fragment = SPA_PTROFF(this->e.fragment, size, void);
+			this->e.fragment_size -= size;
+			*need_flush = NEED_FLUSH_FRAGMENT;
+		} else {
+			this->e.fragment = NULL;
+			*need_flush = NEED_FLUSH_ALL;
+		}
+		return 0;
+	}
+
+	if (src_size < (size_t)this->e.codesize) {
+		*dst_out = 0;
+		return 0;
+	}
+
+	res = opus_multistream_encode_float(
+		this->enc, src, this->e.samples, dst, dst_size);
+	if (res < 0)
+		return -EINVAL;
+	*dst_out = res;
+
+	this->e.packet_size += res;
+	this->e.payload->frame_count++;
+
+	if (this->e.packet_size > this->mtu) {
+		/* Fragment packet */
+		this->e.fragment_count = ceildiv(this->e.packet_size - header_size,
+				this->mtu - header_size);
+
+		this->e.payload->is_fragmented = 1;
+		this->e.payload->is_first_fragment = 1;
+		this->e.payload->frame_count = this->e.fragment_count;
+
+		this->e.fragment_size = this->e.packet_size - this->mtu;
+		this->e.fragment = SPA_PTROFF(dst, *dst_out - this->e.fragment_size, void);
+		*need_flush = NEED_FLUSH_FRAGMENT;
+
+		/*
+		 * We keep the rest of the encoded frame in the same buffer, and rely
+		 * that the caller won't overwrite it before the next call to encode()
+		 */
+		*dst_out = SPA_PTRDIFF(this->e.fragment, dst);
+	} else {
+		*need_flush = NEED_FLUSH_ALL;
+	}
+
+	return this->e.codesize;
+}
+
+static SPA_UNUSED int codec_start_decode (void *data,
+		const void *src, size_t src_size, uint16_t *seqnum, uint32_t *timestamp)
+{
+	struct impl *this = data;
+	const struct rtp_header *header = src;
+	const struct rtp_payload *payload = SPA_PTROFF(src, sizeof(struct rtp_header), void);
+	size_t header_size = sizeof(struct rtp_header) + sizeof(struct rtp_payload);
+
+	spa_return_val_if_fail (src_size > header_size, -EINVAL);
+
+	if (seqnum)
+		*seqnum = ntohs(header->sequence_number);
+	if (timestamp)
+		*timestamp = ntohl(header->timestamp);
+
+	if (payload->is_fragmented) {
+		if (payload->is_first_fragment) {
+			this->d.fragment_size = 0;
+		} else if (payload->frame_count + 1 != this->d.fragment_count ||
+				(payload->frame_count == 1 && !payload->is_last_fragment)){
+			/* Fragments not in right order: drop packet */
+			return -EINVAL;
+		}
+		this->d.fragment_count = payload->frame_count;
+	} else {
+		if (payload->frame_count != 1)
+			return -EINVAL;
+		this->d.fragment_count = 0;
+	}
+
+	return header_size;
+}
+
+static SPA_UNUSED int codec_decode(void *data,
+		const void *src, size_t src_size,
+		void *dst, size_t dst_size,
+		size_t *dst_out)
+{
+	struct impl *this = data;
+	int consumed = src_size;
+	int res;
+	int dst_samples;
+
+	if (this->d.fragment_count > 0) {
+		/* Fragmented frame */
+		size_t avail;
+		avail = SPA_MIN(sizeof(this->d.fragment) - this->d.fragment_size, src_size);
+		memcpy(SPA_PTROFF(this->d.fragment, this->d.fragment_size, void), src, avail);
+
+		this->d.fragment_size += avail;
+
+		if (this->d.fragment_count > 1) {
+			/* More fragments to come */
+			*dst_out = 0;
+			return consumed;
+		}
+
+		src = this->d.fragment;
+		src_size = this->d.fragment_size;
+
+		this->d.fragment_count = 0;
+		this->d.fragment_size = 0;
+	}
+
+	dst_samples = dst_size / (sizeof(float) * this->channels);
+	res = opus_multistream_decode_float(this->dec, src, src_size, dst, dst_samples, 0);
+	if (res < 0)
+		return -EINVAL;
+	*dst_out = (size_t)res * this->channels * sizeof(float);
+
+	return consumed;
+}
+
+static int codec_abr_process(void *data, size_t unsent)
+{
+	const uint64_t interval = SPA_NSEC_PER_SEC;
+	struct impl *this = data;
+	struct abr *abr = &this->e.abr;
+	bool level_bad, level_good;
+	uint32_t actual_bitrate;
+
+	abr->total_size += this->e.packet_size;
+
+	if (this->e.payload->is_fragmented && !this->e.payload->is_first_fragment)
+		return 0;
+
+	abr->now += this->e.frame_dms * SPA_NSEC_PER_MSEC / 10;
+
+	abr->buffer_level = SPA_MAX(abr->buffer_level, unsent);
+	abr->packet_size = SPA_MAX(abr->packet_size, (uint32_t)this->e.packet_size);
+	abr->packet_size = SPA_MAX(abr->packet_size, 128u);
+
+	level_bad = abr->buffer_level > 2 * (uint32_t)this->mtu || abr->bad;
+	level_good = abr->buffer_level == 0;
+
+	if (!(abr->last_update + interval <= abr->now ||
+			(level_bad && abr->last_change + interval <= abr->now)))
+		return 0;
+
+	actual_bitrate = (uint64_t)abr->total_size*8*SPA_NSEC_PER_SEC
+		/ SPA_MAX(1u, abr->now - abr->last_update);
+
+	spa_log_debug(log, "opus ABR bitrate:%d actual:%d level:%d (%s) bad:%d retry:%ds size:%d",
+			(int)this->e.bitrate,
+			(int)actual_bitrate,
+			(int)abr->buffer_level,
+			level_bad ? "bad" : (level_good ? "good" : "-"),
+			(int)abr->bad,
+			(int)(abr->retry_interval / SPA_NSEC_PER_SEC),
+			(int)abr->packet_size);
+
+	if (level_bad) {
+		this->e.next_bitrate = this->e.bitrate * 11 / 12;
+		abr->last_change = abr->now;
+		abr->retry_interval = SPA_MIN(abr->retry_interval + 10*interval,
+				30 * interval);
+	} else if (!level_good) {
+		abr->last_change = abr->now;
+	} else if (abr->now < abr->last_change + abr->retry_interval) {
+		/* noop */
+	} else if (actual_bitrate*3/2 < (uint32_t)this->e.bitrate) {
+		/* actual bitrate is small compared to target; probably silence */
+	} else {
+		this->e.next_bitrate = this->e.bitrate
+			+ SPA_MAX(1, this->e.bitrate_max / 40);
+		abr->last_change = abr->now;
+		abr->retry_interval = SPA_MAX(abr->retry_interval, (5+4)*interval)
+			- 4*interval;
+	}
+
+	abr->last_update = abr->now;
+	abr->buffer_level = 0;
+	abr->bad = false;
+	abr->packet_size = 0;
+	abr->total_size = 0;
+
+	return 0;
+}
+
+static int codec_reduce_bitpool(void *data)
+{
+	struct impl *this = data;
+	struct abr *abr = &this->e.abr;
+	abr->bad = true;
+	return 0;
+}
+
+static int codec_increase_bitpool(void *data)
+{
+	return 0;
+}
+
+static void codec_set_log(struct spa_log *global_log)
+{
+	log = global_log;
+	spa_log_topic_init(log, &log_topic);
+}
+
+#define OPUS_05_COMMON_DEFS					\
+	.codec_id = A2DP_CODEC_VENDOR,				\
+	.vendor = { .vendor_id = OPUS_05_VENDOR_ID,		\
+			.codec_id = OPUS_05_CODEC_ID },		\
+	.fill_caps = codec_fill_caps,				\
+	.select_config = codec_select_config,			\
+	.enum_config = codec_enum_config,			\
+	.validate_config = codec_validate_config,		\
+	.caps_preference_cmp = codec_caps_preference_cmp,	\
+	.init = codec_init,					\
+	.deinit = codec_deinit,					\
+	.get_block_size = codec_get_block_size,			\
+	.abr_process = codec_abr_process,			\
+	.start_encode = codec_start_encode,			\
+	.encode = codec_encode,					\
+	.reduce_bitpool = codec_reduce_bitpool,			\
+	.increase_bitpool = codec_increase_bitpool,		\
+	.set_log = codec_set_log
+
+#define OPUS_05_COMMON_FULL_DEFS				\
+	OPUS_05_COMMON_DEFS,					\
+	.start_decode = codec_start_decode,			\
+	.decode = codec_decode
+
+const struct a2dp_codec a2dp_codec_opus_05 = {
+	OPUS_05_COMMON_FULL_DEFS,
+	.id = SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05,
+	.name = "opus_05",
+	.description = "Opus",
+};
+
+const struct a2dp_codec a2dp_codec_opus_05_51 = {
+	OPUS_05_COMMON_DEFS,
+	.id = SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_51,
+	.name = "opus_05_51",
+	.description = "Opus 5.1 Surround",
+};
+
+const struct a2dp_codec a2dp_codec_opus_05_71 = {
+	OPUS_05_COMMON_DEFS,
+	.id = SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_71,
+	.name = "opus_05_71",
+	.description = "Opus 7.1 Surround",
+};
+
+/* Bidi return channel codec: doesn't have endpoints */
+const struct a2dp_codec a2dp_codec_opus_05_return = {
+	OPUS_05_COMMON_FULL_DEFS,
+	.id = 0,
+	.name = "opus_05_duplex_bidi",
+	.description = "Opus Duplex Bidi channel",
+};
+
+const struct a2dp_codec a2dp_codec_opus_05_duplex = {
+	OPUS_05_COMMON_FULL_DEFS,
+	.id = SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_DUPLEX,
+	.name = "opus_05_duplex",
+	.description = "Opus Duplex",
+	.duplex_codec = &a2dp_codec_opus_05_return,
+};
+
+const struct a2dp_codec a2dp_codec_opus_05_pro = {
+	OPUS_05_COMMON_DEFS,
+	.id = SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_PRO,
+	.name = "opus_05_pro",
+	.description = "Opus Pro Audio",
+	.init_props = codec_init_props,
+	.clear_props = codec_clear_props,
+	.duplex_codec = &a2dp_codec_opus_05_return,
+};
+
+A2DP_CODEC_EXPORT_DEF(
+	"opus",
+	&a2dp_codec_opus_05,
+	&a2dp_codec_opus_05_51,
+	&a2dp_codec_opus_05_71,
+	&a2dp_codec_opus_05_duplex,
+	&a2dp_codec_opus_05_pro
+);
diff --git a/spa/plugins/bluez5/a2dp-codec-sbc.c b/spa/plugins/bluez5/a2dp-codec-sbc.c
index 6a43c666a6dc4010477f1c32d5ba57ef75776314..35d55355a1c028bc765909932d3d15b794acb5f6 100644
--- a/spa/plugins/bluez5/a2dp-codec-sbc.c
+++ b/spa/plugins/bluez5/a2dp-codec-sbc.c
@@ -229,8 +229,8 @@ static int codec_select_config(const struct a2dp_codec *codec, uint32_t flags,
 	return sizeof(conf);
 }
 
-static int codec_caps_preference_cmp(const struct a2dp_codec *codec, const void *caps1, size_t caps1_size,
-		const void *caps2, size_t caps2_size, const struct a2dp_codec_audio_info *info)
+static int codec_caps_preference_cmp(const struct a2dp_codec *codec, uint32_t flags, const void *caps1, size_t caps1_size,
+		const void *caps2, size_t caps2_size, const struct a2dp_codec_audio_info *info, const struct spa_dict *global_settings)
 {
 	a2dp_sbc_t conf1, conf2;
 	a2dp_sbc_t *conf;
@@ -356,7 +356,7 @@ static int codec_set_bitpool(struct impl *this, int bitpool)
 	return this->sbc.bitpool;
 }
 
-static int codec_enum_config(const struct a2dp_codec *codec,
+static int codec_enum_config(const struct a2dp_codec *codec, uint32_t flags,
 		const void *caps, size_t caps_size, uint32_t id, uint32_t idx,
 		struct spa_pod_builder *b, struct spa_pod **param)
 {
diff --git a/spa/plugins/bluez5/a2dp-codecs.c b/spa/plugins/bluez5/a2dp-codecs.c
index c78b2f261f3e4f8aed5c92f0fc7278dfee2d0b5a..42c7a193c1a759b6ecd5892928157a5d4b968e68 100644
--- a/spa/plugins/bluez5/a2dp-codecs.c
+++ b/spa/plugins/bluez5/a2dp-codecs.c
@@ -62,7 +62,8 @@ int a2dp_codec_select_config(const struct a2dp_codec_config configs[], size_t n,
 
 bool a2dp_codec_check_caps(const struct a2dp_codec *codec, unsigned int codec_id,
 			   const void *caps, size_t caps_size,
-			   const struct a2dp_codec_audio_info *info)
+			   const struct a2dp_codec_audio_info *info,
+			   const struct spa_dict *global_settings)
 {
 	uint8_t config[A2DP_MAX_CAPS_SIZE];
 	int res;
@@ -73,7 +74,7 @@ bool a2dp_codec_check_caps(const struct a2dp_codec *codec, unsigned int codec_id
 	if (caps == NULL)
 		return false;
 
-	res = codec->select_config(codec, 0, caps, caps_size, info, NULL, config);
+	res = codec->select_config(codec, 0, caps, caps_size, info, global_settings, config);
 	if (res < 0)
 		return false;
 
diff --git a/spa/plugins/bluez5/a2dp-codecs.h b/spa/plugins/bluez5/a2dp-codecs.h
index bd041b29efda980c721b5c9959c115e7313d22e9..7fb5cd510611b6c48591b97d822d58649955633b 100644
--- a/spa/plugins/bluez5/a2dp-codecs.h
+++ b/spa/plugins/bluez5/a2dp-codecs.h
@@ -33,6 +33,7 @@
 #include <spa/support/plugin.h>
 #include <spa/pod/pod.h>
 #include <spa/pod/builder.h>
+#include <spa/support/log.h>
 
 #include "a2dp-codec-caps.h"
 
@@ -43,7 +44,7 @@
 
 #define SPA_TYPE_INTERFACE_Bluez5CodecA2DP	SPA_TYPE_INFO_INTERFACE_BASE "Bluez5:Codec:A2DP:Private"
 
-#define SPA_VERSION_BLUEZ5_CODEC_A2DP		1
+#define SPA_VERSION_BLUEZ5_CODEC_A2DP		5
 
 struct spa_bluez5_codec_a2dp {
 	struct spa_interface iface;
@@ -62,6 +63,7 @@ extern const struct a2dp_codec * const * const codec_plugin_a2dp_codecs;
 extern const char *codec_plugin_factory_name;
 #endif
 
+#define A2DP_CODEC_FLAG_SINK		(1 << 0)
 
 #define A2DP_CODEC_DEFAULT_RATE		48000
 #define A2DP_CODEC_DEFAULT_CHANNELS	2
@@ -96,8 +98,8 @@ struct a2dp_codec {
 	int (*select_config) (const struct a2dp_codec *codec, uint32_t flags,
 			const void *caps, size_t caps_size,
 			const struct a2dp_codec_audio_info *info,
-			const struct spa_dict *settings, uint8_t config[A2DP_MAX_CAPS_SIZE]);
-	int (*enum_config) (const struct a2dp_codec *codec,
+			const struct spa_dict *global_settings, uint8_t config[A2DP_MAX_CAPS_SIZE]);
+	int (*enum_config) (const struct a2dp_codec *codec, uint32_t flags,
 			const void *caps, size_t caps_size, uint32_t id, uint32_t idx,
 			struct spa_pod_builder *builder, struct spa_pod **param);
 	int (*validate_config) (const struct a2dp_codec *codec, uint32_t flags,
@@ -109,10 +111,11 @@ struct a2dp_codec {
 	 * The caps handed in correspond to this codec_id, but are
 	 * otherwise not checked beforehand.
 	 */
-	int (*caps_preference_cmp) (const struct a2dp_codec *codec, const void *caps1, size_t caps1_size,
-			const void *caps2, size_t caps2_size, const struct a2dp_codec_audio_info *info);
+	int (*caps_preference_cmp) (const struct a2dp_codec *codec, uint32_t flags, const void *caps1, size_t caps1_size,
+			const void *caps2, size_t caps2_size, const struct a2dp_codec_audio_info *info,
+			const struct spa_dict *global_settings);
 
-	void *(*init_props) (const struct a2dp_codec *codec, const struct spa_dict *settings);
+	void *(*init_props) (const struct a2dp_codec *codec, uint32_t flags, const struct spa_dict *settings);
 	void (*clear_props) (void *);
 	int (*enum_props) (void *props, const struct spa_dict *settings, uint32_t id, uint32_t idx,
 			struct spa_pod_builder *builder, struct spa_pod **param);
@@ -144,6 +147,8 @@ struct a2dp_codec {
 
 	int (*reduce_bitpool) (void *data);
 	int (*increase_bitpool) (void *data);
+
+	void (*set_log) (struct spa_log *global_log);
 };
 
 struct a2dp_codec_config {
@@ -156,6 +161,7 @@ int a2dp_codec_select_config(const struct a2dp_codec_config configs[], size_t n,
 	uint32_t cap, int preferred_value);
 
 bool a2dp_codec_check_caps(const struct a2dp_codec *codec, unsigned int codec_id,
-	const void *caps, size_t caps_size, const struct a2dp_codec_audio_info *info);
+	const void *caps, size_t caps_size, const struct a2dp_codec_audio_info *info,
+	const struct spa_dict *global_settings);
 
 #endif
diff --git a/spa/plugins/bluez5/a2dp-sink.c b/spa/plugins/bluez5/a2dp-sink.c
index 35a2a60d3a145d68dc397b8718d91ab4f9c6405c..fac3b57a7c5071974f213dae06d6e6b2e89eb1b3 100644
--- a/spa/plugins/bluez5/a2dp-sink.c
+++ b/spa/plugins/bluez5/a2dp-sink.c
@@ -137,6 +137,8 @@ struct impl {
 	unsigned int started:1;
 	unsigned int following:1;
 
+	unsigned int is_duplex:1;
+
 	struct spa_source source;
 	int timerfd;
 	struct spa_source flush_source;
@@ -743,7 +745,7 @@ again:
 		 * => timeout = (quantum - max_excess)/quantum * packet_time
 		 */
 		uint64_t max_excess = 2*256;
-		uint64_t packet_samples = this->frame_count * this->block_size / port->frame_size;
+		uint64_t packet_samples = (uint64_t)this->frame_count * this->block_size / port->frame_size;
 		uint64_t packet_time = packet_samples * SPA_NSEC_PER_SEC / port->current_format.info.raw.rate;
 		uint64_t quantum = SPA_LIKELY(this->clock) ? this->clock->duration : 0;
 		uint64_t timeout = (quantum > max_excess) ?
@@ -843,7 +845,7 @@ static void a2dp_on_timeout(struct spa_source *source)
 	prev_time = this->current_time;
 	now_time = this->current_time = this->next_time;
 
-	spa_log_debug(this->log, "%p: timeout %"PRIu64" %"PRIu64"", this,
+	spa_log_debug(this->log, "%p: timer %"PRIu64" %"PRIu64"", this,
 			now_time, now_time - prev_time);
 
 	if (SPA_LIKELY(this->position)) {
@@ -908,7 +910,8 @@ static int do_start(struct impl *this)
 	for (i = 0; i < size; i++)
 		spa_log_debug(this->log, "  %d: %02x", i, conf[i]);
 
-	this->codec_data = this->codec->init(this->codec, 0,
+	this->codec_data = this->codec->init(this->codec,
+			this->is_duplex ? A2DP_CODEC_FLAG_SINK : 0,
 			this->transport->configuration,
 			this->transport->configuration_len,
 			&port->current_format,
@@ -1195,6 +1198,7 @@ impl_node_port_enum_params(void *object, int seq,
 			return -EIO;
 
 		if ((res = this->codec->enum_config(this->codec,
+					this->is_duplex ? A2DP_CODEC_FLAG_SINK : 0,
 					this->transport->configuration,
 					this->transport->configuration_len,
 					id, result.index, &b, &param)) != 1)
@@ -1457,8 +1461,8 @@ static int impl_node_process(void *object)
 	spa_return_val_if_fail(this != NULL, -EINVAL);
 
 	port = &this->port;
-	io = port->io;
-	spa_return_val_if_fail(io != NULL, -EIO);
+	if ((io = port->io) == NULL)
+		return -EIO;
 
 	if (this->position && this->position->clock.flags & SPA_IO_CLOCK_FLAG_FREEWHEEL) {
 		io->status = SPA_STATUS_NEED_DATA;
@@ -1697,6 +1701,9 @@ impl_init(const struct spa_handle_factory *factory,
 
 	spa_list_init(&port->ready);
 
+	if (info && (str = spa_dict_lookup(info, "api.bluez5.a2dp-duplex")) != NULL)
+		this->is_duplex = spa_atob(str);
+
 	if (info && (str = spa_dict_lookup(info, SPA_KEY_API_BLUEZ5_TRANSPORT)))
 		sscanf(str, "pointer:%p", &this->transport);
 
@@ -1708,9 +1715,20 @@ impl_init(const struct spa_handle_factory *factory,
 		spa_log_error(this->log, "a transport codec is needed");
 		return -EINVAL;
 	}
+
 	this->codec = this->transport->a2dp_codec;
+
+	if (this->is_duplex) {
+		if (!this->codec->duplex_codec) {
+			spa_log_error(this->log, "transport codec doesn't support duplex");
+			return -EINVAL;
+		}
+		this->codec = this->codec->duplex_codec;
+	}
+
 	if (this->codec->init_props != NULL)
 		this->codec_props = this->codec->init_props(this->codec,
+					this->is_duplex ? A2DP_CODEC_FLAG_SINK : 0,
 					this->transport->device->settings);
 
 	reset_props(this, &this->props);
diff --git a/spa/plugins/bluez5/a2dp-source.c b/spa/plugins/bluez5/a2dp-source.c
index e5618ee272680a33d89e7624f141d264ade426a6..0082b9d1e3ee25a4fa60160b49885b7a6667598d 100644
--- a/spa/plugins/bluez5/a2dp-source.c
+++ b/spa/plugins/bluez5/a2dp-source.c
@@ -60,18 +60,16 @@ static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.source.a2dp
 #undef SPA_LOG_TOPIC_DEFAULT
 #define SPA_LOG_TOPIC_DEFAULT &log_topic
 
+#include "decode-buffer.h"
+
 #define DEFAULT_CLOCK_NAME	"clock.system.monotonic"
 
 struct props {
-	uint32_t min_latency;
-	uint32_t max_latency;
 	char clock_name[64];
 };
 
 #define FILL_FRAMES 2
 #define MAX_BUFFERS 32
-#define MIN_LATENCY	512
-#define MAX_LATENCY	1024
 
 struct buffer {
 	uint32_t id;
@@ -89,6 +87,7 @@ struct port {
 	uint64_t info_all;
 	struct spa_port_info info;
 	struct spa_io_buffers *io;
+	struct spa_io_rate_match *rate_match;
 	struct spa_latency_info latency;
 #define IDX_EnumFormat	0
 #define IDX_Meta	1
@@ -105,8 +104,7 @@ struct port {
 	struct spa_list free;
 	struct spa_list ready;
 
-	struct buffer *current_buffer;
-	uint32_t ready_offset;
+	struct spa_bt_decode_buffer buffer;
 };
 
 struct impl {
@@ -120,6 +118,8 @@ struct impl {
 	struct spa_hook_list hooks;
 	struct spa_callbacks callbacks;
 
+	uint32_t quantum_limit;
+
 	uint64_t info_all;
 	struct spa_node_info info;
 #define IDX_PropInfo	0
@@ -137,16 +137,25 @@ struct impl {
 	unsigned int started:1;
 	unsigned int transport_acquired:1;
 	unsigned int following:1;
+	unsigned int matching:1;
+	unsigned int resampling:1;
 
 	unsigned int is_input:1;
 	unsigned int is_duplex:1;
+	unsigned int use_duplex_source:1;
 
 	int fd;
 	struct spa_source source;
 
+	struct spa_source timer_source;
+	int timerfd;
+
 	struct spa_io_clock *clock;
         struct spa_io_position *position;
 
+	uint64_t current_time;
+	uint64_t next_time;
+
 	const struct a2dp_codec *codec;
 	bool codec_props_changed;
 	void *codec_props;
@@ -154,10 +163,8 @@ struct impl {
 	struct spa_audio_info codec_format;
 
 	uint8_t buffer_read[4096];
-	uint8_t buffer_decoded[65536];
 	struct timespec now;
 	uint64_t sample_count;
-	uint64_t skip_count;
 
 	int duplex_timerfd;
 	uint64_t duplex_timeout;
@@ -165,13 +172,8 @@ struct impl {
 
 #define CHECK_PORT(this,d,p)    ((d) == SPA_DIRECTION_OUTPUT && (p) == 0)
 
-static const uint32_t default_min_latency = MIN_LATENCY;
-static const uint32_t default_max_latency = MAX_LATENCY;
-
 static void reset_props(struct props *props)
 {
-	props->min_latency = default_min_latency;
-	props->max_latency = default_max_latency;
 	strncpy(props->clock_name, DEFAULT_CLOCK_NAME, sizeof(props->clock_name));
 }
 
@@ -200,43 +202,19 @@ static int impl_node_enum_params(void *object, int seq,
 	switch (id) {
 	case SPA_PARAM_PropInfo:
 	{
-		struct props *p = &this->props;
-
 		switch (result.index) {
-		case 0:
-			param = spa_pod_builder_add_object(&b,
-				SPA_TYPE_OBJECT_PropInfo, id,
-				SPA_PROP_INFO_id,   SPA_POD_Id(SPA_PROP_minLatency),
-				SPA_PROP_INFO_description, SPA_POD_String("The minimum latency"),
-				SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Int(p->min_latency, 1, INT32_MAX));
-			break;
-		case 1:
-			param = spa_pod_builder_add_object(&b,
-				SPA_TYPE_OBJECT_PropInfo, id,
-				SPA_PROP_INFO_id,   SPA_POD_Id(SPA_PROP_maxLatency),
-				SPA_PROP_INFO_description, SPA_POD_String("The maximum latency"),
-				SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Int(p->max_latency, 1, INT32_MAX));
-			break;
 		default:
 			enum_codec = true;
-			index_offset = 2;
+			index_offset = 0;
 		}
 		break;
 	}
 	case SPA_PARAM_Props:
 	{
-		struct props *p = &this->props;
-
 		switch (result.index) {
-		case 0:
-			param = spa_pod_builder_add_object(&b,
-				SPA_TYPE_OBJECT_Props, id,
-				SPA_PROP_minLatency, SPA_POD_Int(p->min_latency),
-				SPA_PROP_maxLatency, SPA_POD_Int(p->max_latency));
-			break;
 		default:
 			enum_codec = true;
-			index_offset = 1;
+			index_offset = 0;
 		}
 		break;
 	}
@@ -267,13 +245,38 @@ static int impl_node_enum_params(void *object, int seq,
 	return 0;
 }
 
-static int do_reassing_follower(struct spa_loop *loop,
+static int set_timeout(struct impl *this, uint64_t time)
+{
+	struct itimerspec ts;
+	ts.it_value.tv_sec = time / SPA_NSEC_PER_SEC;
+	ts.it_value.tv_nsec = time % SPA_NSEC_PER_SEC;
+	ts.it_interval.tv_sec = 0;
+	ts.it_interval.tv_nsec = 0;
+	return spa_system_timerfd_settime(this->data_system,
+			this->timerfd, SPA_FD_TIMER_ABSTIME, &ts, NULL);
+}
+
+static int set_timers(struct impl *this)
+{
+	struct timespec now;
+
+	spa_system_clock_gettime(this->data_system, CLOCK_MONOTONIC, &now);
+	this->next_time = SPA_TIMESPEC_TO_NSEC(&now);
+
+	return set_timeout(this, this->following ? 0 : this->next_time);
+}
+
+static int do_reassign_follower(struct spa_loop *loop,
 			bool async,
 			uint32_t seq,
 			const void *data,
 			size_t size,
 			void *user_data)
 {
+	struct impl *this = user_data;
+	struct port *port = &this->port;
+
+	spa_bt_decode_buffer_recover(&port->buffer);
 	return 0;
 }
 
@@ -309,7 +312,7 @@ static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size)
 	if (this->started && following != this->following) {
 		spa_log_debug(this->log, "%p: reassign follower %d->%d", this, this->following, following);
 		this->following = following;
-		spa_loop_invoke(this->data_loop, do_reassing_follower, 0, NULL, 0, true, this);
+		spa_loop_invoke(this->data_loop, do_reassign_follower, 0, NULL, 0, true, this);
 	}
 	return 0;
 }
@@ -324,10 +327,7 @@ static int apply_props(struct impl *this, const struct spa_pod *param)
 	if (param == NULL) {
 		reset_props(&new_props);
 	} else {
-		spa_pod_parse_object(param,
-				SPA_TYPE_OBJECT_Props, NULL,
-				SPA_PROP_minLatency, SPA_POD_OPT_Int(&new_props.min_latency),
-				SPA_PROP_maxLatency, SPA_POD_OPT_Int(&new_props.max_latency));
+		/* noop */
 	}
 
 	changed = (memcmp(&new_props, &this->props, sizeof(struct props)) != 0);
@@ -372,7 +372,6 @@ static void reset_buffers(struct port *port)
 
 	spa_list_init(&port->free);
 	spa_list_init(&port->ready);
-	port->current_buffer = NULL;
 
 	for (i = 0; i < port->n_buffers; i++) {
 		struct buffer *b = &port->buffers[i];
@@ -449,30 +448,15 @@ static int32_t decode_data(struct impl *this, uint8_t *src, uint32_t src_size,
 	return dst_size - avail;
 }
 
-static void skip_ready_buffers(struct impl *this)
-{
-	struct port *port = &this->port;
-
-	/* Move all buffers from ready to free */
-	while (!spa_list_is_empty(&port->ready)) {
-		struct buffer *b;
-		b = spa_list_first(&port->ready, struct buffer, link);
-		spa_list_remove(&b->link);
-		spa_list_append(&port->free, &b->link);
-		spa_assert(!b->outstanding);
-		this->skip_count += b->buf->datas[0].chunk->size / port->frame_size;
-	}
-}
-
 static void a2dp_on_ready_read(struct spa_source *source)
 {
 	struct impl *this = source->data;
 	struct port *port = &this->port;
-	struct spa_io_buffers *io = port->io;
-	int32_t size_read, decoded, avail;
-	struct spa_data *datas;
-	struct buffer *buffer;
-	uint32_t min_data;
+	struct timespec now;
+	void *buf;
+	int32_t size_read, decoded;
+	uint32_t avail;
+	uint64_t dt;
 
 	/* make sure the source is an input */
 	if ((source->rmask & SPA_IO_IN) == 0) {
@@ -486,9 +470,6 @@ static void a2dp_on_ready_read(struct spa_source *source)
 
 	spa_log_trace(this->log, "socket poll");
 
-	/* update the current pts */
-	spa_system_clock_gettime(this->data_system, CLOCK_MONOTONIC, &this->now);
-
 	/* read */
 	size_read = read_data (this);
 	if (size_read == 0)
@@ -497,7 +478,9 @@ static void a2dp_on_ready_read(struct spa_source *source)
 		spa_log_error(this->log, "failed to read data: %s", spa_strerror(size_read));
 		goto stop;
 	}
-	spa_log_trace(this->log, "read socket data %d", size_read);
+
+	/* update the current pts */
+	spa_system_clock_gettime(this->data_system, CLOCK_MONOTONIC, &now);
 
 	if (this->codec_props_changed && this->codec_props
 			&& this->codec->update_props) {
@@ -505,111 +488,33 @@ static void a2dp_on_ready_read(struct spa_source *source)
 		this->codec_props_changed = false;
 	}
 
-	/* decode */
-	decoded = decode_data(this, this->buffer_read, size_read,
-			this->buffer_decoded, sizeof (this->buffer_decoded));
+	/* decode to buffer */
+	buf = spa_bt_decode_buffer_get_write(&port->buffer, &avail);
+	spa_log_trace(this->log, "read socket data size:%d, avail:%d", size_read, avail);
+	decoded = decode_data(this, this->buffer_read, size_read, buf, avail);
 	if (decoded < 0) {
 		spa_log_debug(this->log, "failed to decode data: %d", decoded);
 		return;
 	}
-	if (decoded == 0)
+	if (decoded == 0) {
+		spa_log_trace(this->log, "no decoded socket data");
 		return;
-
-	spa_log_trace(this->log, "decoded socket data %d", decoded);
+	}
 
 	/* discard when not started */
 	if (!this->started)
 		return;
 
-	/* get buffer */
-	if (!port->current_buffer) {
-		if (spa_list_is_empty(&port->free)) {
-			/* xrun, skip ahead */
-			skip_ready_buffers(this);
-			this->skip_count += decoded / port->frame_size;
-			this->sample_count += decoded / port->frame_size;
-			return;
-		}
-		if (this->skip_count > 0) {
-			spa_log_info(this->log, "%p: xrun, skipped %"PRIu64" usec",
-			             this, (uint64_t)(this->skip_count * SPA_USEC_PER_SEC / port->current_format.info.raw.rate));
-			this->skip_count = 0;
-		}
-
-		buffer = spa_list_first(&port->free, struct buffer, link);
-		spa_list_remove(&buffer->link);
-
-		port->current_buffer = buffer;
-		port->ready_offset = 0;
-		spa_log_trace(this->log, "dequeue %d", buffer->id);
-
-		if (buffer->h) {
-			buffer->h->seq = this->sample_count;
-			buffer->h->pts = SPA_TIMESPEC_TO_NSEC(&this->now);
-			buffer->h->dts_offset = 0;
-		}
-	} else {
-		buffer = port->current_buffer;
-	}
-	datas = buffer->buf->datas;
-
-	/* copy data into buffer */
-	avail = SPA_MIN(decoded, (int32_t)(datas[0].maxsize - port->ready_offset));
-	if (avail < decoded)
-		spa_log_warn(this->log, "buffer too small (%d > %d)", decoded, avail);
-	memcpy ((uint8_t *)datas[0].data + port->ready_offset, this->buffer_decoded, avail);
-	port->ready_offset += avail;
-	this->sample_count += decoded / port->frame_size;
-
-	/* send buffer if full */
-	min_data = SPA_MIN(this->props.min_latency * port->frame_size, datas[0].maxsize / 2);
-	if (port->ready_offset >= min_data) {
-		uint64_t sample_count;
-
-		datas[0].chunk->offset = 0;
-		datas[0].chunk->size = port->ready_offset;
-		datas[0].chunk->stride = port->frame_size;
-
-		sample_count = datas[0].chunk->size / port->frame_size;
-
-		spa_log_trace(this->log, "queue %d", buffer->id);
-		spa_list_append(&port->ready, &buffer->link);
-		port->current_buffer = NULL;
-
-		if (!this->following && this->clock) {
-			this->clock->nsec = SPA_TIMESPEC_TO_NSEC(&this->now);
-			this->clock->duration = sample_count * this->clock->rate.denom / port->current_format.info.raw.rate;
-			this->clock->position = this->sample_count * this->clock->rate.denom / port->current_format.info.raw.rate;
-			this->clock->delay = 0;
-			this->clock->rate_diff = 1.0f;
-			this->clock->next_nsec = this->clock->nsec + (uint64_t)sample_count * SPA_NSEC_PER_SEC / port->current_format.info.raw.rate;
-		}
-	}
-
-	/* done if there are no buffers ready */
-	if (spa_list_is_empty(&port->ready))
-		return;
-
-	if (this->following)
-		return;
-
-	/* process the buffer if IO does not have any */
-	if (io != NULL && io->status != SPA_STATUS_HAVE_DATA) {
-		struct buffer *b;
+	spa_bt_decode_buffer_write_packet(&port->buffer, decoded);
 
-		if (io->buffer_id < port->n_buffers)
-			recycle_buffer(this, port, io->buffer_id);
+	dt = SPA_TIMESPEC_TO_NSEC(&this->now);
+	this->now = now;
+	dt = SPA_TIMESPEC_TO_NSEC(&this->now) - dt;
 
-		b = spa_list_first(&port->ready, struct buffer, link);
-		spa_list_remove(&b->link);
-		b->outstanding = true;
+	spa_log_trace(this->log, "decoded socket data size:%d frames:%d dt:%d dms",
+			(int)decoded, (int)decoded/port->frame_size,
+			(int)(dt / 100000));
 
-		io->buffer_id = b->id;
-		io->status = SPA_STATUS_HAVE_DATA;
-	}
-
-	/* notify ready */
-	spa_node_call_ready(&this->callbacks, SPA_STATUS_HAVE_DATA);
 	return;
 
 stop:
@@ -641,6 +546,75 @@ static void a2dp_on_duplex_timeout(struct spa_source *source)
 	a2dp_on_ready_read(source);
 }
 
+static int setup_matching(struct impl *this)
+{
+	struct port *port = &this->port;
+
+	if (this->position && port->rate_match) {
+		port->rate_match->rate = 1 / port->buffer.corr;
+
+		this->matching = this->following;
+		this->resampling = this->matching ||
+			(port->current_format.info.raw.rate != this->position->clock.rate.denom);
+	} else {
+		this->matching = false;
+		this->resampling = false;
+	}
+
+	if (port->rate_match)
+		SPA_FLAG_UPDATE(port->rate_match->flags, SPA_IO_RATE_MATCH_FLAG_ACTIVE, this->matching);
+
+	return 0;
+}
+
+static void a2dp_on_timeout(struct spa_source *source)
+{
+	struct impl *this = source->data;
+	struct port *port = &this->port;
+	uint64_t exp, duration;
+	uint32_t rate;
+	struct spa_io_buffers *io = port->io;
+	uint64_t prev_time, now_time;
+
+	if (this->transport == NULL)
+		return;
+
+	if (this->started && spa_system_timerfd_read(this->data_system, this->timerfd, &exp) < 0)
+		spa_log_warn(this->log, "error reading timerfd: %s", strerror(errno));
+
+	prev_time = this->current_time;
+	now_time = this->current_time = this->next_time;
+
+	spa_log_trace(this->log, "%p: timer %"PRIu64" %"PRIu64"", this,
+			now_time, now_time - prev_time);
+
+	if (SPA_LIKELY(this->position)) {
+		duration = this->position->clock.duration;
+		rate = this->position->clock.rate.denom;
+	} else {
+		duration = 1024;
+		rate = 48000;
+	}
+
+	setup_matching(this);
+
+	this->next_time = now_time + duration * SPA_NSEC_PER_SEC / port->buffer.corr / rate;
+
+	if (SPA_LIKELY(this->clock)) {
+		this->clock->nsec = now_time;
+		this->clock->position += duration;
+		this->clock->duration = duration;
+		this->clock->rate_diff = port->buffer.corr;
+		this->clock->next_nsec = this->next_time;
+	}
+
+	spa_log_trace(this->log, "%p: %d", this, io->status);
+	io->status = SPA_STATUS_HAVE_DATA;
+	spa_node_call_ready(&this->callbacks, SPA_STATUS_HAVE_DATA);
+
+	set_timeout(this, this->next_time);
+}
+
 static int transport_start(struct impl *this)
 {
 	int res, val;
@@ -656,7 +630,8 @@ static int transport_start(struct impl *this)
 
 	this->transport_acquired = true;
 
-	this->codec_data = this->codec->init(this->codec, 0,
+	this->codec_data = this->codec->init(this->codec,
+			this->is_duplex ? 0 : A2DP_CODEC_FLAG_SINK,
 			this->transport->configuration,
 			this->transport->configuration_len,
 			&port->current_format,
@@ -683,13 +658,19 @@ static int transport_start(struct impl *this)
 	if (setsockopt(this->transport->fd, SOL_SOCKET, SO_PRIORITY, &val, sizeof(val)) < 0)
 		spa_log_warn(this->log, "SO_PRIORITY failed: %m");
 
-	reset_buffers(&this->port);
+	reset_buffers(port);
+
+	spa_bt_decode_buffer_clear(&port->buffer);
+	if ((res = spa_bt_decode_buffer_init(&port->buffer, this->log,
+			port->frame_size, port->current_format.info.raw.rate,
+			this->quantum_limit, this->quantum_limit)) < 0)
+		return res;
 
 	this->fd = this->transport->fd;
 
 	this->source.data = this;
 
-	if (!this->is_duplex) {
+	if (!this->use_duplex_source) {
 		this->source.fd = this->transport->fd;
 		this->source.func = a2dp_on_ready_read;
 		this->source.mask = SPA_IO_IN;
@@ -704,7 +685,8 @@ static int transport_start(struct impl *this)
 		 * XXX: The reason for this should be found and fixed.
 		 * XXX: To work around this, for now we just do the stupid thing and poll
 		 * XXX: on a timer, chosen so that it's fast enough for the aptX-LL codec
-		 * XXX: we currently support (which sends mSBC data).
+		 * XXX: we currently support (which sends mSBC data), and also for Opus
+		 * XXX: forward stream.
 		 */
 		this->source.fd = this->duplex_timerfd;
 		this->source.func = a2dp_on_duplex_timeout;
@@ -712,12 +694,22 @@ static int transport_start(struct impl *this)
 		this->source.rmask = 0;
 		spa_loop_add_source(this->data_loop, &this->source);
 
-		this->duplex_timeout = SPA_NSEC_PER_MSEC * 75/10;
+		this->duplex_timeout = SPA_NSEC_PER_MSEC * 25/10;
 		set_duplex_timeout(this, this->duplex_timeout);
 	}
 
+	this->timer_source.data = this;
+	this->timer_source.fd = this->timerfd;
+	this->timer_source.func = a2dp_on_timeout;
+	this->timer_source.mask = SPA_IO_IN;
+	this->timer_source.rmask = 0;
+	spa_loop_add_source(this->data_loop, &this->timer_source);
+
 	this->sample_count = 0;
-	this->skip_count = 0;
+
+	setup_matching(this);
+
+	set_timers(this);
 
 	return 0;
 }
@@ -729,13 +721,13 @@ static int do_start(struct impl *this)
 	if (this->started)
 		return 0;
 
+	spa_return_val_if_fail(this->transport != NULL, -EIO);
+
 	this->following = is_following(this);
 
 	spa_log_debug(this->log, "%p: start state:%d following:%d",
 			this, this->transport->state, this->following);
 
-	spa_return_val_if_fail(this->transport != NULL, -EIO);
-
 	if (this->transport->state >= SPA_BT_TRANSPORT_STATE_PENDING ||
 			this->is_duplex)
 		res = transport_start(this);
@@ -753,6 +745,7 @@ static int do_remove_source(struct spa_loop *loop,
 			    void *user_data)
 {
 	struct impl *this = user_data;
+	struct itimerspec ts;
 
 	spa_log_debug(this->log, "%p: remove source", this);
 
@@ -761,11 +754,20 @@ static int do_remove_source(struct spa_loop *loop,
 	if (this->source.loop)
 		spa_loop_remove_source(this->data_loop, &this->source);
 
+	if (this->timer_source.loop)
+		spa_loop_remove_source(this->data_loop, &this->timer_source);
+	ts.it_value.tv_sec = 0;
+	ts.it_value.tv_nsec = 0;
+	ts.it_interval.tv_sec = 0;
+	ts.it_interval.tv_nsec = 0;
+	spa_system_timerfd_settime(this->data_system, this->timerfd, 0, &ts, NULL);
+
 	return 0;
 }
 
 static int transport_stop(struct impl *this)
 {
+	struct port *port = &this->port;
 	int res;
 
 	spa_log_debug(this->log, "%p: transport stop", this);
@@ -783,6 +785,8 @@ static int transport_stop(struct impl *this)
 		this->codec->deinit(this->codec_data);
 	this->codec_data = NULL;
 
+	spa_bt_decode_buffer_clear(&port->buffer);
+
 	return res;
 }
 
@@ -836,24 +840,20 @@ static int impl_node_send_command(void *object, const struct spa_command *comman
 
 static void emit_node_info(struct impl *this, bool full)
 {
-	char latency[64] = SPA_STRINGIFY(MIN_LATENCY)"/48000";
 	uint64_t old = full ? this->info.change_mask : 0;
 
 	struct spa_dict_item node_info_items[] = {
 		{ SPA_KEY_DEVICE_API, "bluez5" },
 		{ SPA_KEY_MEDIA_CLASS, this->is_input ? "Audio/Source" : "Stream/Output/Audio" },
-		{ SPA_KEY_NODE_LATENCY, latency },
+		{ SPA_KEY_NODE_LATENCY, this->is_input ? "" : "512/48000" },
 		{ "media.name", ((this->transport && this->transport->device->name) ?
-		                 this->transport->device->name : "A2DP") },
-                { SPA_KEY_NODE_DRIVER, this->is_input ? "true" : "false" },
+					this->transport->device->name : "A2DP") },
+		{ SPA_KEY_NODE_DRIVER, this->is_input ? "true" : "false" },
 	};
 
 	if (full)
 		this->info.change_mask = this->info_all;
 	if (this->info.change_mask) {
-		if (this->transport && this->port.have_format)
-			snprintf(latency, sizeof(latency), "%d/%d", (int)this->props.min_latency,
-					(int)this->port.current_format.info.raw.rate);
 		this->info.props = &SPA_DICT_INIT_ARRAY(node_info_items);
 		spa_node_emit_info(&this->hooks, &this->info);
 		this->info.change_mask = old;
@@ -968,6 +968,7 @@ impl_node_port_enum_params(void *object, int seq,
 			return -EIO;
 
 		if ((res = this->codec->enum_config(this->codec,
+					this->is_duplex ? 0 : A2DP_CODEC_FLAG_SINK,
 					this->transport->configuration,
 					this->transport->configuration_len,
 					id, result.index, &b, &param)) != 1)
@@ -991,11 +992,11 @@ impl_node_port_enum_params(void *object, int seq,
 
 		param = spa_pod_builder_add_object(&b,
 			SPA_TYPE_OBJECT_ParamBuffers, id,
-			SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(8, 8, MAX_BUFFERS),
+			SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(2, 1, MAX_BUFFERS),
 			SPA_PARAM_BUFFERS_blocks,  SPA_POD_Int(1),
 			SPA_PARAM_BUFFERS_size,    SPA_POD_CHOICE_RANGE_Int(
-							this->props.max_latency * port->frame_size,
-							this->props.min_latency * port->frame_size,
+							this->quantum_limit * port->frame_size,
+							16 * port->frame_size,
 							INT32_MAX),
 			SPA_PARAM_BUFFERS_stride,  SPA_POD_Int(port->frame_size));
 		break;
@@ -1021,6 +1022,12 @@ impl_node_port_enum_params(void *object, int seq,
 				SPA_PARAM_IO_id,   SPA_POD_Id(SPA_IO_Buffers),
 				SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers)));
 			break;
+		case 1:
+			param = spa_pod_builder_add_object(&b,
+					SPA_TYPE_OBJECT_ParamIO, id,
+					SPA_PARAM_IO_id,   SPA_POD_Id(SPA_IO_RateMatch),
+					SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_rate_match)));
+			break;
 		default:
 			return 0;
 		}
@@ -1059,7 +1066,6 @@ static int clear_buffers(struct impl *this, struct port *port)
 		spa_list_init(&port->ready);
 		port->n_buffers = 0;
 	}
-	port->current_buffer = NULL;
 	return 0;
 }
 
@@ -1217,6 +1223,9 @@ impl_node_port_set_io(void *object,
 	case SPA_IO_Buffers:
 		port->io = data;
 		break;
+	case SPA_IO_RateMatch:
+		port->rate_match = data;
+		break;
 	default:
 		return -ENOENT;
 	}
@@ -1244,6 +1253,88 @@ static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t
 	return 0;
 }
 
+static uint32_t get_samples(struct impl *this, uint32_t *duration)
+{
+	struct port *port = &this->port;
+	uint32_t samples;
+
+	if (SPA_LIKELY(port->rate_match) && this->resampling) {
+		samples = port->rate_match->size;
+	} else {
+		if (SPA_LIKELY(this->position))
+			samples = this->position->clock.duration * port->current_format.info.raw.rate
+				/ this->position->clock.rate.denom;
+		else
+			samples = 1024;
+	}
+
+	if (SPA_LIKELY(this->position))
+		*duration = this->position->clock.duration * port->current_format.info.raw.rate
+			/ this->position->clock.rate.denom;
+	else if (SPA_LIKELY(this->clock))
+		*duration = this->clock->duration * port->current_format.info.raw.rate
+			/ this->clock->rate.denom;
+	else
+		*duration = 1024 * port->current_format.info.raw.rate / 48000;
+
+	return samples;
+}
+
+static void process_buffering(struct impl *this)
+{
+	struct port *port = &this->port;
+	uint32_t duration;
+	const uint32_t samples = get_samples(this, &duration);
+	uint32_t avail;
+	void *buf;
+
+	spa_bt_decode_buffer_process(&port->buffer, samples, duration);
+
+	setup_matching(this);
+
+	buf = spa_bt_decode_buffer_get_read(&port->buffer, &avail);
+
+	/* copy data to buffers */
+	if (!spa_list_is_empty(&port->free) && avail > 0) {
+		struct buffer *buffer;
+		struct spa_data *datas;
+		uint32_t data_size;
+
+		data_size = samples * port->frame_size;
+
+		avail = SPA_MIN(avail, data_size);
+
+		spa_bt_decode_buffer_read(&port->buffer, avail);
+
+		buffer = spa_list_first(&port->free, struct buffer, link);
+		spa_list_remove(&buffer->link);
+
+		spa_log_trace(this->log, "dequeue %d", buffer->id);
+
+		if (buffer->h) {
+			buffer->h->seq = this->sample_count;
+			buffer->h->pts = SPA_TIMESPEC_TO_NSEC(&this->now);
+			buffer->h->dts_offset = 0;
+		}
+
+		datas = buffer->buf->datas;
+
+		spa_assert(datas[0].maxsize >= data_size);
+
+		datas[0].chunk->offset = 0;
+		datas[0].chunk->size = avail;
+		datas[0].chunk->stride = port->frame_size;
+
+		memcpy(datas[0].data, buf, avail);
+
+		this->sample_count += avail / port->frame_size;
+
+		/* ready buffer if full */
+		spa_log_trace(this->log, "queue %d frames:%d", buffer->id, (int)avail / port->frame_size);
+		spa_list_append(&port->ready, &buffer->link);
+	}
+}
+
 static int impl_node_process(void *object)
 {
 	struct impl *this = object;
@@ -1254,8 +1345,8 @@ static int impl_node_process(void *object)
 	spa_return_val_if_fail(this != NULL, -EINVAL);
 
 	port = &this->port;
-	io = port->io;
-	spa_return_val_if_fail(io != NULL, -EIO);
+	if ((io = port->io) == NULL)
+		return -EIO;
 
 	spa_log_trace(this->log, "%p status:%d", this, io->status);
 
@@ -1269,6 +1360,9 @@ static int impl_node_process(void *object)
 		io->buffer_id = SPA_ID_INVALID;
 	}
 
+	/* Handle buffering delay */
+	process_buffering(this);
+
 	/* Return if there are no buffers ready to be processed */
 	if (spa_list_is_empty(&port->ready))
 		return SPA_STATUS_OK;
@@ -1350,16 +1444,19 @@ static int impl_get_interface(struct spa_handle *handle, const char *type, void
 static int impl_clear(struct spa_handle *handle)
 {
 	struct impl *this = (struct impl *) handle;
+	struct port *port = &this->port;
 	if (this->codec_data)
 		this->codec->deinit(this->codec_data);
 	if (this->codec_props && this->codec->clear_props)
 		this->codec->clear_props(this->codec_props);
 	if (this->transport)
 		spa_hook_remove(&this->transport_listener);
+	spa_system_close(this->data_system, this->timerfd);
 	if (this->duplex_timerfd >= 0) {
 		spa_system_close(this->data_system, this->duplex_timerfd);
 		this->duplex_timerfd = -1;
 	}
+	spa_bt_decode_buffer_clear(&port->buffer);
 	return 0;
 }
 
@@ -1451,7 +1548,11 @@ impl_init(const struct spa_handle_factory *factory,
 	spa_list_init(&port->ready);
 	spa_list_init(&port->free);
 
+	this->quantum_limit = 8192;
+
 	if (info != NULL) {
+		if (info && (str = spa_dict_lookup(info, "clock.quantum-limit")))
+			spa_atou32(str, &this->quantum_limit, 0);
 		if ((str = spa_dict_lookup(info, SPA_KEY_API_BLUEZ5_TRANSPORT)) != NULL)
 			sscanf(str, "pointer:%p", &this->transport);
 		if ((str = spa_dict_lookup(info, "bluez5.a2dp-source-role")) != NULL)
@@ -1478,15 +1579,20 @@ impl_init(const struct spa_handle_factory *factory,
 		this->codec = this->codec->duplex_codec;
 		this->is_input = true;
 	}
+	this->use_duplex_source = this->is_duplex || (this->codec->duplex_codec != NULL);
 
 	if (this->codec->init_props != NULL)
 		this->codec_props = this->codec->init_props(this->codec,
+					this->is_duplex ? 0 : A2DP_CODEC_FLAG_SINK,
 					this->transport->device->settings);
 
 	spa_bt_transport_add_listener(this->transport,
 			&this->transport_listener, &transport_events, this);
 
-	if (this->is_duplex) {
+	this->timerfd = spa_system_timerfd_create(this->data_system,
+			CLOCK_MONOTONIC, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK);
+
+	if (this->use_duplex_source) {
 		this->duplex_timerfd = spa_system_timerfd_create(this->data_system,
 				CLOCK_MONOTONIC, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK);
 	} else {
diff --git a/spa/plugins/bluez5/backend-native.c b/spa/plugins/bluez5/backend-native.c
index f5c781dfb0ddbcba8d0e88fcccab15e2343cb76a..c38656bbbd5b4e9d188e55c5910b593204c2dcb7 100644
--- a/spa/plugins/bluez5/backend-native.c
+++ b/spa/plugins/bluez5/backend-native.c
@@ -29,6 +29,7 @@
 #include <sys/types.h>
 #include <sys/stat.h>
 #include <fcntl.h>
+#include <poll.h>
 
 #include <bluetooth/bluetooth.h>
 #include <bluetooth/sco.h>
@@ -73,6 +74,7 @@ struct impl {
 	struct spa_log *log;
 	struct spa_loop *main_loop;
 	struct spa_system *main_system;
+	struct spa_loop_utils *loop_utils;
 	struct spa_dbus *dbus;
 	DBusConnection *conn;
 
@@ -127,6 +129,7 @@ struct rfcomm {
 	struct spa_hook transport_listener;
 	enum spa_bt_profile profile;
 	struct spa_source timer;
+	struct spa_source *volume_sync_timer;
 	char* path;
 	bool has_volume;
 	struct rfcomm_volume volumes[SPA_BT_VOLUME_ID_TERM];
@@ -225,6 +228,8 @@ finish:
 
 static int codec_switch_stop_timer(struct rfcomm *rfcomm);
 
+static void volume_sync_stop_timer(struct rfcomm *rfcomm);
+
 static void rfcomm_free(struct rfcomm *rfcomm)
 {
 	codec_switch_stop_timer(rfcomm);
@@ -247,6 +252,8 @@ static void rfcomm_free(struct rfcomm *rfcomm)
 		close (rfcomm->source.fd);
 		rfcomm->source.fd = -1;
 	}
+	if (rfcomm->volume_sync_timer)
+		spa_loop_utils_destroy_source(rfcomm->backend->loop_utils, rfcomm->volume_sync_timer);
 	free(rfcomm);
 }
 
@@ -809,6 +816,7 @@ static bool rfcomm_hfp_ag(struct rfcomm *rfcomm, char* buf)
 		rfcomm->hfp_ag_switching_codec = false;
 		rfcomm->hfp_ag_initial_codec_setup = HFP_AG_INITIAL_CODEC_SETUP_NONE;
 		codec_switch_stop_timer(rfcomm);
+		volume_sync_stop_timer(rfcomm);
 
 		if (selected_codec != HFP_AUDIO_CODEC_CVSD && selected_codec != HFP_AUDIO_CODEC_MSBC) {
 			spa_log_warn(backend->log, "unsupported codec negotiation: %d", selected_codec);
@@ -1203,6 +1211,18 @@ fail_close:
 	return -1;
 }
 
+static int rfcomm_ag_sync_volume(struct rfcomm *rfcomm, bool later);
+
+static void wait_for_socket(int fd)
+{
+	struct pollfd fds[1];
+	const int timeout_ms = 500;
+
+	fds[0].fd = fd;
+	fds[0].events = POLLIN | POLLERR | POLLHUP;
+	poll(fds, 1, timeout_ms);
+}
+
 static int sco_acquire_cb(void *data, bool optional)
 {
 	struct spa_bt_transport *t = data;
@@ -1225,6 +1245,25 @@ static int sco_acquire_cb(void *data, bool optional)
 	rfcomm_hfp_ag_set_cind(td->rfcomm, true);
 #endif
 
+	/*
+	 * Send RFCOMM volume after connection is ready, and also after
+	 * a timeout.
+	 *
+	 * Some headsets adjust their HFP volume when in A2DP mode
+	 * without reporting via RFCOMM to us, so the volume level can
+	 * be out of sync, and we can't know what it is. Moreover, they may
+	 * take the first +VGS command after connection only partially
+	 * into account, and need a long enough timeout.
+	 *
+	 * E.g. with Sennheiser HD-250BT, the first +VGS changes the
+	 * actual volume, but does not update the level in the hardware
+	 * volume buttons, which is updated by an +VGS event only after
+	 * sufficient time is elapsed from the connection.
+	 */
+	wait_for_socket(sock);
+	rfcomm_ag_sync_volume(td->rfcomm, false);
+	rfcomm_ag_sync_volume(td->rfcomm, true);
+
 	t->fd = sock;
 
 	/* Fallback value */
@@ -1471,10 +1510,8 @@ fail_close:
 	return -1;
 }
 
-static int sco_set_volume_cb(void *data, int id, float volume)
+static int rfcomm_ag_set_volume(struct spa_bt_transport *t, int id)
 {
-	struct spa_bt_transport *t = data;
-	struct spa_bt_transport_volume *t_volume = &t->volumes[id];
 	struct transport_data *td = t->user_data;
 	struct rfcomm *rfcomm = td->rfcomm;
 	const char *format;
@@ -1485,12 +1522,7 @@ static int sco_set_volume_cb(void *data, int id, float volume)
 	    || !(rfcomm->has_volume && rfcomm->volumes[id].active))
 		return -ENOTSUP;
 
-	value = spa_bt_volume_linear_to_hw(volume, t_volume->hw_volume_max);
-	t_volume->volume = volume;
-
-	if (rfcomm->volumes[id].hw_volume == value)
-		return 0;
-	rfcomm->volumes[id].hw_volume = value;
+	value = rfcomm->volumes[id].hw_volume;
 
 	if (id == SPA_BT_VOLUME_ID_RX)
 		if (rfcomm->profile & SPA_BT_PROFILE_HFP_HF)
@@ -1511,6 +1543,29 @@ static int sco_set_volume_cb(void *data, int id, float volume)
 	return 0;
 }
 
+static int sco_set_volume_cb(void *data, int id, float volume)
+{
+	struct spa_bt_transport *t = data;
+	struct spa_bt_transport_volume *t_volume = &t->volumes[id];
+	struct transport_data *td = t->user_data;
+	struct rfcomm *rfcomm = td->rfcomm;
+	int value;
+
+	if (!rfcomm_volume_enabled(rfcomm)
+	    || !(rfcomm->profile & SPA_BT_PROFILE_HEADSET_HEAD_UNIT)
+	    || !(rfcomm->has_volume && rfcomm->volumes[id].active))
+		return -ENOTSUP;
+
+	value = spa_bt_volume_linear_to_hw(volume, t_volume->hw_volume_max);
+	t_volume->volume = volume;
+
+	if (rfcomm->volumes[id].hw_volume == value)
+		return 0;
+	rfcomm->volumes[id].hw_volume = value;
+
+	return rfcomm_ag_set_volume(t, id);
+}
+
 static const struct spa_bt_transport_implementation sco_transport_impl = {
 	SPA_VERSION_BT_TRANSPORT_IMPLEMENTATION,
 	.acquire = sco_acquire_cb,
@@ -1570,6 +1625,60 @@ static int codec_switch_stop_timer(struct rfcomm *rfcomm)
 	return 0;
 }
 
+static void volume_sync_stop_timer(struct rfcomm *rfcomm)
+{
+	if (rfcomm->volume_sync_timer)
+		spa_loop_utils_update_timer(rfcomm->backend->loop_utils, rfcomm->volume_sync_timer,
+				NULL, NULL, false);
+}
+
+static void volume_sync_timer_event(void *data, uint64_t expirations)
+{
+	struct rfcomm *rfcomm = data;
+
+	volume_sync_stop_timer(rfcomm);
+
+	if (rfcomm->transport) {
+		rfcomm_ag_set_volume(rfcomm->transport, SPA_BT_VOLUME_ID_TX);
+		rfcomm_ag_set_volume(rfcomm->transport, SPA_BT_VOLUME_ID_RX);
+	}
+}
+
+static int volume_sync_start_timer(struct rfcomm *rfcomm)
+{
+	struct timespec ts;
+	const uint64_t timeout = 1500 * SPA_NSEC_PER_MSEC;
+
+	if (rfcomm->volume_sync_timer == NULL)
+		rfcomm->volume_sync_timer = spa_loop_utils_add_timer(rfcomm->backend->loop_utils,
+				volume_sync_timer_event, rfcomm);
+
+	if (rfcomm->volume_sync_timer == NULL)
+		return -EIO;
+
+	ts.tv_sec = timeout / SPA_NSEC_PER_SEC;
+	ts.tv_nsec = timeout % SPA_NSEC_PER_SEC;
+	spa_loop_utils_update_timer(rfcomm->backend->loop_utils, rfcomm->volume_sync_timer,
+			&ts, NULL, false);
+
+	return 0;
+}
+
+static int rfcomm_ag_sync_volume(struct rfcomm *rfcomm, bool later)
+{
+	if (rfcomm->transport == NULL)
+		return -ENOENT;
+
+	if (!later) {
+		rfcomm_ag_set_volume(rfcomm->transport, SPA_BT_VOLUME_ID_TX);
+		rfcomm_ag_set_volume(rfcomm->transport, SPA_BT_VOLUME_ID_RX);
+	} else {
+		volume_sync_start_timer(rfcomm);
+	}
+
+	return 0;
+}
+
 static void codec_switch_timer_event(struct spa_source *source)
 {
 	struct rfcomm *rfcomm = source->data;
@@ -2222,6 +2331,7 @@ struct spa_bt_backend *backend_native_new(struct spa_bt_monitor *monitor,
 	backend->dbus = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DBus);
 	backend->main_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Loop);
 	backend->main_system = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_System);
+	backend->loop_utils = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_LoopUtils);
 	backend->conn = dbus_connection;
 	backend->sco.fd = -1;
 
diff --git a/spa/plugins/bluez5/bluez-hardware.conf b/spa/plugins/bluez5/bluez-hardware.conf
index 8f8cd0290b08cf3537ac70f9457469909d64ad09..0247f75d5630460a4ccd50e25b3ac461536eb22d 100644
--- a/spa/plugins/bluez5/bluez-hardware.conf
+++ b/spa/plugins/bluez5/bluez-hardware.conf
@@ -39,6 +39,7 @@ bluez5.features.device = [
     { name = "Motorola DC800", no-features = [ sbc-xq ] },  # #pipewire-1590
     { name = "Motorola S305", no-features = [ sbc-xq ] },  # #pipewire-1590
     { name = "Soundcore Life P2-L", no-features = [ msbc-alt1, msbc-alt1-rtl ] },
+    { name = "Soundcore Motion B", no-features = [ hw-volume ] },
     { name = "SoundCore mini", no-features = [ hw-volume ] },  # #pipewire-1686
     { name = "SoundCore 2", no-features = [ sbc-xq ] },  # #pipewire-2291
     { name = "Tribit MAXSound Plus", no-features = [ hw-volume ] },  # #pipewire-1592
diff --git a/spa/plugins/bluez5/bluez5-dbus.c b/spa/plugins/bluez5/bluez5-dbus.c
index 8b57911ab5c3bcc74b5424437113db91e69c796c..f43d4b57d2ff59049b98b8256ed357fafb29223a 100644
--- a/spa/plugins/bluez5/bluez5-dbus.c
+++ b/spa/plugins/bluez5/bluez5-dbus.c
@@ -125,6 +125,10 @@ struct spa_bt_monitor {
 
 	struct spa_bt_quirks *quirks;
 
+#define MAX_SETTINGS 128
+	struct spa_dict_item global_setting_items[MAX_SETTINGS];
+	struct spa_dict global_settings;
+
 	/* A reference audio info for A2DP codec configuration. */
 	struct a2dp_codec_audio_info default_audio_info;
 };
@@ -437,18 +441,21 @@ static int a2dp_codec_to_endpoint(const struct a2dp_codec *codec,
 	return 0;
 }
 
-static const struct a2dp_codec *a2dp_endpoint_to_codec(struct spa_bt_monitor *monitor, const char *endpoint)
+static const struct a2dp_codec *a2dp_endpoint_to_codec(struct spa_bt_monitor *monitor, const char *endpoint, bool *sink)
 {
 	const char *ep_name;
 	const struct a2dp_codec * const * const a2dp_codecs = monitor->a2dp_codecs;
 	int i;
 
-	if (spa_strstartswith(endpoint, A2DP_SINK_ENDPOINT "/"))
+	if (spa_strstartswith(endpoint, A2DP_SINK_ENDPOINT "/")) {
 		ep_name = endpoint + strlen(A2DP_SINK_ENDPOINT "/");
-	else if (spa_strstartswith(endpoint, A2DP_SOURCE_ENDPOINT "/"))
+		*sink = true;
+	} else if (spa_strstartswith(endpoint, A2DP_SOURCE_ENDPOINT "/")) {
 		ep_name = endpoint + strlen(A2DP_SOURCE_ENDPOINT "/");
-	else
+		*sink = false;
+	} else {
 		return NULL;
+	}
 
 	for (i = 0; a2dp_codecs[i]; i++) {
 		const struct a2dp_codec *codec = a2dp_codecs[i];
@@ -486,6 +493,7 @@ static DBusHandlerResult endpoint_select_configuration(DBusConnection *conn, DBu
 	DBusError err;
 	int i, size, res;
 	const struct a2dp_codec *codec;
+	bool sink;
 
 	dbus_error_init(&err);
 
@@ -501,14 +509,15 @@ static DBusHandlerResult endpoint_select_configuration(DBusConnection *conn, DBu
 	for (i = 0; i < size; i++)
 		spa_log_debug(monitor->log, "  %d: %02x", i, cap[i]);
 
-	codec = a2dp_endpoint_to_codec(monitor, path);
+	codec = a2dp_endpoint_to_codec(monitor, path, &sink);
 	if (codec != NULL)
 		/* FIXME: We can't determine which device the SelectConfiguration()
 		 * call is associated with, therefore device settings are not passed.
 		 * This causes inconsistency with SelectConfiguration() triggered
 		 * by codec switching.
 		  */
-		res = codec->select_config(codec, 0, cap, size, &monitor->default_audio_info, NULL, config);
+		res = codec->select_config(codec, sink ? A2DP_CODEC_FLAG_SINK : 0, cap, size, &monitor->default_audio_info,
+				&monitor->global_settings, config);
 	else
 		res = -ENOTSUP;
 
@@ -1543,7 +1552,7 @@ static bool device_props_ready(struct spa_bt_device *device)
 	return device->adapter && device->address;
 }
 
-bool spa_bt_device_supports_a2dp_codec(struct spa_bt_device *device, const struct a2dp_codec *codec)
+bool spa_bt_device_supports_a2dp_codec(struct spa_bt_device *device, const struct a2dp_codec *codec, bool sink)
 {
 	struct spa_bt_monitor *monitor = device->monitor;
 	struct spa_bt_remote_endpoint *ep;
@@ -1579,15 +1588,22 @@ bool spa_bt_device_supports_a2dp_codec(struct spa_bt_device *device, const struc
 	}
 
 	spa_list_for_each(ep, &device->remote_endpoint_list, device_link) {
+		const enum spa_bt_profile profile = spa_bt_profile_from_uuid(ep->uuid);
+		const enum spa_bt_profile expected = sink ?
+			SPA_BT_PROFILE_A2DP_SINK : SPA_BT_PROFILE_A2DP_SOURCE;
+
+		if (profile != expected)
+			continue;
+
 		if (a2dp_codec_check_caps(codec, ep->codec, ep->capabilities, ep->capabilities_len,
-					  &ep->monitor->default_audio_info))
+						&ep->monitor->default_audio_info, &monitor->global_settings))
 			return true;
 	}
 
 	return false;
 }
 
-const struct a2dp_codec **spa_bt_device_get_supported_a2dp_codecs(struct spa_bt_device *device, size_t *count)
+const struct a2dp_codec **spa_bt_device_get_supported_a2dp_codecs(struct spa_bt_device *device, size_t *count, bool sink)
 {
 	struct spa_bt_monitor *monitor = device->monitor;
 	const struct a2dp_codec * const * const a2dp_codecs = monitor->a2dp_codecs;
@@ -1603,7 +1619,7 @@ const struct a2dp_codec **spa_bt_device_get_supported_a2dp_codecs(struct spa_bt_
 
 	j = 0;
 	for (i = 0; a2dp_codecs[i] != NULL; ++i) {
-		if (spa_bt_device_supports_a2dp_codec(device, a2dp_codecs[i])) {
+		if (spa_bt_device_supports_a2dp_codec(device, a2dp_codecs[i], sink)) {
 			supported_codecs[j] = a2dp_codecs[i];
 			++j;
 		}
@@ -2511,7 +2527,7 @@ static const struct spa_bt_transport_implementation transport_impl = {
 	.set_volume = transport_set_volume,
 };
 
-static void append_basic_array_variant_dict_entry(DBusMessageIter *dict, int key_type_int, void* key, const char* variant_type_str, const char* array_type_str, int array_type_int, void* data, int data_size);
+static void append_basic_array_variant_dict_entry(DBusMessageIter *dict, const char* key, const char* variant_type_str, const char* array_type_str, int array_type_int, void* data, int data_size);
 
 static void a2dp_codec_switch_reply(DBusPendingCall *pending, void *userdata);
 
@@ -2569,10 +2585,10 @@ static bool a2dp_codec_switch_process_current(struct spa_bt_a2dp_codec_switch *s
 	char *local_endpoint = NULL;
 	int res, config_size;
 	dbus_bool_t dbus_ret;
-	const char *str;
 	DBusMessage *m;
 	DBusMessageIter iter, d;
 	int i;
+	bool sink;
 
 	/* Try setting configuration for current codec on current endpoint in list */
 
@@ -2603,8 +2619,10 @@ static bool a2dp_codec_switch_process_current(struct spa_bt_a2dp_codec_switch *s
 
 	if (sw->profile & SPA_BT_PROFILE_A2DP_SINK) {
 		local_endpoint_base = A2DP_SOURCE_ENDPOINT;
+		sink = false;
 	} else if (sw->profile & SPA_BT_PROFILE_A2DP_SOURCE) {
 		local_endpoint_base = A2DP_SINK_ENDPOINT;
+		sink = true;
 	} else {
 		spa_log_debug(sw->device->monitor->log, "a2dp codec switch %p: bad profile (%d), try next",
 		              sw, sw->profile);
@@ -2630,9 +2648,9 @@ static bool a2dp_codec_switch_process_current(struct spa_bt_a2dp_codec_switch *s
 		}
 	}
 
-	res = codec->select_config(codec, 0, ep->capabilities, ep->capabilities_len,
+	res = codec->select_config(codec, sink ? A2DP_CODEC_FLAG_SINK : 0, ep->capabilities, ep->capabilities_len,
 				   &sw->device->monitor->default_audio_info,
-				   sw->device->settings, config);
+				   &sw->device->monitor->global_settings, config);
 	if (res < 0) {
 		spa_log_debug(sw->device->monitor->log, "a2dp codec switch %p: incompatible capabilities (%d), try next",
 		              sw, res);
@@ -2659,8 +2677,7 @@ static bool a2dp_codec_switch_process_current(struct spa_bt_a2dp_codec_switch *s
 	dbus_message_iter_init_append(m, &iter);
 	dbus_message_iter_append_basic(&iter, DBUS_TYPE_OBJECT_PATH, &local_endpoint);
 	dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{sv}", &d);
-	str = "Capabilities";
-	append_basic_array_variant_dict_entry(&d, DBUS_TYPE_STRING, &str, "ay", "y", DBUS_TYPE_BYTE, config, config_size);
+	append_basic_array_variant_dict_entry(&d, "Capabilities", "ay", "y", DBUS_TYPE_BYTE, config, config_size);
 	dbus_message_iter_close_container(&iter, &d);
 
 	spa_assert(sw->pending == NULL);
@@ -2878,6 +2895,7 @@ static int a2dp_codec_switch_cmp(const void *a, const void *b)
 	const struct a2dp_codec *codec = *sw->codec_iter;
 	const char *path1 = *(char **)a, *path2 = *(char **)b;
 	struct spa_bt_remote_endpoint *ep1, *ep2;
+	uint32_t flags;
 
 	ep1 = device_remote_endpoint_find(sw->device, path1);
 	ep2 = device_remote_endpoint_find(sw->device, path2);
@@ -2886,6 +2904,10 @@ static int a2dp_codec_switch_cmp(const void *a, const void *b)
 		ep1 = NULL;
 	if (ep2 != NULL && (ep2->uuid == NULL || ep2->codec != codec->codec_id || ep2->capabilities == NULL))
 		ep2 = NULL;
+	if (ep1 && ep2 && !spa_streq(ep1->uuid, ep2->uuid)) {
+		ep1 = NULL;
+		ep2 = NULL;
+	}
 
 	if (ep1 == NULL && ep2 == NULL)
 		return 0;
@@ -2894,8 +2916,11 @@ static int a2dp_codec_switch_cmp(const void *a, const void *b)
 	else if (ep2 == NULL)
 		return -1;
 
-	return codec->caps_preference_cmp(codec, ep1->capabilities, ep1->capabilities_len,
-			ep2->capabilities, ep2->capabilities_len, &sw->device->monitor->default_audio_info);
+	flags = spa_streq(ep1->uuid, SPA_BT_UUID_A2DP_SOURCE) ? A2DP_CODEC_FLAG_SINK : 0;
+
+	return codec->caps_preference_cmp(codec, flags, ep1->capabilities, ep1->capabilities_len,
+			ep2->capabilities, ep2->capabilities_len, &sw->device->monitor->default_audio_info,
+			&sw->device->monitor->global_settings);
 }
 
 /* Ensure there's a transport for at least one of the listed codecs */
@@ -2913,7 +2938,7 @@ int spa_bt_device_ensure_a2dp_codec(struct spa_bt_device *device, const struct a
 	}
 
 	for (i = 0; codecs[i] != NULL; ++i) {
-		if (spa_bt_device_supports_a2dp_codec(device, codecs[i])) {
+		if (spa_bt_device_supports_a2dp_codec(device, codecs[i], true)) {
 			preferred_codec = codecs[i];
 			break;
 		}
@@ -3031,6 +3056,7 @@ static DBusHandlerResult endpoint_set_configuration(DBusConnection *conn,
 	struct spa_bt_transport *transport;
 	const struct a2dp_codec *codec;
 	int profile;
+	bool sink;
 
 	if (!dbus_message_has_signature(m, "oa{sv}")) {
 		spa_log_warn(monitor->log, "invalid SetConfiguration() signature");
@@ -3039,7 +3065,7 @@ static DBusHandlerResult endpoint_set_configuration(DBusConnection *conn,
 	endpoint = dbus_message_get_path(m);
 
 	profile = a2dp_endpoint_to_profile(endpoint);
-	codec = a2dp_endpoint_to_codec(monitor, endpoint);
+	codec = a2dp_endpoint_to_codec(monitor, endpoint, &sink);
 	if (codec == NULL) {
 		spa_log_warn(monitor->log, "unknown SetConfiguration() codec");
 		return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
@@ -3100,7 +3126,7 @@ static DBusHandlerResult endpoint_set_configuration(DBusConnection *conn,
 
 	if (codec->validate_config) {
 		struct spa_audio_info info;
-		if (codec->validate_config(codec, 0,
+		if (codec->validate_config(codec, sink ? A2DP_CODEC_FLAG_SINK : 0,
 					transport->configuration, transport->configuration_len,
 					&info) < 0) {
 			spa_log_error(monitor->log, "invalid transport configuration");
@@ -3256,10 +3282,10 @@ static void bluez_register_endpoint_reply(DBusPendingCall *pending, void *user_d
 	dbus_pending_call_unref(pending);
 }
 
-static void append_basic_variant_dict_entry(DBusMessageIter *dict, int key_type_int, void* key, int variant_type_int, const char* variant_type_str, void* variant) {
+static void append_basic_variant_dict_entry(DBusMessageIter *dict, const char* key, int variant_type_int, const char* variant_type_str, void* variant) {
 	DBusMessageIter dict_entry_it, variant_it;
 	dbus_message_iter_open_container(dict, DBUS_TYPE_DICT_ENTRY, NULL, &dict_entry_it);
-	dbus_message_iter_append_basic(&dict_entry_it, key_type_int, key);
+	dbus_message_iter_append_basic(&dict_entry_it, DBUS_TYPE_STRING, &key);
 
 	dbus_message_iter_open_container(&dict_entry_it, DBUS_TYPE_VARIANT, variant_type_str, &variant_it);
 	dbus_message_iter_append_basic(&variant_it, variant_type_int, variant);
@@ -3267,10 +3293,10 @@ static void append_basic_variant_dict_entry(DBusMessageIter *dict, int key_type_
 	dbus_message_iter_close_container(dict, &dict_entry_it);
 }
 
-static void append_basic_array_variant_dict_entry(DBusMessageIter *dict, int key_type_int, void* key, const char* variant_type_str, const char* array_type_str, int array_type_int, void* data, int data_size) {
+static void append_basic_array_variant_dict_entry(DBusMessageIter *dict, const char* key, const char* variant_type_str, const char* array_type_str, int array_type_int, void* data, int data_size) {
 	DBusMessageIter dict_entry_it, variant_it, array_it;
 	dbus_message_iter_open_container(dict, DBUS_TYPE_DICT_ENTRY, NULL, &dict_entry_it);
-	dbus_message_iter_append_basic(&dict_entry_it, key_type_int, key);
+	dbus_message_iter_append_basic(&dict_entry_it, DBUS_TYPE_STRING, &key);
 
 	dbus_message_iter_open_container(&dict_entry_it, DBUS_TYPE_VARIANT, variant_type_str, &variant_it);
 	dbus_message_iter_open_container(&variant_it, DBUS_TYPE_ARRAY, array_type_str, &array_it);
@@ -3283,40 +3309,42 @@ static void append_basic_array_variant_dict_entry(DBusMessageIter *dict, int key
 static int bluez_register_endpoint(struct spa_bt_monitor *monitor,
                              const char *path, const char *endpoint,
 			     const char *uuid, const struct a2dp_codec *codec) {
-	char *str, *object_path = NULL;
+	char *object_path = NULL;
 	DBusMessage *m;
 	DBusMessageIter object_it, dict_it;
 	DBusPendingCall *call;
 	uint8_t caps[A2DP_MAX_CAPS_SIZE];
 	int ret, caps_size;
 	uint16_t codec_id = codec->codec_id;
+	bool sink;
 
 	ret = a2dp_codec_to_endpoint(codec, endpoint, &object_path);
 	if (ret < 0)
-		return ret;
+		goto error;
 
-	caps_size = codec->fill_caps(codec, 0, caps);
-	if (caps_size < 0)
-		return caps_size;
+	sink = spa_streq(endpoint, A2DP_SINK_ENDPOINT);
+
+	ret = caps_size = codec->fill_caps(codec, sink ? A2DP_CODEC_FLAG_SINK : 0, caps);
+	if (ret < 0)
+		goto error;
 
 	m = dbus_message_new_method_call(BLUEZ_SERVICE,
 	                                 path,
 	                                 BLUEZ_MEDIA_INTERFACE,
 	                                 "RegisterEndpoint");
-	if (m == NULL)
-		return -EIO;
+	if (m == NULL) {
+		ret = -EIO;
+		goto error;
+	}
 
 	dbus_message_iter_init_append(m, &object_it);
 	dbus_message_iter_append_basic(&object_it, DBUS_TYPE_OBJECT_PATH, &object_path);
 
 	dbus_message_iter_open_container(&object_it, DBUS_TYPE_ARRAY, "{sv}", &dict_it);
 
-	str = "UUID";
-	append_basic_variant_dict_entry(&dict_it, DBUS_TYPE_STRING, &str, DBUS_TYPE_STRING, "s", &uuid);
-	str = "Codec";
-	append_basic_variant_dict_entry(&dict_it, DBUS_TYPE_STRING, &str, DBUS_TYPE_BYTE, "y", &codec_id);
-	str = "Capabilities";
-	append_basic_array_variant_dict_entry(&dict_it, DBUS_TYPE_STRING, &str, "ay", "y", DBUS_TYPE_BYTE, caps, caps_size);
+	append_basic_variant_dict_entry(&dict_it,"UUID", DBUS_TYPE_STRING, "s", &uuid);
+	append_basic_variant_dict_entry(&dict_it, "Codec", DBUS_TYPE_BYTE, "y", &codec_id);
+	append_basic_array_variant_dict_entry(&dict_it, "Capabilities", "ay", "y", DBUS_TYPE_BYTE, caps, caps_size);
 
 	dbus_message_iter_close_container(&object_it, &dict_it);
 
@@ -3327,6 +3355,10 @@ static int bluez_register_endpoint(struct spa_bt_monitor *monitor,
 	free(object_path);
 
 	return 0;
+
+error:
+	free(object_path);
+	return ret;
 }
 
 static int register_a2dp_endpoint(struct spa_bt_monitor *monitor,
@@ -3416,7 +3448,6 @@ static int adapter_register_endpoints(struct spa_bt_adapter *a)
 static void append_a2dp_object(DBusMessageIter *iter, const char *endpoint,
 		const char *uuid, uint8_t codec_id, uint8_t *caps, size_t caps_size)
 {
-	char* str;
 	const char *interface_name = BLUEZ_MEDIA_ENDPOINT_INTERFACE;
 	DBusMessageIter object, array, entry, dict;
 	dbus_bool_t delay_reporting;
@@ -3431,16 +3462,12 @@ static void append_a2dp_object(DBusMessageIter *iter, const char *endpoint,
 
 	dbus_message_iter_open_container(&entry, DBUS_TYPE_ARRAY, "{sv}", &dict);
 
-	str = "UUID";
-	append_basic_variant_dict_entry(&dict, DBUS_TYPE_STRING, &str, DBUS_TYPE_STRING, "s", &uuid);
-	str = "Codec";
-	append_basic_variant_dict_entry(&dict, DBUS_TYPE_STRING, &str, DBUS_TYPE_BYTE, "y", &codec_id);
-	str = "Capabilities";
-	append_basic_array_variant_dict_entry(&dict, DBUS_TYPE_STRING, &str, "ay", "y", DBUS_TYPE_BYTE, caps, caps_size);
+	append_basic_variant_dict_entry(&dict, "UUID", DBUS_TYPE_STRING, "s", &uuid);
+	append_basic_variant_dict_entry(&dict, "Codec", DBUS_TYPE_BYTE, "y", &codec_id);
+	append_basic_array_variant_dict_entry(&dict, "Capabilities", "ay", "y", DBUS_TYPE_BYTE, caps, caps_size);
 	if (spa_bt_profile_from_uuid(uuid) & SPA_BT_PROFILE_A2DP_SOURCE) {
-		str = "DelayReporting";
 		delay_reporting = TRUE;
-		append_basic_variant_dict_entry(&dict, DBUS_TYPE_STRING, &str, DBUS_TYPE_BOOLEAN, "b", &delay_reporting);
+		append_basic_variant_dict_entry(&dict, "DelayReporting", DBUS_TYPE_BOOLEAN, "b", &delay_reporting);
 	}
 
 	dbus_message_iter_close_container(&entry, &dict);
@@ -3495,11 +3522,11 @@ static DBusHandlerResult object_manager_handler(DBusConnection *c, DBusMessage *
 			if (!is_a2dp_codec_enabled(monitor, codec))
 				continue;
 
-			caps_size = codec->fill_caps(codec, 0, caps);
-			if (caps_size < 0)
-				continue;
-
 			if (codec->decode != NULL) {
+				caps_size = codec->fill_caps(codec, A2DP_CODEC_FLAG_SINK, caps);
+				if (caps_size < 0)
+					continue;
+
 				ret = a2dp_codec_to_endpoint(codec, A2DP_SINK_ENDPOINT, &endpoint);
 				if (ret == 0) {
 					spa_log_info(monitor->log, "register A2DP sink codec %s: %s", a2dp_codecs[i]->name, endpoint);
@@ -3510,6 +3537,10 @@ static DBusHandlerResult object_manager_handler(DBusConnection *c, DBusMessage *
 			}
 
 			if (codec->encode != NULL) {
+				caps_size = codec->fill_caps(codec, 0, caps);
+				if (caps_size < 0)
+					continue;
+
 				ret = a2dp_codec_to_endpoint(codec, A2DP_SOURCE_ENDPOINT, &endpoint);
 				if (ret == 0) {
 					spa_log_info(monitor->log, "register A2DP source codec %s: %s", a2dp_codecs[i]->name, endpoint);
@@ -4280,6 +4311,7 @@ static int impl_clear(struct spa_handle *handle)
 	struct spa_bt_device *d;
 	struct spa_bt_remote_endpoint *ep;
 	struct spa_bt_transport *t;
+	const struct spa_dict_item *it;
 	size_t i;
 
 	monitor = (struct spa_bt_monitor *) handle;
@@ -4311,6 +4343,11 @@ static int impl_clear(struct spa_handle *handle)
 		monitor->backends[i] = NULL;
 	}
 
+	spa_dict_for_each(it, &monitor->global_settings) {
+		free((void *)it->key);
+		free((void *)it->value);
+	}
+
 	free((void*)monitor->enabled_codecs.items);
 	spa_zero(monitor->enabled_codecs);
 
@@ -4447,6 +4484,26 @@ fallback:
 	return 0;
 }
 
+static void get_global_settings(struct spa_bt_monitor *this, const struct spa_dict *dict)
+{
+	uint32_t n_items = 0;
+	uint32_t i;
+
+	if (dict == NULL) {
+		this->global_settings = SPA_DICT_INIT(this->global_setting_items, 0);
+		return;
+	}
+
+	for (i = 0; i < dict->n_items && n_items < SPA_N_ELEMENTS(this->global_setting_items); i++) {
+		const struct spa_dict_item *it = &dict->items[i];
+		if (spa_strstartswith(it->key, "bluez5.") && it->value != NULL)
+			this->global_setting_items[n_items++] =
+				SPA_DICT_ITEM_INIT(strdup(it->key), strdup(it->value));
+	}
+
+	this->global_settings = SPA_DICT_INIT(this->global_setting_items, n_items);
+}
+
 static int
 impl_init(const struct spa_handle_factory *factory,
 	  struct spa_handle *handle,
@@ -4540,6 +4597,8 @@ impl_init(const struct spa_handle_factory *factory,
 
 	this->backend_selection = BACKEND_NATIVE;
 
+	get_global_settings(this, info);
+
 	if (info) {
 		const char *str;
 		uint32_t tmp;
diff --git a/spa/plugins/bluez5/bluez5-device.c b/spa/plugins/bluez5/bluez5-device.c
index 6c45a028b25a98478137eba40871490dab7bd165..5a23ea4409c1c0842957ae9952b1be1cba2c6d68 100644
--- a/spa/plugins/bluez5/bluez5-device.c
+++ b/spa/plugins/bluez5/bluez5-device.c
@@ -301,6 +301,8 @@ static void emit_info(struct impl *this, bool full);
 
 static float get_soft_volume_boost(struct node *node)
 {
+	const struct a2dp_codec *codec = node->transport ? node->transport->a2dp_codec : NULL;
+
 	/*
 	 * For A2DP duplex, the duplex microphone channel sometimes does not appear
 	 * to have hardware gain, and input volume is very low.
@@ -310,7 +312,8 @@ static float get_soft_volume_boost(struct node *node)
 	 * If this causes clipping, the user can just reduce the mic volume to
 	 * bring SW gain below 1.
 	 */
-	if (node->a2dp_duplex && node->transport &&
+	if (node->a2dp_duplex && node->transport && codec && codec->info &&
+			spa_atob(spa_dict_lookup(codec->info, "duplex.boost")) &&
 			node->id == DEVICE_ID_SOURCE &&
 			!node->transport->volumes[SPA_BT_VOLUME_ID_RX].active)
 		return 10.0f;	/* 20 dB boost */
@@ -337,35 +340,57 @@ static void node_update_soft_volumes(struct node *node, float hw_volume)
 	}
 }
 
-static void volume_changed(void *userdata)
+static bool node_update_volume_from_transport(struct node *node, bool reset)
 {
-	struct node *node = userdata;
 	struct impl *impl = node->impl;
 	struct spa_bt_transport_volume *t_volume;
 	float prev_hw_volume;
 
 	if (!node->transport || !spa_bt_transport_volume_enabled(node->transport))
-		return;
+		return false;
 
 	/* PW is the controller for remote device. */
 	if (impl->profile != DEVICE_PROFILE_A2DP
 	    && impl->profile !=  DEVICE_PROFILE_HSP_HFP)
-		return;
+		return false;
 
 	t_volume = &node->transport->volumes[node->id];
 
 	if (!t_volume->active)
-		return;
+		return false;
 
 	prev_hw_volume = node_get_hw_volume(node);
-	for (uint32_t i = 0; i < node->n_channels; ++i) {
-		node->volumes[i] = prev_hw_volume > 0.0f
-			? node->volumes[i] * t_volume->volume / prev_hw_volume
-			: t_volume->volume;
+
+	if (!reset) {
+		for (uint32_t i = 0; i < node->n_channels; ++i) {
+			node->volumes[i] = prev_hw_volume > 0.0f
+				? node->volumes[i] * t_volume->volume / prev_hw_volume
+				: t_volume->volume;
+		}
+	} else {
+		for (uint32_t i = 0; i < node->n_channels; ++i)
+			node->volumes[i] = t_volume->volume;
 	}
 
 	node_update_soft_volumes(node, t_volume->volume);
 
+	/*
+	 * Consider volume changes from the headset as requested
+	 * by the user, and to be saved by the SM.
+	 */
+	node->save = true;
+
+	return true;
+}
+
+static void volume_changed(void *userdata)
+{
+	struct node *node = userdata;
+	struct impl *impl = node->impl;
+
+	if (!node_update_volume_from_transport(node, false))
+		return;
+
 	emit_volume(impl, node);
 
 	impl->info.change_mask |= SPA_DEVICE_CHANGE_MASK_PARAMS;
@@ -469,6 +494,8 @@ static void emit_node(struct impl *this, struct spa_bt_transport *t,
 				this->nodes[id].volumes[i] = this->nodes[id].volumes[i % prev_channels];
 		}
 
+		node_update_volume_from_transport(&this->nodes[id], true);
+
 		boost = get_soft_volume_boost(&this->nodes[id]);
 		if (boost != 1.0f) {
 			size_t i;
@@ -888,7 +915,7 @@ static void profiles_changed(void *userdata, uint32_t prev_profiles, uint32_t pr
 	if (this->bt_dev->connected_profiles & SPA_BT_PROFILE_A2DP_SINK) {
 		free(this->supported_codecs);
 		this->supported_codecs = spa_bt_device_get_supported_a2dp_codecs(
-			this->bt_dev, &this->supported_codec_count);
+			this->bt_dev, &this->supported_codec_count, true);
 	}
 
 	switch (this->profile) {
@@ -1103,7 +1130,7 @@ static void set_initial_profile(struct impl *this)
 	if (this->supported_codecs)
 		free(this->supported_codecs);
 	this->supported_codecs = spa_bt_device_get_supported_a2dp_codecs(
-					this->bt_dev, &this->supported_codec_count);
+		this->bt_dev, &this->supported_codec_count, true);
 
 	/* Prefer A2DP, then HFP, then null, but select AG if the device
 	   appears not to have A2DP_SINK or any HEAD_UNIT profile */
@@ -1301,16 +1328,17 @@ static bool validate_profile(struct impl *this, uint32_t profile,
 }
 
 static struct spa_pod *build_route(struct impl *this, struct spa_pod_builder *b,
-		uint32_t id, uint32_t port, uint32_t dev, uint32_t profile)
+		uint32_t id, uint32_t port, uint32_t profile)
 {
 	struct spa_bt_device *device = this->bt_dev;
 	struct spa_pod_frame f[2];
 	enum spa_direction direction;
-	const char *name_prefix, *description, *port_type;
+	const char *name_prefix, *description, *hfp_description, *port_type;
 	enum spa_bt_form_factor ff;
 	enum spa_bluetooth_audio_codec codec;
 	char name[128];
 	uint32_t i, j, mask, next;
+	uint32_t dev = SPA_ID_INVALID, enum_dev;
 
 	ff = spa_bt_form_factor_from_class(device->bluetooth_class);
 
@@ -1318,52 +1346,62 @@ static struct spa_pod *build_route(struct impl *this, struct spa_pod_builder *b,
 	case SPA_BT_FORM_FACTOR_HEADSET:
 		name_prefix = "headset";
 		description = _("Headset");
+		hfp_description = _("Handsfree");
 		port_type = "headset";
 		break;
 	case SPA_BT_FORM_FACTOR_HANDSFREE:
 		name_prefix = "handsfree";
 		description = _("Handsfree");
+		hfp_description = _("Handsfree (HFP)");
 		port_type = "handsfree";
 		break;
 	case SPA_BT_FORM_FACTOR_MICROPHONE:
 		name_prefix = "microphone";
 		description = _("Microphone");
+		hfp_description = _("Handsfree");
 		port_type = "mic";
 		break;
 	case SPA_BT_FORM_FACTOR_SPEAKER:
 		name_prefix = "speaker";
 		description = _("Speaker");
+		hfp_description = _("Handsfree");
 		port_type = "speaker";
 		break;
 	case SPA_BT_FORM_FACTOR_HEADPHONE:
 		name_prefix = "headphone";
 		description = _("Headphone");
+		hfp_description = _("Handsfree");
 		port_type = "headphones";
 		break;
 	case SPA_BT_FORM_FACTOR_PORTABLE:
 		name_prefix = "portable";
 		description = _("Portable");
+		hfp_description = _("Handsfree");
 		port_type = "portable";
 		break;
 	case SPA_BT_FORM_FACTOR_CAR:
 		name_prefix = "car";
 		description = _("Car");
+		hfp_description = _("Handsfree");
 		port_type = "car";
 		break;
 	case SPA_BT_FORM_FACTOR_HIFI:
 		name_prefix = "hifi";
 		description = _("HiFi");
+		hfp_description = _("Handsfree");
 		port_type = "hifi";
 		break;
 	case SPA_BT_FORM_FACTOR_PHONE:
 		name_prefix = "phone";
 		description = _("Phone");
+		hfp_description = _("Handsfree");
 		port_type = "phone";
 		break;
 	case SPA_BT_FORM_FACTOR_UNKNOWN:
 	default:
 		name_prefix = "bluetooth";
 		description = _("Bluetooth");
+		hfp_description = _("Bluetooth (HFP)");
 		port_type = "bluetooth";
 		break;
 	}
@@ -1372,16 +1410,51 @@ static struct spa_pod *build_route(struct impl *this, struct spa_pod_builder *b,
 	case 0:
 		direction = SPA_DIRECTION_INPUT;
 		snprintf(name, sizeof(name), "%s-input", name_prefix);
+		enum_dev = DEVICE_ID_SOURCE;
+		if (profile == DEVICE_PROFILE_A2DP)
+			dev = enum_dev;
+		else if (profile != SPA_ID_INVALID)
+			enum_dev = SPA_ID_INVALID;
 		break;
 	case 1:
 		direction = SPA_DIRECTION_OUTPUT;
 		snprintf(name, sizeof(name), "%s-output", name_prefix);
+		enum_dev = DEVICE_ID_SINK;
+		if (profile == DEVICE_PROFILE_A2DP)
+			dev = enum_dev;
+		else if (profile != SPA_ID_INVALID)
+			enum_dev = SPA_ID_INVALID;
+		break;
+	case 2:
+		direction = SPA_DIRECTION_INPUT;
+		snprintf(name, sizeof(name), "%s-hf-input", name_prefix);
+		description = hfp_description;
+		enum_dev = DEVICE_ID_SOURCE;
+		if (profile == DEVICE_PROFILE_HSP_HFP)
+			dev = enum_dev;
+		else if (profile != SPA_ID_INVALID)
+			enum_dev = SPA_ID_INVALID;
+		break;
+	case 3:
+		direction = SPA_DIRECTION_OUTPUT;
+		snprintf(name, sizeof(name), "%s-hf-output", name_prefix);
+		description = hfp_description;
+		enum_dev = DEVICE_ID_SINK;
+		if (profile == DEVICE_PROFILE_HSP_HFP)
+			dev = enum_dev;
+		else if (profile != SPA_ID_INVALID)
+			enum_dev = SPA_ID_INVALID;
 		break;
 	default:
 		errno = EINVAL;
 		return NULL;
 	}
 
+	if (enum_dev == SPA_ID_INVALID) {
+		errno = EINVAL;
+		return NULL;
+	}
+
 	spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_ParamRoute, id);
 	spa_pod_builder_add(b,
 		SPA_PARAM_ROUTE_index, SPA_POD_Int(port),
@@ -1406,6 +1479,11 @@ static struct spa_pod *build_route(struct impl *this, struct spa_pod_builder *b,
 	for (i = 1; (j = get_profile_from_index(this, i, &next, &codec)) != SPA_ID_INVALID; i = next) {
 		uint32_t profile_mask;
 
+		if (j == DEVICE_PROFILE_A2DP && !(port == 0 || port == 1))
+			continue;
+		if (j == DEVICE_PROFILE_HSP_HFP && !(port == 2 || port == 3))
+			continue;
+
 		profile_mask = profile_direction_mask(this, j, codec);
 		if (!(profile_mask & (1 << direction)))
 			continue;
@@ -1472,8 +1550,7 @@ static struct spa_pod *build_route(struct impl *this, struct spa_pod_builder *b,
 
 	spa_pod_builder_prop(b, SPA_PARAM_ROUTE_devices, 0);
 	spa_pod_builder_push_array(b, &f[1]);
-	/* port and device indexes are the same, 0=source, 1=sink */
-	spa_pod_builder_int(b, port);
+	spa_pod_builder_int(b, enum_dev);
 	spa_pod_builder_pop(b, &f[1]);
 
 	if (profile != SPA_ID_INVALID) {
@@ -1640,9 +1717,8 @@ static int impl_enum_params(void *object, int seq,
 	case SPA_PARAM_EnumRoute:
 	{
 		switch (result.index) {
-		case 0: case 1:
-			param = build_route(this, &b, id, result.index,
-					SPA_ID_INVALID, SPA_ID_INVALID);
+		case 0: case 1: case 2: case 3:
+			param = build_route(this, &b, id, result.index, SPA_ID_INVALID);
 			if (param == NULL)
 				goto next;
 			break;
@@ -1654,9 +1730,8 @@ static int impl_enum_params(void *object, int seq,
 	case SPA_PARAM_Route:
 	{
 		switch (result.index) {
-		case 0: case 1:
-			param = build_route(this, &b, id, result.index,
-					result.index, this->profile);
+		case 0: case 1: case 2: case 3:
+			param = build_route(this, &b, id, result.index, this->profile);
 			if (param == NULL)
 				goto next;
 			break;
diff --git a/spa/plugins/bluez5/codec-loader.c b/spa/plugins/bluez5/codec-loader.c
index cce3c4bc65aac7736f782fd8b6625c40aa08b89b..172c42309455b473ad22924c1de7de227036d291 100644
--- a/spa/plugins/bluez5/codec-loader.c
+++ b/spa/plugins/bluez5/codec-loader.c
@@ -63,6 +63,11 @@ static int codec_order(const struct a2dp_codec *c)
 		SPA_BLUETOOTH_AUDIO_CODEC_APTX_LL_DUPLEX,
 		SPA_BLUETOOTH_AUDIO_CODEC_FASTSTREAM,
 		SPA_BLUETOOTH_AUDIO_CODEC_FASTSTREAM_DUPLEX,
+		SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05,
+		SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_51,
+		SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_71,
+		SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_DUPLEX,
+		SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_PRO,
 	};
 	size_t i;
 	for (i = 0; i < SPA_N_ELEMENTS(order); ++i)
@@ -138,6 +143,9 @@ static int load_a2dp_codecs_from(struct impl *impl, const char *factory_name, co
 
 		spa_log_debug(impl->log, "loaded A2DP codec %s from %s", c->name, factory_name);
 
+		if (c->set_log)
+			c->set_log(impl->log);
+
 		impl->codecs[impl->n_codecs++] = c;
 		++n_codecs;
 
@@ -171,7 +179,8 @@ const struct a2dp_codec * const *load_a2dp_codecs(struct spa_plugin_loader *load
 		A2DP_CODEC_FACTORY_LIB("faststream"),
 		A2DP_CODEC_FACTORY_LIB("ldac"),
 		A2DP_CODEC_FACTORY_LIB("sbc"),
-		A2DP_CODEC_FACTORY_LIB("lc3plus")
+		A2DP_CODEC_FACTORY_LIB("lc3plus"),
+		A2DP_CODEC_FACTORY_LIB("opus")
 #undef A2DP_CODEC_FACTORY_LIB
 	};
 
diff --git a/spa/plugins/bluez5/decode-buffer.h b/spa/plugins/bluez5/decode-buffer.h
new file mode 100644
index 0000000000000000000000000000000000000000..434f735512c7fd94d4e719a1e946c5a3e2789ddc
--- /dev/null
+++ b/spa/plugins/bluez5/decode-buffer.h
@@ -0,0 +1,486 @@
+/* Spa Bluez5 decode buffer
+ *
+ * Copyright © 2022 Pauli Virtanen
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+/**
+ * \file decode-buffer.h   Buffering for Bluetooth sources
+ *
+ * A linear buffer, which is compacted when it gets half full.
+ *
+ * Also contains buffering logic, which calculates a rate correction
+ * factor to maintain the buffer level at the target value.
+ *
+ * Consider typical packet intervals with nominal frame duration
+ * of 10ms:
+ *
+ *     ... 5ms | 5ms | 20ms | 5ms | 5ms | 20ms ...
+ *
+ *     ... 3ms | 3ms | 4ms | 30ms | 3ms | 3ms | 4ms | 30ms ...
+ *
+ * plus random jitter; 10ms nominal may occasionally have 20+ms interval.
+ * The regular timer cycle cannot be aligned with this, so process()
+ * may occur at any time.
+ *
+ * The buffer level is the difference between the number of samples in
+ * buffer immediately after receiving a packet, and the samples consumed
+ * before receiving the next packet.
+ *
+ * The buffer level indicates how much any packet can be delayed without
+ * underrun. If it is positive, there are no underruns.
+ *
+ * The rate correction aims to maintain the average level at a safety margin.
+ */
+
+#ifndef SPA_BLUEZ5_DECODE_BUFFER_H
+#define SPA_BLUEZ5_DECODE_BUFFER_H
+
+#include <stdlib.h>
+#include <spa/utils/defs.h>
+#include <spa/support/log.h>
+
+#define BUFFERING_LONG_MSEC		(2*60000)
+#define BUFFERING_SHORT_MSEC		1000
+#define BUFFERING_RATE_DIFF_MAX		0.005
+
+/**
+ * Safety margin.
+ *
+ * The spike is the long-window maximum difference
+ * between minimum and average buffer level.
+ */
+#define BUFFERING_TARGET(spike,packet_size)				\
+	SPA_CLAMP((spike)*3/2, (packet_size), 6*(packet_size))
+
+/**
+ * Rate controller.
+ *
+ * It's here in a form, where it operates on the running average
+ * so it's compatible with the level spike determination, and
+ * clamping the rate to a range is easy. The impulse response
+ * is similar to spa_dll, and step response does not have sign changes.
+ *
+ * The controller iterates as
+ *
+ *    avg(j+1) = (1 - beta) avg(j) + beta level(j)
+ *    corr(j+1) = corr(j) + a [avg(j+1) - avg(j)] / duration
+ *			  + b [avg(j) - target] / duration
+ *
+ * with beta = duration/avg_period < 0.5 is the moving average parameter,
+ * and a = beta/3 + ..., b = beta^2/27 + ....
+ *
+ * This choice results to c(j) being low-pass filtered, and buffer level(j)
+ * converging towards target with stable damped evolution with eigenvalues
+ * real and close to each other around (1 - beta)^(1/3).
+ *
+ * Derivation:
+ *
+ * The deviation from the buffer level target evolves as
+ *
+ *     delta(j) = level(j) - target
+ *     delta(j+1) = delta(j) + r(j) - c(j+1)
+ *
+ * where r is samples received in one duration, and c corrected rate
+ * (samples per duration).
+ *
+ * The rate correction is in general determined by linear filter f
+ *
+ *     c(j+1) = c(j) + \sum_{k=0}^\infty delta(j - k) f(k)
+ *
+ * If \sum_k f(k) is not zero, the only fixed point is c=r, delta=0,
+ * so this structure (if the filter is stable) rate matches and
+ * drives buffer level to target.
+ *
+ * The z-transform then is
+ *
+ *     delta(z) = G(z) r(z)
+ *     c(z) = F(z) delta(z)
+ *     G(z) = (z - 1) / [(z - 1)^2 + z f(z)]
+ *     F(z) = f(z) / (z - 1)
+ *
+ * We now want: poles of G(z) must be in |z|<1 for stability, F(z)
+ * should damp high frequencies, and f(z) is causal.
+ *
+ * To satisfy the conditions, take
+ *
+ *     (z - 1)^2 + z f(z) = p(z) / q(z)
+ *
+ * where p(z) is polynomial with leading term z^n with wanted root
+ * structure, and q(z) is any polynomial with leading term z^{n-2}.
+ * This guarantees f(z) is causal, and G(z) = (z-1) q(z) / p(z).
+ * We can choose p(z) and q(z) to improve low-pass properties of F(z).
+ *
+ * Simplest choice is p(z)=(z-x)^2 and q(z)=1, but that gives flat
+ * high frequency response in F(z). Better choice is p(z) = (z-u)*(z-v)*(z-w)
+ * and q(z) = z - r. To make F(z) better lowpass, one can cancel
+ * a resulting 1/z pole in F(z) by setting r=u*v*w. Then,
+ *
+ *     G(z) = (z - u*v*w)*(z - 1) / [(z - u)*(z - v)*(z - w)]
+ *     F(z) = (a z + b - a) / (z - 1) *	 H(z)
+ *     H(z) = beta / (z - 1 + beta)
+ *     beta = 1 - u*v*w
+ *     a = [(1-u) + (1-v) + (1-w) - beta] / beta
+ *     b = (1-u)*(1-v)*(1-w) / beta
+ *
+ * which corresponds to iteration for c(j):
+ *
+ *    avg(j+1) = (1 - beta) avg(j) + beta delta(j)
+ *    c(j+1) = c(j) + a [avg(j+1) - avg(j)] + b avg(j)
+ *
+ * So the controller operates on the running average,
+ * which gives the low-pass property for c(j).
+ *
+ * The simplest filter is obtained by putting the poles at
+ * u=v=w=(1-beta)**(1/3). Since beta << 1, computing the root
+ * can be avoided by expanding in series.
+ *
+ * Overshoot in impulse response could be reduced by moving one of the
+ * poles closer to z=1, but this increases the step response time.
+ */
+struct spa_bt_rate_control
+{
+	double avg;
+	double corr;
+};
+
+static void spa_bt_rate_control_init(struct spa_bt_rate_control *this, double level)
+{
+	this->avg = level;
+	this->corr = 1.0;
+}
+
+static double spa_bt_rate_control_update(struct spa_bt_rate_control *this, double level,
+		double target, double duration, double period)
+{
+	/*
+	 * u = (1 - beta)^(1/3)
+	 * x = a / beta
+	 * y = b / beta
+	 * a = (2 + u) * (1 - u)^2 / beta
+	 * b = (1 - u)^3 / beta
+	 * beta -> 0
+	 */
+	const double beta = SPA_CLAMP(duration / period, 0, 0.5);
+	const double x = 1.0/3;
+	const double y = beta/27;
+	double avg;
+
+	avg = beta * level + (1 - beta) * this->avg;
+	this->corr += x * (avg - this->avg) / period
+		+ y * (this->avg - target) / period;
+	this->avg = avg;
+
+	this->corr = SPA_CLAMP(this->corr,
+			1 - BUFFERING_RATE_DIFF_MAX,
+			1 + BUFFERING_RATE_DIFF_MAX);
+
+	return this->corr;
+}
+
+
+/** Windowed min/max */
+struct spa_bt_ptp
+{
+	union {
+		int32_t min;
+		int32_t mins[4];
+	};
+	union {
+		int32_t max;
+		int32_t maxs[4];
+	};
+	uint32_t pos;
+	uint32_t period;
+};
+
+struct spa_bt_decode_buffer
+{
+	struct spa_log *log;
+
+	uint32_t frame_size;
+	uint32_t rate;
+
+	uint8_t *buffer_decoded;
+	uint32_t buffer_size;
+	uint32_t buffer_reserve;
+	uint32_t write_index;
+	uint32_t read_index;
+
+	struct spa_bt_ptp spike;	/**< spikes (long window) */
+	struct spa_bt_ptp packet_size;	/**< packet size (short window) */
+
+	struct spa_bt_rate_control ctl;
+	double corr;
+
+	uint32_t prev_consumed;
+	uint32_t prev_avail;
+	uint32_t prev_duration;
+	uint32_t underrun;
+	uint32_t pos;
+
+	uint8_t received:1;
+	uint8_t buffering:1;
+};
+
+static void spa_bt_ptp_init(struct spa_bt_ptp *p, int32_t period)
+{
+	size_t i;
+
+	spa_zero(*p);
+	for (i = 0; i < SPA_N_ELEMENTS(p->mins); ++i) {
+		p->mins[i] = INT32_MAX;
+		p->maxs[i] = INT32_MIN;
+	}
+	p->period = period;
+}
+
+static void spa_bt_ptp_update(struct spa_bt_ptp *p, int32_t value, uint32_t duration)
+{
+	const size_t n = SPA_N_ELEMENTS(p->mins);
+	size_t i;
+
+	for (i = 0; i < n; ++i) {
+		p->mins[i] = SPA_MIN(p->mins[i], value);
+		p->maxs[i] = SPA_MAX(p->maxs[i], value);
+	}
+
+	p->pos += duration;
+	if (p->pos >= p->period / (n - 1)) {
+		p->pos = 0;
+		for (i = 1; i < SPA_N_ELEMENTS(p->mins); ++i) {
+			p->mins[i-1] = p->mins[i];
+			p->maxs[i-1] = p->maxs[i];
+		}
+		p->mins[n-1] = INT32_MAX;
+		p->maxs[n-1] = INT32_MIN;
+	}
+}
+
+static int spa_bt_decode_buffer_init(struct spa_bt_decode_buffer *this, struct spa_log *log,
+		uint32_t frame_size, uint32_t rate, uint32_t quantum_limit, uint32_t reserve)
+{
+	spa_zero(*this);
+	this->frame_size = frame_size;
+	this->rate = rate;
+	this->log = log;
+	this->buffer_reserve = this->frame_size * reserve;
+	this->buffer_size = this->frame_size * quantum_limit * 2;
+	this->buffer_size += this->buffer_reserve;
+	this->corr = 1.0;
+	this->buffering = true;
+
+	spa_bt_rate_control_init(&this->ctl, 0);
+
+	spa_bt_ptp_init(&this->spike, (uint64_t)this->rate * BUFFERING_LONG_MSEC / 1000);
+	spa_bt_ptp_init(&this->packet_size, (uint64_t)this->rate * BUFFERING_SHORT_MSEC / 1000);
+
+	if ((this->buffer_decoded = malloc(this->buffer_size)) == NULL) {
+		this->buffer_size = 0;
+		return -ENOMEM;
+	}
+	return 0;
+}
+
+static void spa_bt_decode_buffer_clear(struct spa_bt_decode_buffer *this)
+{
+	free(this->buffer_decoded);
+	spa_zero(*this);
+}
+
+static void spa_bt_decode_buffer_compact(struct spa_bt_decode_buffer *this)
+{
+	uint32_t avail;
+
+	spa_assert(this->read_index <= this->write_index);
+
+	if (this->read_index == this->write_index) {
+		this->read_index = 0;
+		this->write_index = 0;
+		goto done;
+	}
+
+	if (this->write_index > this->read_index + this->buffer_size - this->buffer_reserve) {
+		/* Drop data to keep buffer reserve free */
+		spa_log_info(this->log, "%p buffer overrun: dropping data", this);
+		this->read_index = this->write_index + this->buffer_reserve - this->buffer_size;
+	}
+
+	if (this->write_index < (this->buffer_size - this->buffer_reserve) / 2
+			|| this->read_index == 0)
+		goto done;
+
+	avail = this->write_index - this->read_index;
+	spa_memmove(this->buffer_decoded,
+			SPA_PTROFF(this->buffer_decoded, this->read_index, void),
+			avail);
+	this->read_index = 0;
+	this->write_index = avail;
+
+done:
+	spa_assert(this->buffer_size - this->write_index >= this->buffer_reserve);
+}
+
+static void *spa_bt_decode_buffer_get_write(struct spa_bt_decode_buffer *this, uint32_t *avail)
+{
+	spa_bt_decode_buffer_compact(this);
+	spa_assert(this->buffer_size >= this->write_index);
+	*avail = this->buffer_size - this->write_index;
+	return SPA_PTROFF(this->buffer_decoded, this->write_index, void);
+}
+
+static void spa_bt_decode_buffer_write_packet(struct spa_bt_decode_buffer *this, uint32_t size)
+{
+	spa_assert(size % this->frame_size == 0);
+	this->write_index += size;
+	this->received = true;
+	spa_bt_ptp_update(&this->packet_size, size / this->frame_size, size / this->frame_size);
+}
+
+static void *spa_bt_decode_buffer_get_read(struct spa_bt_decode_buffer *this, uint32_t *avail)
+{
+	spa_assert(this->write_index >= this->read_index);
+	if (!this->buffering)
+		*avail = this->write_index - this->read_index;
+	else
+		*avail = 0;
+	return SPA_PTROFF(this->buffer_decoded, this->read_index, void);
+}
+
+static void spa_bt_decode_buffer_read(struct spa_bt_decode_buffer *this, uint32_t size)
+{
+	spa_assert(size % this->frame_size == 0);
+	this->read_index += size;
+}
+
+static void spa_bt_decode_buffer_recover(struct spa_bt_decode_buffer *this)
+{
+	int32_t size = (this->write_index - this->read_index) / this->frame_size;
+	int32_t level;
+
+	this->prev_avail = size * this->frame_size;
+	this->prev_consumed = this->prev_duration;
+
+	level = (int32_t)this->prev_avail/this->frame_size
+		- (int32_t)this->prev_duration;
+	this->corr = 1.0;
+
+	spa_bt_rate_control_init(&this->ctl, level);
+}
+
+static void spa_bt_decode_buffer_process(struct spa_bt_decode_buffer *this, uint32_t samples, uint32_t duration)
+{
+	const uint32_t data_size = samples * this->frame_size;
+	const int32_t packet_size = SPA_CLAMP(this->packet_size.max, 0, INT32_MAX/8);
+	const int32_t max_level = SPA_MAX(8 * packet_size, (int32_t)duration);
+	uint32_t avail;
+
+	if (SPA_UNLIKELY(duration != this->prev_duration)) {
+		this->prev_duration = duration;
+		spa_bt_decode_buffer_recover(this);
+	}
+
+	if (SPA_UNLIKELY(this->buffering)) {
+		int32_t size = (this->write_index - this->read_index) / this->frame_size;
+
+		this->corr = 1.0;
+
+		spa_log_trace(this->log, "%p buffering size:%d", this, (int)size);
+
+		if (this->received &&
+				packet_size > 0 &&
+				size >= SPA_MAX(3*packet_size, (int32_t)duration))
+			this->buffering = false;
+		else
+			return;
+
+		spa_bt_decode_buffer_recover(this);
+	}
+
+	spa_bt_decode_buffer_get_read(this, &avail);
+
+	if (this->received) {
+		const uint32_t avg_period = (uint64_t)this->rate * BUFFERING_SHORT_MSEC / 1000;
+		int32_t level, target;
+
+		/* Track buffer level */
+		level = (int32_t)(this->prev_avail/this->frame_size) - (int32_t)this->prev_consumed;
+		level = SPA_MAX(level, -max_level);
+		this->prev_consumed = SPA_MIN(this->prev_consumed, avg_period);
+
+		spa_bt_ptp_update(&this->spike, this->ctl.avg - level, this->prev_consumed);
+
+		/* Update target level */
+		target = BUFFERING_TARGET(this->spike.max, packet_size);
+
+		if (level > SPA_MAX(4 * target, 2*(int32_t)duration) &&
+				avail > data_size) {
+			/* Lagging too much: drop data */
+			uint32_t size = SPA_MIN(avail - data_size,
+					(level - target*5/2) * this->frame_size);
+
+			spa_bt_decode_buffer_read(this, size);
+			spa_log_trace(this->log, "%p overrun samples:%d level:%d target:%d",
+					this, (int)size/this->frame_size,
+					(int)level, (int)target);
+
+			spa_bt_decode_buffer_recover(this);
+		}
+
+		this->pos += this->prev_consumed;
+		if (this->pos > this->rate) {
+			spa_log_debug(this->log,
+					"%p avg:%d target:%d level:%d buffer:%d spike:%d corr:%f",
+					this,
+					(int)this->ctl.avg,
+					(int)target,
+					(int)level,
+					(int)(avail / this->frame_size),
+					(int)this->spike.max,
+					(double)this->corr);
+			this->pos = 0;
+		}
+
+		this->corr = spa_bt_rate_control_update(&this->ctl,
+				level, target, this->prev_consumed, avg_period);
+
+		spa_bt_decode_buffer_get_read(this, &avail);
+
+		this->prev_consumed = 0;
+		this->prev_avail = avail;
+		this->underrun = 0;
+		this->received = false;
+	}
+
+	if (avail < data_size) {
+		spa_log_trace(this->log, "%p underrun samples:%d", this,
+				(data_size - avail) / this->frame_size);
+		this->underrun += samples;
+		if (this->underrun >= SPA_MIN((uint32_t)max_level, this->buffer_size / this->frame_size)) {
+			this->buffering = true;
+			spa_log_debug(this->log, "%p underrun too much: start buffering", this);
+		}
+	}
+
+	this->prev_consumed += samples;
+}
+
+#endif
diff --git a/spa/plugins/bluez5/defs.h b/spa/plugins/bluez5/defs.h
index 2bdace5d3e9590edd554ca7eb4e0b5cbf7c4edc9..90348afa05f1805507adb52451241fce75105a46 100644
--- a/spa/plugins/bluez5/defs.h
+++ b/spa/plugins/bluez5/defs.h
@@ -134,13 +134,13 @@ extern "C" {
 
 #define BLUEZ_ERROR_NOT_SUPPORTED "org.bluez.Error.NotSupported"
 
-#define SPA_BT_UUID_A2DP_SOURCE "0000110A-0000-1000-8000-00805F9B34FB"
-#define SPA_BT_UUID_A2DP_SINK   "0000110B-0000-1000-8000-00805F9B34FB"
-#define SPA_BT_UUID_HSP_HS      "00001108-0000-1000-8000-00805F9B34FB"
-#define SPA_BT_UUID_HSP_HS_ALT  "00001131-0000-1000-8000-00805F9B34FB"
-#define SPA_BT_UUID_HSP_AG      "00001112-0000-1000-8000-00805F9B34FB"
-#define SPA_BT_UUID_HFP_HF      "0000111E-0000-1000-8000-00805F9B34FB"
-#define SPA_BT_UUID_HFP_AG      "0000111F-0000-1000-8000-00805F9B34FB"
+#define SPA_BT_UUID_A2DP_SOURCE "0000110a-0000-1000-8000-00805f9b34fb"
+#define SPA_BT_UUID_A2DP_SINK   "0000110b-0000-1000-8000-00805f9b34fb"
+#define SPA_BT_UUID_HSP_HS      "00001108-0000-1000-8000-00805f9b34fb"
+#define SPA_BT_UUID_HSP_HS_ALT  "00001131-0000-1000-8000-00805f9b34fb"
+#define SPA_BT_UUID_HSP_AG      "00001112-0000-1000-8000-00805f9b34fb"
+#define SPA_BT_UUID_HFP_HF      "0000111e-0000-1000-8000-00805f9b34fb"
+#define SPA_BT_UUID_HFP_AG      "0000111f-0000-1000-8000-00805f9b34fb"
 
 #define PROFILE_HSP_AG	"/Profile/HSPAG"
 #define PROFILE_HSP_HS	"/Profile/HSPHS"
@@ -490,8 +490,8 @@ int spa_bt_device_add_profile(struct spa_bt_device *device, enum spa_bt_profile
 int spa_bt_device_connect_profile(struct spa_bt_device *device, enum spa_bt_profile profile);
 int spa_bt_device_check_profiles(struct spa_bt_device *device, bool force);
 int spa_bt_device_ensure_a2dp_codec(struct spa_bt_device *device, const struct a2dp_codec * const *codecs);
-bool spa_bt_device_supports_a2dp_codec(struct spa_bt_device *device, const struct a2dp_codec *codec);
-const struct a2dp_codec **spa_bt_device_get_supported_a2dp_codecs(struct spa_bt_device *device, size_t *count);
+bool spa_bt_device_supports_a2dp_codec(struct spa_bt_device *device, const struct a2dp_codec *codec, bool sink);
+const struct a2dp_codec **spa_bt_device_get_supported_a2dp_codecs(struct spa_bt_device *device, size_t *count, bool sink);
 int spa_bt_device_ensure_hfp_codec(struct spa_bt_device *device, unsigned int codec);
 int spa_bt_device_supports_hfp_codec(struct spa_bt_device *device, unsigned int codec);
 int spa_bt_device_release_transports(struct spa_bt_device *device);
diff --git a/spa/plugins/bluez5/meson.build b/spa/plugins/bluez5/meson.build
index 03f986b37249096b2ee37c67f3d5ea438301b914..2a17c78ee7cdd74b391d6f35d0622a604b755f8b 100644
--- a/spa/plugins/bluez5/meson.build
+++ b/spa/plugins/bluez5/meson.build
@@ -111,13 +111,23 @@ if ldac_dep.found()
 endif
 
 if get_option('bluez5-codec-lc3plus').allowed() and lc3plus_dep.found()
-  lc3plus_args = codec_args
-  lc3plus_dep = [ lc3plus_dep ]
   bluez_codec_lc3plus = shared_library('spa-codec-bluez5-lc3plus',
     [ 'a2dp-codec-lc3plus.c', 'a2dp-codecs.c' ],
     include_directories : [ configinc ],
-    c_args : ldac_args,
+    c_args : codec_args,
     dependencies : [ spa_dep, lc3plus_dep, mathlib ],
     install : true,
     install_dir : spa_plugindir / 'bluez5')
 endif
+
+if get_option('bluez5-codec-opus').allowed() and opus_dep.found()
+  opus_args = codec_args
+  opus_dep = [ opus_dep ]
+  bluez_codec_opus = shared_library('spa-codec-bluez5-opus',
+    [ 'a2dp-codec-opus.c', 'a2dp-codecs.c' ],
+    include_directories : [ configinc ],
+    c_args : opus_args,
+    dependencies : [ spa_dep, opus_dep, mathlib ],
+    install : true,
+    install_dir : spa_plugindir / 'bluez5')
+endif
diff --git a/spa/plugins/bluez5/sco-io.c b/spa/plugins/bluez5/sco-io.c
index 0b399e910f22c64c51b607e50325643cbe2e6cfc..06577507677842838ea6ddaa718665e514608368 100644
--- a/spa/plugins/bluez5/sco-io.c
+++ b/spa/plugins/bluez5/sco-io.c
@@ -55,16 +55,12 @@
  * since kernel might not report it as the socket MTU, see
  * https://lore.kernel.org/linux-bluetooth/20201210003528.3pmaxvubiwegxmhl@pali/T/
  *
- * Since 24 is the packet size for the smallest setting (ALT1), we'll stop
- * reading when rx packet of at least this size is seen, and use its size as the
- * heuristic maximum write MTU. Of course, if we have a source connected, we'll
- * continue reading without stopping.
+ * We continue reading also when there's no source connected, to keep socket
+ * flushed.
  *
  * XXX: when the kernel/backends start giving the right values, the heuristic
  * XXX: can be removed
  */
-#define HEURISTIC_MIN_MTU 24
-
 #define MAX_MTU 1024
 
 
@@ -94,12 +90,6 @@ static void update_source(struct spa_bt_sco_io *io)
 	int enabled;
 	int changed = 0;
 
-	enabled = io->source_cb != NULL || io->read_size < HEURISTIC_MIN_MTU;
-	if (SPA_FLAG_IS_SET(io->source.mask, SPA_IO_IN) != enabled) {
-		SPA_FLAG_UPDATE(io->source.mask, SPA_IO_IN, enabled);
-		changed = 1;
-	}
-
 	enabled = io->sink_cb != NULL;
 	if (SPA_FLAG_IS_SET(io->source.mask, SPA_IO_OUT) != enabled) {
 		SPA_FLAG_UPDATE(io->source.mask, SPA_IO_OUT, enabled);
@@ -118,11 +108,6 @@ static void sco_io_on_ready(struct spa_source *source)
 	if (SPA_FLAG_IS_SET(source->rmask, SPA_IO_IN)) {
 		int res;
 
-		/*
-		 * Note that we will read from the socket for a few times even
-		 * when there is no source callback, to autodetect packet size.
-		 */
-
 	read_again:
 		res = read(io->fd, io->read_buffer, SPA_MIN(io->read_mtu, MAX_MTU));
 		if (res <= 0) {
diff --git a/spa/plugins/bluez5/sco-sink.c b/spa/plugins/bluez5/sco-sink.c
index 909a14928910a39f94a39aa4d5e65905296132c5..d92bada2af852540f132759f84a24c27cd1302f0 100644
--- a/spa/plugins/bluez5/sco-sink.c
+++ b/spa/plugins/bluez5/sco-sink.c
@@ -759,6 +759,7 @@ static void emit_node_info(struct impl *this, bool full)
 		{ SPA_KEY_MEDIA_CLASS, "Stream/Input/Audio" },
 		{ "media.name", ((this->transport && this->transport->device->name) ?
 		                 this->transport->device->name : "HSP/HFP") },
+		{ SPA_KEY_MEDIA_ROLE, "Communication" },
 	};
 	bool is_ag = this->transport &&
 		(this->transport->profile & SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY);
@@ -1146,8 +1147,8 @@ static int impl_node_process(void *object)
 	spa_return_val_if_fail(this != NULL, -EINVAL);
 
 	port = &this->port;
-	io = port->io;
-	spa_return_val_if_fail(io != NULL, -EIO);
+	if ((io = port->io) == NULL)
+		return -EIO;
 
 	if (io->status == SPA_STATUS_HAVE_DATA && io->buffer_id < port->n_buffers) {
 		struct buffer *b = &port->buffers[io->buffer_id];
diff --git a/spa/plugins/bluez5/sco-source.c b/spa/plugins/bluez5/sco-source.c
index 1819029b83250a15e38bbd54fb96fa55c16bb0a6..52a1d27cce5eac79b64dd67214a93b52b0424033 100644
--- a/spa/plugins/bluez5/sco-source.c
+++ b/spa/plugins/bluez5/sco-source.c
@@ -58,11 +58,11 @@ static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.source.sco"
 #undef SPA_LOG_TOPIC_DEFAULT
 #define SPA_LOG_TOPIC_DEFAULT &log_topic
 
+#include "decode-buffer.h"
+
 #define DEFAULT_CLOCK_NAME	"clock.system.monotonic"
 
 struct props {
-	uint32_t min_latency;
-	uint32_t max_latency;
 	char clock_name[64];
 };
 
@@ -101,8 +101,7 @@ struct port {
 	struct spa_list free;
 	struct spa_list ready;
 
-	struct buffer *current_buffer;
-	uint32_t ready_offset;
+	struct spa_bt_decode_buffer buffer;
 };
 
 struct impl {
@@ -116,6 +115,8 @@ struct impl {
 	struct spa_hook_list hooks;
 	struct spa_callbacks callbacks;
 
+	uint32_t quantum_limit;
+
 	uint64_t info_all;
 	struct spa_node_info info;
 #define IDX_PropInfo	0
@@ -132,10 +133,18 @@ struct impl {
 
 	unsigned int started:1;
 	unsigned int following:1;
+	unsigned int matching:1;
+	unsigned int resampling:1;
+
+	struct spa_source timer_source;
+	int timerfd;
 
 	struct spa_io_clock *clock;
 	struct spa_io_position *position;
 
+	uint64_t current_time;
+	uint64_t next_time;
+
 	/* mSBC */
 	sbc_t msbc;
 	bool msbc_seq_initialized;
@@ -150,13 +159,8 @@ struct impl {
 
 #define CHECK_PORT(this,d,p)	((d) == SPA_DIRECTION_OUTPUT && (p) == 0)
 
-static const uint32_t default_min_latency = 128;
-static const uint32_t default_max_latency = 512;
-
 static void reset_props(struct props *props)
 {
-	props->min_latency = default_min_latency;
-	props->max_latency = default_max_latency;
 	strncpy(props->clock_name, DEFAULT_CLOCK_NAME, sizeof(props->clock_name));
 }
 
@@ -184,23 +188,7 @@ static int impl_node_enum_params(void *object, int seq,
 	switch (id) {
 	case SPA_PARAM_PropInfo:
 	{
-		struct props *p = &this->props;
-
 		switch (result.index) {
-		case 0:
-			param = spa_pod_builder_add_object(&b,
-				SPA_TYPE_OBJECT_PropInfo, id,
-				SPA_PROP_INFO_id,   SPA_POD_Id(SPA_PROP_minLatency),
-				SPA_PROP_INFO_description, SPA_POD_String("The minimum latency"),
-				SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Int(p->min_latency, 1, INT32_MAX));
-			break;
-		case 1:
-			param = spa_pod_builder_add_object(&b,
-				SPA_TYPE_OBJECT_PropInfo, id,
-				SPA_PROP_INFO_id,   SPA_POD_Id(SPA_PROP_maxLatency),
-				SPA_PROP_INFO_description, SPA_POD_String("The maximum latency"),
-				SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Int(p->max_latency, 1, INT32_MAX));
-			break;
 		default:
 			return 0;
 		}
@@ -208,15 +196,7 @@ static int impl_node_enum_params(void *object, int seq,
 	}
 	case SPA_PARAM_Props:
 	{
-		struct props *p = &this->props;
-
 		switch (result.index) {
-		case 0:
-			param = spa_pod_builder_add_object(&b,
-				SPA_TYPE_OBJECT_Props, id,
-				SPA_PROP_minLatency, SPA_POD_Int(p->min_latency),
-				SPA_PROP_maxLatency, SPA_POD_Int(p->max_latency));
-			break;
 		default:
 			return 0;
 		}
@@ -237,6 +217,41 @@ static int impl_node_enum_params(void *object, int seq,
 	return 0;
 }
 
+static int set_timeout(struct impl *this, uint64_t time)
+{
+       struct itimerspec ts;
+       ts.it_value.tv_sec = time / SPA_NSEC_PER_SEC;
+       ts.it_value.tv_nsec = time % SPA_NSEC_PER_SEC;
+       ts.it_interval.tv_sec = 0;
+       ts.it_interval.tv_nsec = 0;
+       return spa_system_timerfd_settime(this->data_system,
+		       this->timerfd, SPA_FD_TIMER_ABSTIME, &ts, NULL);
+}
+
+static int set_timers(struct impl *this)
+{
+       struct timespec now;
+
+       spa_system_clock_gettime(this->data_system, CLOCK_MONOTONIC, &now);
+       this->next_time = SPA_TIMESPEC_TO_NSEC(&now);
+
+       return set_timeout(this, this->following ? 0 : this->next_time);
+}
+
+static int do_reassign_follower(struct spa_loop *loop,
+			bool async,
+			uint32_t seq,
+			const void *data,
+			size_t size,
+			void *user_data)
+{
+	struct impl *this = user_data;
+	struct port *port = &this->port;
+
+	spa_bt_decode_buffer_recover(&port->buffer);
+	return 0;
+}
+
 static inline bool is_following(struct impl *this)
 {
 	return this->position && this->clock && this->position->clock.id != this->clock->id;
@@ -269,6 +284,7 @@ static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size)
 	if (this->started && following != this->following) {
 		spa_log_debug(this->log, "%p: reassign follower %d->%d", this, this->following, following);
 		this->following = following;
+		spa_loop_invoke(this->data_loop, do_reassign_follower, 0, NULL, 0, true, this);
 	}
 
 	return 0;
@@ -284,10 +300,7 @@ static int apply_props(struct impl *this, const struct spa_pod *param)
 	if (param == NULL) {
 		reset_props(&new_props);
 	} else {
-		spa_pod_parse_object(param,
-				SPA_TYPE_OBJECT_Props, NULL,
-				SPA_PROP_minLatency, SPA_POD_OPT_Int(&new_props.min_latency),
-				SPA_PROP_maxLatency, SPA_POD_OPT_Int(&new_props.max_latency));
+		/* noop */
 	}
 
 	changed = (memcmp(&new_props, &this->props, sizeof(struct props)) != 0);
@@ -326,8 +339,6 @@ static void reset_buffers(struct port *port)
 	spa_list_init(&port->free);
 	spa_list_init(&port->ready);
 
-	port->current_buffer = NULL;
-
 	for (i = 0; i < port->n_buffers; i++) {
 		struct buffer *b = &port->buffers[i];
 		spa_list_append(&port->free, &b->link);
@@ -422,116 +433,107 @@ static bool is_zero_packet(uint8_t *data, int size)
 	return true;
 }
 
-static void preprocess_and_decode_msbc_data(void *userdata, uint8_t *read_data, int size_read)
+static uint32_t preprocess_and_decode_msbc_data(void *userdata, uint8_t *read_data, int size_read)
 {
 	struct impl *this = userdata;
 	struct port *port = &this->port;
-	struct spa_data *datas = port->current_buffer->buf->datas;
+	uint32_t decoded = 0;
+	int i;
 
 	spa_log_trace(this->log, "handling mSBC data");
 
-	/* check if the packet contains only zeros - if so ignore the packet.
-	   This is necessary, because some kernels insert bogus "all-zero" packets
-	   into the datastream.
-	   See https://gitlab.freedesktop.org/pipewire/pipewire/-/issues/549 */
-	if (is_zero_packet(read_data, size_read)) {
-		return;
-	}
+	/*
+	 * Check if the packet contains only zeros - if so ignore the packet.
+	 * This is necessary, because some kernels insert bogus "all-zero" packets
+	 * into the datastream.
+	 * See https://gitlab.freedesktop.org/pipewire/pipewire/-/issues/549
+	 */
+	if (is_zero_packet(read_data, size_read))
+		return 0;
 
-	int i;
 	for (i = 0; i < size_read; ++i) {
+		void *buf;
+		uint32_t avail;
+		int seq, processed;
+		size_t written;
+
 		msbc_buffer_append_byte(this, read_data[i]);
 
-		/* Handle found mSBC packets.
-		 *
-		 * XXX: if there's no space for the decoded audio in
-		 * XXX: the current buffer, we'll drop data.
+		if (this->msbc_buffer_pos != MSBC_ENCODED_SIZE)
+			continue;
+
+		/*
+		 * Handle found mSBC packet
 		 */
-		if (this->msbc_buffer_pos == MSBC_ENCODED_SIZE) {
-			spa_log_trace(this->log, "Received full mSBC packet, start processing it");
-
-			if (port->ready_offset + MSBC_DECODED_SIZE <= datas[0].maxsize) {
-				int seq, processed;
-				size_t written;
-				spa_log_trace(this->log,
-					"Output buffer has space, processing mSBC packet");
-
-				/* Check sequence number */
-				seq = ((this->msbc_buffer[1] >> 4) & 1) |
-				      ((this->msbc_buffer[1] >> 6) & 2);
-
-				spa_log_trace(this->log, "mSBC packet seq=%u", seq);
-				if (!this->msbc_seq_initialized) {
-					this->msbc_seq_initialized = true;
-					this->msbc_seq = seq;
-				} else if (seq != this->msbc_seq) {
-					spa_log_info(this->log,
-						"missing mSBC packet: %u != %u", seq, this->msbc_seq);
-					this->msbc_seq = seq;
-					/* TODO: Implement PLC. */
-				}
-				this->msbc_seq = (this->msbc_seq + 1) % 4;
-
-				/* decode frame */
-				processed = sbc_decode(
-					&this->msbc, this->msbc_buffer + 2, MSBC_ENCODED_SIZE - 3,
-					(uint8_t *)datas[0].data + port->ready_offset, MSBC_DECODED_SIZE,
-					&written);
-
-				if (processed < 0) {
-					spa_log_warn(this->log, "sbc_decode failed: %d", processed);
-					/* TODO: manage errors */
-					continue;
-				}
-
-				port->ready_offset += written;
-
-			} else {
-				spa_log_warn(this->log, "Output buffer full, dropping mSBC packet");
-			}
+
+		buf = spa_bt_decode_buffer_get_write(&port->buffer, &avail);
+
+		/* Check sequence number */
+		seq = ((this->msbc_buffer[1] >> 4) & 1) |
+			((this->msbc_buffer[1] >> 6) & 2);
+
+		spa_log_trace(this->log, "mSBC packet seq=%u", seq);
+		if (!this->msbc_seq_initialized) {
+			this->msbc_seq_initialized = true;
+			this->msbc_seq = seq;
+		} else if (seq != this->msbc_seq) {
+			/* TODO: PLC (too late to insert data now) */
+			spa_log_info(this->log,
+					"missing mSBC packet: %u != %u", seq, this->msbc_seq);
+			this->msbc_seq = seq;
 		}
+
+		this->msbc_seq = (this->msbc_seq + 1) % 4;
+
+		if (avail < MSBC_DECODED_SIZE)
+			spa_log_warn(this->log, "Output buffer full, dropping msbc data");
+
+		/* decode frame */
+		processed = sbc_decode(
+			&this->msbc, this->msbc_buffer + 2, MSBC_ENCODED_SIZE - 3,
+					buf, avail, &written);
+
+		if (processed < 0) {
+			spa_log_warn(this->log, "sbc_decode failed: %d", processed);
+			/* TODO: manage errors */
+			continue;
+		}
+
+		spa_bt_decode_buffer_write_packet(&port->buffer, written);
+		decoded += written;
 	}
+
+	return decoded;
 }
 
 static int sco_source_cb(void *userdata, uint8_t *read_data, int size_read)
 {
 	struct impl *this = userdata;
 	struct port *port = &this->port;
-	struct spa_io_buffers *io = port->io;
-	struct spa_data *datas;
-	uint32_t min_data;
+	uint32_t decoded;
+	uint64_t dt;
 
 	if (this->transport == NULL) {
 		spa_log_debug(this->log, "no transport, stop reading");
 		goto stop;
 	}
 
-	/* get buffer */
-	if (!port->current_buffer) {
-		if (spa_list_is_empty(&port->free)) {
-			spa_log_warn(this->log, "buffer not available");
-			return 0;
-		}
-		port->current_buffer = spa_list_first(&port->free, struct buffer, link);
-		spa_list_remove(&port->current_buffer->link);
-		port->ready_offset = 0;
-	}
-	datas = port->current_buffer->buf->datas;
-
 	/* update the current pts */
+	dt = SPA_TIMESPEC_TO_NSEC(&this->now);
 	spa_system_clock_gettime(this->data_system, CLOCK_MONOTONIC, &this->now);
+	dt = SPA_TIMESPEC_TO_NSEC(&this->now) - dt;
 
 	/* handle data read from socket */
-	spa_log_trace(this->log, "read socket data %d", size_read);
 #if 0
 	hexdump_to_log(this, read_data, size_read);
 #endif
 
 	if (this->transport->codec == HFP_AUDIO_CODEC_MSBC) {
-		preprocess_and_decode_msbc_data(userdata, read_data, size_read);
-
+		decoded = preprocess_and_decode_msbc_data(userdata, read_data, size_read);
 	} else {
+		uint32_t avail;
 		uint8_t *packet;
+
 		if (size_read != 48 && is_zero_packet(read_data, size_read)) {
 			/* Adapter is returning non-standard CVSD stream. For example
 			 * Intel 8087:0029 at Firmware revision 0.0 build 191 week 21 2021
@@ -539,62 +541,101 @@ static int sco_source_cb(void *userdata, uint8_t *read_data, int size_read)
 			 */
 			return 0;
 		}
-		packet = (uint8_t *)datas[0].data + port->ready_offset;
-		spa_memmove(packet, read_data, size_read);
-		port->ready_offset += size_read;
+
+		if (size_read % port->frame_size != 0) {
+			/* Unaligned data: reception or adapter problem.
+			 * Consider the whole packet lost and report.
+			 */
+			spa_log_debug(this->log,
+					"received bad Bluetooth SCO CVSD packet");
+			return 0;
+		}
+
+		packet = spa_bt_decode_buffer_get_write(&port->buffer, &avail);
+		avail = SPA_MIN(avail, (uint32_t)size_read);
+		spa_memmove(packet, read_data, avail);
+		spa_bt_decode_buffer_write_packet(&port->buffer, avail);
+
+		decoded = avail;
 	}
 
-	/* send buffer if full */
-	min_data = SPA_MIN(this->props.min_latency * port->frame_size, datas[0].maxsize / 2);
-	if (port->ready_offset >= min_data) {
-		uint64_t sample_count;
+	spa_log_trace(this->log, "read socket data size:%d decoded frames:%d dt:%d dms",
+			size_read, decoded / port->frame_size,
+			(int)(dt / 100000));
 
-		datas[0].chunk->offset = 0;
-		datas[0].chunk->size = port->ready_offset;
-		datas[0].chunk->stride = port->frame_size;
+	return 0;
 
-		sample_count = datas[0].chunk->size / port->frame_size;
-		spa_list_append(&port->ready, &port->current_buffer->link);
-		port->current_buffer = NULL;
-
-		if (!this->following && this->clock) {
-			this->clock->nsec = SPA_TIMESPEC_TO_NSEC(&this->now);
-			this->clock->duration = sample_count * this->clock->rate.denom / port->current_format.info.raw.rate;
-			this->clock->position += this->clock->duration;
-			this->clock->delay = 0;
-			this->clock->rate_diff = 1.0f;
-			this->clock->next_nsec = this->clock->nsec;
-		}
+stop:
+	return 1;
+}
+
+static int setup_matching(struct impl *this)
+{
+	struct port *port = &this->port;
+
+	if (this->position && port->rate_match) {
+		port->rate_match->rate = 1 / port->buffer.corr;
+
+		this->matching = this->following;
+		this->resampling = this->matching ||
+			(port->current_format.info.raw.rate != this->position->clock.rate.denom);
+	} else {
+		this->matching = false;
+		this->resampling = false;
 	}
 
-	/* done if there are no buffers ready */
-	if (spa_list_is_empty(&port->ready))
-		return 0;
+	if (port->rate_match)
+		SPA_FLAG_UPDATE(port->rate_match->flags, SPA_IO_RATE_MATCH_FLAG_ACTIVE, this->matching);
 
-	if (this->following)
-		return 0;
+	return 0;
+}
+
+static void sco_on_timeout(struct spa_source *source)
+{
+	struct impl *this = source->data;
+	struct port *port = &this->port;
+	uint64_t exp, duration;
+	uint32_t rate;
+	struct spa_io_buffers *io = port->io;
+	uint64_t prev_time, now_time;
+
+	if (this->transport == NULL)
+		return;
+
+	if (this->started && spa_system_timerfd_read(this->data_system, this->timerfd, &exp) < 0)
+		spa_log_warn(this->log, "error reading timerfd: %s", strerror(errno));
+
+	prev_time = this->current_time;
+	now_time = this->current_time = this->next_time;
 
-	/* process the buffer if IO does not have any */
-	if (io->status != SPA_STATUS_HAVE_DATA) {
-		struct buffer *b;
+	spa_log_trace(this->log, "%p: timer %"PRIu64" %"PRIu64"", this,
+			now_time, now_time - prev_time);
 
-		if (io->buffer_id < port->n_buffers)
-			recycle_buffer(this, port, io->buffer_id);
+	if (SPA_LIKELY(this->position)) {
+		duration = this->position->clock.duration;
+		rate = this->position->clock.rate.denom;
+	} else {
+		duration = 1024;
+		rate = 48000;
+	}
+
+	setup_matching(this);
 
-		b = spa_list_first(&port->ready, struct buffer, link);
-		spa_list_remove(&b->link);
-		b->outstanding = true;
+	this->next_time = now_time + duration * SPA_NSEC_PER_SEC / port->buffer.corr / rate;
 
-		io->buffer_id = b->id;
-		io->status = SPA_STATUS_HAVE_DATA;
+	if (SPA_LIKELY(this->clock)) {
+		this->clock->nsec = now_time;
+		this->clock->position += duration;
+		this->clock->duration = duration;
+		this->clock->rate_diff = port->buffer.corr;
+		this->clock->next_nsec = this->next_time;
 	}
 
-	/* notify ready */
+	spa_log_trace(this->log, "%p: %d", this, io->status);
+	io->status = SPA_STATUS_HAVE_DATA;
 	spa_node_call_ready(&this->callbacks, SPA_STATUS_HAVE_DATA);
-	return 0;
 
-stop:
-	return 1;
+	set_timeout(this, this->next_time);
 }
 
 static int do_add_source(struct spa_loop *loop,
@@ -613,6 +654,7 @@ static int do_add_source(struct spa_loop *loop,
 
 static int do_start(struct impl *this)
 {
+	struct port *port = &this->port;
 	bool do_accept;
 	int res;
 
@@ -636,7 +678,13 @@ static int do_start(struct impl *this)
 		return res;
 
 	/* Reset the buffers and sample count */
-	reset_buffers(&this->port);
+	reset_buffers(port);
+
+	spa_bt_decode_buffer_clear(&port->buffer);
+	if ((res = spa_bt_decode_buffer_init(&port->buffer, this->log,
+			port->frame_size, port->current_format.info.raw.rate,
+			this->quantum_limit, this->quantum_limit)) < 0)
+		return res;
 
 	/* Init mSBC if needed */
 	if (this->transport->codec == HFP_AUDIO_CODEC_MSBC) {
@@ -653,6 +701,17 @@ static int do_start(struct impl *this)
 		goto fail;
 	spa_loop_invoke(this->data_loop, do_add_source, 0, NULL, 0, true, this);
 
+	/* Start timer */
+	this->timer_source.data = this;
+	this->timer_source.fd = this->timerfd;
+	this->timer_source.func = sco_on_timeout;
+	this->timer_source.mask = SPA_IO_IN;
+	this->timer_source.rmask = 0;
+	spa_loop_add_source(this->data_loop, &this->timer_source);
+
+	setup_matching(this);
+	set_timers(this);
+
 	/* Set the started flag */
 	this->started = true;
 
@@ -671,15 +730,25 @@ static int do_remove_source(struct spa_loop *loop,
 			    void *user_data)
 {
 	struct impl *this = user_data;
+	struct itimerspec ts;
 
 	if (this->transport && this->transport->sco_io)
 		spa_bt_sco_io_set_source_cb(this->transport->sco_io, NULL, NULL);
 
+	if (this->timer_source.loop)
+		spa_loop_remove_source(this->data_loop, &this->timer_source);
+	ts.it_value.tv_sec = 0;
+	ts.it_value.tv_nsec = 0;
+	ts.it_interval.tv_sec = 0;
+	ts.it_interval.tv_nsec = 0;
+	spa_system_timerfd_settime(this->data_system, this->timerfd, 0, &ts, NULL);
+
 	return 0;
 }
 
 static int do_stop(struct impl *this)
 {
+	struct port *port = &this->port;
 	int res = 0;
 
 	if (!this->started)
@@ -696,6 +765,8 @@ static int do_stop(struct impl *this)
 		res = spa_bt_transport_release(this->transport);
 	}
 
+	spa_bt_decode_buffer_clear(&port->buffer);
+
 	return res;
 }
 
@@ -738,14 +809,12 @@ static void emit_node_info(struct impl *this, bool full)
 		{ SPA_KEY_MEDIA_CLASS, "Audio/Source" },
 		{ SPA_KEY_NODE_DRIVER, "true" },
 	};
-
-	char latency[64] = "128/8000";
 	const struct spa_dict_item ag_node_info_items[] = {
 		{ SPA_KEY_DEVICE_API, "bluez5" },
 		{ SPA_KEY_MEDIA_CLASS, "Stream/Output/Audio" },
-		{ SPA_KEY_NODE_LATENCY, latency },
 		{ "media.name", ((this->transport && this->transport->device->name) ?
 					this->transport->device->name : "HSP/HFP") },
+		{ SPA_KEY_MEDIA_ROLE, "Communication" },
 	};
 	bool is_ag = this->transport && (this->transport->profile & SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY);
 	uint64_t old = full ? this->info.change_mask : 0;
@@ -753,9 +822,6 @@ static void emit_node_info(struct impl *this, bool full)
 	if (full)
 		this->info.change_mask = this->info_all;
 	if (this->info.change_mask) {
-		if (this->transport && this->port.have_format)
-			snprintf(latency, sizeof(latency), "%d/%d", (int)this->props.min_latency,
-					(int)this->port.current_format.info.raw.rate);
 		this->info.props = is_ag ?
 			&SPA_DICT_INIT_ARRAY(ag_node_info_items) :
 			&SPA_DICT_INIT_ARRAY(hu_node_info_items);
@@ -902,11 +968,11 @@ impl_node_port_enum_params(void *object, int seq,
 
 		param = spa_pod_builder_add_object(&b,
 			SPA_TYPE_OBJECT_ParamBuffers, id,
-			SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(8, 8, MAX_BUFFERS),
+			SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(2, 1, MAX_BUFFERS),
 			SPA_PARAM_BUFFERS_blocks,  SPA_POD_Int(1),
 			SPA_PARAM_BUFFERS_size,    SPA_POD_CHOICE_RANGE_Int(
-							this->props.max_latency * port->frame_size,
-							this->props.min_latency * port->frame_size,
+							this->quantum_limit * port->frame_size,
+							16 * port->frame_size,
 							INT32_MAX),
 			SPA_PARAM_BUFFERS_stride,  SPA_POD_Int(port->frame_size));
 		break;
@@ -976,7 +1042,6 @@ static int clear_buffers(struct impl *this, struct port *port)
 		spa_list_init(&port->ready);
 		port->n_buffers = 0;
 	}
-	port->current_buffer = NULL;
 	return 0;
 }
 
@@ -1147,6 +1212,79 @@ static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t
 	return 0;
 }
 
+static uint32_t get_samples(struct impl *this, uint32_t *duration)
+{
+	struct port *port = &this->port;
+	uint32_t samples;
+
+	if (SPA_LIKELY(port->rate_match) && this->resampling) {
+		samples = port->rate_match->size;
+	} else {
+		if (SPA_LIKELY(this->position))
+			samples = this->position->clock.duration * port->current_format.info.raw.rate
+				/ this->position->clock.rate.denom;
+		else
+			samples = 1024;
+	}
+
+	if (SPA_LIKELY(this->position))
+		*duration = this->position->clock.duration * port->current_format.info.raw.rate
+			/ this->position->clock.rate.denom;
+	else if (SPA_LIKELY(this->clock))
+		*duration = this->clock->duration * port->current_format.info.raw.rate
+			/ this->clock->rate.denom;
+	else
+		*duration = 1024 * port->current_format.info.raw.rate / 48000;
+
+	return samples;
+}
+
+static void process_buffering(struct impl *this)
+{
+	struct port *port = &this->port;
+	uint32_t duration;
+	const uint32_t samples = get_samples(this, &duration);
+	void *buf;
+	uint32_t avail;
+
+	spa_bt_decode_buffer_process(&port->buffer, samples, duration);
+
+	setup_matching(this);
+
+	buf = spa_bt_decode_buffer_get_read(&port->buffer, &avail);
+
+	/* copy data to buffers */
+	if (!spa_list_is_empty(&port->free) && avail > 0) {
+		struct buffer *buffer;
+		struct spa_data *datas;
+		uint32_t data_size;
+
+		data_size = samples * port->frame_size;
+
+		avail = SPA_MIN(avail, data_size);
+
+		spa_bt_decode_buffer_read(&port->buffer, avail);
+
+		buffer = spa_list_first(&port->free, struct buffer, link);
+		spa_list_remove(&buffer->link);
+
+		spa_log_trace(this->log, "dequeue %d", buffer->id);
+
+		datas = buffer->buf->datas;
+
+		spa_assert(datas[0].maxsize >= data_size);
+
+		datas[0].chunk->offset = 0;
+		datas[0].chunk->size = avail;
+		datas[0].chunk->stride = port->frame_size;
+		memcpy(datas[0].data, buf, avail);
+
+		/* ready buffer if full */
+		spa_log_trace(this->log, "queue %d frames:%d", buffer->id, (int)avail / port->frame_size);
+		spa_list_append(&port->ready, &buffer->link);
+	}
+}
+
 static int impl_node_process(void *object)
 {
 	struct impl *this = object;
@@ -1157,8 +1295,8 @@ static int impl_node_process(void *object)
 	spa_return_val_if_fail(this != NULL, -EINVAL);
 
 	port = &this->port;
-	io = port->io;
-	spa_return_val_if_fail(io != NULL, -EIO);
+	if ((io = port->io) == NULL)
+		return -EIO;
 
 	/* Return if we already have a buffer */
 	if (io->status == SPA_STATUS_HAVE_DATA)
@@ -1170,6 +1308,9 @@ static int impl_node_process(void *object)
 		io->buffer_id = SPA_ID_INVALID;
 	}
 
+	/* Produce data */
+	process_buffering(this);
+
 	/* Return if there are no buffers ready to be processed */
 	if (spa_list_is_empty(&port->ready))
 		return SPA_STATUS_OK;
@@ -1252,6 +1393,8 @@ static int impl_clear(struct spa_handle *handle)
 	struct impl *this = (struct impl *) handle;
 	if (this->transport)
 		spa_hook_remove(&this->transport_listener);
+	spa_system_close(this->data_system, this->timerfd);
+	spa_bt_decode_buffer_clear(&this->port.buffer);
 	return 0;
 }
 
@@ -1341,6 +1484,10 @@ impl_init(const struct spa_handle_factory *factory,
 	spa_list_init(&port->ready);
 	spa_list_init(&port->free);
 
+	this->quantum_limit = 8192;
+	if (info && (str = spa_dict_lookup(info, "clock.quantum-limit")))
+		spa_atou32(str, &this->quantum_limit, 0);
+
 	if (info && (str = spa_dict_lookup(info, SPA_KEY_API_BLUEZ5_TRANSPORT)))
 		sscanf(str, "pointer:%p", &this->transport);
 
@@ -1351,6 +1498,9 @@ impl_init(const struct spa_handle_factory *factory,
 	spa_bt_transport_add_listener(this->transport,
 			&this->transport_listener, &transport_events, this);
 
+	this->timerfd = spa_system_timerfd_create(this->data_system,
+			CLOCK_MONOTONIC, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK);
+
 	return 0;
 }
 
diff --git a/spa/plugins/control/mixer.c b/spa/plugins/control/mixer.c
index 29c5376137ab0bdd66d7b48b93ebec30ac3c5c95..81a3bd5191cab114c76fe5b8fb0c8ddac84aabc9 100644
--- a/spa/plugins/control/mixer.c
+++ b/spa/plugins/control/mixer.c
@@ -587,8 +587,8 @@ static int impl_node_process(void *object)
 	spa_return_val_if_fail(this != NULL, -EINVAL);
 
 	outport = GET_OUT_PORT(this, 0);
-	outio = outport->io;
-	spa_return_val_if_fail(outio != NULL, -EIO);
+	if ((outio = outport->io) == NULL)
+		return -EIO;
 
 	spa_log_trace_fp(this->log, NAME " %p: status %p %d %d",
 			this, outio, outio->status, outio->buffer_id);
diff --git a/spa/plugins/ffmpeg/ffmpeg-dec.c b/spa/plugins/ffmpeg/ffmpeg-dec.c
index 245e3a8720234bd20d9b7efebaa411810e041277..e9c4c1e3013deb9123da8035808157dc7a33adad 100644
--- a/spa/plugins/ffmpeg/ffmpeg-dec.c
+++ b/spa/plugins/ffmpeg/ffmpeg-dec.c
@@ -38,6 +38,8 @@
 #include <spa/param/video/format.h>
 #include <spa/pod/filter.h>
 
+#include "ffmpeg.h"
+
 #define IS_VALID_PORT(this,d,id)	((id) == 0)
 #define GET_IN_PORT(this,p)		(&this->in_ports[p])
 #define GET_OUT_PORT(this,p)		(&this->out_ports[p])
@@ -456,6 +458,20 @@ impl_get_interface(struct spa_handle *handle, const char *type, void **interface
 	return 0;
 }
 
+static int
+impl_clear(struct spa_handle *handle)
+{
+	spa_return_val_if_fail(handle != NULL, -EINVAL);
+
+	return 0;
+}
+
+size_t
+spa_ffmpeg_dec_get_size(const struct spa_handle_factory *factory, const struct spa_dict *params)
+{
+	return sizeof(struct impl);
+}
+
 int
 spa_ffmpeg_dec_init(struct spa_handle *handle,
 		    const struct spa_dict *info,
@@ -466,6 +482,7 @@ spa_ffmpeg_dec_init(struct spa_handle *handle,
 	struct port *port;
 
 	handle->get_interface = impl_get_interface;
+	handle->clear = impl_clear;
 
 	this = (struct impl *) handle;
 
diff --git a/spa/plugins/ffmpeg/ffmpeg-enc.c b/spa/plugins/ffmpeg/ffmpeg-enc.c
index b9f2f277b4a9fecfed617dcbd4ded76ef04cf14e..621818386f4578f3597edc7a4fff447b745b6700 100644
--- a/spa/plugins/ffmpeg/ffmpeg-enc.c
+++ b/spa/plugins/ffmpeg/ffmpeg-enc.c
@@ -37,6 +37,8 @@
 #include <spa/param/video/format-utils.h>
 #include <spa/pod/filter.h>
 
+#include "ffmpeg.h"
+
 #define IS_VALID_PORT(this,d,id)	((id) == 0)
 #define GET_IN_PORT(this,p)		(&this->in_ports[p])
 #define GET_OUT_PORT(this,p)		(&this->out_ports[p])
@@ -435,6 +437,20 @@ impl_get_interface(struct spa_handle *handle, const char *type, void **interface
 	return 0;
 }
 
+static int
+impl_clear(struct spa_handle *handle)
+{
+	spa_return_val_if_fail(handle != NULL, -EINVAL);
+
+	return 0;
+}
+
+size_t
+spa_ffmpeg_enc_get_size(const struct spa_handle_factory *factory, const struct spa_dict *params)
+{
+	return sizeof(struct impl);
+}
+
 int
 spa_ffmpeg_enc_init(struct spa_handle *handle,
 		    const struct spa_dict *info,
@@ -444,6 +460,7 @@ spa_ffmpeg_enc_init(struct spa_handle *handle,
 	struct port *port;
 
 	handle->get_interface = impl_get_interface;
+	handle->clear = impl_clear;
 
 	this = (struct impl *) handle;
 
diff --git a/spa/plugins/ffmpeg/ffmpeg.c b/spa/plugins/ffmpeg/ffmpeg.c
index 39ec93e7113ef7cd0463086713da8dc94a8a9acb..9546dae2bf50e7621d96ee60d32029725aac8d6f 100644
--- a/spa/plugins/ffmpeg/ffmpeg.c
+++ b/spa/plugins/ffmpeg/ffmpeg.c
@@ -30,10 +30,7 @@
 
 #include <libavcodec/avcodec.h>
 
-int spa_ffmpeg_dec_init(struct spa_handle *handle, const struct spa_dict *info,
-			const struct spa_support *support, uint32_t n_support);
-int spa_ffmpeg_enc_init(struct spa_handle *handle, const struct spa_dict *info,
-			const struct spa_support *support, uint32_t n_support);
+#include "ffmpeg.h"
 
 static int
 ffmpeg_dec_init(const struct spa_handle_factory *factory,
@@ -130,8 +127,12 @@ static const AVCodec *find_codec_by_index(uint32_t index)
 SPA_EXPORT
 int spa_handle_factory_enum(const struct spa_handle_factory **factory, uint32_t *index)
 {
-	static struct spa_handle_factory f;
 	static char name[128];
+	static struct spa_handle_factory f = {
+		SPA_VERSION_HANDLE_FACTORY,
+		.name = name,
+		.enum_interface_info = ffmpeg_enum_interface_info,
+	};
 
 #if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(58, 10, 100)
 	avcodec_register_all();
@@ -144,16 +145,14 @@ int spa_handle_factory_enum(const struct spa_handle_factory **factory, uint32_t
 
 	if (av_codec_is_encoder(c)) {
 		snprintf(name, sizeof(name), "encoder.%s", c->name);
+		f.get_size = spa_ffmpeg_enc_get_size;
 		f.init = ffmpeg_enc_init;
 	} else {
 		snprintf(name, sizeof(name), "decoder.%s", c->name);
+		f.get_size = spa_ffmpeg_dec_get_size;
 		f.init = ffmpeg_dec_init;
 	}
 
-	f.name = name;
-	f.info = NULL;
-	f.enum_interface_info = ffmpeg_enum_interface_info;
-
 	*factory = &f;
 	(*index)++;
 
diff --git a/spa/plugins/ffmpeg/ffmpeg.h b/spa/plugins/ffmpeg/ffmpeg.h
new file mode 100644
index 0000000000000000000000000000000000000000..19078d813e0a44117bce7b6db35125095b5a5718
--- /dev/null
+++ b/spa/plugins/ffmpeg/ffmpeg.h
@@ -0,0 +1,20 @@
+#ifndef SPA_FFMPEG_H
+#define SPA_FFMPEG_H
+
+#include <stdint.h>
+#include <stddef.h>
+
+struct spa_dict;
+struct spa_handle;
+struct spa_support;
+struct spa_handle_factory;
+
+int spa_ffmpeg_dec_init(struct spa_handle *handle, const struct spa_dict *info,
+			const struct spa_support *support, uint32_t n_support);
+int spa_ffmpeg_enc_init(struct spa_handle *handle, const struct spa_dict *info,
+			const struct spa_support *support, uint32_t n_support);
+
+size_t spa_ffmpeg_dec_get_size(const struct spa_handle_factory *factory, const struct spa_dict *params);
+size_t spa_ffmpeg_enc_get_size(const struct spa_handle_factory *factory, const struct spa_dict *params);
+
+#endif
diff --git a/spa/plugins/libcamera/libcamera-device.cpp b/spa/plugins/libcamera/libcamera-device.cpp
index 7f006a0cc04e86a050b6f2561d3b47fe6e32200a..f2f10c01785fb4b0267c2a4065392d218dae86a5 100644
--- a/spa/plugins/libcamera/libcamera-device.cpp
+++ b/spa/plugins/libcamera/libcamera-device.cpp
@@ -78,35 +78,32 @@ struct impl {
 	std::shared_ptr<Camera> camera;
 };
 
-std::string cameraModel(const Camera *camera)
+static std::string cameraModel(const Camera *camera)
 {
 	const ControlList &props = camera->properties();
-	std::string name;
-	if (props.contains(properties::Model))
-		name = props.get(properties::Model);
-	else
-		name = camera->id();
-        return name;
+
+	if (auto model = props.get(properties::Model))
+		return std::move(model.value());
+
+	return camera->id();
 }
 
-std::string cameraLoc(const Camera *camera)
+static const char *cameraLoc(const Camera *camera)
 {
 	const ControlList &props = camera->properties();
-	std::string location;
-	if (props.contains(properties::Location)) {
-		switch (props.get(properties::Location)) {
+
+	if (auto location = props.get(properties::Location)) {
+		switch (location.value()) {
 		case properties::CameraLocationFront:
-			location = "front";
-			break;
+			return "front";
 		case properties::CameraLocationBack:
-			location = "back";
-			break;
+			return "back";
 		case properties::CameraLocationExternal:
-			location = "external";
-			break;
+			return "external";
 		}
 	}
-	return location;
+
+	return nullptr;
 }
 
 static int emit_info(struct impl *impl, bool full)
@@ -116,7 +113,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], location[10], model[256], name[256];
+	char path[256], model[256], name[256];
 
 	info = SPA_DEVICE_INFO_INIT();
 
@@ -127,9 +124,11 @@ static int emit_info(struct impl *impl, bool full)
 	ADD_ITEM(SPA_KEY_OBJECT_PATH, path);
 	ADD_ITEM(SPA_KEY_DEVICE_API, "libcamera");
 	ADD_ITEM(SPA_KEY_MEDIA_CLASS, "Video/Device");
-	ADD_ITEM(SPA_KEY_API_LIBCAMERA_PATH, (char *)impl->props.device);
-	snprintf(location, sizeof(location), "%s", cameraLoc(impl->camera.get()).c_str());
-	ADD_ITEM(SPA_KEY_API_LIBCAMERA_LOCATION, location);
+	ADD_ITEM(SPA_KEY_API_LIBCAMERA_PATH, impl->props.device);
+
+	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);
diff --git a/spa/plugins/libcamera/libcamera-source.cpp b/spa/plugins/libcamera/libcamera-source.cpp
index 948b4413654ce76a6c434e4aee2fe022298a4318..0d32b745b72c399c423169e8af553847d1cfcbd7 100644
--- a/spa/plugins/libcamera/libcamera-source.cpp
+++ b/spa/plugins/libcamera/libcamera-source.cpp
@@ -823,8 +823,8 @@ static int impl_node_process(void *object)
 	spa_return_val_if_fail(impl != NULL, -EINVAL);
 
 	port = GET_OUT_PORT(impl, 0);
-	io = port->io;
-	spa_return_val_if_fail(io != NULL, -EIO);
+	if ((io = port->io) == NULL)
+		return -EIO;
 
 	if (port->control)
 		process_control(impl, &port->control->sequence);
diff --git a/spa/plugins/meson.build b/spa/plugins/meson.build
index fcf007aa9efae4ec5d9193431676bf7fe755f541..6df9381701bacec30f1c895cf9d2d6192f9ac91f 100644
--- a/spa/plugins/meson.build
+++ b/spa/plugins/meson.build
@@ -1,6 +1,9 @@
 if alsa_dep.found()
   subdir('alsa')
 endif
+if get_option('avb').require(host_machine.system() == 'linux', error_message: 'AVB support is only available on Linux').allowed()
+  subdir('avb')
+endif
 if get_option('audioconvert').allowed()
   subdir('audioconvert')
 endif
@@ -52,4 +55,4 @@ if libcamera_dep.found()
   subdir('libcamera')
 endif
 
-subdir('aec')
\ No newline at end of file
+subdir('aec')
diff --git a/spa/plugins/support/cpu.c b/spa/plugins/support/cpu.c
index 01cff4854d8a27776ac372d39345c5b009a4e6f7..67440af16f7d0868f7732bb950b22a25b3c6fa1e 100644
--- a/spa/plugins/support/cpu.c
+++ b/spa/plugins/support/cpu.c
@@ -30,7 +30,7 @@
 #include <sched.h>
 #include <fcntl.h>
 
-#ifdef __FreeBSD__
+#if defined(__FreeBSD__) || defined(__MidnightBSD__)
 #include <sys/sysctl.h>
 #endif
 
diff --git a/spa/plugins/support/logger.c b/spa/plugins/support/logger.c
index 4d308fa908bf93c32b8ef0c52aa6d1ba7f3c363b..3b854c65eff17f1da02401bf2ae78d8afedf3c2a 100644
--- a/spa/plugins/support/logger.c
+++ b/spa/plugins/support/logger.c
@@ -42,7 +42,7 @@
 
 #include "log-patterns.h"
 
-#ifdef __FreeBSD__
+#if defined(__FreeBSD__) || defined(__MidnightBSD__)
 #define CLOCK_MONOTONIC_RAW CLOCK_MONOTONIC
 #endif
 
diff --git a/spa/plugins/support/loop.c b/spa/plugins/support/loop.c
index e02cbed87958ad5b9638cd66783a0f3e1b243b04..758ce59db192332bf7d7a475f3f5fd38e265b166 100644
--- a/spa/plugins/support/loop.c
+++ b/spa/plugins/support/loop.c
@@ -350,6 +350,7 @@ static void loop_leave(void *object)
 
 	if (--impl->enter_count == 0) {
 		impl->thread = 0;
+		flush_items(impl);
 		impl->polling = false;
 	}
 }
diff --git a/spa/plugins/support/null-audio-sink.c b/spa/plugins/support/null-audio-sink.c
index 8a90d2ae540933732458e278a629429fbfae53f0..abee4bedd5f46c523e4e93f0523152acd031e0cc 100644
--- a/spa/plugins/support/null-audio-sink.c
+++ b/spa/plugins/support/null-audio-sink.c
@@ -42,6 +42,7 @@
 #include <spa/node/keys.h>
 #include <spa/param/audio/format-utils.h>
 #include <spa/debug/types.h>
+#include <spa/debug/mem.h>
 #include <spa/param/audio/type-info.h>
 #include <spa/param/param.h>
 #include <spa/pod/filter.h>
@@ -57,6 +58,7 @@ struct props {
 	uint32_t n_pos;
 	uint32_t pos[SPA_AUDIO_MAX_CHANNELS];
 	char clock_name[64];
+	unsigned int debug:1;
 };
 
 static void reset_props(struct props *props)
@@ -65,6 +67,7 @@ static void reset_props(struct props *props)
 	props->rate = 0;
 	props->n_pos = 0;
 	strncpy(props->clock_name, DEFAULT_CLOCK_NAME, sizeof(props->clock_name));
+	props->debug = false;
 }
 
 #define DEFAULT_CHANNELS	2
@@ -734,9 +737,8 @@ static int impl_node_process(void *object)
 	spa_return_val_if_fail(this != NULL, -EINVAL);
 
 	port = &this->port;
-
-	io = port->io;
-	spa_return_val_if_fail(io != NULL, -EIO);
+	if ((io = port->io) == NULL)
+		return -EIO;
 
 	if (io->status != SPA_STATUS_HAVE_DATA)
 		return io->status;
@@ -744,6 +746,20 @@ static int impl_node_process(void *object)
 		io->status = -EINVAL;
 		return io->status;
 	}
+	if (this->props.debug) {
+		struct buffer *b;
+		uint32_t i;
+
+		b = &port->buffers[io->buffer_id];
+		for (i = 0; i < b->outbuf->n_datas; i++) {
+			uint32_t offs, size;
+			struct spa_data *d = b->outbuf->datas;
+
+			offs = SPA_MIN(d->chunk->offset, d->maxsize);
+			size = SPA_MIN(d->maxsize - offs, d->chunk->size);
+			spa_debug_mem(i, SPA_PTROFF(d[i].data, offs, void), SPA_MIN(16u, size));;
+		}
+	}
 	io->status = SPA_STATUS_OK;
 	return SPA_STATUS_HAVE_DATA;
 }
diff --git a/spa/plugins/test/fakesink.c b/spa/plugins/test/fakesink.c
index fee62334b84ffa77ad49a1ad7271728260949bbb..104f27eb9da639e30c1d4f57ae7aaf6e277e4695 100644
--- a/spa/plugins/test/fakesink.c
+++ b/spa/plugins/test/fakesink.c
@@ -639,9 +639,8 @@ static int impl_node_process(void *object)
 	spa_return_val_if_fail(this != NULL, -EINVAL);
 
 	port = &this->port;
-
-	io = port->io;
-	spa_return_val_if_fail(io != NULL, -EIO);
+	if ((io = port->io) == NULL)
+		return -EIO;
 
 	if (io->status == SPA_STATUS_HAVE_DATA && io->buffer_id < port->n_buffers) {
 		struct buffer *b = &port->buffers[io->buffer_id];
diff --git a/spa/plugins/test/fakesrc.c b/spa/plugins/test/fakesrc.c
index fa7bee7e0a866f113fe485087313072998ec8cc4..6dde68455d1c56d68f7ed7e482a3c87f808f9b9f 100644
--- a/spa/plugins/test/fakesrc.c
+++ b/spa/plugins/test/fakesrc.c
@@ -680,8 +680,8 @@ static int impl_node_process(void *object)
 	spa_return_val_if_fail(this != NULL, -EINVAL);
 
 	port = &this->port;
-	io = port->io;
-	spa_return_val_if_fail(io != NULL, -EIO);
+	if ((io = port->io) == NULL)
+		return -EIO;
 
 	if (io->status == SPA_STATUS_HAVE_DATA)
 		return SPA_STATUS_HAVE_DATA;
diff --git a/spa/plugins/v4l2/v4l2-source.c b/spa/plugins/v4l2/v4l2-source.c
index e49568b1bd314a67dd6f006f2fb8587d0370a56a..ac83032a8b515f1fa03fd59efcf8d781b49d578b 100644
--- a/spa/plugins/v4l2/v4l2-source.c
+++ b/spa/plugins/v4l2/v4l2-source.c
@@ -879,8 +879,8 @@ static int impl_node_process(void *object)
 	spa_return_val_if_fail(this != NULL, -EINVAL);
 
 	port = GET_OUT_PORT(this, 0);
-	io = port->io;
-	spa_return_val_if_fail(io != NULL, -EIO);
+	if ((io = port->io) == NULL)
+		return -EIO;
 
 	if (port->control)
 		process_control(this, &port->control->sequence);
diff --git a/spa/plugins/v4l2/v4l2-udev.c b/spa/plugins/v4l2/v4l2-udev.c
index ff5433e08c8409cf82ef7cc7afd2c55c5ebe4329..df1b1f8906b85a9a5a8b2ef976a7ff9e4016058a 100644
--- a/spa/plugins/v4l2/v4l2-udev.c
+++ b/spa/plugins/v4l2/v4l2-udev.c
@@ -279,11 +279,10 @@ static int emit_object_info(struct impl *this, struct device *device)
 		items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_SUBSYSTEM, str);
 	}
 	if ((str = udev_device_get_property_value(dev, "ID_VENDOR_ID")) && *str) {
-		char *dec = alloca(6); /* 65535 is max */
 		int32_t val;
-
 		if (spa_atoi32(str, &val, 16)) {
-			snprintf(dec, 6, "%d", val);
+			char *dec = alloca(12); /* 0xffff is max */
+			snprintf(dec, 12, "0x%04x", val);
 			items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_VENDOR_ID, dec);
 		}
 	}
@@ -302,11 +301,10 @@ static int emit_object_info(struct impl *this, struct device *device)
 		items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_VENDOR_NAME, str);
 	}
 	if ((str = udev_device_get_property_value(dev, "ID_MODEL_ID")) && *str) {
-		char *dec = alloca(6); /* 65535 is max */
 		int32_t val;
-
 		if (spa_atoi32(str, &val, 16)) {
-			snprintf(dec, 6, "%d", val);
+			char *dec = alloca(12); /* 0xffff is max */
+			snprintf(dec, 12, "0x%04x", val);
 			items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_PRODUCT_ID, dec);
 		}
 	}
diff --git a/spa/plugins/v4l2/v4l2-utils.c b/spa/plugins/v4l2/v4l2-utils.c
index 80fa2bd8769efdc0c179b6d9df5f7b25759d9358..14e967f731b63fd9de6341da0bceb3c4be8ed0e1 100644
--- a/spa/plugins/v4l2/v4l2-utils.c
+++ b/spa/plugins/v4l2/v4l2-utils.c
@@ -1107,6 +1107,8 @@ spa_v4l2_enum_controls(struct impl *this, int seq,
 	spa_log_debug(this->log, "test control %08x", queryctrl.id);
 
 	if (query_ext_ctrl_ioctl(port, &queryctrl) != 0) {
+		if (errno == ENOTTY)
+			goto enum_end;
 		if (errno == EINVAL) {
 			if (queryctrl.id != next_fl)
 				goto enum_end;
@@ -1123,6 +1125,7 @@ spa_v4l2_enum_controls(struct impl *this, int seq,
 		}
 		res = -errno;
 		spa_log_error(this->log, "'%s' VIDIOC_QUERYCTRL: %m", this->props.device);
+		spa_v4l2_close(dev);
 		return res;
 	}
 	if (result.next & next_fl)
diff --git a/spa/plugins/videotestsrc/videotestsrc.c b/spa/plugins/videotestsrc/videotestsrc.c
index 2c7e9e1aee6986b1eb0ef0d1ac5ec39253605f48..97742975963b66e7d1a24e074d9e06cf17396b76 100644
--- a/spa/plugins/videotestsrc/videotestsrc.c
+++ b/spa/plugins/videotestsrc/videotestsrc.c
@@ -787,8 +787,8 @@ static int impl_node_process(void *object)
 	spa_return_val_if_fail(this != NULL, -EINVAL);
 
 	port = &this->port;
-	io = port->io;
-	spa_return_val_if_fail(io != NULL, -EIO);
+	if ((io = port->io) == NULL)
+		return -EIO;
 
 	if (io->status == SPA_STATUS_HAVE_DATA)
 		return SPA_STATUS_HAVE_DATA;
diff --git a/spa/plugins/volume/volume.c b/spa/plugins/volume/volume.c
index fb49ab74f370af9e8daa3377219cb1bbd6882b32..40556a33c6ffd1e4154cb6944d0501b38ba8a37a 100644
--- a/spa/plugins/volume/volume.c
+++ b/spa/plugins/volume/volume.c
@@ -679,8 +679,8 @@ static int impl_node_process(void *object)
 	spa_return_val_if_fail(this != NULL, -EINVAL);
 
 	out_port = GET_OUT_PORT(this, 0);
-	output = out_port->io;
-	spa_return_val_if_fail(output != NULL, -EIO);
+	if ((output = out_port->io) == NULL)
+		return -EIO;
 
 	if (output->status == SPA_STATUS_HAVE_DATA)
 		return SPA_STATUS_HAVE_DATA;
@@ -692,8 +692,8 @@ static int impl_node_process(void *object)
 	}
 
 	in_port = GET_IN_PORT(this, 0);
-	input = in_port->io;
-	spa_return_val_if_fail(input != NULL, -EIO);
+	if ((input = in_port->io) == NULL)
+		return -EIO;
 
 	if (input->status != SPA_STATUS_HAVE_DATA)
 		return SPA_STATUS_NEED_DATA;
diff --git a/spa/plugins/vulkan/vulkan-compute-filter.c b/spa/plugins/vulkan/vulkan-compute-filter.c
index fbe9e769e6ff41e23fb55f6df1414136cceabf2b..66157075a11789718a3dc7c628c2c3bfeae5fecf 100644
--- a/spa/plugins/vulkan/vulkan-compute-filter.c
+++ b/spa/plugins/vulkan/vulkan-compute-filter.c
@@ -578,8 +578,8 @@ static int impl_node_process(void *object)
 	spa_return_val_if_fail(this != NULL, -EINVAL);
 
 	inport = &this->port[SPA_DIRECTION_INPUT];
-	inio = inport->io;
-	spa_return_val_if_fail(inio != NULL, -EIO);
+	if ((inio = inport->io) == NULL)
+		return -EIO;
 
 	if (inio->status != SPA_STATUS_HAVE_DATA)
 		return inio->status;
@@ -590,8 +590,8 @@ static int impl_node_process(void *object)
 	}
 
 	outport = &this->port[SPA_DIRECTION_OUTPUT];
-	outio = outport->io;
-	spa_return_val_if_fail(outio != NULL, -EIO);
+	if ((outio = outport->io) == NULL)
+		return -EIO;
 
 	if (outio->status == SPA_STATUS_HAVE_DATA)
 		return SPA_STATUS_HAVE_DATA;
diff --git a/spa/plugins/vulkan/vulkan-compute-source.c b/spa/plugins/vulkan/vulkan-compute-source.c
index 3f26c9b24b3a3f272248cc591770317846eba2b5..8cce5489f6475570203f2497dfaa751866c8af16 100644
--- a/spa/plugins/vulkan/vulkan-compute-source.c
+++ b/spa/plugins/vulkan/vulkan-compute-source.c
@@ -802,8 +802,8 @@ static int impl_node_process(void *object)
 	spa_return_val_if_fail(this != NULL, -EINVAL);
 
 	port = &this->port;
-	io = port->io;
-	spa_return_val_if_fail(io != NULL, -EIO);
+	if ((io = port->io) == NULL)
+		return -EIO;
 
 	if (io->status == SPA_STATUS_HAVE_DATA)
 		return SPA_STATUS_HAVE_DATA;
diff --git a/spa/plugins/vulkan/vulkan-utils.c b/spa/plugins/vulkan/vulkan-utils.c
index ff9a51eb36bcfb78a39caaf4ae9070890bf839f4..ae3337b776d3698c1943a31557651b5ab9788cad 100644
--- a/spa/plugins/vulkan/vulkan-utils.c
+++ b/spa/plugins/vulkan/vulkan-utils.c
@@ -6,7 +6,7 @@
 #include <sys/mman.h>
 #include <fcntl.h>
 #include <string.h>
-#ifndef __FreeBSD__
+#if !defined(__FreeBSD__) && !defined(__MidnightBSD__)
 #include <alloca.h>
 #endif
 #include <errno.h>
diff --git a/spa/tests/stress-ringbuffer.c b/spa/tests/stress-ringbuffer.c
index 2c3f38af2eb062b54c4411a0b4ebe159b7b04037..6a7e98fb53c40cafc871d1e846e97ce2bf4471ac 100644
--- a/spa/tests/stress-ringbuffer.c
+++ b/spa/tests/stress-ringbuffer.c
@@ -11,10 +11,10 @@
 #define ARRAY_SIZE 63
 #define MAX_VALUE 0x10000
 
-#ifdef __FreeBSD__
+#if defined(__FreeBSD__) || defined(__MidnightBSD__)
 #include <sys/param.h>
 #if (__FreeBSD_version >= 1400000 && __FreeBSD_version < 1400043) \
-    || (__FreeBSD_version < 1300523)
+    || (__FreeBSD_version < 1300523) || defined(__MidnightBSD__)
 static int sched_getcpu(void) { return -1; };
 #endif
 #endif
diff --git a/spa/tools/spa-inspect.c b/spa/tools/spa-inspect.c
index 87dd9ff969777e1a422168fdd2e0b35f1ae07540..4e7910401eb6987ebb31fd3d11b7688e30dba4f6 100644
--- a/spa/tools/spa-inspect.c
+++ b/spa/tools/spa-inspect.c
@@ -232,7 +232,7 @@ static void inspect_factory(struct data *data, const struct spa_handle_factory *
 	if ((res =
 	     spa_handle_factory_init(factory, handle, NULL, data->support, data->n_support)) < 0) {
 		printf("can't make factory instance: %d\n", res);
-		return;
+		goto out;
 	}
 
 	printf("factory instance:\n");
@@ -256,6 +256,12 @@ static void inspect_factory(struct data *data, const struct spa_handle_factory *
 		else
 			printf("skipping unknown interface\n");
 	}
+
+	if ((res = spa_handle_clear(handle)) < 0)
+		printf("failed to clear handle: %s\n", spa_strerror(res));
+
+out:
+	free(handle);
 }
 
 static const struct spa_loop_methods impl_loop = {
diff --git a/src/daemon/client-rt.conf.in b/src/daemon/client-rt.conf.in
index 30bd11513d9b0998a88bb3f94c35dae3a6d5a120..f62b1e2281054af0e073362ff8ab4789645130ac 100644
--- a/src/daemon/client-rt.conf.in
+++ b/src/daemon/client-rt.conf.in
@@ -82,7 +82,7 @@ stream.properties = {
     #node.autoconnect      = true
     #resample.quality      = 4
     #channelmix.normalize  = false
-    #channelmix.mix-lfe    = true
+    #channelmix.mix-lfe    = false
     #channelmix.upmix      = true
     #channelmix.upmix-method = psd  # none, simple
     #channelmix.lfe-cutoff = 150
@@ -90,4 +90,5 @@ stream.properties = {
     #channelmix.rear-delay = 12.0
     #channelmix.stereo-widen = 0.0
     #channelmix.hilbert-taps = 0
+    #dither.noise = 0
 }
diff --git a/src/daemon/client.conf.in b/src/daemon/client.conf.in
index ffe23c82768a1a9f44bfebaff6c20e887f4f5303..3931149f7e842bcaf6f7af8556eeafd1cadbee79 100644
--- a/src/daemon/client.conf.in
+++ b/src/daemon/client.conf.in
@@ -7,6 +7,7 @@
 # @PIPEWIRE_CONFIG_DIR@/client.conf.d/ for system-wide changes or in
 # ~/.config/pipewire/client.conf.d/ for local changes.
 #
+
 context.properties = {
     ## Configure properties in the system.
     #mem.warn-mlock  = false
@@ -80,4 +81,5 @@ stream.properties = {
     #channelmix.rear-delay = 12.0
     #channelmix.stereo-widen = 0.0
     #channelmix.hilbert-taps = 0
+    #dither.noise = 0
 }
diff --git a/src/daemon/filter-chain.conf.in b/src/daemon/filter-chain.conf.in
new file mode 100644
index 0000000000000000000000000000000000000000..28d2a5ad3f7f7c719db2f832faff03512c3fb331
--- /dev/null
+++ b/src/daemon/filter-chain.conf.in
@@ -0,0 +1,62 @@
+# Filter-chain config file for PipeWire version @VERSION@ #
+#
+# This is a base config file for running filters.
+#
+# Place filter fragments in @PIPEWIRE_CONFIG_DIR@/filter-chain.conf.d/
+# for system-wide changes or in ~/.config/pipewire/filter-chain.conf.d/
+# for local changes.
+#
+# Run the filters with pipewire -c filter-chain.conf
+#
+
+context.properties = {
+    ## Configure properties in the system.
+    #mem.warn-mlock  = false
+    #mem.allow-mlock = true
+    #mem.mlock-all   = false
+    log.level        = 0
+}
+
+context.spa-libs = {
+    #<factory-name regex> = <library-name>
+    #
+    # Used to find spa factory names. It maps an spa factory name
+    # regular expression to a library name that should contain
+    # that factory.
+    #
+    audio.convert.* = audioconvert/libspa-audioconvert
+    support.*       = support/libspa-support
+}
+
+context.modules = [
+    #{ name = <module-name>
+    #    [ args  = { <key> = <value> ... } ]
+    #    [ flags = [ [ ifexists ] [ nofail ] ]
+    #}
+    #
+    # Loads a module with the given parameters.
+    # If ifexists is given, the module is ignored when it is not found.
+    # If nofail is given, module initialization failures are ignored.
+    #
+    # Uses realtime scheduling to boost the audio thread priorities
+    { name = libpipewire-module-rt
+        args = {
+            #rt.prio      = 88
+            #rt.time.soft = -1
+            #rt.time.hard = -1
+        }
+        flags = [ ifexists nofail ]
+    }
+
+    # The native communication protocol.
+    { name = libpipewire-module-protocol-native }
+
+    # Allows creating nodes that run in the context of the
+    # client. Is used by all clients that want to provide
+    # data to PipeWire.
+    { name = libpipewire-module-client-node }
+
+    # Makes a factory for wrapping nodes in an adapter with a
+    # converter and resampler.
+    { name = libpipewire-module-adapter }
+]
diff --git a/src/daemon/filter-chain/demonic.conf b/src/daemon/filter-chain/demonic.conf
index b2953dc8174259aac214895a3cbaa8bcd3462fda..df52dbafb8ec4ee51f2eaeff2f603fff16ff5402 100644
--- a/src/daemon/filter-chain/demonic.conf
+++ b/src/daemon/filter-chain/demonic.conf
@@ -1,46 +1,9 @@
 # filter-chain example config file for PipeWire version @VERSION@ #
-context.properties = {
-    ## Configure properties in the system.
-    #mem.warn-mlock  = false
-    #mem.allow-mlock = true
-    #mem.mlock-all   = false
-    log.level        = 0
-}
-
-context.spa-libs = {
-    #<factory-name regex> = <library-name>
-    #
-    # Used to find spa factory names. It maps an spa factory name
-    # regular expression to a library name that should contain
-    # that factory.
-    #
-    audio.convert.* = audioconvert/libspa-audioconvert
-    support.*       = support/libspa-support
-}
-
+#
+# Copy this file into a conf.d/ directory such as
+# ~/.config/pipewire/filter-chain.conf.d/
+#
 context.modules = [
-    # Uses realtime scheduling to boost the audio thread priorities
-    { name = libpipewire-module-rt
-        args = {
-            #rt.prio      = 88
-            #rt.time.soft = -1
-            #rt.time.hard = -1
-        }
-        flags = [ ifexists nofail ]
-    }
-
-    # The native communication protocol.
-    { name = libpipewire-module-protocol-native }
-
-    # Allows creating nodes that run in the context of the
-    # client. Is used by all clients that want to provide
-    # data to PipeWire.
-    { name = libpipewire-module-client-node }
-
-    # Makes a factory for wrapping nodes in an adapter with a
-    # converter and resampler.
-    { name = libpipewire-module-adapter }
-
     { name = libpipewire-module-filter-chain
         args = {
             #audio.format    = F32
diff --git a/src/daemon/filter-chain/meson.build b/src/daemon/filter-chain/meson.build
index c0540c1ff625b5b9a7265eb9affe9d193bbcc47d..91d4458687333ee1a118850a8c6f7ffa8b36ec0f 100644
--- a/src/daemon/filter-chain/meson.build
+++ b/src/daemon/filter-chain/meson.build
@@ -1,6 +1,8 @@
 conf_files = [
   [ 'demonic.conf', 'demonic.conf' ],
-  [ 'duplicate-FL.conf', 'duplicate-FL.conf' ],
+  [ 'source-duplicate-FL.conf', 'source-duplicate-FL.conf' ],
+  [ 'sink-mix-FL-FR.conf', 'sink-mix-FL-FR.conf' ],
+  [ 'sink-make-LFE.conf', 'sink-make-LFE.conf' ],
   [ 'sink-virtual-surround-5.1-kemar.conf', 'sink-virtual-surround-5.1-kemar.conf' ],
   [ 'sink-virtual-surround-7.1-hesuvi.conf', 'sink-virtual-surround-7.1-hesuvi.conf' ],
   [ 'sink-dolby-surround.conf', 'sink-dolby-surround.conf' ],
diff --git a/src/daemon/filter-chain/sink-dolby-surround.conf b/src/daemon/filter-chain/sink-dolby-surround.conf
index 78dd1572265de77f44bcb029513bc7de03e0f8d2..a53009f7cf56d17df9bcc6b6c285bb0e785be9ec 100644
--- a/src/daemon/filter-chain/sink-dolby-surround.conf
+++ b/src/daemon/filter-chain/sink-dolby-surround.conf
@@ -1,29 +1,9 @@
 # Dolby Surround encoder sink
 #
-# start with pipewire -c filter-chain/sink-dolby-surround.conf
+# Copy this file into a conf.d/ directory such as
+# ~/.config/pipewire/filter-chain.conf.d/
 #
-context.properties = {
-    log.level = 0
-}
-
-context.spa-libs = {
-    audio.convert.* = audioconvert/libspa-audioconvert
-    support.*       = support/libspa-support
-}
-
 context.modules = [
-    { name = libpipewire-module-rt
-        args = {
-            #rt.prio      = 88
-            #rt.time.soft = -1
-            #rt.time.hard = -1
-        }
-        flags = [ ifexists nofail ]
-    }
-    { name = libpipewire-module-protocol-native }
-    { name = libpipewire-module-client-node }
-    { name = libpipewire-module-adapter }
-
     { name = libpipewire-module-filter-chain
         args = {
             node.description = "Dolby Surround Sink"
diff --git a/src/daemon/filter-chain/sink-eq6.conf b/src/daemon/filter-chain/sink-eq6.conf
index cd671883a2c68131c81dc36a5ba667c77d811ac5..4cdf21b8964263870293ca446c624c9562cdd927 100644
--- a/src/daemon/filter-chain/sink-eq6.conf
+++ b/src/daemon/filter-chain/sink-eq6.conf
@@ -1,29 +1,9 @@
 # 6 band sink equalizer
 #
-# start with pipewire -c filter-chain/sink-eq6.conf
+# Copy this file into a conf.d/ directory such as
+# ~/.config/pipewire/filter-chain.conf.d/
 #
-context.properties = {
-    log.level = 0
-}
-
-context.spa-libs = {
-    audio.convert.* = audioconvert/libspa-audioconvert
-    support.*       = support/libspa-support
-}
-
 context.modules = [
-    { name = libpipewire-module-rt
-        args = {
-            #rt.prio      = 88
-            #rt.time.soft = -1
-            #rt.time.hard = -1
-        }
-        flags = [ ifexists nofail ]
-    }
-    { name = libpipewire-module-protocol-native }
-    { name = libpipewire-module-client-node }
-    { name = libpipewire-module-adapter }
-
     { name = libpipewire-module-filter-chain
         args = {
             node.description = "Equalizer Sink"
diff --git a/src/daemon/filter-chain/sink-make-LFE.conf b/src/daemon/filter-chain/sink-make-LFE.conf
new file mode 100644
index 0000000000000000000000000000000000000000..4ab770e38ecf8293a1aa1e79d2629f690e26e371
--- /dev/null
+++ b/src/daemon/filter-chain/sink-make-LFE.conf
@@ -0,0 +1,56 @@
+# An example filter chain that makes a stereo sink that mixes
+# the FL and FR channels to FL, FR, LFE
+#
+# Copy this file into a conf.d/ directory
+#
+context.modules = [
+    { name = libpipewire-module-filter-chain
+        args = {
+            node.description = "LFE example"
+            media.name       = "LFE example"
+            filter.graph = {
+                nodes = [
+                    { name = copyIL type = builtin label = copy }
+                    { name = copyOL type = builtin label = copy }
+                    { name = copyIR type = builtin label = copy }
+                    { name = copyOR type = builtin label = copy }
+                    {
+                        name   = mix
+                        type   = builtin
+                        label  = mixer
+                        control = {
+                          "Gain 1" = 0.5
+                          "Gain 2" = 0.5
+                        }
+                    }
+                    {
+                        type  = builtin
+                        name  = lpLFE
+                        label = bq_lowpass
+                        control = { "Freq" = 150.0 }
+                    }
+                ]
+                links = [
+                    { output = "copyIL:Out" input = "copyOL:In" }
+                    { output = "copyIR:Out" input = "copyOR:In" }
+                    { output = "copyIL:Out" input = "mix:In 1" }
+                    { output = "copyIR:Out" input = "mix:In 2" }
+                    { output = "mix:Out" input = "lpLFE:In" }
+                ]
+                inputs  = [ "copyIL:In" "copyIR:In" ]
+                outputs = [ "copyOL:Out" "copyOR:Out" "lpLFE:Out"]
+            }
+            capture.props = {
+                node.name         = "input_lfe"
+                audio.position    = [ FL FR ]
+                media.class       = "Audio/Sink"
+            }
+            playback.props = {
+                node.name         = "output_lfe"
+                audio.position    = [ FL FR LFE ]
+                stream.dont-remix = true
+                node.passive      = true
+            }
+        }
+    }
+]
diff --git a/src/daemon/filter-chain/sink-matrix-spatialiser.conf b/src/daemon/filter-chain/sink-matrix-spatialiser.conf
index 5519d3a672be2ddfba10e994adc1ea7f0f7b4076..b39890cb7dd53879214e5576b1f3fca979b974b2 100644
--- a/src/daemon/filter-chain/sink-matrix-spatialiser.conf
+++ b/src/daemon/filter-chain/sink-matrix-spatialiser.conf
@@ -1,30 +1,12 @@
 # Matrix Spatialiser sink
 #
-# start with pipewire -c filter-chain/sink-matrix-spatialiser.conf
+# Copy this file into a conf.d/ directory such as
+# ~/.config/pipewire/filter-chain.conf.d/
+#
 # ( Jean-Philippe Guillemin <hyp3ri0n@sfr.fr> )
-
-context.properties = {
-    log.level = 0
-}
-
-context.spa-libs = {
-    audio.convert.* = audioconvert/libspa-audioconvert
-    support.*       = support/libspa-support
-}
+#
 
 context.modules = [
-    { name = libpipewire-module-rt
-        args = {
-            #rt.prio      = 88
-            #rt.time.soft = -1
-            #rt.time.hard = -1
-        }
-        flags = [ ifexists nofail ]
-    }
-    { name = libpipewire-module-protocol-native }
-    { name = libpipewire-module-client-node }
-    { name = libpipewire-module-adapter }
-
     { name = libpipewire-module-filter-chain
         args = {
             node.description = "Matrix Spatialiser"
diff --git a/src/daemon/filter-chain/sink-mix-FL-FR.conf b/src/daemon/filter-chain/sink-mix-FL-FR.conf
new file mode 100644
index 0000000000000000000000000000000000000000..8288fad51858c93ca9e86289983f40183fe4f074
--- /dev/null
+++ b/src/daemon/filter-chain/sink-mix-FL-FR.conf
@@ -0,0 +1,40 @@
+# An example filter chain that makes a stereo sink that mixes
+# the FL and FR channels to a single FL channel
+#
+# Copy this file into a conf.d/ directory such as
+# ~/.config/pipewire/filter-chain.conf.d/
+#
+context.modules = [
+    { name = libpipewire-module-filter-chain
+        args = {
+            node.description = "Mix example"
+            media.name       = "Mix example"
+            filter.graph = {
+                nodes = [
+                    {
+                        name   = mix
+                        type   = builtin
+                        label  = mixer
+                        control = {
+                          "Gain 1" = 0.5
+                          "Gain 2" = 0.5
+                        }
+                    }
+                ]
+                inputs  = [ "mix:In 1" "mix:In 2" ]
+                outputs = [ "mix:Out" ]
+            }
+            capture.props = {
+                node.name         = "mix_input.mix-FL-FR-to-FL"
+                audio.position    = [ FL FR ]
+                media.class       = "Audio/Sink"
+            }
+            playback.props = {
+                node.name         = "mix_output.mix-FL-FR-to-FL"
+                audio.position    = [ FL ]
+                stream.dont-remix = true
+                node.passive      = true
+            }
+        }
+    }
+]
diff --git a/src/daemon/filter-chain/sink-virtual-surround-5.1-kemar.conf b/src/daemon/filter-chain/sink-virtual-surround-5.1-kemar.conf
index fa12ca4a542e98188c0bed5fb5ab66c8ee0163bb..ee8929b061292ac08841ff989cd907eea89fb802 100644
--- a/src/daemon/filter-chain/sink-virtual-surround-5.1-kemar.conf
+++ b/src/daemon/filter-chain/sink-virtual-surround-5.1-kemar.conf
@@ -1,29 +1,9 @@
 # Convolver sink
 #
-# start with pipewire -c filter-chain/sink-virtual-surround-5.1-kemar.conf
+# Copy this file into a conf.d/ directory such as
+# ~/.config/pipewire/filter-chain.conf.d/
 #
-context.properties = {
-    log.level = 0
-}
-
-context.spa-libs = {
-    audio.convert.* = audioconvert/libspa-audioconvert
-    support.*       = support/libspa-support
-}
-
 context.modules = [
-    { name = libpipewire-module-rt
-        args = {
-            #rt.prio      = 88
-            #rt.time.soft = -1
-            #rt.time.hard = -1
-        }
-        flags = [ ifexists nofail ]
-    }
-    { name = libpipewire-module-protocol-native }
-    { name = libpipewire-module-client-node }
-    { name = libpipewire-module-adapter }
-
     { name = libpipewire-module-filter-chain
         args = {
             node.description = "Virtual Surround Sink"
diff --git a/src/daemon/filter-chain/sink-virtual-surround-7.1-hesuvi.conf b/src/daemon/filter-chain/sink-virtual-surround-7.1-hesuvi.conf
index ee596a530da69261088fa1fc8e844453debebd5e..4aad3102cb16e630c3a6262cf7e1639f03a5b4d9 100644
--- a/src/daemon/filter-chain/sink-virtual-surround-7.1-hesuvi.conf
+++ b/src/daemon/filter-chain/sink-virtual-surround-7.1-hesuvi.conf
@@ -1,29 +1,9 @@
 # Convolver sink
 #
-# start with pipewire -c filter-chain/sink-virtual-surround-7.1-hesuvi.conf
+# Copy this file into a conf.d/ directory such as
+# ~/.config/pipewire/filter-chain.conf.d/
 #
-context.properties = {
-    log.level = 0
-}
-
-context.spa-libs = {
-    audio.convert.* = audioconvert/libspa-audioconvert
-    support.*       = support/libspa-support
-}
-
 context.modules = [
-    { name = libpipewire-module-rt
-        args = {
-            #rt.prio      = 88
-            #rt.time.soft = -1
-            #rt.time.hard = -1
-        }
-        flags = [ ifexists nofail ]
-    }
-    { name = libpipewire-module-protocol-native }
-    { name = libpipewire-module-client-node }
-    { name = libpipewire-module-adapter }
-
     { name = libpipewire-module-filter-chain
         args = {
             node.description = "Virtual Surround Sink"
diff --git a/src/daemon/filter-chain/duplicate-FL.conf b/src/daemon/filter-chain/source-duplicate-FL.conf
similarity index 72%
rename from src/daemon/filter-chain/duplicate-FL.conf
rename to src/daemon/filter-chain/source-duplicate-FL.conf
index 077f3bfacdea96a4c8e168e02d55f034e25a2c5f..7e0158f1834af757fae8efa9d1a5d392796ed50d 100644
--- a/src/daemon/filter-chain/duplicate-FL.conf
+++ b/src/daemon/filter-chain/source-duplicate-FL.conf
@@ -1,7 +1,8 @@
-# An example filter chain for duplicating the FL channel
+# An example filter chain that makes a source that duplicates the FL channel
 # to FL and FR.
 #
-# Copy this file into a conf.d/ directory
+# Copy this file into a conf.d/ directory such as
+# ~/.config/pipewire/filter-chain.conf.d/
 #
 context.modules = [
     { name = libpipewire-module-filter-chain
@@ -36,13 +37,15 @@ context.modules = [
                 outputs = [ "copyOL:Out" "copyOR:Out" ]
             }
             capture.props = {
-                node.name        = "remap_input.remap-FL-to-FL-FR"
-                audio.position  = [ FL ]
+                node.name         = "remap_input.remap-FL-to-FL-FR"
+                audio.position    = [ FL ]
                 stream.dont-remix = true
+                node.passive      = true
             }
             playback.props = {
-                node.name        = "remap_output.remap-FL-to-FL-FR"
-                audio.position  = [ FL FR ]
+                node.name         = "remap_output.remap-FL-to-FL-FR"
+                audio.position    = [ FL FR ]
+                media.class       = "Audio/Source"
             }
         }
     }
diff --git a/src/daemon/filter-chain/source-rnnoise.conf b/src/daemon/filter-chain/source-rnnoise.conf
index fba5e29645c46322597dd50cb170371d1f89147a..5babd8111aca690f1945ebcb837985ab8f8bbf49 100644
--- a/src/daemon/filter-chain/source-rnnoise.conf
+++ b/src/daemon/filter-chain/source-rnnoise.conf
@@ -1,29 +1,9 @@
 # Noise canceling source
 #
-# start with pipewire -c filter-chain/source-rnnoise.conf
+# Copy this file into a conf.d/ directory such as
+# ~/.config/pipewire/filter-chain.conf.d/
 #
-context.properties = {
-    log.level = 0
-}
-
-context.spa-libs = {
-    audio.convert.* = audioconvert/libspa-audioconvert
-    support.*       = support/libspa-support
-}
-
 context.modules = [
-    { name = libpipewire-module-rt
-        args = {
-            #rt.prio      = 88
-            #rt.time.soft = -1
-            #rt.time.hard = -1
-        }
-        flags = [ ifexists nofail ]
-    }
-    { name = libpipewire-module-protocol-native }
-    { name = libpipewire-module-client-node }
-    { name = libpipewire-module-adapter }
-
     { name = libpipewire-module-filter-chain
         args = {
             node.description = "Noise Canceling source"
diff --git a/src/daemon/jack.conf.in b/src/daemon/jack.conf.in
index 48b637c7297b19ec674c1f30c1660e461fd87529..e3e2dc790ed60171610ca82528022e75d1457aeb 100644
--- a/src/daemon/jack.conf.in
+++ b/src/daemon/jack.conf.in
@@ -81,7 +81,8 @@ jack.properties = {
      # ignore-all:      Ignore all self connect requests
      #jack.self-connect-mode = allow
      #jack.locked-process    = true
-     #jack.default-as-system    = false
+     #jack.default-as-system = false
+     #jack.fix-midi-events   = true
 }
 
 # client specific properties
diff --git a/src/daemon/meson.build b/src/daemon/meson.build
index a2e4c55e3768aa15b847608bc258eddcf304688d..5d5914e4f44f15f35308464a9bdb6f4a9883334a 100644
--- a/src/daemon/meson.build
+++ b/src/daemon/meson.build
@@ -67,9 +67,11 @@ conf_files = [
  'pipewire.conf',
  'client.conf',
  'client-rt.conf',
+ 'filter-chain.conf',
  'jack.conf',
  'minimal.conf',
  'pipewire-pulse.conf',
+ 'pipewire-avb.conf',
 ]
 
 foreach c : conf_files
@@ -99,6 +101,14 @@ executable('pipewire-pulse',
   dependencies : [ spa_dep, pipewire_dep, ],
 )
 
+executable('pipewire-avb',
+  pipewire_daemon_sources,
+  install: true,
+  c_args : pipewire_c_args,
+  include_directories : [ configinc ],
+  dependencies : [ spa_dep, pipewire_dep, ],
+)
+
 ln = find_program('ln')
 
 custom_target('pipewire-uninstalled',
diff --git a/src/daemon/minimal.conf.in b/src/daemon/minimal.conf.in
index 49227dddf9555b5963e9fd9c2882fee458cac77f..8de1f21e1f76677a1acd2ce078997470d2fb65c2 100644
--- a/src/daemon/minimal.conf.in
+++ b/src/daemon/minimal.conf.in
@@ -27,7 +27,7 @@ context.properties = {
 
     ## Properties for the DSP configuration.
     #default.clock.rate          = 48000
-    #default.clock.allowed-rates = [ 44100 48000 ]
+    #default.clock.allowed-rates = [ 48000 ]
     #default.clock.quantum       = 1024
     #default.clock.min-quantum   = 32
     #default.clock.max-quantum   = 2048
@@ -212,6 +212,7 @@ context.objects = [
             #channelmix.stereo-widen = 0.0
             #channelmix.hilbert-taps = 0
             channelmix.disable     = true
+            #dither.noise = 0
             #node.param.Props      = {
             #    params = [
             #        audio.channels 6
@@ -220,6 +221,7 @@ context.objects = [
             adapter.auto-port-config = {
                 mode = dsp
                 monitor = false
+                control = false
                 position = unknown   # aux, preserve
             }
             #node.param.PortConfig    = {
@@ -272,6 +274,7 @@ context.objects = [
             #channelmix.stereo-widen = 0.0
             #channelmix.hilbert-taps = 0
             channelmix.disable     = true
+            #dither.noise = 0
             #node.param.Props      = {
             #    params = [
             #        audio.format  S16
@@ -280,6 +283,7 @@ context.objects = [
             adapter.auto-port-config = {
                 mode = dsp
                 monitor = false
+                control = false
                 position = unknown   # aux, preserve
             }
             #node.param.PortConfig    = {
diff --git a/src/daemon/pipewire-avb.conf.in b/src/daemon/pipewire-avb.conf.in
new file mode 100644
index 0000000000000000000000000000000000000000..b4e465f07082948d767784c8ae3670567d4f177f
--- /dev/null
+++ b/src/daemon/pipewire-avb.conf.in
@@ -0,0 +1,73 @@
+# PulseAudio config file for PipeWire version @VERSION@ #
+#
+# Copy and edit this file in @PIPEWIRE_CONFIG_DIR@ for system-wide changes
+# or in ~/.config/pipewire for local changes.
+#
+# It is also possible to place a file with an updated section in
+# @PIPEWIRE_CONFIG_DIR@/pipewire-pulse.conf.d/ for system-wide changes or in
+# ~/.config/pipewire/pipewire-pulse.conf.d/ for local changes.
+#
+
+context.properties = {
+    ## Configure properties in the system.
+    #mem.warn-mlock  = false
+    #mem.allow-mlock = true
+    #mem.mlock-all   = false
+    #log.level       = 2
+
+    #default.clock.quantum-limit = 8192
+}
+
+context.spa-libs = {
+    audio.convert.* = audioconvert/libspa-audioconvert
+    support.*       = support/libspa-support
+}
+
+context.modules = [
+    { name = libpipewire-module-rt
+        args = {
+            nice.level   = -11
+            #rt.prio      = 88
+            #rt.time.soft = -1
+            #rt.time.hard = -1
+        }
+        flags = [ ifexists nofail ]
+    }
+    { name = libpipewire-module-protocol-native }
+    { name = libpipewire-module-client-node }
+    { name = libpipewire-module-adapter }
+    { name = libpipewire-module-avb
+        args = {
+	    # contents of avb.properties can also be placed here
+	    # to have config per server.
+        }
+    }
+]
+
+# Extra modules can be loaded here. Setup in default.pa can be moved here
+context.exec = [
+    #{ path = "pactl"        args = "load-module module-always-sink" }
+]
+
+stream.properties = {
+    #node.latency          = 1024/48000
+    #node.autoconnect      = true
+    #resample.quality      = 4
+    #channelmix.normalize  = false
+    #channelmix.mix-lfe    = false
+    #channelmix.upmix      = true
+    #channelmix.lfe-cutoff = 120
+    #channelmix.fc-cutoff  = 6000
+    #channelmix.rear-delay = 12.0
+    #channelmix.stereo-widen = 0.1
+    #channelmix.hilbert-taps = 0
+}
+
+avb.properties = {
+    # the addresses this server listens on
+    #ifname = "eth0.2"
+    ifname = "enp3s0"
+    # These overrides are only applied when running in a vm.
+    vm.overrides = {
+    }
+}
diff --git a/src/daemon/pipewire-pulse.conf.in b/src/daemon/pipewire-pulse.conf.in
index 3ab1528177942ce395d7509984bf83da519471f4..1ee3494a084b9f5f792330043c240c5e0f038ffb 100644
--- a/src/daemon/pipewire-pulse.conf.in
+++ b/src/daemon/pipewire-pulse.conf.in
@@ -66,6 +66,7 @@ stream.properties = {
     #channelmix.rear-delay = 12.0
     #channelmix.stereo-widen = 0.0
     #channelmix.hilbert-taps = 0
+    #dither.noise = 0
 }
 
 pulse.properties = {
diff --git a/src/daemon/pipewire.conf.in b/src/daemon/pipewire.conf.in
index 787e45f42004432be8f07473b9e005b95141e4d4..748c6173f036f2b4a1eaec4644ffd494b46b8bd5 100644
--- a/src/daemon/pipewire.conf.in
+++ b/src/daemon/pipewire.conf.in
@@ -27,7 +27,7 @@ context.properties = {
 
     ## Properties for the DSP configuration.
     #default.clock.rate          = 48000
-    #default.clock.allowed-rates = [ 44100 48000 ]
+    #default.clock.allowed-rates = [ 48000 ]
     #default.clock.quantum       = 1024
     default.clock.min-quantum   = 16
     #default.clock.max-quantum   = 2048
@@ -54,6 +54,7 @@ context.spa-libs = {
     # that factory.
     #
     audio.convert.* = audioconvert/libspa-audioconvert
+    avb.*           = avb/libspa-avb
     api.alsa.*      = alsa/libspa-alsa
     api.v4l2.*      = v4l2/libspa-v4l2
     api.libcamera.* = libcamera/libspa-libcamera
diff --git a/src/daemon/systemd/user/filter-chain.service.in b/src/daemon/systemd/user/filter-chain.service.in
new file mode 100644
index 0000000000000000000000000000000000000000..542cbd7f38218804b397a0b53107c5cbb114fae9
--- /dev/null
+++ b/src/daemon/systemd/user/filter-chain.service.in
@@ -0,0 +1,21 @@
+[Unit]
+Description=PipeWire filter chain daemon
+
+After=pipewire.service pipewire-session-manager.service
+BindsTo=pipewire.service
+
+[Service]
+LockPersonality=yes
+MemoryDenyWriteExecute=yes
+NoNewPrivileges=yes
+RestrictNamespaces=yes
+SystemCallArchitectures=native
+SystemCallFilter=@system-service
+Type=simple
+ExecStart=@PW_BINARY@ -c filter-chain.conf
+Restart=on-failure
+Slice=session.slice
+
+[Install]
+Also=pipewire.socket
+WantedBy=default.target
diff --git a/src/daemon/systemd/user/meson.build b/src/daemon/systemd/user/meson.build
index d17f3794f63b32e091c4f56e7c2b4be6228c0f36..10227629df3f5a3b8c013bfd59aeb11ee7e4cc51 100644
--- a/src/daemon/systemd/user/meson.build
+++ b/src/daemon/systemd/user/meson.build
@@ -20,3 +20,8 @@ configure_file(input : 'pipewire-pulse.service.in',
                output : 'pipewire-pulse.service',
                configuration : systemd_config,
                install_dir : systemd_user_services_dir)
+
+configure_file(input : 'filter-chain.service.in',
+               output : 'filter-chain.service',
+               configuration : systemd_config,
+               install_dir : systemd_user_services_dir)
diff --git a/src/examples/audio-src.c b/src/examples/audio-src.c
index f65f6148ae0e866377ef91e186e6e57e66ff05ba..121773219d910007ab84b5919eb2e53258311de8 100644
--- a/src/examples/audio-src.c
+++ b/src/examples/audio-src.c
@@ -120,6 +120,7 @@ int main(int argc, char *argv[])
 	struct data data = { 0, };
 	const struct spa_pod *params[1];
 	uint8_t buffer[1024];
+	struct pw_properties *props;
 	struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
 
 	pw_init(&argc, &argv);
@@ -142,14 +143,17 @@ int main(int argc, char *argv[])
 	 * you need to listen to is the process event where you need to produce
 	 * the data.
 	 */
+	props = pw_properties_new(PW_KEY_MEDIA_TYPE, "Audio",
+			PW_KEY_MEDIA_CATEGORY, "Playback",
+			PW_KEY_MEDIA_ROLE, "Music",
+			NULL);
+	if (argc > 1)
+		/* Set stream target if given on command line */
+		pw_properties_set(props, PW_KEY_TARGET_OBJECT, argv[1]);
 	data.stream = pw_stream_new_simple(
 			pw_main_loop_get_loop(data.loop),
 			"audio-src",
-			pw_properties_new(
-				PW_KEY_MEDIA_TYPE, "Audio",
-				PW_KEY_MEDIA_CATEGORY, "Playback",
-				PW_KEY_MEDIA_ROLE, "Music",
-				NULL),
+			props,
 			&stream_events,
 			&data);
 
@@ -165,7 +169,7 @@ int main(int argc, char *argv[])
 	 * called in a realtime thread. */
 	pw_stream_connect(data.stream,
 			  PW_DIRECTION_OUTPUT,
-			  argc > 1 ? (uint32_t)atoi(argv[1]) : PW_ID_ANY,
+			  PW_ID_ANY,
 			  PW_STREAM_FLAG_AUTOCONNECT |
 			  PW_STREAM_FLAG_MAP_BUFFERS |
 			  PW_STREAM_FLAG_RT_PROCESS,
diff --git a/src/examples/export-sink.c b/src/examples/export-sink.c
index 05d6380dfe1ce7368d7d559f7685cc63d062d3fb..9e0985475ec1591514135b7193be3c285c48c66f 100644
--- a/src/examples/export-sink.c
+++ b/src/examples/export-sink.c
@@ -472,7 +472,7 @@ static void make_node(struct data *data)
 
 	props = pw_properties_new(PW_KEY_NODE_AUTOCONNECT, "true", NULL);
 	if (data->path)
-		pw_properties_set(props, PW_KEY_NODE_TARGET, data->path);
+		pw_properties_set(props, PW_KEY_TARGET_OBJECT, data->path);
 	pw_properties_set(props, PW_KEY_MEDIA_CLASS, "Stream/Input/Video");
 	pw_properties_set(props, PW_KEY_MEDIA_TYPE, "Video");
 	pw_properties_set(props, PW_KEY_MEDIA_CATEGORY, "Capture");
diff --git a/src/examples/export-source.c b/src/examples/export-source.c
index 0310efb5b4fe9b8e601c33f792edb902e475b710..f84d01d9262d6e004ed984955707acf74fc0390d 100644
--- a/src/examples/export-source.c
+++ b/src/examples/export-source.c
@@ -483,7 +483,7 @@ static void make_node(struct data *data)
 				  PW_KEY_MEDIA_ROLE, "Music",
 				  NULL);
 	if (data->path)
-		pw_properties_set(props, PW_KEY_NODE_TARGET, data->path);
+		pw_properties_set(props, PW_KEY_TARGET_OBJECT, data->path);
 
 	data->impl_node.iface = SPA_INTERFACE_INIT(
 			SPA_TYPE_INTERFACE_Node,
diff --git a/src/examples/export-spa.c b/src/examples/export-spa.c
index 864555d1f583b3cd3b5f88d739a76f64223edd5a..e2f27cf12c58a5f500b55882f319e07f2db9a219 100644
--- a/src/examples/export-spa.c
+++ b/src/examples/export-spa.c
@@ -93,7 +93,7 @@ static int make_node(struct data *data)
 
 	if (data->path) {
 		pw_properties_set(props, PW_KEY_NODE_AUTOCONNECT, "true");
-		pw_properties_set(props, PW_KEY_NODE_TARGET, data->path);
+		pw_properties_set(props, PW_KEY_TARGET_OBJECT, data->path);
 	}
 
 	data->proxy = pw_core_export(data->core,
diff --git a/src/examples/video-dsp-play.c b/src/examples/video-dsp-play.c
index 05828b5918220a952008791d5ddcb1d752ed1525..71f9b01998030de63cc47534c9bd8a0dca51ce3c 100644
--- a/src/examples/video-dsp-play.c
+++ b/src/examples/video-dsp-play.c
@@ -263,7 +263,7 @@ int main(int argc, char *argv[])
 				PW_KEY_MEDIA_CATEGORY, "Capture",
 				PW_KEY_MEDIA_ROLE, "DSP",
 				PW_KEY_NODE_AUTOCONNECT, data.target ? "true" : "false",
-				PW_KEY_NODE_TARGET, data.target,
+				PW_KEY_TARGET_OBJECT, data.target,
 				PW_KEY_MEDIA_CLASS, "Stream/Input/Video",
 				NULL),
 			&filter_events,
diff --git a/src/examples/video-play-fixate.c b/src/examples/video-play-fixate.c
index 3131c144521ed29f2a34800b45ad522d00aeefe3..1badc3c39a8d60877b642a535419222122c3217a 100644
--- a/src/examples/video-play-fixate.c
+++ b/src/examples/video-play-fixate.c
@@ -419,6 +419,7 @@ int main(int argc, char *argv[])
 	const struct spa_pod *params[2];
 	uint8_t buffer[1024];
 	struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
+	struct pw_properties *props;
 	int res, n_params;
 
 	pw_init(&argc, &argv);
@@ -440,19 +441,22 @@ int main(int argc, char *argv[])
 	 * you need to listen to is the process event where you need to consume
 	 * the data provided to you.
 	 */
+	props = pw_properties_new(PW_KEY_MEDIA_TYPE, "Video",
+			PW_KEY_MEDIA_CATEGORY, "Capture",
+			PW_KEY_MEDIA_ROLE, "Camera",
+			NULL),
+	data.path = argc > 1 ? argv[1] : NULL;
+	if (data.path)
+		/* Set stream target if given on command line */
+		pw_properties_set(props, PW_KEY_TARGET_OBJECT, data.path);
+
 	data.stream = pw_stream_new_simple(
 			pw_main_loop_get_loop(data.loop),
-			"video-play-reneg",
-			pw_properties_new(
-				PW_KEY_MEDIA_TYPE, "Video",
-				PW_KEY_MEDIA_CATEGORY, "Capture",
-				PW_KEY_MEDIA_ROLE, "Camera",
-				NULL),
+			"video-play-fixate",
+			props,
 			&stream_events,
 			&data);
 
-	data.path = argc > 1 ? argv[1] : NULL;
-
 	if (SDL_Init(SDL_INIT_VIDEO) < 0) {
 		fprintf(stderr, "can't initialize SDL: %s\n", SDL_GetError());
 		return -1;
@@ -477,7 +481,7 @@ int main(int argc, char *argv[])
 	 */
 	if ((res = pw_stream_connect(data.stream,
 			  PW_DIRECTION_INPUT,
-			  data.path ? (uint32_t)atoi(data.path) : PW_ID_ANY,
+			  PW_ID_ANY,
 			  PW_STREAM_FLAG_AUTOCONNECT |	/* try to automatically connect this stream */
 			  PW_STREAM_FLAG_MAP_BUFFERS,	/* mmap the buffer data for us */
 			  params, n_params))		/* extra parameters, see above */ < 0) {
diff --git a/src/examples/video-play-pull.c b/src/examples/video-play-pull.c
index 37f7e731413d545501bcfbbc25aa6e14cf3b085f..2b2c34660c1275ef98c1698b9aa4bf8b2be78897 100644
--- a/src/examples/video-play-pull.c
+++ b/src/examples/video-play-pull.c
@@ -493,6 +493,7 @@ int main(int argc, char *argv[])
 	const struct spa_pod *params[2];
 	uint8_t buffer[1024];
 	struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
+	struct pw_properties *props;
 	int res, n_params;
 
 	pw_init(&argc, &argv);
@@ -517,20 +518,23 @@ int main(int argc, char *argv[])
 	 * you need to listen to is the process event where you need to consume
 	 * the data provided to you.
 	 */
+	props = pw_properties_new(PW_KEY_MEDIA_TYPE, "Video",
+			PW_KEY_MEDIA_CATEGORY, "Capture",
+			PW_KEY_MEDIA_ROLE, "Camera",
+			PW_KEY_PRIORITY_DRIVER, "10000",
+			NULL),
+	data.path = argc > 1 ? argv[1] : NULL;
+	if (data.path)
+		/* Set stream target if given on command line */
+		pw_properties_set(props, PW_KEY_TARGET_OBJECT, data.path);
+
 	data.stream = pw_stream_new_simple(
 			pw_main_loop_get_loop(data.loop),
 			"video-play",
-			pw_properties_new(
-				PW_KEY_MEDIA_TYPE, "Video",
-				PW_KEY_MEDIA_CATEGORY, "Capture",
-				PW_KEY_MEDIA_ROLE, "Camera",
-				PW_KEY_PRIORITY_DRIVER, "10000",
-				NULL),
+			props,
 			&stream_events,
 			&data);
 
-	data.path = argc > 1 ? argv[1] : NULL;
-
 	if (SDL_Init(SDL_INIT_VIDEO) < 0) {
 		fprintf(stderr, "can't initialize SDL: %s\n", SDL_GetError());
 		return -1;
@@ -552,7 +556,7 @@ int main(int argc, char *argv[])
 	 */
 	if ((res = pw_stream_connect(data.stream,
 			  PW_DIRECTION_INPUT,
-			  data.path ? (uint32_t)atoi(data.path) : PW_ID_ANY,
+			  PW_ID_ANY,
 			  PW_STREAM_FLAG_DRIVER |	/* we're driver, we pull */
 			  PW_STREAM_FLAG_AUTOCONNECT |	/* try to automatically connect this stream */
 			  PW_STREAM_FLAG_MAP_BUFFERS,	/* mmap the buffer data for us */
diff --git a/src/examples/video-play-reneg.c b/src/examples/video-play-reneg.c
index 3c86bd0969252181133333fcf8ce15fb1b96f479..aa071850afad34a68860867ba8161a9a891ddd67 100644
--- a/src/examples/video-play-reneg.c
+++ b/src/examples/video-play-reneg.c
@@ -346,6 +346,7 @@ int main(int argc, char *argv[])
 	const struct spa_pod *params[2];
 	uint8_t buffer[1024];
 	struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
+	struct pw_properties *props;
 	int res, n_params;
 
 	pw_init(&argc, &argv);
@@ -367,19 +368,22 @@ int main(int argc, char *argv[])
 	 * you need to listen to is the process event where you need to consume
 	 * the data provided to you.
 	 */
+	props = pw_properties_new(PW_KEY_MEDIA_TYPE, "Video",
+			PW_KEY_MEDIA_CATEGORY, "Capture",
+			PW_KEY_MEDIA_ROLE, "Camera",
+			NULL),
+	data.path = argc > 1 ? argv[1] : NULL;
+	if (data.path)
+		/* Set stream target if given on command line */
+		pw_properties_set(props, PW_KEY_TARGET_OBJECT, data.path);
+
 	data.stream = pw_stream_new_simple(
 			pw_main_loop_get_loop(data.loop),
 			"video-play-reneg",
-			pw_properties_new(
-				PW_KEY_MEDIA_TYPE, "Video",
-				PW_KEY_MEDIA_CATEGORY, "Capture",
-				PW_KEY_MEDIA_ROLE, "Camera",
-				NULL),
+			props,
 			&stream_events,
 			&data);
 
-	data.path = argc > 1 ? argv[1] : NULL;
-
 	if (SDL_Init(SDL_INIT_VIDEO) < 0) {
 		fprintf(stderr, "can't initialize SDL: %s\n", SDL_GetError());
 		return -1;
@@ -401,7 +405,7 @@ int main(int argc, char *argv[])
 	 */
 	if ((res = pw_stream_connect(data.stream,
 			  PW_DIRECTION_INPUT,
-			  data.path ? (uint32_t)atoi(data.path) : PW_ID_ANY,
+			  PW_ID_ANY,
 			  PW_STREAM_FLAG_AUTOCONNECT |	/* try to automatically connect this stream */
 			  PW_STREAM_FLAG_MAP_BUFFERS,	/* mmap the buffer data for us */
 			  params, n_params))		/* extra parameters, see above */ < 0) {
diff --git a/src/examples/video-play.c b/src/examples/video-play.c
index 20362b892828ea767435510e53e09e5b306a3705..68589c90218db9c5a1600893f60fb69f9df95194 100644
--- a/src/examples/video-play.c
+++ b/src/examples/video-play.c
@@ -439,6 +439,7 @@ int main(int argc, char *argv[])
 	const struct spa_pod *params[2];
 	uint8_t buffer[1024];
 	struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
+	struct pw_properties *props;
 	int res, n_params;
 
 	pw_init(&argc, &argv);
@@ -460,19 +461,21 @@ int main(int argc, char *argv[])
 	 * you need to listen to is the process event where you need to consume
 	 * the data provided to you.
 	 */
+	props = pw_properties_new(PW_KEY_MEDIA_TYPE, "Video",
+			PW_KEY_MEDIA_CATEGORY, "Capture",
+			PW_KEY_MEDIA_ROLE, "Camera",
+			NULL),
+	data.path = argc > 1 ? argv[1] : NULL;
+	if (data.path)
+		pw_properties_set(props, PW_KEY_TARGET_OBJECT, data.path);
+
 	data.stream = pw_stream_new_simple(
 			pw_main_loop_get_loop(data.loop),
 			"video-play",
-			pw_properties_new(
-				PW_KEY_MEDIA_TYPE, "Video",
-				PW_KEY_MEDIA_CATEGORY, "Capture",
-				PW_KEY_MEDIA_ROLE, "Camera",
-				NULL),
+			props,
 			&stream_events,
 			&data);
 
-	data.path = argc > 1 ? argv[1] : NULL;
-
 	if (SDL_Init(SDL_INIT_VIDEO) < 0) {
 		fprintf(stderr, "can't initialize SDL: %s\n", SDL_GetError());
 		return -1;
@@ -494,7 +497,7 @@ int main(int argc, char *argv[])
 	 */
 	if ((res = pw_stream_connect(data.stream,
 			  PW_DIRECTION_INPUT,
-			  data.path ? (uint32_t)atoi(data.path) : PW_ID_ANY,
+			  PW_ID_ANY,
 			  PW_STREAM_FLAG_AUTOCONNECT |	/* try to automatically connect this stream */
 			  PW_STREAM_FLAG_INACTIVE |	/* we will activate ourselves */
 			  PW_STREAM_FLAG_MAP_BUFFERS,	/* mmap the buffer data for us */
diff --git a/src/examples/video-src-alloc.c b/src/examples/video-src-alloc.c
index a35ecc8a7775faca7a2cfc721f63287e03443175..ef364fd1d7d23a53abb40b5cf922c0ceda0dbde6 100644
--- a/src/examples/video-src-alloc.c
+++ b/src/examples/video-src-alloc.c
@@ -442,7 +442,7 @@ int main(int argc, char *argv[])
 	 * the server.  */
 	pw_stream_connect(data.stream,
 			  PW_DIRECTION_OUTPUT,
-			  PW_ID_ANY,			/* link to any node */
+			  PW_ID_ANY,
 			  PW_STREAM_FLAG_DRIVER |
 			  PW_STREAM_FLAG_ALLOC_BUFFERS,
 			  params, 1);
diff --git a/src/examples/video-src-fixate.c b/src/examples/video-src-fixate.c
index bdfbb33a286ddd5ab25164596244de2d328cb608..6d621d16367279b0297d7c3c9bc900467f1f7dd0 100644
--- a/src/examples/video-src-fixate.c
+++ b/src/examples/video-src-fixate.c
@@ -578,7 +578,7 @@ int main(int argc, char *argv[])
 	 * the server.  */
 	pw_stream_connect(data.stream,
 			  PW_DIRECTION_OUTPUT,
-			  PW_ID_ANY,			/* link to any node */
+			  PW_ID_ANY,
 			  PW_STREAM_FLAG_DRIVER |
 			  PW_STREAM_FLAG_ALLOC_BUFFERS,
 			  params, 2);
diff --git a/src/examples/video-src-reneg.c b/src/examples/video-src-reneg.c
index d0431210a8638a5e46d50877931b5d2ace0bb249..172e7dc915637bd8a17fa6ac32977bd3c9c388df 100644
--- a/src/examples/video-src-reneg.c
+++ b/src/examples/video-src-reneg.c
@@ -487,7 +487,7 @@ int main(int argc, char *argv[])
 	 * the server.  */
 	pw_stream_connect(data.stream,
 			  PW_DIRECTION_OUTPUT,
-			  PW_ID_ANY,			/* link to any node */
+			  PW_ID_ANY,
 			  PW_STREAM_FLAG_DRIVER |
 			  PW_STREAM_FLAG_ALLOC_BUFFERS,
 			  params, 1);
diff --git a/src/gst/gstpipewiresrc.c b/src/gst/gstpipewiresrc.c
index 32bcf4988b4f545c6e956c44da24b216e089ccab..4e8e8bd4e7f10f846027443bbb7aadecc6108907 100644
--- a/src/gst/gstpipewiresrc.c
+++ b/src/gst/gstpipewiresrc.c
@@ -44,6 +44,7 @@
 #include <unistd.h>
 
 #include <spa/pod/builder.h>
+#include <spa/utils/result.h>
 
 #include <gst/net/gstnetclientclock.h>
 #include <gst/allocators/gstfdmemory.h>
@@ -443,6 +444,7 @@ buffer_recycle (GstMiniObject *obj)
 {
   GstPipeWireSrc *src;
   GstPipeWirePoolData *data;
+  int res;
 
   data = gst_pipewire_pool_get_data (GST_BUFFER_CAST(obj));
 
@@ -466,8 +468,11 @@ buffer_recycle (GstMiniObject *obj)
 
   data->queued = TRUE;
 
-  GST_LOG_OBJECT (src, "recycle buffer %p", obj);
-  pw_stream_queue_buffer (src->stream, data->b);
+  if ((res = pw_stream_queue_buffer (src->stream, data->b)) < 0)
+    GST_WARNING_OBJECT (src, "can't queue recycled buffer %p, %s", obj, spa_strerror(res));
+  else
+    GST_LOG_OBJECT (src, "recycle buffer %p", obj);
+
   pw_thread_loop_unlock (src->core->loop);
 
   GST_OBJECT_UNLOCK (data->pool);
@@ -481,9 +486,9 @@ on_add_buffer (void *_data, struct pw_buffer *b)
   GstPipeWireSrc *pwsrc = _data;
   GstPipeWirePoolData *data;
 
-  GST_DEBUG_OBJECT (pwsrc, "add buffer");
   gst_pipewire_pool_wrap_buffer (pwsrc->pool, b);
   data = b->user_data;
+  GST_DEBUG_OBJECT (pwsrc, "add buffer %p", data->buf);
   data->owner = pwsrc;
   data->queued = TRUE;
   GST_MINI_OBJECT_CAST (data->buf)->dispose = buffer_recycle;
@@ -495,15 +500,18 @@ on_remove_buffer (void *_data, struct pw_buffer *b)
   GstPipeWireSrc *pwsrc = _data;
   GstPipeWirePoolData *data = b->user_data;
   GstBuffer *buf = data->buf;
+  int res;
 
   GST_DEBUG_OBJECT (pwsrc, "remove buffer %p", buf);
 
   GST_MINI_OBJECT_CAST (buf)->dispose = NULL;
 
-  if (data->queued)
+  if (data->queued) {
     gst_buffer_unref (buf);
-  else
-    pw_stream_queue_buffer (pwsrc->stream, b);
+  } else {
+    if ((res = pw_stream_queue_buffer (pwsrc->stream, b)) < 0)
+      GST_WARNING_OBJECT (pwsrc, "can't queue removed buffer %p, %s", buf, spa_strerror(res));
+  }
 }
 
 static GstBuffer *dequeue_buffer(GstPipeWireSrc *pwsrc)
@@ -560,19 +568,23 @@ static GstBuffer *dequeue_buffer(GstPipeWireSrc *pwsrc)
       meta->height = crop->region.size.height;
     }
   }
-  gst_buffer_add_parent_buffer_meta (buf, data->buf);
-  gst_buffer_unref (data->buf);
   for (i = 0; i < b->buffer->n_datas; i++) {
     struct spa_data *d = &b->buffer->datas[i];
     GstMemory *pmem = gst_buffer_peek_memory (data->buf, i);
     if (pmem) {
-      GstMemory *mem = gst_memory_share (pmem, d->chunk->offset, d->chunk->size);
+      GstMemory *mem;
+      if (!pwsrc->always_copy)
+        mem = gst_memory_share (pmem, d->chunk->offset, d->chunk->size);
+      else
+        mem = gst_memory_copy (pmem, d->chunk->offset, d->chunk->size);
       gst_buffer_insert_memory (buf, i, mem);
-      spa_assert_se(mem->size <= mem->maxsize);
     }
     if (d->chunk->flags & SPA_CHUNK_FLAG_CORRUPTED)
       GST_BUFFER_FLAG_SET (buf, GST_BUFFER_FLAG_CORRUPTED);
   }
+  if (!pwsrc->always_copy)
+    gst_buffer_add_parent_buffer_meta (buf, data->buf);
+  gst_buffer_unref (data->buf);
   return buf;
 }
 
@@ -1026,10 +1038,10 @@ gst_pipewire_src_create (GstPushSrc * psrc, GstBuffer ** buffer)
 
   pwsrc = GST_PIPEWIRE_SRC (psrc);
 
+  pw_thread_loop_lock (pwsrc->core->loop);
   if (!pwsrc->negotiated)
     goto not_negotiated;
 
-  pw_thread_loop_lock (pwsrc->core->loop);
   while (TRUE) {
     enum pw_stream_state state;
 
@@ -1083,12 +1095,7 @@ gst_pipewire_src_create (GstPushSrc * psrc, GstBuffer ** buffer)
   }
   pw_thread_loop_unlock (pwsrc->core->loop);
 
-  if (pwsrc->always_copy) {
-    *buffer = gst_buffer_copy_deep (buf);
-    gst_buffer_unref (buf);
-  }
-  else
-    *buffer = buf;
+  *buffer = buf;
 
   if (pwsrc->is_live)
     base_time = GST_ELEMENT_CAST (psrc)->base_time;
@@ -1126,6 +1133,7 @@ gst_pipewire_src_create (GstPushSrc * psrc, GstBuffer ** buffer)
 
 not_negotiated:
   {
+    pw_thread_loop_unlock (pwsrc->core->loop);
     return GST_FLOW_NOT_NEGOTIATED;
   }
 streaming_eos:
@@ -1348,7 +1356,9 @@ gst_pipewire_src_change_state (GstElement * element, GstStateChange transition)
     case GST_STATE_CHANGE_PLAYING_TO_PAUSED:
       break;
     case GST_STATE_CHANGE_PAUSED_TO_READY:
+      pw_thread_loop_lock (this->core->loop);
       this->negotiated = FALSE;
+      pw_thread_loop_unlock (this->core->loop);
       break;
     case GST_STATE_CHANGE_READY_TO_NULL:
       gst_pipewire_src_close (this);
diff --git a/src/modules/flatpak-utils.h b/src/modules/flatpak-utils.h
new file mode 100644
index 0000000000000000000000000000000000000000..8952ac4e32887396cac80ac7314b3e85cb0a4b08
--- /dev/null
+++ b/src/modules/flatpak-utils.h
@@ -0,0 +1,156 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef FLATPAK_UTILS_H
+#define FLATPAK_UTILS_H
+
+#include <stdio.h>
+#include <string.h>
+#include <fcntl.h>
+#include <sys/mman.h>
+#include <sys/stat.h>
+#ifdef HAVE_SYS_VFS_H
+#include <sys/vfs.h>
+#endif
+
+#include "config.h"
+
+#ifdef HAVE_GLIB2
+#include <glib.h>
+#endif
+
+#include <spa/utils/result.h>
+#include <pipewire/log.h>
+
+static int pw_check_flatpak_parse_metadata(const char *buf, size_t size, char **app_id, char **devices)
+{
+#ifdef HAVE_GLIB2
+	/*
+	 * See flatpak-metadata(5)
+	 *
+	 * The .flatpak-info file is in GLib key_file .ini format.
+	 */
+	g_autoptr(GKeyFile) metadata = NULL;
+	char *s;
+
+	metadata = g_key_file_new();
+	if (!g_key_file_load_from_data(metadata, buf, size, G_KEY_FILE_NONE, NULL))
+		return -EINVAL;
+
+	if (app_id) {
+		s = g_key_file_get_value(metadata, "Application", "name", NULL);
+		*app_id = s ? strdup(s) : NULL;
+		g_free(s);
+	}
+
+	if (devices) {
+		s = g_key_file_get_value(metadata, "Context", "devices", NULL);
+		*devices = s ? strdup(s) : NULL;
+		g_free(s);
+	}
+
+	return 0;
+#else
+	return -ENOTSUP;
+#endif
+}
+
+static int pw_check_flatpak(pid_t pid, char **app_id, char **devices)
+{
+#if defined(__linux__)
+	char root_path[2048];
+	int root_fd, info_fd, res;
+	struct stat stat_buf;
+
+	if (app_id)
+		*app_id = NULL;
+	if (devices)
+		*devices = NULL;
+
+	snprintf(root_path, sizeof(root_path), "/proc/%d/root", (int)pid);
+	root_fd = openat (AT_FDCWD, root_path, O_RDONLY | O_NONBLOCK | O_DIRECTORY | O_CLOEXEC | O_NOCTTY);
+	if (root_fd == -1) {
+		res = -errno;
+		if (res == -EACCES) {
+			struct statfs buf;
+			/* Access to the root dir isn't allowed. This can happen if the root is on a fuse
+			 * filesystem, such as in a toolbox container. We will never have a fuse rootfs
+			 * in the flatpak case, so in that case its safe to ignore this and
+			 * continue to detect other types of apps. */
+			if (statfs(root_path, &buf) == 0 &&
+			    buf.f_type == 0x65735546) /* FUSE_SUPER_MAGIC */
+				return 0;
+		}
+		/* Not able to open the root dir shouldn't happen. Probably the app died and
+		 * we're failing due to /proc/$pid not existing. In that case fail instead
+		 * of treating this as privileged. */
+		pw_log_info("failed to open \"%s\": %s", root_path, spa_strerror(res));
+		return res;
+	}
+	info_fd = openat (root_fd, ".flatpak-info", O_RDONLY | O_CLOEXEC | O_NOCTTY);
+	close (root_fd);
+	if (info_fd == -1) {
+		if (errno == ENOENT) {
+			pw_log_debug("no .flatpak-info, client on the host");
+			/* No file => on the host */
+			return 0;
+		}
+		res = -errno;
+		pw_log_error("error opening .flatpak-info: %m");
+		return res;
+        }
+	if (fstat (info_fd, &stat_buf) != 0 || !S_ISREG (stat_buf.st_mode)) {
+		/* Some weird fd => failure, assume sandboxed */
+		pw_log_error("error fstat .flatpak-info: %m");
+	} else if (app_id || devices) {
+		/* Parse the application ID if needed */
+		const size_t size = stat_buf.st_size;
+
+		if (size > 0) {
+			void *buf = mmap(NULL, size, PROT_READ, MAP_PRIVATE, info_fd, 0);
+			if (buf != MAP_FAILED) {
+				res = pw_check_flatpak_parse_metadata(buf, size, app_id, devices);
+				munmap(buf, size);
+			} else {
+				res = -errno;
+			}
+		} else {
+			res = -EINVAL;
+		}
+
+		if (res == -EINVAL)
+			pw_log_error("PID %d .flatpak-info file is malformed",
+					(int)pid);
+		else if (res < 0)
+			pw_log_error("PID %d .flatpak-info parsing failed: %s",
+					(int)pid, spa_strerror(res));
+	}
+	close(info_fd);
+	return 1;
+#else
+	return 0;
+#endif
+}
+
+#endif /* FLATPAK_UTILS_H */
diff --git a/src/modules/meson.build b/src/modules/meson.build
index 3c8d6db5ba34878ca052190f644df16dea9ea47c..3e0e3a3fdccd890fcdfc83da5c7db5e33cf3efba 100644
--- a/src/modules/meson.build
+++ b/src/modules/meson.build
@@ -5,6 +5,7 @@ subdir('spa')
 module_sources = [
   'module-access.c',
   'module-adapter.c',
+  'module-avb.c',
   'module-client-device.c',
   'module-client-node.c',
   'module-echo-cancel.c',
@@ -32,12 +33,17 @@ module_sources = [
   'module-x11-bell.c',
 ]
 
+pipewire_module_access_deps = [spa_dep, mathlib, dl_lib, pipewire_dep]
+if flatpak_support
+  pipewire_module_access_deps += glib2_dep
+endif
+
 pipewire_module_access = shared_library('pipewire-module-access', [ 'module-access.c' ],
   include_directories : [configinc],
   install : true,
   install_dir : modules_install_dir,
   install_rpath: modules_install_dir,
-  dependencies : [spa_dep, mathlib, dl_lib, pipewire_dep],
+  dependencies : pipewire_module_access_deps
 )
 
 pipewire_module_loopback = shared_library('pipewire-module-loopback',
@@ -276,6 +282,10 @@ if avahi_dep.found()
   cdata.set('HAVE_AVAHI', true)
 endif
 
+if flatpak_support
+  pipewire_module_protocol_pulse_deps += glib2_dep
+endif
+
 pipewire_module_protocol_pulse = shared_library('pipewire-module-protocol-pulse',
   pipewire_module_protocol_pulse_sources,
   include_directories : [configinc],
@@ -469,7 +479,7 @@ pipewire_module_raop_sink = shared_library('pipewire-module-raop-sink',
 endif
 summary({'raop-sink (requires OpenSSL)': build_module_raop}, bool_yn: true, section: 'Optional Modules')
 
-roc_lib = cc.find_library('roc', required: get_option('roc'))
+roc_lib = cc.find_library('roc', has_headers: ['roc/config.h' ], required: get_option('roc'))
 summary({'ROC': roc_lib.found()}, bool_yn: true, section: 'Streaming between daemons')
 
 build_module_roc = roc_lib.found()
@@ -516,3 +526,30 @@ pipewire_module_fallback_sink = shared_library('pipewire-module-fallback-sink',
   install_rpath: modules_install_dir,
   dependencies : [mathlib, dl_lib, rt_lib, pipewire_dep],
 )
+
+build_module_avb = get_option('avb').require(host_machine.system() == 'linux', error_message: 'AVB support is only available on Linux').allowed()
+if build_module_avb
+pipewire_module_avb = shared_library('pipewire-module-avb',
+  [ 'module-avb.c',
+    'module-avb/avb.c',
+    'module-avb/adp.c',
+    'module-avb/acmp.c',
+    'module-avb/aecp.c',
+    'module-avb/aecp-aem.c',
+    'module-avb/avdecc.c',
+    'module-avb/maap.c',
+    'module-avb/mmrp.c',
+    'module-avb/mrp.c',
+    'module-avb/msrp.c',
+    'module-avb/mvrp.c',
+    'module-avb/srp.c',
+    'module-avb/stream.c'
+    ],
+  include_directories : [configinc],
+  install : true,
+  install_dir : modules_install_dir,
+  install_rpath: modules_install_dir,
+  dependencies : [mathlib, dl_lib, rt_lib, pipewire_dep],
+)
+endif
+summary({'avb': build_module_avb}, bool_yn: true, section: 'Optional Modules')
diff --git a/src/modules/module-access.c b/src/modules/module-access.c
index a5df8aa5e0cf25e02129114046bdf798056953a6..9b6d02861001a05ca7a7f5eb17feea8810f3bea2 100644
--- a/src/modules/module-access.c
+++ b/src/modules/module-access.c
@@ -46,6 +46,8 @@
 #include <pipewire/impl.h>
 #include <pipewire/private.h>
 
+#include "flatpak-utils.h"
+
 /** \page page_module_access PipeWire Module: Access
  *
  *
@@ -75,6 +77,9 @@
  *       on an external actor to update that property once permission is
  *       granted or rejected.
  *
+ * For connections from applications running inside Flatpak not mediated
+ * by a portal, the `access` module itself sets the `pipewire.access.portal.app_id`
+ * property to the Flatpak application ID.
  *
  * ## Module Options
  *
@@ -184,54 +189,6 @@ exit:
 	return res;
 }
 
-#if defined(__linux__)
-static int check_flatpak(struct pw_impl_client *client, int pid)
-{
-	char root_path[2048];
-	int root_fd, info_fd, res;
-	struct stat stat_buf;
-
-	sprintf(root_path, "/proc/%u/root", pid);
-	root_fd = openat (AT_FDCWD, root_path, O_RDONLY | O_NONBLOCK | O_DIRECTORY | O_CLOEXEC | O_NOCTTY);
-	if (root_fd == -1) {
-		res = -errno;
-		if (res == -EACCES) {
-			struct statfs buf;
-			/* Access to the root dir isn't allowed. This can happen if the root is on a fuse
-			 * filesystem, such as in a toolbox container. We will never have a fuse rootfs
-			 * in the flatpak case, so in that case its safe to ignore this and
-			 * continue to detect other types of apps. */
-			if (statfs(root_path, &buf) == 0 &&
-			    buf.f_type == 0x65735546) /* FUSE_SUPER_MAGIC */
-				return 0;
-		}
-		/* Not able to open the root dir shouldn't happen. Probably the app died and
-		 * we're failing due to /proc/$pid not existing. In that case fail instead
-		 * of treating this as privileged. */
-		pw_log_info("failed to open \"%s\": %s", root_path, spa_strerror(res));
-		return res;
-	}
-	info_fd = openat (root_fd, ".flatpak-info", O_RDONLY | O_CLOEXEC | O_NOCTTY);
-	close (root_fd);
-	if (info_fd == -1) {
-		if (errno == ENOENT) {
-			pw_log_debug("no .flatpak-info, client on the host");
-			/* No file => on the host */
-			return 0;
-		}
-		res = -errno;
-		pw_log_error("error opening .flatpak-info: %m");
-		return res;
-        }
-	if (fstat (info_fd, &stat_buf) != 0 || !S_ISREG (stat_buf.st_mode)) {
-		/* Some weird fd => failure, assume sandboxed */
-		pw_log_error("error fstat .flatpak-info: %m");
-	}
-	close(info_fd);
-	return 1;
-}
-#endif
-
 static void
 context_check_access(void *data, struct pw_impl_client *client)
 {
@@ -240,6 +197,8 @@ context_check_access(void *data, struct pw_impl_client *client)
 	struct spa_dict_item items[2];
 	const struct pw_properties *props;
 	const char *str, *access;
+	char *flatpak_app_id = NULL;
+	int nitems = 0;
 	int pid, res;
 
 	pid = -EINVAL;
@@ -298,8 +257,7 @@ context_check_access(void *data, struct pw_impl_client *client)
 	    (access = pw_properties_get(impl->properties, "access.force")) != NULL)
 		goto wait_permissions;
 
-#if defined(__linux__)
-	res = check_flatpak(client, pid);
+	res = pw_check_flatpak(pid, &flatpak_app_id, NULL);
 	if (res != 0) {
 		if (res < 0) {
 			if (res == -EACCES) {
@@ -313,9 +271,11 @@ context_check_access(void *data, struct pw_impl_client *client)
 			pw_log_debug(" %p: flatpak client %p added", impl, client);
 		}
 		access = "flatpak";
+		items[nitems++] = SPA_DICT_ITEM_INIT("pipewire.access.portal.app_id",
+				flatpak_app_id);
 		goto wait_permissions;
 	}
-#endif
+
 	if ((access = pw_properties_get(props, PW_KEY_CLIENT_ACCESS)) == NULL)
 		access = "unrestricted";
 
@@ -326,24 +286,28 @@ context_check_access(void *data, struct pw_impl_client *client)
 
 granted:
 	pw_log_info("%p: client %p '%s' access granted", impl, client, access);
-	items[0] = SPA_DICT_ITEM_INIT(PW_KEY_ACCESS, access);
-	pw_impl_client_update_properties(client, &SPA_DICT_INIT(items, 1));
+	items[nitems++] = SPA_DICT_ITEM_INIT(PW_KEY_ACCESS, access);
+	pw_impl_client_update_properties(client, &SPA_DICT_INIT(items, nitems));
 
 	permissions[0] = PW_PERMISSION_INIT(PW_ID_ANY, PW_PERM_ALL);
 	pw_impl_client_update_permissions(client, 1, permissions);
-	return;
+	goto done;
 
 wait_permissions:
 	pw_log_info("%p: client %p wait for '%s' permissions",
 			impl, client, access);
-	items[0] = SPA_DICT_ITEM_INIT(PW_KEY_ACCESS, access);
-	pw_impl_client_update_properties(client, &SPA_DICT_INIT(items, 1));
-	return;
+	items[nitems++] = SPA_DICT_ITEM_INIT(PW_KEY_ACCESS, access);
+	pw_impl_client_update_properties(client, &SPA_DICT_INIT(items, nitems));
+	goto done;
 
 rejected:
 	pw_resource_error(pw_impl_client_get_core_resource(client), res, access);
-	items[0] = SPA_DICT_ITEM_INIT(PW_KEY_ACCESS, access);
-	pw_impl_client_update_properties(client, &SPA_DICT_INIT(items, 1));
+	items[nitems++] = SPA_DICT_ITEM_INIT(PW_KEY_ACCESS, access);
+	pw_impl_client_update_properties(client, &SPA_DICT_INIT(items, nitems));
+	goto done;
+
+done:
+	free(flatpak_app_id);
 	return;
 }
 
diff --git a/src/modules/module-adapter/adapter.c b/src/modules/module-adapter/adapter.c
index aab785527c613819a153399355529a40535f5111..3c2cd56b9c9b4ac7cba220d58cce7abccbbc5015 100644
--- a/src/modules/module-adapter/adapter.c
+++ b/src/modules/module-adapter/adapter.c
@@ -95,7 +95,7 @@ static void node_port_init(void *data, struct pw_impl_port *port)
 	struct pw_properties *new;
 	const char *str, *path, *desc, *nick, *name, *node_name, *media_class;
 	char position[8], *prefix;
-	bool is_monitor, is_device, is_duplex, is_virtual;
+	bool is_monitor, is_device, is_duplex, is_virtual, is_control = false;
 
 	direction = pw_impl_port_get_direction(port);
 
@@ -105,6 +105,9 @@ static void node_port_init(void *data, struct pw_impl_port *port)
 	if (!is_monitor && direction != n->direction)
 		return;
 
+	if ((str = pw_properties_get(old, PW_KEY_FORMAT_DSP)) != NULL)
+		is_control = spa_streq(str, "8 bit raw midi");
+
 	path = pw_properties_get(n->props, PW_KEY_OBJECT_PATH);
 	media_class = pw_properties_get(n->props, PW_KEY_MEDIA_CLASS);
 
@@ -120,7 +123,10 @@ static void node_port_init(void *data, struct pw_impl_port *port)
 
 	new = pw_properties_new(NULL, NULL);
 
-	if (is_duplex)
+	if (is_control)
+		prefix = direction == PW_DIRECTION_INPUT ?
+			"control" : "notify";
+	else if (is_duplex)
 		prefix = direction == PW_DIRECTION_INPUT ?
 			"playback" : "capture";
 	else if (is_virtual)
@@ -156,13 +162,20 @@ static void node_port_init(void *data, struct pw_impl_port *port)
 	pw_properties_setf(new, PW_KEY_OBJECT_PATH, "%s:%s_%d",
 			path ? path : node_name, prefix, pw_impl_port_get_id(port));
 
-	pw_properties_setf(new, PW_KEY_PORT_NAME, "%s_%s", prefix, str);
+	if (is_control)
+		pw_properties_setf(new, PW_KEY_PORT_NAME, "%s", prefix);
+	else
+		pw_properties_setf(new, PW_KEY_PORT_NAME, "%s_%s", prefix, str);
 
 	if ((node_name = nick) == NULL && (node_name = desc) == NULL &&
 	    (node_name = name) == NULL)
 		node_name = "node";
 
-	pw_properties_setf(new, PW_KEY_PORT_ALIAS, "%s:%s_%s",
+	if (is_control)
+		pw_properties_setf(new, PW_KEY_PORT_ALIAS, "%s:%s",
+			node_name, prefix);
+	else
+		pw_properties_setf(new, PW_KEY_PORT_ALIAS, "%s:%s_%s",
 			node_name, prefix, str);
 
 	pw_impl_port_update_properties(port, &new->dict);
@@ -240,7 +253,7 @@ static int do_auto_port_config(struct node *n, const char *str)
 	int res, position = POSITION_PRESERVE;
 	struct spa_pod *param;
 	uint32_t media_type, media_subtype;
-	bool have_format = false, monitor = false;
+	bool have_format = false, monitor = false, control = false;
 	struct spa_audio_info format = { 0, };
 	enum spa_param_port_config_mode mode = SPA_PARAM_PORT_CONFIG_MODE_none;
 	struct spa_json it[2];
@@ -260,6 +273,8 @@ static int do_auto_port_config(struct node *n, const char *str)
 				mode = SPA_PARAM_PORT_CONFIG_MODE_none;
 		} else if (spa_streq(key, "monitor")) {
 			monitor = spa_atob(val);
+		} else if (spa_streq(key, "control")) {
+			control = spa_atob(val);
 		} else if (spa_streq(key, "position")) {
 			if (spa_streq(val, "unknown"))
 				position = POSITION_UNKNOWN;
@@ -331,6 +346,7 @@ static int do_auto_port_config(struct node *n, const char *str)
 		SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(n->direction),
 		SPA_PARAM_PORT_CONFIG_mode,      SPA_POD_Id(mode),
 		SPA_PARAM_PORT_CONFIG_monitor,   SPA_POD_Bool(monitor),
+		SPA_PARAM_PORT_CONFIG_control,   SPA_POD_Bool(control),
 		SPA_PARAM_PORT_CONFIG_format,    SPA_POD_Pod(param));
 	pw_impl_node_set_param(n->node, SPA_PARAM_PortConfig, 0, param);
 
diff --git a/src/modules/module-avb.c b/src/modules/module-avb.c
new file mode 100644
index 0000000000000000000000000000000000000000..e70c6503183f1ade1b9a3fb6300db75b25628b18
--- /dev/null
+++ b/src/modules/module-avb.c
@@ -0,0 +1,130 @@
+/* PipeWire
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+#include "config.h"
+
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/utils/json.h>
+
+#include <pipewire/impl.h>
+#include <pipewire/private.h>
+#include <pipewire/i18n.h>
+
+#include "module-avb/avb.h"
+
+/** \page page_module_avb PipeWire Module: AVB
+ */
+
+#define NAME "avb"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+#define MODULE_USAGE	" "
+
+static const struct spa_dict_item module_props[] = {
+	{ PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+	{ PW_KEY_MODULE_DESCRIPTION, "Manage an AVB network" },
+	{ PW_KEY_MODULE_USAGE, MODULE_USAGE },
+	{ PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+
+struct impl {
+	struct pw_context *context;
+
+	struct pw_impl_module *module;
+	struct spa_hook module_listener;
+
+	struct pw_avb *avb;
+};
+
+static void impl_free(struct impl *impl)
+{
+	free(impl);
+}
+
+static void module_destroy(void *data)
+{
+	struct impl *impl = data;
+	spa_hook_remove(&impl->module_listener);
+	impl_free(impl);
+}
+
+static const struct pw_impl_module_events module_events = {
+	PW_VERSION_IMPL_MODULE_EVENTS,
+	.destroy = module_destroy,
+};
+
+SPA_EXPORT
+int pipewire__module_init(struct pw_impl_module *module, const char *args)
+{
+	struct pw_context *context = pw_impl_module_get_context(module);
+	struct pw_properties *props;
+	struct impl *impl;
+	int res;
+
+	PW_LOG_TOPIC_INIT(mod_topic);
+
+	impl = calloc(1, sizeof(struct impl));
+	if (impl == NULL)
+		goto error_errno;
+
+	pw_log_debug("module %p: new %s", impl, args);
+
+	if (args == NULL)
+		args = "";
+
+	props = pw_properties_new_string(args);
+	if (props == NULL)
+		goto error_errno;
+
+	impl->module = module;
+	impl->context = context;
+
+	impl->avb = pw_avb_new(context, props, 0);
+	if (impl->avb == NULL)
+		goto error_errno;
+
+	pw_impl_module_add_listener(module, &impl->module_listener, &module_events, impl);
+
+	pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props));
+
+	return 0;
+
+error_errno:
+	res = -errno;
+	if (impl)
+		impl_free(impl);
+	return res;
+}
diff --git a/src/modules/module-avb/aaf.h b/src/modules/module-avb/aaf.h
new file mode 100644
index 0000000000000000000000000000000000000000..b444ce25101d0bf385a1fe5fd52e3eafbee1ef6d
--- /dev/null
+++ b/src/modules/module-avb/aaf.h
@@ -0,0 +1,102 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef AVB_AAF_H
+#define AVB_AAF_H
+
+struct avb_packet_aaf {
+	uint8_t subtype;
+#if __BYTE_ORDER == __BIG_ENDIAN
+	unsigned sv:1;
+	unsigned version:3;
+	unsigned mr:1;
+	unsigned _r1:1;
+	unsigned gv:1;
+	unsigned tv:1;
+
+	uint8_t seq_num;
+
+	unsigned _r2:7;
+	unsigned tu:1;
+#elif __BYTE_ORDER == __LITTLE_ENDIAN
+	unsigned tv:1;
+	unsigned gv:1;
+	unsigned _r1:1;
+	unsigned mr:1;
+	unsigned version:3;
+	unsigned sv:1;
+
+	uint8_t seq_num;
+
+	unsigned tu:1;
+	unsigned _r2:7;
+#endif
+	uint64_t stream_id;
+	uint32_t timestamp;
+#define AVB_AAF_FORMAT_USER		0x00
+#define AVB_AAF_FORMAT_FLOAT_32BIT	0x01
+#define AVB_AAF_FORMAT_INT_32BIT	0x02
+#define AVB_AAF_FORMAT_INT_24BIT	0x03
+#define AVB_AAF_FORMAT_INT_16BIT	0x04
+#define AVB_AAF_FORMAT_AES3_32BIT	0x05
+	uint8_t format;
+
+#define AVB_AAF_PCM_NSR_USER		0x00
+#define AVB_AAF_PCM_NSR_8KHZ		0x01
+#define AVB_AAF_PCM_NSR_16KHZ		0x02
+#define AVB_AAF_PCM_NSR_32KHZ		0x03
+#define AVB_AAF_PCM_NSR_44_1KHZ	0x04
+#define AVB_AAF_PCM_NSR_48KHZ		0x05
+#define AVB_AAF_PCM_NSR_88_2KHZ	0x06
+#define AVB_AAF_PCM_NSR_96KHZ		0x07
+#define AVB_AAF_PCM_NSR_176_4KHZ	0x08
+#define AVB_AAF_PCM_NSR_192KHZ	0x09
+#define AVB_AAF_PCM_NSR_24KHZ		0x0A
+#if __BYTE_ORDER == __BIG_ENDIAN
+	unsigned nsr:4;
+	unsigned _r3:4;
+#elif __BYTE_ORDER == __LITTLE_ENDIAN
+	unsigned _r3:4;
+	unsigned nsr:4;
+#endif
+	uint8_t chan_per_frame;
+	uint8_t bit_depth;
+	uint16_t data_len;
+
+#define AVB_AAF_PCM_SP_NORMAL		0x00
+#define AVB_AAF_PCM_SP_SPARSE		0x01
+#if __BYTE_ORDER == __BIG_ENDIAN
+	unsigned _r4:3;
+	unsigned sp:1;
+	unsigned event:4;
+#elif __BYTE_ORDER == __LITTLE_ENDIAN
+	unsigned event:4;
+	unsigned sp:1;
+	unsigned _r4:3;
+#endif
+	uint8_t _r5;
+	uint8_t payload[0];
+} __attribute__ ((__packed__));
+
+#endif /* AVB_AAF_H */
diff --git a/src/modules/module-avb/acmp.c b/src/modules/module-avb/acmp.c
new file mode 100644
index 0000000000000000000000000000000000000000..18ea1ba92c3948a09579f6e75a3a146c56be1d42
--- /dev/null
+++ b/src/modules/module-avb/acmp.c
@@ -0,0 +1,477 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <spa/utils/json.h>
+#include <spa/debug/mem.h>
+
+#include <pipewire/pipewire.h>
+
+#include "acmp.h"
+#include "msrp.h"
+#include "internal.h"
+#include "stream.h"
+
+static const uint8_t mac[6] = AVB_BROADCAST_MAC;
+
+struct pending {
+	struct spa_list link;
+	uint64_t last_time;
+	uint64_t timeout;
+	uint16_t old_sequence_id;
+	uint16_t sequence_id;
+	uint16_t retry;
+	size_t size;
+	void *ptr;
+};
+
+struct acmp {
+	struct server *server;
+	struct spa_hook server_listener;
+
+#define PENDING_TALKER		0
+#define PENDING_LISTENER	1
+#define PENDING_CONTROLLER	2
+	struct spa_list pending[3];
+	uint16_t sequence_id[3];
+};
+
+static void *pending_new(struct acmp *acmp, uint32_t type, uint64_t now, uint32_t timeout_ms,
+		const void *m, size_t size)
+{
+	struct pending *p;
+	struct avb_ethernet_header *h;
+	struct avb_packet_acmp *pm;
+
+	p = calloc(1, sizeof(*p) + size);
+	if (p == NULL)
+		return NULL;
+	p->last_time = now;
+	p->timeout = timeout_ms * SPA_NSEC_PER_MSEC;
+	p->sequence_id = acmp->sequence_id[type]++;
+	p->size = size;
+	p->ptr = SPA_PTROFF(p, sizeof(*p), void);
+	memcpy(p->ptr, m, size);
+
+	h = p->ptr;
+	pm = SPA_PTROFF(h, sizeof(*h), void);
+	p->old_sequence_id = ntohs(pm->sequence_id);
+	pm->sequence_id = htons(p->sequence_id);
+	spa_list_append(&acmp->pending[type], &p->link);
+
+	return p->ptr;
+}
+
+static struct pending *pending_find(struct acmp *acmp, uint32_t type, uint16_t sequence_id)
+{
+	struct pending *p;
+	spa_list_for_each(p, &acmp->pending[type], link)
+		if (p->sequence_id == sequence_id)
+			return p;
+	return NULL;
+}
+
+static void pending_free(struct acmp *acmp, struct pending *p)
+{
+	spa_list_remove(&p->link);
+	free(p);
+}
+
+struct msg_info {
+	uint16_t type;
+	const char *name;
+	int (*handle) (struct acmp *acmp, uint64_t now, const void *m, int len);
+};
+
+static int reply_not_supported(struct acmp *acmp, uint8_t type, const void *m, int len)
+{
+	struct server *server = acmp->server;
+	uint8_t buf[len];
+	struct avb_ethernet_header *h = (void*)buf;
+	struct avb_packet_acmp *reply = SPA_PTROFF(h, sizeof(*h), void);
+
+	memcpy(h, m, len);
+	AVB_PACKET_ACMP_SET_MESSAGE_TYPE(reply, type);
+	AVB_PACKET_ACMP_SET_STATUS(reply, AVB_ACMP_STATUS_NOT_SUPPORTED);
+
+	return avb_server_send_packet(server, h->src, AVB_TSN_ETH, buf, len);
+}
+
+static int retry_pending(struct acmp *acmp, uint64_t now, struct pending *p)
+{
+	struct server *server = acmp->server;
+	struct avb_ethernet_header *h = p->ptr;
+	p->retry++;
+	p->last_time = now;
+	return avb_server_send_packet(server, h->dest, AVB_TSN_ETH, p->ptr, p->size);
+}
+
+static int handle_connect_tx_command(struct acmp *acmp, uint64_t now, const void *m, int len)
+{
+	struct server *server = acmp->server;
+	uint8_t buf[len];
+	struct avb_ethernet_header *h = (void*)buf;
+	struct avb_packet_acmp *reply = SPA_PTROFF(h, sizeof(*h), void);
+	const struct avb_packet_acmp *p = SPA_PTROFF(m, sizeof(*h), void);
+	int status = AVB_ACMP_STATUS_SUCCESS;
+	struct stream *stream;
+
+	if (be64toh(p->talker_guid) != server->entity_id)
+		return 0;
+
+	memcpy(buf, m, len);
+	stream = server_find_stream(server, SPA_DIRECTION_OUTPUT,
+			reply->talker_unique_id);
+	if (stream == NULL) {
+		status = AVB_ACMP_STATUS_TALKER_NO_STREAM_INDEX;
+		goto done;
+	}
+
+	AVB_PACKET_ACMP_SET_MESSAGE_TYPE(reply, AVB_ACMP_MESSAGE_TYPE_CONNECT_TX_RESPONSE);
+	reply->stream_id = htobe64(stream->id);
+
+	stream_activate(stream, now);
+
+	memcpy(reply->stream_dest_mac, stream->addr, 6);
+	reply->connection_count = htons(1);
+	reply->stream_vlan_id = htons(stream->vlan_id);
+
+done:
+	AVB_PACKET_ACMP_SET_STATUS(reply, status);
+	return avb_server_send_packet(server, h->dest, AVB_TSN_ETH, buf, len);
+}
+
+static int handle_connect_tx_response(struct acmp *acmp, uint64_t now, const void *m, int len)
+{
+	struct server *server = acmp->server;
+	struct avb_ethernet_header *h;
+	const struct avb_packet_acmp *resp = SPA_PTROFF(m, sizeof(*h), void);
+	struct avb_packet_acmp *reply;
+	struct pending *pending;
+	uint16_t sequence_id;
+	struct stream *stream;
+	int res;
+
+	if (be64toh(resp->listener_guid) != server->entity_id)
+		return 0;
+
+	sequence_id = ntohs(resp->sequence_id);
+
+	pending = pending_find(acmp, PENDING_TALKER, sequence_id);
+	if (pending == NULL)
+		return 0;
+
+	h = pending->ptr;
+	pending->size = SPA_MIN((int)pending->size, len);
+	memcpy(h, m, pending->size);
+
+	reply = SPA_PTROFF(h, sizeof(*h), void);
+	reply->sequence_id = htons(pending->old_sequence_id);
+	AVB_PACKET_ACMP_SET_MESSAGE_TYPE(reply, AVB_ACMP_MESSAGE_TYPE_CONNECT_RX_RESPONSE);
+
+	stream = server_find_stream(server, SPA_DIRECTION_INPUT,
+			ntohs(reply->listener_unique_id));
+	if (stream == NULL)
+		return 0;
+
+	stream->peer_id = be64toh(reply->stream_id);
+	memcpy(stream->addr, reply->stream_dest_mac, 6);
+	stream_activate(stream, now);
+
+	res = avb_server_send_packet(server, h->dest, AVB_TSN_ETH, h, pending->size);
+
+	pending_free(acmp, pending);
+
+	return res;
+}
+
+static int handle_disconnect_tx_command(struct acmp *acmp, uint64_t now, const void *m, int len)
+{
+	struct server *server = acmp->server;
+	uint8_t buf[len];
+	struct avb_ethernet_header *h = (void*)buf;
+	struct avb_packet_acmp *reply = SPA_PTROFF(h, sizeof(*h), void);
+	const struct avb_packet_acmp *p = SPA_PTROFF(m, sizeof(*h), void);
+	int status = AVB_ACMP_STATUS_SUCCESS;
+	struct stream *stream;
+
+	if (be64toh(p->talker_guid) != server->entity_id)
+		return 0;
+
+	memcpy(buf, m, len);
+	stream = server_find_stream(server, SPA_DIRECTION_OUTPUT,
+			reply->talker_unique_id);
+	if (stream == NULL) {
+		status = AVB_ACMP_STATUS_TALKER_NO_STREAM_INDEX;
+		goto done;
+	}
+
+	AVB_PACKET_ACMP_SET_MESSAGE_TYPE(reply, AVB_ACMP_MESSAGE_TYPE_DISCONNECT_TX_RESPONSE);
+
+	stream_deactivate(stream, now);
+
+done:
+	AVB_PACKET_ACMP_SET_STATUS(reply, status);
+	return avb_server_send_packet(server, h->dest, AVB_TSN_ETH, buf, len);
+}
+
+static int handle_disconnect_tx_response(struct acmp *acmp, uint64_t now, const void *m, int len)
+{
+	struct server *server = acmp->server;
+	struct avb_ethernet_header *h;
+	struct avb_packet_acmp *reply;
+	const struct avb_packet_acmp *resp = SPA_PTROFF(m, sizeof(*h), void);
+	struct pending *pending;
+	uint16_t sequence_id;
+	struct stream *stream;
+	int res;
+
+	if (be64toh(resp->listener_guid) != server->entity_id)
+		return 0;
+
+	sequence_id = ntohs(resp->sequence_id);
+
+	pending = pending_find(acmp, PENDING_TALKER, sequence_id);
+	if (pending == NULL)
+		return 0;
+
+	h = pending->ptr;
+	pending->size = SPA_MIN((int)pending->size, len);
+	memcpy(h, m, pending->size);
+
+	reply = SPA_PTROFF(h, sizeof(*h), void);
+	reply->sequence_id = htons(pending->old_sequence_id);
+	AVB_PACKET_ACMP_SET_MESSAGE_TYPE(reply, AVB_ACMP_MESSAGE_TYPE_DISCONNECT_RX_RESPONSE);
+
+	stream = server_find_stream(server, SPA_DIRECTION_INPUT,
+			reply->listener_unique_id);
+	if (stream == NULL)
+		return 0;
+
+	stream_deactivate(stream, now);
+
+	res = avb_server_send_packet(server, h->dest, AVB_TSN_ETH, h, pending->size);
+
+	pending_free(acmp, pending);
+
+	return res;
+}
+
+static int handle_connect_rx_command(struct acmp *acmp, uint64_t now, const void *m, int len)
+{
+	struct server *server = acmp->server;
+	struct avb_ethernet_header *h;
+	const struct avb_packet_acmp *p = SPA_PTROFF(m, sizeof(*h), void);
+	struct avb_packet_acmp *cmd;
+
+	if (be64toh(p->listener_guid) != server->entity_id)
+		return 0;
+
+	h = pending_new(acmp, PENDING_TALKER, now,
+			AVB_ACMP_TIMEOUT_CONNECT_TX_COMMAND_MS, m, len);
+	if (h == NULL)
+		return -errno;
+
+	cmd = SPA_PTROFF(h, sizeof(*h), void);
+	AVB_PACKET_ACMP_SET_MESSAGE_TYPE(cmd, AVB_ACMP_MESSAGE_TYPE_CONNECT_TX_COMMAND);
+	AVB_PACKET_ACMP_SET_STATUS(cmd, AVB_ACMP_STATUS_SUCCESS);
+
+	return avb_server_send_packet(server, h->dest, AVB_TSN_ETH, h, len);
+}
+
+static int handle_ignore(struct acmp *acmp, uint64_t now, const void *m, int len)
+{
+	return 0;
+}
+
+static int handle_disconnect_rx_command(struct acmp *acmp, uint64_t now, const void *m, int len)
+{
+	struct server *server = acmp->server;
+	struct avb_ethernet_header *h;
+	const struct avb_packet_acmp *p = SPA_PTROFF(m, sizeof(*h), void);
+	struct avb_packet_acmp *cmd;
+
+	if (be64toh(p->listener_guid) != server->entity_id)
+		return 0;
+
+	h = pending_new(acmp, PENDING_TALKER, now,
+			AVB_ACMP_TIMEOUT_DISCONNECT_TX_COMMAND_MS, m, len);
+	if (h == NULL)
+		return -errno;
+
+	cmd = SPA_PTROFF(h, sizeof(*h), void);
+	AVB_PACKET_ACMP_SET_MESSAGE_TYPE(cmd, AVB_ACMP_MESSAGE_TYPE_DISCONNECT_TX_COMMAND);
+	AVB_PACKET_ACMP_SET_STATUS(cmd, AVB_ACMP_STATUS_SUCCESS);
+
+	return avb_server_send_packet(server, h->dest, AVB_TSN_ETH, h, len);
+}
+
+static const struct msg_info msg_info[] = {
+	{ AVB_ACMP_MESSAGE_TYPE_CONNECT_TX_COMMAND, "connect-tx-command", handle_connect_tx_command, },
+	{ AVB_ACMP_MESSAGE_TYPE_CONNECT_TX_RESPONSE, "connect-tx-response", handle_connect_tx_response, },
+	{ AVB_ACMP_MESSAGE_TYPE_DISCONNECT_TX_COMMAND, "disconnect-tx-command", handle_disconnect_tx_command, },
+	{ AVB_ACMP_MESSAGE_TYPE_DISCONNECT_TX_RESPONSE, "disconnect-tx-response", handle_disconnect_tx_response, },
+	{ AVB_ACMP_MESSAGE_TYPE_GET_TX_STATE_COMMAND, "get-tx-state-command", NULL, },
+	{ AVB_ACMP_MESSAGE_TYPE_GET_TX_STATE_RESPONSE, "get-tx-state-response", handle_ignore, },
+	{ AVB_ACMP_MESSAGE_TYPE_CONNECT_RX_COMMAND, "connect-rx-command", handle_connect_rx_command, },
+	{ AVB_ACMP_MESSAGE_TYPE_CONNECT_RX_RESPONSE, "connect-rx-response", handle_ignore, },
+	{ AVB_ACMP_MESSAGE_TYPE_DISCONNECT_RX_COMMAND, "disconnect-rx-command", handle_disconnect_rx_command, },
+	{ AVB_ACMP_MESSAGE_TYPE_DISCONNECT_RX_RESPONSE, "disconnect-rx-response", handle_ignore, },
+	{ AVB_ACMP_MESSAGE_TYPE_GET_RX_STATE_COMMAND, "get-rx-state-command", NULL, },
+	{ AVB_ACMP_MESSAGE_TYPE_GET_RX_STATE_RESPONSE, "get-rx-state-response", handle_ignore, },
+	{ AVB_ACMP_MESSAGE_TYPE_GET_TX_CONNECTION_COMMAND, "get-tx-connection-command", NULL, },
+	{ AVB_ACMP_MESSAGE_TYPE_GET_TX_CONNECTION_RESPONSE, "get-tx-connection-response", handle_ignore, },
+};
+
+static inline const struct msg_info *find_msg_info(uint16_t type, const char *name)
+{
+	uint32_t i;
+	for (i = 0; i < SPA_N_ELEMENTS(msg_info); i++) {
+		if ((name == NULL && type == msg_info[i].type) ||
+		    (name != NULL && spa_streq(name, msg_info[i].name)))
+			return &msg_info[i];
+	}
+	return NULL;
+}
+
+static int acmp_message(void *data, uint64_t now, const void *message, int len)
+{
+	struct acmp *acmp = data;
+	struct server *server = acmp->server;
+	const struct avb_ethernet_header *h = message;
+	const struct avb_packet_acmp *p = SPA_PTROFF(h, sizeof(*h), void);
+	const struct msg_info *info;
+	int message_type;
+
+	if (ntohs(h->type) != AVB_TSN_ETH)
+		return 0;
+	if (memcmp(h->dest, mac, 6) != 0 &&
+	    memcmp(h->dest, server->mac_addr, 6) != 0)
+		return 0;
+
+	if (AVB_PACKET_GET_SUBTYPE(&p->hdr) != AVB_SUBTYPE_ACMP)
+		return 0;
+
+	message_type = AVB_PACKET_ACMP_GET_MESSAGE_TYPE(p);
+
+	info = find_msg_info(message_type, NULL);
+	if (info == NULL)
+		return 0;
+
+	pw_log_info("got ACMP message %s", info->name);
+
+	if (info->handle == NULL)
+		return reply_not_supported(acmp, message_type | 1, message, len);
+
+	return info->handle(acmp, now, message, len);
+}
+
+static void acmp_destroy(void *data)
+{
+	struct acmp *acmp = data;
+	spa_hook_remove(&acmp->server_listener);
+	free(acmp);
+}
+
+static void check_timeout(struct acmp *acmp, uint64_t now, uint16_t type)
+{
+	struct pending *p, *t;
+
+	spa_list_for_each_safe(p, t, &acmp->pending[type], link) {
+		if (p->last_time + p->timeout > now)
+			continue;
+
+		if (p->retry == 0) {
+			pw_log_info("%p: pending timeout, retry", p);
+			retry_pending(acmp, now, p);
+		} else {
+			pw_log_info("%p: pending timeout, fail", p);
+			pending_free(acmp, p);
+		}
+	}
+}
+static void acmp_periodic(void *data, uint64_t now)
+{
+	struct acmp *acmp = data;
+	check_timeout(acmp, now, PENDING_TALKER);
+	check_timeout(acmp, now, PENDING_LISTENER);
+	check_timeout(acmp, now, PENDING_CONTROLLER);
+}
+
+static int do_help(struct acmp *acmp, const char *args, FILE *out)
+{
+	fprintf(out, "{ \"type\": \"help\","
+			"\"text\": \""
+			  "/adp/help: this help \\n"
+			"\" }");
+	return 0;
+}
+
+static int acmp_command(void *data, uint64_t now, const char *command, const char *args, FILE *out)
+{
+	struct acmp *acmp = data;
+	int res;
+
+	if (!spa_strstartswith(command, "/acmp/"))
+		return 0;
+
+	command += strlen("/acmp/");
+
+	if (spa_streq(command, "help"))
+		res = do_help(acmp, args, out);
+	else
+		res = -ENOTSUP;
+
+	return res;
+}
+
+static const struct server_events server_events = {
+	AVB_VERSION_SERVER_EVENTS,
+	.destroy = acmp_destroy,
+	.message = acmp_message,
+	.periodic = acmp_periodic,
+	.command = acmp_command
+};
+
+struct avb_acmp *avb_acmp_register(struct server *server)
+{
+	struct acmp *acmp;
+
+	acmp = calloc(1, sizeof(*acmp));
+	if (acmp == NULL)
+		return NULL;
+
+	acmp->server = server;
+	spa_list_init(&acmp->pending[PENDING_TALKER]);
+	spa_list_init(&acmp->pending[PENDING_LISTENER]);
+	spa_list_init(&acmp->pending[PENDING_CONTROLLER]);
+
+	avdecc_server_add_listener(server, &acmp->server_listener, &server_events, acmp);
+
+	return (struct avb_acmp*)acmp;
+}
+
+void avb_acmp_unregister(struct avb_acmp *acmp)
+{
+	acmp_destroy(acmp);
+}
diff --git a/src/modules/module-avb/acmp.h b/src/modules/module-avb/acmp.h
new file mode 100644
index 0000000000000000000000000000000000000000..5a41c661f2ce8b1939eb797eeaf1433f6c71ea4c
--- /dev/null
+++ b/src/modules/module-avb/acmp.h
@@ -0,0 +1,99 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef AVB_ACMP_H
+#define AVB_ACMP_H
+
+#include "packets.h"
+#include "internal.h"
+
+#define AVB_ACMP_MESSAGE_TYPE_CONNECT_TX_COMMAND		0
+#define AVB_ACMP_MESSAGE_TYPE_CONNECT_TX_RESPONSE		1
+#define AVB_ACMP_MESSAGE_TYPE_DISCONNECT_TX_COMMAND		2
+#define AVB_ACMP_MESSAGE_TYPE_DISCONNECT_TX_RESPONSE		3
+#define AVB_ACMP_MESSAGE_TYPE_GET_TX_STATE_COMMAND		4
+#define AVB_ACMP_MESSAGE_TYPE_GET_TX_STATE_RESPONSE		5
+#define AVB_ACMP_MESSAGE_TYPE_CONNECT_RX_COMMAND		6
+#define AVB_ACMP_MESSAGE_TYPE_CONNECT_RX_RESPONSE		7
+#define AVB_ACMP_MESSAGE_TYPE_DISCONNECT_RX_COMMAND		8
+#define AVB_ACMP_MESSAGE_TYPE_DISCONNECT_RX_RESPONSE		9
+#define AVB_ACMP_MESSAGE_TYPE_GET_RX_STATE_COMMAND		10
+#define AVB_ACMP_MESSAGE_TYPE_GET_RX_STATE_RESPONSE		11
+#define AVB_ACMP_MESSAGE_TYPE_GET_TX_CONNECTION_COMMAND		12
+#define AVB_ACMP_MESSAGE_TYPE_GET_TX_CONNECTION_RESPONSE	13
+
+#define AVB_ACMP_STATUS_SUCCESS				0
+#define AVB_ACMP_STATUS_LISTENER_UNKNOWN_ID		1
+#define AVB_ACMP_STATUS_TALKER_UNKNOWN_ID		2
+#define AVB_ACMP_STATUS_TALKER_DEST_MAC_FAIL		3
+#define AVB_ACMP_STATUS_TALKER_NO_STREAM_INDEX		4
+#define AVB_ACMP_STATUS_TALKER_NO_BANDWIDTH		5
+#define AVB_ACMP_STATUS_TALKER_EXCLUSIVE		6
+#define AVB_ACMP_STATUS_LISTENER_TALKER_TIMEOUT		7
+#define AVB_ACMP_STATUS_LISTENER_EXCLUSIVE		8
+#define AVB_ACMP_STATUS_STATE_UNAVAILABLE		9
+#define AVB_ACMP_STATUS_NOT_CONNECTED			10
+#define AVB_ACMP_STATUS_NO_SUCH_CONNECTION		11
+#define AVB_ACMP_STATUS_COULD_NOT_SEND_MESSAGE		12
+#define AVB_ACMP_STATUS_TALKER_MISBEHAVING		13
+#define AVB_ACMP_STATUS_LISTENER_MISBEHAVING		14
+#define AVB_ACMP_STATUS_RESERVED			15
+#define AVB_ACMP_STATUS_CONTROLLER_NOT_AUTHORIZED	16
+#define AVB_ACMP_STATUS_INCOMPATIBLE_REQUEST		17
+#define AVB_ACMP_STATUS_LISTENER_INVALID_CONNECTION	18
+#define AVB_ACMP_STATUS_NOT_SUPPORTED			31
+
+#define AVB_ACMP_TIMEOUT_CONNECT_TX_COMMAND_MS		2000
+#define AVB_ACMP_TIMEOUT_DISCONNECT_TX_COMMAND_MS	200
+#define AVB_ACMP_TIMEOUT_GET_TX_STATE_COMMAND		200
+#define AVB_ACMP_TIMEOUT_CONNECT_RX_COMMAND_MS		4500
+#define AVB_ACMP_TIMEOUT_DISCONNECT_RX_COMMAND_MS	500
+#define AVB_ACMP_TIMEOUT_GET_RX_STATE_COMMAND_MS	200
+#define AVB_ACMP_TIMEOUT_GET_TX_CONNECTION_COMMAND	200
+
+struct avb_packet_acmp {
+	struct avb_packet_header hdr;
+	uint64_t stream_id;
+	uint64_t controller_guid;
+	uint64_t talker_guid;
+	uint64_t listener_guid;
+	uint16_t talker_unique_id;
+	uint16_t listener_unique_id;
+	char stream_dest_mac[6];
+	uint16_t connection_count;
+	uint16_t sequence_id;
+	uint16_t flags;
+	uint16_t stream_vlan_id;
+	uint16_t reserved;
+} __attribute__ ((__packed__));
+
+#define AVB_PACKET_ACMP_SET_MESSAGE_TYPE(p,v)		AVB_PACKET_SET_SUB1(&(p)->hdr, v)
+#define AVB_PACKET_ACMP_SET_STATUS(p,v)			AVB_PACKET_SET_SUB2(&(p)->hdr, v)
+
+#define AVB_PACKET_ACMP_GET_MESSAGE_TYPE(p)		AVB_PACKET_GET_SUB1(&(p)->hdr)
+#define AVB_PACKET_ACMP_GET_STATUS(p)			AVB_PACKET_GET_SUB2(&(p)->hdr)
+
+struct avb_acmp *avb_acmp_register(struct server *server);
+
+#endif /* AVB_ACMP_H */
diff --git a/src/modules/module-avb/adp.c b/src/modules/module-avb/adp.c
new file mode 100644
index 0000000000000000000000000000000000000000..6b13c41b7739ff11445cec48a16df62aa9fa7b61
--- /dev/null
+++ b/src/modules/module-avb/adp.c
@@ -0,0 +1,381 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <spa/utils/json.h>
+
+#include <pipewire/pipewire.h>
+
+#include "adp.h"
+#include "aecp-aem-descriptors.h"
+#include "internal.h"
+#include "utils.h"
+
+static const uint8_t mac[6] = AVB_BROADCAST_MAC;
+
+struct entity {
+	struct spa_list link;
+	uint64_t entity_id;
+	uint64_t last_time;
+	int valid_time;
+	unsigned advertise:1;
+	size_t len;
+	uint8_t buf[128];
+};
+
+struct adp {
+	struct server *server;
+	struct spa_hook server_listener;
+
+	struct spa_list entities;
+	uint32_t available_index;
+};
+
+static struct entity *find_entity_by_id(struct adp *adp, uint64_t id)
+{
+	struct entity *e;
+	spa_list_for_each(e, &adp->entities, link)
+		if (e->entity_id == id)
+			return e;
+	return NULL;
+}
+static void entity_free(struct entity *e)
+{
+	spa_list_remove(&e->link);
+	free(e);
+}
+
+static int send_departing(struct adp *adp, uint64_t now, struct entity *e)
+{
+	struct avb_ethernet_header *h = (void*)e->buf;
+	struct avb_packet_adp *p = SPA_PTROFF(h, sizeof(*h), void);
+
+	AVB_PACKET_ADP_SET_MESSAGE_TYPE(p, AVB_ADP_MESSAGE_TYPE_ENTITY_DEPARTING);
+	p->available_index = htonl(adp->available_index++);
+	avb_server_send_packet(adp->server, mac, AVB_TSN_ETH, e->buf, e->len);
+	e->last_time = now;
+	return 0;
+}
+
+static int send_advertise(struct adp *adp, uint64_t now, struct entity *e)
+{
+	struct avb_ethernet_header *h = (void*)e->buf;
+	struct avb_packet_adp *p = SPA_PTROFF(h, sizeof(*h), void);
+
+	AVB_PACKET_ADP_SET_MESSAGE_TYPE(p, AVB_ADP_MESSAGE_TYPE_ENTITY_AVAILABLE);
+	p->available_index = htonl(adp->available_index++);
+	avb_server_send_packet(adp->server, mac, AVB_TSN_ETH, e->buf, e->len);
+	e->last_time = now;
+	return 0;
+}
+
+static int send_discover(struct adp *adp, uint64_t entity_id)
+{
+	uint8_t buf[128];
+	struct avb_ethernet_header *h = (void*)buf;
+	struct avb_packet_adp *p = SPA_PTROFF(h, sizeof(*h), void);
+	size_t len = sizeof(*h) + sizeof(*p);
+
+	spa_memzero(buf, sizeof(buf));
+	AVB_PACKET_SET_SUBTYPE(&p->hdr, AVB_SUBTYPE_ADP);
+	AVB_PACKET_SET_LENGTH(&p->hdr, AVB_ADP_CONTROL_DATA_LENGTH);
+	AVB_PACKET_ADP_SET_MESSAGE_TYPE(p, AVB_ADP_MESSAGE_TYPE_ENTITY_DISCOVER);
+	p->entity_id = htonl(entity_id);
+	avb_server_send_packet(adp->server, mac, AVB_TSN_ETH, buf, len);
+	return 0;
+}
+
+static int adp_message(void *data, uint64_t now, const void *message, int len)
+{
+	struct adp *adp = data;
+	struct server *server = adp->server;
+	const struct avb_ethernet_header *h = message;
+	const struct avb_packet_adp *p = SPA_PTROFF(h, sizeof(*h), void);
+	struct entity *e;
+	int message_type;
+	char buf[128];
+	uint64_t entity_id;
+
+	if (ntohs(h->type) != AVB_TSN_ETH)
+		return 0;
+	if (memcmp(h->dest, mac, 6) != 0 &&
+	    memcmp(h->dest, server->mac_addr, 6) != 0)
+		return 0;
+
+	if (AVB_PACKET_GET_SUBTYPE(&p->hdr) != AVB_SUBTYPE_ADP ||
+	    AVB_PACKET_GET_LENGTH(&p->hdr) < AVB_ADP_CONTROL_DATA_LENGTH)
+		return 0;
+
+	message_type = AVB_PACKET_ADP_GET_MESSAGE_TYPE(p);
+	entity_id = be64toh(p->entity_id);
+
+	e = find_entity_by_id(adp, entity_id);
+
+	switch (message_type) {
+	case AVB_ADP_MESSAGE_TYPE_ENTITY_AVAILABLE:
+		if (e == NULL) {
+			e = calloc(1, sizeof(*e));
+			if (e == NULL)
+				return -errno;
+
+			memcpy(e->buf, message, len);
+			e->len = len;
+			e->valid_time = AVB_PACKET_ADP_GET_VALID_TIME(p);
+			e->entity_id = entity_id;
+			spa_list_append(&adp->entities, &e->link);
+			pw_log_info("entity %s available",
+				avb_utils_format_id(buf, sizeof(buf), entity_id));
+		}
+		e->last_time = now;
+		break;
+	case AVB_ADP_MESSAGE_TYPE_ENTITY_DEPARTING:
+		if (e != NULL) {
+			pw_log_info("entity %s departing",
+				avb_utils_format_id(buf, sizeof(buf), entity_id));
+			entity_free(e);
+		}
+		break;
+	case AVB_ADP_MESSAGE_TYPE_ENTITY_DISCOVER:
+		pw_log_info("entity %s advertise",
+				avb_utils_format_id(buf, sizeof(buf), entity_id));
+		if (entity_id == 0UL) {
+			spa_list_for_each(e, &adp->entities, link)
+				if (e->advertise)
+					send_advertise(adp, now, e);
+		} else if (e != NULL &&
+		    e->advertise && e->entity_id == entity_id) {
+			send_advertise(adp, now, e);
+		}
+		break;
+	default:
+		return -EINVAL;
+	}
+	return 0;
+}
+
+static void adp_destroy(void *data)
+{
+	struct adp *adp = data;
+	spa_hook_remove(&adp->server_listener);
+	free(adp);
+}
+
+static void check_timeout(struct adp *adp, uint64_t now)
+{
+	struct entity *e, *t;
+	char buf[128];
+
+	spa_list_for_each_safe(e, t, &adp->entities, link) {
+		if (e->last_time + (e->valid_time + 2) * SPA_NSEC_PER_SEC > now)
+			continue;
+
+		pw_log_info("entity %s timeout",
+			avb_utils_format_id(buf, sizeof(buf), e->entity_id));
+
+		if (e->advertise)
+			send_departing(adp, now, e);
+
+		entity_free(e);
+	}
+}
+static void check_readvertize(struct adp *adp, uint64_t now, struct entity *e)
+{
+	char buf[128];
+
+	if (!e->advertise)
+		return;
+
+	if (e->last_time + (e->valid_time / 2) * SPA_NSEC_PER_SEC > now)
+		return;
+
+	pw_log_debug("entity %s readvertise",
+		avb_utils_format_id(buf, sizeof(buf), e->entity_id));
+
+	send_advertise(adp, now, e);
+}
+
+static int check_advertise(struct adp *adp, uint64_t now)
+{
+	struct server *server = adp->server;
+	const struct descriptor *d;
+	struct avb_aem_desc_entity *entity;
+	struct avb_aem_desc_avb_interface *avb_interface;
+	struct entity *e;
+	uint64_t entity_id;
+	struct avb_ethernet_header *h;
+	struct avb_packet_adp *p;
+	char buf[128];
+
+	d = server_find_descriptor(server, AVB_AEM_DESC_ENTITY, 0);
+	if (d == NULL)
+		return 0;
+
+	entity = d->ptr;
+	entity_id = be64toh(entity->entity_id);
+
+	if ((e = find_entity_by_id(adp, entity_id)) != NULL) {
+		if (e->advertise)
+			check_readvertize(adp, now, e);
+		return 0;
+	}
+
+	d = server_find_descriptor(server, AVB_AEM_DESC_AVB_INTERFACE, 0);
+	avb_interface = d ? d->ptr : NULL;
+
+	pw_log_info("entity %s advertise",
+		avb_utils_format_id(buf, sizeof(buf), entity_id));
+
+	e = calloc(1, sizeof(*e));
+	if (e == NULL)
+		return -errno;
+
+	e->advertise = true;
+	e->valid_time = 10;
+	e->last_time = now;
+	e->entity_id = entity_id;
+	e->len = sizeof(*h) + sizeof(*p);
+
+	h = (void*)e->buf;
+	p = SPA_PTROFF(h, sizeof(*h), void);
+	AVB_PACKET_SET_LENGTH(&p->hdr, AVB_ADP_CONTROL_DATA_LENGTH);
+	AVB_PACKET_SET_SUBTYPE(&p->hdr, AVB_SUBTYPE_ADP);
+	AVB_PACKET_ADP_SET_MESSAGE_TYPE(p, AVB_ADP_MESSAGE_TYPE_ENTITY_AVAILABLE);
+	AVB_PACKET_ADP_SET_VALID_TIME(p, e->valid_time);
+
+	p->entity_id = entity->entity_id;
+	p->entity_model_id = entity->entity_model_id;
+	p->entity_capabilities = entity->entity_capabilities;
+	p->talker_stream_sources = entity->talker_stream_sources;
+	p->talker_capabilities = entity->talker_capabilities;
+	p->listener_stream_sinks = entity->listener_stream_sinks;
+	p->listener_capabilities = entity->listener_capabilities;
+	p->controller_capabilities = entity->controller_capabilities;
+	p->available_index = entity->available_index;
+	if (avb_interface) {
+		p->gptp_grandmaster_id = avb_interface->clock_identity;
+		p->gptp_domain_number = avb_interface->domain_number;
+	}
+	p->identify_control_index = 0;
+	p->interface_index = 0;
+	p->association_id = entity->association_id;
+
+	spa_list_append(&adp->entities, &e->link);
+
+	return 0;
+}
+
+static void adp_periodic(void *data, uint64_t now)
+{
+	struct adp *adp = data;
+	check_timeout(adp, now);
+	check_advertise(adp, now);
+}
+
+static int do_help(struct adp *adp, const char *args, FILE *out)
+{
+	fprintf(out, "{ \"type\": \"help\","
+			"\"text\": \""
+			  "/adp/help: this help \\n"
+			  "/adp/discover [{ \"entity-id\": <id> }] : trigger discover\\n"
+			"\" }");
+	return 0;
+}
+
+static int do_discover(struct adp *adp, const char *args, FILE *out)
+{
+	struct spa_json it[2];
+	char key[128];
+	uint64_t entity_id = 0ULL;
+
+	spa_json_init(&it[0], args, strlen(args));
+	if (spa_json_enter_object(&it[0], &it[1]) <= 0)
+		return -EINVAL;
+
+	while (spa_json_get_string(&it[1], key, sizeof(key)) > 0) {
+		int len;
+		const char *value;
+		uint64_t id_val;
+
+		if ((len = spa_json_next(&it[1], &value)) <= 0)
+			break;
+
+		if (spa_json_is_null(value, len))
+			continue;
+
+		if (spa_streq(key, "entity-id")) {
+			if (avb_utils_parse_id(value, len, &id_val) >= 0)
+				entity_id = id_val;
+		}
+	}
+	send_discover(adp, entity_id);
+	return 0;
+}
+
+static int adp_command(void *data, uint64_t now, const char *command, const char *args, FILE *out)
+{
+	struct adp *adp = data;
+	int res;
+
+	if (!spa_strstartswith(command, "/adp/"))
+		return 0;
+
+	command += strlen("/adp/");
+
+	if (spa_streq(command, "help"))
+		res = do_help(adp, args, out);
+	else if (spa_streq(command, "discover"))
+		res = do_discover(adp, args, out);
+	else
+		res = -ENOTSUP;
+
+	return res;
+}
+
+static const struct server_events server_events = {
+	AVB_VERSION_SERVER_EVENTS,
+	.destroy = adp_destroy,
+	.message = adp_message,
+	.periodic = adp_periodic,
+	.command = adp_command
+};
+
+struct avb_adp *avb_adp_register(struct server *server)
+{
+	struct adp *adp;
+
+	adp = calloc(1, sizeof(*adp));
+	if (adp == NULL)
+		return NULL;
+
+	adp->server = server;
+	spa_list_init(&adp->entities);
+
+	avdecc_server_add_listener(server, &adp->server_listener, &server_events, adp);
+
+	return (struct avb_adp*)adp;
+}
+
+void avb_adp_unregister(struct avb_adp *adp)
+{
+	adp_destroy(adp);
+}
diff --git a/src/modules/module-avb/adp.h b/src/modules/module-avb/adp.h
new file mode 100644
index 0000000000000000000000000000000000000000..c546088d8ac130b7dfb1f5939ea6057efd2d78df
--- /dev/null
+++ b/src/modules/module-avb/adp.h
@@ -0,0 +1,105 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef AVB_ADP_H
+#define AVB_ADP_H
+
+#include "packets.h"
+#include "internal.h"
+
+#define AVB_ADP_MESSAGE_TYPE_ENTITY_AVAILABLE		0
+#define AVB_ADP_MESSAGE_TYPE_ENTITY_DEPARTING		1
+#define AVB_ADP_MESSAGE_TYPE_ENTITY_DISCOVER		2
+
+#define AVB_ADP_ENTITY_CAPABILITY_EFU_MODE				(1u<<0)
+#define AVB_ADP_ENTITY_CAPABILITY_ADDRESS_ACCESS_SUPPORTED		(1u<<1)
+#define AVB_ADP_ENTITY_CAPABILITY_GATEWAY_ENTITY			(1u<<2)
+#define AVB_ADP_ENTITY_CAPABILITY_AEM_SUPPORTED				(1u<<3)
+#define AVB_ADP_ENTITY_CAPABILITY_LEGACY_AVC				(1u<<4)
+#define AVB_ADP_ENTITY_CAPABILITY_ASSOCIATION_ID_SUPPORTED		(1u<<5)
+#define AVB_ADP_ENTITY_CAPABILITY_ASSOCIATION_ID_VALID			(1u<<6)
+#define AVB_ADP_ENTITY_CAPABILITY_VENDOR_UNIQUE_SUPPORTED		(1u<<7)
+#define AVB_ADP_ENTITY_CAPABILITY_CLASS_A_SUPPORTED			(1u<<8)
+#define AVB_ADP_ENTITY_CAPABILITY_CLASS_B_SUPPORTED			(1u<<9)
+#define AVB_ADP_ENTITY_CAPABILITY_GPTP_SUPPORTED			(1u<<10)
+#define AVB_ADP_ENTITY_CAPABILITY_AEM_AUTHENTICATION_SUPPORTED		(1u<<11)
+#define AVB_ADP_ENTITY_CAPABILITY_AEM_AUTHENTICATION_REQUIRED		(1u<<12)
+#define AVB_ADP_ENTITY_CAPABILITY_AEM_PERSISTENT_ACQUIRE_SUPPORTED	(1u<<13)
+#define AVB_ADP_ENTITY_CAPABILITY_AEM_IDENTIFY_CONTROL_INDEX_VALID	(1u<<14)
+#define AVB_ADP_ENTITY_CAPABILITY_AEM_INTERFACE_INDEX_VALID		(1u<<15)
+#define AVB_ADP_ENTITY_CAPABILITY_GENERAL_CONTROLLER_IGNORE		(1u<<16)
+#define AVB_ADP_ENTITY_CAPABILITY_ENTITY_NOT_READY			(1u<<17)
+
+#define AVB_ADP_TALKER_CAPABILITY_IMPLEMENTED				(1u<<0)
+#define AVB_ADP_TALKER_CAPABILITY_OTHER_SOURCE				(1u<<9)
+#define AVB_ADP_TALKER_CAPABILITY_CONTROL_SOURCE			(1u<<10)
+#define AVB_ADP_TALKER_CAPABILITY_MEDIA_CLOCK_SOURCE			(1u<<11)
+#define AVB_ADP_TALKER_CAPABILITY_SMPTE_SOURCE				(1u<<12)
+#define AVB_ADP_TALKER_CAPABILITY_MIDI_SOURCE				(1u<<13)
+#define AVB_ADP_TALKER_CAPABILITY_AUDIO_SOURCE				(1u<<14)
+#define AVB_ADP_TALKER_CAPABILITY_VIDEO_SOURCE				(1u<<15)
+
+#define AVB_ADP_LISTENER_CAPABILITY_IMPLEMENTED				(1u<<0)
+#define AVB_ADP_LISTENER_CAPABILITY_OTHER_SINK				(1u<<9)
+#define AVB_ADP_LISTENER_CAPABILITY_CONTROL_SINK			(1u<<10)
+#define AVB_ADP_LISTENER_CAPABILITY_MEDIA_CLOCK_SINK			(1u<<11)
+#define AVB_ADP_LISTENER_CAPABILITY_SMPTE_SINK				(1u<<12)
+#define AVB_ADP_LISTENER_CAPABILITY_MIDI_SINK				(1u<<13)
+#define AVB_ADP_LISTENER_CAPABILITY_AUDIO_SINK				(1u<<14)
+#define AVB_ADP_LISTENER_CAPABILITY_VIDEO_SINK				(1u<<15)
+
+#define AVB_ADP_CONTROLLER_CAPABILITY_IMPLEMENTED			(1u<<0)
+#define AVB_ADP_CONTROLLER_CAPABILITY_LAYER3_PROXY			(1u<<1)
+
+#define AVB_ADP_CONTROL_DATA_LENGTH		56
+
+struct avb_packet_adp {
+	struct avb_packet_header hdr;
+	uint64_t entity_id;
+	uint64_t entity_model_id;
+	uint32_t entity_capabilities;
+	uint16_t talker_stream_sources;
+	uint16_t talker_capabilities;
+	uint16_t listener_stream_sinks;
+	uint16_t listener_capabilities;
+	uint32_t controller_capabilities;
+	uint32_t available_index;
+	uint64_t gptp_grandmaster_id;
+	uint8_t gptp_domain_number;
+	uint8_t reserved0[3];
+	uint16_t identify_control_index;
+	uint16_t interface_index;
+	uint64_t association_id;
+	uint32_t reserved1;
+} __attribute__ ((__packed__));
+
+#define AVB_PACKET_ADP_SET_MESSAGE_TYPE(p,v)		AVB_PACKET_SET_SUB1(&(p)->hdr, v)
+#define AVB_PACKET_ADP_SET_VALID_TIME(p,v)		AVB_PACKET_SET_SUB2(&(p)->hdr, v)
+
+#define AVB_PACKET_ADP_GET_MESSAGE_TYPE(p)		AVB_PACKET_GET_SUB1(&(p)->hdr)
+#define AVB_PACKET_ADP_GET_VALID_TIME(p)		AVB_PACKET_GET_SUB2(&(p)->hdr)
+
+struct avb_adp *avb_adp_register(struct server *server);
+
+#endif /* AVB_ADP_H */
diff --git a/src/modules/module-avb/aecp-aem-descriptors.h b/src/modules/module-avb/aecp-aem-descriptors.h
new file mode 100644
index 0000000000000000000000000000000000000000..101c33168f23ef276bb4acebb2d99f6511fa22ce
--- /dev/null
+++ b/src/modules/module-avb/aecp-aem-descriptors.h
@@ -0,0 +1,247 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef AVB_AECP_AEM_DESCRIPTORS_H
+#define AVB_AECP_AEM_DESCRIPTORS_H
+
+#include "internal.h"
+
+#define AVB_AEM_DESC_ENTITY			0x0000
+#define AVB_AEM_DESC_CONFIGURATION		0x0001
+#define AVB_AEM_DESC_AUDIO_UNIT			0x0002
+#define AVB_AEM_DESC_VIDEO_UNIT			0x0003
+#define AVB_AEM_DESC_SENSOR_UNIT		0x0004
+#define AVB_AEM_DESC_STREAM_INPUT		0x0005
+#define AVB_AEM_DESC_STREAM_OUTPUT		0x0006
+#define AVB_AEM_DESC_JACK_INPUT			0x0007
+#define AVB_AEM_DESC_JACK_OUTPUT		0x0008
+#define AVB_AEM_DESC_AVB_INTERFACE		0x0009
+#define AVB_AEM_DESC_CLOCK_SOURCE		0x000a
+#define AVB_AEM_DESC_MEMORY_OBJECT		0x000b
+#define AVB_AEM_DESC_LOCALE			0x000c
+#define AVB_AEM_DESC_STRINGS			0x000d
+#define AVB_AEM_DESC_STREAM_PORT_INPUT		0x000e
+#define AVB_AEM_DESC_STREAM_PORT_OUTPUT		0x000f
+#define AVB_AEM_DESC_EXTERNAL_PORT_INPUT	0x0010
+#define AVB_AEM_DESC_EXTERNAL_PORT_OUTPUT	0x0011
+#define AVB_AEM_DESC_INTERNAL_PORT_INPUT	0x0012
+#define AVB_AEM_DESC_INTERNAL_PORT_OUTPUT	0x0013
+#define AVB_AEM_DESC_AUDIO_CLUSTER		0x0014
+#define AVB_AEM_DESC_VIDEO_CLUSTER		0x0015
+#define AVB_AEM_DESC_SENSOR_CLUSTER		0x0016
+#define AVB_AEM_DESC_AUDIO_MAP			0x0017
+#define AVB_AEM_DESC_VIDEO_MAP			0x0018
+#define AVB_AEM_DESC_SENSOR_MAP			0x0019
+#define AVB_AEM_DESC_CONTROL			0x001a
+#define AVB_AEM_DESC_SIGNAL_SELECTOR		0x001b
+#define AVB_AEM_DESC_MIXER			0x001c
+#define AVB_AEM_DESC_MATRIX			0x001d
+#define AVB_AEM_DESC_MATRIX_SIGNAL		0x001e
+#define AVB_AEM_DESC_SIGNAL_SPLITTER		0x001f
+#define AVB_AEM_DESC_SIGNAL_COMBINER		0x0020
+#define AVB_AEM_DESC_SIGNAL_DEMULTIPLEXER	0x0021
+#define AVB_AEM_DESC_SIGNAL_MULTIPLEXER		0x0022
+#define AVB_AEM_DESC_SIGNAL_TRANSCODER		0x0023
+#define AVB_AEM_DESC_CLOCK_DOMAIN		0x0024
+#define AVB_AEM_DESC_CONTROL_BLOCK		0x0025
+#define AVB_AEM_DESC_INVALID			0xffff
+
+struct avb_aem_desc_entity {
+	uint64_t entity_id;
+	uint64_t entity_model_id;
+	uint32_t entity_capabilities;
+	uint16_t talker_stream_sources;
+	uint16_t talker_capabilities;
+	uint16_t listener_stream_sinks;
+	uint16_t listener_capabilities;
+	uint32_t controller_capabilities;
+	uint32_t available_index;
+	uint64_t association_id;
+	char entity_name[64];
+	uint16_t vendor_name_string;
+	uint16_t model_name_string;
+	char firmware_version[64];
+	char group_name[64];
+	char serial_number[64];
+	uint16_t configurations_count;
+	uint16_t current_configuration;
+} __attribute__ ((__packed__));
+
+struct avb_aem_desc_descriptor_count {
+	uint16_t descriptor_type;
+	uint16_t descriptor_count;
+} __attribute__ ((__packed__));
+
+struct avb_aem_desc_configuration {
+	char object_name[64];
+	uint16_t localized_description;
+	uint16_t descriptor_counts_count;
+	uint16_t descriptor_counts_offset;
+	struct avb_aem_desc_descriptor_count descriptor_counts[0];
+} __attribute__ ((__packed__));
+
+struct avb_aem_desc_sampling_rate {
+	uint32_t pull_frequency;
+} __attribute__ ((__packed__));
+
+struct avb_aem_desc_audio_unit {
+	char object_name[64];
+	uint16_t localized_description;
+	uint16_t clock_domain_index;
+	uint16_t number_of_stream_input_ports;
+	uint16_t base_stream_input_port;
+	uint16_t number_of_stream_output_ports;
+	uint16_t base_stream_output_port;
+	uint16_t number_of_external_input_ports;
+	uint16_t base_external_input_port;
+	uint16_t number_of_external_output_ports;
+	uint16_t base_external_output_port;
+	uint16_t number_of_internal_input_ports;
+	uint16_t base_internal_input_port;
+	uint16_t number_of_internal_output_ports;
+	uint16_t base_internal_output_port;
+	uint16_t number_of_controls;
+	uint16_t base_control;
+	uint16_t number_of_signal_selectors;
+	uint16_t base_signal_selector;
+	uint16_t number_of_mixers;
+	uint16_t base_mixer;
+	uint16_t number_of_matrices;
+	uint16_t base_matrix;
+	uint16_t number_of_splitters;
+	uint16_t base_splitter;
+	uint16_t number_of_combiners;
+	uint16_t base_combiner;
+	uint16_t number_of_demultiplexers;
+	uint16_t base_demultiplexer;
+	uint16_t number_of_multiplexers;
+	uint16_t base_multiplexer;
+	uint16_t number_of_transcoders;
+	uint16_t base_transcoder;
+	uint16_t number_of_control_blocks;
+	uint16_t base_control_block;
+	uint32_t current_sampling_rate;
+	uint16_t sampling_rates_offset;
+	uint16_t sampling_rates_count;
+	struct avb_aem_desc_sampling_rate sampling_rates[0];
+} __attribute__ ((__packed__));
+
+#define AVB_AEM_DESC_STREAM_FLAG_SYNC_SOURCE			(1u<<0)
+#define AVB_AEM_DESC_STREAM_FLAG_CLASS_A			(1u<<1)
+#define AVB_AEM_DESC_STREAM_FLAG_CLASS_B			(1u<<2)
+#define AVB_AEM_DESC_STREAM_FLAG_SUPPORTS_ENCRYPTED		(1u<<3)
+#define AVB_AEM_DESC_STREAM_FLAG_PRIMARY_BACKUP_SUPPORTED	(1u<<4)
+#define AVB_AEM_DESC_STREAM_FLAG_PRIMARY_BACKUP_VALID		(1u<<5)
+#define AVB_AEM_DESC_STREAM_FLAG_SECONDARY_BACKUP_SUPPORTED	(1u<<6)
+#define AVB_AEM_DESC_STREAM_FLAG_SECONDARY_BACKUP_VALID		(1u<<7)
+#define AVB_AEM_DESC_STREAM_FLAG_TERTIARY_BACKUP_SUPPORTED	(1u<<8)
+#define AVB_AEM_DESC_STREAM_FLAG_TERTIARY_BACKUP_VALID		(1u<<9)
+
+struct avb_aem_desc_stream {
+	char object_name[64];
+	uint16_t localized_description;
+	uint16_t clock_domain_index;
+	uint16_t stream_flags;
+	uint64_t current_format;
+	uint16_t formats_offset;
+	uint16_t number_of_formats;
+	uint64_t backup_talker_entity_id_0;
+	uint16_t backup_talker_unique_id_0;
+	uint64_t backup_talker_entity_id_1;
+	uint16_t backup_talker_unique_id_1;
+	uint64_t backup_talker_entity_id_2;
+	uint16_t backup_talker_unique_id_2;
+	uint64_t backedup_talker_entity_id;
+	uint16_t backedup_talker_unique;
+	uint16_t avb_interface_index;
+	uint32_t buffer_length;
+	uint64_t stream_formats[0];
+} __attribute__ ((__packed__));
+
+#define AVB_AEM_DESC_AVB_INTERFACE_FLAG_GPTP_GRANDMASTER_SUPPORTED	(1<<0)
+#define AVB_AEM_DESC_AVB_INTERFACE_FLAG_GPTP_SUPPORTED			(1<<1)
+#define AVB_AEM_DESC_AVB_INTERFACE_FLAG_SRP_SUPPORTED			(1<<2)
+
+struct avb_aem_desc_avb_interface {
+	char object_name[64];
+	uint16_t localized_description;
+	uint8_t mac_address[6];
+	uint16_t interface_flags;
+	uint64_t clock_identity;
+	uint8_t priority1;
+	uint8_t clock_class;
+	uint16_t offset_scaled_log_variance;
+	uint8_t clock_accuracy;
+	uint8_t priority2;
+	uint8_t domain_number;
+	int8_t log_sync_interval;
+	int8_t log_announce_interval;
+	int8_t log_pdelay_interval;
+	uint16_t port_number;
+} __attribute__ ((__packed__));
+
+#define AVB_AEM_DESC_CLOCK_SOURCE_TYPE_INTERNAL			0x0000
+#define AVB_AEM_DESC_CLOCK_SOURCE_TYPE_EXTERNAL			0x0001
+#define AVB_AEM_DESC_CLOCK_SOURCE_TYPE_INPUT_STREAM		0x0002
+#define AVB_AEM_DESC_CLOCK_SOURCE_TYPE_MEDIA_CLOCK_STREAM	0x0003
+#define AVB_AEM_DESC_CLOCK_SOURCE_TYPE_EXPANSION		0xffff
+
+struct avb_aem_desc_clock_source {
+	char object_name[64];
+	uint16_t localized_description;
+	uint16_t clock_source_flags;
+	uint16_t clock_source_type;
+	uint64_t clock_source_identifier;
+	uint16_t clock_source_location_type;
+	uint16_t clock_source_location_index;
+} __attribute__ ((__packed__));
+
+struct avb_aem_desc_locale {
+	char locale_identifier[64];
+	uint16_t number_of_strings;
+	uint16_t base_strings;
+} __attribute__ ((__packed__));
+
+struct avb_aem_desc_strings {
+	char string_0[64];
+	char string_1[64];
+	char string_2[64];
+	char string_3[64];
+	char string_4[64];
+	char string_5[64];
+	char string_6[64];
+} __attribute__ ((__packed__));
+
+struct avb_aem_desc_stream_port {
+	uint16_t clock_domain_index;
+	uint16_t port_flags;
+	uint16_t number_of_controls;
+	uint16_t base_control;
+	uint16_t number_of_clusters;
+	uint16_t base_cluster;
+	uint16_t number_of_maps;
+	uint16_t base_map;
+} __attribute__ ((__packed__));
+
+#endif /* AVB_AECP_AEM_DESCRIPTORS_H */
diff --git a/src/modules/module-avb/aecp-aem.c b/src/modules/module-avb/aecp-aem.c
new file mode 100644
index 0000000000000000000000000000000000000000..22a2ac2dd7ec4761626034bd563115eddf4ffe0c
--- /dev/null
+++ b/src/modules/module-avb/aecp-aem.c
@@ -0,0 +1,286 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "aecp-aem.h"
+#include "aecp-aem-descriptors.h"
+
+static int reply_status(struct aecp *aecp, int status, const void *m, int len)
+{
+	struct server *server = aecp->server;
+	uint8_t buf[len];
+	struct avb_ethernet_header *h = (void*)buf;
+	struct avb_packet_aecp_header *reply = SPA_PTROFF(h, sizeof(*h), void);
+
+	memcpy(buf, m, len);
+	AVB_PACKET_AECP_SET_MESSAGE_TYPE(reply, AVB_AECP_MESSAGE_TYPE_AEM_RESPONSE);
+	AVB_PACKET_AECP_SET_STATUS(reply, status);
+
+	return avb_server_send_packet(server, h->src, AVB_TSN_ETH, buf, len);
+}
+
+static int reply_not_implemented(struct aecp *aecp, const void *m, int len)
+{
+	return reply_status(aecp, AVB_AECP_AEM_STATUS_NOT_IMPLEMENTED, m, len);
+}
+
+static int reply_success(struct aecp *aecp, const void *m, int len)
+{
+	return reply_status(aecp, AVB_AECP_AEM_STATUS_SUCCESS, m, len);
+}
+
+/* ACQUIRE_ENTITY */
+static int handle_acquire_entity(struct aecp *aecp, const void *m, int len)
+{
+	struct server *server = aecp->server;
+	const struct avb_packet_aecp_aem *p = m;
+	const struct avb_packet_aecp_aem_acquire *ae;
+	const struct descriptor *desc;
+	uint16_t desc_type, desc_id;
+
+	ae = (const struct avb_packet_aecp_aem_acquire*)p->payload;
+
+	desc_type = ntohs(ae->descriptor_type);
+	desc_id = ntohs(ae->descriptor_id);
+
+	desc = server_find_descriptor(server, desc_type, desc_id);
+	if (desc == NULL)
+		return reply_status(aecp, AVB_AECP_AEM_STATUS_NO_SUCH_DESCRIPTOR, p, len);
+
+	if (desc_type != AVB_AEM_DESC_ENTITY || desc_id != 0)
+		return reply_not_implemented(aecp, m, len);
+
+	return reply_success(aecp, m, len);
+}
+
+/* LOCK_ENTITY */
+static int handle_lock_entity(struct aecp *aecp, const void *m, int len)
+{
+	struct server *server = aecp->server;
+	const struct avb_packet_aecp_aem *p = m;
+	const struct avb_packet_aecp_aem_acquire *ae;
+	const struct descriptor *desc;
+	uint16_t desc_type, desc_id;
+
+	ae = (const struct avb_packet_aecp_aem_acquire*)p->payload;
+
+	desc_type = ntohs(ae->descriptor_type);
+	desc_id = ntohs(ae->descriptor_id);
+
+	desc = server_find_descriptor(server, desc_type, desc_id);
+	if (desc == NULL)
+		return reply_status(aecp, AVB_AECP_AEM_STATUS_NO_SUCH_DESCRIPTOR, p, len);
+
+	if (desc_type != AVB_AEM_DESC_ENTITY || desc_id != 0)
+		return reply_not_implemented(aecp, m, len);
+
+	return reply_success(aecp, m, len);
+}
+
+/* READ_DESCRIPTOR */
+static int handle_read_descriptor(struct aecp *aecp, const void *m, int len)
+{
+	struct server *server = aecp->server;
+	const struct avb_ethernet_header *h = m;
+	const struct avb_packet_aecp_aem *p = SPA_PTROFF(h, sizeof(*h), void);
+	struct avb_packet_aecp_aem *reply;
+	const struct avb_packet_aecp_aem_read_descriptor *rd;
+	uint16_t desc_type, desc_id;
+	const struct descriptor *desc;
+	uint8_t buf[2048];
+	size_t size, psize;
+
+	rd = (struct avb_packet_aecp_aem_read_descriptor*)p->payload;
+
+	desc_type = ntohs(rd->descriptor_type);
+	desc_id = ntohs(rd->descriptor_id);
+
+	pw_log_info("descriptor type:%04x index:%d", desc_type, desc_id);
+
+	desc = server_find_descriptor(server, desc_type, desc_id);
+	if (desc == NULL)
+		return reply_status(aecp, AVB_AECP_AEM_STATUS_NO_SUCH_DESCRIPTOR, m, len);
+
+	memcpy(buf, m, len);
+
+	psize = sizeof(*rd);
+	size = sizeof(*h) + sizeof(*reply) + psize;
+
+	memcpy(buf + size, desc->ptr, desc->size);
+	size += desc->size;
+	psize += desc->size;
+
+	h = (void*)buf;
+	reply = SPA_PTROFF(h, sizeof(*h), void);
+	AVB_PACKET_AECP_SET_MESSAGE_TYPE(&reply->aecp, AVB_AECP_MESSAGE_TYPE_AEM_RESPONSE);
+	AVB_PACKET_AECP_SET_STATUS(&reply->aecp, AVB_AECP_AEM_STATUS_SUCCESS);
+	AVB_PACKET_SET_LENGTH(&reply->aecp.hdr, psize + 12);
+
+	return avb_server_send_packet(server, h->src, AVB_TSN_ETH, buf, size);
+}
+
+/* GET_AVB_INFO */
+static int handle_get_avb_info(struct aecp *aecp, const void *m, int len)
+{
+	struct server *server = aecp->server;
+	const struct avb_ethernet_header *h = m;
+	const struct avb_packet_aecp_aem *p = SPA_PTROFF(h, sizeof(*h), void);
+	struct avb_packet_aecp_aem *reply;
+	struct avb_packet_aecp_aem_get_avb_info *i;
+	struct avb_aem_desc_avb_interface *avb_interface;
+	uint16_t desc_type, desc_id;
+	const struct descriptor *desc;
+	uint8_t buf[2048];
+	size_t size, psize;
+
+	i = (struct avb_packet_aecp_aem_get_avb_info*)p->payload;
+
+	desc_type = ntohs(i->descriptor_type);
+	desc_id = ntohs(i->descriptor_id);
+
+	desc = server_find_descriptor(server, desc_type, desc_id);
+	if (desc == NULL)
+		return reply_status(aecp, AVB_AECP_AEM_STATUS_NO_SUCH_DESCRIPTOR, m, len);
+
+	if (desc_type != AVB_AEM_DESC_AVB_INTERFACE || desc_id != 0)
+		return reply_not_implemented(aecp, m, len);
+
+	avb_interface = desc->ptr;
+
+	memcpy(buf, m, len);
+
+	psize = sizeof(*i);
+	size = sizeof(*h) + sizeof(*reply) + psize;
+
+	h = (void*)buf;
+	reply = SPA_PTROFF(h, sizeof(*h), void);
+	AVB_PACKET_AECP_SET_MESSAGE_TYPE(&reply->aecp, AVB_AECP_MESSAGE_TYPE_AEM_RESPONSE);
+	AVB_PACKET_AECP_SET_STATUS(&reply->aecp, AVB_AECP_AEM_STATUS_SUCCESS);
+	AVB_PACKET_SET_LENGTH(&reply->aecp.hdr, psize + 12);
+
+	i = (struct avb_packet_aecp_aem_get_avb_info*)reply->payload;
+	i->gptp_grandmaster_id = avb_interface->clock_identity;
+	i->propagation_delay = htonl(0);
+	i->gptp_domain_number = avb_interface->domain_number;
+	i->flags = 0;
+	i->msrp_mappings_count = htons(0);
+
+	return avb_server_send_packet(server, h->src, AVB_TSN_ETH, buf, size);
+}
+
+/* AEM_COMMAND */
+struct cmd_info {
+	uint16_t type;
+	const char *name;
+	int (*handle) (struct aecp *aecp, const void *p, int len);
+};
+
+static const struct cmd_info cmd_info[] = {
+	{ AVB_AECP_AEM_CMD_ACQUIRE_ENTITY, "acquire-entity", handle_acquire_entity, },
+	{ AVB_AECP_AEM_CMD_LOCK_ENTITY, "lock-entity", handle_lock_entity, },
+	{ AVB_AECP_AEM_CMD_ENTITY_AVAILABLE, "entity-available", NULL, },
+	{ AVB_AECP_AEM_CMD_CONTROLLER_AVAILABLE, "controller-available", NULL, },
+	{ AVB_AECP_AEM_CMD_READ_DESCRIPTOR, "read-descriptor", handle_read_descriptor, },
+	{ AVB_AECP_AEM_CMD_WRITE_DESCRIPTOR, "write-descriptor", NULL, },
+	{ AVB_AECP_AEM_CMD_SET_CONFIGURATION, "set-configuration", NULL, },
+	{ AVB_AECP_AEM_CMD_GET_CONFIGURATION, "get-configuration", NULL, },
+	{ AVB_AECP_AEM_CMD_SET_STREAM_FORMAT, "set-stream-format", NULL, },
+	{ AVB_AECP_AEM_CMD_GET_STREAM_FORMAT, "get-stream-format", NULL, },
+	{ AVB_AECP_AEM_CMD_SET_VIDEO_FORMAT, "set-video-format", NULL, },
+	{ AVB_AECP_AEM_CMD_GET_VIDEO_FORMAT, "get-video-format", NULL, },
+	{ AVB_AECP_AEM_CMD_SET_SENSOR_FORMAT, "set-sensor-format", NULL, },
+	{ AVB_AECP_AEM_CMD_GET_SENSOR_FORMAT, "get-sensor-format", NULL, },
+	{ AVB_AECP_AEM_CMD_SET_STREAM_INFO, "set-stream-info", NULL, },
+	{ AVB_AECP_AEM_CMD_GET_STREAM_INFO, "get-stream-info", NULL, },
+	{ AVB_AECP_AEM_CMD_SET_NAME, "set-name", NULL, },
+	{ AVB_AECP_AEM_CMD_GET_NAME, "get-name", NULL, },
+	{ AVB_AECP_AEM_CMD_SET_ASSOCIATION_ID, "set-association-id", NULL, },
+	{ AVB_AECP_AEM_CMD_GET_ASSOCIATION_ID, "get-association-id", NULL, },
+	{ AVB_AECP_AEM_CMD_SET_SAMPLING_RATE, "set-sampling-rate", NULL, },
+	{ AVB_AECP_AEM_CMD_GET_SAMPLING_RATE, "get-sampling-rate", NULL, },
+	{ AVB_AECP_AEM_CMD_SET_CLOCK_SOURCE, "set-clock-source", NULL, },
+	{ AVB_AECP_AEM_CMD_GET_CLOCK_SOURCE, "get-clock-source", NULL, },
+	{ AVB_AECP_AEM_CMD_SET_CONTROL, "set-control", NULL, },
+	{ AVB_AECP_AEM_CMD_GET_CONTROL, "get-control", NULL, },
+	{ AVB_AECP_AEM_CMD_INCREMENT_CONTROL, "increment-control", NULL, },
+	{ AVB_AECP_AEM_CMD_DECREMENT_CONTROL, "decrement-control", NULL, },
+	{ AVB_AECP_AEM_CMD_SET_SIGNAL_SELECTOR, "set-signal-selector", NULL, },
+	{ AVB_AECP_AEM_CMD_GET_SIGNAL_SELECTOR, "get-signal-selector", NULL, },
+	{ AVB_AECP_AEM_CMD_SET_MIXER, "set-mixer", NULL, },
+	{ AVB_AECP_AEM_CMD_GET_MIXER, "get-mixer", NULL, },
+	{ AVB_AECP_AEM_CMD_SET_MATRIX, "set-matrix", NULL, },
+	{ AVB_AECP_AEM_CMD_GET_MATRIX, "get-matrix", NULL, },
+	{ AVB_AECP_AEM_CMD_START_STREAMING, "start-streaming", NULL, },
+	{ AVB_AECP_AEM_CMD_STOP_STREAMING, "stop-streaming", NULL, },
+	{ AVB_AECP_AEM_CMD_REGISTER_UNSOLICITED_NOTIFICATION, "register-unsolicited-notification", NULL, },
+	{ AVB_AECP_AEM_CMD_DEREGISTER_UNSOLICITED_NOTIFICATION, "deregister-unsolicited-notification", NULL, },
+	{ AVB_AECP_AEM_CMD_IDENTIFY_NOTIFICATION, "identify-notification", NULL, },
+	{ AVB_AECP_AEM_CMD_GET_AVB_INFO, "get-avb-info", handle_get_avb_info, },
+	{ AVB_AECP_AEM_CMD_GET_AS_PATH, "get-as-path", NULL, },
+	{ AVB_AECP_AEM_CMD_GET_COUNTERS, "get-counters", NULL, },
+	{ AVB_AECP_AEM_CMD_REBOOT, "reboot", NULL, },
+	{ AVB_AECP_AEM_CMD_GET_AUDIO_MAP, "get-audio-map", NULL, },
+	{ AVB_AECP_AEM_CMD_ADD_AUDIO_MAPPINGS, "add-audio-mappings", NULL, },
+	{ AVB_AECP_AEM_CMD_REMOVE_AUDIO_MAPPINGS, "remove-audio-mappings", NULL, },
+	{ AVB_AECP_AEM_CMD_GET_VIDEO_MAP, "get-video-map", NULL, },
+	{ AVB_AECP_AEM_CMD_ADD_VIDEO_MAPPINGS, "add-video-mappings", NULL, },
+	{ AVB_AECP_AEM_CMD_REMOVE_VIDEO_MAPPINGS, "remove-video-mappings", NULL, },
+	{ AVB_AECP_AEM_CMD_GET_SENSOR_MAP, "get-sensor-map", NULL, }
+};
+
+static inline const struct cmd_info *find_cmd_info(uint16_t type, const char *name)
+{
+	uint32_t i;
+	for (i = 0; i < SPA_N_ELEMENTS(cmd_info); i++) {
+		if ((name == NULL && type == cmd_info[i].type) ||
+		    (name != NULL && spa_streq(name, cmd_info[i].name)))
+			return &cmd_info[i];
+	}
+	return NULL;
+}
+
+int avb_aecp_aem_handle_command(struct aecp *aecp, const void *m, int len)
+{
+	const struct avb_ethernet_header *h = m;
+	const struct avb_packet_aecp_aem *p = SPA_PTROFF(h, sizeof(*h), void);
+	uint16_t cmd_type;
+	const struct cmd_info *info;
+
+	cmd_type = AVB_PACKET_AEM_GET_COMMAND_TYPE(p);
+
+	info = find_cmd_info(cmd_type, NULL);
+	if (info == NULL)
+		return reply_not_implemented(aecp, m, len);
+
+	pw_log_info("aem command %s", info->name);
+
+	if (info->handle == NULL)
+		return reply_not_implemented(aecp, m, len);
+
+	return info->handle(aecp, m, len);
+}
+
+int avb_aecp_aem_handle_response(struct aecp *aecp, const void *m, int len)
+{
+	return 0;
+}
diff --git a/src/modules/module-avb/aecp-aem.h b/src/modules/module-avb/aecp-aem.h
new file mode 100644
index 0000000000000000000000000000000000000000..dcf26b5b7441fa293d3f00cab1c36ad6c26c9b19
--- /dev/null
+++ b/src/modules/module-avb/aecp-aem.h
@@ -0,0 +1,345 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef AVB_AEM_H
+#define AVB_AEM_H
+
+#include "aecp.h"
+
+#define AVB_AECP_AEM_STATUS_SUCCESS			0
+#define AVB_AECP_AEM_STATUS_NOT_IMPLEMENTED		1
+#define AVB_AECP_AEM_STATUS_NO_SUCH_DESCRIPTOR		2
+#define AVB_AECP_AEM_STATUS_ENTITY_LOCKED		3
+#define AVB_AECP_AEM_STATUS_ENTITY_ACQUIRED		4
+#define AVB_AECP_AEM_STATUS_NOT_AUTHENTICATED		5
+#define AVB_AECP_AEM_STATUS_AUTHENTICATION_DISABLED	6
+#define AVB_AECP_AEM_STATUS_BAD_ARGUMENTS		7
+#define AVB_AECP_AEM_STATUS_NO_RESOURCES		8
+#define AVB_AECP_AEM_STATUS_IN_PROGRESS			9
+#define AVB_AECP_AEM_STATUS_ENTITY_MISBEHAVING		10
+#define AVB_AECP_AEM_STATUS_NOT_SUPPORTED		11
+#define AVB_AECP_AEM_STATUS_STREAM_IS_RUNNING		12
+
+#define AVB_AECP_AEM_CMD_ACQUIRE_ENTITY				0x0000
+#define AVB_AECP_AEM_CMD_LOCK_ENTITY				0x0001
+#define AVB_AECP_AEM_CMD_ENTITY_AVAILABLE			0x0002
+#define AVB_AECP_AEM_CMD_CONTROLLER_AVAILABLE			0x0003
+#define AVB_AECP_AEM_CMD_READ_DESCRIPTOR			0x0004
+#define AVB_AECP_AEM_CMD_WRITE_DESCRIPTOR			0x0005
+#define AVB_AECP_AEM_CMD_SET_CONFIGURATION			0x0006
+#define AVB_AECP_AEM_CMD_GET_CONFIGURATION			0x0007
+#define AVB_AECP_AEM_CMD_SET_STREAM_FORMAT			0x0008
+#define AVB_AECP_AEM_CMD_GET_STREAM_FORMAT			0x0009
+#define AVB_AECP_AEM_CMD_SET_VIDEO_FORMAT			0x000a
+#define AVB_AECP_AEM_CMD_GET_VIDEO_FORMAT			0x000b
+#define AVB_AECP_AEM_CMD_SET_SENSOR_FORMAT			0x000c
+#define AVB_AECP_AEM_CMD_GET_SENSOR_FORMAT			0x000d
+#define AVB_AECP_AEM_CMD_SET_STREAM_INFO			0x000e
+#define AVB_AECP_AEM_CMD_GET_STREAM_INFO			0x000f
+#define AVB_AECP_AEM_CMD_SET_NAME				0x0010
+#define AVB_AECP_AEM_CMD_GET_NAME				0x0011
+#define AVB_AECP_AEM_CMD_SET_ASSOCIATION_ID			0x0012
+#define AVB_AECP_AEM_CMD_GET_ASSOCIATION_ID			0x0013
+#define AVB_AECP_AEM_CMD_SET_SAMPLING_RATE			0x0014
+#define AVB_AECP_AEM_CMD_GET_SAMPLING_RATE			0x0015
+#define AVB_AECP_AEM_CMD_SET_CLOCK_SOURCE			0x0016
+#define AVB_AECP_AEM_CMD_GET_CLOCK_SOURCE			0x0017
+#define AVB_AECP_AEM_CMD_SET_CONTROL				0x0018
+#define AVB_AECP_AEM_CMD_GET_CONTROL				0x0019
+#define AVB_AECP_AEM_CMD_INCREMENT_CONTROL			0x001a
+#define AVB_AECP_AEM_CMD_DECREMENT_CONTROL			0x001b
+#define AVB_AECP_AEM_CMD_SET_SIGNAL_SELECTOR			0x001c
+#define AVB_AECP_AEM_CMD_GET_SIGNAL_SELECTOR			0x001d
+#define AVB_AECP_AEM_CMD_SET_MIXER				0x001e
+#define AVB_AECP_AEM_CMD_GET_MIXER				0x001f
+#define AVB_AECP_AEM_CMD_SET_MATRIX				0x0020
+#define AVB_AECP_AEM_CMD_GET_MATRIX				0x0021
+#define AVB_AECP_AEM_CMD_START_STREAMING			0x0022
+#define AVB_AECP_AEM_CMD_STOP_STREAMING				0x0023
+#define AVB_AECP_AEM_CMD_REGISTER_UNSOLICITED_NOTIFICATION	0x0024
+#define AVB_AECP_AEM_CMD_DEREGISTER_UNSOLICITED_NOTIFICATION	0x0025
+#define AVB_AECP_AEM_CMD_IDENTIFY_NOTIFICATION			0x0026
+#define AVB_AECP_AEM_CMD_GET_AVB_INFO				0x0027
+#define AVB_AECP_AEM_CMD_GET_AS_PATH				0x0028
+#define AVB_AECP_AEM_CMD_GET_COUNTERS				0x0029
+#define AVB_AECP_AEM_CMD_REBOOT					0x002a
+#define AVB_AECP_AEM_CMD_GET_AUDIO_MAP				0x002b
+#define AVB_AECP_AEM_CMD_ADD_AUDIO_MAPPINGS			0x002c
+#define AVB_AECP_AEM_CMD_REMOVE_AUDIO_MAPPINGS			0x002d
+#define AVB_AECP_AEM_CMD_GET_VIDEO_MAP				0x002e
+#define AVB_AECP_AEM_CMD_ADD_VIDEO_MAPPINGS			0x002f
+#define AVB_AECP_AEM_CMD_REMOVE_VIDEO_MAPPINGS			0x0030
+#define AVB_AECP_AEM_CMD_GET_SENSOR_MAP				0x0031
+#define AVB_AECP_AEM_CMD_ADD_SENSOR_MAPPINGS			0x0032
+#define AVB_AECP_AEM_CMD_REMOVE_SENSOR_MAPPINGS			0x0033
+#define AVB_AECP_AEM_CMD_START_OPERATION			0x0034
+#define AVB_AECP_AEM_CMD_ABORT_OPERATION			0x0035
+#define AVB_AECP_AEM_CMD_OPERATION_STATUS			0x0036
+#define AVB_AECP_AEM_CMD_AUTH_ADD_KEY				0x0037
+#define AVB_AECP_AEM_CMD_AUTH_DELETE_KEY			0x0038
+#define AVB_AECP_AEM_CMD_AUTH_GET_KEY_LIST			0x0039
+#define AVB_AECP_AEM_CMD_AUTH_GET_KEY				0x003a
+#define AVB_AECP_AEM_CMD_AUTH_ADD_KEY_TO_CHAIN			0x003b
+#define AVB_AECP_AEM_CMD_AUTH_DELETE_KEY_FROM_CHAIN		0x003c
+#define AVB_AECP_AEM_CMD_AUTH_GET_KEYCHAIN_LIST			0x003d
+#define AVB_AECP_AEM_CMD_AUTH_GET_IDENTITY			0x003e
+#define AVB_AECP_AEM_CMD_AUTH_ADD_TOKEN				0x003f
+#define AVB_AECP_AEM_CMD_AUTH_DELETE_TOKEN			0x0040
+#define AVB_AECP_AEM_CMD_AUTHENTICATE				0x0041
+#define AVB_AECP_AEM_CMD_DEAUTHENTICATE				0x0042
+#define AVB_AECP_AEM_CMD_ENABLE_TRANSPORT_SECURITY		0x0043
+#define AVB_AECP_AEM_CMD_DISABLE_TRANSPORT_SECURITY		0x0044
+#define AVB_AECP_AEM_CMD_ENABLE_STREAM_ENCRYPTION		0x0045
+#define AVB_AECP_AEM_CMD_DISABLE_STREAM_ENCRYPTION		0x0046
+#define AVB_AECP_AEM_CMD_SET_MEMORY_OBJECT_LENGTH		0x0047
+#define AVB_AECP_AEM_CMD_GET_MEMORY_OBJECT_LENGTH		0x0048
+#define AVB_AECP_AEM_CMD_SET_STREAM_BACKUP			0x0049
+#define AVB_AECP_AEM_CMD_GET_STREAM_BACKUP			0x004a
+#define AVB_AECP_AEM_CMD_EXPANSION				0x7fff
+
+#define AVB_AEM_ACQUIRE_ENTITY_PERSISTENT_FLAG			(1<<0)
+
+struct avb_packet_aecp_aem_acquire {
+	uint32_t flags;
+	uint64_t owner_guid;
+	uint16_t descriptor_type;
+	uint16_t descriptor_id;
+} __attribute__ ((__packed__));
+
+struct avb_packet_aecp_aem_lock {
+	uint32_t flags;
+	uint64_t locked_guid;
+	uint16_t descriptor_type;
+	uint16_t descriptor_id;
+} __attribute__ ((__packed__));
+
+struct avb_packet_aecp_aem_read_descriptor {
+	uint16_t configuration;
+	uint8_t reserved[2];
+	uint16_t descriptor_type;
+	uint16_t descriptor_id;
+} __attribute__ ((__packed__));
+
+struct avb_packet_aecp_aem_setget_configuration {
+	uint16_t reserved;
+	uint16_t configuration_index;
+} __attribute__ ((__packed__));
+
+struct avb_packet_aecp_aem_setget_stream_format {
+	uint16_t descriptor_type;
+	uint16_t descriptor_id;
+	uint64_t stream_format;
+} __attribute__ ((__packed__));
+
+struct avb_packet_aecp_aem_setget_video_format {
+	uint16_t descriptor_type;
+	uint16_t descriptor_id;
+	uint32_t format_specific;
+	uint16_t aspect_ratio;
+	uint16_t color_space;
+	uint32_t frame_size;
+} __attribute__ ((__packed__));
+
+struct avb_packet_aecp_aem_setget_sensor_format {
+	uint16_t descriptor_type;
+	uint16_t descriptor_id;
+	uint64_t sensor_format;
+} __attribute__ ((__packed__));
+
+
+#define AVB_AEM_STREAM_INFO_FLAG_CLASS_B			(1u<<0)
+#define AVB_AEM_STREAM_INFO_FLAG_FAST_CONNECT			(1u<<1)
+#define AVB_AEM_STREAM_INFO_FLAG_SAVED_STATE			(1u<<2)
+#define AVB_AEM_STREAM_INFO_FLAG_STREAMING_WAIT			(1u<<3)
+#define AVB_AEM_STREAM_INFO_FLAG_ENCRYPTED_PDU			(1u<<4)
+#define AVB_AEM_STREAM_INFO_FLAG_STREAM_VLAN_ID_VALID		(1u<<25)
+#define AVB_AEM_STREAM_INFO_FLAG_CONNECTED			(1u<<26)
+#define AVB_AEM_STREAM_INFO_FLAG_MSRP_FAILURE_VALID		(1u<<27)
+#define AVB_AEM_STREAM_INFO_FLAG_STREAM_DEST_MAC_VALID		(1u<<28)
+#define AVB_AEM_STREAM_INFO_FLAG_MSRP_ACC_LAT_VALID		(1u<<29)
+#define AVB_AEM_STREAM_INFO_FLAG_STREAM_ID_VALID		(1u<<30)
+#define AVB_AEM_STREAM_INFO_FLAG_STREAM_FORMAT_VALID		(1u<<31)
+
+struct avb_packet_aecp_aem_setget_stream_info {
+	uint16_t descriptor_type;
+	uint16_t descriptor_index;
+	uint32_t aem_stream_info_flags;
+	uint64_t stream_format;
+	uint64_t stream_id;
+	uint32_t msrp_accumulated_latency;
+	uint8_t stream_dest_mac[6];
+	uint8_t msrp_failure_code;
+	uint8_t reserved;
+	uint64_t msrp_failure_bridge_id;
+	uint16_t stream_vlan_id;
+	uint16_t reserved2;
+} __attribute__ ((__packed__));
+
+struct avb_packet_aecp_aem_setget_name {
+	uint16_t descriptor_type;
+	uint16_t descriptor_index;
+	uint16_t name_index;
+	uint16_t configuration_index;
+	char name[64];
+} __attribute__ ((__packed__));
+
+struct avb_packet_aecp_aem_setget_association_id {
+	uint16_t descriptor_type;
+	uint16_t descriptor_index;
+	uint64_t association_id;
+} __attribute__ ((__packed__));
+
+struct avb_packet_aecp_aem_setget_sampling_rate {
+	uint16_t descriptor_type;
+	uint16_t descriptor_id;
+	uint32_t sampling_rate;
+} __attribute__ ((__packed__));
+
+struct avb_packet_aecp_aem_setget_clock_source {
+	uint16_t descriptor_type;
+	uint16_t descriptor_id;
+	uint16_t clock_source_index;
+	uint16_t reserved;
+} __attribute__ ((__packed__));
+
+struct avb_packet_aecp_aem_setget_control {
+	uint16_t descriptor_type;
+	uint16_t descriptor_id;
+} __attribute__ ((__packed__));
+
+struct avb_packet_aecp_aem_incdec_control {
+	uint16_t descriptor_type;
+	uint16_t descriptor_id;
+	uint16_t index_count;
+	uint16_t reserved;
+} __attribute__ ((__packed__));
+
+struct avb_packet_aecp_aem_setget_signal_selector {
+	uint16_t descriptor_type;
+	uint16_t descriptor_id;
+	uint16_t signal_type;
+	uint16_t signal_index;
+	uint16_t signal_output;
+	uint16_t reserved;
+} __attribute__ ((__packed__));
+
+struct avb_packet_aecp_aem_setget_mixer {
+	uint16_t descriptor_type;
+	uint16_t descriptor_id;
+} __attribute__ ((__packed__));
+
+struct avb_packet_aecp_aem_setget_matrix {
+	uint16_t descriptor_type;
+	uint16_t descriptor_index;
+	uint16_t matrix_column;
+	uint16_t matrix_row;
+	uint16_t region_width;
+	uint16_t region_height;
+	uint16_t rep_direction_value_count;
+	uint16_t item_offset;
+} __attribute__ ((__packed__));
+
+struct avb_packet_aecp_aem_startstop_streaming {
+	uint16_t descriptor_type;
+	uint16_t descriptor_id;
+} __attribute__ ((__packed__));
+
+struct avb_packet_aecp_aem_identify_notification {
+	uint16_t descriptor_type;
+	uint16_t descriptor_id;
+} __attribute__ ((__packed__));
+
+struct avb_packet_aecp_aem_msrp_mapping {
+	uint8_t traffic_class;
+	uint8_t priority;
+	uint16_t vlan_id;
+} __attribute__ ((__packed__));
+
+#define AVB_AEM_AVB_INFO_FLAG_GPTP_GRANDMASTER_SUPPORTED	(1u<<0)
+#define AVB_AEM_AVB_INFO_FLAG_GPTP_ENABLED			(1u<<1)
+#define AVB_AEM_AVB_INFO_FLAG_SRP_ENABLED			(1u<<2)
+
+struct avb_packet_aecp_aem_get_avb_info {
+	uint16_t descriptor_type;
+	uint16_t descriptor_id;
+	uint64_t gptp_grandmaster_id;
+	uint32_t propagation_delay;
+	uint8_t gptp_domain_number;
+	uint8_t flags;
+	uint16_t msrp_mappings_count;
+	uint8_t msrp_mappings[0];
+} __attribute__ ((__packed__));
+
+struct avb_packet_aecp_aem_get_as_path {
+	uint16_t descriptor_index;
+	uint16_t reserved;
+} __attribute__ ((__packed__));
+
+struct avb_packet_aecp_aem_get_counters {
+	uint16_t descriptor_type;
+	uint16_t descriptor_id;
+	uint32_t counters_valid;
+	uint8_t counters_block[0];
+} __attribute__ ((__packed__));
+
+struct avb_packet_aecp_aem_reboot {
+	uint16_t descriptor_type;
+	uint16_t descriptor_id;
+} __attribute__ ((__packed__));
+
+struct avb_packet_aecp_aem_start_operation {
+	uint16_t descriptor_type;
+	uint16_t descriptor_id;
+	uint16_t operation_id;
+	uint16_t operation_type;
+} __attribute__ ((__packed__));
+
+struct avb_packet_aecp_aem_operation_status {
+	uint16_t descriptor_type;
+	uint16_t descriptor_id;
+	uint16_t operation_id;
+	uint16_t percent_complete;
+} __attribute__ ((__packed__));
+
+struct avb_packet_aecp_aem {
+	struct avb_packet_aecp_header aecp;
+#if __BYTE_ORDER == __BIG_ENDIAN
+	unsigned u:1;
+	unsigned cmd1:7;
+#elif __BYTE_ORDER == __LITTLE_ENDIAN
+	unsigned cmd1:7;
+	unsigned u:1;
+#endif
+	uint8_t cmd2;
+	uint8_t payload[0];
+} __attribute__ ((__packed__));
+
+#define AVB_PACKET_AEM_SET_COMMAND_TYPE(p,v)		((p)->cmd1 = ((v) >> 8),(p)->cmd2 = (v))
+
+#define AVB_PACKET_AEM_GET_COMMAND_TYPE(p)		((p)->cmd1 << 8 | (p)->cmd2)
+
+int avb_aecp_aem_handle_command(struct aecp *aecp, const void *m, int len);
+int avb_aecp_aem_handle_response(struct aecp *aecp, const void *m, int len);
+
+#endif /* AVB_AEM_H */
diff --git a/src/modules/module-avb/aecp.c b/src/modules/module-avb/aecp.c
new file mode 100644
index 0000000000000000000000000000000000000000..3c25b0ea31ab81accea0520926869f1ffac01b38
--- /dev/null
+++ b/src/modules/module-avb/aecp.c
@@ -0,0 +1,169 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <spa/utils/json.h>
+#include <spa/debug/mem.h>
+
+#include <pipewire/pipewire.h>
+
+#include "aecp.h"
+#include "aecp-aem.h"
+#include "internal.h"
+
+static const uint8_t mac[6] = AVB_BROADCAST_MAC;
+
+struct msg_info {
+	uint16_t type;
+	const char *name;
+	int (*handle) (struct aecp *aecp, const void *p, int len);
+};
+
+static int reply_not_implemented(struct aecp *aecp, const void *p, int len)
+{
+	struct server *server = aecp->server;
+	uint8_t buf[len];
+	struct avb_ethernet_header *h = (void*)buf;
+	struct avb_packet_aecp_header *reply = SPA_PTROFF(h, sizeof(*h), void);
+
+	memcpy(h, p, len);
+	AVB_PACKET_AECP_SET_STATUS(reply, AVB_AECP_STATUS_NOT_IMPLEMENTED);
+
+	return avb_server_send_packet(server, h->src, AVB_TSN_ETH, buf, len);
+}
+
+static const struct msg_info msg_info[] = {
+	{ AVB_AECP_MESSAGE_TYPE_AEM_COMMAND, "aem-command", avb_aecp_aem_handle_command, },
+	{ AVB_AECP_MESSAGE_TYPE_AEM_RESPONSE, "aem-response", avb_aecp_aem_handle_response, },
+	{ AVB_AECP_MESSAGE_TYPE_ADDRESS_ACCESS_COMMAND, "address-access-command", NULL, },
+	{ AVB_AECP_MESSAGE_TYPE_ADDRESS_ACCESS_RESPONSE, "address-access-response", NULL, },
+	{ AVB_AECP_MESSAGE_TYPE_AVC_COMMAND, "avc-command", NULL, },
+	{ AVB_AECP_MESSAGE_TYPE_AVC_RESPONSE, "avc-response", NULL, },
+	{ AVB_AECP_MESSAGE_TYPE_VENDOR_UNIQUE_COMMAND, "vendor-unique-command", NULL, },
+	{ AVB_AECP_MESSAGE_TYPE_VENDOR_UNIQUE_RESPONSE, "vendor-unique-response", NULL, },
+	{ AVB_AECP_MESSAGE_TYPE_EXTENDED_COMMAND, "extended-command", NULL, },
+	{ AVB_AECP_MESSAGE_TYPE_EXTENDED_RESPONSE, "extended-response", NULL, },
+};
+
+static inline const struct msg_info *find_msg_info(uint16_t type, const char *name)
+{
+	uint32_t i;
+	for (i = 0; i < SPA_N_ELEMENTS(msg_info); i++) {
+		if ((name == NULL && type == msg_info[i].type) ||
+		    (name != NULL && spa_streq(name, msg_info[i].name)))
+			return &msg_info[i];
+	}
+	return NULL;
+}
+
+static int aecp_message(void *data, uint64_t now, const void *message, int len)
+{
+	struct aecp *aecp = data;
+	struct server *server = aecp->server;
+	const struct avb_ethernet_header *h = message;
+	const struct avb_packet_aecp_header *p = SPA_PTROFF(h, sizeof(*h), void);
+	const struct msg_info *info;
+	int message_type;
+
+	if (ntohs(h->type) != AVB_TSN_ETH)
+		return 0;
+	if (memcmp(h->dest, mac, 6) != 0 &&
+	    memcmp(h->dest, server->mac_addr, 6) != 0)
+		return 0;
+	if (AVB_PACKET_GET_SUBTYPE(&p->hdr) != AVB_SUBTYPE_AECP)
+		return 0;
+
+	message_type = AVB_PACKET_AECP_GET_MESSAGE_TYPE(p);
+
+	info = find_msg_info(message_type, NULL);
+	if (info == NULL)
+		return reply_not_implemented(aecp, message, len);
+
+	pw_log_debug("got AECP message %s", info->name);
+
+	if (info->handle == NULL)
+		return reply_not_implemented(aecp, message, len);
+
+	return info->handle(aecp, message, len);
+}
+
+static void aecp_destroy(void *data)
+{
+	struct aecp *aecp = data;
+	spa_hook_remove(&aecp->server_listener);
+	free(aecp);
+}
+
+static int do_help(struct aecp *aecp, const char *args, FILE *out)
+{
+	fprintf(out, "{ \"type\": \"help\","
+			"\"text\": \""
+			  "/adp/help: this help \\n"
+			"\" }");
+	return 0;
+}
+
+static int aecp_command(void *data, uint64_t now, const char *command, const char *args, FILE *out)
+{
+	struct aecp *aecp = data;
+	int res;
+
+	if (!spa_strstartswith(command, "/aecp/"))
+		return 0;
+
+	command += strlen("/aecp/");
+
+	if (spa_streq(command, "help"))
+		res = do_help(aecp, args, out);
+	else
+		res = -ENOTSUP;
+
+	return res;
+}
+
+static const struct server_events server_events = {
+	AVB_VERSION_SERVER_EVENTS,
+	.destroy = aecp_destroy,
+	.message = aecp_message,
+	.command = aecp_command
+};
+
+struct avb_aecp *avb_aecp_register(struct server *server)
+{
+	struct aecp *aecp;
+
+	aecp = calloc(1, sizeof(*aecp));
+	if (aecp == NULL)
+		return NULL;
+
+	aecp->server = server;
+
+	avdecc_server_add_listener(server, &aecp->server_listener, &server_events, aecp);
+
+	return (struct avb_aecp*)aecp;
+}
+
+void avb_aecp_unregister(struct avb_aecp *aecp)
+{
+	aecp_destroy(aecp);
+}
diff --git a/src/modules/module-avb/aecp.h b/src/modules/module-avb/aecp.h
new file mode 100644
index 0000000000000000000000000000000000000000..a3515f0e7c683ff2934772a48dcc079535f92f83
--- /dev/null
+++ b/src/modules/module-avb/aecp.h
@@ -0,0 +1,60 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef AVB_AECP_H
+#define AVB_AECP_H
+
+#include "packets.h"
+#include "internal.h"
+
+#define AVB_AECP_MESSAGE_TYPE_AEM_COMMAND		0
+#define AVB_AECP_MESSAGE_TYPE_AEM_RESPONSE		1
+#define AVB_AECP_MESSAGE_TYPE_ADDRESS_ACCESS_COMMAND	2
+#define AVB_AECP_MESSAGE_TYPE_ADDRESS_ACCESS_RESPONSE	3
+#define AVB_AECP_MESSAGE_TYPE_AVC_COMMAND		4
+#define AVB_AECP_MESSAGE_TYPE_AVC_RESPONSE		5
+#define AVB_AECP_MESSAGE_TYPE_VENDOR_UNIQUE_COMMAND	6
+#define AVB_AECP_MESSAGE_TYPE_VENDOR_UNIQUE_RESPONSE	7
+#define AVB_AECP_MESSAGE_TYPE_EXTENDED_COMMAND		14
+#define AVB_AECP_MESSAGE_TYPE_EXTENDED_RESPONSE		15
+
+#define AVB_AECP_STATUS_SUCCESS				0
+#define AVB_AECP_STATUS_NOT_IMPLEMENTED			1
+
+struct avb_packet_aecp_header {
+	struct avb_packet_header hdr;
+	uint64_t target_guid;
+	uint64_t controller_guid;
+	uint16_t sequence_id;
+} __attribute__ ((__packed__));
+
+#define AVB_PACKET_AECP_SET_MESSAGE_TYPE(p,v)		AVB_PACKET_SET_SUB1(&(p)->hdr, v)
+#define AVB_PACKET_AECP_SET_STATUS(p,v)			AVB_PACKET_SET_SUB2(&(p)->hdr, v)
+
+#define AVB_PACKET_AECP_GET_MESSAGE_TYPE(p)		AVB_PACKET_GET_SUB1(&(p)->hdr)
+#define AVB_PACKET_AECP_GET_STATUS(p)			AVB_PACKET_GET_SUB2(&(p)->hdr)
+
+struct avb_aecp *avb_aecp_register(struct server *server);
+
+#endif /* AVB_AECP_H */
diff --git a/src/modules/module-avb/avb.c b/src/modules/module-avb/avb.c
new file mode 100644
index 0000000000000000000000000000000000000000..34526936f767949f16c2e50ba7eebc90261f3dd9
--- /dev/null
+++ b/src/modules/module-avb/avb.c
@@ -0,0 +1,108 @@
+/* PipeWire
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "internal.h"
+
+#include <spa/support/cpu.h>
+
+struct pw_avb *pw_avb_new(struct pw_context *context,
+		struct pw_properties *props, size_t user_data_size)
+{
+	struct impl *impl;
+	const struct spa_support *support;
+	uint32_t n_support;
+	struct spa_cpu *cpu;
+	const char *str;
+	int res = 0;
+
+	impl = calloc(1, sizeof(*impl) + user_data_size);
+	if (impl == NULL)
+		goto error_exit;
+
+	if (props == NULL)
+		props = pw_properties_new(NULL, NULL);
+	if (props == NULL)
+		goto error_free;
+
+	support = pw_context_get_support(context, &n_support);
+	cpu = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_CPU);
+
+	pw_context_conf_update_props(context, "avb.properties", props);
+
+	if ((str = pw_properties_get(props, "vm.overrides")) != NULL) {
+		if (cpu != NULL && spa_cpu_get_vm_type(cpu) != SPA_CPU_VM_NONE)
+			pw_properties_update_string(props, str, strlen(str));
+		pw_properties_set(props, "vm.overrides", NULL);
+	}
+
+	impl->context = context;
+	impl->loop = pw_context_get_main_loop(context);
+	impl->props = props;
+	impl->core = pw_context_get_object(context, PW_TYPE_INTERFACE_Core);
+	if (impl->core == NULL) {
+		str = pw_properties_get(props, PW_KEY_REMOTE_NAME);
+		impl->core = pw_context_connect(context,
+				pw_properties_new(
+					PW_KEY_REMOTE_NAME, str,
+					NULL),
+				0);
+		impl->do_disconnect = true;
+	}
+	if (impl->core == NULL) {
+		res = -errno;
+		pw_log_error("can't connect: %m");
+		goto error_free;
+	}
+
+	impl->work_queue = pw_context_get_work_queue(context);
+
+	spa_list_init(&impl->servers);
+
+	avdecc_server_new(impl, &props->dict);
+
+	return (struct pw_avb*)impl;
+
+error_free:
+	free(impl);
+error_exit:
+	pw_properties_free(props);
+	if (res < 0)
+		errno = -res;
+	return NULL;
+}
+
+static void impl_free(struct impl *impl)
+{
+	struct server *s;
+
+	spa_list_consume(s, &impl->servers, link)
+		avdecc_server_free(s);
+	free(impl);
+}
+
+void pw_avb_destroy(struct pw_avb *avb)
+{
+	struct impl *impl = (struct impl*)avb;
+	impl_free(impl);
+}
diff --git a/src/modules/module-avb/avb.h b/src/modules/module-avb/avb.h
new file mode 100644
index 0000000000000000000000000000000000000000..cad7dd2f9f5af85f6faf4fe59cf5359ea6c704ae
--- /dev/null
+++ b/src/modules/module-avb/avb.h
@@ -0,0 +1,44 @@
+/* PipeWire
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_AVB_H
+#define PIPEWIRE_AVB_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct pw_context;
+struct pw_properties;
+struct pw_avb;
+
+struct pw_avb *pw_avb_new(struct pw_context *context,
+		struct pw_properties *props, size_t user_data_size);
+void pw_avb_destroy(struct pw_avb *avb);
+
+#ifdef __cplusplus
+}  /* extern "C" */
+#endif
+
+#endif /* PIPEWIRE_AVB_H */
diff --git a/src/modules/module-avb/avdecc.c b/src/modules/module-avb/avdecc.c
new file mode 100644
index 0000000000000000000000000000000000000000..308ba4807971479431cb6fdf73608a107eb4ab74
--- /dev/null
+++ b/src/modules/module-avb/avdecc.c
@@ -0,0 +1,335 @@
+/* PipeWire
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <linux/if_ether.h>
+#include <linux/if_packet.h>
+#include <linux/filter.h>
+#include <linux/net_tstamp.h>
+#include <limits.h>
+#include <net/if.h>
+#include <arpa/inet.h>
+#include <sys/ioctl.h>
+#include <unistd.h>
+
+#include <spa/support/cpu.h>
+#include <spa/debug/mem.h>
+
+#include <pipewire/pipewire.h>
+
+#include "avb.h"
+#include "packets.h"
+#include "internal.h"
+#include "stream.h"
+#include "acmp.h"
+#include "adp.h"
+#include "aecp.h"
+#include "maap.h"
+#include "mmrp.h"
+#include "msrp.h"
+#include "mvrp.h"
+#include "descriptors.h"
+#include "utils.h"
+
+#define DEFAULT_INTERVAL	1
+
+#define server_emit(s,m,v,...) spa_hook_list_call(&s->listener_list, struct server_events, m, v, ##__VA_ARGS__)
+#define server_emit_destroy(s)		server_emit(s, destroy, 0)
+#define server_emit_message(s,n,m,l)	server_emit(s, message, 0, n, m, l)
+#define server_emit_periodic(s,n)	server_emit(s, periodic, 0, n)
+#define server_emit_command(s,n,c,a,f)	server_emit(s, command, 0, n, c, a, f)
+
+static void on_timer_event(void *data, uint64_t expirations)
+{
+	struct server *server = data;
+	struct timespec now;
+	clock_gettime(CLOCK_REALTIME, &now);
+	server_emit_periodic(server, SPA_TIMESPEC_TO_NSEC(&now));
+}
+
+static void on_socket_data(void *data, int fd, uint32_t mask)
+{
+	struct server *server = data;
+	struct timespec now;
+
+	if (mask & SPA_IO_IN) {
+		int len;
+		uint8_t buffer[2048];
+
+		len = recv(fd, buffer, sizeof(buffer), 0);
+
+		if (len < 0) {
+			pw_log_warn("got recv error: %m");
+		}
+		else if (len < (int)sizeof(struct avb_packet_header)) {
+			pw_log_warn("short packet received (%d < %d)", len,
+					(int)sizeof(struct avb_packet_header));
+		} else {
+			clock_gettime(CLOCK_REALTIME, &now);
+			server_emit_message(server, SPA_TIMESPEC_TO_NSEC(&now), buffer, len);
+		}
+	}
+}
+
+int avb_server_send_packet(struct server *server, const uint8_t dest[6],
+		uint16_t type, void *data, size_t size)
+{
+	struct avb_ethernet_header *hdr = (struct avb_ethernet_header*)data;
+	int res = 0;
+
+	memcpy(hdr->dest, dest, ETH_ALEN);
+	memcpy(hdr->src, server->mac_addr, ETH_ALEN);
+	hdr->type = htons(type);
+
+	if (send(server->source->fd, data, size, 0) < 0) {
+		res = -errno;
+		pw_log_warn("got send error: %m");
+	}
+	return res;
+}
+
+static int load_filter(int fd, uint16_t eth, const uint8_t dest[6], const uint8_t mac[6])
+{
+	struct sock_fprog filter;
+	struct sock_filter bpf_code[] = {
+		BPF_STMT(BPF_LD|BPF_H|BPF_ABS,  12),
+		BPF_JUMP(BPF_JMP|BPF_JEQ,       eth,        0, 8),
+		BPF_STMT(BPF_LD|BPF_W|BPF_ABS,  2),
+		BPF_JUMP(BPF_JMP|BPF_JEQ,       (dest[2] << 24) |
+						(dest[3] << 16) |
+						(dest[4] <<  8) |
+						(dest[5]),  0, 2),
+		BPF_STMT(BPF_LD|BPF_H|BPF_ABS,  0),
+		BPF_JUMP(BPF_JMP|BPF_JEQ,       (dest[0] << 8) |
+						(dest[1]),  3, 4),
+		BPF_JUMP(BPF_JMP|BPF_JEQ,       (mac[2] << 24) |
+						(mac[3] << 16) |
+						(mac[4] <<  8) |
+						(mac[5]),   0, 3),
+		BPF_STMT(BPF_LD|BPF_H|BPF_ABS,  0),
+		BPF_JUMP(BPF_JMP|BPF_JEQ,       (mac[0] <<  8) |
+						(mac[1]), 0, 1),
+		BPF_STMT(BPF_RET,               0x00040000),
+		BPF_STMT(BPF_RET,               0x00000000),
+	};
+	filter.len = sizeof(bpf_code) / 8;
+	filter.filter = bpf_code;
+
+	if (setsockopt(fd, SOL_SOCKET, SO_ATTACH_FILTER,
+				&filter, sizeof(filter)) < 0) {
+		pw_log_error("setsockopt(ATTACH_FILTER) failed: %m");
+		return -errno;
+	}
+	return 0;
+}
+
+int avb_server_make_socket(struct server *server, uint16_t type, const uint8_t mac[6])
+{
+	int fd, res;
+	struct ifreq req;
+	struct packet_mreq mreq;
+	struct sockaddr_ll sll;
+
+	fd = socket(AF_PACKET, SOCK_RAW|SOCK_NONBLOCK, htons(ETH_P_ALL));
+	if (fd < 0) {
+		pw_log_error("socket() failed: %m");
+		return -errno;
+	}
+
+	spa_zero(req);
+	snprintf(req.ifr_name, sizeof(req.ifr_name), "%s", server->ifname);
+	if (ioctl(fd, SIOCGIFINDEX, &req) < 0) {
+		res = -errno;
+		pw_log_error("SIOCGIFINDEX %s failed: %m", server->ifname);
+		goto error_close;
+	}
+	server->ifindex = req.ifr_ifindex;
+
+	spa_zero(req);
+	snprintf(req.ifr_name, sizeof(req.ifr_name), "%s", server->ifname);
+	if (ioctl(fd, SIOCGIFHWADDR, &req) < 0) {
+		res = -errno;
+		pw_log_error("SIOCGIFHWADDR %s failed: %m", server->ifname);
+		goto error_close;
+	}
+	memcpy(server->mac_addr, req.ifr_hwaddr.sa_data, sizeof(server->mac_addr));
+
+	server->entity_id = (uint64_t)server->mac_addr[0] << 56 |
+			(uint64_t)server->mac_addr[1] << 48 |
+			(uint64_t)server->mac_addr[2] << 40 |
+			(uint64_t)0xff << 32 |
+			(uint64_t)0xfe << 24 |
+			(uint64_t)server->mac_addr[3] << 16 |
+			(uint64_t)server->mac_addr[4] << 8 |
+			(uint64_t)server->mac_addr[5];
+
+	spa_zero(sll);
+	sll.sll_family = AF_PACKET;
+	sll.sll_protocol = htons(ETH_P_ALL);
+	sll.sll_ifindex = server->ifindex;
+	if (bind(fd, (struct sockaddr *) &sll, sizeof(sll)) < 0) {
+		res = -errno;
+		pw_log_error("bind() failed: %m");
+		goto error_close;
+	}
+
+	spa_zero(mreq);
+	mreq.mr_ifindex = server->ifindex;
+	mreq.mr_type = PACKET_MR_MULTICAST;
+	mreq.mr_alen = ETH_ALEN;
+	memcpy(mreq.mr_address, mac, ETH_ALEN);
+
+	if (setsockopt(fd, SOL_PACKET, PACKET_ADD_MEMBERSHIP,
+				&mreq, sizeof(mreq)) < 0) {
+		res = -errno;
+		pw_log_error("setsockopt(ADD_MEMBERSHIP) failed: %m");
+		goto error_close;
+	}
+
+	if ((res = load_filter(fd, type, mac, server->mac_addr)) < 0)
+		goto error_close;
+
+	return fd;
+
+error_close:
+	close(fd);
+	return res;
+}
+
+static int setup_socket(struct server *server)
+{
+	struct impl *impl = server->impl;
+	int fd, res;
+	static const uint8_t bmac[6] = AVB_BROADCAST_MAC;
+	struct timespec value, interval;
+
+	fd = avb_server_make_socket(server, AVB_TSN_ETH, bmac);
+	if (fd < 0)
+		return fd;
+
+	pw_log_info("0x%"PRIx64" %d", server->entity_id, server->ifindex);
+
+	server->source = pw_loop_add_io(impl->loop, fd, SPA_IO_IN, true, on_socket_data, server);
+	if (server->source == NULL) {
+		res = -errno;
+		pw_log_error("server %p: can't create server source: %m", impl);
+		goto error_no_source;
+	}
+	server->timer = pw_loop_add_timer(impl->loop, on_timer_event, server);
+	if (server->timer == NULL) {
+		res = -errno;
+		pw_log_error("server %p: can't create timer source: %m", impl);
+		goto error_no_timer;
+	}
+	value.tv_sec = 0;
+	value.tv_nsec = 1;
+	interval.tv_sec = DEFAULT_INTERVAL;
+	interval.tv_nsec = 0;
+	pw_loop_update_timer(impl->loop, server->timer, &value, &interval, false);
+
+	return 0;
+
+error_no_timer:
+	pw_loop_destroy_source(impl->loop, server->source);
+	server->source = NULL;
+error_no_source:
+	close(fd);
+	return res;
+}
+
+struct server *avdecc_server_new(struct impl *impl, struct spa_dict *props)
+{
+	struct server *server;
+	int res = 0;
+
+	server = calloc(1, sizeof(*server));
+	if (server == NULL)
+		return NULL;
+
+	server->impl = impl;
+	spa_list_append(&impl->servers, &server->link);
+	server->ifname = strdup(spa_dict_lookup(props, "ifname"));
+	spa_hook_list_init(&server->listener_list);
+	spa_list_init(&server->descriptors);
+	spa_list_init(&server->streams);
+
+	server->debug_messages = false;
+
+	if ((res = setup_socket(server)) < 0)
+		goto error_free;
+
+	init_descriptors(server);
+
+	server->mrp = avb_mrp_new(server);
+	if (server->mrp == NULL)
+		goto error_free;
+
+	avb_aecp_register(server);
+	server->maap = avb_maap_register(server);
+	server->mmrp = avb_mmrp_register(server);
+	server->msrp = avb_msrp_register(server);
+	server->mvrp = avb_mvrp_register(server);
+	avb_adp_register(server);
+	avb_acmp_register(server);
+
+	server->domain_attr = avb_msrp_attribute_new(server->msrp,
+			AVB_MSRP_ATTRIBUTE_TYPE_DOMAIN);
+	server->domain_attr->attr.domain.sr_class_id = AVB_MSRP_CLASS_ID_DEFAULT;
+	server->domain_attr->attr.domain.sr_class_priority = AVB_MSRP_PRIORITY_DEFAULT;
+	server->domain_attr->attr.domain.sr_class_vid = htons(AVB_DEFAULT_VLAN);
+
+	avb_mrp_attribute_begin(server->domain_attr->mrp, 0);
+	avb_mrp_attribute_join(server->domain_attr->mrp, 0, true);
+
+	server_create_stream(server, SPA_DIRECTION_INPUT, 0);
+	server_create_stream(server, SPA_DIRECTION_OUTPUT, 0);
+
+	avb_maap_reserve(server->maap, 1);
+
+	return server;
+
+error_free:
+	free(server);
+	if (res < 0)
+		errno = -res;
+	return NULL;
+}
+
+void avdecc_server_add_listener(struct server *server, struct spa_hook *listener,
+		const struct server_events *events, void *data)
+{
+	spa_hook_list_append(&server->listener_list, listener, events, data);
+}
+
+void avdecc_server_free(struct server *server)
+{
+	struct impl *impl = server->impl;
+
+	spa_list_remove(&server->link);
+	if (server->source)
+		pw_loop_destroy_source(impl->loop, server->source);
+	if (server->timer)
+		pw_loop_destroy_source(impl->loop, server->source);
+	spa_hook_list_clean(&server->listener_list);
+	free(server);
+}
diff --git a/src/modules/module-avb/descriptors.h b/src/modules/module-avb/descriptors.h
new file mode 100644
index 0000000000000000000000000000000000000000..56397e3d33f51b7191b5e2cfd610ba55926c2138
--- /dev/null
+++ b/src/modules/module-avb/descriptors.h
@@ -0,0 +1,274 @@
+/* PipeWire
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "aecp-aem.h"
+#include "aecp-aem-descriptors.h"
+#include "internal.h"
+
+void init_descriptors(struct server *server)
+{
+	server_add_descriptor(server, AVB_AEM_DESC_STRINGS, 0,
+			sizeof(struct avb_aem_desc_strings),
+			&(struct avb_aem_desc_strings)
+	{
+		.string_0 = "PipeWire",
+		.string_1 = "Configuration 1",
+		.string_2 = "Wim Taymans",
+	});
+	server_add_descriptor(server, AVB_AEM_DESC_LOCALE, 0,
+			sizeof(struct avb_aem_desc_locale),
+			&(struct avb_aem_desc_locale)
+	{
+		.locale_identifier = "en-EN",
+		.number_of_strings = htons(1),
+		.base_strings = htons(0)
+	});
+	server_add_descriptor(server, AVB_AEM_DESC_ENTITY, 0,
+			sizeof(struct avb_aem_desc_entity),
+			&(struct avb_aem_desc_entity)
+	{
+		.entity_id = htobe64(server->entity_id),
+		.entity_model_id = htobe64(0),
+		.entity_capabilities = htonl(
+			AVB_ADP_ENTITY_CAPABILITY_AEM_SUPPORTED |
+			AVB_ADP_ENTITY_CAPABILITY_CLASS_A_SUPPORTED |
+			AVB_ADP_ENTITY_CAPABILITY_GPTP_SUPPORTED |
+			AVB_ADP_ENTITY_CAPABILITY_AEM_IDENTIFY_CONTROL_INDEX_VALID |
+			AVB_ADP_ENTITY_CAPABILITY_AEM_INTERFACE_INDEX_VALID),
+
+		.talker_stream_sources = htons(8),
+		.talker_capabilities = htons(
+			AVB_ADP_TALKER_CAPABILITY_IMPLEMENTED |
+			AVB_ADP_TALKER_CAPABILITY_AUDIO_SOURCE),
+		.listener_stream_sinks = htons(8),
+		.listener_capabilities = htons(
+			AVB_ADP_LISTENER_CAPABILITY_IMPLEMENTED |
+			AVB_ADP_LISTENER_CAPABILITY_AUDIO_SINK),
+		.controller_capabilities = htons(0),
+		.available_index = htonl(0),
+		.association_id = htobe64(0),
+		.entity_name = "PipeWire",
+		.vendor_name_string = htons(2),
+		.model_name_string = htons(0),
+		.firmware_version = "0.3.48",
+		.group_name = "",
+		.serial_number = "",
+		.configurations_count = htons(1),
+		.current_configuration = htons(0)
+	});
+	struct {
+		struct avb_aem_desc_configuration desc;
+		struct avb_aem_desc_descriptor_count descriptor_counts[8];
+	} __attribute__ ((__packed__)) config =
+	{
+		{
+		.object_name = "Configuration 1",
+		.localized_description = htons(1),
+		.descriptor_counts_count = htons(8),
+		.descriptor_counts_offset = htons(
+			4 + sizeof(struct avb_aem_desc_configuration)),
+		},
+		.descriptor_counts = {
+			{ htons(AVB_AEM_DESC_AUDIO_UNIT), htons(1) },
+			{ htons(AVB_AEM_DESC_STREAM_INPUT), htons(1) },
+			{ htons(AVB_AEM_DESC_STREAM_OUTPUT), htons(1) },
+			{ htons(AVB_AEM_DESC_AVB_INTERFACE), htons(1) },
+			{ htons(AVB_AEM_DESC_CLOCK_SOURCE), htons(1) },
+			{ htons(AVB_AEM_DESC_CONTROL), htons(2) },
+			{ htons(AVB_AEM_DESC_LOCALE), htons(1) },
+			{ htons(AVB_AEM_DESC_CLOCK_DOMAIN), htons(1) }
+		}
+	};
+	server_add_descriptor(server, AVB_AEM_DESC_CONFIGURATION, 0,
+			sizeof(config), &config);
+
+	struct {
+		struct avb_aem_desc_audio_unit desc;
+		struct avb_aem_desc_sampling_rate sampling_rates[6];
+	} __attribute__ ((__packed__)) audio_unit =
+	{
+		{
+		.object_name = "PipeWire",
+		.localized_description = htons(0),
+		.clock_domain_index = htons(0),
+		.number_of_stream_input_ports = htons(1),
+		.base_stream_input_port = htons(0),
+		.number_of_stream_output_ports = htons(1),
+		.base_stream_output_port = htons(0),
+		.number_of_external_input_ports = htons(8),
+		.base_external_input_port = htons(0),
+		.number_of_external_output_ports = htons(8),
+		.base_external_output_port = htons(0),
+		.number_of_internal_input_ports = htons(0),
+		.base_internal_input_port = htons(0),
+		.number_of_internal_output_ports = htons(0),
+		.base_internal_output_port = htons(0),
+		.number_of_controls = htons(0),
+		.base_control = htons(0),
+		.number_of_signal_selectors = htons(0),
+		.base_signal_selector = htons(0),
+		.number_of_mixers = htons(0),
+		.base_mixer = htons(0),
+		.number_of_matrices = htons(0),
+		.base_matrix = htons(0),
+		.number_of_splitters = htons(0),
+		.base_splitter = htons(0),
+		.number_of_combiners = htons(0),
+		.base_combiner = htons(0),
+		.number_of_demultiplexers = htons(0),
+		.base_demultiplexer = htons(0),
+		.number_of_multiplexers = htons(0),
+		.base_multiplexer = htons(0),
+		.number_of_transcoders = htons(0),
+		.base_transcoder = htons(0),
+		.number_of_control_blocks = htons(0),
+		.base_control_block = htons(0),
+		.current_sampling_rate = htonl(48000),
+		.sampling_rates_offset = htons(
+			4 + sizeof(struct avb_aem_desc_audio_unit)),
+		.sampling_rates_count = htons(6),
+		},
+		.sampling_rates = {
+			{ .pull_frequency = htonl(44100) },
+			{ .pull_frequency = htonl(48000) },
+			{ .pull_frequency = htonl(88200) },
+			{ .pull_frequency = htonl(96000) },
+			{ .pull_frequency = htonl(176400) },
+			{ .pull_frequency = htonl(192000) },
+		}
+	};
+	server_add_descriptor(server, AVB_AEM_DESC_AUDIO_UNIT, 0,
+			sizeof(audio_unit), &audio_unit);
+
+	struct {
+		struct avb_aem_desc_stream desc;
+		uint64_t stream_formats[6];
+	} __attribute__ ((__packed__)) stream_input_0 =
+	{
+		{
+		.object_name = "Stream Input 1",
+		.localized_description = htons(0xffff),
+		.clock_domain_index = htons(0),
+		.stream_flags = htons(
+				AVB_AEM_DESC_STREAM_FLAG_SYNC_SOURCE |
+				AVB_AEM_DESC_STREAM_FLAG_CLASS_A),
+		.current_format = htobe64(0x00a0020840000800ULL),
+		.formats_offset = htons(
+			4 + sizeof(struct avb_aem_desc_stream)),
+		.number_of_formats = htons(6),
+		.backup_talker_entity_id_0 = htobe64(0),
+		.backup_talker_unique_id_0 = htons(0),
+		.backup_talker_entity_id_1 = htobe64(0),
+		.backup_talker_unique_id_1 = htons(0),
+		.backup_talker_entity_id_2 = htobe64(0),
+		.backup_talker_unique_id_2 = htons(0),
+		.backedup_talker_entity_id = htobe64(0),
+		.backedup_talker_unique = htons(0),
+		.avb_interface_index = htons(0),
+		.buffer_length = htons(8)
+		},
+		.stream_formats = {
+			htobe64(0x00a0010860000800ULL),
+			htobe64(0x00a0020860000800ULL),
+			htobe64(0x00a0030860000800ULL),
+			htobe64(0x00a0040860000800ULL),
+			htobe64(0x00a0050860000800ULL),
+			htobe64(0x00a0060860000800ULL),
+		},
+	};
+	server_add_descriptor(server, AVB_AEM_DESC_STREAM_INPUT, 0,
+			sizeof(stream_input_0), &stream_input_0);
+
+	struct {
+		struct avb_aem_desc_stream desc;
+		uint64_t stream_formats[6];
+	} __attribute__ ((__packed__)) stream_output_0 =
+	{
+		{
+		.object_name = "Stream Output 1",
+		.localized_description = htons(0xffff),
+		.clock_domain_index = htons(0),
+		.stream_flags = htons(
+				AVB_AEM_DESC_STREAM_FLAG_CLASS_A),
+		.current_format = htobe64(0x00a0020840000800ULL),
+		.formats_offset = htons(
+			4 + sizeof(struct avb_aem_desc_stream)),
+		.number_of_formats = htons(6),
+		.backup_talker_entity_id_0 = htobe64(0),
+		.backup_talker_unique_id_0 = htons(0),
+		.backup_talker_entity_id_1 = htobe64(0),
+		.backup_talker_unique_id_1 = htons(0),
+		.backup_talker_entity_id_2 = htobe64(0),
+		.backup_talker_unique_id_2 = htons(0),
+		.backedup_talker_entity_id = htobe64(0),
+		.backedup_talker_unique = htons(0),
+		.avb_interface_index = htons(0),
+		.buffer_length = htons(8)
+		},
+		.stream_formats = {
+			htobe64(0x00a0010860000800ULL),
+			htobe64(0x00a0020860000800ULL),
+			htobe64(0x00a0030860000800ULL),
+			htobe64(0x00a0040860000800ULL),
+			htobe64(0x00a0050860000800ULL),
+			htobe64(0x00a0060860000800ULL),
+		},
+	};
+	server_add_descriptor(server, AVB_AEM_DESC_STREAM_OUTPUT, 0,
+			sizeof(stream_output_0), &stream_output_0);
+
+	struct avb_aem_desc_avb_interface avb_interface = {
+		.localized_description = htons(0xffff),
+		.interface_flags = htons(
+				AVB_AEM_DESC_AVB_INTERFACE_FLAG_GPTP_GRANDMASTER_SUPPORTED),
+		.clock_identity = htobe64(0),
+		.priority1 = 0,
+		.clock_class = 0,
+		.offset_scaled_log_variance = htons(0),
+		.clock_accuracy = 0,
+		.priority2 = 0,
+		.domain_number = 0,
+		.log_sync_interval = 0,
+		.log_announce_interval = 0,
+		.log_pdelay_interval = 0,
+		.port_number = 0,
+	};
+	strncpy(avb_interface.object_name, server->ifname, 63);
+	memcpy(avb_interface.mac_address, server->mac_addr, 6);
+	server_add_descriptor(server, AVB_AEM_DESC_AVB_INTERFACE, 0,
+			sizeof(avb_interface), &avb_interface);
+
+	struct avb_aem_desc_clock_source clock_source = {
+		.object_name = "Stream Clock",
+		.localized_description = htons(0xffff),
+		.clock_source_flags = htons(0),
+		.clock_source_type = htons(
+				AVB_AEM_DESC_CLOCK_SOURCE_TYPE_INPUT_STREAM),
+		.clock_source_identifier = htobe64(0),
+		.clock_source_location_type = htons(AVB_AEM_DESC_STREAM_INPUT),
+		.clock_source_location_index = htons(0),
+	};
+	server_add_descriptor(server, AVB_AEM_DESC_CLOCK_SOURCE, 0,
+			sizeof(clock_source), &clock_source);
+}
diff --git a/src/modules/module-avb/iec61883.h b/src/modules/module-avb/iec61883.h
new file mode 100644
index 0000000000000000000000000000000000000000..6ca8724ad71a080b26ffaf018197c2e698db36b0
--- /dev/null
+++ b/src/modules/module-avb/iec61883.h
@@ -0,0 +1,110 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef AVB_IEC61883_H
+#define AVB_IEC61883_H
+
+#include "packets.h"
+
+struct avb_packet_iec61883 {
+	uint8_t subtype;
+#if __BYTE_ORDER == __BIG_ENDIAN
+	unsigned sv:1;
+	unsigned version:3;
+	unsigned mr:1;
+	unsigned _r1:1;
+	unsigned gv:1;
+	unsigned tv:1;
+
+	uint8_t seq_num;
+
+	unsigned _r2:7;
+	unsigned tu:1;
+#elif __BYTE_ORDER == __LITTLE_ENDIAN
+	unsigned tv:1;
+	unsigned gv:1;
+	unsigned _r1:1;
+	unsigned mr:1;
+	unsigned version:3;
+	unsigned sv:1;
+
+	uint8_t seq_num;
+
+	unsigned tu:1;
+	unsigned _r2:7;
+#endif
+	uint64_t stream_id;
+	uint32_t timestamp;
+	uint32_t gateway_info;
+	uint16_t data_len;
+#if __BYTE_ORDER == __BIG_ENDIAN
+	uint8_t tag:2;
+	uint8_t channel:6;
+
+	uint8_t tcode:4;
+	uint8_t app:4;
+
+	uint8_t qi1:2;		/* CIP Quadlet Indicator 1 */
+	uint8_t sid:6;		/* CIP Source ID */
+
+	uint8_t dbs;		/* CIP Data Block Size */
+
+	uint8_t fn:2;		/* CIP Fraction Number */
+	uint8_t qpc:3;		/* CIP Quadlet Padding Count */
+	uint8_t sph:1;		/* CIP Source Packet Header */
+	uint8_t _r3:2;
+
+	uint8_t dbc;		/* CIP Data Block Continuity */
+
+	uint8_t qi2:2;		/* CIP Quadlet Indicator 2 */
+	uint8_t format_id:6;	/* CIP Format ID */
+#elif __BYTE_ORDER == __LITTLE_ENDIAN
+	uint8_t channel:6;
+	uint8_t tag:2;
+
+	uint8_t app:4;
+	uint8_t tcode:4;
+
+	uint8_t sid:6;		/* CIP Source ID */
+	uint8_t qi1:2;		/* CIP Quadlet Indicator 1 */
+
+	uint8_t dbs;		/* CIP Data Block Size */
+
+	uint8_t _r3:2;
+	uint8_t sph:1;		/* CIP Source Packet Header */
+	uint8_t qpc:3;		/* CIP Quadlet Padding Count */
+	uint8_t fn:2;		/* CIP Fraction Number */
+
+	uint8_t dbc;		/* CIP Data Block Continuity */
+
+	uint8_t format_id:6;	/* CIP Format ID */
+	uint8_t qi2:2;		/* CIP Quadlet Indicator 2 */
+#endif
+	uint8_t fdf;		/* CIP Format Dependent Field */
+        uint16_t syt;
+
+	uint8_t payload[0];
+} __attribute__ ((__packed__));
+
+#endif /* AVB_IEC61883_H */
diff --git a/src/modules/module-avb/internal.h b/src/modules/module-avb/internal.h
new file mode 100644
index 0000000000000000000000000000000000000000..f0a1c14995454db901e9fe7ce75d74393aeb170f
--- /dev/null
+++ b/src/modules/module-avb/internal.h
@@ -0,0 +1,167 @@
+/* PipeWire
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef AVB_INTERNAL_H
+#define AVB_INTERNAL_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <pipewire/pipewire.h>
+
+struct server;
+struct avb_mrp;
+
+#define AVB_TSN_ETH 0x22f0
+#define AVB_BROADCAST_MAC { 0x91, 0xe0, 0xf0, 0x01, 0x00, 0x00 };
+
+struct impl {
+	struct pw_loop *loop;
+	struct pw_context *context;
+	struct spa_hook context_listener;
+	struct pw_core *core;
+	unsigned do_disconnect:1;
+
+	struct pw_properties *props;
+	struct pw_work_queue *work_queue;
+
+	struct spa_list servers;
+};
+
+struct server_events {
+#define AVB_VERSION_SERVER_EVENTS	0
+	uint32_t version;
+
+	/** the server is destroyed */
+	void (*destroy) (void *data);
+
+	int (*message) (void *data, uint64_t now, const void *message, int len);
+
+	void (*periodic) (void *data, uint64_t now);
+
+	int (*command) (void *data, uint64_t now, const char *command, const char *args, FILE *out);
+};
+
+struct descriptor {
+	struct spa_list link;
+	uint16_t type;
+	uint16_t index;
+	uint32_t size;
+	void *ptr;
+};
+
+struct server {
+	struct spa_list link;
+	struct impl *impl;
+
+	char *ifname;
+	uint8_t mac_addr[6];
+	uint64_t entity_id;
+	int ifindex;
+
+	struct spa_source *source;
+	struct spa_source *timer;
+
+	struct spa_hook_list listener_list;
+
+	struct spa_list descriptors;
+	struct spa_list streams;
+
+	unsigned debug_messages:1;
+
+	struct avb_mrp *mrp;
+	struct avb_mmrp *mmrp;
+	struct avb_mvrp *mvrp;
+	struct avb_msrp *msrp;
+	struct avb_maap *maap;
+
+	struct avb_msrp_attribute *domain_attr;
+};
+
+#include "stream.h"
+
+static inline const struct descriptor *server_find_descriptor(struct server *server,
+		uint16_t type, uint16_t index)
+{
+	struct descriptor *d;
+	spa_list_for_each(d, &server->descriptors, link) {
+		if (d->type == type &&
+		    d->index == index)
+			return d;
+	}
+	return NULL;
+}
+static inline void *server_add_descriptor(struct server *server,
+		uint16_t type, uint16_t index, size_t size, void *ptr)
+{
+	struct descriptor *d;
+
+	if ((d = calloc(1, sizeof(struct descriptor) + size)) == NULL)
+		return NULL;
+
+	d->type = type;
+	d->index = index;
+	d->size = size;
+	d->ptr = SPA_PTROFF(d, sizeof(struct descriptor), void);
+	if (ptr)
+		memcpy(d->ptr, ptr, size);
+	spa_list_append(&server->descriptors, &d->link);
+	return d->ptr;
+}
+
+static inline struct stream *server_find_stream(struct server *server,
+		enum spa_direction direction, uint16_t index)
+{
+	struct stream *s;
+	spa_list_for_each(s, &server->streams, link) {
+		if (s->direction == direction &&
+		    s->index == index)
+			return s;
+	}
+	return NULL;
+}
+
+struct server *avdecc_server_new(struct impl *impl, struct spa_dict *props);
+void avdecc_server_free(struct server *server);
+
+void avdecc_server_add_listener(struct server *server, struct spa_hook *listener,
+		const struct server_events *events, void *data);
+
+int avb_server_make_socket(struct server *server, uint16_t type, const uint8_t mac[6]);
+
+int avb_server_send_packet(struct server *server, const uint8_t dest[6],
+		uint16_t type, void *data, size_t size);
+
+struct aecp {
+	struct server *server;
+	struct spa_hook server_listener;
+};
+
+
+#ifdef __cplusplus
+}  /* extern "C" */
+#endif
+
+#endif /* AVB_INTERNAL_H */
diff --git a/src/modules/module-avb/maap.c b/src/modules/module-avb/maap.c
new file mode 100644
index 0000000000000000000000000000000000000000..bd72a2635d918d115e4709c5b5855e58744b00ab
--- /dev/null
+++ b/src/modules/module-avb/maap.c
@@ -0,0 +1,470 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <unistd.h>
+
+#include <spa/utils/json.h>
+
+#include <pipewire/pipewire.h>
+#include <pipewire/conf.h>
+
+#include "utils.h"
+#include "maap.h"
+
+#define MAAP_ALLOCATION_POOL_SIZE	0xFE00
+#define MAAP_ALLOCATION_POOL_BASE	 { 0x91, 0xe0, 0xf0, 0x00, 0x00, 0x00 }
+static uint8_t maap_base[6] = MAAP_ALLOCATION_POOL_BASE;
+
+#define MAAP_PROBE_RETRANSMITS		3
+
+#define MAAP_PROBE_INTERVAL_MS		500
+#define MAAP_PROBE_INTERVAL_VAR_MS	100
+
+#define MAAP_ANNOUNCE_INTERVAL_MS	3000
+#define MAAP_ANNOUNCE_INTERVAL_VAR_MS	2000
+
+struct maap {
+	struct server *server;
+	struct spa_hook server_listener;
+
+	struct pw_properties *props;
+
+	struct spa_source *source;
+
+#define STATE_IDLE	0
+#define STATE_PROBE	1
+#define STATE_ANNOUNCE	2
+	uint32_t state;
+	uint64_t timeout;
+	uint32_t probe_count;
+
+	unsigned short xsubi[3];
+
+	uint16_t offset;
+	uint16_t count;
+};
+
+static const char *message_type_as_string(uint8_t message_type)
+{
+	switch (message_type) {
+	case AVB_MAAP_MESSAGE_TYPE_PROBE:
+		return "PROBE";
+	case AVB_MAAP_MESSAGE_TYPE_DEFEND:
+		return "DEFEND";
+	case AVB_MAAP_MESSAGE_TYPE_ANNOUNCE:
+		return "ANNOUNCE";
+	}
+	return "INVALID";
+}
+
+static void maap_message_debug(struct maap *maap, const struct avb_packet_maap *p)
+{
+	uint32_t v;
+	const uint8_t *addr;
+
+	v = AVB_PACKET_MAAP_GET_MESSAGE_TYPE(p);
+	pw_log_info("message-type: %d (%s)", v, message_type_as_string(v));
+	pw_log_info("  maap-version: %d", AVB_PACKET_MAAP_GET_MAAP_VERSION(p));
+	pw_log_info("  length: %d", AVB_PACKET_GET_LENGTH(&p->hdr));
+
+	pw_log_info("  stream-id: 0x%"PRIx64, AVB_PACKET_MAAP_GET_STREAM_ID(p));
+	addr = AVB_PACKET_MAAP_GET_REQUEST_START(p);
+	pw_log_info("  request-start: %02x:%02x:%02x:%02x:%02x:%02x",
+			addr[0], addr[1], addr[2], addr[3], addr[4], addr[5]);
+	pw_log_info("  request-count: %d", AVB_PACKET_MAAP_GET_REQUEST_COUNT(p));
+	addr = AVB_PACKET_MAAP_GET_CONFLICT_START(p);
+	pw_log_info("  conflict-start: %02x:%02x:%02x:%02x:%02x:%02x",
+			addr[0], addr[1], addr[2], addr[3], addr[4], addr[5]);
+	pw_log_info("  conflict-count: %d", AVB_PACKET_MAAP_GET_CONFLICT_COUNT(p));
+}
+
+#define PROBE_TIMEOUT(n) ((n) + (MAAP_PROBE_INTERVAL_MS + \
+                        drand48() * MAAP_PROBE_INTERVAL_VAR_MS) * SPA_NSEC_PER_MSEC)
+#define ANNOUNCE_TIMEOUT(n) ((n) + (MAAP_ANNOUNCE_INTERVAL_MS + \
+                        drand48() * MAAP_ANNOUNCE_INTERVAL_VAR_MS) * SPA_NSEC_PER_MSEC)
+
+static int make_new_address(struct maap *maap, uint64_t now, int range)
+{
+	maap->offset = nrand48(maap->xsubi) % (MAAP_ALLOCATION_POOL_SIZE - range);
+	maap->count = range;
+	maap->state = STATE_PROBE;
+	maap->probe_count = MAAP_PROBE_RETRANSMITS;
+	maap->timeout = PROBE_TIMEOUT(now);
+	return 0;
+}
+
+static uint16_t maap_check_conflict(struct maap *maap, const uint8_t request_start[6],
+		uint16_t request_count, uint8_t conflict_start[6])
+{
+	uint16_t our_start, our_end;
+	uint16_t req_start, req_end;
+	uint16_t conf_start, conf_count = 0;
+
+	if (memcmp(request_start, maap_base, 4) != 0)
+		return 0;
+
+	our_start = maap->offset;
+	our_end = our_start + maap->count;
+	req_start = request_start[4] << 8 | request_start[5];
+	req_end = req_start + request_count;
+
+	if (our_start >= req_start && our_start <= req_end) {
+		conf_start = our_start;
+		conf_count = SPA_MIN(our_end, req_end) - our_start;
+	}
+	else if (req_start >= our_start && req_start <= our_end) {
+		conf_start = req_start;
+		conf_count = SPA_MIN(req_end, our_end) - req_start;
+	}
+	if (conf_count == 0)
+		return 0;
+
+	conflict_start[4] = conf_start >> 8;
+	conflict_start[5] = conf_start;
+	return conf_count;
+}
+
+static int send_packet(struct maap *maap, uint64_t now,
+		uint8_t type, const uint8_t conflict_start[6], uint16_t conflict_count)
+{
+	struct avb_ethernet_header *h;
+	struct avb_packet_maap *p;
+	uint8_t buf[1024];
+	uint8_t bmac[6] = AVB_MAAP_MAC;
+	int res = 0;
+	uint8_t start[6];
+
+	spa_memzero(buf, sizeof(buf));
+	h = (void*)buf;
+	p = SPA_PTROFF(h, sizeof(*h), void);
+
+	memcpy(h->dest, bmac, 6);
+	memcpy(h->src, maap->server->mac_addr, 6);
+	h->type = htons(AVB_TSN_ETH);
+
+	p->hdr.subtype = AVB_SUBTYPE_MAAP;
+	AVB_PACKET_SET_LENGTH(&p->hdr, sizeof(*p));
+
+	AVB_PACKET_MAAP_SET_MAAP_VERSION(p, 1);
+	AVB_PACKET_MAAP_SET_MESSAGE_TYPE(p, type);
+
+	memcpy(start, maap_base, 4);
+	start[4] = maap->offset >> 8;
+	start[5] = maap->offset;
+	AVB_PACKET_MAAP_SET_REQUEST_START(p, start);
+	AVB_PACKET_MAAP_SET_REQUEST_COUNT(p, maap->count);
+	if (conflict_count) {
+		AVB_PACKET_MAAP_SET_CONFLICT_START(p, conflict_start);
+		AVB_PACKET_MAAP_SET_CONFLICT_COUNT(p, conflict_count);
+	}
+
+	if (maap->server->debug_messages) {
+		pw_log_info("send: %d (%s)", type, message_type_as_string(type));
+		maap_message_debug(maap, p);
+	}
+
+	if (send(maap->source->fd, p, sizeof(*h) + sizeof(*p), 0) < 0) {
+		res = -errno;
+		pw_log_warn("got send error: %m");
+	}
+	return res;
+}
+
+static int handle_probe(struct maap *maap, uint64_t now, const struct avb_packet_maap *p)
+{
+	uint8_t conflict_start[6];
+	uint16_t conflict_count;
+
+	conflict_count = maap_check_conflict(maap, p->request_start, ntohs(p->request_count),
+				conflict_start);
+	if (conflict_count == 0)
+		return 0;
+
+	switch (maap->state) {
+	case STATE_PROBE:
+		make_new_address(maap, now, 8);
+		break;
+	case STATE_ANNOUNCE:
+		send_packet(maap, now, AVB_MAAP_MESSAGE_TYPE_DEFEND, conflict_start, conflict_count);
+		break;
+	}
+	return 0;
+}
+
+static int handle_defend(struct maap *maap, uint64_t now, const struct avb_packet_maap *p)
+{
+	uint8_t conflict_start[6];
+	uint16_t conflict_count;
+
+	conflict_count = maap_check_conflict(maap, p->conflict_start, ntohs(p->conflict_count),
+				conflict_start);
+	if (conflict_count != 0)
+		make_new_address(maap, now, 8);
+	return 0;
+}
+
+static int maap_message(struct maap *maap, uint64_t now, const void *message, int len)
+{
+	const struct avb_packet_maap *p = message;
+
+	if (AVB_PACKET_GET_SUBTYPE(&p->hdr) != AVB_SUBTYPE_MAAP)
+		return 0;
+
+	if (maap->server->debug_messages)
+		maap_message_debug(maap, p);
+
+	switch (AVB_PACKET_MAAP_GET_MESSAGE_TYPE(p)) {
+	case AVB_MAAP_MESSAGE_TYPE_PROBE:
+		handle_probe(maap, now, p);
+		break;
+	case AVB_MAAP_MESSAGE_TYPE_DEFEND:
+	case AVB_MAAP_MESSAGE_TYPE_ANNOUNCE:
+		handle_defend(maap, now, p);
+		break;
+	}
+	return 0;
+}
+
+static void on_socket_data(void *data, int fd, uint32_t mask)
+{
+	struct maap *maap = data;
+	struct timespec now;
+
+	if (mask & SPA_IO_IN) {
+		int len;
+		uint8_t buffer[2048];
+
+		len = recv(fd, buffer, sizeof(buffer), 0);
+
+		if (len < 0) {
+			pw_log_warn("got recv error: %m");
+		}
+		else if (len < (int)sizeof(struct avb_packet_header)) {
+			pw_log_warn("short packet received (%d < %d)", len,
+					(int)sizeof(struct avb_packet_header));
+		} else {
+			clock_gettime(CLOCK_REALTIME, &now);
+			maap_message(maap, SPA_TIMESPEC_TO_NSEC(&now), buffer, len);
+		}
+	}
+}
+
+static int load_state(struct maap *maap)
+{
+	const char *str;
+	char key[512];
+	struct spa_json it[3];
+	bool have_offset = false;
+	int count = 0, offset = 0;
+
+	snprintf(key, sizeof(key), "maap.%s", maap->server->ifname);
+	pw_conf_load_state("module-avb", key, maap->props);
+
+	if ((str = pw_properties_get(maap->props, "maap.addresses")) == NULL)
+		return 0;
+
+	spa_json_init(&it[0], str, strlen(str));
+	if (spa_json_enter_array(&it[0], &it[1]) <= 0)
+		return 0;
+
+	if (spa_json_enter_object(&it[1], &it[2]) <= 0)
+		return 0;
+
+	while (spa_json_get_string(&it[2], key, sizeof(key)) > 0) {
+		const char *val;
+		int len;
+
+		if ((len = spa_json_next(&it[2], &val)) <= 0)
+			break;
+
+		if (spa_streq(key, "start")) {
+			uint8_t addr[6];
+			if (avb_utils_parse_addr(val, len, addr) >= 0 &&
+			    memcmp(addr, maap_base, 4) == 0) {
+				offset = addr[4] << 8 | addr[5];
+				have_offset = true;
+			}
+		}
+		else if (spa_streq(key, "count")) {
+			spa_json_parse_int(val, len, &count);
+		}
+	}
+	if (count > 0 && have_offset) {
+		maap->count = count;
+		maap->offset = offset;
+		maap->state = STATE_PROBE;
+		maap->probe_count = MAAP_PROBE_RETRANSMITS;
+		maap->timeout = PROBE_TIMEOUT(0);
+	}
+	return 0;
+}
+
+static int save_state(struct maap *maap)
+{
+	char *ptr;
+	size_t size;
+	FILE *f;
+	char key[512];
+	uint32_t count;
+
+	if ((f = open_memstream(&ptr, &size)) == NULL)
+		return -errno;
+
+	fprintf(f, "[ ");
+	fprintf(f, "{ \"start\": \"%02x:%02x:%02x:%02x:%02x:%02x\", ",
+			maap_base[0], maap_base[1], maap_base[2],
+			maap_base[3], (maap->offset >> 8) & 0xff,
+			maap->offset & 0xff);
+	fprintf(f, " \"count\": %u } ", maap->count);
+	fprintf(f, "]");
+	fclose(f);
+
+	count = pw_properties_set(maap->props, "maap.addresses", ptr);
+	free(ptr);
+
+	if (count > 0) {
+		snprintf(key, sizeof(key), "maap.%s", maap->server->ifname);
+		pw_conf_save_state("module-avb", key, maap->props);
+	}
+	return 0;
+}
+
+static void maap_periodic(void *data, uint64_t now)
+{
+	struct maap *maap = data;
+
+	if (now < maap->timeout)
+		return;
+
+	switch(maap->state) {
+	case STATE_IDLE:
+		break;
+	case STATE_PROBE:
+		send_packet(maap, now, AVB_MAAP_MESSAGE_TYPE_PROBE, NULL, 0);
+		if (--maap->probe_count == 0) {
+			maap->state = STATE_ANNOUNCE;
+			save_state(maap);
+		}
+		maap->timeout = PROBE_TIMEOUT(now);
+		break;
+	case STATE_ANNOUNCE:
+		send_packet(maap, now, AVB_MAAP_MESSAGE_TYPE_ANNOUNCE, NULL, 0);
+		maap->timeout = ANNOUNCE_TIMEOUT(now);
+		break;
+	}
+}
+
+static void maap_free(struct maap *maap)
+{
+	pw_loop_destroy_source(maap->server->impl->loop, maap->source);
+	spa_hook_remove(&maap->server_listener);
+	pw_properties_free(maap->props);
+	free(maap);
+}
+
+static void maap_destroy(void *data)
+{
+	struct maap *maap = data;
+	maap_free(maap);
+}
+
+static const struct server_events server_events = {
+	AVB_VERSION_SERVER_EVENTS,
+	.destroy = maap_destroy,
+	.periodic = maap_periodic,
+};
+
+struct avb_maap *avb_maap_register(struct server *server)
+{
+	struct maap *maap;
+	uint8_t bmac[6] = AVB_MAAP_MAC;
+	int fd, res;
+
+	fd = avb_server_make_socket(server, AVB_TSN_ETH, bmac);
+	if (fd < 0) {
+		res = fd;
+		goto error;
+	}
+
+	maap = calloc(1, sizeof(*maap));
+	if (maap == NULL) {
+		res = -errno;
+		goto error_close;
+	}
+	maap->props = pw_properties_new(NULL, NULL);
+	if (maap->props == NULL) {
+		res = -errno;
+		goto error_free;
+	}
+
+	maap->server = server;
+	pw_log_info("0x%"PRIx64" %d", server->entity_id, server->ifindex);
+
+	if (pw_getrandom(maap->xsubi, sizeof(maap->xsubi), 0) != sizeof(maap->xsubi)) {
+		res = -errno;
+		goto error_free;
+	}
+	load_state(maap);
+
+	maap->source = pw_loop_add_io(server->impl->loop, fd, SPA_IO_IN, true, on_socket_data, maap);
+	if (maap->source == NULL) {
+		res = -errno;
+		pw_log_error("maap %p: can't create maap source: %m", maap);
+		goto error_free;
+	}
+	avdecc_server_add_listener(server, &maap->server_listener, &server_events, maap);
+
+	return (struct avb_maap *)maap;
+
+error_free:
+	free(maap);
+error_close:
+	close(fd);
+error:
+	errno = -res;
+	return NULL;
+}
+
+int avb_maap_reserve(struct avb_maap *m, uint32_t count)
+{
+	struct maap *maap = (struct maap*)m;
+	if (count > maap->count)
+		make_new_address(maap, 0, count);
+	return 0;
+}
+
+int avb_maap_get_address(struct avb_maap *m, uint8_t addr[6], uint32_t index)
+{
+	struct maap *maap = (struct maap*)m;
+	uint16_t offset;
+
+	if (maap->state != STATE_ANNOUNCE)
+		return -EAGAIN;
+
+	memcpy(addr, maap_base, 6);
+	offset = maap->offset + index;
+	addr[4] = offset >> 8;
+	addr[5] = offset;
+	return 0;
+}
diff --git a/src/modules/module-avb/maap.h b/src/modules/module-avb/maap.h
new file mode 100644
index 0000000000000000000000000000000000000000..6e56f8e9ae857a24913fff9871da203c150d550b
--- /dev/null
+++ b/src/modules/module-avb/maap.h
@@ -0,0 +1,70 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef AVB_MAAP_H
+#define AVB_MAAP_H
+
+#include "packets.h"
+#include "internal.h"
+
+#define AVB_TSN_ETH 0x22f0
+#define AVB_MAAP_MAC { 0x91, 0xe0, 0xf0, 0x00, 0xff, 0x00 };
+
+#define AVB_MAAP_MESSAGE_TYPE_PROBE		1
+#define AVB_MAAP_MESSAGE_TYPE_DEFEND		2
+#define AVB_MAAP_MESSAGE_TYPE_ANNOUNCE		3
+
+struct avb_packet_maap {
+	struct avb_packet_header hdr;
+	uint64_t stream_id;
+	uint8_t request_start[6];
+	uint16_t request_count;
+	uint8_t conflict_start[6];
+	uint16_t conflict_count;
+} __attribute__ ((__packed__));
+
+#define AVB_PACKET_MAAP_SET_MESSAGE_TYPE(p,v)		AVB_PACKET_SET_SUB1(&(p)->hdr, v)
+#define AVB_PACKET_MAAP_SET_MAAP_VERSION(p,v)		AVB_PACKET_SET_SUB2(&(p)->hdr, v)
+#define AVB_PACKET_MAAP_SET_STREAM_ID(p,v)		((p)->stream_id = htobe64(v))
+#define AVB_PACKET_MAAP_SET_REQUEST_START(p,v)		memcpy((p)->request_start, (v), 6)
+#define AVB_PACKET_MAAP_SET_REQUEST_COUNT(p,v)		((p)->request_count = htons(v))
+#define AVB_PACKET_MAAP_SET_CONFLICT_START(p,v)		memcpy((p)->conflict_start, (v), 6)
+#define AVB_PACKET_MAAP_SET_CONFLICT_COUNT(p,v)		((p)->conflict_count = htons(v))
+
+#define AVB_PACKET_MAAP_GET_MESSAGE_TYPE(p)		AVB_PACKET_GET_SUB1(&(p)->hdr)
+#define AVB_PACKET_MAAP_GET_MAAP_VERSION(p)		AVB_PACKET_GET_SUB2(&(p)->hdr)
+#define AVB_PACKET_MAAP_GET_STREAM_ID(p)		be64toh((p)->stream_id)
+#define AVB_PACKET_MAAP_GET_REQUEST_START(p)		((p)->request_start)
+#define AVB_PACKET_MAAP_GET_REQUEST_COUNT(p)		ntohs((p)->request_count)
+#define AVB_PACKET_MAAP_GET_CONFLICT_START(p)		((p)->conflict_start)
+#define AVB_PACKET_MAAP_GET_CONFLICT_COUNT(p)		ntohs((p)->conflict_count)
+
+struct avb_maap;
+
+struct avb_maap *avb_maap_register(struct server *server);
+
+int avb_maap_reserve(struct avb_maap *maap, uint32_t count);
+int avb_maap_get_address(struct avb_maap *maap, uint8_t addr[6], uint32_t index);
+
+#endif /* AVB_MAAP_H */
diff --git a/src/modules/module-avb/mmrp.c b/src/modules/module-avb/mmrp.c
new file mode 100644
index 0000000000000000000000000000000000000000..022aea8b3c25334fb55e03de848d7a1995cd624f
--- /dev/null
+++ b/src/modules/module-avb/mmrp.c
@@ -0,0 +1,233 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <unistd.h>
+
+#include <pipewire/pipewire.h>
+
+#include "utils.h"
+#include "mmrp.h"
+
+static const uint8_t mmrp_mac[6] = AVB_MMRP_MAC;
+
+struct attr {
+	struct avb_mmrp_attribute attr;
+	struct spa_list link;
+};
+
+struct mmrp {
+	struct server *server;
+	struct spa_hook server_listener;
+
+	struct spa_source *source;
+
+	struct spa_list attributes;
+};
+
+static bool mmrp_check_header(void *data, const void *hdr, size_t *hdr_size, bool *has_params)
+{
+	const struct avb_packet_mmrp_msg *msg = hdr;
+	uint8_t attr_type = msg->attribute_type;
+
+	if (!AVB_MMRP_ATTRIBUTE_TYPE_VALID(attr_type))
+		return false;
+
+	*hdr_size = sizeof(*msg);
+	*has_params = false;
+	return true;
+}
+
+static int mmrp_attr_event(void *data, uint64_t now, uint8_t attribute_type, uint8_t event)
+{
+	struct mmrp *mmrp = data;
+	struct attr *a;
+	spa_list_for_each(a, &mmrp->attributes, link)
+		if (a->attr.type == attribute_type)
+			avb_mrp_attribute_update_state(a->attr.mrp, now, event);
+	return 0;
+}
+
+static void debug_service_requirement(const struct avb_packet_mmrp_service_requirement *t)
+{
+	char buf[128];
+	pw_log_info("service requirement");
+	pw_log_info(" %s", avb_utils_format_addr(buf, sizeof(buf), t->addr));
+}
+
+static int process_service_requirement(struct mmrp *mmrp, uint64_t now, uint8_t attr_type,
+		const void *m, uint8_t event, uint8_t param, int num)
+{
+	const struct avb_packet_mmrp_service_requirement *t = m;
+	struct attr *a;
+
+	debug_service_requirement(t);
+
+	spa_list_for_each(a, &mmrp->attributes, link)
+		if (a->attr.type == attr_type &&
+		    memcmp(a->attr.attr.service_requirement.addr, t->addr, 6) == 0)
+			avb_mrp_attribute_rx_event(a->attr.mrp, now, event);
+	return 0;
+}
+
+static void debug_process_mac(const struct avb_packet_mmrp_mac *t)
+{
+	char buf[128];
+	pw_log_info("mac");
+	pw_log_info(" %s", avb_utils_format_addr(buf, sizeof(buf), t->addr));
+}
+
+static int process_mac(struct mmrp *mmrp, uint64_t now, uint8_t attr_type,
+		const void *m, uint8_t event, uint8_t param, int num)
+{
+	const struct avb_packet_mmrp_mac *t = m;
+	struct attr *a;
+
+	debug_process_mac(t);
+
+	spa_list_for_each(a, &mmrp->attributes, link)
+		if (a->attr.type == attr_type &&
+		    memcmp(a->attr.attr.mac.addr, t->addr, 6) == 0)
+			avb_mrp_attribute_rx_event(a->attr.mrp, now, event);
+	return 0;
+}
+
+static const struct {
+	int (*dispatch) (struct mmrp *mmrp, uint64_t now, uint8_t attr_type,
+			const void *m, uint8_t event, uint8_t param, int num);
+} dispatch[] = {
+	[AVB_MMRP_ATTRIBUTE_TYPE_SERVICE_REQUIREMENT] = { process_service_requirement, },
+	[AVB_MMRP_ATTRIBUTE_TYPE_MAC] = { process_mac, },
+};
+
+static int mmrp_process(void *data, uint64_t now, uint8_t attribute_type, const void *value,
+			uint8_t event, uint8_t param, int index)
+{
+	struct mmrp *mmrp = data;
+	return dispatch[attribute_type].dispatch(mmrp, now,
+				attribute_type, value, event, param, index);
+}
+
+static const struct avb_mrp_parse_info info = {
+	AVB_VERSION_MRP_PARSE_INFO,
+	.check_header = mmrp_check_header,
+	.attr_event = mmrp_attr_event,
+	.process = mmrp_process,
+};
+
+static int mmrp_message(struct mmrp *mmrp, uint64_t now, const void *message, int len)
+{
+	pw_log_debug("MMRP");
+	return avb_mrp_parse_packet(mmrp->server->mrp,
+			now, message, len, &info, mmrp);
+}
+
+static void on_socket_data(void *data, int fd, uint32_t mask)
+{
+	struct mmrp *mmrp = data;
+	struct timespec now;
+
+	if (mask & SPA_IO_IN) {
+		int len;
+		uint8_t buffer[2048];
+
+		len = recv(fd, buffer, sizeof(buffer), 0);
+
+		if (len < 0) {
+			pw_log_warn("got recv error: %m");
+		}
+		else if (len < (int)sizeof(struct avb_packet_header)) {
+			pw_log_warn("short packet received (%d < %d)", len,
+					(int)sizeof(struct avb_packet_header));
+		} else {
+			clock_gettime(CLOCK_REALTIME, &now);
+			mmrp_message(mmrp, SPA_TIMESPEC_TO_NSEC(&now), buffer, len);
+		}
+	}
+}
+static void mmrp_destroy(void *data)
+{
+	struct mmrp *mmrp = data;
+	spa_hook_remove(&mmrp->server_listener);
+	pw_loop_destroy_source(mmrp->server->impl->loop, mmrp->source);
+	free(mmrp);
+}
+
+static const struct server_events server_events = {
+	AVB_VERSION_SERVER_EVENTS,
+	.destroy = mmrp_destroy,
+};
+
+struct avb_mmrp_attribute *avb_mmrp_attribute_new(struct avb_mmrp *m,
+		uint8_t type)
+{
+	struct mmrp *mmrp = (struct mmrp*)m;
+	struct avb_mrp_attribute *attr;
+	struct attr *a;
+
+	attr = avb_mrp_attribute_new(mmrp->server->mrp, sizeof(struct attr));
+
+	a = attr->user_data;
+	a->attr.mrp = attr;
+	a->attr.type = type;
+	spa_list_append(&mmrp->attributes, &a->link);
+
+	return &a->attr;
+}
+
+struct avb_mmrp *avb_mmrp_register(struct server *server)
+{
+	struct mmrp *mmrp;
+	int fd, res;
+
+	fd = avb_server_make_socket(server, AVB_MMRP_ETH, mmrp_mac);
+	if (fd < 0) {
+		errno = -fd;
+		return NULL;
+	}
+	mmrp = calloc(1, sizeof(*mmrp));
+	if (mmrp == NULL) {
+		res = -errno;
+		goto error_close;
+	}
+
+	mmrp->server = server;
+	spa_list_init(&mmrp->attributes);
+
+	mmrp->source = pw_loop_add_io(server->impl->loop, fd, SPA_IO_IN, true, on_socket_data, mmrp);
+	if (mmrp->source == NULL) {
+		res = -errno;
+		pw_log_error("mmrp %p: can't create mmrp source: %m", mmrp);
+		goto error_no_source;
+	}
+	avdecc_server_add_listener(server, &mmrp->server_listener, &server_events, mmrp);
+
+	return (struct avb_mmrp*)mmrp;
+
+error_no_source:
+	free(mmrp);
+error_close:
+	close(fd);
+	errno = -res;
+	return NULL;
+}
diff --git a/src/modules/module-avb/mmrp.h b/src/modules/module-avb/mmrp.h
new file mode 100644
index 0000000000000000000000000000000000000000..b7bcf8c46ef78d5189a3ae58c79b669d21fe11e4
--- /dev/null
+++ b/src/modules/module-avb/mmrp.h
@@ -0,0 +1,68 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef AVB_MMRP_H
+#define AVB_MMRP_H
+
+#include "mrp.h"
+#include "internal.h"
+
+#define AVB_MMRP_ETH 0x88f6
+#define AVB_MMRP_MAC { 0x01, 0x80, 0xc2, 0x00, 0x00, 0x20 }
+
+#define AVB_MMRP_ATTRIBUTE_TYPE_SERVICE_REQUIREMENT	1
+#define AVB_MMRP_ATTRIBUTE_TYPE_MAC			2
+#define AVB_MMRP_ATTRIBUTE_TYPE_VALID(t)		((t)>=1 && (t)<=2)
+
+struct avb_packet_mmrp_msg {
+	uint8_t attribute_type;
+	uint8_t attribute_length;
+	uint8_t attribute_list[0];
+} __attribute__ ((__packed__));
+
+struct avb_packet_mmrp_service_requirement {
+	unsigned char addr[6];
+} __attribute__ ((__packed__));
+
+struct avb_packet_mmrp_mac {
+	unsigned char addr[6];
+} __attribute__ ((__packed__));
+
+struct avb_mmrp;
+
+struct avb_mmrp_attribute {
+	struct avb_mrp_attribute *mrp;
+	uint8_t type;
+	union {
+		struct avb_packet_mmrp_service_requirement service_requirement;
+		struct avb_packet_mmrp_mac mac;
+	} attr;
+};
+
+struct avb_mmrp_attribute *avb_mmrp_attribute_new(struct avb_mmrp *mmrp,
+		uint8_t type);
+
+struct avb_mmrp *avb_mmrp_register(struct server *server);
+
+#endif /* AVB_MMRP_H */
diff --git a/src/modules/module-avb/mrp.c b/src/modules/module-avb/mrp.c
new file mode 100644
index 0000000000000000000000000000000000000000..7b6bc464681a9591a2b68e75a3fde7d5aeb1ceec
--- /dev/null
+++ b/src/modules/module-avb/mrp.c
@@ -0,0 +1,612 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <pipewire/pipewire.h>
+
+#include "mrp.h"
+
+#define MRP_JOINTIMER_MS	100
+#define MRP_LVTIMER_MS		1000
+#define MRP_LVATIMER_MS		10000
+#define MRP_PERIODTIMER_MS	1000
+
+#define mrp_emit(s,m,v,...)		spa_hook_list_call(&s->listener_list, struct avb_mrp_events, m, v, ##__VA_ARGS__)
+#define mrp_emit_event(s,n,e)		mrp_emit(s,event,0,n,e)
+#define mrp_emit_notify(s,n,a,e)	mrp_emit(s,notify,0,n,a,e)
+
+#define mrp_attribute_emit(a,m,v,...)		spa_hook_list_call(&a->listener_list, struct avb_mrp_attribute_events, m, v, ##__VA_ARGS__)
+#define mrp_attribute_emit_notify(a,n,e)	mrp_attribute_emit(a,notify,0,n,e)
+
+
+struct mrp;
+
+struct attribute {
+	struct avb_mrp_attribute attr;
+	struct mrp *mrp;
+	struct spa_list link;
+	uint8_t applicant_state;
+	uint8_t registrar_state;
+	uint64_t leave_timeout;
+	unsigned joined:1;
+	struct spa_hook_list listener_list;
+};
+
+struct mrp {
+	struct server *server;
+	struct spa_hook server_listener;
+
+	struct spa_hook_list listener_list;
+
+	struct spa_list attributes;
+
+	uint64_t periodic_timeout;
+	uint64_t leave_all_timeout;
+	uint64_t join_timeout;
+};
+
+static void mrp_destroy(void *data)
+{
+	struct mrp *mrp = data;
+	spa_hook_remove(&mrp->server_listener);
+	free(mrp);
+}
+
+static void global_event(struct mrp *mrp, uint64_t now, uint8_t event)
+{
+	struct attribute *a;
+	spa_list_for_each(a, &mrp->attributes, link)
+		avb_mrp_attribute_update_state(&a->attr, now, event);
+	mrp_emit_event(mrp, now, event);
+}
+
+static void mrp_periodic(void *data, uint64_t now)
+{
+	struct mrp *mrp = data;
+	bool leave_all = false;
+	struct attribute *a;
+
+	if (now > mrp->periodic_timeout) {
+		if (mrp->periodic_timeout > 0)
+			global_event(mrp, now, AVB_MRP_EVENT_PERIODIC);
+		mrp->periodic_timeout = now + MRP_PERIODTIMER_MS * SPA_NSEC_PER_MSEC;
+	}
+	if (now > mrp->leave_all_timeout) {
+		if (mrp->leave_all_timeout > 0) {
+			global_event(mrp, now, AVB_MRP_EVENT_RX_LVA);
+			leave_all = true;
+		}
+		mrp->leave_all_timeout = now + (MRP_LVATIMER_MS + (random() % (MRP_LVATIMER_MS / 2)))
+			* SPA_NSEC_PER_MSEC;
+	}
+
+	if (now > mrp->join_timeout) {
+		if (mrp->join_timeout > 0) {
+			uint8_t event = leave_all ? AVB_MRP_EVENT_TX_LVA : AVB_MRP_EVENT_TX;
+			global_event(mrp, now, event);
+		}
+		mrp->join_timeout = now + MRP_JOINTIMER_MS * SPA_NSEC_PER_MSEC;
+	}
+
+	spa_list_for_each(a, &mrp->attributes, link) {
+		if (a->leave_timeout > 0 && now > a->leave_timeout) {
+			a->leave_timeout = 0;
+			avb_mrp_attribute_update_state(&a->attr, now, AVB_MRP_EVENT_LV_TIMER);
+		}
+	}
+}
+
+static const struct server_events server_events = {
+	AVB_VERSION_SERVER_EVENTS,
+	.destroy = mrp_destroy,
+	.periodic = mrp_periodic,
+};
+
+int avb_mrp_parse_packet(struct avb_mrp *mrp, uint64_t now, const void *pkt, int len,
+		const struct avb_mrp_parse_info *info, void *data)
+{
+	uint8_t *e = SPA_PTROFF(pkt, len, uint8_t);
+	uint8_t *m = SPA_PTROFF(pkt, sizeof(struct avb_packet_mrp), uint8_t);
+
+	while (m < e && (m[0] != 0 || m[1] != 0)) {
+		const struct avb_packet_mrp_hdr *hdr = (const struct avb_packet_mrp_hdr*)m;
+		uint8_t attr_type = hdr->attribute_type;
+		uint8_t attr_len = hdr->attribute_length;
+		size_t hdr_size;
+		bool has_param;
+
+		if (!info->check_header(data, hdr, &hdr_size, &has_param))
+			return -EINVAL;
+
+		m += hdr_size;
+
+		while (m < e && (m[0] != 0 || m[1] != 0)) {
+			const struct avb_packet_mrp_vector *v =
+				(const struct avb_packet_mrp_vector*)m;
+			uint16_t i, num_values = AVB_MRP_VECTOR_GET_NUM_VALUES(v);
+			uint8_t event_len = (num_values+2)/3;
+			uint8_t param_len = has_param ? (num_values+3)/4 : 0;
+			int plen = sizeof(*v) + attr_len + event_len + param_len;
+			const uint8_t *first = v->first_value;
+			uint8_t event[3], param[4] = { 0, };
+
+			if (m + plen > e)
+				return -EPROTO;
+
+			if (v->lva)
+				info->attr_event(data, now, attr_type, AVB_MRP_EVENT_RX_LVA);
+
+			for (i = 0; i < num_values; i++) {
+				if (i % 3 == 0) {
+					uint8_t ep = first[attr_len + i/3];
+					event[2] = ep % 6; ep /= 6;
+					event[1] = ep % 6; ep /= 6;
+					event[0] = ep % 6;
+				}
+				if (has_param && (i % 4 == 0)) {
+					uint8_t ep = first[attr_len + event_len + i/4];
+					param[3] = ep % 4; ep /= 4;
+					param[2] = ep % 4; ep /= 4;
+					param[1] = ep % 4; ep /= 4;
+					param[0] = ep % 4;
+				}
+				info->process(data, now, attr_type, first,
+						event[i%3], param[i%4], i);
+			}
+			m += plen;
+		}
+		m += 2;
+	}
+	return 0;
+}
+
+const char *avb_mrp_notify_name(uint8_t notify)
+{
+	switch(notify) {
+	case AVB_MRP_NOTIFY_NEW:
+		return "new";
+	case AVB_MRP_NOTIFY_JOIN:
+		return "join";
+	case AVB_MRP_NOTIFY_LEAVE:
+		return "leave";
+	}
+	return "unknown";
+}
+
+const char *avb_mrp_send_name(uint8_t send)
+{
+	switch(send) {
+	case AVB_MRP_SEND_NEW:
+		return "new";
+	case AVB_MRP_SEND_JOININ:
+		return "joinin";
+	case AVB_MRP_SEND_IN:
+		return "in";
+	case AVB_MRP_SEND_JOINMT:
+		return "joinmt";
+	case AVB_MRP_SEND_MT:
+		return "mt";
+	case AVB_MRP_SEND_LV:
+		return "leave";
+	}
+	return "unknown";
+}
+
+struct avb_mrp_attribute *avb_mrp_attribute_new(struct avb_mrp *m,
+		size_t user_size)
+{
+	struct mrp *mrp = (struct mrp*)m;
+	struct attribute *a;
+
+	a = calloc(1, sizeof(*a) + user_size);
+	if (a == NULL)
+		return NULL;
+
+	a->mrp = mrp;
+	a->attr.user_data = SPA_PTROFF(a, sizeof(*a), void);
+	spa_hook_list_init(&a->listener_list);
+	spa_list_append(&mrp->attributes, &a->link);
+
+	return &a->attr;
+}
+
+void avb_mrp_attribute_destroy(struct avb_mrp_attribute *attr)
+{
+	struct attribute *a = SPA_CONTAINER_OF(attr, struct attribute, attr);
+	spa_list_remove(&a->link);
+	free(a);
+}
+
+void avb_mrp_attribute_add_listener(struct avb_mrp_attribute *attr, struct spa_hook *listener,
+		const struct avb_mrp_attribute_events *events, void *data)
+{
+	struct attribute *a = SPA_CONTAINER_OF(attr, struct attribute, attr);
+	spa_hook_list_append(&a->listener_list, listener, events, data);
+}
+
+void avb_mrp_attribute_update_state(struct avb_mrp_attribute *attr, uint64_t now,
+		int event)
+{
+	struct attribute *a = SPA_CONTAINER_OF(attr, struct attribute, attr);
+	struct mrp *mrp = a->mrp;
+	uint8_t notify = 0, state;
+	uint8_t send = 0;
+
+	state = a->registrar_state;
+
+	switch (event) {
+	case AVB_MRP_EVENT_BEGIN:
+		state = AVB_MRP_MT;
+		break;
+	case AVB_MRP_EVENT_RX_NEW:
+		notify = AVB_MRP_NOTIFY_NEW;
+		switch (state) {
+		case AVB_MRP_LV:
+			a->leave_timeout = 0;
+			break;
+		}
+		state = AVB_MRP_IN;
+		break;
+	case AVB_MRP_EVENT_RX_JOININ:
+	case AVB_MRP_EVENT_RX_JOINMT:
+		switch (state) {
+		case AVB_MRP_LV:
+			a->leave_timeout = 0;
+                        break;
+		case AVB_MRP_MT:
+			notify = AVB_MRP_NOTIFY_JOIN;
+			break;
+		}
+		state = AVB_MRP_IN;
+		break;
+	case AVB_MRP_EVENT_RX_LV:
+	case AVB_MRP_EVENT_RX_LVA:
+	case AVB_MRP_EVENT_TX_LVA:
+	case AVB_MRP_EVENT_REDECLARE:
+		switch (state) {
+		case AVB_MRP_IN:
+			a->leave_timeout = now + MRP_LVTIMER_MS * SPA_NSEC_PER_MSEC;
+			//state = AVB_MRP_LV;
+			break;
+		}
+		break;
+	case AVB_MRP_EVENT_FLUSH:
+		switch (state) {
+		case AVB_MRP_LV:
+			notify = AVB_MRP_NOTIFY_LEAVE;
+			break;
+		}
+		state = AVB_MRP_MT;
+		break;
+	case AVB_MRP_EVENT_LV_TIMER:
+		switch (state) {
+		case AVB_MRP_LV:
+			notify = AVB_MRP_NOTIFY_LEAVE;
+			state = AVB_MRP_MT;
+			break;
+		}
+		break;
+	default:
+		break;
+	}
+	if (notify) {
+		mrp_attribute_emit_notify(a, now, notify);
+		mrp_emit_notify(mrp, now, &a->attr, notify);
+	}
+
+	if (a->registrar_state != state || notify) {
+		pw_log_debug("attr %p: %d %d -> %d %d", a, event, a->registrar_state, state, notify);
+		a->registrar_state = state;
+	}
+
+	state = a->applicant_state;
+
+	switch (event) {
+	case AVB_MRP_EVENT_BEGIN:
+		state = AVB_MRP_VO;
+		break;
+	case AVB_MRP_EVENT_NEW:
+		switch (state) {
+		case AVB_MRP_VN:
+		case AVB_MRP_AN:
+			break;
+		default:
+			state = AVB_MRP_VN;
+			break;
+		}
+		break;
+	case AVB_MRP_EVENT_JOIN:
+		switch (state) {
+		case AVB_MRP_VO:
+		case AVB_MRP_LO:
+			state = AVB_MRP_VP;
+			break;
+		case AVB_MRP_LA:
+			state = AVB_MRP_AA;
+			break;
+		case AVB_MRP_AO:
+			state = AVB_MRP_AP;
+			break;
+		case AVB_MRP_QO:
+			state = AVB_MRP_QP;
+			break;
+		}
+		break;
+	case AVB_MRP_EVENT_LV:
+		switch (state) {
+		case AVB_MRP_VP:
+			state = AVB_MRP_VO;
+			break;
+		case AVB_MRP_VN:
+		case AVB_MRP_AN:
+		case AVB_MRP_AA:
+		case AVB_MRP_QA:
+			state = AVB_MRP_LA;
+			break;
+		case AVB_MRP_AP:
+			state = AVB_MRP_AO;
+			break;
+		case AVB_MRP_QP:
+			state = AVB_MRP_QO;
+			break;
+		}
+		break;
+	case AVB_MRP_EVENT_RX_JOININ:
+		switch (state) {
+		case AVB_MRP_VO:
+			state = AVB_MRP_AO;
+			break;
+		case AVB_MRP_VP:
+			state = AVB_MRP_AP;
+			break;
+		case AVB_MRP_AA:
+			state = AVB_MRP_QA;
+			break;
+		case AVB_MRP_AO:
+			state = AVB_MRP_QO;
+			break;
+		case AVB_MRP_AP:
+			state = AVB_MRP_QP;
+			break;
+		}
+		SPA_FALLTHROUGH;
+	case AVB_MRP_EVENT_RX_IN:
+		switch (state) {
+		case AVB_MRP_AA:
+			state = AVB_MRP_QA;
+			break;
+		}
+		break;
+	case AVB_MRP_EVENT_RX_JOINMT:
+	case AVB_MRP_EVENT_RX_MT:
+		switch (state) {
+		case AVB_MRP_QA:
+			state = AVB_MRP_AA;
+			break;
+		case AVB_MRP_QO:
+			state = AVB_MRP_AO;
+			break;
+		case AVB_MRP_QP:
+			state = AVB_MRP_AP;
+			break;
+		case AVB_MRP_LO:
+			state = AVB_MRP_VO;
+			break;
+		}
+		break;
+	case AVB_MRP_EVENT_RX_LV:
+	case AVB_MRP_EVENT_RX_LVA:
+	case AVB_MRP_EVENT_REDECLARE:
+		switch (state) {
+		case AVB_MRP_VO:
+		case AVB_MRP_AO:
+		case AVB_MRP_QO:
+			state = AVB_MRP_LO;
+			break;
+		case AVB_MRP_AN:
+			state = AVB_MRP_VN;
+			break;
+		case AVB_MRP_AA:
+		case AVB_MRP_QA:
+		case AVB_MRP_AP:
+		case AVB_MRP_QP:
+			state = AVB_MRP_VP;
+			break;
+		}
+		break;
+	case AVB_MRP_EVENT_PERIODIC:
+		switch (state) {
+		case AVB_MRP_QA:
+			state = AVB_MRP_AA;
+			break;
+		case AVB_MRP_QP:
+			state = AVB_MRP_AP;
+			break;
+		}
+		break;
+	case AVB_MRP_EVENT_TX:
+		switch (state) {
+		case AVB_MRP_VP:
+		case AVB_MRP_AA:
+		case AVB_MRP_AP:
+			if (a->registrar_state == AVB_MRP_IN)
+				send = AVB_MRP_SEND_JOININ;
+			else
+				send = AVB_MRP_SEND_JOINMT;
+			break;
+		case AVB_MRP_VN:
+		case AVB_MRP_AN:
+			send = AVB_MRP_SEND_NEW;
+			break;
+		case AVB_MRP_LA:
+			send = AVB_MRP_SEND_LV;
+			break;
+		case AVB_MRP_LO:
+			if (a->registrar_state == AVB_MRP_IN)
+				send = AVB_MRP_SEND_IN;
+			else
+				send = AVB_MRP_SEND_MT;
+			break;
+		}
+		switch (state) {
+		case AVB_MRP_VP:
+			state = AVB_MRP_AA;
+			break;
+		case AVB_MRP_VN:
+			state = AVB_MRP_AN;
+			break;
+		case AVB_MRP_AN:
+			if(a->registrar_state == AVB_MRP_IN)
+				state = AVB_MRP_QA;
+			else
+				state = AVB_MRP_AA;
+			break;
+		case AVB_MRP_AA:
+		case AVB_MRP_AP:
+			state = AVB_MRP_QA;
+			break;
+		case AVB_MRP_LA:
+		case AVB_MRP_LO:
+			state = AVB_MRP_VO;
+			break;
+		}
+		break;
+	case AVB_MRP_EVENT_TX_LVA:
+	{
+		switch (state) {
+		case AVB_MRP_VP:
+			if (a->registrar_state == AVB_MRP_IN)
+				send = AVB_MRP_SEND_IN;
+			else
+				send = AVB_MRP_SEND_MT;
+			break;
+		case AVB_MRP_VN:
+		case AVB_MRP_AN:
+			send = AVB_MRP_SEND_NEW;
+			break;
+                case AVB_MRP_AA:
+                case AVB_MRP_QA:
+                case AVB_MRP_AP:
+		case AVB_MRP_QP:
+			if (a->registrar_state == AVB_MRP_IN)
+				send = AVB_MRP_SEND_JOININ;
+			else
+				send = AVB_MRP_SEND_JOINMT;
+			break;
+		}
+		switch (state) {
+		case AVB_MRP_VO:
+		case AVB_MRP_LA:
+		case AVB_MRP_AO:
+		case AVB_MRP_QO:
+			state = AVB_MRP_LO;
+			break;
+		case AVB_MRP_VP:
+			state = AVB_MRP_AA;
+			break;
+		case AVB_MRP_VN:
+			state = AVB_MRP_AN;
+			break;
+		case AVB_MRP_AN:
+		case AVB_MRP_AA:
+		case AVB_MRP_AP:
+		case AVB_MRP_QP:
+			state = AVB_MRP_QA;
+			break;
+		}
+		break;
+	}
+	default:
+		break;
+	}
+	if (a->applicant_state != state || send) {
+		pw_log_debug("attr %p: %d %d -> %d %d", a, event, a->applicant_state, state, send);
+		a->applicant_state = state;
+	}
+	if (a->joined)
+		a->attr.pending_send = send;
+}
+
+void avb_mrp_attribute_rx_event(struct avb_mrp_attribute *attr, uint64_t now, uint8_t event)
+{
+	static const int map[] = {
+		[AVB_MRP_ATTRIBUTE_EVENT_NEW] = AVB_MRP_EVENT_RX_NEW,
+		[AVB_MRP_ATTRIBUTE_EVENT_JOININ] = AVB_MRP_EVENT_RX_JOININ,
+		[AVB_MRP_ATTRIBUTE_EVENT_IN] = AVB_MRP_EVENT_RX_IN,
+		[AVB_MRP_ATTRIBUTE_EVENT_JOINMT] = AVB_MRP_EVENT_RX_JOINMT,
+		[AVB_MRP_ATTRIBUTE_EVENT_MT] = AVB_MRP_EVENT_RX_MT,
+		[AVB_MRP_ATTRIBUTE_EVENT_LV] = AVB_MRP_EVENT_RX_LV,
+	};
+	avb_mrp_attribute_update_state(attr, now, map[event]);
+}
+
+void avb_mrp_attribute_begin(struct avb_mrp_attribute *attr, uint64_t now)
+{
+	struct attribute *a = SPA_CONTAINER_OF(attr, struct attribute, attr);
+	a->leave_timeout = 0;
+	avb_mrp_attribute_update_state(attr, now, AVB_MRP_EVENT_BEGIN);
+}
+
+void avb_mrp_attribute_join(struct avb_mrp_attribute *attr, uint64_t now, bool is_new)
+{
+	struct attribute *a = SPA_CONTAINER_OF(attr, struct attribute, attr);
+	a->joined = true;
+	int event = is_new ? AVB_MRP_EVENT_NEW : AVB_MRP_EVENT_JOIN;
+	avb_mrp_attribute_update_state(attr, now, event);
+}
+
+void avb_mrp_attribute_leave(struct avb_mrp_attribute *attr, uint64_t now)
+{
+	struct attribute *a = SPA_CONTAINER_OF(attr, struct attribute, attr);
+	avb_mrp_attribute_update_state(attr, now, AVB_MRP_EVENT_LV);
+	a->joined = false;
+}
+
+void avb_mrp_destroy(struct avb_mrp *mrp)
+{
+	mrp_destroy(mrp);
+}
+
+struct avb_mrp *avb_mrp_new(struct server *server)
+{
+	struct mrp *mrp;
+
+	mrp = calloc(1, sizeof(*mrp));
+	if (mrp == NULL)
+		return NULL;
+
+	mrp->server = server;
+	spa_list_init(&mrp->attributes);
+	spa_hook_list_init(&mrp->listener_list);
+
+	avdecc_server_add_listener(server, &mrp->server_listener, &server_events, mrp);
+
+	return (struct avb_mrp*)mrp;
+}
+
+void avb_mrp_add_listener(struct avb_mrp *m, struct spa_hook *listener,
+		const struct avb_mrp_events *events, void *data)
+{
+	struct mrp *mrp = (struct mrp*)m;
+	spa_hook_list_append(&mrp->listener_list, listener, events, data);
+}
diff --git a/src/modules/module-avb/mrp.h b/src/modules/module-avb/mrp.h
new file mode 100644
index 0000000000000000000000000000000000000000..0a05d4b4da34878bec760e169da1581e13f90b96
--- /dev/null
+++ b/src/modules/module-avb/mrp.h
@@ -0,0 +1,181 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef AVB_MRP_H
+#define AVB_MRP_H
+
+#include "packets.h"
+#include "internal.h"
+
+#define AVB_MRP_PROTOCOL_VERSION	0
+
+struct avb_packet_mrp {
+	struct avb_ethernet_header eth;
+	uint8_t version;
+} __attribute__ ((__packed__));
+
+struct avb_packet_mrp_hdr {
+	uint8_t attribute_type;
+	uint8_t attribute_length;
+} __attribute__ ((__packed__));
+
+struct avb_packet_mrp_vector {
+#if __BYTE_ORDER == __BIG_ENDIAN
+	unsigned lva:3;
+	unsigned nv1:5;
+#elif __BYTE_ORDER == __LITTLE_ENDIAN
+	unsigned nv1:5;
+	unsigned lva:3;
+#endif
+	uint8_t nv2;
+	uint8_t first_value[0];
+} __attribute__ ((__packed__));
+
+#define AVB_MRP_VECTOR_SET_NUM_VALUES(a,v)	((a)->nv1 = ((v) >> 8),(a)->nv2 = (v))
+#define AVB_MRP_VECTOR_GET_NUM_VALUES(a)	((a)->nv1 << 8 | (a)->nv2)
+
+struct avb_packet_mrp_footer {
+	uint16_t end_mark;
+} __attribute__ ((__packed__));
+
+/* applicant states */
+#define AVB_MRP_VO	0		/* Very anxious Observer */
+#define AVB_MRP_VP	1		/* Very anxious Passive */
+#define AVB_MRP_VN	2		/* Very anxious New */
+#define AVB_MRP_AN	3		/* Anxious New */
+#define AVB_MRP_AA	4		/* Anxious Active */
+#define AVB_MRP_QA	5		/* Quiet Active */
+#define AVB_MRP_LA	6		/* Leaving Active */
+#define AVB_MRP_AO	7		/* Anxious Observer */
+#define AVB_MRP_QO	8		/* Quiet Observer */
+#define AVB_MRP_AP	9		/* Anxious Passive */
+#define AVB_MRP_QP	10		/* Quiet Passive */
+#define AVB_MRP_LO	11		/* Leaving Observer */
+
+/* registrar states */
+#define AVB_MRP_IN	16
+#define AVB_MRP_LV	17
+#define AVB_MRP_MT	18
+
+/* events */
+#define AVB_MRP_EVENT_BEGIN		0
+#define AVB_MRP_EVENT_NEW		1
+#define AVB_MRP_EVENT_JOIN		2
+#define AVB_MRP_EVENT_LV		3
+#define AVB_MRP_EVENT_TX		4
+#define AVB_MRP_EVENT_TX_LVA		5
+#define AVB_MRP_EVENT_TX_LVAF		6
+#define AVB_MRP_EVENT_RX_NEW		7
+#define AVB_MRP_EVENT_RX_JOININ		8
+#define AVB_MRP_EVENT_RX_IN		9
+#define AVB_MRP_EVENT_RX_JOINMT		10
+#define AVB_MRP_EVENT_RX_MT		11
+#define AVB_MRP_EVENT_RX_LV		12
+#define AVB_MRP_EVENT_RX_LVA		13
+#define AVB_MRP_EVENT_FLUSH		14
+#define AVB_MRP_EVENT_REDECLARE		15
+#define AVB_MRP_EVENT_PERIODIC		16
+#define AVB_MRP_EVENT_LV_TIMER		17
+#define AVB_MRP_EVENT_LVA_TIMER		18
+
+/* attribute events */
+#define AVB_MRP_ATTRIBUTE_EVENT_NEW	0
+#define AVB_MRP_ATTRIBUTE_EVENT_JOININ	1
+#define AVB_MRP_ATTRIBUTE_EVENT_IN	2
+#define AVB_MRP_ATTRIBUTE_EVENT_JOINMT	3
+#define AVB_MRP_ATTRIBUTE_EVENT_MT	4
+#define AVB_MRP_ATTRIBUTE_EVENT_LV	5
+
+#define AVB_MRP_SEND_NEW		1
+#define AVB_MRP_SEND_JOININ		2
+#define AVB_MRP_SEND_IN			3
+#define AVB_MRP_SEND_JOINMT		4
+#define AVB_MRP_SEND_MT			5
+#define AVB_MRP_SEND_LV			6
+
+#define AVB_MRP_NOTIFY_NEW		1
+#define AVB_MRP_NOTIFY_JOIN		2
+#define AVB_MRP_NOTIFY_LEAVE		3
+
+const char *avb_mrp_notify_name(uint8_t notify);
+const char *avb_mrp_send_name(uint8_t send);
+
+struct avb_mrp_attribute {
+	uint8_t pending_send;
+	void *user_data;
+};
+
+struct avb_mrp_attribute_events {
+#define AVB_VERSION_MRP_ATTRIBUTE_EVENTS	0
+	uint32_t version;
+
+	void (*notify) (void *data, uint64_t now, uint8_t notify);
+};
+
+struct avb_mrp_attribute *avb_mrp_attribute_new(struct avb_mrp *mrp,
+		size_t user_size);
+void avb_mrp_attribute_destroy(struct avb_mrp_attribute *attr);
+
+void avb_mrp_attribute_update_state(struct avb_mrp_attribute *attr, uint64_t now, int event);
+
+void avb_mrp_attribute_rx_event(struct avb_mrp_attribute *attr, uint64_t now, uint8_t event);
+
+void avb_mrp_attribute_begin(struct avb_mrp_attribute *attr, uint64_t now);
+void avb_mrp_attribute_join(struct avb_mrp_attribute *attr, uint64_t now, bool is_new);
+void avb_mrp_attribute_leave(struct avb_mrp_attribute *attr, uint64_t now);
+
+void avb_mrp_attribute_add_listener(struct avb_mrp_attribute *attr, struct spa_hook *listener,
+		const struct avb_mrp_attribute_events *events, void *data);
+
+struct avb_mrp_parse_info {
+#define AVB_VERSION_MRP_PARSE_INFO	0
+	uint32_t version;
+
+	bool (*check_header) (void *data, const void *hdr, size_t *hdr_size, bool *has_params);
+
+	int (*attr_event) (void *data, uint64_t now, uint8_t attribute_type, uint8_t event);
+
+	int (*process) (void *data, uint64_t now, uint8_t attribute_type, const void *value,
+			uint8_t event, uint8_t param, int index);
+};
+
+int avb_mrp_parse_packet(struct avb_mrp *mrp, uint64_t now, const void *pkt, int size,
+		const struct avb_mrp_parse_info *cb, void *data);
+
+struct avb_mrp_events {
+#define AVB_VERSION_MRP_EVENTS	0
+	uint32_t version;
+
+	void (*event) (void *data, uint64_t now, uint8_t event);
+
+	void (*notify) (void *data, uint64_t now, struct avb_mrp_attribute *attr, uint8_t notify);
+};
+
+struct avb_mrp *avb_mrp_new(struct server *server);
+void avb_mrp_destroy(struct avb_mrp *mrp);
+
+void avb_mrp_add_listener(struct avb_mrp *mrp, struct spa_hook *listener,
+		const struct avb_mrp_events *events, void *data);
+
+#endif /* AVB_MRP_H */
diff --git a/src/modules/module-avb/msrp.c b/src/modules/module-avb/msrp.c
new file mode 100644
index 0000000000000000000000000000000000000000..85d3ff9be7aa9e8d3826166c0ada503d277063af
--- /dev/null
+++ b/src/modules/module-avb/msrp.c
@@ -0,0 +1,459 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <unistd.h>
+
+#include <spa/debug/mem.h>
+
+#include <pipewire/pipewire.h>
+
+#include "utils.h"
+#include "msrp.h"
+
+static const uint8_t msrp_mac[6] = AVB_MSRP_MAC;
+
+struct attr {
+	struct avb_msrp_attribute attr;
+	struct msrp *msrp;
+	struct spa_hook listener;
+	struct spa_list link;
+};
+
+struct msrp {
+	struct server *server;
+	struct spa_hook server_listener;
+	struct spa_hook mrp_listener;
+
+	struct spa_source *source;
+
+	struct spa_list attributes;
+};
+
+static void debug_msrp_talker_common(const struct avb_packet_msrp_talker *t)
+{
+	char buf[128];
+	pw_log_info(" stream-id: %s", avb_utils_format_id(buf, sizeof(buf), be64toh(t->stream_id)));
+	pw_log_info(" dest-addr: %s", avb_utils_format_addr(buf, sizeof(buf), t->dest_addr));
+	pw_log_info(" vlan-id:   %d", ntohs(t->vlan_id));
+	pw_log_info(" tspec-max-frame-size: %d", ntohs(t->tspec_max_frame_size));
+	pw_log_info(" tspec-max-interval-frames: %d", ntohs(t->tspec_max_interval_frames));
+	pw_log_info(" priority: %d", t->priority);
+	pw_log_info(" rank: %d", t->rank);
+	pw_log_info(" accumulated-latency: %d", ntohl(t->accumulated_latency));
+}
+
+static void debug_msrp_talker(const struct avb_packet_msrp_talker *t)
+{
+	pw_log_info("talker");
+	debug_msrp_talker_common(t);
+}
+
+static void notify_talker(struct msrp *msrp, uint64_t now, struct attr *attr, uint8_t notify)
+{
+	pw_log_info("> notify talker: %s", avb_mrp_notify_name(notify));
+	debug_msrp_talker(&attr->attr.attr.talker);
+}
+
+static int process_talker(struct msrp *msrp, uint64_t now, uint8_t attr_type,
+		const void *m, uint8_t event, uint8_t param, int num)
+{
+	const struct avb_packet_msrp_talker *t = m;
+	struct attr *a;
+	spa_list_for_each(a, &msrp->attributes, link)
+		if (a->attr.type == attr_type &&
+		    a->attr.attr.talker.stream_id == t->stream_id) {
+			a->attr.attr.talker = *t;
+			avb_mrp_attribute_rx_event(a->attr.mrp, now, event);
+		}
+	return 0;
+}
+static int encode_talker(struct msrp *msrp, struct attr *a, void *m)
+{
+	struct avb_packet_msrp_msg *msg = m;
+	struct avb_packet_mrp_vector *v;
+	struct avb_packet_msrp_talker *t;
+	struct avb_packet_mrp_footer *f;
+	uint8_t *ev;
+	size_t attr_list_length = sizeof(*v) + sizeof(*t) + sizeof(*f) + 1;
+
+	msg->attribute_type = AVB_MSRP_ATTRIBUTE_TYPE_TALKER_ADVERTISE;
+	msg->attribute_length = sizeof(*t);
+	msg->attribute_list_length = htons(attr_list_length);
+
+	v = (struct avb_packet_mrp_vector *)msg->attribute_list;
+	v->lva = 0;
+	AVB_MRP_VECTOR_SET_NUM_VALUES(v, 1);
+
+	t = (struct avb_packet_msrp_talker *)v->first_value;
+	*t = a->attr.attr.talker;
+
+	ev = SPA_PTROFF(t, sizeof(*t), uint8_t);
+	*ev = a->attr.mrp->pending_send * 6 * 6;
+
+	f = SPA_PTROFF(ev, sizeof(*ev), struct avb_packet_mrp_footer);
+	f->end_mark = 0;
+
+	return attr_list_length + sizeof(*msg);
+}
+
+
+static void debug_msrp_talker_fail(const struct avb_packet_msrp_talker_fail *t)
+{
+	char buf[128];
+	pw_log_info("talker fail");
+	debug_msrp_talker_common(&t->talker);
+	pw_log_info(" bridge-id: %s", avb_utils_format_id(buf, sizeof(buf), be64toh(t->bridge_id)));
+	pw_log_info(" failure-code: %d", t->failure_code);
+}
+
+static int process_talker_fail(struct msrp *msrp, uint64_t now, uint8_t attr_type,
+		const void *m, uint8_t event, uint8_t param, int num)
+{
+	const struct avb_packet_msrp_talker_fail *t = m;
+	struct attr *a;
+
+	debug_msrp_talker_fail(t);
+
+	spa_list_for_each(a, &msrp->attributes, link)
+		if (a->attr.type == attr_type &&
+		    a->attr.attr.talker_fail.talker.stream_id == t->talker.stream_id)
+			avb_mrp_attribute_rx_event(a->attr.mrp, now, event);
+	return 0;
+}
+
+static void debug_msrp_listener(const struct avb_packet_msrp_listener *l, uint8_t param)
+{
+	char buf[128];
+	pw_log_info("listener");
+	pw_log_info(" %s", avb_utils_format_id(buf, sizeof(buf), be64toh(l->stream_id)));
+	pw_log_info(" %d", param);
+}
+
+static void notify_listener(struct msrp *msrp, uint64_t now, struct attr *attr, uint8_t notify)
+{
+	pw_log_info("> notify listener: %s", avb_mrp_notify_name(notify));
+	debug_msrp_listener(&attr->attr.attr.listener, attr->attr.param);
+}
+
+static int process_listener(struct msrp *msrp, uint64_t now, uint8_t attr_type,
+		const void *m, uint8_t event, uint8_t param, int num)
+{
+	const struct avb_packet_msrp_listener *l = m;
+	struct attr *a;
+	spa_list_for_each(a, &msrp->attributes, link)
+		if (a->attr.type == attr_type &&
+		    a->attr.attr.listener.stream_id == l->stream_id)
+			avb_mrp_attribute_rx_event(a->attr.mrp, now, event);
+	return 0;
+}
+static int encode_listener(struct msrp *msrp, struct attr *a, void *m)
+{
+	struct avb_packet_msrp_msg *msg = m;
+	struct avb_packet_mrp_vector *v;
+	struct avb_packet_msrp_listener *l;
+	struct avb_packet_mrp_footer *f;
+	uint8_t *ev;
+	size_t attr_list_length = sizeof(*v) + sizeof(*l) + sizeof(*f) + 1 + 1;
+
+	msg->attribute_type = AVB_MSRP_ATTRIBUTE_TYPE_LISTENER;
+	msg->attribute_length = sizeof(*l);
+	msg->attribute_list_length = htons(attr_list_length);
+
+	v = (struct avb_packet_mrp_vector *)msg->attribute_list;
+	v->lva = 0;
+	AVB_MRP_VECTOR_SET_NUM_VALUES(v, 1);
+
+	l = (struct avb_packet_msrp_listener *)v->first_value;
+	*l = a->attr.attr.listener;
+
+	ev = SPA_PTROFF(l, sizeof(*l), uint8_t);
+	*ev = a->attr.mrp->pending_send * 6 * 6;
+
+	ev = SPA_PTROFF(ev, sizeof(*ev), uint8_t);
+	*ev = a->attr.param * 4 * 4 * 4;
+
+	f = SPA_PTROFF(ev, sizeof(*ev), struct avb_packet_mrp_footer);
+	f->end_mark = 0;
+
+	return attr_list_length + sizeof(*msg);
+}
+
+static void debug_msrp_domain(const struct avb_packet_msrp_domain *d)
+{
+	pw_log_info("domain");
+	pw_log_info(" id: %d", d->sr_class_id);
+	pw_log_info(" prio: %d", d->sr_class_priority);
+	pw_log_info(" vid: %d", ntohs(d->sr_class_vid));
+}
+
+static void notify_domain(struct msrp *msrp, uint64_t now, struct attr *attr, uint8_t notify)
+{
+	pw_log_info("> notify domain: %s", avb_mrp_notify_name(notify));
+	debug_msrp_domain(&attr->attr.attr.domain);
+}
+
+static int process_domain(struct msrp *msrp, uint64_t now, uint8_t attr_type,
+		const void *m, uint8_t event, uint8_t param, int num)
+{
+	struct attr *a;
+	spa_list_for_each(a, &msrp->attributes, link)
+		if (a->attr.type == attr_type)
+			avb_mrp_attribute_rx_event(a->attr.mrp, now, event);
+	return 0;
+}
+
+static int encode_domain(struct msrp *msrp, struct attr *a, void *m)
+{
+	struct avb_packet_msrp_msg *msg = m;
+	struct avb_packet_mrp_vector *v;
+	struct avb_packet_msrp_domain *d;
+	struct avb_packet_mrp_footer *f;
+	uint8_t *ev;
+	size_t attr_list_length = sizeof(*v) + sizeof(*d) + sizeof(*f) + 1;
+
+	msg->attribute_type = AVB_MSRP_ATTRIBUTE_TYPE_DOMAIN;
+	msg->attribute_length = sizeof(*d);
+	msg->attribute_list_length = htons(attr_list_length);
+
+	v = (struct avb_packet_mrp_vector *)msg->attribute_list;
+	v->lva = 0;
+	AVB_MRP_VECTOR_SET_NUM_VALUES(v, 1);
+
+	d = (struct avb_packet_msrp_domain *)v->first_value;
+	*d = a->attr.attr.domain;
+
+	ev = SPA_PTROFF(d, sizeof(*d), uint8_t);
+	*ev = a->attr.mrp->pending_send * 36;
+
+	f = SPA_PTROFF(ev, sizeof(*ev), struct avb_packet_mrp_footer);
+	f->end_mark = 0;
+
+	return attr_list_length + sizeof(*msg);
+}
+
+static const struct {
+	const char *name;
+	int (*process) (struct msrp *msrp, uint64_t now, uint8_t attr_type,
+			const void *m, uint8_t event, uint8_t param, int num);
+	int (*encode) (struct msrp *msrp, struct attr *attr, void *m);
+	void (*notify) (struct msrp *msrp, uint64_t now, struct attr *attr, uint8_t notify);
+} dispatch[] = {
+	[AVB_MSRP_ATTRIBUTE_TYPE_TALKER_ADVERTISE] = { "talker", process_talker, encode_talker, notify_talker, },
+	[AVB_MSRP_ATTRIBUTE_TYPE_TALKER_FAILED] = { "talker-fail", process_talker_fail, NULL, NULL },
+	[AVB_MSRP_ATTRIBUTE_TYPE_LISTENER] = { "listener", process_listener, encode_listener, notify_listener },
+	[AVB_MSRP_ATTRIBUTE_TYPE_DOMAIN] = { "domain", process_domain, encode_domain, notify_domain, },
+};
+
+static bool msrp_check_header(void *data, const void *hdr, size_t *hdr_size, bool *has_params)
+{
+	const struct avb_packet_msrp_msg *msg = hdr;
+	uint8_t attr_type = msg->attribute_type;
+
+	if (!AVB_MSRP_ATTRIBUTE_TYPE_VALID(attr_type))
+		return false;
+
+	*hdr_size = sizeof(*msg);
+	*has_params = attr_type == AVB_MSRP_ATTRIBUTE_TYPE_LISTENER;
+	return true;
+}
+
+static int msrp_attr_event(void *data, uint64_t now, uint8_t attribute_type, uint8_t event)
+{
+	struct msrp *msrp = data;
+	struct attr *a;
+	spa_list_for_each(a, &msrp->attributes, link)
+		if (a->attr.type == attribute_type)
+			avb_mrp_attribute_update_state(a->attr.mrp, now, event);
+	return 0;
+}
+
+static int msrp_process(void *data, uint64_t now, uint8_t attribute_type, const void *value,
+			uint8_t event, uint8_t param, int index)
+{
+	struct msrp *msrp = data;
+	return dispatch[attribute_type].process(msrp, now,
+				attribute_type, value, event, param, index);
+}
+
+static const struct avb_mrp_parse_info info = {
+	AVB_VERSION_MRP_PARSE_INFO,
+	.check_header = msrp_check_header,
+	.attr_event = msrp_attr_event,
+	.process = msrp_process,
+};
+
+
+static int msrp_message(struct msrp *msrp, uint64_t now, const void *message, int len)
+{
+	return avb_mrp_parse_packet(msrp->server->mrp,
+			now, message, len, &info, msrp);
+}
+static void on_socket_data(void *data, int fd, uint32_t mask)
+{
+	struct msrp *msrp = data;
+	struct timespec now;
+
+	if (mask & SPA_IO_IN) {
+		int len;
+		uint8_t buffer[2048];
+
+		len = recv(fd, buffer, sizeof(buffer), 0);
+
+		if (len < 0) {
+			pw_log_warn("got recv error: %m");
+		}
+		else if (len < (int)sizeof(struct avb_packet_header)) {
+			pw_log_warn("short packet received (%d < %d)", len,
+					(int)sizeof(struct avb_packet_header));
+		} else {
+			clock_gettime(CLOCK_REALTIME, &now);
+			msrp_message(msrp, SPA_TIMESPEC_TO_NSEC(&now), buffer, len);
+		}
+	}
+}
+
+static void msrp_destroy(void *data)
+{
+	struct msrp *msrp = data;
+	spa_hook_remove(&msrp->server_listener);
+	pw_loop_destroy_source(msrp->server->impl->loop, msrp->source);
+	free(msrp);
+}
+
+static const struct server_events server_events = {
+	AVB_VERSION_SERVER_EVENTS,
+	.destroy = msrp_destroy,
+};
+
+static void msrp_notify(void *data, uint64_t now, uint8_t notify)
+{
+	struct attr *a = data;
+	struct msrp *msrp = a->msrp;
+	return dispatch[a->attr.type].notify(msrp, now, a, notify);
+}
+
+static const struct avb_mrp_attribute_events mrp_attr_events = {
+	AVB_VERSION_MRP_ATTRIBUTE_EVENTS,
+	.notify = msrp_notify,
+};
+
+struct avb_msrp_attribute *avb_msrp_attribute_new(struct avb_msrp *m,
+		uint8_t type)
+{
+	struct msrp *msrp = (struct msrp*)m;
+	struct avb_mrp_attribute *attr;
+	struct attr *a;
+
+	attr = avb_mrp_attribute_new(msrp->server->mrp, sizeof(struct attr));
+
+	a = attr->user_data;
+	a->msrp = msrp;
+	a->attr.mrp = attr;
+	a->attr.type = type;
+	spa_list_append(&msrp->attributes, &a->link);
+	avb_mrp_attribute_add_listener(attr, &a->listener, &mrp_attr_events, a);
+
+	return &a->attr;
+}
+
+static void msrp_event(void *data, uint64_t now, uint8_t event)
+{
+	struct msrp *msrp = data;
+	uint8_t buffer[2048];
+	struct avb_packet_mrp *p = (struct avb_packet_mrp*)buffer;
+	struct avb_packet_mrp_footer *f;
+	void *msg = SPA_PTROFF(buffer, sizeof(*p), void);
+	struct attr *a;
+	int len, count = 0;
+	size_t total = sizeof(*p) + 2;
+
+	p->version = AVB_MRP_PROTOCOL_VERSION;
+
+	spa_list_for_each(a, &msrp->attributes, link) {
+		if (!a->attr.mrp->pending_send)
+			continue;
+		if (dispatch[a->attr.type].encode == NULL)
+			continue;
+
+		pw_log_debug("send %s %s", dispatch[a->attr.type].name,
+				avb_mrp_send_name(a->attr.mrp->pending_send));
+
+		len = dispatch[a->attr.type].encode(msrp, a, msg);
+		if (len < 0)
+			break;
+
+		count++;
+		msg = SPA_PTROFF(msg, len, void);
+		total += len;
+	}
+	f = (struct avb_packet_mrp_footer *)msg;
+	f->end_mark = 0;
+
+	if (count > 0)
+		avb_server_send_packet(msrp->server, msrp_mac, AVB_MSRP_ETH,
+				buffer, total);
+}
+
+static const struct avb_mrp_events mrp_events = {
+	AVB_VERSION_MRP_EVENTS,
+	.event = msrp_event,
+};
+
+struct avb_msrp *avb_msrp_register(struct server *server)
+{
+	struct msrp *msrp;
+	int fd, res;
+
+	fd = avb_server_make_socket(server, AVB_MSRP_ETH, msrp_mac);
+	if (fd < 0) {
+		errno = -fd;
+		return NULL;
+	}
+	msrp = calloc(1, sizeof(*msrp));
+	if (msrp == NULL) {
+		res = -errno;
+		goto error_close;
+	}
+
+	msrp->server = server;
+	spa_list_init(&msrp->attributes);
+
+	msrp->source = pw_loop_add_io(server->impl->loop, fd, SPA_IO_IN, true, on_socket_data, msrp);
+	if (msrp->source == NULL) {
+		res = -errno;
+		pw_log_error("msrp %p: can't create msrp source: %m", msrp);
+		goto error_no_source;
+	}
+	avdecc_server_add_listener(server, &msrp->server_listener, &server_events, msrp);
+	avb_mrp_add_listener(server->mrp, &msrp->mrp_listener, &mrp_events, msrp);
+
+	return (struct avb_msrp*)msrp;
+
+error_no_source:
+	free(msrp);
+error_close:
+	close(fd);
+	errno = -res;
+	return NULL;
+}
diff --git a/src/modules/module-avb/msrp.h b/src/modules/module-avb/msrp.h
new file mode 100644
index 0000000000000000000000000000000000000000..0922e6bf9499168e48cbff6568a1a5a4f2a2b78a
--- /dev/null
+++ b/src/modules/module-avb/msrp.h
@@ -0,0 +1,134 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef AVB_MSRP_H
+#define AVB_MSRP_H
+
+#include "internal.h"
+#include "mrp.h"
+
+#define AVB_MSRP_ETH 0x22ea
+#define AVB_MSRP_MAC { 0x01, 0x80, 0xc2, 0x00, 0x00, 0xe };
+
+#define AVB_MSRP_ATTRIBUTE_TYPE_TALKER_ADVERTISE	1
+#define AVB_MSRP_ATTRIBUTE_TYPE_TALKER_FAILED		2
+#define AVB_MSRP_ATTRIBUTE_TYPE_LISTENER		3
+#define AVB_MSRP_ATTRIBUTE_TYPE_DOMAIN			4
+#define AVB_MSRP_ATTRIBUTE_TYPE_VALID(t)		((t)>=1 && (t)<=4)
+
+struct avb_packet_msrp_msg {
+	uint8_t attribute_type;
+	uint8_t attribute_length;
+	uint16_t attribute_list_length;
+	uint8_t attribute_list[0];
+} __attribute__ ((__packed__));
+
+#define AVB_MSRP_TSPEC_MAX_INTERVAL_FRAMES_DEFAULT	1
+#define AVB_MSRP_RANK_DEFAULT				1
+#define AVB_MSRP_PRIORITY_DEFAULT			3
+
+struct avb_packet_msrp_talker {
+	uint64_t stream_id;
+	uint8_t dest_addr[6];
+	uint16_t vlan_id;
+	uint16_t tspec_max_frame_size;
+	uint16_t tspec_max_interval_frames;
+#if __BYTE_ORDER == __BIG_ENDIAN
+	unsigned priority:3;
+	unsigned rank:1;
+	unsigned reserved:4;
+#elif __BYTE_ORDER == __LITTLE_ENDIAN
+	unsigned reserved:4;
+	unsigned rank:1;
+	unsigned priority:3;
+#endif
+	uint32_t accumulated_latency;
+} __attribute__ ((__packed__));
+
+/* failure codes */
+#define AVB_MRP_FAIL_BANDWIDTH			1
+#define AVB_MRP_FAIL_BRIDGE			2
+#define AVB_MRP_FAIL_TC_BANDWIDTH		3
+#define AVB_MRP_FAIL_ID_BUSY			4
+#define AVB_MRP_FAIL_DSTADDR_BUSY		5
+#define AVB_MRP_FAIL_PREEMPTED			6
+#define AVB_MRP_FAIL_LATENCY_CHNG		7
+#define AVB_MRP_FAIL_PORT_NOT_AVB		8
+#define AVB_MRP_FAIL_DSTADDR_FULL		9
+#define AVB_MRP_FAIL_AVB_MRP_RESOURCE		10
+#define AVB_MRP_FAIL_MMRP_RESOURCE		11
+#define AVB_MRP_FAIL_DSTADDR_FAIL		12
+#define AVB_MRP_FAIL_PRIO_NOT_SR		13
+#define AVB_MRP_FAIL_FRAME_SIZE			14
+#define AVB_MRP_FAIL_FANIN_EXCEED		15
+#define AVB_MRP_FAIL_STREAM_CHANGE		16
+#define AVB_MRP_FAIL_VLAN_BLOCKED		17
+#define AVB_MRP_FAIL_VLAN_DISABLED		18
+#define AVB_MRP_FAIL_SR_PRIO_ERR		19
+
+struct avb_packet_msrp_talker_fail {
+	struct avb_packet_msrp_talker talker;
+	uint64_t bridge_id;
+	uint8_t failure_code;
+} __attribute__ ((__packed__));
+
+struct avb_packet_msrp_listener {
+	uint64_t stream_id;
+} __attribute__ ((__packed__));
+
+/* domain discovery */
+#define AVB_MSRP_CLASS_ID_DEFAULT	6
+#define AVB_DEFAULT_VLAN		2
+
+struct avb_packet_msrp_domain {
+	uint8_t sr_class_id;
+	uint8_t sr_class_priority;
+	uint16_t sr_class_vid;
+} __attribute__ ((__packed__));
+
+#define AVB_MSRP_LISTENER_PARAM_IGNORE		0
+#define AVB_MSRP_LISTENER_PARAM_ASKING_FAILED	1
+#define AVB_MSRP_LISTENER_PARAM_READY		2
+#define AVB_MSRP_LISTENER_PARAM_READY_FAILED	3
+
+struct avb_msrp_attribute {
+	struct avb_mrp_attribute *mrp;
+	uint8_t type;
+	uint8_t param;
+	union {
+		struct avb_packet_msrp_talker talker;
+		struct avb_packet_msrp_talker_fail talker_fail;
+		struct avb_packet_msrp_listener listener;
+		struct avb_packet_msrp_domain domain;
+	} attr;
+};
+
+struct avb_msrp;
+
+struct avb_msrp_attribute *avb_msrp_attribute_new(struct avb_msrp *msrp,
+		uint8_t type);
+
+struct avb_msrp *avb_msrp_register(struct server *server);
+
+#endif /* AVB_MSRP_H */
diff --git a/src/modules/module-avb/mvrp.c b/src/modules/module-avb/mvrp.c
new file mode 100644
index 0000000000000000000000000000000000000000..2f5f6eaa7a216ee28dea62daad7951397b52db4c
--- /dev/null
+++ b/src/modules/module-avb/mvrp.c
@@ -0,0 +1,297 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <unistd.h>
+
+#include <pipewire/pipewire.h>
+
+#include "mvrp.h"
+
+static const uint8_t mvrp_mac[6] = AVB_MVRP_MAC;
+
+struct attr {
+	struct avb_mvrp_attribute attr;
+	struct spa_hook listener;
+	struct spa_list link;
+	struct mvrp *mvrp;
+};
+
+struct mvrp {
+	struct server *server;
+	struct spa_hook server_listener;
+	struct spa_hook mrp_listener;
+
+	struct spa_source *source;
+
+	struct spa_list attributes;
+};
+
+static bool mvrp_check_header(void *data, const void *hdr, size_t *hdr_size, bool *has_params)
+{
+	const struct avb_packet_mvrp_msg *msg = hdr;
+	uint8_t attr_type = msg->attribute_type;
+
+	if (!AVB_MVRP_ATTRIBUTE_TYPE_VALID(attr_type))
+		return false;
+
+	*hdr_size = sizeof(*msg);
+	*has_params = false;
+	return true;
+}
+
+static int mvrp_attr_event(void *data, uint64_t now, uint8_t attribute_type, uint8_t event)
+{
+	struct mvrp *mvrp = data;
+	struct attr *a;
+	spa_list_for_each(a, &mvrp->attributes, link)
+		if (a->attr.type == attribute_type)
+			avb_mrp_attribute_rx_event(a->attr.mrp, now, event);
+	return 0;
+}
+
+static void debug_vid(const struct avb_packet_mvrp_vid *t)
+{
+	pw_log_info("vid");
+	pw_log_info(" %d", ntohs(t->vlan));
+}
+
+static int process_vid(struct mvrp *mvrp, uint64_t now, uint8_t attr_type,
+		const void *m, uint8_t event, uint8_t param, int num)
+{
+	return mvrp_attr_event(mvrp, now, attr_type, event);
+}
+
+static int encode_vid(struct mvrp *mvrp, struct attr *a, void *m)
+{
+	struct avb_packet_mvrp_msg *msg = m;
+	struct avb_packet_mrp_vector *v;
+	struct avb_packet_mvrp_vid *d;
+	struct avb_packet_mrp_footer *f;
+	uint8_t *ev;
+	size_t attr_list_length = sizeof(*v) + sizeof(*d) + sizeof(*f) + 1;
+
+	msg->attribute_type = AVB_MVRP_ATTRIBUTE_TYPE_VID;
+	msg->attribute_length = sizeof(*d);
+
+	v = (struct avb_packet_mrp_vector *)msg->attribute_list;
+	v->lva = 0;
+	AVB_MRP_VECTOR_SET_NUM_VALUES(v, 1);
+
+	d = (struct avb_packet_mvrp_vid *)v->first_value;
+	*d = a->attr.attr.vid;
+
+	ev = SPA_PTROFF(d, sizeof(*d), uint8_t);
+	*ev = a->attr.mrp->pending_send * 36;
+
+	f = SPA_PTROFF(ev, sizeof(*ev), struct avb_packet_mrp_footer);
+	f->end_mark = 0;
+
+	return attr_list_length + sizeof(*msg);
+}
+
+static void notify_vid(struct mvrp *mvrp, uint64_t now, struct attr *attr, uint8_t notify)
+{
+	pw_log_info("> notify vid: %s", avb_mrp_notify_name(notify));
+	debug_vid(&attr->attr.attr.vid);
+}
+
+static const struct {
+	const char *name;
+	int (*process) (struct mvrp *mvrp, uint64_t now, uint8_t attr_type,
+			const void *m, uint8_t event, uint8_t param, int num);
+	int (*encode) (struct mvrp *mvrp, struct attr *attr, void *m);
+	void (*notify) (struct mvrp *mvrp, uint64_t now, struct attr *attr, uint8_t notify);
+} dispatch[] = {
+	[AVB_MVRP_ATTRIBUTE_TYPE_VID] = { "vid", process_vid, encode_vid, notify_vid },
+};
+
+static int mvrp_process(void *data, uint64_t now, uint8_t attribute_type, const void *value,
+			uint8_t event, uint8_t param, int index)
+{
+	struct mvrp *mvrp = data;
+	return dispatch[attribute_type].process(mvrp, now,
+				attribute_type, value, event, param, index);
+}
+
+static const struct avb_mrp_parse_info info = {
+	AVB_VERSION_MRP_PARSE_INFO,
+	.check_header = mvrp_check_header,
+	.attr_event = mvrp_attr_event,
+	.process = mvrp_process,
+};
+
+static int mvrp_message(struct mvrp *mvrp, uint64_t now, const void *message, int len)
+{
+	pw_log_debug("MVRP");
+	return avb_mrp_parse_packet(mvrp->server->mrp,
+			now, message, len, &info, mvrp);
+}
+
+static void on_socket_data(void *data, int fd, uint32_t mask)
+{
+	struct mvrp *mvrp = data;
+	struct timespec now;
+
+	if (mask & SPA_IO_IN) {
+		int len;
+		uint8_t buffer[2048];
+
+		len = recv(fd, buffer, sizeof(buffer), 0);
+
+		if (len < 0) {
+			pw_log_warn("got recv error: %m");
+		}
+		else if (len < (int)sizeof(struct avb_packet_header)) {
+			pw_log_warn("short packet received (%d < %d)", len,
+					(int)sizeof(struct avb_packet_header));
+		} else {
+			clock_gettime(CLOCK_REALTIME, &now);
+			mvrp_message(mvrp, SPA_TIMESPEC_TO_NSEC(&now), buffer, len);
+		}
+	}
+}
+
+static void mvrp_destroy(void *data)
+{
+	struct mvrp *mvrp = data;
+	spa_hook_remove(&mvrp->server_listener);
+	pw_loop_destroy_source(mvrp->server->impl->loop, mvrp->source);
+	free(mvrp);
+}
+
+static const struct server_events server_events = {
+	AVB_VERSION_SERVER_EVENTS,
+	.destroy = mvrp_destroy,
+};
+
+static void mvrp_notify(void *data, uint64_t now, uint8_t notify)
+{
+	struct attr *a = data;
+	struct mvrp *mvrp = a->mvrp;
+	return dispatch[a->attr.type].notify(mvrp, now, a, notify);
+}
+
+static const struct avb_mrp_attribute_events mrp_attr_events = {
+	AVB_VERSION_MRP_ATTRIBUTE_EVENTS,
+	.notify = mvrp_notify,
+};
+
+struct avb_mvrp_attribute *avb_mvrp_attribute_new(struct avb_mvrp *m,
+		uint8_t type)
+{
+	struct mvrp *mvrp = (struct mvrp*)m;
+	struct avb_mrp_attribute *attr;
+	struct attr *a;
+
+	attr = avb_mrp_attribute_new(mvrp->server->mrp, sizeof(struct attr));
+
+	a = attr->user_data;
+	a->attr.mrp = attr;
+	a->attr.type = type;
+	spa_list_append(&mvrp->attributes, &a->link);
+	avb_mrp_attribute_add_listener(attr, &a->listener, &mrp_attr_events, a);
+
+	return &a->attr;
+}
+
+static void mvrp_event(void *data, uint64_t now, uint8_t event)
+{
+	struct mvrp *mvrp = data;
+	uint8_t buffer[2048];
+	struct avb_packet_mrp *p = (struct avb_packet_mrp*)buffer;
+	struct avb_packet_mrp_footer *f;
+	void *msg = SPA_PTROFF(buffer, sizeof(*p), void);
+	struct attr *a;
+	int len, count = 0;
+	size_t total = sizeof(*p) + 2;
+
+	p->version = AVB_MRP_PROTOCOL_VERSION;
+
+	spa_list_for_each(a, &mvrp->attributes, link) {
+		if (!a->attr.mrp->pending_send)
+			continue;
+		if (dispatch[a->attr.type].encode == NULL)
+			continue;
+
+		pw_log_debug("send %s %s", dispatch[a->attr.type].name,
+				avb_mrp_send_name(a->attr.mrp->pending_send));
+
+		len = dispatch[a->attr.type].encode(mvrp, a, msg);
+		if (len < 0)
+			break;
+
+		count++;
+		msg = SPA_PTROFF(msg, len, void);
+		total += len;
+	}
+	f = (struct avb_packet_mrp_footer *)msg;
+	f->end_mark = 0;
+
+	if (count > 0)
+		avb_server_send_packet(mvrp->server, mvrp_mac, AVB_MVRP_ETH,
+				buffer, total);
+}
+
+static const struct avb_mrp_events mrp_events = {
+	AVB_VERSION_MRP_EVENTS,
+	.event = mvrp_event,
+};
+
+struct avb_mvrp *avb_mvrp_register(struct server *server)
+{
+	struct mvrp *mvrp;
+	int fd, res;
+
+	fd = avb_server_make_socket(server, AVB_MVRP_ETH, mvrp_mac);
+	if (fd < 0) {
+		errno = -fd;
+		return NULL;
+	}
+	mvrp = calloc(1, sizeof(*mvrp));
+	if (mvrp == NULL) {
+		res = -errno;
+		goto error_close;
+	}
+
+	mvrp->server = server;
+	spa_list_init(&mvrp->attributes);
+
+	mvrp->source = pw_loop_add_io(server->impl->loop, fd, SPA_IO_IN, true, on_socket_data, mvrp);
+	if (mvrp->source == NULL) {
+		res = -errno;
+		pw_log_error("mvrp %p: can't create mvrp source: %m", mvrp);
+		goto error_no_source;
+	}
+	avdecc_server_add_listener(server, &mvrp->server_listener, &server_events, mvrp);
+	avb_mrp_add_listener(server->mrp, &mvrp->mrp_listener, &mrp_events, mvrp);
+
+	return (struct avb_mvrp*)mvrp;
+
+error_no_source:
+	free(mvrp);
+error_close:
+	close(fd);
+	errno = -res;
+	return NULL;
+}
diff --git a/src/modules/module-avb/mvrp.h b/src/modules/module-avb/mvrp.h
new file mode 100644
index 0000000000000000000000000000000000000000..da3d5dcf82fa025c037df0544030d7b729a29614
--- /dev/null
+++ b/src/modules/module-avb/mvrp.h
@@ -0,0 +1,62 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef AVB_MVRP_H
+#define AVB_MVRP_H
+
+#include "mrp.h"
+#include "internal.h"
+
+#define AVB_MVRP_ETH 0x88f5
+#define AVB_MVRP_MAC { 0x01, 0x80, 0xc2, 0x00, 0x00, 0x21 };
+
+struct avb_packet_mvrp_msg {
+	uint8_t attribute_type;
+	uint8_t attribute_length;
+	uint8_t attribute_list[0];
+} __attribute__ ((__packed__));
+
+#define AVB_MVRP_ATTRIBUTE_TYPE_VID			1
+#define AVB_MVRP_ATTRIBUTE_TYPE_VALID(t)		((t)==1)
+
+struct avb_packet_mvrp_vid {
+	uint16_t vlan;
+} __attribute__ ((__packed__));
+
+struct avb_mvrp;
+
+struct avb_mvrp_attribute {
+	struct avb_mrp_attribute *mrp;
+	uint8_t type;
+	union {
+		struct avb_packet_mvrp_vid vid;
+	} attr;
+};
+
+struct avb_mvrp_attribute *avb_mvrp_attribute_new(struct avb_mvrp *mvrp,
+		uint8_t type);
+
+struct avb_mvrp *avb_mvrp_register(struct server *server);
+
+#endif /* AVB_MVRP_H */
diff --git a/src/modules/module-avb/packets.h b/src/modules/module-avb/packets.h
new file mode 100644
index 0000000000000000000000000000000000000000..f35738a433c14818922a3d75db2cad0c10542762
--- /dev/null
+++ b/src/modules/module-avb/packets.h
@@ -0,0 +1,101 @@
+/* Spa AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef AVB_PACKETS_H
+#define AVB_PACKETS_H
+
+#include <arpa/inet.h>
+
+#define AVB_SUBTYPE_61883_IIDC		0x00
+#define AVB_SUBTYPE_MMA_STREAM		0x01
+#define AVB_SUBTYPE_AAF			0x02
+#define AVB_SUBTYPE_CVF			0x03
+#define AVB_SUBTYPE_CRF			0x04
+#define AVB_SUBTYPE_TSCF		0x05
+#define AVB_SUBTYPE_SVF			0x06
+#define AVB_SUBTYPE_RVF			0x07
+#define AVB_SUBTYPE_AEF_CONTINUOUS	0x6E
+#define AVB_SUBTYPE_VSF_STREAM		0x6F
+#define AVB_SUBTYPE_EF_STREAM		0x7F
+#define AVB_SUBTYPE_NTSCF		0x82
+#define AVB_SUBTYPE_ESCF		0xEC
+#define AVB_SUBTYPE_EECF		0xED
+#define AVB_SUBTYPE_AEF_DISCRETE	0xEE
+#define AVB_SUBTYPE_ADP			0xFA
+#define AVB_SUBTYPE_AECP		0xFB
+#define AVB_SUBTYPE_ACMP		0xFC
+#define AVB_SUBTYPE_MAAP		0xFE
+#define AVB_SUBTYPE_EF_CONTROL		0xFF
+
+struct avb_ethernet_header {
+	uint8_t dest[6];
+	uint8_t src[6];
+	uint16_t type;
+} __attribute__ ((__packed__));
+
+struct avb_frame_header {
+	uint8_t dest[6];
+	uint8_t src[6];
+	uint16_t type;		/* 802.1Q Virtual Lan 0x8100 */
+	uint16_t prio_cfi_id;
+	uint16_t etype;
+} __attribute__ ((__packed__));
+
+struct avb_packet_header {
+	uint8_t subtype;
+#if __BYTE_ORDER == __BIG_ENDIAN
+	unsigned sv:1;			/* stream_id valid */
+	unsigned version:3;
+	unsigned subtype_data1:4;
+
+	unsigned subtype_data2:5;
+	unsigned len1:3;
+#elif __BYTE_ORDER == __LITTLE_ENDIAN
+	unsigned subtype_data1:4;
+	unsigned version:3;
+	unsigned sv:1;
+
+	unsigned len1:3;
+	unsigned subtype_data2:5;
+#elif
+#error "Unknown byte order"
+#endif
+	uint8_t len2:8;
+} __attribute__ ((__packed__));
+
+#define AVB_PACKET_SET_SUBTYPE(p,v)	((p)->subtype = (v))
+#define AVB_PACKET_SET_SV(p,v)		((p)->sv = (v))
+#define AVB_PACKET_SET_VERSION(p,v)	((p)->version = (v))
+#define AVB_PACKET_SET_SUB1(p,v)	((p)->subtype_data1 = (v))
+#define AVB_PACKET_SET_SUB2(p,v)	((p)->subtype_data2 = (v))
+#define AVB_PACKET_SET_LENGTH(p,v)	((p)->len1 = ((v) >> 8),(p)->len2 = (v))
+
+#define AVB_PACKET_GET_SUBTYPE(p)	((p)->subtype)
+#define AVB_PACKET_GET_SV(p)		((p)->sv)
+#define AVB_PACKET_GET_VERSION(p)	((p)->version)
+#define AVB_PACKET_GET_SUB1(p)		((p)->subtype_data1)
+#define AVB_PACKET_GET_SUB2(p)		((p)->subtype_data2)
+#define AVB_PACKET_GET_LENGTH(p)	((p)->len1 << 8 | (p)->len2)
+
+#endif /* AVB_PACKETS_H */
diff --git a/src/modules/module-avb/srp.c b/src/modules/module-avb/srp.c
new file mode 100644
index 0000000000000000000000000000000000000000..89d75f12a98f202cbb0d772ded9a3954888c9ffd
--- /dev/null
+++ b/src/modules/module-avb/srp.c
@@ -0,0 +1,59 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <pipewire/pipewire.h>
+
+#include "srp.h"
+
+struct srp {
+	struct server *server;
+	struct spa_hook server_listener;
+};
+
+static void srp_destroy(void *data)
+{
+	struct srp *srp = data;
+	spa_hook_remove(&srp->server_listener);
+	free(srp);
+}
+
+static const struct server_events server_events = {
+	AVB_VERSION_SERVER_EVENTS,
+	.destroy = srp_destroy,
+};
+
+int avb_srp_register(struct server *server)
+{
+	struct srp *srp;
+
+	srp = calloc(1, sizeof(*srp));
+	if (srp == NULL)
+		return -errno;
+
+	srp->server = server;
+
+	avdecc_server_add_listener(server, &srp->server_listener, &server_events, srp);
+
+	return 0;
+}
diff --git a/src/modules/module-avb/srp.h b/src/modules/module-avb/srp.h
new file mode 100644
index 0000000000000000000000000000000000000000..853321fb366fd27d983e0ad32a5c753453f97063
--- /dev/null
+++ b/src/modules/module-avb/srp.h
@@ -0,0 +1,32 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef AVB_SRP_H
+#define AVB_SRP_H
+
+#include "internal.h"
+
+int avb_srp_register(struct server *server);
+
+#endif /* AVB_SRP_H */
diff --git a/src/modules/module-avb/stream.c b/src/modules/module-avb/stream.c
new file mode 100644
index 0000000000000000000000000000000000000000..b86f0814c36452907bd1df197b74a4bdd799852f
--- /dev/null
+++ b/src/modules/module-avb/stream.c
@@ -0,0 +1,589 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <unistd.h>
+#include <linux/if_ether.h>
+#include <linux/if_packet.h>
+#include <linux/net_tstamp.h>
+#include <net/if.h>
+#include <sys/ioctl.h>
+
+#include <spa/debug/mem.h>
+#include <spa/pod/builder.h>
+#include <spa/param/audio/format-utils.h>
+
+#include "iec61883.h"
+#include "stream.h"
+#include "utils.h"
+#include "aecp-aem-descriptors.h"
+
+static void on_stream_destroy(void *d)
+{
+	struct stream *stream = d;
+	spa_hook_remove(&stream->stream_listener);
+	stream->stream = NULL;
+}
+
+static void on_source_stream_process(void *data)
+{
+	struct stream *stream = data;
+	struct pw_buffer *buf;
+	struct spa_data *d;
+	uint32_t index, n_bytes;
+	int32_t avail, wanted;
+
+	if ((buf = pw_stream_dequeue_buffer(stream->stream)) == NULL) {
+		pw_log_debug("out of buffers: %m");
+		return;
+	}
+
+	d = buf->buffer->datas;
+
+	wanted = buf->requested ? buf->requested * stream->stride : d[0].maxsize;
+
+	n_bytes = SPA_MIN(d[0].maxsize, (uint32_t)wanted);
+
+	avail = spa_ringbuffer_get_read_index(&stream->ring, &index);
+
+	if (avail < wanted) {
+		pw_log_debug("capture underrun %d < %d", avail, wanted);
+		memset(d[0].data, 0, n_bytes);
+	} else {
+		spa_ringbuffer_read_data(&stream->ring,
+				stream->buffer_data,
+				stream->buffer_size,
+				index % stream->buffer_size,
+				d[0].data, n_bytes);
+		index += n_bytes;
+		spa_ringbuffer_read_update(&stream->ring, index);
+	}
+
+	d[0].chunk->size = n_bytes;
+	d[0].chunk->stride = stream->stride;
+	d[0].chunk->offset = 0;
+	buf->size = n_bytes / stream->stride;
+
+	pw_stream_queue_buffer(stream->stream, buf);
+}
+
+static const struct pw_stream_events source_stream_events = {
+	PW_VERSION_STREAM_EVENTS,
+	.destroy = on_stream_destroy,
+	.process = on_source_stream_process
+};
+
+static inline void
+set_iovec(struct spa_ringbuffer *rbuf, void *buffer, uint32_t size,
+		uint32_t offset, struct iovec *iov, uint32_t len)
+{
+	iov[0].iov_len = SPA_MIN(len, size - offset);
+	iov[0].iov_base = SPA_PTROFF(buffer, offset, void);
+	iov[1].iov_len = len - iov[0].iov_len;
+	iov[1].iov_base = buffer;
+}
+
+static int flush_write(struct stream *stream, uint64_t current_time)
+{
+	int32_t avail;
+	uint32_t index;
+        uint64_t ptime, txtime;
+	int pdu_count;
+	ssize_t n;
+	struct avb_frame_header *h = (void*)stream->pdu;
+	struct avb_packet_iec61883 *p = SPA_PTROFF(h, sizeof(*h), void);
+	uint8_t dbc;
+
+	avail = spa_ringbuffer_get_read_index(&stream->ring, &index);
+
+	pdu_count = (avail / stream->stride) / stream->frames_per_pdu;
+
+	txtime = current_time + stream->t_uncertainty;
+	ptime = txtime + stream->mtt;
+	dbc = stream->dbc;
+
+	while (pdu_count--) {
+		*(uint64_t*)CMSG_DATA(stream->cmsg) = txtime;
+
+		set_iovec(&stream->ring,
+			stream->buffer_data,
+			stream->buffer_size,
+			index % stream->buffer_size,
+			&stream->iov[1], stream->payload_size);
+
+		p->seq_num = stream->pdu_seq++;
+		p->tv = 1;
+		p->timestamp = ptime;
+		p->dbc = dbc;
+
+		n = sendmsg(stream->source->fd, &stream->msg, 0);
+		if (n < 0 || n != (ssize_t)stream->pdu_size) {
+			pw_log_error("sendmsg() failed %zd != %zd: %m",
+					n, stream->pdu_size);
+		}
+		txtime += stream->pdu_period;
+		ptime += stream->pdu_period;
+		index += stream->payload_size;
+		dbc += stream->frames_per_pdu;
+	}
+	stream->dbc = dbc;
+	spa_ringbuffer_read_update(&stream->ring, index);
+	return 0;
+}
+
+static void on_sink_stream_process(void *data)
+{
+	struct stream *stream = data;
+	struct pw_buffer *buf;
+	struct spa_data *d;
+	int32_t filled;
+	uint32_t index, offs, avail, size;
+	struct timespec now;
+
+	if ((buf = pw_stream_dequeue_buffer(stream->stream)) == NULL) {
+		pw_log_debug("out of buffers: %m");
+		return;
+	}
+
+	d = buf->buffer->datas;
+
+	offs = SPA_MIN(d[0].chunk->offset, d[0].maxsize);
+	size = SPA_MIN(d[0].chunk->size, d[0].maxsize - offs);
+	avail = size - offs;
+
+	filled = spa_ringbuffer_get_write_index(&stream->ring, &index);
+
+	if (filled >= (int32_t)stream->buffer_size) {
+		pw_log_warn("playback overrun %d >= %zd", filled, stream->buffer_size);
+	} else {
+		spa_ringbuffer_write_data(&stream->ring,
+				stream->buffer_data,
+				stream->buffer_size,
+				index % stream->buffer_size,
+				SPA_PTROFF(d[0].data, offs, void), avail);
+		index += avail;
+		spa_ringbuffer_write_update(&stream->ring, index);
+	}
+	pw_stream_queue_buffer(stream->stream, buf);
+
+	clock_gettime(CLOCK_TAI, &now);
+	flush_write(stream, SPA_TIMESPEC_TO_NSEC(&now));
+}
+
+static void setup_pdu(struct stream *stream)
+{
+	struct avb_frame_header *h;
+	struct avb_packet_iec61883 *p;
+	ssize_t payload_size, hdr_size, pdu_size;
+
+	spa_memzero(stream->pdu, sizeof(stream->pdu));
+	h = (struct avb_frame_header*)stream->pdu;
+	p = SPA_PTROFF(h, sizeof(*h), void);
+
+	hdr_size = sizeof(*h) + sizeof(*p);
+	payload_size = stream->stride * stream->frames_per_pdu;
+	pdu_size = hdr_size + payload_size;
+
+	h->type = htons(0x8100);
+	h->prio_cfi_id = htons((stream->prio << 13) | stream->vlan_id);
+	h->etype = htons(0x22f0);
+
+	if (stream->direction == SPA_DIRECTION_OUTPUT) {
+		p->subtype = AVB_SUBTYPE_61883_IIDC;
+		p->sv = 1;
+		p->stream_id = htobe64(stream->id);
+		p->data_len = htons(payload_size+8);
+		p->tag = 0x1;
+		p->channel = 0x1f;
+		p->tcode = 0xa;
+		p->sid = 0x3f;
+		p->dbs = stream->info.info.raw.channels;
+		p->qi2 = 0x2;
+		p->format_id = 0x10;
+		p->fdf = 0x2;
+		p->syt = htons(0x0008);
+	}
+	stream->hdr_size = hdr_size;
+	stream->payload_size = payload_size;
+	stream->pdu_size = pdu_size;
+}
+
+static int setup_msg(struct stream *stream)
+{
+	stream->iov[0].iov_base = stream->pdu;
+	stream->iov[0].iov_len = stream->hdr_size;
+	stream->iov[1].iov_base = SPA_PTROFF(stream->pdu, stream->hdr_size, void);
+	stream->iov[1].iov_len = stream->payload_size;
+	stream->iov[2].iov_base = SPA_PTROFF(stream->pdu, stream->hdr_size, void);
+	stream->iov[2].iov_len = 0;
+	stream->msg.msg_name = &stream->sock_addr;
+	stream->msg.msg_namelen = sizeof(stream->sock_addr);
+	stream->msg.msg_iov = stream->iov;
+	stream->msg.msg_iovlen = 3;
+	stream->msg.msg_control = stream->control;
+	stream->msg.msg_controllen = sizeof(stream->control);
+	stream->cmsg = CMSG_FIRSTHDR(&stream->msg);
+	stream->cmsg->cmsg_level = SOL_SOCKET;
+	stream->cmsg->cmsg_type = SCM_TXTIME;
+	stream->cmsg->cmsg_len = CMSG_LEN(sizeof(__u64));
+	return 0;
+}
+
+static const struct pw_stream_events sink_stream_events = {
+	PW_VERSION_STREAM_EVENTS,
+	.destroy = on_stream_destroy,
+	.process = on_sink_stream_process
+};
+
+struct stream *server_create_stream(struct server *server,
+		enum spa_direction direction, uint16_t index)
+{
+	struct stream *stream;
+	const struct descriptor *desc;
+	uint32_t n_params;
+	const struct spa_pod *params[1];
+	uint8_t buffer[1024];
+	struct spa_pod_builder b;
+	int res;
+
+	desc = server_find_descriptor(server,
+			direction == SPA_DIRECTION_INPUT ?
+			AVB_AEM_DESC_STREAM_INPUT :
+			AVB_AEM_DESC_STREAM_OUTPUT, index);
+	if (desc == NULL)
+		return NULL;
+
+	stream = calloc(1, sizeof(*stream));
+	if (stream == NULL)
+		return NULL;
+
+	stream->server = server;
+	stream->direction = direction;
+	stream->index = index;
+	stream->desc = desc;
+	spa_list_append(&server->streams, &stream->link);
+
+	stream->prio = AVB_MSRP_PRIORITY_DEFAULT;
+	stream->vlan_id = AVB_DEFAULT_VLAN;
+
+	stream->id = (uint64_t)server->mac_addr[0] << 56 |
+			(uint64_t)server->mac_addr[1] << 48 |
+			(uint64_t)server->mac_addr[2] << 40 |
+			(uint64_t)server->mac_addr[3] << 32 |
+			(uint64_t)server->mac_addr[4] << 24 |
+			(uint64_t)server->mac_addr[5] << 16 |
+			htons(index);
+
+	stream->vlan_attr = avb_mvrp_attribute_new(server->mvrp,
+			AVB_MVRP_ATTRIBUTE_TYPE_VID);
+	stream->vlan_attr->attr.vid.vlan = htons(stream->vlan_id);
+
+	stream->buffer_data = calloc(1, BUFFER_SIZE);
+	stream->buffer_size = BUFFER_SIZE;
+	spa_ringbuffer_init(&stream->ring);
+
+	if (direction == SPA_DIRECTION_INPUT) {
+		stream->stream = pw_stream_new(server->impl->core, "source",
+			pw_properties_new(
+				PW_KEY_MEDIA_CLASS, "Audio/Source",
+				PW_KEY_NODE_NAME, "avb.source",
+				PW_KEY_NODE_DESCRIPTION, "AVB Source",
+				PW_KEY_NODE_WANT_DRIVER, "true",
+				NULL));
+	} else {
+		stream->stream = pw_stream_new(server->impl->core, "sink",
+			pw_properties_new(
+				PW_KEY_MEDIA_CLASS, "Audio/Sink",
+				PW_KEY_NODE_NAME, "avb.sink",
+				PW_KEY_NODE_DESCRIPTION, "AVB Sink",
+				PW_KEY_NODE_WANT_DRIVER, "true",
+				NULL));
+	}
+	if (stream->stream == NULL)
+		goto error_free;
+
+	pw_stream_add_listener(stream->stream,
+			&stream->stream_listener,
+			direction == SPA_DIRECTION_INPUT ?
+				&source_stream_events :
+				&sink_stream_events,
+			stream);
+
+	stream->info.info.raw.format = SPA_AUDIO_FORMAT_S24_32_BE;
+	stream->info.info.raw.flags = SPA_AUDIO_FLAG_UNPOSITIONED;
+	stream->info.info.raw.rate = 48000;
+	stream->info.info.raw.channels = 8;
+	stream->stride = stream->info.info.raw.channels * 4;
+
+	n_params = 0;
+	spa_pod_builder_init(&b, buffer, sizeof(buffer));
+	params[n_params++] = spa_format_audio_raw_build(&b,
+			SPA_PARAM_EnumFormat, &stream->info.info.raw);
+
+	if ((res = pw_stream_connect(stream->stream,
+			pw_direction_reverse(direction),
+			PW_ID_ANY,
+			PW_STREAM_FLAG_MAP_BUFFERS |
+			PW_STREAM_FLAG_INACTIVE |
+			PW_STREAM_FLAG_RT_PROCESS,
+			params, n_params)) < 0)
+		goto error_free_stream;
+
+	stream->frames_per_pdu = 6;
+	stream->pdu_period = SPA_NSEC_PER_SEC * stream->frames_per_pdu /
+                          stream->info.info.raw.rate;
+
+	setup_pdu(stream);
+	setup_msg(stream);
+
+	stream->listener_attr = avb_msrp_attribute_new(server->msrp,
+			AVB_MSRP_ATTRIBUTE_TYPE_LISTENER);
+	stream->talker_attr = avb_msrp_attribute_new(server->msrp,
+			AVB_MSRP_ATTRIBUTE_TYPE_TALKER_ADVERTISE);
+	stream->talker_attr->attr.talker.vlan_id = htons(stream->vlan_id);
+	stream->talker_attr->attr.talker.tspec_max_frame_size = htons(32 + stream->frames_per_pdu * stream->stride);
+	stream->talker_attr->attr.talker.tspec_max_interval_frames =
+		htons(AVB_MSRP_TSPEC_MAX_INTERVAL_FRAMES_DEFAULT);
+	stream->talker_attr->attr.talker.priority = stream->prio;
+	stream->talker_attr->attr.talker.rank = AVB_MSRP_RANK_DEFAULT;
+	stream->talker_attr->attr.talker.accumulated_latency = htonl(95);
+
+	return stream;
+
+error_free_stream:
+	pw_stream_destroy(stream->stream);
+	errno = -res;
+error_free:
+	free(stream);
+	return NULL;
+}
+
+void stream_destroy(struct stream *stream)
+{
+	avb_mrp_attribute_destroy(stream->listener_attr->mrp);
+	spa_list_remove(&stream->link);
+	free(stream);
+}
+
+static int setup_socket(struct stream *stream)
+{
+	struct server *server = stream->server;
+	int fd, res;
+	char buf[128];
+	struct ifreq req;
+
+	fd = socket(AF_PACKET, SOCK_RAW | SOCK_NONBLOCK, htons(ETH_P_ALL));
+	if (fd < 0) {
+		pw_log_error("socket() failed: %m");
+		return -errno;
+	}
+
+	spa_zero(req);
+	snprintf(req.ifr_name, sizeof(req.ifr_name), "%s", server->ifname);
+	res = ioctl(fd, SIOCGIFINDEX, &req);
+	if (res < 0) {
+		pw_log_error("SIOCGIFINDEX %s failed: %m", server->ifname);
+		res = -errno;
+		goto error_close;
+	}
+
+	spa_zero(stream->sock_addr);
+	stream->sock_addr.sll_family = AF_PACKET;
+	stream->sock_addr.sll_protocol = htons(ETH_P_TSN);
+	stream->sock_addr.sll_ifindex = req.ifr_ifindex;
+
+	if (stream->direction == SPA_DIRECTION_OUTPUT) {
+		struct sock_txtime txtime_cfg;
+
+		res = setsockopt(fd, SOL_SOCKET, SO_PRIORITY, &stream->prio,
+				sizeof(stream->prio));
+		if (res < 0) {
+			pw_log_error("setsockopt(SO_PRIORITY %d) failed: %m", stream->prio);
+			res = -errno;
+			goto error_close;
+		}
+
+		txtime_cfg.clockid = CLOCK_TAI;
+		txtime_cfg.flags = 0;
+		res = setsockopt(fd, SOL_SOCKET, SO_TXTIME, &txtime_cfg,
+				sizeof(txtime_cfg));
+		if (res < 0) {
+			pw_log_error("setsockopt(SO_TXTIME) failed: %m");
+			res = -errno;
+			goto error_close;
+		}
+	} else {
+		struct packet_mreq mreq;
+
+		res = bind(fd, (struct sockaddr *) &stream->sock_addr, sizeof(stream->sock_addr));
+		if (res < 0) {
+			pw_log_error("bind() failed: %m");
+			res = -errno;
+			goto error_close;
+		}
+
+		spa_zero(mreq);
+		mreq.mr_ifindex = req.ifr_ifindex;
+		mreq.mr_type = PACKET_MR_MULTICAST;
+		mreq.mr_alen = ETH_ALEN;
+		memcpy(&mreq.mr_address, stream->addr, ETH_ALEN);
+		res = setsockopt(fd, SOL_PACKET, PACKET_ADD_MEMBERSHIP,
+				&mreq, sizeof(struct packet_mreq));
+
+		pw_log_info("join %s", avb_utils_format_addr(buf, 128, stream->addr));
+
+		if (res < 0) {
+			pw_log_error("setsockopt(ADD_MEMBERSHIP) failed: %m");
+			res = -errno;
+			goto error_close;
+		}
+	}
+	return fd;
+
+error_close:
+	close(fd);
+	return res;
+}
+
+static void handle_iec61883_packet(struct stream *stream,
+		struct avb_packet_iec61883 *p, int len)
+{
+	uint32_t index, n_bytes;
+	int32_t filled;
+
+	filled = spa_ringbuffer_get_write_index(&stream->ring, &index);
+	n_bytes = ntohs(p->data_len) - 8;
+
+	if (filled + n_bytes > stream->buffer_size) {
+		pw_log_debug("capture overrun");
+	} else {
+		spa_ringbuffer_write_data(&stream->ring,
+				stream->buffer_data,
+				stream->buffer_size,
+				index % stream->buffer_size,
+				p->payload, n_bytes);
+		index += n_bytes;
+		spa_ringbuffer_write_update(&stream->ring, index);
+	}
+}
+
+static void on_socket_data(void *data, int fd, uint32_t mask)
+{
+	struct stream *stream = data;
+
+	if (mask & SPA_IO_IN) {
+		int len;
+		uint8_t buffer[2048];
+
+		len = recv(fd, buffer, sizeof(buffer), 0);
+
+		if (len < 0) {
+			pw_log_warn("got recv error: %m");
+		}
+		else if (len < (int)sizeof(struct avb_packet_header)) {
+			pw_log_warn("short packet received (%d < %d)", len,
+					(int)sizeof(struct avb_packet_header));
+		} else {
+			struct avb_frame_header *h = (void*)buffer;
+			struct avb_packet_iec61883 *p = SPA_PTROFF(h, sizeof(*h), void);
+
+			if (memcmp(h->dest, stream->addr, 6) != 0 ||
+			    p->subtype != AVB_SUBTYPE_61883_IIDC)
+				return;
+
+			handle_iec61883_packet(stream, p, len - sizeof(*h));
+		}
+	}
+}
+
+int stream_activate(struct stream *stream, uint64_t now)
+{
+	struct server *server = stream->server;
+	struct avb_frame_header *h = (void*)stream->pdu;
+	int fd, res;
+
+	if (stream->source == NULL) {
+		if ((fd = setup_socket(stream)) < 0)
+			return fd;
+
+		stream->source = pw_loop_add_io(server->impl->loop, fd,
+				SPA_IO_IN, true, on_socket_data, stream);
+		if (stream->source == NULL) {
+			res = -errno;
+			pw_log_error("stream %p: can't create source: %m", stream);
+			close(fd);
+			return res;
+		}
+	}
+
+	avb_mrp_attribute_begin(stream->vlan_attr->mrp, now);
+	avb_mrp_attribute_join(stream->vlan_attr->mrp, now, true);
+
+	if (stream->direction == SPA_DIRECTION_INPUT) {
+		stream->listener_attr->attr.listener.stream_id = htobe64(stream->peer_id);
+		stream->listener_attr->param = AVB_MSRP_LISTENER_PARAM_READY;
+		avb_mrp_attribute_begin(stream->listener_attr->mrp, now);
+		avb_mrp_attribute_join(stream->listener_attr->mrp, now, true);
+
+		stream->talker_attr->attr.talker.stream_id = htobe64(stream->peer_id);
+		avb_mrp_attribute_begin(stream->talker_attr->mrp, now);
+	} else {
+		if ((res = avb_maap_get_address(server->maap, stream->addr, stream->index)) < 0)
+			return res;
+
+		stream->listener_attr->attr.listener.stream_id = htobe64(stream->id);
+		stream->listener_attr->param = AVB_MSRP_LISTENER_PARAM_IGNORE;
+		avb_mrp_attribute_begin(stream->listener_attr->mrp, now);
+
+		stream->talker_attr->attr.talker.stream_id = htobe64(stream->id);
+		memcpy(stream->talker_attr->attr.talker.dest_addr, stream->addr, 6);
+
+		stream->sock_addr.sll_halen = ETH_ALEN;
+		memcpy(&stream->sock_addr.sll_addr, stream->addr, ETH_ALEN);
+		memcpy(h->dest, stream->addr, 6);
+		memcpy(h->src, server->mac_addr, 6);
+		avb_mrp_attribute_begin(stream->talker_attr->mrp, now);
+		avb_mrp_attribute_join(stream->talker_attr->mrp, now, true);
+	}
+	pw_stream_set_active(stream->stream, true);
+	return 0;
+}
+
+int stream_deactivate(struct stream *stream, uint64_t now)
+{
+	pw_stream_set_active(stream->stream, false);
+
+	if (stream->source != NULL) {
+		pw_loop_destroy_source(stream->server->impl->loop, stream->source);
+		stream->source = NULL;
+	}
+
+	avb_mrp_attribute_leave(stream->vlan_attr->mrp, now);
+
+	if (stream->direction == SPA_DIRECTION_INPUT) {
+		avb_mrp_attribute_leave(stream->listener_attr->mrp, now);
+	} else {
+		avb_mrp_attribute_leave(stream->talker_attr->mrp, now);
+	}
+	return 0;
+}
diff --git a/src/modules/module-avb/stream.h b/src/modules/module-avb/stream.h
new file mode 100644
index 0000000000000000000000000000000000000000..7062e2552b1a21dacd0693862b02f712a1fa75da
--- /dev/null
+++ b/src/modules/module-avb/stream.h
@@ -0,0 +1,104 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef AVB_STREAM_H
+#define AVB_STREAM_H
+
+#include <sys/socket.h>
+#include <sys/types.h>
+#include <linux/if_packet.h>
+#include <net/if.h>
+
+#include <spa/utils/ringbuffer.h>
+#include <spa/param/audio/format.h>
+
+#include <pipewire/pipewire.h>
+
+#define BUFFER_SIZE	(1u<<16)
+#define BUFFER_MASK	(BUFFER_SIZE-1)
+
+struct stream {
+	struct spa_list link;
+
+	struct server *server;
+
+	uint16_t direction;
+	uint16_t index;
+	const struct descriptor *desc;
+	uint64_t id;
+	uint64_t peer_id;
+
+	struct pw_stream *stream;
+	struct spa_hook stream_listener;
+
+	uint8_t addr[6];
+	struct spa_source *source;
+	int prio;
+	int vlan_id;
+	int mtt;
+	int t_uncertainty;
+	uint32_t frames_per_pdu;
+	int ptime_tolerance;
+
+	uint8_t pdu[2048];
+	size_t hdr_size;
+	size_t payload_size;
+	size_t pdu_size;
+	int64_t pdu_period;
+	uint8_t pdu_seq;
+	uint8_t prev_seq;
+	uint8_t dbc;
+
+	struct iovec iov[3];
+	struct sockaddr_ll sock_addr;
+	struct msghdr msg;
+	char control[CMSG_SPACE(sizeof(uint64_t))];
+	struct cmsghdr *cmsg;
+
+	struct spa_ringbuffer ring;
+	void *buffer_data;
+	size_t buffer_size;
+
+	uint64_t format;
+	uint32_t stride;
+	struct spa_audio_info info;
+
+	struct avb_msrp_attribute *talker_attr;
+	struct avb_msrp_attribute *listener_attr;
+	struct avb_mvrp_attribute *vlan_attr;
+};
+
+#include "msrp.h"
+#include "mvrp.h"
+#include "maap.h"
+
+struct stream *server_create_stream(struct server *server,
+		enum spa_direction direction, uint16_t index);
+
+void stream_destroy(struct stream *stream);
+
+int stream_activate(struct stream *stream, uint64_t now);
+int stream_deactivate(struct stream *stream, uint64_t now);
+
+#endif /* AVB_STREAM_H */
diff --git a/src/modules/module-avb/utils.h b/src/modules/module-avb/utils.h
new file mode 100644
index 0000000000000000000000000000000000000000..f6267224b244ffe18e0f26187a119e06102038d2
--- /dev/null
+++ b/src/modules/module-avb/utils.h
@@ -0,0 +1,86 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef AVB_UTILS_H
+#define AVB_UTILS_H
+
+#include <spa/utils/json.h>
+
+#include "internal.h"
+
+static inline char *avb_utils_format_id(char *str, size_t size, const uint64_t id)
+{
+	snprintf(str, size, "%02x:%02x:%02x:%02x:%02x:%02x:%04x",
+			(uint8_t)(id >> 56),
+			(uint8_t)(id >> 48),
+			(uint8_t)(id >> 40),
+			(uint8_t)(id >> 32),
+			(uint8_t)(id >> 24),
+			(uint8_t)(id >> 16),
+			(uint16_t)(id));
+	return str;
+}
+
+static inline int avb_utils_parse_id(const char *str, int len, uint64_t *id)
+{
+	char s[64];
+	uint8_t v[6];
+	uint16_t unique_id;
+	if (spa_json_parse_stringn(str, len, s, sizeof(s)) <= 0)
+		return -EINVAL;
+	if (sscanf(s, "%hhx:%hhx:%hhx:%hhx:%hhx:%hhx:%hx",
+			&v[0], &v[1], &v[2], &v[3],
+			&v[4], &v[5], &unique_id) == 7) {
+		*id = (uint64_t) v[0] << 56 |
+			    (uint64_t) v[1] << 48 |
+			    (uint64_t) v[2] << 40 |
+			    (uint64_t) v[3] << 32 |
+			    (uint64_t) v[4] << 24 |
+			    (uint64_t) v[5] << 16 |
+			    unique_id;
+	} else if (!spa_atou64(str, id, 0))
+		return -EINVAL;
+	return 0;
+}
+
+static inline char *avb_utils_format_addr(char *str, size_t size, const uint8_t addr[6])
+{
+	snprintf(str, size, "%02x:%02x:%02x:%02x:%02x:%02x",
+			addr[0], addr[1], addr[2], addr[3], addr[4], addr[5]);
+	return str;
+}
+static inline int avb_utils_parse_addr(const char *str, int len, uint8_t addr[6])
+{
+	char s[64];
+	uint8_t v[6];
+	if (spa_json_parse_stringn(str, len, s, sizeof(s)) <= 0)
+		return -EINVAL;
+	if (sscanf(s, "%hhx:%hhx:%hhx:%hhx:%hhx:%hhx",
+			&v[0], &v[1], &v[2], &v[3], &v[4], &v[5]) != 6)
+		return -EINVAL;
+	memcpy(addr, v, 6);
+	return 0;
+}
+
+#endif /* AVB_UTILS_H */
diff --git a/src/modules/module-client-node/remote-node.c b/src/modules/module-client-node/remote-node.c
index c0af9a21ffa0174997770e57894de7c05d5ade9c..387711515ec4c16f00df59e92f01661b79b76327 100644
--- a/src/modules/module-client-node/remote-node.c
+++ b/src/modules/module-client-node/remote-node.c
@@ -857,14 +857,8 @@ static int link_signal_func(void *user_data)
 {
 	struct link *link = user_data;
 	struct spa_system *data_system = link->data->context->data_system;
-	struct timespec ts = { 0, 0 };
-
-	pw_log_trace_fp("link %p: signal", link);
-
-	spa_system_clock_gettime(data_system, CLOCK_MONOTONIC, &ts);
-	link->target.activation->status = PW_NODE_ACTIVATION_TRIGGERED;
-	link->target.activation->signal_time = SPA_TIMESPEC_TO_NSEC(&ts);
 
+	pw_log_trace_fp("link %p: signal %p", link, link->target.activation);
 	if (SPA_UNLIKELY(spa_system_eventfd_write(data_system, link->signalfd, 1) < 0))
 		pw_log_warn("link %p: write failed %m", link);
 
@@ -930,7 +924,7 @@ client_node_set_activation(void *_data,
 		link->map = mm;
 		link->target.activation = ptr;
 		link->signalfd = signalfd;
-		link->target.signal = link_signal_func;
+		link->target.signal_func = link_signal_func;
 		link->target.data = link;
 		link->target.node = NULL;
 		spa_list_append(&data->links, &link->link);
diff --git a/src/modules/module-echo-cancel.c b/src/modules/module-echo-cancel.c
index 8d1205a2a2e98633d3bd2040226e1f626f210375..5ac3cb6be336d48c8e8fe42cf7e952c7bb53824d 100644
--- a/src/modules/module-echo-cancel.c
+++ b/src/modules/module-echo-cancel.c
@@ -80,6 +80,7 @@
  *
  * - \ref PW_KEY_AUDIO_RATE
  * - \ref PW_KEY_AUDIO_CHANNELS
+ * - \ref SPA_KEY_AUDIO_POSITION
  * - \ref PW_KEY_MEDIA_CLASS
  * - \ref PW_KEY_NODE_LATENCY
  * - \ref PW_KEY_NODE_NAME
@@ -89,7 +90,6 @@
  * - \ref PW_KEY_NODE_VIRTUAL
  * - \ref PW_KEY_NODE_LATENCY
  * - \ref PW_KEY_REMOTE_NAME
- * - \ref SPA_KEY_AUDIO_POSITION
  *
  * ## Example configuration
  *\code{.unparsed}
@@ -125,6 +125,10 @@
 PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
 #define PW_LOG_TOPIC_DEFAULT mod_topic
 
+#define DEFAULT_RATE 48000
+#define DEFAULT_CHANNELS 2
+#define DEFAULT_POSITION "[ FL FR ]"
+
 /* Hopefully this is enough for any combination of AEC engine and resampler
  * input requirement for rate matching */
 #define MAX_BUFSIZE_MS 100
@@ -859,9 +863,15 @@ static void parse_audio_info(struct pw_properties *props, struct spa_audio_info_
 	*info = SPA_AUDIO_INFO_RAW_INIT(
 			.format = SPA_AUDIO_FORMAT_F32P);
 	info->rate = pw_properties_get_uint32(props, PW_KEY_AUDIO_RATE, info->rate);
+	if (info->rate == 0)
+		info->rate = DEFAULT_RATE;
+
 	info->channels = pw_properties_get_uint32(props, PW_KEY_AUDIO_CHANNELS, info->channels);
+	info->channels = SPA_MIN(info->channels, SPA_AUDIO_MAX_CHANNELS);
 	if ((str = pw_properties_get(props, SPA_KEY_AUDIO_POSITION)) != NULL)
 		parse_position(info, str, strlen(str));
+	if (info->channels == 0)
+		parse_position(info, DEFAULT_POSITION, strlen(DEFAULT_POSITION));
 }
 
 static void copy_props(struct impl *impl, struct pw_properties *props, const char *key)
@@ -927,14 +937,6 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
 
 	parse_audio_info(props, &impl->info);
 
-	if (impl->info.channels == 0) {
-		impl->info.channels = 2;
-		impl->info.position[0] = SPA_AUDIO_CHANNEL_FL;
-		impl->info.position[1] = SPA_AUDIO_CHANNEL_FR;
-	}
-	if (impl->info.rate == 0)
-		impl->info.rate = 48000;
-
 	if ((str = pw_properties_get(props, "source.props")) != NULL)
 		pw_properties_update_string(impl->source_props, str, strlen(str));
 	if ((str = pw_properties_get(props, "sink.props")) != NULL)
@@ -991,8 +993,6 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
 		goto error;
 	}
 
-	(void)SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_AUDIO_AEC, (struct spa_audio_aec *)impl->aec);
-
 	pw_log_info("Using plugin AEC %s", impl->aec->name);
 
 	if ((str = pw_properties_get(props, "aec.args")) != NULL)
diff --git a/src/modules/module-example-sink.c b/src/modules/module-example-sink.c
index 1d5face8701145a61f397700c0fb2acebe248411..5d8fe1c6e0bdf5113757214fc697093b4f71d958 100644
--- a/src/modules/module-example-sink.c
+++ b/src/modules/module-example-sink.c
@@ -64,6 +64,7 @@
  * Options with well-known behavior.
  *
  * - \ref PW_KEY_REMOTE_NAME
+ * - \ref PW_KEY_AUDIO_FORMAT
  * - \ref PW_KEY_AUDIO_RATE
  * - \ref PW_KEY_AUDIO_CHANNELS
  * - \ref SPA_KEY_AUDIO_POSITION
@@ -107,7 +108,7 @@ PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
 			"[ node.description=<description of the nodes> ] "			\
 			"[ audio.format=<format, default:"DEFAULT_FORMAT"> ] "			\
 			"[ audio.rate=<sample rate, default: "SPA_STRINGIFY(DEFAULT_RATE)"> ] "			\
-			"[ audio.channels=<number of channels, default:"SPA_STRINGIFY(EFAULT_CHANNELS) "> ] "	\
+			"[ audio.channels=<number of channels, default:"SPA_STRINGIFY(DEFAULT_CHANNELS) "> ] "	\
 			"[ audio.position=<channel map, default:"DEFAULT_POSITION"> ] "		\
 			"[ stream.props=<properties> ] "
 
@@ -340,56 +341,59 @@ static void parse_position(struct spa_audio_info_raw *info, const char *val, siz
 	}
 }
 
-static int parse_audio_info(struct impl *impl)
+static void parse_audio_info(const struct pw_properties *props, struct spa_audio_info_raw *info)
 {
-	struct pw_properties *props = impl->stream_props;
-	struct spa_audio_info_raw *info = &impl->info;
 	const char *str;
 
 	spa_zero(*info);
-
 	if ((str = pw_properties_get(props, PW_KEY_AUDIO_FORMAT)) == NULL)
 		str = DEFAULT_FORMAT;
 	info->format = format_from_name(str, strlen(str));
+
+	info->rate = pw_properties_get_uint32(props, PW_KEY_AUDIO_RATE, info->rate);
+	if (info->rate == 0)
+		info->rate = DEFAULT_RATE;
+
+	info->channels = pw_properties_get_uint32(props, PW_KEY_AUDIO_CHANNELS, info->channels);
+	info->channels = SPA_MIN(info->channels, SPA_AUDIO_MAX_CHANNELS);
+	if ((str = pw_properties_get(props, SPA_KEY_AUDIO_POSITION)) != NULL)
+		parse_position(info, str, strlen(str));
+	if (info->channels == 0)
+		parse_position(info, DEFAULT_POSITION, strlen(DEFAULT_POSITION));
+}
+
+static int calc_frame_size(const struct spa_audio_info_raw *info)
+{
+	int res = info->channels;
 	switch (info->format) {
-	case SPA_AUDIO_FORMAT_S8:
 	case SPA_AUDIO_FORMAT_U8:
-		impl->frame_size = 1;
-		break;
+	case SPA_AUDIO_FORMAT_S8:
+	case SPA_AUDIO_FORMAT_ALAW:
+	case SPA_AUDIO_FORMAT_ULAW:
+		return res;
 	case SPA_AUDIO_FORMAT_S16:
-		impl->frame_size = 2;
-		break;
+	case SPA_AUDIO_FORMAT_S16_OE:
+	case SPA_AUDIO_FORMAT_U16:
+		return res * 2;
 	case SPA_AUDIO_FORMAT_S24:
-		impl->frame_size = 3;
-		break;
+	case SPA_AUDIO_FORMAT_S24_OE:
+	case SPA_AUDIO_FORMAT_U24:
+		return res * 3;
 	case SPA_AUDIO_FORMAT_S24_32:
+	case SPA_AUDIO_FORMAT_S24_32_OE:
 	case SPA_AUDIO_FORMAT_S32:
+	case SPA_AUDIO_FORMAT_S32_OE:
+	case SPA_AUDIO_FORMAT_U32:
+	case SPA_AUDIO_FORMAT_U32_OE:
 	case SPA_AUDIO_FORMAT_F32:
-		impl->frame_size = 4;
-		break;
+	case SPA_AUDIO_FORMAT_F32_OE:
+		return res * 4;
 	case SPA_AUDIO_FORMAT_F64:
-		impl->frame_size = 8;
-		break;
+	case SPA_AUDIO_FORMAT_F64_OE:
+		return res * 8;
 	default:
-		pw_log_error("unsupported format '%s'", str);
-		return -EINVAL;
-	}
-	info->rate = pw_properties_get_uint32(props, PW_KEY_AUDIO_RATE, DEFAULT_RATE);
-	if (info->rate == 0) {
-		pw_log_error("invalid rate '%s'", str);
-		return -EINVAL;
+		return 0;
 	}
-	info->channels = pw_properties_get_uint32(props, PW_KEY_AUDIO_CHANNELS, DEFAULT_CHANNELS);
-	if ((str = pw_properties_get(props, SPA_KEY_AUDIO_POSITION)) == NULL)
-		str = DEFAULT_POSITION;
-	parse_position(info, str, strlen(str));
-	if (info->channels == 0) {
-		pw_log_error("invalid channels '%s'", str);
-		return -EINVAL;
-	}
-	impl->frame_size *= info->channels;
-
-	return 0;
 }
 
 static void copy_props(struct impl *impl, struct pw_properties *props, const char *key)
@@ -467,7 +471,11 @@ 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);
 
-	if ((res = parse_audio_info(impl)) < 0) {
+	parse_audio_info(impl->stream_props, &impl->info);
+
+	impl->frame_size = calc_frame_size(&impl->info);
+	if (impl->frame_size == 0) {
+		res = -EINVAL;
 		pw_log_error( "can't parse audio format");
 		goto error;
 	}
diff --git a/src/modules/module-example-source.c b/src/modules/module-example-source.c
index 2b480538f947c87abdccfa6036d91095986b35a0..ccf6f388b9a5cf531153f0815538376b7839382f 100644
--- a/src/modules/module-example-source.c
+++ b/src/modules/module-example-source.c
@@ -64,6 +64,7 @@
  * Options with well-known behavior.
  *
  * - \ref PW_KEY_REMOTE_NAME
+ * - \ref PW_KEY_AUDIO_FORMAT
  * - \ref PW_KEY_AUDIO_RATE
  * - \ref PW_KEY_AUDIO_CHANNELS
  * - \ref SPA_KEY_AUDIO_POSITION
@@ -107,7 +108,7 @@ PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
 			"[ node.description=<description of the nodes> ] "			\
 			"[ audio.format=<format, default:"DEFAULT_FORMAT"> ] "			\
 			"[ audio.rate=<sample rate, default: "SPA_STRINGIFY(DEFAULT_RATE)"> ] "			\
-			"[ audio.channels=<number of channels, default:"SPA_STRINGIFY(EFAULT_CHANNELS) "> ] "	\
+			"[ audio.channels=<number of channels, default:"SPA_STRINGIFY(DEFAULT_CHANNELS) "> ] "	\
 			"[ audio.position=<channel map, default:"DEFAULT_POSITION"> ] "		\
 			"[ stream.props=<properties> ] "
 
@@ -344,56 +345,59 @@ static void parse_position(struct spa_audio_info_raw *info, const char *val, siz
 	}
 }
 
-static int parse_audio_info(struct impl *impl)
+static void parse_audio_info(const struct pw_properties *props, struct spa_audio_info_raw *info)
 {
-	struct pw_properties *props = impl->stream_props;
-	struct spa_audio_info_raw *info = &impl->info;
 	const char *str;
 
 	spa_zero(*info);
-
 	if ((str = pw_properties_get(props, PW_KEY_AUDIO_FORMAT)) == NULL)
 		str = DEFAULT_FORMAT;
 	info->format = format_from_name(str, strlen(str));
+
+	info->rate = pw_properties_get_uint32(props, PW_KEY_AUDIO_RATE, info->rate);
+	if (info->rate == 0)
+		info->rate = DEFAULT_RATE;
+
+	info->channels = pw_properties_get_uint32(props, PW_KEY_AUDIO_CHANNELS, info->channels);
+	info->channels = SPA_MIN(info->channels, SPA_AUDIO_MAX_CHANNELS);
+	if ((str = pw_properties_get(props, SPA_KEY_AUDIO_POSITION)) != NULL)
+		parse_position(info, str, strlen(str));
+	if (info->channels == 0)
+		parse_position(info, DEFAULT_POSITION, strlen(DEFAULT_POSITION));
+}
+
+static int calc_frame_size(const struct spa_audio_info_raw *info)
+{
+	int res = info->channels;
 	switch (info->format) {
-	case SPA_AUDIO_FORMAT_S8:
 	case SPA_AUDIO_FORMAT_U8:
-		impl->frame_size = 1;
-		break;
+	case SPA_AUDIO_FORMAT_S8:
+	case SPA_AUDIO_FORMAT_ALAW:
+	case SPA_AUDIO_FORMAT_ULAW:
+		return res;
 	case SPA_AUDIO_FORMAT_S16:
-		impl->frame_size = 2;
-		break;
+	case SPA_AUDIO_FORMAT_S16_OE:
+	case SPA_AUDIO_FORMAT_U16:
+		return res * 2;
 	case SPA_AUDIO_FORMAT_S24:
-		impl->frame_size = 3;
-		break;
+	case SPA_AUDIO_FORMAT_S24_OE:
+	case SPA_AUDIO_FORMAT_U24:
+		return res * 3;
 	case SPA_AUDIO_FORMAT_S24_32:
+	case SPA_AUDIO_FORMAT_S24_32_OE:
 	case SPA_AUDIO_FORMAT_S32:
+	case SPA_AUDIO_FORMAT_S32_OE:
+	case SPA_AUDIO_FORMAT_U32:
+	case SPA_AUDIO_FORMAT_U32_OE:
 	case SPA_AUDIO_FORMAT_F32:
-		impl->frame_size = 4;
-		break;
+	case SPA_AUDIO_FORMAT_F32_OE:
+		return res * 4;
 	case SPA_AUDIO_FORMAT_F64:
-		impl->frame_size = 8;
-		break;
+	case SPA_AUDIO_FORMAT_F64_OE:
+		return res * 8;
 	default:
-		pw_log_error("unsupported format '%s'", str);
-		return -EINVAL;
-	}
-	info->rate = pw_properties_get_uint32(props, PW_KEY_AUDIO_RATE, DEFAULT_RATE);
-	if (info->rate == 0) {
-		pw_log_error("invalid rate '%s'", str);
-		return -EINVAL;
+		return 0;
 	}
-	info->channels = pw_properties_get_uint32(props, PW_KEY_AUDIO_CHANNELS, DEFAULT_CHANNELS);
-	if ((str = pw_properties_get(props, SPA_KEY_AUDIO_POSITION)) == NULL)
-		str = DEFAULT_POSITION;
-	parse_position(info, str, strlen(str));
-	if (info->channels == 0) {
-		pw_log_error("invalid channels '%s'", str);
-		return -EINVAL;
-	}
-	impl->frame_size *= info->channels;
-
-	return 0;
 }
 
 static void copy_props(struct impl *impl, struct pw_properties *props, const char *key)
@@ -471,7 +475,11 @@ 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);
 
-	if ((res = parse_audio_info(impl)) < 0) {
+	parse_audio_info(impl->stream_props, &impl->info);
+
+	impl->frame_size = calc_frame_size(&impl->info);
+	if (impl->frame_size == 0) {
+		res = -EINVAL;
 		pw_log_error( "can't parse audio format");
 		goto error;
 	}
diff --git a/src/modules/module-filter-chain.c b/src/modules/module-filter-chain.c
index 9e8b475b9eea7effe515cef8ac31a29a77b1b7aa..d63567407269d719dd2348b888640fe1ac085617 100644
--- a/src/modules/module-filter-chain.c
+++ b/src/modules/module-filter-chain.c
@@ -591,6 +591,8 @@ static void capture_process(void *d)
 	struct graph *graph = &impl->graph;
 	uint32_t i, outsize = 0, n_hndl = graph->n_hndl;
 	int32_t stride = 0;
+	struct graph_port *port;
+	struct spa_data *bd;
 
 	if ((in = pw_stream_dequeue_buffer(impl->capture)) == NULL)
 		pw_log_debug("out of capture buffers: %m");
@@ -602,30 +604,37 @@ static void capture_process(void *d)
 		goto done;
 
 	for (i = 0; i < in->buffer->n_datas; i++) {
-		struct spa_data *ds = &in->buffer->datas[i];
-		struct graph_port *port = &graph->input[i];
 		uint32_t offs, size;
 
-		offs = SPA_MIN(ds->chunk->offset, ds->maxsize);
-		size = SPA_MIN(ds->chunk->size, ds->maxsize - offs);
+		bd = &in->buffer->datas[i];
 
-		if (port->desc)
+		offs = SPA_MIN(bd->chunk->offset, bd->maxsize);
+		size = SPA_MIN(bd->chunk->size, bd->maxsize - offs);
+
+		port = i < graph->n_input ? &graph->input[i] : NULL;
+
+		if (port && port->desc)
 			port->desc->connect_port(port->hndl, port->port,
-				SPA_PTROFF(ds->data, offs, void));
+				SPA_PTROFF(bd->data, offs, void));
 
-		outsize = SPA_MAX(outsize, size);
-		stride = SPA_MAX(stride, ds->chunk->stride);
+		outsize = i == 0 ? size : SPA_MIN(outsize, size);
+		stride = SPA_MAX(stride, bd->chunk->stride);
 	}
 	for (i = 0; i < out->buffer->n_datas; i++) {
-		struct spa_data *dd = &out->buffer->datas[i];
-		struct graph_port *port = &graph->output[i];
-		if (port->desc)
-			port->desc->connect_port(port->hndl, port->port, dd->data);
+		bd = &out->buffer->datas[i];
+
+		outsize = SPA_MIN(outsize, bd->maxsize);
+
+		port = i < graph->n_output ? &graph->output[i] : NULL;
+
+		if (port && port->desc)
+			port->desc->connect_port(port->hndl, port->port, bd->data);
 		else
-			memset(dd->data, 0, outsize);
-		dd->chunk->offset = 0;
-		dd->chunk->size = outsize;
-		dd->chunk->stride = stride;
+			memset(bd->data, 0, outsize);
+
+		bd->chunk->offset = 0;
+		bd->chunk->size = outsize;
+		bd->chunk->stride = stride;
 	}
 	for (i = 0; i < n_hndl; i++) {
 		struct graph_hndl *hndl = &graph->hndl[i];
@@ -1690,15 +1699,29 @@ static int setup_graph(struct graph *graph, struct spa_json *inputs, struct spa_
 	 * graph n_hndl times when needed. */
 	n_hndl = impl->capture_info.channels / n_input;
 	if (n_hndl != impl->playback_info.channels / n_output) {
-		pw_log_error("invalid channels");
+		pw_log_error("invalid channels. The capture stream has %1$d channels and "
+				"the filter has %2$d inputs. The playback stream has %3$d channels "
+				"and the filter has %4$d outputs. capture:%1$d / input:%2$d != "
+				"playback:%3$d / output:%4$d. Check inputs and outputs objects.",
+				impl->capture_info.channels, n_input,
+				impl->playback_info.channels, n_output);
 		res = -EINVAL;
 		goto error;
 	}
 	if (n_hndl > MAX_HNDL) {
-		pw_log_error("too many channels");
+		pw_log_error("too many channels. %d > %d", n_hndl, MAX_HNDL);
 		res = -EINVAL;
 		goto error;
 	}
+	if (n_hndl == 0) {
+		n_hndl = 1;
+		pw_log_warn("The capture stream has %1$d channels and "
+				"the filter has %2$d inputs. The playback stream has %3$d channels "
+				"and the filter has %4$d outputs. Some filter ports will be "
+				"unconnected..",
+				impl->capture_info.channels, n_input,
+				impl->playback_info.channels, n_output);
+	}
 	pw_log_info("using %d instances %d %d", n_hndl, n_input, n_output);
 
 	/* now go over all nodes and create instances. */
@@ -2083,8 +2106,9 @@ static void parse_audio_info(struct pw_properties *props, struct spa_audio_info_
 
 	*info = SPA_AUDIO_INFO_RAW_INIT(
 			.format = SPA_AUDIO_FORMAT_F32P);
-	info->rate = pw_properties_get_int32(props, PW_KEY_AUDIO_RATE, 0);
-	info->channels = pw_properties_get_int32(props, PW_KEY_AUDIO_CHANNELS, 0);
+	info->rate = pw_properties_get_int32(props, PW_KEY_AUDIO_RATE, info->rate);
+	info->channels = pw_properties_get_int32(props, PW_KEY_AUDIO_CHANNELS, info->channels);
+	info->channels = SPA_MIN(info->channels, SPA_AUDIO_MAX_CHANNELS);
 	if ((str = pw_properties_get(props, SPA_KEY_AUDIO_POSITION)) != NULL)
 		parse_position(info, str, strlen(str));
 }
diff --git a/src/modules/module-filter-chain/convolver.c b/src/modules/module-filter-chain/convolver.c
index 075886583dcde92f676c1199da9f67574fd44e75..dcf54d03398bfd92ba3411660b3ff63169c25f36 100644
--- a/src/modules/module-filter-chain/convolver.c
+++ b/src/modules/module-filter-chain/convolver.c
@@ -232,9 +232,12 @@ static void convolver1_free(struct convolver1 *conv)
 		fft_cpx_free(&conv->segments[i]);
 		fft_cpx_free(&conv->segmentsIr[i]);
 	}
-	fft_destroy(conv->fft);
-	fft_destroy(conv->ifft);
-	fft_free(conv->fft_buffer);
+	if (conv->fft)
+		fft_destroy(conv->fft);
+	if (conv->ifft)
+		fft_destroy(conv->ifft);
+	if (conv->fft_buffer)
+		fft_free(conv->fft_buffer);
 	free(conv->segments);
 	free(conv->segmentsIr);
 	fft_cpx_free(&conv->pre_mult);
diff --git a/src/modules/module-filter-chain/ladspa_plugin.c b/src/modules/module-filter-chain/ladspa_plugin.c
index 76d80dc841b11940bee93bfadaad71d75ee6c731..591002bea33da82d339b58661775a807b33a8649 100644
--- a/src/modules/module-filter-chain/ladspa_plugin.c
+++ b/src/modules/module-filter-chain/ladspa_plugin.c
@@ -167,7 +167,7 @@ static const struct fc_descriptor *ladspa_make_desc(struct fc_plugin *plugin, co
 	desc->desc.free = ladspa_free;
 
 	desc->desc.name = d->Label;
-	desc->desc.flags = d->Properties;
+	desc->desc.flags = 0;
 
 	desc->desc.n_ports = d->PortCount;
 	desc->desc.ports = calloc(desc->desc.n_ports, sizeof(struct fc_port));
diff --git a/src/modules/module-filter-chain/lv2_plugin.c b/src/modules/module-filter-chain/lv2_plugin.c
index 55b59a5c40dd458f1719a91454cd88fdfa4dd6d5..9f655700792e284fac69b94b4b6a1ba0b3827c13 100644
--- a/src/modules/module-filter-chain/lv2_plugin.c
+++ b/src/modules/module-filter-chain/lv2_plugin.c
@@ -37,9 +37,9 @@
 #include <lilv/lilv.h>
 #include <lv2/lv2plug.in/ns/ext/atom/atom.h>
 #include <lv2/lv2plug.in/ns/ext/buf-size/buf-size.h>
-#include "lv2/lv2plug.in/ns/ext/worker/worker.h"
-#include "lv2/lv2plug.in/ns/ext/options/options.h"
-#include "lv2/lv2plug.in/ns/ext/parameters/parameters.h"
+#include <lv2/lv2plug.in/ns/ext/worker/worker.h>
+#include <lv2/lv2plug.in/ns/ext/options/options.h>
+#include <lv2/lv2plug.in/ns/ext/parameters/parameters.h>
 
 #include "plugin.h"
 
diff --git a/src/modules/module-loopback.c b/src/modules/module-loopback.c
index 543ee3d818b338496d098d5ab09be2a6a690bc8c..88462a61eb6499f21d710b03eb2bb08c147e58a3 100644
--- a/src/modules/module-loopback.c
+++ b/src/modules/module-loopback.c
@@ -99,7 +99,8 @@
  *         playback.props = {
  *             node.name = "playback.CM106_stereo_pair_2"
  *             audio.position = [ RL RR ]
- *             node.target = "alsa_output.usb-0d8c_USB_Sound_Device-00.analog-surround-71"
+ *             target.object = "alsa_output.usb-0d8c_USB_Sound_Device-00.analog-surround-71"
+ *             node.dont-reconnect = true
  *             stream.dont-remix = true
  *             node.passive = true
  *         }
@@ -188,33 +189,35 @@ static void capture_process(void *d)
 		pw_log_debug("out of playback buffers: %m");
 
 	if (in != NULL && out != NULL) {
+		uint32_t outsize = UINT32_MAX;
+		int32_t stride = 0;
+		struct spa_data *d;
+		const void *src[in->buffer->n_datas];
 
-		for (i = 0; i < out->buffer->n_datas; i++) {
-			struct spa_data *ds, *dd;
-			uint32_t outsize = 0;
-			int32_t stride = 0;
-
-			dd = &out->buffer->datas[i];
+		for (i = 0; i < in->buffer->n_datas; i++) {
+			uint32_t offs, size;
 
-			if (i < in->buffer->n_datas) {
-				uint32_t offs, size;
+			d = &in->buffer->datas[i];
+			offs = SPA_MIN(d->chunk->offset, d->maxsize);
+			size = SPA_MIN(d->chunk->size, d->maxsize - offs);
 
-				ds = &in->buffer->datas[i];
+			src[i] = SPA_PTROFF(d->data, offs, void);
+			outsize = SPA_MIN(outsize, size);
+			stride = SPA_MAX(stride, d->chunk->stride);
+		}
+		for (i = 0; i < out->buffer->n_datas; i++) {
+			d = &out->buffer->datas[i];
 
-				offs = SPA_MIN(ds->chunk->offset, ds->maxsize);
-				size = SPA_MIN(ds->chunk->size, ds->maxsize - offs);
-				stride = SPA_MAX(stride, stride);
+			outsize = SPA_MIN(outsize, d->maxsize);
 
-				memcpy(dd->data,
-					SPA_PTROFF(ds->data, offs, void), size);
+			if (i < in->buffer->n_datas)
+				memcpy(d->data, src[i], outsize);
+			else
+				memset(d->data, 0, outsize);
 
-				outsize = SPA_MAX(outsize, size);
-			} else {
-				memset(dd->data, 0, outsize);
-			}
-			dd->chunk->offset = 0;
-			dd->chunk->size = outsize;
-			dd->chunk->stride = stride;
+			d->chunk->offset = 0;
+			d->chunk->size = outsize;
+			d->chunk->stride = stride;
 		}
 	}
 
@@ -453,6 +456,7 @@ static void parse_audio_info(struct pw_properties *props, struct spa_audio_info_
 			.format = SPA_AUDIO_FORMAT_F32P);
 	info->rate = pw_properties_get_int32(props, PW_KEY_AUDIO_RATE, 0);
 	info->channels = pw_properties_get_uint32(props, PW_KEY_AUDIO_CHANNELS, 0);
+	info->channels = SPA_MIN(info->channels, SPA_AUDIO_MAX_CHANNELS);
 	if ((str = pw_properties_get(props, SPA_KEY_AUDIO_POSITION)) != NULL)
 		parse_position(info, str, strlen(str));
 }
diff --git a/src/modules/module-pipe-tunnel.c b/src/modules/module-pipe-tunnel.c
index 02d8d1b55c1ea41c114dc4d0ae535f47a2d53183..ff4a2596867ee45143a7ce217b715d8502379c0f 100644
--- a/src/modules/module-pipe-tunnel.c
+++ b/src/modules/module-pipe-tunnel.c
@@ -84,6 +84,7 @@
  * Options with well-known behavior.
  *
  * - \ref PW_KEY_REMOTE_NAME
+ * - \ref PW_KEY_AUDIO_FORMAT
  * - \ref PW_KEY_AUDIO_RATE
  * - \ref PW_KEY_AUDIO_CHANNELS
  * - \ref SPA_KEY_AUDIO_POSITION
@@ -93,7 +94,7 @@
  * - \ref PW_KEY_NODE_GROUP
  * - \ref PW_KEY_NODE_VIRTUAL
  * - \ref PW_KEY_MEDIA_CLASS
- * - \ref PW_KEY_NODE_TARGET to specify the remote name or id to link to
+ * - \ref PW_KEY_TARGET_OBJECT to specify the remote name or serial id to link to
  *
  * When not otherwise specified, the pipe will accept or produce a
  * 16 bits, stereo, 48KHz sample stream.
@@ -107,10 +108,11 @@
  *         tunnel.mode = playback
  *         # Set the pipe name to tunnel to
  *         pipe.filename = "/tmp/fifo_output"
+ *         #audio.format=<sample format>
  *         #audio.rate=<sample rate>
  *         #audio.channels=<number of channels>
  *         #audio.position=<channel map>
- *         #node.target=<remote target node>
+ *         #target.object=<remote target node>
  *         stream.props = {
  *             # extra sink properties
  *         }
@@ -125,6 +127,11 @@
 #define DEFAULT_CAPTURE_FILENAME 	"/tmp/fifo_input"
 #define DEFAULT_PLAYBACK_FILENAME	"/tmp/fifo_output"
 
+#define DEFAULT_FORMAT "S16"
+#define DEFAULT_RATE 48000
+#define DEFAULT_CHANNELS 2
+#define DEFAULT_POSITION "[ FL FR ]"
+
 PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
 #define PW_LOG_TOPIC_DEFAULT mod_topic
 
@@ -132,7 +139,8 @@ PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
 			"[ node.latency=<latency as fraction> ] "		\
 			"[ node.name=<name of the nodes> ] "			\
 			"[ node.description=<description of the nodes> ] "	\
-			"[ node.target=<remote node target name> ] "		\
+			"[ target.object=<remote node target name> ] "		\
+			"[ audio.format=<sample format> ] "			\
 			"[ audio.rate=<sample rate> ] "				\
 			"[ audio.channels=<number of channels> ] "		\
 			"[ audio.position=<channel map> ] "			\
@@ -377,7 +385,7 @@ static int create_fifo(struct impl *impl)
 		do_unlink_fifo = true;
 	}
 
-	if ((fd = open(filename, O_RDWR | O_CLOEXEC | O_NONBLOCK, 0)) <= 0) {
+	if ((fd = open(filename, O_RDWR | O_CLOEXEC | O_NONBLOCK, 0)) < 0) {
 		res = -errno;
 		pw_log_error("open('%s'): %s", filename, spa_strerror(res));
 		goto error;
@@ -408,7 +416,7 @@ static int create_fifo(struct impl *impl)
 error:
 	if (do_unlink_fifo)
 		unlink(filename);
-	if (fd > 0)
+	if (fd >= 0)
 		close(fd);
 	return res;
 }
@@ -453,7 +461,7 @@ static void impl_destroy(struct impl *impl)
 			unlink(impl->filename);
 		free(impl->filename);
 	}
-	if (impl->fd > 0)
+	if (impl->fd >= 0)
 		close(impl->fd);
 
 	pw_properties_free(impl->stream_props);
@@ -511,31 +519,28 @@ static inline uint32_t format_from_name(const char *name, size_t len)
 	return SPA_AUDIO_FORMAT_UNKNOWN;
 }
 
-static void parse_audio_info(struct pw_properties *props, struct spa_audio_info_raw *info)
+static void parse_audio_info(const struct pw_properties *props, struct spa_audio_info_raw *info)
 {
 	const char *str;
 
-	*info = SPA_AUDIO_INFO_RAW_INIT(
-			.rate = 48000,
-			.channels = 2,
-			.format = SPA_AUDIO_FORMAT_S16);
-
-	if ((str = pw_properties_get(props, PW_KEY_AUDIO_FORMAT)) != NULL) {
-		uint32_t id;
-
-		id = format_from_name(str, strlen(str));
-		if (id != SPA_AUDIO_FORMAT_UNKNOWN)
-			info->format = id;
-	}
+	spa_zero(*info);
+	if ((str = pw_properties_get(props, PW_KEY_AUDIO_FORMAT)) == NULL)
+		str = DEFAULT_FORMAT;
+	info->format = format_from_name(str, strlen(str));
 
 	info->rate = pw_properties_get_uint32(props, PW_KEY_AUDIO_RATE, info->rate);
+	if (info->rate == 0)
+		info->rate = DEFAULT_RATE;
+
 	info->channels = pw_properties_get_uint32(props, PW_KEY_AUDIO_CHANNELS, info->channels);
+	info->channels = SPA_MIN(info->channels, SPA_AUDIO_MAX_CHANNELS);
 	if ((str = pw_properties_get(props, SPA_KEY_AUDIO_POSITION)) != NULL)
 		parse_position(info, str, strlen(str));
-
+	if (info->channels == 0)
+		parse_position(info, DEFAULT_POSITION, strlen(DEFAULT_POSITION));
 }
 
-static int calc_frame_size(struct spa_audio_info_raw *info)
+static int calc_frame_size(const struct spa_audio_info_raw *info)
 {
 	int res = info->channels;
 	switch (info->format) {
@@ -662,13 +667,6 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
 
 	parse_audio_info(impl->stream_props, &impl->info);
 
-	if (impl->info.rate != 0 &&
-	    pw_properties_get(props, PW_KEY_NODE_RATE) == NULL)
-		pw_properties_setf(props, PW_KEY_NODE_RATE,
-				"1/%u", impl->info.rate),
-
-	copy_props(impl, props, PW_KEY_NODE_RATE);
-
 	impl->frame_size = calc_frame_size(&impl->info);
 	if (impl->frame_size == 0) {
 		pw_log_error("unsupported audio format:%d channels:%d",
@@ -676,6 +674,13 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
 		res = -EINVAL;
 		goto error;
 	}
+	if (impl->info.rate != 0 &&
+	    pw_properties_get(props, PW_KEY_NODE_RATE) == NULL)
+		pw_properties_setf(props, PW_KEY_NODE_RATE,
+				"1/%u", impl->info.rate),
+
+	copy_props(impl, props, PW_KEY_NODE_RATE);
+
 	impl->leftover = calloc(1, impl->frame_size);
 	if (impl->leftover == NULL) {
 		res = -errno;
diff --git a/src/modules/module-protocol-native.c b/src/modules/module-protocol-native.c
index 556ee14c03ea142baac1f091cbe0f774474c0dad..5ec880e80afdfdf84634d4405c74ca16f04cd980 100644
--- a/src/modules/module-protocol-native.c
+++ b/src/modules/module-protocol-native.c
@@ -38,7 +38,7 @@
 #ifdef HAVE_PWD_H
 #include <pwd.h>
 #endif
-#if defined(__FreeBSD__)
+#if defined(__FreeBSD__) || defined(__MidnightBSD__)
 #include <sys/ucred.h>
 #endif
 
@@ -103,6 +103,11 @@ PW_LOG_TOPIC(mod_topic_connection, "conn." NAME);
  * - XDG_RUNTIME_DIR
  * - USERPROFILE
  *
+ * The socket address will be written into the notification file descriptor
+ * if the following environment variable is set:
+ *
+ * - PIPEWIRE_NOTIFICATION_FD
+ *
  * When a client connect, the connection will be made to:
  *
  * - PIPEWIRE_REMOTE : the environment with the remote name
@@ -534,7 +539,7 @@ static struct client_data *client_new(struct server *s, int fd)
 	struct pw_impl_client *client;
 	struct pw_protocol *protocol = s->this.protocol;
 	socklen_t len;
-#if defined(__FreeBSD__)
+#if defined(__FreeBSD__) || defined(__MidnightBSD__)
 	struct xucred xucred;
 #else
 	struct ucred ucred;
@@ -583,7 +588,7 @@ static struct client_data *client_new(struct server *s, int fd)
 					(int)len, buffer);
 		}
 	}
-#elif defined(__FreeBSD__)
+#elif defined(__FreeBSD__) || defined(__MidnightBSD__)
 	len = sizeof(xucred);
 	if (getsockopt(fd, 0, LOCAL_PEERCRED, &xucred, &len) < 0) {
 		pw_log_warn("server %p: no peercred: %m", s);
@@ -605,12 +610,15 @@ static struct client_data *client_new(struct server *s, int fd)
 	if (client == NULL)
 		goto exit;
 
-
 	this = pw_impl_client_get_user_data(client);
 	spa_list_append(&s->this.client_list, &this->protocol_link);
 
 	this->server = s;
 	this->client = client;
+	pw_map_init(&this->compat_v2.types, 0, 32);
+
+	pw_impl_client_add_listener(client, &this->client_listener, &client_events, this);
+
 	this->source = pw_loop_add_io(pw_context_get_main_loop(context),
 				      fd, SPA_IO_ERR | SPA_IO_HUP, true,
 				      connection_data, this);
@@ -625,15 +633,11 @@ static struct client_data *client_new(struct server *s, int fd)
 		goto cleanup_client;
 	}
 
-	pw_map_init(&this->compat_v2.types, 0, 32);
-
 	pw_protocol_native_connection_add_listener(this->connection,
 						   &this->conn_listener,
 						   &server_conn_events,
 						   this);
 
-	pw_impl_client_add_listener(client, &this->client_listener, &client_events, this);
-
 	if ((res = pw_impl_client_register(client, NULL)) < 0)
 		goto cleanup_client;
 
@@ -761,6 +765,44 @@ socket_data(void *data, int fd, uint32_t mask)
 	}
 }
 
+static int write_socket_address(struct server *s)
+{
+	long v;
+	int fd, res = 0;
+	char *endptr;
+	const char *env = getenv("PIPEWIRE_NOTIFICATION_FD");
+
+	if (env == NULL || env[0] == '\0')
+		return 0;
+
+	errno = 0;
+	v = strtol(env, &endptr, 10);
+	if (endptr[0] != '\0')
+		errno = EINVAL;
+	if (errno != 0) {
+		res = -errno;
+		pw_log_error("server %p: strtol() failed with error: %m", s);
+		goto error;
+	}
+	fd = (int)v;
+	if (v != fd) {
+		res = -ERANGE;
+		pw_log_error("server %p: invalid fd %ld: %s", s, v, spa_strerror(res));
+		goto error;
+	}
+	if (dprintf(fd, "%s\n", s->addr.sun_path) < 0) {
+		res = -errno;
+		pw_log_error("server %p: dprintf() failed with error: %m", s);
+		goto error;
+	}
+	close(fd);
+	unsetenv("PIPEWIRE_NOTIFICATION_FD");
+	return 0;
+
+error:
+	return res;
+}
+
 static int add_socket(struct pw_protocol *protocol, struct server *s)
 {
 	socklen_t size;
@@ -815,6 +857,12 @@ static int add_socket(struct pw_protocol *protocol, struct server *s)
 		}
 	}
 
+	res = write_socket_address(s);
+	if (res < 0) {
+		pw_log_error("server %p: failed to write socket address: %s", s,
+				spa_strerror(res));
+		goto error_close;
+	}
 	s->activated = activated;
 	s->loop = pw_context_get_main_loop(protocol->context);
 	if (s->loop == NULL) {
@@ -990,35 +1038,9 @@ error:
 	goto done;
 }
 
-static void on_client_connection_destroy(void *data)
-{
-	struct client *impl = data;
-	spa_hook_remove(&impl->conn_listener);
-}
-
-static void on_client_need_flush(void *data)
-{
-        struct client *impl = data;
-
-	pw_log_trace("need flush");
-	impl->need_flush = true;
-
-	if (impl->source && !(impl->source->mask & SPA_IO_OUT)) {
-		pw_loop_update_io(impl->context->main_loop,
-				impl->source, impl->source->mask | SPA_IO_OUT);
-	}
-}
-
-static const struct pw_protocol_native_connection_events client_conn_events = {
-	PW_VERSION_PROTOCOL_NATIVE_CONNECTION_EVENTS,
-	.destroy = on_client_connection_destroy,
-	.need_flush = on_client_need_flush,
-};
-
 static int impl_connect_fd(struct pw_protocol_client *client, int fd, bool do_close)
 {
 	struct client *impl = SPA_CONTAINER_OF(client, struct client, this);
-	int res;
 
 	impl->connected = false;
 	impl->disconnecting = false;
@@ -1028,23 +1050,10 @@ static int impl_connect_fd(struct pw_protocol_client *client, int fd, bool do_cl
 					fd,
 					SPA_IO_IN | SPA_IO_OUT | SPA_IO_HUP | SPA_IO_ERR,
 					do_close, on_remote_data, impl);
-	if (impl->source == NULL) {
-		res = -errno;
-		goto error_cleanup;
-	}
+	if (impl->source == NULL)
+		return -errno;
 
-	pw_protocol_native_connection_add_listener(impl->connection,
-						   &impl->conn_listener,
-						   &client_conn_events,
-						   impl);
 	return 0;
-
-error_cleanup:
-	if (impl->connection) {
-		pw_protocol_native_connection_destroy(impl->connection);
-		impl->connection = NULL;
-	}
-	return res;
 }
 
 static void impl_disconnect(struct pw_protocol_client *client)
@@ -1057,9 +1066,7 @@ static void impl_disconnect(struct pw_protocol_client *client)
                 pw_loop_destroy_source(impl->context->main_loop, impl->source);
 	impl->source = NULL;
 
-	if (impl->connection)
-                pw_protocol_native_connection_destroy(impl->connection);
-	impl->connection = NULL;
+	pw_protocol_native_connection_set_fd(impl->connection, -1);
 }
 
 static void impl_destroy(struct pw_protocol_client *client)
@@ -1068,6 +1075,10 @@ static void impl_destroy(struct pw_protocol_client *client)
 
 	impl_disconnect(client);
 
+	if (impl->connection)
+                pw_protocol_native_connection_destroy(impl->connection);
+	impl->connection = NULL;
+
 	spa_list_remove(&client->link);
 	client_unref(impl);
 }
@@ -1134,6 +1145,31 @@ error:
 	goto done;
 }
 
+static void on_client_connection_destroy(void *data)
+{
+	struct client *impl = data;
+	spa_hook_remove(&impl->conn_listener);
+}
+
+static void on_client_need_flush(void *data)
+{
+        struct client *impl = data;
+
+	pw_log_trace("need flush");
+	impl->need_flush = true;
+
+	if (impl->source && !(impl->source->mask & SPA_IO_OUT)) {
+		pw_loop_update_io(impl->context->main_loop,
+				impl->source, impl->source->mask | SPA_IO_OUT);
+	}
+}
+
+static const struct pw_protocol_native_connection_events client_conn_events = {
+	PW_VERSION_PROTOCOL_NATIVE_CONNECTION_EVENTS,
+	.destroy = on_client_connection_destroy,
+	.need_flush = on_client_need_flush,
+};
+
 static struct pw_protocol_client *
 impl_new_client(struct pw_protocol *protocol,
 		struct pw_core *core,
@@ -1160,6 +1196,10 @@ impl_new_client(struct pw_protocol *protocol,
 		res = -errno;
 		goto error_free;
 	}
+	pw_protocol_native_connection_add_listener(impl->connection,
+						   &impl->conn_listener,
+						   &client_conn_events,
+						   impl);
 
 	if (props) {
 		str = spa_dict_lookup(props, PW_KEY_REMOTE_INTENTION);
diff --git a/src/modules/module-protocol-pulse.c b/src/modules/module-protocol-pulse.c
index f638880a8a9ea2fb51cb2d4e2959ac2c1e9949e4..29ba0c09241fc0c3f1b322abaec150cdbc00b0ae 100644
--- a/src/modules/module-protocol-pulse.c
+++ b/src/modules/module-protocol-pulse.c
@@ -214,14 +214,16 @@
  * VMs usually can't support the low latency settings that are possible on real
  * hardware.
  *
- * ## Application settings (Rules)
- *
- * The pulse protocol module supports generic config rules. It provides a `quirks`
- * and an `update-props` action.
+ * ## Stream settings and rules
  *
  * Streams created by module-protocol-pulse will use the stream.properties
  * section and stream.rules sections as usual.
  *
+ * ## Application settings (Rules)
+ *
+ * The pulse protocol module supports generic config rules. It supports a pulse.rules
+ * section with a `quirks` and an `update-props` action.
+ *
  *\code{.unparsed}
  * pulse.rules = [
  *     {
diff --git a/src/modules/module-protocol-pulse/client.c b/src/modules/module-protocol-pulse/client.c
index 167955c0e79d7bf01758f782184dc89a1d62d2b1..fe7d7885831557d91de73289c19af44f942c3ef6 100644
--- a/src/modules/module-protocol-pulse/client.c
+++ b/src/modules/module-protocol-pulse/client.c
@@ -159,10 +159,10 @@ void client_free(struct client *client)
 		pending_sample_free(p);
 
 	if (client->message)
-		message_free(impl, client->message, false, false);
+		message_free(client->message, false, false);
 
 	spa_list_consume(msg, &client->out_messages, link)
-		message_free(impl, msg, true, false);
+		message_free(msg, true, false);
 
 	spa_list_consume(o, &client->operations, link)
 		operation_free(o);
@@ -177,6 +177,9 @@ void client_free(struct client *client)
 	free(client->default_sink);
 	free(client->default_source);
 
+	free(client->temporary_default_sink);
+	free(client->temporary_default_source);
+
 	pw_properties_free(client->props);
 	pw_properties_free(client->routes);
 
@@ -220,14 +223,12 @@ int client_queue_message(struct client *client, struct message *msg)
 	return 0;
 
 error:
-	message_free(impl, msg, false, false);
+	message_free(msg, false, false);
 	return res;
 }
 
 static int client_try_flush_messages(struct client *client)
 {
-	struct impl *impl = client->impl;
-
 	pw_log_trace("client %p: flushing", client);
 
 	spa_assert(!client->disconnect);
@@ -254,7 +255,7 @@ static int client_try_flush_messages(struct client *client)
 		} else {
 			if (debug_messages && m->channel == SPA_ID_INVALID)
 				message_dump(SPA_LOG_LEVEL_INFO, m);
-			message_free(impl, m, true, false);
+			message_free(m, true, false);
 			client->out_index = 0;
 			continue;
 		}
@@ -307,7 +308,7 @@ static bool drop_from_out_queue(struct client *client, struct message *m)
 	if (m == first && client->out_index > 0)
 		return false;
 
-	message_free(client->impl, m, true, false);
+	message_free(m, true, false);
 
 	return true;
 }
diff --git a/src/modules/module-protocol-pulse/client.h b/src/modules/module-protocol-pulse/client.h
index d9344584038d9291135a963ffda090c507da12dc..ed0ee813f99cb56caa736443c66eb56188a2daa0 100644
--- a/src/modules/module-protocol-pulse/client.h
+++ b/src/modules/module-protocol-pulse/client.h
@@ -56,7 +56,7 @@ struct client {
 	struct server *server;
 
 	int ref;
-	const char *name;
+	const char *name; /* owned by `client::props` */
 
 	struct spa_source *source;
 
@@ -75,6 +75,8 @@ struct client {
 	struct pw_manager_object *metadata_default;
 	char *default_sink;
 	char *default_source;
+	char *temporary_default_sink;		/**< pending value, for MOVE_* commands */
+	char *temporary_default_source;		/**< pending value, for MOVE_* commands */
 	struct pw_manager_object *metadata_routes;
 	struct pw_properties *routes;
 
diff --git a/src/modules/module-protocol-pulse/message.c b/src/modules/module-protocol-pulse/message.c
index 43b7ba2a3aefa73455773a769076a1692aa5bd21..e70b03a2bcd87233221925ed2a74abc6106aa011 100644
--- a/src/modules/module-protocol-pulse/message.c
+++ b/src/modules/module-protocol-pulse/message.c
@@ -390,6 +390,9 @@ static int ensure_size(struct message *m, uint32_t size)
 	uint32_t alloc, diff;
 	void *data;
 
+	if (m->length > m->allocated)
+		return -ENOMEM;
+
 	if (m->length + size <= m->allocated)
 		return size;
 
@@ -397,12 +400,13 @@ static int ensure_size(struct message *m, uint32_t size)
 	diff = alloc - m->allocated;
 	if ((data = realloc(m->data, alloc)) == NULL) {
 		free(m->data);
-		m->stat->allocated -= m->allocated;
+		m->data = NULL;
+		m->impl->stat.allocated -= m->allocated;
 		m->allocated = 0;
 		return -errno;
 	}
-	m->stat->allocated += diff;
-	m->stat->accumulated += diff;
+	m->impl->stat.allocated += diff;
+	m->impl->stat.accumulated += diff;
 	m->data = data;
 	m->allocated = alloc;
 	return size;
@@ -826,18 +830,20 @@ struct message *message_alloc(struct impl *impl, uint32_t channel, uint32_t size
 		msg = spa_list_first(&impl->free_messages, struct message, link);
 		spa_list_remove(&msg->link);
 		pw_log_trace("using recycled message %p size:%d", msg, size);
+
+		spa_assert(msg->impl == impl);
 	} else {
 		if ((msg = calloc(1, sizeof(*msg))) == NULL)
 			return NULL;
 
 		pw_log_trace("new message %p size:%d", msg, size);
-		msg->stat = &impl->stat;
-		msg->stat->n_allocated++;
-		msg->stat->n_accumulated++;
+		msg->impl = impl;
+		msg->impl->stat.n_allocated++;
+		msg->impl->stat.n_accumulated++;
 	}
 
 	if (ensure_size(msg, size) < 0) {
-		message_free(impl, msg, false, true);
+		message_free(msg, false, true);
 		return NULL;
 	}
 
@@ -849,23 +855,23 @@ struct message *message_alloc(struct impl *impl, uint32_t channel, uint32_t size
 	return msg;
 }
 
-void message_free(struct impl *impl, struct message *msg, bool dequeue, bool destroy)
+void message_free(struct message *msg, bool dequeue, bool destroy)
 {
 	if (dequeue)
 		spa_list_remove(&msg->link);
 
-	if (msg->stat->allocated > MAX_ALLOCATED || msg->allocated > MAX_SIZE)
+	if (msg->impl->stat.allocated > MAX_ALLOCATED || msg->allocated > MAX_SIZE)
 		destroy = true;
 
 	if (destroy) {
 		pw_log_trace("destroy message %p size:%d", msg, msg->allocated);
-		msg->stat->n_allocated--;
-		msg->stat->allocated -= msg->allocated;
+		msg->impl->stat.n_allocated--;
+		msg->impl->stat.allocated -= msg->allocated;
 		free(msg->data);
 		free(msg);
 	} else {
 		pw_log_trace("recycle message %p size:%d/%d", msg, msg->length, msg->allocated);
-		spa_list_append(&impl->free_messages, &msg->link);
+		spa_list_append(&msg->impl->free_messages, &msg->link);
 		msg->length = 0;
 	}
 }
diff --git a/src/modules/module-protocol-pulse/message.h b/src/modules/module-protocol-pulse/message.h
index 022f70e1be667918d503d180f9d6a782d56f2998..ad952929b3d9d061d1be6101c606124562893ac5 100644
--- a/src/modules/module-protocol-pulse/message.h
+++ b/src/modules/module-protocol-pulse/message.h
@@ -32,12 +32,10 @@
 #include <spa/support/log.h>
 
 struct impl;
-struct client;
-struct stats;
 
 struct message {
 	struct spa_list link;
-	struct stats *stat;
+	struct impl *impl;
 	uint32_t extra[4];
 	uint32_t channel;
 	uint32_t allocated;
@@ -69,7 +67,7 @@ enum {
 };
 
 struct message *message_alloc(struct impl *impl, uint32_t channel, uint32_t size);
-void message_free(struct impl *impl, struct message *msg, bool dequeue, bool destroy);
+void message_free(struct message *msg, bool dequeue, bool destroy);
 int message_get(struct message *m, ...);
 int message_put(struct message *m, ...);
 int message_dump(enum spa_log_level level, struct message *m);
diff --git a/src/modules/module-protocol-pulse/pulse-server.c b/src/modules/module-protocol-pulse/pulse-server.c
index 7dead6055432e66bff2783fc5a57ac3eb25cf93e..ff7e1e9957075f3c808cde17310cd63a5515dab6 100644
--- a/src/modules/module-protocol-pulse/pulse-server.c
+++ b/src/modules/module-protocol-pulse/pulse-server.c
@@ -87,9 +87,6 @@
 #define DEFAULT_POSITION	"[ FL FR ]"
 
 #define MAX_FORMATS	32
-/* The max amount of data we send in one block when capturing. In PulseAudio this
- * size is derived from the mempool PA_MEMPOOL_SLOT_SIZE */
-#define MAX_FRAGSIZE	(64*1024)
 
 #define TEMPORARY_MOVE_TIMEOUT	(SPA_NSEC_PER_SEC)
 
@@ -455,6 +452,10 @@ static uint32_t fix_playback_buffer_attr(struct stream *s, struct buffer_attr *a
 	if (frame_size == 0)
 		frame_size = 4;
 
+	pw_log_info("[%s] maxlength:%u tlength:%u minreq:%u prebuf:%u",
+			s->client->name, attr->maxlength, attr->tlength,
+			attr->minreq, attr->prebuf);
+
 	minreq = frac_to_bytes_round_up(s->min_req, &s->ss);
 	max_latency = defs->quantum_limit * frame_size;
 
@@ -651,6 +652,9 @@ static uint32_t fix_record_buffer_attr(struct stream *s, struct buffer_attr *att
 	if (frame_size == 0)
 		frame_size = 4;
 
+	pw_log_info("[%s] maxlength:%u fragsize:%u",
+			s->client->name, attr->maxlength, attr->fragsize);
+
 	if (attr->maxlength == (uint32_t) -1 || attr->maxlength > MAXLENGTH)
 		attr->maxlength = MAXLENGTH;
 	attr->maxlength -= attr->maxlength % frame_size;
@@ -660,22 +664,23 @@ static uint32_t fix_record_buffer_attr(struct stream *s, struct buffer_attr *att
 
 	if (attr->fragsize == (uint32_t) -1 || attr->fragsize == 0)
 		attr->fragsize = frac_to_bytes_round_up(s->default_frag, &s->ss);
-	attr->fragsize -= attr->fragsize % frame_size;
+	attr->fragsize = SPA_MIN(attr->fragsize, attr->maxlength);
+	attr->fragsize = SPA_ROUND_UP(attr->fragsize, frame_size);
 	attr->fragsize = SPA_MAX(attr->fragsize, minfrag);
-	attr->fragsize = SPA_MAX(attr->fragsize, frame_size);
 
-	if (attr->fragsize > attr->maxlength)
-		attr->fragsize = attr->maxlength;
+	/* pulseaudio configures the source to half of the fragsize. It also
+	 * immediately sends chunks to clients. Configure a 2/3 of the fragsize
+	 * as the latency. */
+	latency = attr->fragsize * 2 / 3;
 
-	attr->tlength = attr->minreq = attr->prebuf = 0;
+	if (s->adjust_latency)
+		attr->fragsize = SPA_ROUND_UP(latency, frame_size);
 
-	if (s->early_requests) {
-		latency = attr->fragsize;
-	} else if (s->adjust_latency) {
-		latency = attr->fragsize;
-	} else {
-		latency = attr->fragsize;
-	}
+	attr->tlength = attr->prebuf = 0;
+
+	/* make sure can queue at least to fragsize without overruns */
+	if (attr->maxlength < attr->fragsize * 4)
+		attr->maxlength = attr->fragsize * 4;
 
 	pw_log_info("[%s] maxlength:%u fragsize:%u minfrag:%u latency:%u",
 			s->client->name, attr->maxlength, attr->fragsize, minfrag,
@@ -748,13 +753,12 @@ static int reply_create_record_stream(struct stream *stream, struct pw_manager_o
 		peer = find_linked(manager, peer->id, PW_DIRECTION_OUTPUT);
 	if (peer && pw_manager_object_is_source_or_monitor(peer)) {
 		name = pw_properties_get(peer->props, PW_KEY_NODE_NAME);
+		peer_index = peer->index;
 		if (!pw_manager_object_is_source(peer)) {
 			size_t len = (name ? strlen(name) : 5) + 10;
-			peer_index = peer->index;
 			peer_name = tmp = alloca(len);
 			snprintf(tmp, len, "%s.monitor", name ? name : "sink");
 		} else {
-			peer_index = peer->index;
 			peer_name = name;
 		}
 	} else {
@@ -849,6 +853,13 @@ static void manager_added(void *data, struct pw_manager_object *o)
 			s->peer_index = peer->index;
 
 			peer_name = pw_properties_get(peer->props, PW_KEY_NODE_NAME);
+			if (peer_name && s->direction == PW_DIRECTION_INPUT &&
+			    pw_manager_object_is_monitor(peer)) {
+				int len = strlen(peer_name) + 10;
+				char *tmp = alloca(len);
+				snprintf(tmp, len, "%s.monitor", peer_name);
+				peer_name = tmp;
+			}
 			if (peer_name != NULL)
 				stream_send_moved(s, peer->index, peer_name);
 		}
@@ -951,6 +962,8 @@ static void manager_metadata(void *data, struct pw_manager_object *o,
 				free(client->default_sink);
 				client->default_sink = value ? strdup(value) : NULL;
 			}
+			free(client->temporary_default_sink);
+			client->temporary_default_sink = NULL;
 		}
 		if (key == NULL || spa_streq(key, "default.audio.source")) {
 			if (value != NULL) {
@@ -964,6 +977,8 @@ static void manager_metadata(void *data, struct pw_manager_object *o,
 				free(client->default_source);
 				client->default_source = value ? strdup(value) : NULL;
 			}
+			free(client->temporary_default_source);
+			client->temporary_default_source = NULL;
 		}
 		if (changed)
 			send_default_change_subscribe_event(client, true, true);
@@ -1024,12 +1039,12 @@ static int do_set_client_name(struct client *client, uint32_t command, uint32_t
 		changed++;
 	}
 
+	client_update_quirks(client);
+
 	client->name = pw_properties_get(client->props, PW_KEY_APP_NAME);
 	pw_log_info("[%s] %s tag:%d", client->name,
 			commands[command].name, tag);
 
-	client_update_quirks(client);
-
 	if (client->core == NULL) {
 		client->core = pw_context_connect(impl->context,
 				pw_properties_copy(client->props), 0);
@@ -1263,7 +1278,7 @@ do_process_done(struct spa_loop *loop,
 	stream->timestamp = pd->pwt.now;
 	stream->delay = pd->pwt.buffered * SPA_USEC_PER_SEC / stream->ss.rate;
 	if (pd->pwt.rate.denom > 0)
-		stream->delay = pd->pwt.delay * SPA_USEC_PER_SEC / pd->pwt.rate.denom;
+		stream->delay += pd->pwt.delay * SPA_USEC_PER_SEC * pd->pwt.rate.num / pd->pwt.rate.denom;
 
 	if (stream->direction == PW_DIRECTION_OUTPUT) {
 		if (pd->quantum != stream->last_quantum)
@@ -1318,10 +1333,10 @@ do_process_done(struct spa_loop *loop,
 				stream->read_index += skip;
 				avail = stream->attr.fragsize;
 			}
+			pw_log_trace("avail:%d index:%u", avail, index);
 
 			while ((uint32_t)avail >= stream->attr.fragsize) {
-				towrite = SPA_MIN(avail, MAX_FRAGSIZE);
-				towrite = SPA_ROUND_DOWN(towrite, stream->frame_size);
+				towrite = SPA_MIN((uint32_t)avail, stream->attr.fragsize);
 
 				msg = message_alloc(impl, stream->channel, towrite);
 				if (msg == NULL)
@@ -1792,7 +1807,7 @@ static int do_create_record_stream(struct client *client, uint32_t command, uint
 	struct channel_map map;
 	uint32_t source_index;
 	const char *source_name;
-	struct buffer_attr attr;
+	struct buffer_attr attr = { 0 };
 	bool corked = false,
 		no_remap = false,
 		no_remix = false,
@@ -2111,7 +2126,7 @@ static int do_get_playback_latency(struct client *client, uint32_t command, uint
 	if (stream == NULL || stream->type != STREAM_TYPE_PLAYBACK)
 		return -ENOENT;
 
-	pw_log_debug("read:%"PRIx64" write:%"PRIx64" queued:%"PRIi64" delay:%"PRIi64
+	pw_log_debug("read:0x%"PRIx64" write:0x%"PRIx64" queued:%"PRIi64" delay:%"PRIi64
 			" playing:%"PRIu64,
 			stream->read_index, stream->write_index,
 			stream->write_index - stream->read_index, stream->delay,
@@ -2160,6 +2175,11 @@ static int do_get_record_latency(struct client *client, uint32_t command, uint32
 	if (stream == NULL || stream->type != STREAM_TYPE_RECORD)
 		return -ENOENT;
 
+	pw_log_debug("read:0x%"PRIx64" write:0x%"PRIx64" queued:%"PRIi64" delay:%"PRIi64,
+			stream->read_index, stream->write_index,
+			stream->write_index - stream->read_index, stream->delay);
+
+
 	gettimeofday(&now, NULL);
 	reply = reply_new(client, tag);
 	message_put(reply,
@@ -3236,6 +3256,7 @@ static int do_update_proplist(struct client *client, uint32_t command, uint32_t
 	} else {
 		if (pw_properties_update(client->props, &props->dict) > 0) {
 			client_update_quirks(client);
+			client->name = pw_properties_get(client->props, PW_KEY_APP_NAME);
 			pw_core_update_properties(client->core, &client->props->dict);
 		}
 	}
@@ -3675,6 +3696,27 @@ static int fill_card_info(struct client *client, struct message *m,
 	return 0;
 }
 
+static int fill_sink_info_proplist(struct message *m, const struct spa_dict *sink_props,
+		const struct pw_manager_object *card)
+{
+	struct pw_device_info *card_info = card ? card->info : NULL;
+	struct pw_properties *props = NULL;
+
+	if (card_info && card_info->props) {
+		props = pw_properties_new_dict(sink_props);
+		if (props == NULL)
+			return -ENOMEM;
+
+		pw_properties_add(props, card_info->props);
+		sink_props = &props->dict;
+	}
+	message_put(m, TAG_PROPLIST, sink_props, TAG_INVALID);
+
+	pw_properties_free(props);
+
+	return 0;
+}
+
 static int fill_sink_info(struct client *client, struct message *m,
 		struct pw_manager_object *o)
 {
@@ -3767,8 +3809,10 @@ static int fill_sink_info(struct client *client, struct message *m,
 		TAG_INVALID);
 
 	if (client->version >= 13) {
+		int res;
+		if ((res = fill_sink_info_proplist(m, info->props, card)) < 0)
+			return res;
 		message_put(m,
-			TAG_PROPLIST, info->props,
 			TAG_USEC, 0LL,			/* requested latency */
 			TAG_INVALID);
 	}
@@ -3857,22 +3901,27 @@ static int fill_sink_info(struct client *client, struct message *m,
 	return 0;
 }
 
-static int fill_source_info_proplist(struct message *m, struct pw_manager_object *o,
-		struct pw_node_info *info)
+static int fill_source_info_proplist(struct message *m, const struct spa_dict *source_props,
+		const struct pw_manager_object *card, const bool is_monitor)
 {
+	struct pw_device_info *card_info = card ? card->info : NULL;
 	struct pw_properties *props = NULL;
-	struct spa_dict *props_dict = info->props;
 
-	if (pw_manager_object_is_monitor(o)) {
-		props = pw_properties_new_dict(info->props);
+	if ((card_info && card_info->props) || is_monitor) {
+		props = pw_properties_new_dict(source_props);
 		if (props == NULL)
 			return -ENOMEM;
 
-		pw_properties_set(props, PW_KEY_DEVICE_CLASS, "monitor");
-		props_dict = &props->dict;
+		if (card_info && card_info->props)
+			pw_properties_add(props, card_info->props);
+
+		if (is_monitor)
+			pw_properties_set(props, PW_KEY_DEVICE_CLASS, "monitor");
+
+		source_props = &props->dict;
 	}
+	message_put(m, TAG_PROPLIST, source_props, TAG_INVALID);
 
-	message_put(m, TAG_PROPLIST, props_dict, TAG_INVALID);
 	pw_properties_free(props);
 
 	return 0;
@@ -3974,7 +4023,7 @@ static int fill_source_info(struct client *client, struct message *m,
 
 	if (client->version >= 13) {
 		int res;
-		if ((res = fill_source_info_proplist(m, o, info)) < 0)
+		if ((res = fill_source_info_proplist(m, info->props, card, is_monitor)) < 0)
 			return res;
 		message_put(m,
 			TAG_USEC, 0LL,			/* requested latency */
@@ -4313,7 +4362,7 @@ error_invalid:
 	goto error;
 error:
 	if (reply)
-		message_free(impl, reply, false, false);
+		message_free(reply, false, false);
 	return res;
 }
 
@@ -4389,7 +4438,7 @@ static int do_get_sample_info(struct client *client, uint32_t command, uint32_t
 
 error:
 	if (reply)
-		message_free(impl, reply, false, false);
+		message_free(reply, false, false);
 	return res;
 }
 
@@ -4713,6 +4762,20 @@ static int do_set_default(struct client *client, uint32_t command, uint32_t tag,
 	if (res < 0)
 		return res;
 
+	/*
+	 * The metadata is not necessarily updated within one server sync.
+	 * Correct functioning of MOVE_* commands requires knowing the current
+	 * default target, so we need to stash temporary values here in case
+	 * the client emits them before metadata gets updated.
+	 */
+	if (sink) {
+		free(client->temporary_default_sink);
+		client->temporary_default_sink = name ? strdup(name) : NULL;
+	} else {
+		free(client->temporary_default_source);
+		client->temporary_default_source = name ? strdup(name) : NULL;
+	}
+
 	return operation_new(client, tag);
 }
 
@@ -4754,6 +4817,7 @@ static int do_move_stream(struct client *client, uint32_t command, uint32_t tag,
 	int target_id;
 	int64_t target_serial;
 	const char *name_device;
+	const char *name;
 	struct pw_node_info *info;
 	struct selector sel;
 	int res;
@@ -4790,7 +4854,13 @@ static int do_move_stream(struct client *client, uint32_t command, uint32_t tag,
 	if ((dev = find_device(client, index_device, name_device, sink, NULL)) == NULL)
 		return -ENOENT;
 
-	dev_default = find_device(client, SPA_ID_INVALID, NULL, sink, NULL);
+	/*
+	 * The client metadata is not necessarily yet updated after SET_DEFAULT command,
+	 * so use the temporary values if they are still set.
+	 */
+	name = sink ? client->temporary_default_sink : client->temporary_default_source;
+	dev_default = find_device(client, SPA_ID_INVALID, name, sink, NULL);
+
 	if (dev == dev_default) {
 		/*
 		 * When moving streams to a node that is equal to the default,
@@ -4816,6 +4886,11 @@ static int do_move_stream(struct client *client, uint32_t command, uint32_t tag,
 			SPA_TYPE_INFO_BASE"Id", "%"PRIi64, target_serial)) < 0)
 		return res;
 
+	name = spa_dict_lookup(info->props, PW_KEY_NODE_NAME);
+	pw_log_debug("[%s] %s done tag:%u index:%u name:%s target:%d target-serial:%"PRIi64, client->name,
+			commands[command].name, tag, index, name ? name : "<null>",
+			target_id, target_serial);
+
 	/* We will temporarily claim the stream was already moved */
 	set_temporary_move_target(client, o, dev->index);
 	send_object_event(client, o, SUBSCRIPTION_EVENT_CHANGE);
@@ -5353,7 +5428,7 @@ static void impl_clear(struct impl *impl)
 		client_free(c);
 
 	spa_list_consume(msg, &impl->free_messages, link)
-		message_free(impl, msg, true, true);
+		message_free(msg, true, true);
 
 	pw_map_for_each(&impl->samples, impl_free_sample, impl);
 	pw_map_clear(&impl->samples);
diff --git a/src/modules/module-protocol-pulse/remap.c b/src/modules/module-protocol-pulse/remap.c
index 1ddd9e3a7b58efc4934c0ca7acdd01eda53be3f9..aba66bb0729e801602d6cfbac7f610163f541148 100644
--- a/src/modules/module-protocol-pulse/remap.c
+++ b/src/modules/module-protocol-pulse/remap.c
@@ -43,6 +43,7 @@ const struct str_map media_role_map[] = {
 
 const struct str_map props_key_map[] = {
 	{ PW_KEY_DEVICE_BUS_PATH, "device.bus_path" },
+	{ PW_KEY_DEVICE_SYSFS_PATH, "sysfs.path" },
 	{ PW_KEY_DEVICE_FORM_FACTOR, "device.form_factor" },
 	{ PW_KEY_DEVICE_ICON_NAME, "device.icon_name" },
 	{ PW_KEY_DEVICE_INTENDED_ROLES, "device.intended_roles" },
diff --git a/src/modules/module-protocol-pulse/server.c b/src/modules/module-protocol-pulse/server.c
index d6b7797b877683fb5ce134d45b3e8f8c4468360d..b363a66bc846b7acd93a4c5ae5b520c9daf83842 100644
--- a/src/modules/module-protocol-pulse/server.c
+++ b/src/modules/module-protocol-pulse/server.c
@@ -60,13 +60,13 @@
 #include "server.h"
 #include "stream.h"
 #include "utils.h"
+#include "flatpak-utils.h"
 
 #define LISTEN_BACKLOG 32
 #define MAX_CLIENTS 64
 
 static int handle_packet(struct client *client, struct message *msg)
 {
-	struct impl * const impl = client->impl;
 	uint32_t command, tag;
 	int res = 0;
 
@@ -110,7 +110,7 @@ static int handle_packet(struct client *client, struct message *msg)
 	res = cmd->run(client, command, tag, msg);
 
 finish:
-	message_free(impl, msg, false, false);
+	message_free(msg, false, false);
 	if (res < 0)
 		reply_error(client, command, tag, res);
 
@@ -119,7 +119,6 @@ finish:
 
 static int handle_memblock(struct client *client, struct message *msg)
 {
-	struct impl * const impl = client->impl;
 	struct stream *stream;
 	uint32_t channel, flags, index;
 	int64_t offset, diff;
@@ -190,7 +189,7 @@ static int handle_memblock(struct client *client, struct message *msg)
 	stream_send_request(stream);
 
 finish:
-	message_free(impl, msg, false, false);
+	message_free(msg, false, false);
 	return res;
 }
 
@@ -264,7 +263,7 @@ static int do_read(struct client *client)
 		}
 
 		if (client->message)
-			message_free(impl, client->message, false, false);
+			message_free(client->message, false, false);
 
 		client->message = message_alloc(impl, channel, length);
 	} else if (client->message &&
@@ -419,14 +418,47 @@ on_connect(void *data, int fd, uint32_t mask)
 		client_access = server->client_access;
 
 	if (server->addr.ss_family == AF_UNIX) {
+		char *app_id = NULL, *devices = NULL;
+
 #ifdef SO_PRIORITY
 		val = 6;
 		if (setsockopt(client_fd, SOL_SOCKET, SO_PRIORITY, &val, sizeof(val)) < 0)
 			pw_log_warn("setsockopt(SO_PRIORITY) failed: %m");
 #endif
 		pid = get_client_pid(client, client_fd);
-		if (pid != 0 && check_flatpak(client, pid) == 1)
+		if (pid != 0 && pw_check_flatpak(pid, &app_id, &devices) == 1) {
+			/*
+			 * XXX: we should really use Portal client access here
+			 *
+			 * However, session managers currently support only camera
+			 * permissions, and the XDG Portal doesn't have a "Sound Manager"
+			 * permission defined. So for now, use access=flatpak, and determine
+			 * extra permissions here.
+			 *
+			 * The application has access to the Pulseaudio socket,
+			 * and with real PA it would always then have full sound access.
+			 * We'll restrict the full access here behind devices=all;
+			 * if the application can access all devices it can then
+			 * also sound and camera devices directly, so granting also the
+			 * Manager permissions here is reasonable.
+			 *
+			 * The "Manager" permission in any case is also currently not safe
+			 * as the session manager does not check any permission store
+			 * for it.
+			 */
 			client_access = "flatpak";
+			pw_properties_set(client->props, "pipewire.access.portal.app_id",
+					app_id);
+
+			if (devices && (spa_streq(devices, "all") ||
+							spa_strstartswith(devices, "all;") ||
+							strstr(devices, ";all;")))
+				pw_properties_set(client->props, PW_KEY_MEDIA_CATEGORY, "Manager");
+			else
+				pw_properties_set(client->props, PW_KEY_MEDIA_CATEGORY, NULL);
+		}
+		free(devices);
+		free(app_id);
 	}
 	else if (server->addr.ss_family == AF_INET || server->addr.ss_family == AF_INET6) {
 
@@ -460,7 +492,7 @@ static int parse_unix_address(const char *address, struct sockaddr_storage *addr
 	if (address[0] != '/') {
 		char runtime_dir[PATH_MAX];
 
-		if ((res = get_runtime_dir(runtime_dir, sizeof(runtime_dir), "pulse")) < 0)
+		if ((res = get_runtime_dir(runtime_dir, sizeof(runtime_dir))) < 0)
 			return res;
 
 		res = snprintf(addr.sun_path, sizeof(addr.sun_path),
diff --git a/src/modules/module-protocol-pulse/utils.c b/src/modules/module-protocol-pulse/utils.c
index 52ddf75fc9ac1e3e751118dfb4e7b0a06b82bed0..1c9bc1c5f30cb4d4188e6248976bbbbd31478d51 100644
--- a/src/modules/module-protocol-pulse/utils.c
+++ b/src/modules/module-protocol-pulse/utils.c
@@ -50,27 +50,30 @@
 #include "log.h"
 #include "utils.h"
 
-int get_runtime_dir(char *buf, size_t buflen, const char *dir)
+int get_runtime_dir(char *buf, size_t buflen)
 {
-	const char *runtime_dir;
+	const char *runtime_dir, *dir = NULL;
 	struct stat stat_buf;
 	int res, size;
 
 	runtime_dir = getenv("PULSE_RUNTIME_PATH");
-	if (runtime_dir == NULL)
+	if (runtime_dir == NULL) {
 		runtime_dir = getenv("XDG_RUNTIME_DIR");
-
+		dir = "pulse";
+	}
 	if (runtime_dir == NULL) {
 		pw_log_error("could not find a suitable runtime directory in"
 				"$PULSE_RUNTIME_PATH and $XDG_RUNTIME_DIR");
 		return -ENOENT;
 	}
 
-	size = snprintf(buf, buflen, "%s/%s", runtime_dir, dir);
+	size = snprintf(buf, buflen, "%s%s%s", runtime_dir,
+			dir ? "/" : "", dir ? dir : "");
 	if (size < 0)
 		return -errno;
 	if ((size_t) size >= buflen) {
-		pw_log_error("path %s/%s too long", runtime_dir, dir);
+		pw_log_error("path %s%s%s too long", runtime_dir,
+				dir ? "/" : "", dir ? dir : "");
 		return -ENAMETOOLONG;
 	}
 
@@ -149,7 +152,7 @@ pid_t get_client_pid(struct client *client, int client_fd)
 		pw_log_warn("client %p: no peercred: %m", client);
 	} else
 		return ucred.pid;
-#elif defined(__FreeBSD__)
+#elif defined(__FreeBSD__) || defined(__MidnightBSD__)
 	struct xucred xucred;
 	len = sizeof(xucred);
 	if (getsockopt(client_fd, 0, LOCAL_PEERCRED, &xucred, &len) < 0) {
@@ -182,7 +185,7 @@ int create_pid_file(void) {
 	FILE *f;
 	int res;
 
-	if ((res = get_runtime_dir(pid_file, sizeof(pid_file), "pulse")) < 0)
+	if ((res = get_runtime_dir(pid_file, sizeof(pid_file))) < 0)
 		return res;
 
 	if (strlen(pid_file) > PATH_MAX - sizeof("/pid")) {
diff --git a/src/modules/module-protocol-pulse/utils.h b/src/modules/module-protocol-pulse/utils.h
index 488951f9fdb40762c330005bf5747e2db5cbd766..fafccf3f802dae6038e6d050f0ea703b62c9ed99 100644
--- a/src/modules/module-protocol-pulse/utils.h
+++ b/src/modules/module-protocol-pulse/utils.h
@@ -31,7 +31,7 @@
 struct client;
 struct pw_context;
 
-int get_runtime_dir(char *buf, size_t buflen, const char *dir);
+int get_runtime_dir(char *buf, size_t buflen);
 int check_flatpak(struct client *client, pid_t pid);
 pid_t get_client_pid(struct client *client, int client_fd);
 const char *get_server_name(struct pw_context *context);
diff --git a/src/modules/module-pulse-tunnel.c b/src/modules/module-pulse-tunnel.c
index a8a641e9cac428856b79b24439b3acd2b944eb94..f0231b9f3117e830551a64686ce8fec77337e95a 100644
--- a/src/modules/module-pulse-tunnel.c
+++ b/src/modules/module-pulse-tunnel.c
@@ -80,6 +80,7 @@
  * Options with well-known behavior.
  *
  * - \ref PW_KEY_REMOTE_NAME
+ * - \ref PW_KEY_AUDIO_FORMAT
  * - \ref PW_KEY_AUDIO_RATE
  * - \ref PW_KEY_AUDIO_CHANNELS
  * - \ref SPA_KEY_AUDIO_POSITION
@@ -89,7 +90,7 @@
  * - \ref PW_KEY_NODE_GROUP
  * - \ref PW_KEY_NODE_VIRTUAL
  * - \ref PW_KEY_MEDIA_CLASS
- * - \ref PW_KEY_NODE_TARGET to specify the remote name or id to link to
+ * - \ref PW_KEY_TARGET_OBJECT to specify the remote node.name or serial.id to link to
  *
  * ## Example configuration of a virtual sink
  *
@@ -103,7 +104,7 @@
  *         #audio.rate=<sample rate>
  *         #audio.channels=<number of channels>
  *         #audio.position=<channel map>
- *         #node.target=<remote target node>
+ *         #target.object=<remote target name>
  *         stream.props = {
  *             # extra sink properties
  *         }
@@ -118,11 +119,17 @@
 PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
 #define PW_LOG_TOPIC_DEFAULT mod_topic
 
+#define DEFAULT_FORMAT "S16"
+#define DEFAULT_RATE 48000
+#define DEFAULT_CHANNELS 2
+#define DEFAULT_POSITION "[ FL FR ]"
+
 #define MODULE_USAGE	"[ remote.name=<remote> ] "				\
 			"[ node.latency=<latency as fraction> ] "		\
 			"[ node.name=<name of the nodes> ] "			\
 			"[ node.description=<description of the nodes> ] "	\
 			"[ node.target=<remote node target name> ] "		\
+			"[ audio.format=<sample format> ] "			\
 			"[ audio.rate=<sample rate> ] "				\
 			"[ audio.channels=<number of channels> ] "		\
 			"[ audio.position=<channel map> ] "			\
@@ -271,7 +278,7 @@ static void playback_stream_process(void *d)
 	} else {
 		float error, corr;
 
-		error = (float)(impl->current_latency) - (float)impl->target_latency;
+		error = (float)impl->target_latency - (float)impl->current_latency;
 		error = SPA_CLAMP(error, -impl->max_error, impl->max_error);
 
 		corr = spa_dll_update(&impl->dll, error);
@@ -415,33 +422,47 @@ static int create_stream(struct impl *impl)
 static void context_state_cb(pa_context *c, void *userdata)
 {
 	struct impl *impl = userdata;
+	bool do_destroy = false;
 	switch (pa_context_get_state(c)) {
-	case PA_CONTEXT_READY:
 	case PA_CONTEXT_TERMINATED:
 	case PA_CONTEXT_FAILED:
+		do_destroy = true;
+		SPA_FALLTHROUGH;
+	case PA_CONTEXT_READY:
 		pa_threaded_mainloop_signal(impl->pa_mainloop, 0);
 		break;
 	case PA_CONTEXT_UNCONNECTED:
+		do_destroy = true;
+		break;
 	case PA_CONTEXT_CONNECTING:
 	case PA_CONTEXT_AUTHORIZING:
 	case PA_CONTEXT_SETTING_NAME:
 		break;
 	}
+	if (do_destroy)
+		pw_impl_module_schedule_destroy(impl->module);
 }
 
 static void stream_state_cb(pa_stream *s, void * userdata)
 {
 	struct impl *impl = userdata;
+	bool do_destroy = false;
 	switch (pa_stream_get_state(s)) {
-	case PA_STREAM_READY:
 	case PA_STREAM_FAILED:
 	case PA_STREAM_TERMINATED:
+		do_destroy = true;
+		SPA_FALLTHROUGH;
+	case PA_STREAM_READY:
 		pa_threaded_mainloop_signal(impl->pa_mainloop, 0);
 		break;
 	case PA_STREAM_UNCONNECTED:
+		do_destroy = true;
+		break;
 	case PA_STREAM_CREATING:
 		break;
 	}
+	if (do_destroy)
+		pw_impl_module_schedule_destroy(impl->module);
 }
 
 static void stream_read_request_cb(pa_stream *s, size_t length, void *userdata)
@@ -653,7 +674,9 @@ static int create_pulse_stream(struct impl *impl)
 	pa_stream_set_overflow_callback(impl->pa_stream, stream_overflow_cb, impl);
 	pa_stream_set_latency_update_callback(impl->pa_stream, stream_latency_update_cb, impl);
 
-	remote_node_target = pw_properties_get(impl->props, PW_KEY_NODE_TARGET);
+	remote_node_target = pw_properties_get(impl->props, PW_KEY_TARGET_OBJECT);
+	if (remote_node_target == NULL)
+		remote_node_target = pw_properties_get(impl->props, PW_KEY_NODE_TARGET);
 
 	bufferattr.fragsize = (uint32_t) -1;
 	bufferattr.minreq = (uint32_t) -1;
@@ -823,28 +846,25 @@ static inline uint32_t format_from_name(const char *name, size_t len)
 	return SPA_AUDIO_FORMAT_UNKNOWN;
 }
 
-static void parse_audio_info(struct pw_properties *props, struct spa_audio_info_raw *info)
+static void parse_audio_info(const struct pw_properties *props, struct spa_audio_info_raw *info)
 {
 	const char *str;
 
-	*info = SPA_AUDIO_INFO_RAW_INIT(
-			.rate = 48000,
-			.channels = 2,
-			.format = SPA_AUDIO_FORMAT_S16);
-
-	if ((str = pw_properties_get(props, PW_KEY_AUDIO_FORMAT)) != NULL) {
-		uint32_t id;
-
-		id = format_from_name(str, strlen(str));
-		if (id != SPA_AUDIO_FORMAT_UNKNOWN)
-			info->format = id;
-	}
+	spa_zero(*info);
+	if ((str = pw_properties_get(props, PW_KEY_AUDIO_FORMAT)) == NULL)
+		str = DEFAULT_FORMAT;
+	info->format = format_from_name(str, strlen(str));
 
 	info->rate = pw_properties_get_uint32(props, PW_KEY_AUDIO_RATE, info->rate);
+	if (info->rate == 0)
+		info->rate = DEFAULT_RATE;
+
 	info->channels = pw_properties_get_uint32(props, PW_KEY_AUDIO_CHANNELS, info->channels);
+	info->channels = SPA_MIN(info->channels, SPA_AUDIO_MAX_CHANNELS);
 	if ((str = pw_properties_get(props, SPA_KEY_AUDIO_POSITION)) != NULL)
 		parse_position(info, str, strlen(str));
-
+	if (info->channels == 0)
+		parse_position(info, DEFAULT_POSITION, strlen(DEFAULT_POSITION));
 }
 
 static int calc_frame_size(struct spa_audio_info_raw *info)
diff --git a/src/modules/module-raop-discover.c b/src/modules/module-raop-discover.c
index 88ca2062e6152f11f4b98830715e6de18b2fa96f..dd26bad950630b0c952dce8ffeaf01f7663d0c49 100644
--- a/src/modules/module-raop-discover.c
+++ b/src/modules/module-raop-discover.c
@@ -223,6 +223,8 @@ static void pw_properties_from_avahi_string(const char *key, const char *value,
 		 *  4 = FairPlay SAPv2.5. */
 		if (str_in_list(value, ",", "1"))
 			value = "RSA";
+		else if (str_in_list(value, ",", "4"))
+			value = "auth_setup";
 		else
 			value = "none";
 		pw_properties_set(props, "raop.encryption.type", value);
diff --git a/src/modules/module-raop-sink.c b/src/modules/module-raop-sink.c
index e3bbce5123365d8ab983d40329f74562a45d35b9..83d279f2cc6e35f47596a62bcde9bdf6c39729f7 100644
--- a/src/modules/module-raop-sink.c
+++ b/src/modules/module-raop-sink.c
@@ -110,7 +110,7 @@ PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
 			"[ node.name=<name of the nodes> ] "					\
 			"[ node.description=<description of the nodes> ] "			\
 			"[ audio.format=<format, default:"DEFAULT_FORMAT"> ] "			\
-			"[ audio.rate=<sample rate, default: "SPA_STRINGIFY(DEFAuLT_RATE)"> ] "			\
+			"[ audio.rate=<sample rate, default: "SPA_STRINGIFY(DEFAULT_RATE)"> ] "			\
 			"[ audio.channels=<number of channels, default:"SPA_STRINGIFY(DEFAULT_CHANNELS)"> ] "	\
 			"[ audio.position=<channel map, default:"DEFAULT_POSITION"> ] "		\
 			"[ stream.props=<properties> ] "
@@ -130,6 +130,7 @@ enum {
 enum {
 	CRYPTO_NONE,
 	CRYPTO_RSA,
+	CRYPTO_AUTH_SETUP,
 };
 enum {
 	CODEC_PCM,
@@ -257,7 +258,8 @@ static inline uint64_t ntp_now(int clockid)
 	return timespec_to_ntp(&now);
 }
 
-static int send_udp_sync_packet(struct impl *impl)
+static int send_udp_sync_packet(struct impl *impl,
+		struct sockaddr *dest_addr, socklen_t addrlen)
 {
 	uint32_t pkt[5];
 	uint32_t rtptime = impl->rtptime;
@@ -278,10 +280,11 @@ static int send_udp_sync_packet(struct impl *impl)
 	pw_log_debug("sync: delayed:%u now:%"PRIu64" rtptime:%u",
 			rtptime - delay, transmitted, rtptime);
 
-	return write(impl->control_fd, pkt, sizeof(pkt));
+	return sendto(impl->control_fd, pkt, sizeof(pkt), 0, dest_addr, addrlen);
 }
 
-static int send_udp_timing_packet(struct impl *impl, uint64_t remote, uint64_t received)
+static int send_udp_timing_packet(struct impl *impl, uint64_t remote, uint64_t received,
+		struct sockaddr *dest_addr, socklen_t addrlen)
 {
 	uint32_t pkt[8];
 	uint64_t transmitted;
@@ -299,7 +302,7 @@ static int send_udp_timing_packet(struct impl *impl, uint64_t remote, uint64_t r
 	pw_log_debug("sync: remote:%"PRIu64" received:%"PRIu64" transmitted:%"PRIu64,
 			remote, received, transmitted);
 
-	return write(impl->timing_fd, pkt, sizeof(pkt));
+	return sendto(impl->timing_fd, pkt, sizeof(pkt), 0, dest_addr, addrlen);
 }
 
 static int write_codec_pcm(void *dst, void *frames, uint32_t n_frames)
@@ -345,7 +348,7 @@ static int flush_to_udp_packet(struct impl *impl)
 	impl->sync++;
 	if (impl->first || impl->sync == impl->sync_period) {
 		impl->sync = 0;
-		send_udp_sync_packet(impl);
+		send_udp_sync_packet(impl, NULL, 0);
 	}
 	pkt[0] = htonl(0x80600000);
 	if (impl->first)
@@ -373,7 +376,7 @@ static int flush_to_udp_packet(struct impl *impl)
 	impl->seq = (impl->seq + 1) & 0xffff;
 
 	pw_log_debug("send %u", len + 12);
-	res = write(impl->server_fd, pkt, len + 12);
+	res = send(impl->server_fd, pkt, len + 12, 0);
 
 	impl->first = false;
 
@@ -417,7 +420,7 @@ static int flush_to_tcp_packet(struct impl *impl)
 	impl->seq = (impl->seq + 1) & 0xffff;
 
 	pw_log_debug("send %u", len + 16);
-	res = write(impl->server_fd, pkt, len + 16);
+	res = send(impl->server_fd, pkt, len + 16, 0);
 
 	impl->first = false;
 
@@ -593,9 +596,12 @@ on_timing_source_io(void *data, int fd, uint32_t mask)
 
 	if (mask & SPA_IO_IN) {
 		uint64_t remote, received;
+		struct sockaddr_storage sender;
+		socklen_t sender_size = sizeof(sender);
 
 		received = ntp_now(CLOCK_MONOTONIC);
-		bytes = read(impl->timing_fd, packet, sizeof(packet));
+		bytes = recvfrom(impl->timing_fd, packet, sizeof(packet), 0,
+				(struct sockaddr*)&sender, &sender_size);
 		if (bytes < 0) {
 			pw_log_debug("error reading timing packet: %m");
 			return;
@@ -609,7 +615,11 @@ on_timing_source_io(void *data, int fd, uint32_t mask)
 			return;
 
 		remote = ((uint64_t)ntohl(packet[6])) << 32 | ntohl(packet[7]);
-		send_udp_timing_packet(impl, remote, received);
+		if (send_udp_timing_packet(impl, remote, received,
+				(struct sockaddr *)&sender, sender_size) < 0) {
+			pw_log_warn("error sending timing packet");
+			return;
+		}
 	}
 }
 
@@ -833,10 +843,8 @@ static void rtsp_setup_reply(void *data, int status, const struct spa_dict *head
 			return;
 
 		ntp = ntp_now(CLOCK_MONOTONIC);
-		send_udp_timing_packet(impl, ntp, ntp);
+		send_udp_timing_packet(impl, ntp, ntp, NULL, 0);
 
-		impl->timing_source = pw_loop_add_io(impl->loop, impl->timing_fd,
-				SPA_IO_IN, false, on_timing_source_io, impl);
 		impl->control_source = pw_loop_add_io(impl->loop, impl->control_fd,
 				SPA_IO_IN, false, on_control_source_io, impl);
 
@@ -868,6 +876,9 @@ static int rtsp_do_setup(struct impl *impl)
 		if (impl->control_fd < 0 || impl->timing_fd < 0)
 			goto error;
 
+		impl->timing_source = pw_loop_add_io(impl->loop, impl->timing_fd,
+				SPA_IO_IN, false, on_timing_source_io, impl);
+
 		pw_properties_setf(impl->headers, "Transport",
 				"RTP/AVP/UDP;unicast;interleaved=0-1;mode=record;"
 				"control_port=%u;timing_port=%u",
@@ -988,7 +999,7 @@ static int rtsp_do_announce(struct impl *impl)
 	char iv[16*2];
 	int res, frames, i, ip_version;
 	char *sdp;
-        char local_ip[256];
+	char local_ip[256];
 
 	host = pw_properties_get(impl->props, "raop.hostname");
 
@@ -1048,6 +1059,32 @@ static int rtsp_do_announce(struct impl *impl)
 	return res;
 }
 
+static void rtsp_auth_setup_reply(void *data, int status, const struct spa_dict *headers)
+{
+	struct impl *impl = data;
+
+	pw_log_info("reply %d", status);
+
+	impl->encryption = CRYPTO_NONE;
+
+	rtsp_do_announce(impl);
+}
+
+static int rtsp_do_auth_setup(struct impl *impl)
+{
+	int res;
+
+	char output[] = 
+		"\x01"
+		"\x59\x02\xed\xe9\x0d\x4e\xf2\xbd\x4c\xb6\x8a\x63\x30\x03\x82\x07"
+		"\xa9\x4d\xbd\x50\xd8\xaa\x46\x5b\x5d\x8c\x01\x2a\x0c\x7e\x1d\x4e";
+
+	res = pw_rtsp_client_url_send(impl->rtsp, "/auth-setup", "POST", &impl->headers->dict,
+			"application/octet-stream", output, rtsp_auth_setup_reply, impl);
+
+	return res;
+}
+
 static const char *find_attr(char **tokens, const char *key)
 {
 	int i;
@@ -1143,7 +1180,7 @@ static int rtsp_do_auth(struct impl *impl, const struct spa_dict *headers)
 				DEFAULT_USER_NAME, realm, nonce, resp);
 	}
 	else
-		return -EINVAL;
+		goto error;
 
 	pw_properties_setf(impl->headers, "Authorization", "%s %s",
 			tokens[0], auth);
@@ -1168,7 +1205,10 @@ static void rtsp_options_reply(void *data, int status, const struct spa_dict *he
 		rtsp_do_auth(impl, headers);
 		break;
 	case 200:
-		rtsp_do_announce(impl);
+		if (impl->encryption == CRYPTO_AUTH_SETUP)
+			rtsp_do_auth_setup(impl);
+		else
+			rtsp_do_announce(impl);
 		break;
 	}
 }
@@ -1484,56 +1524,59 @@ static void parse_position(struct spa_audio_info_raw *info, const char *val, siz
 	}
 }
 
-static int parse_audio_info(struct impl *impl)
+static void parse_audio_info(const struct pw_properties *props, struct spa_audio_info_raw *info)
 {
-	struct pw_properties *props = impl->stream_props;
-	struct spa_audio_info_raw *info = &impl->info;
 	const char *str;
 
 	spa_zero(*info);
-
 	if ((str = pw_properties_get(props, PW_KEY_AUDIO_FORMAT)) == NULL)
 		str = DEFAULT_FORMAT;
 	info->format = format_from_name(str, strlen(str));
+
+	info->rate = pw_properties_get_uint32(props, PW_KEY_AUDIO_RATE, info->rate);
+	if (info->rate == 0)
+		info->rate = DEFAULT_RATE;
+
+	info->channels = pw_properties_get_uint32(props, PW_KEY_AUDIO_CHANNELS, info->channels);
+	info->channels = SPA_MIN(info->channels, SPA_AUDIO_MAX_CHANNELS);
+	if ((str = pw_properties_get(props, SPA_KEY_AUDIO_POSITION)) != NULL)
+		parse_position(info, str, strlen(str));
+	if (info->channels == 0)
+		parse_position(info, DEFAULT_POSITION, strlen(DEFAULT_POSITION));
+}
+
+static int calc_frame_size(struct spa_audio_info_raw *info)
+{
+	int res = info->channels;
 	switch (info->format) {
-	case SPA_AUDIO_FORMAT_S8:
 	case SPA_AUDIO_FORMAT_U8:
-		impl->frame_size = 1;
-		break;
+	case SPA_AUDIO_FORMAT_S8:
+	case SPA_AUDIO_FORMAT_ALAW:
+	case SPA_AUDIO_FORMAT_ULAW:
+		return res;
 	case SPA_AUDIO_FORMAT_S16:
-		impl->frame_size = 2;
-		break;
+	case SPA_AUDIO_FORMAT_S16_OE:
+	case SPA_AUDIO_FORMAT_U16:
+		return res * 2;
 	case SPA_AUDIO_FORMAT_S24:
-		impl->frame_size = 3;
-		break;
+	case SPA_AUDIO_FORMAT_S24_OE:
+	case SPA_AUDIO_FORMAT_U24:
+		return res * 3;
 	case SPA_AUDIO_FORMAT_S24_32:
+	case SPA_AUDIO_FORMAT_S24_32_OE:
 	case SPA_AUDIO_FORMAT_S32:
+	case SPA_AUDIO_FORMAT_S32_OE:
+	case SPA_AUDIO_FORMAT_U32:
+	case SPA_AUDIO_FORMAT_U32_OE:
 	case SPA_AUDIO_FORMAT_F32:
-		impl->frame_size = 4;
-		break;
+	case SPA_AUDIO_FORMAT_F32_OE:
+		return res * 4;
 	case SPA_AUDIO_FORMAT_F64:
-		impl->frame_size = 8;
-		break;
+	case SPA_AUDIO_FORMAT_F64_OE:
+		return res * 8;
 	default:
-		pw_log_error("unsupported format '%s'", str);
-		return -EINVAL;
-	}
-	info->rate = pw_properties_get_uint32(props, PW_KEY_AUDIO_RATE, DEFAULT_RATE);
-	if (info->rate == 0) {
-		pw_log_error("invalid rate '%s'", str);
-		return -EINVAL;
-	}
-	info->channels = pw_properties_get_uint32(props, PW_KEY_AUDIO_CHANNELS, DEFAULT_CHANNELS);
-	if ((str = pw_properties_get(props, SPA_KEY_AUDIO_POSITION)) == NULL)
-		str = DEFAULT_POSITION;
-	parse_position(info, str, strlen(str));
-	if (info->channels == 0) {
-		pw_log_error("invalid channels '%s'", str);
-		return -EINVAL;
+		return 0;
 	}
-	impl->frame_size *= info->channels;
-
-	return 0;
 }
 
 static void copy_props(struct impl *impl, struct pw_properties *props, const char *key)
@@ -1617,8 +1660,13 @@ 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);
 
-	if ((res = parse_audio_info(impl)) < 0) {
-		pw_log_error( "can't parse audio format");
+	parse_audio_info(impl->stream_props, &impl->info);
+
+	impl->frame_size = calc_frame_size(&impl->info);
+	if (impl->frame_size == 0) {
+		pw_log_error("unsupported audio format:%d channels:%d",
+				impl->info.format, impl->info.channels);
+		res = -EINVAL;
 		goto error;
 	}
 
@@ -1630,6 +1678,7 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
 		impl->protocol = PROTO_TCP;
 	else {
 		pw_log_error( "can't handle transport %s", str);
+		res = -EINVAL;
 		goto error;
 	}
 
@@ -1639,8 +1688,11 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
 		impl->encryption = CRYPTO_NONE;
 	else if (spa_streq(str, "RSA"))
 		impl->encryption = CRYPTO_RSA;
+	else if (spa_streq(str, "auth_setup"))
+		impl->encryption = CRYPTO_AUTH_SETUP;
 	else {
 		pw_log_error( "can't handle encryption type %s", str);
+		res = -EINVAL;
 		goto error;
 	}
 
@@ -1650,6 +1702,7 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
 		impl->codec = CODEC_PCM;
 	else {
 		pw_log_error( "can't handle codec type %s", str);
+		res = -EINVAL;
 		goto error;
 	}
 	str = pw_properties_get(props, "raop.password");
diff --git a/src/modules/module-raop/rtsp-client.c b/src/modules/module-raop/rtsp-client.c
index c241bf40d04b4951cc17da983cc6a0c8c7e12a97..792b441908b8adaee955f0ed5783a4801f514af4 100644
--- a/src/modules/module-raop/rtsp-client.c
+++ b/src/modules/module-raop/rtsp-client.c
@@ -273,12 +273,30 @@ static int process_input(struct pw_rtsp_client *client)
 			int cseq;
 			struct message *msg;
 			const struct spa_dict_item *it;
+			const char *content_type;
+			unsigned int content_length;
 
 			spa_dict_for_each(it, &client->headers->dict)
 				pw_log_info(" %s: %s", it->key, it->value);
 
 			cseq = pw_properties_get_int32(client->headers, "CSeq", 0);
-
+			content_type = pw_properties_get(client->headers, "Content-Type");
+			if (content_type != NULL && strcmp(content_type, "application/octet-stream") == 0) {
+				pw_log_info("binary response received");
+				content_length = pw_properties_get_uint64(client->headers, "Content-Length", 0);
+				char content_buf[content_length];
+				res = read(client->source->fd, content_buf, content_length);
+				pw_log_debug("read %d bytes", res);
+				if (res == 0)
+					return -EPIPE;
+				if (res < 0) {
+					res = -errno;
+					if (res != -EAGAIN && res != -EWOULDBLOCK)
+						return res;
+					return 0;
+				}
+				pw_properties_set(client->headers, "body", content_buf);
+			}
 			if ((msg = find_pending(client, cseq)) != NULL) {
 				msg->reply(msg->user_data, client->status, &client->headers->dict);
 				spa_list_remove(&msg->link);
@@ -466,7 +484,7 @@ int pw_rtsp_client_disconnect(struct pw_rtsp_client *client)
 	return 0;
 }
 
-int pw_rtsp_client_send(struct pw_rtsp_client *client,
+int pw_rtsp_client_url_send(struct pw_rtsp_client *client, const char *url,
 		const char *cmd, const struct spa_dict *headers,
 		const char *content_type, const char *content,
 		void (*reply) (void *user_data, int status, const struct spa_dict *headers),
@@ -485,7 +503,7 @@ int pw_rtsp_client_send(struct pw_rtsp_client *client,
 
 	cseq = ++client->cseq;
 
-	fprintf(f, "%s %s RTSP/1.0\r\n", cmd, client->url);
+	fprintf(f, "%s %s RTSP/1.0\r\n", cmd, url);
 	fprintf(f, "CSeq: %d\r\n", cseq);
 
 	if (headers != NULL) {
@@ -519,3 +537,12 @@ int pw_rtsp_client_send(struct pw_rtsp_client *client,
         }
 	return 0;
 }
+
+int pw_rtsp_client_send(struct pw_rtsp_client *client,
+		const char *cmd, const struct spa_dict *headers,
+		const char *content_type, const char *content,
+		void (*reply) (void *user_data, int status, const struct spa_dict *headers),
+		void *user_data)
+{
+	return pw_rtsp_client_url_send(client, client->url, cmd, headers, content_type, content, reply, user_data);
+}
diff --git a/src/modules/module-raop/rtsp-client.h b/src/modules/module-raop/rtsp-client.h
index 4588eb8303d7be6a197c2735688587d737703318..1ff13ee59c089d42233f6d4c6111c491a8d95e80 100644
--- a/src/modules/module-raop/rtsp-client.h
+++ b/src/modules/module-raop/rtsp-client.h
@@ -71,6 +71,12 @@ int pw_rtsp_client_disconnect(struct pw_rtsp_client *client);
 int pw_rtsp_client_get_local_ip(struct pw_rtsp_client *client,
 		int *version, char *ip, size_t len);
 
+int pw_rtsp_client_url_send(struct pw_rtsp_client *client, const char *url,
+		const char *cmd, const struct spa_dict *headers,
+		const char *content_type, const char *content,
+		void (*reply) (void *user_data, int status, const struct spa_dict *headers),
+		void *user_data);
+
 int pw_rtsp_client_send(struct pw_rtsp_client *client,
 		const char *cmd, const struct spa_dict *headers,
 		const char *content_type, const char *content,
diff --git a/src/modules/module-roc-sink.c b/src/modules/module-roc-sink.c
index e6c8b8e4e7f8b974e180eb81d1e09ecf2691a71f..1ba21ecb372e357940d49ef2b46f7638976d2d09 100644
--- a/src/modules/module-roc-sink.c
+++ b/src/modules/module-roc-sink.c
@@ -316,7 +316,7 @@ static int roc_sink_setup(struct module_roc_sink_data *data)
 	/* Fixed to be the same as ROC sender config above */
 	info.rate = 44100;
 	info.channels = 2;
-	info.format = SPA_AUDIO_FORMAT_F32_LE;
+	info.format = SPA_AUDIO_FORMAT_F32;
 	info.position[0] = SPA_AUDIO_CHANNEL_FL;
 	info.position[1] = SPA_AUDIO_CHANNEL_FR;
 
diff --git a/src/modules/module-roc-source.c b/src/modules/module-roc-source.c
index 2d7a676bbf9adf7a7083321c2ebd970dcaf3c5a0..8dc5761b6e2f487314b42a021771edca7cf1a13e 100644
--- a/src/modules/module-roc-source.c
+++ b/src/modules/module-roc-source.c
@@ -337,7 +337,7 @@ static int roc_source_setup(struct module_roc_source_data *data)
 	/* Fixed to be the same as ROC receiver config above */
 	info.rate = 44100;
 	info.channels = 2;
-	info.format = SPA_AUDIO_FORMAT_F32_LE;
+	info.format = SPA_AUDIO_FORMAT_F32;
 	info.position[0] = SPA_AUDIO_CHANNEL_FL;
 	info.position[1] = SPA_AUDIO_CHANNEL_FR;
 	data->stride = info.channels * sizeof(float);
diff --git a/src/modules/module-rt.c b/src/modules/module-rt.c
index cc87c5f678871de8fe60a4d19f28677dde4e6052..0ee3463d29dc4c2701e78d5b04a7dead0858b0c3 100644
--- a/src/modules/module-rt.c
+++ b/src/modules/module-rt.c
@@ -52,7 +52,7 @@
 #include <stdio.h>
 #include <errno.h>
 #include <sys/stat.h>
-#ifdef __FreeBSD__
+#if defined(__FreeBSD__) || defined(__MidnightBSD__)
 #include <sys/thr.h>
 #endif
 #include <fcntl.h>
@@ -153,6 +153,11 @@ static const struct spa_dict_item module_props[] = {
 #ifdef HAVE_DBUS
 #define RTKIT_SERVICE_NAME "org.freedesktop.RealtimeKit1"
 #define RTKIT_OBJECT_PATH "/org/freedesktop/RealtimeKit1"
+#define RTKIT_INTERFACE "org.freedesktop.RealtimeKit1"
+
+#define XDG_PORTAL_SERVICE_NAME "org.freedesktop.portal.Desktop"
+#define XDG_PORTAL_OBJECT_PATH "/org/freedesktop/portal/desktop"
+#define XDG_PORTAL_INTERFACE "org.freedesktop.portal.Realtime"
 
 /** \cond */
 struct pw_rtkit_bus {
@@ -184,7 +189,11 @@ struct impl {
 
 #ifdef HAVE_DBUS
 	bool use_rtkit;
-	struct pw_rtkit_bus *system_bus;
+	/* For D-Bus. These are const static. */
+	const char* service_name;
+	const char* object_path;
+	const char* interface;
+	struct pw_rtkit_bus *rtkit_bus;
 
 	/* These are only for the RTKit implementation to fill in the `thread`
 	 * struct. Since there's barely any overhead here we'll do this
@@ -205,7 +214,7 @@ static pid_t _gettid(void)
 	return (pid_t) gettid();
 #elif defined(__linux__)
 	return syscall(SYS_gettid);
-#elif defined(__FreeBSD__)
+#elif defined(__FreeBSD__) || defined(__MidnightBSD__)
 	long pid;
 	thr_self(&pid);
 	return (pid_t)pid;
@@ -215,7 +224,7 @@ static pid_t _gettid(void)
 }
 
 #ifdef HAVE_DBUS
-struct pw_rtkit_bus *pw_rtkit_bus_get_system(void)
+struct pw_rtkit_bus *pw_rtkit_bus_get(DBusBusType bus_type)
 {
 	struct pw_rtkit_bus *bus;
 	DBusError error;
@@ -231,7 +240,7 @@ struct pw_rtkit_bus *pw_rtkit_bus_get_system(void)
 	if (bus == NULL)
 		return NULL;
 
-	bus->bus = dbus_bus_get_private(DBUS_BUS_SYSTEM, &error);
+	bus->bus = dbus_bus_get_private(bus_type, &error);
 	if (bus->bus == NULL)
 		goto error;
 
@@ -241,12 +250,41 @@ struct pw_rtkit_bus *pw_rtkit_bus_get_system(void)
 
 error:
 	free(bus);
-	pw_log_error("Failed to connect to system bus: %s", error.message);
+	pw_log_error("Failed to connect to %s bus: %s",
+		     bus_type == DBUS_BUS_SYSTEM ? "system" : "session", error.message);
 	dbus_error_free(&error);
 	errno = ECONNREFUSED;
 	return NULL;
 }
 
+struct pw_rtkit_bus *pw_rtkit_bus_get_system(void)
+{
+	return pw_rtkit_bus_get(DBUS_BUS_SYSTEM);
+}
+
+struct pw_rtkit_bus *pw_rtkit_bus_get_session(void)
+{
+	return pw_rtkit_bus_get(DBUS_BUS_SESSION);
+}
+
+bool pw_rtkit_check_xdg_portal(struct pw_rtkit_bus *system_bus)
+{
+	DBusError error;
+        bool ret = true;
+
+	dbus_error_init(&error);
+
+	if (!dbus_bus_name_has_owner(system_bus->bus, XDG_PORTAL_SERVICE_NAME, &error)) {
+		pw_log_warn("Can't find xdg-portal: %s", error.name);
+		ret = false;
+		goto finish;
+	}
+finish:
+	dbus_error_free(&error);
+
+	return ret;
+}
+
 void pw_rtkit_bus_free(struct pw_rtkit_bus *system_bus)
 {
 	dbus_connection_close(system_bus->bus);
@@ -270,7 +308,7 @@ static int translate_error(const char *name)
 	return -EIO;
 }
 
-static long long rtkit_get_int_property(struct pw_rtkit_bus *connection, const char *propname,
+static long long rtkit_get_int_property(struct impl *impl, const char *propname,
 					long long *propval)
 {
 	DBusMessage *m = NULL, *r = NULL;
@@ -280,19 +318,19 @@ static long long rtkit_get_int_property(struct pw_rtkit_bus *connection, const c
 	DBusError error;
 	int current_type;
 	long long ret;
-	const char *interfacestr = "org.freedesktop.RealtimeKit1";
+	struct pw_rtkit_bus *connection = impl->rtkit_bus;
 
 	dbus_error_init(&error);
 
-	if (!(m = dbus_message_new_method_call(RTKIT_SERVICE_NAME,
-					       RTKIT_OBJECT_PATH,
+	if (!(m = dbus_message_new_method_call(impl->service_name,
+					       impl->object_path,
 					       "org.freedesktop.DBus.Properties", "Get"))) {
 		ret = -ENOMEM;
 		goto finish;
 	}
 
 	if (!dbus_message_append_args(m,
-				      DBUS_TYPE_STRING, &interfacestr,
+				      DBUS_TYPE_STRING, &impl->interface,
 				      DBUS_TYPE_STRING, &propname, DBUS_TYPE_INVALID)) {
 		ret = -ENOMEM;
 		goto finish;
@@ -349,60 +387,63 @@ finish:
 	return ret;
 }
 
-int pw_rtkit_get_max_realtime_priority(struct pw_rtkit_bus *connection)
+int pw_rtkit_get_max_realtime_priority(struct impl *impl)
 {
 	long long retval;
 	int err;
 
-	err = rtkit_get_int_property(connection, "MaxRealtimePriority", &retval);
+	err = rtkit_get_int_property(impl, "MaxRealtimePriority", &retval);
 	return err < 0 ? err : retval;
 }
 
-int pw_rtkit_get_min_nice_level(struct pw_rtkit_bus *connection, int *min_nice_level)
+int pw_rtkit_get_min_nice_level(struct impl *impl, int *min_nice_level)
 {
 	long long retval;
 	int err;
 
-	err = rtkit_get_int_property(connection, "MinNiceLevel", &retval);
+	err = rtkit_get_int_property(impl, "MinNiceLevel", &retval);
 	if (err >= 0)
 		*min_nice_level = retval;
 	return err;
 }
 
-long long pw_rtkit_get_rttime_usec_max(struct pw_rtkit_bus *connection)
+long long pw_rtkit_get_rttime_usec_max(struct impl *impl)
 {
 	long long retval;
 	int err;
 
-	err = rtkit_get_int_property(connection, "RTTimeUSecMax", &retval);
+	err = rtkit_get_int_property(impl, "RTTimeUSecMax", &retval);
 	return err < 0 ? err : retval;
 }
 
-int pw_rtkit_make_realtime(struct pw_rtkit_bus *connection, pid_t thread, int priority)
+int pw_rtkit_make_realtime(struct impl *impl, pid_t thread, int priority)
 {
 	DBusMessage *m = NULL, *r = NULL;
+	dbus_uint64_t pid;
 	dbus_uint64_t u64;
 	dbus_uint32_t u32;
 	DBusError error;
 	int ret;
+	struct pw_rtkit_bus *connection = impl->rtkit_bus;
 
 	dbus_error_init(&error);
 
 	if (thread == 0)
 		thread = _gettid();
 
-	if (!(m = dbus_message_new_method_call(RTKIT_SERVICE_NAME,
-					       RTKIT_OBJECT_PATH,
-					       "org.freedesktop.RealtimeKit1",
-					       "MakeThreadRealtime"))) {
+	if (!(m = dbus_message_new_method_call(impl->service_name,
+					       impl->object_path, impl->interface,
+					       "MakeThreadRealtimeWithPID"))) {
 		ret = -ENOMEM;
 		goto finish;
 	}
 
+	pid = (dbus_uint64_t) getpid();
 	u64 = (dbus_uint64_t) thread;
 	u32 = (dbus_uint32_t) priority;
 
 	if (!dbus_message_append_args(m,
+				      DBUS_TYPE_UINT64, &pid,
 				      DBUS_TYPE_UINT64, &u64,
 				      DBUS_TYPE_UINT32, &u32, DBUS_TYPE_INVALID)) {
 		ret = -ENOMEM;
@@ -435,31 +476,34 @@ finish:
 	return ret;
 }
 
-int pw_rtkit_make_high_priority(struct pw_rtkit_bus *connection, pid_t thread, int nice_level)
+int pw_rtkit_make_high_priority(struct impl *impl, pid_t thread, int nice_level)
 {
 	DBusMessage *m = NULL, *r = NULL;
+	dbus_uint64_t pid;
 	dbus_uint64_t u64;
 	dbus_int32_t s32;
 	DBusError error;
 	int ret;
+	struct pw_rtkit_bus *connection = impl->rtkit_bus;
 
 	dbus_error_init(&error);
 
 	if (thread == 0)
 		thread = _gettid();
 
-	if (!(m = dbus_message_new_method_call(RTKIT_SERVICE_NAME,
-					       RTKIT_OBJECT_PATH,
-					       "org.freedesktop.RealtimeKit1",
-					       "MakeThreadHighPriority"))) {
+	if (!(m = dbus_message_new_method_call(impl->service_name,
+					       impl->object_path, impl->interface,
+					       "MakeThreadHighPriorityWithPID"))) {
 		ret = -ENOMEM;
 		goto finish;
 	}
 
+	pid = (dbus_uint64_t) getpid();
 	u64 = (dbus_uint64_t) thread;
 	s32 = (dbus_int32_t) nice_level;
 
 	if (!dbus_message_append_args(m,
+				      DBUS_TYPE_UINT64, &pid,
 				      DBUS_TYPE_UINT64, &u64,
 				      DBUS_TYPE_INT32, &s32, DBUS_TYPE_INVALID)) {
 		ret = -ENOMEM;
@@ -502,8 +546,8 @@ static void module_destroy(void *data)
 	spa_hook_remove(&impl->module_listener);
 
 #ifdef HAVE_DBUS
-	if (impl->system_bus)
-		pw_rtkit_bus_free(impl->system_bus);
+	if (impl->rtkit_bus)
+		pw_rtkit_bus_free(impl->rtkit_bus);
 #endif
 
 	free(impl);
@@ -520,9 +564,8 @@ static const struct pw_impl_module_events module_events = {
  */
 static bool check_realtime_privileges(rlim_t priority)
 {
-	int old_policy;
+	int err, old_policy, new_policy = REALTIME_POLICY;
 	struct sched_param old_sched_params;
-	int new_policy = REALTIME_POLICY;
 	struct sched_param new_sched_params;
 
 	/* We could check `RLIMIT_RTPRIO`, but the BSDs generally don't have
@@ -530,8 +573,8 @@ static bool check_realtime_privileges(rlim_t priority)
 	 * scheduling without that rlimit being set such as `CAP_SYS_NICE` or
 	 * running as root. Instead of checking a bunch of preconditions, we
 	 * just try if setting realtime scheduling works or not. */
-	if (pthread_getschedparam(pthread_self(),&old_policy,&old_sched_params) < 0) {
-		pw_log_warn("Failed to check RLIMIT_RTPRIO %m");
+	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;
 	}
 
@@ -561,13 +604,13 @@ static int sched_set_nice(int nice_level)
 		return -errno;
 }
 
-static int set_nice(struct impl *impl, int nice_level)
+static int set_nice(struct impl *impl, int nice_level, bool warn)
 {
 	int res = 0;
 
 #ifdef HAVE_DBUS
 	if (impl->use_rtkit)
-		res = pw_rtkit_make_high_priority(impl->system_bus, 0, nice_level);
+		res = pw_rtkit_make_high_priority(impl, 0, nice_level);
 	else
 		res = sched_set_nice(nice_level);
 #else
@@ -575,13 +618,13 @@ static int set_nice(struct impl *impl, int nice_level)
 #endif
 
 	if (res < 0) {
-		pw_log_warn("could not set nice-level to %d: %s",
-				nice_level, spa_strerror(res));
+		if (warn)
+			pw_log_warn("could not set nice-level to %d: %s",
+					nice_level, spa_strerror(res));
 	} else {
 		pw_log_info("main thread nice level set to %d",
 				nice_level);
 	}
-
 	return res;
 }
 
@@ -597,11 +640,11 @@ static int set_rlimit(struct impl *impl)
 #ifdef HAVE_DBUS
 	if (impl->use_rtkit) {
 		long long rttime;
-		rttime = pw_rtkit_get_rttime_usec_max(impl->system_bus);
+		rttime = pw_rtkit_get_rttime_usec_max(impl);
 		if (rttime >= 0) {
 			if ((rlim_t)rttime < rl.rlim_cur) {
-				pw_log_debug("clamping rt.time.soft from %ld to %lld because of RTKit",
-					     rl.rlim_cur, rttime);
+				pw_log_debug("clamping rt.time.soft from %llu to %lld because of RTKit",
+					     (long long)rl.rlim_cur, rttime);
 			}
 
 			rl.rlim_cur = SPA_MIN(rl.rlim_cur, (rlim_t)rttime);
@@ -742,7 +785,7 @@ static int impl_get_rt_range(void *object, const struct spa_dict *props,
 		if (min)
 			*min = 1;
 		if (max)
-			*max = pw_rtkit_get_max_realtime_priority(impl->system_bus);
+			*max = pw_rtkit_get_max_realtime_priority(impl);
 	} else {
 		if (min)
 			*min = sched_get_priority_min(REALTIME_POLICY);
@@ -783,7 +826,7 @@ static int impl_acquire_rt(void *object, struct spa_thread *thread, int priority
 
 	if (impl->use_rtkit) {
 		pid = impl_gettid(impl, pt);
-		rtprio_limit = pw_rtkit_get_max_realtime_priority(impl->system_bus);
+		rtprio_limit = pw_rtkit_get_max_realtime_priority(impl);
 		if (rtprio_limit >= 0 && rtprio_limit < priority) {
 			pw_log_info("dropping requested priority %d for thread %d down to %d because of RTKit limits", priority, pid, rtprio_limit);
 			priority = rtprio_limit;
@@ -796,7 +839,7 @@ static int impl_acquire_rt(void *object, struct spa_thread *thread, int priority
 			pw_log_debug("SCHED_OTHER|SCHED_RESET_ON_FORK worked.");
 		}
 
-		if ((err = pw_rtkit_make_realtime(impl->system_bus, pid, priority)) < 0) {
+		if ((err = pw_rtkit_make_realtime(impl, pid, priority)) < 0) {
 			pw_log_warn("could not make thread %d realtime using RTKit: %s", pid, spa_strerror(err));
 			return err;
 		}
@@ -864,31 +907,17 @@ static const struct spa_thread_utils_methods impl_thread_utils = {
 
 
 #ifdef HAVE_DBUS
-static int should_use_rtkit(struct impl *impl, struct pw_context *context, bool *use_rtkit)
+static int check_rtkit(struct impl *impl, struct pw_context *context, bool *can_use_rtkit)
 {
 	const struct pw_properties *context_props;
 	const char *str;
 
-	*use_rtkit = true;
+	*can_use_rtkit = true;
 
 	if ((context_props = pw_context_get_properties(context)) != NULL &&
 	    (str = pw_properties_get(context_props, "support.dbus")) != NULL &&
 	    !pw_properties_parse_bool(str))
-		*use_rtkit = false;
-
-	/* If the user has permissions to use regular realtime scheduling, then
-	 * we'll use that instead of RTKit */
-	if (check_realtime_privileges(impl->rt_prio)) {
-		*use_rtkit = false;
-	} else {
-		if (!(*use_rtkit)) {
-			pw_log_warn("neither regular realtime scheduling nor RTKit are available");
-			return -ENOTSUP;
-		}
-
-		/* TODO: Should this be pw_log_warn or pw_log_debug instead? */
-		pw_log_info("could not use realtime scheduling, falling back to using RTKit instead");
-	}
+		*can_use_rtkit = false;
 
 	return 0;
 }
@@ -922,35 +951,68 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
 	impl->rt_time_soft = pw_properties_get_int32(props, "rt.time.soft", DEFAULT_RT_TIME_SOFT);
 	impl->rt_time_hard = pw_properties_get_int32(props, "rt.time.hard", DEFAULT_RT_TIME_HARD);
 
+	bool can_use_rtkit = false, use_rtkit = false;
+
 #ifdef HAVE_DBUS
 	spa_list_init(&impl->threads_list);
 	pthread_mutex_init(&impl->lock, NULL);
 	pthread_cond_init(&impl->cond, NULL);
 
-	if ((res = should_use_rtkit(impl, context, &impl->use_rtkit)) < 0) {
+	if ((res = check_rtkit(impl, context, &can_use_rtkit)) < 0)
 		goto error;
+#endif
+	/* If the user has permissions to use regular realtime scheduling, as well as
+	 * the nice level we want, then we'll use that instead of RTKit */
+	if (!check_realtime_privileges(impl->rt_prio)) {
+		if (!can_use_rtkit) {
+			res = -ENOTSUP;
+			pw_log_warn("regular realtime scheduling not available (RTKit fallback disabled)");
+			goto error;
+		}
+		use_rtkit = true;
 	}
 
+	if (IS_VALID_NICE_LEVEL(impl->nice_level)) {
+		if (set_nice(impl, impl->nice_level, !can_use_rtkit) < 0)
+			use_rtkit = can_use_rtkit;
+	}
+	set_rlimit(impl);
+
+#ifdef HAVE_DBUS
+	impl->use_rtkit = use_rtkit;
 	if (impl->use_rtkit) {
-		impl->system_bus = pw_rtkit_bus_get_system();
-		if (impl->system_bus == NULL) {
-			res = -errno;
-			pw_log_warn("could not get system bus: %m");
-			goto error;
+		/* Checking xdg-desktop-portal. It works fine in all situations. */
+		impl->rtkit_bus = pw_rtkit_bus_get_session();
+		if (impl->rtkit_bus != NULL) {
+			if (pw_rtkit_check_xdg_portal(impl->rtkit_bus)) {
+				impl->service_name = XDG_PORTAL_SERVICE_NAME;
+				impl->object_path = XDG_PORTAL_OBJECT_PATH;
+				impl->interface = XDG_PORTAL_INTERFACE;
+			} else {
+				pw_log_warn("found session bus but no portal");
+				pw_rtkit_bus_free(impl->rtkit_bus);
+				impl->rtkit_bus = NULL;
+			}
 		}
-	}
-#else
-	if (!check_realtime_privileges(impl->rt_prio)) {
-		res = -ENOTSUP;
-		pw_log_warn("regular realtime scheduling not available (RTKit fallback disabled)");
-		goto error;
+		/* Failed to get xdg-desktop-portal, try to use rtkit. */
+		if (impl->rtkit_bus == NULL) {
+			impl->rtkit_bus = pw_rtkit_bus_get_system();
+			if (impl->rtkit_bus != NULL) {
+				impl->service_name = RTKIT_SERVICE_NAME;
+				impl->object_path = RTKIT_OBJECT_PATH;
+				impl->interface = RTKIT_INTERFACE;
+			} else {
+				res = -errno;
+				pw_log_warn("could not get system bus: %m");
+				goto error;
+			}
+		}
+		/* Retry set_nice with rtkit */
+		if (IS_VALID_NICE_LEVEL(impl->nice_level))
+			set_nice(impl, impl->nice_level, true);
 	}
 #endif
 
-	if (IS_VALID_NICE_LEVEL(impl->nice_level))
-		set_nice(impl, impl->nice_level);
-	set_rlimit(impl);
-
 	impl->thread_utils.iface = SPA_INTERFACE_INIT(
 			SPA_TYPE_INTERFACE_ThreadUtils,
 			SPA_VERSION_THREAD_UTILS,
@@ -978,8 +1040,8 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
 
 error:
 #ifdef HAVE_DBUS
-	if (impl->system_bus)
-		pw_rtkit_bus_free(impl->system_bus);
+	if (impl->rtkit_bus)
+		pw_rtkit_bus_free(impl->rtkit_bus);
 #endif
 	free(impl);
 done:
diff --git a/src/pipewire/buffers.c b/src/pipewire/buffers.c
index e0b74eb253ab29ae4516fa78836ed38d6d0247c4..d27b9ee245378315dc5142b36febb432d35f37f4 100644
--- a/src/pipewire/buffers.c
+++ b/src/pipewire/buffers.c
@@ -166,8 +166,8 @@ param_filter(struct pw_buffers *this,
 	uint8_t ibuf[4096];
         struct spa_pod_builder ib = { 0 };
 	struct spa_pod *oparam, *iparam;
-	uint32_t iidx, oidx, num = 0;
-	int in_res = -EIO, out_res = -EIO;
+	uint32_t iidx, oidx;
+	int in_res = -EIO, out_res = -EIO, num = 0;
 
 	for (iidx = 0;;) {
 	        spa_pod_builder_init(&ib, ibuf, sizeof(ibuf));
diff --git a/src/pipewire/conf.c b/src/pipewire/conf.c
index 4e6fc2ca0d7441e67cdeb77180b3481eb0143f8f..fd41a4063852dd031903bfa13771d0b60c950814 100644
--- a/src/pipewire/conf.c
+++ b/src/pipewire/conf.c
@@ -38,9 +38,11 @@
 #ifdef HAVE_PWD_H
 #include <pwd.h>
 #endif
-#ifdef __FreeBSD__
+#if defined(__FreeBSD__) || defined(__MidnightBSD__)
+#ifndef O_PATH
 #define O_PATH 0
 #endif
+#endif
 
 #include <spa/utils/result.h>
 #include <spa/utils/string.h>
@@ -190,6 +192,9 @@ static int get_config_dir(char *path, size_t size, const char *prefix, const cha
 		return -ENOENT;
 	}
 
+	if (pw_check_option("no-config", "true"))
+		goto no_config;
+
 	if ((res = get_envconf_path(path, size, prefix, name)) != 0) {
 		if ((*level)++ == 0)
 			return res;
@@ -198,20 +203,18 @@ static int get_config_dir(char *path, size_t size, const char *prefix, const cha
 
 	if (*level == 0) {
 		(*level)++;
-		if ((res = get_confdata_path(path, size, prefix, name)) != 0)
+		if ((res = get_homeconf_path(path, size, prefix, name)) != 0)
 			return res;
 	}
-	if (pw_check_option("no-config", "true"))
-		return 0;
-
 	if (*level == 1) {
 		(*level)++;
 		if ((res = get_configdir_path(path, size, prefix, name)) != 0)
 			return res;
 	}
 	if (*level == 2) {
+no_config:
 		(*level)++;
-		if ((res = get_homeconf_path(path, size, prefix, name)) != 0)
+		if ((res = get_confdata_path(path, size, prefix, name)) != 0)
 			return res;
 	}
 	return 0;
@@ -405,12 +408,17 @@ static int conf_load(const char *path, struct pw_properties *conf)
 
 	if (fstat(fd, &sbuf) < 0)
 		goto error_close;
-	if ((data = mmap(NULL, sbuf.st_size, PROT_READ, MAP_PRIVATE, fd, 0)) == MAP_FAILED)
-		goto error_close;
-	close(fd);
 
-	count = pw_properties_update_string(conf, data, sbuf.st_size);
-	munmap(data, sbuf.st_size);
+	if (sbuf.st_size > 0) {
+		if ((data = mmap(NULL, sbuf.st_size, PROT_READ, MAP_PRIVATE, fd, 0)) == MAP_FAILED)
+			goto error_close;
+
+		count = pw_properties_update_string(conf, data, sbuf.st_size);
+		munmap(data, sbuf.st_size);
+	} else {
+		count = 0;
+	}
+	close(fd);
 
 	pw_log_info("%p: loaded config '%s' with %d items", conf, path, count);
 
@@ -423,13 +431,33 @@ error:
 	return -errno;
 }
 
+static bool check_override(struct pw_properties *conf, const char *name, int level)
+{
+	const struct spa_dict_item *it;
+
+	spa_dict_for_each(it, &conf->dict) {
+		int lev, idx;
+
+		if (!spa_streq(name, it->value))
+			continue;
+		if (sscanf(it->key, "override.%d.%d.config.name", &lev, &idx) != 2)
+			continue;
+		if (lev < level)
+			return false;
+	}
+	return true;
+}
+
 static void add_override(struct pw_properties *conf, struct pw_properties *override,
-		const char *path, int level, int index)
+		const char *path, const char *name, int level, int index)
 {
 	const struct spa_dict_item *it;
 	char key[1024];
+
 	snprintf(key, sizeof(key), "override.%d.%d.config.path", level, index);
 	pw_properties_set(conf, key, path);
+	snprintf(key, sizeof(key), "override.%d.%d.config.name", level, index);
+	pw_properties_set(conf, key, name);
 	spa_dict_for_each(it, &override->dict) {
 		snprintf(key, sizeof(key), "override.%d.%d.%s", level, index, it->key);
 		pw_properties_set(conf, key, it->value);
@@ -488,10 +516,16 @@ int pw_conf_load_conf(const char *prefix, const char *name, struct pw_properties
 			return -errno;
 
 		for (i = 0; i < n; i++) {
-			snprintf(fname, sizeof(fname), "%s/%s", path, entries[i]->d_name);
-			if (conf_load(fname, override) >= 0)
-				add_override(conf, override, fname, level, i);
-			pw_properties_clear(override);
+			const char *name = entries[i]->d_name;
+
+			snprintf(fname, sizeof(fname), "%s/%s", path, name);
+			if (check_override(conf, name, level)) {
+				if (conf_load(fname, override) >= 0)
+					add_override(conf, override, fname, name, level, i);
+				pw_properties_clear(override);
+			} else {
+				pw_log_info("skip override %s with lower priority", fname);
+			}
 			free(entries[i]);
 		}
 		free(entries);
@@ -891,6 +925,75 @@ static int update_props(void *user_data, const char *location, const char *key,
 	return 0;
 }
 
+static int try_load_conf(const char *conf_prefix, const char *conf_name,
+			 struct pw_properties *conf)
+{
+	int res;
+
+	if (conf_name == NULL)
+		return -EINVAL;
+	if (spa_streq(conf_name, "null"))
+		return 0;
+	if ((res = pw_conf_load_conf(conf_prefix, conf_name, conf)) < 0) {
+		bool skip_prefix = conf_prefix == NULL || conf_name[0] == '/';
+		pw_log_warn("can't load config %s%s%s: %s",
+				skip_prefix ? "" : conf_prefix,
+				skip_prefix ? "" : "/",
+				conf_name, spa_strerror(res));
+	}
+	return res;
+}
+
+SPA_EXPORT
+int pw_conf_load_conf_for_context(struct pw_properties *props, struct pw_properties *conf)
+{
+	const char *conf_prefix, *conf_name;
+	int res;
+
+	conf_prefix = getenv("PIPEWIRE_CONFIG_PREFIX");
+	if (conf_prefix == NULL)
+		conf_prefix = pw_properties_get(props, PW_KEY_CONFIG_PREFIX);
+
+	conf_name = getenv("PIPEWIRE_CONFIG_NAME");
+	if ((res = try_load_conf(conf_prefix, conf_name, conf)) < 0) {
+		conf_name = pw_properties_get(props, PW_KEY_CONFIG_NAME);
+		if ((res = try_load_conf(conf_prefix, conf_name, conf)) < 0) {
+			conf_name = "client.conf";
+			if ((res = try_load_conf(conf_prefix, conf_name, conf)) < 0) {
+				pw_log_error("can't load default config %s: %s",
+					conf_name, spa_strerror(res));
+				return res;
+			}
+		}
+	}
+
+	conf_name = pw_properties_get(props, PW_KEY_CONFIG_OVERRIDE_NAME);
+	if (conf_name != NULL) {
+		struct pw_properties *override;
+		const char *path, *name;
+
+		override = pw_properties_new(NULL, NULL);
+		if (override == NULL) {
+			res = -errno;
+			return res;
+		}
+
+		conf_prefix = pw_properties_get(props, PW_KEY_CONFIG_OVERRIDE_PREFIX);
+		if ((res = try_load_conf(conf_prefix, conf_name, override)) < 0) {
+			pw_log_error("can't load default override config %s: %s",
+				conf_name, spa_strerror(res));
+			pw_properties_free (override);
+			return res;
+		}
+		path = pw_properties_get(override, "config.path");
+		name = pw_properties_get(override, "config.name");
+		add_override(conf, override, path, name, 0, 1);
+		pw_properties_free(override);
+	}
+
+	return res;
+}
+
 SPA_EXPORT
 int pw_context_conf_update_props(struct pw_context *context,
 		const char *section, struct pw_properties *props)
diff --git a/src/pipewire/conf.h b/src/pipewire/conf.h
index 459b3b6a8583b2293135078dd0c38153c80ea27a..f5c8ba06b2937e7a26b85bfc46218ff09736bc6c 100644
--- a/src/pipewire/conf.h
+++ b/src/pipewire/conf.h
@@ -33,6 +33,7 @@
  * \{
  */
 
+int pw_conf_load_conf_for_context(struct pw_properties *props, struct pw_properties *conf);
 int pw_conf_load_conf(const char *prefix, const char *name, struct pw_properties *conf);
 int pw_conf_load_state(const char *prefix, const char *name, struct pw_properties *conf);
 int pw_conf_save_state(const char *prefix, const char *name, const struct pw_properties *conf);
diff --git a/src/pipewire/context.c b/src/pipewire/context.c
index a0915698bcb5ae7ab79cfecb52535c1a4b04ce02..f86179121c640b2572bed839007e21c801b8963d 100644
--- a/src/pipewire/context.c
+++ b/src/pipewire/context.c
@@ -101,26 +101,6 @@ static void fill_properties(struct pw_context *context)
 	pw_properties_set(properties, PW_KEY_CORE_NAME, context->core->info.name);
 }
 
-static int try_load_conf(struct pw_context *this, const char *conf_prefix,
-		const char *conf_name, struct pw_properties *conf)
-{
-	int res;
-
-	if (conf_name == NULL)
-		return -EINVAL;
-	if (spa_streq(conf_name, "null"))
-		return 0;
-	if ((res = pw_conf_load_conf(conf_prefix, conf_name, conf)) < 0) {
-		bool skip_prefix = conf_prefix == NULL || conf_name[0] == '/';
-		pw_log_warn("%p: can't load config %s%s%s: %s",
-				this,
-				skip_prefix ? "" : conf_prefix,
-				skip_prefix ? "" : "/",
-				conf_name, spa_strerror(res));
-	}
-	return res;
-}
-
 static int context_set_freewheel(struct pw_context *context, bool freewheel)
 {
 	struct spa_thread *thr;
@@ -211,7 +191,7 @@ struct pw_context *pw_context_new(struct pw_loop *main_loop,
 {
 	struct impl *impl;
 	struct pw_context *this;
-	const char *lib, *str, *conf_prefix, *conf_name;
+	const char *lib, *str;
 	void *dbus_iface = NULL;
 	uint32_t n_support;
 	struct pw_properties *pr, *conf;
@@ -270,23 +250,8 @@ struct pw_context *pw_context_new(struct pw_loop *main_loop,
 		goto error_free;
 	}
 	this->conf = conf;
-
-	conf_prefix = getenv("PIPEWIRE_CONFIG_PREFIX");
-	if (conf_prefix == NULL)
-		conf_prefix = pw_properties_get(properties, PW_KEY_CONFIG_PREFIX);
-
-	conf_name = getenv("PIPEWIRE_CONFIG_NAME");
-	if (try_load_conf(this, conf_prefix, conf_name, conf) < 0) {
-		conf_name = pw_properties_get(properties, PW_KEY_CONFIG_NAME);
-		if (try_load_conf(this, conf_prefix, conf_name, conf) < 0) {
-			conf_name = "client.conf";
-			if ((res = try_load_conf(this, conf_prefix, conf_name, conf)) < 0) {
-				pw_log_error("%p: can't load config %s: %s",
-					this, conf_name, spa_strerror(res));
-				goto error_free;
-			}
-		}
-	}
+	if ((res = pw_conf_load_conf_for_context (properties, conf)) < 0)
+		goto error_free;
 
 	n_support = pw_get_support(this->support, SPA_N_ELEMENTS(this->support) - 6);
 	cpu = spa_support_find(this->support, n_support, SPA_TYPE_INTERFACE_CPU);
@@ -542,6 +507,12 @@ struct pw_loop *pw_context_get_main_loop(struct pw_context *context)
 	return context->main_loop;
 }
 
+SPA_EXPORT
+struct pw_data_loop *pw_context_get_data_loop(struct pw_context *context)
+{
+	return context->data_loop_impl;
+}
+
 SPA_EXPORT
 struct pw_work_queue *pw_context_get_work_queue(struct pw_context *context)
 {
@@ -621,98 +592,6 @@ struct pw_global *pw_context_find_global(struct pw_context *context, uint32_t id
 	return global;
 }
 
-/** Find a port to link with
- *
- * \param context a context
- * \param other_port a port to find a link with
- * \param id the id of a port or PW_ID_ANY
- * \param props extra properties
- * \param n_format_filters number of filters
- * \param format_filters array of format filters
- * \param[out] error an error when something is wrong
- * \return a port that can be used to link to \a otherport or NULL on error
- */
-struct pw_impl_port *pw_context_find_port(struct pw_context *context,
-				  struct pw_impl_port *other_port,
-				  uint32_t id,
-				  struct pw_properties *props,
-				  uint32_t n_format_filters,
-				  struct spa_pod **format_filters,
-				  char **error)
-{
-	struct pw_impl_port *best = NULL;
-	bool have_id;
-	struct pw_impl_node *n;
-
-	have_id = id != PW_ID_ANY;
-
-	pw_log_debug("%p: id:%u", context, id);
-
-	spa_list_for_each(n, &context->node_list, link) {
-		if (n->global == NULL)
-			continue;
-
-		if (other_port->node == n)
-			continue;
-
-		if (!global_can_read(context, n->global))
-			continue;
-
-		pw_log_debug("%p: node id:%d", context, n->global->id);
-
-		if (have_id) {
-			if (n->global->id == id) {
-				pw_log_debug("%p: id:%u matches node %p", context, id, n);
-
-				best =
-				    pw_impl_node_find_port(n,
-						pw_direction_reverse(other_port->direction),
-						PW_ID_ANY);
-				if (best)
-					break;
-			}
-		} else {
-			struct pw_impl_port *p, *pin, *pout;
-			uint8_t buf[4096];
-			struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buf, sizeof(buf));
-			struct spa_pod *dummy;
-
-			p = pw_impl_node_find_port(n,
-					pw_direction_reverse(other_port->direction),
-					PW_ID_ANY);
-			if (p == NULL)
-				continue;
-
-			if (p->direction == PW_DIRECTION_OUTPUT) {
-				pin = other_port;
-				pout = p;
-			} else {
-				pin = p;
-				pout = other_port;
-			}
-
-			if (pw_context_find_format(context,
-						pout,
-						pin,
-						props,
-						n_format_filters,
-						format_filters,
-						&dummy,
-						&b,
-						error) < 0) {
-				free(*error);
-				continue;
-			}
-			best = p;
-			break;
-		}
-	}
-	if (best == NULL) {
-		*error = spa_aprintf("No matching Node found");
-	}
-	return best;
-}
-
 SPA_PRINTF_FUNC(7, 8) int pw_context_debug_port_params(struct pw_context *this,
 		struct spa_node *node, enum spa_direction direction,
 		uint32_t port_id, uint32_t id, int err, const char *debug, ...)
@@ -1113,7 +992,7 @@ static bool rates_contains(uint32_t *rates, uint32_t n_rates, uint32_t rate)
  *    the desired final value and activate the followers and then the driver.
  *
  * A complete graph evaluation is performed for each change that is made to the
- * graph, such as making/destroting links, adding/removing nodes, property changes such
+ * graph, such as making/destroying links, adding/removing nodes, property changes such
  * as quantum/rate changes or metadata changes.
  */
 int pw_context_recalc_graph(struct pw_context *context, const char *reason)
@@ -1376,6 +1255,9 @@ again:
 			 * panding change. Apply the change to the position now so
 			 * that we have the right values when we change the node
 			 * states of the driver and followers to RUNNING below */
+			pw_log_debug("%p: apply duration:%"PRIu64" rate:%u/%u", context,
+					n->current_quantum, n->current_rate.num,
+					n->current_rate.denom);
 			n->rt.position->clock.duration = n->current_quantum;
 			n->rt.position->clock.rate = n->current_rate;
 			n->current_pending = false;
diff --git a/src/pipewire/context.h b/src/pipewire/context.h
index 31292c46357b83bcf1b0178c867e277a1a6d24ed..fe658a6ce56c73d160249a16d11e6ea623129a82 100644
--- a/src/pipewire/context.h
+++ b/src/pipewire/context.h
@@ -136,6 +136,9 @@ const struct spa_support *pw_context_get_support(struct pw_context *context, uin
 /** get the context main loop */
 struct pw_loop *pw_context_get_main_loop(struct pw_context *context);
 
+/** get the context data loop. Since 0.3.56 */
+struct pw_data_loop *pw_context_get_data_loop(struct pw_context *context);
+
 /** Get the work queue from the context: Since 0.3.26 */
 struct pw_work_queue *pw_context_get_work_queue(struct pw_context *context);
 
diff --git a/src/pipewire/data-loop.c b/src/pipewire/data-loop.c
index 47ea8d3fde14cf0a2847d889ac6b315a98b86bde..4f4a2bc645a8807cc49b434a7235fc39791fb1fb 100644
--- a/src/pipewire/data-loop.c
+++ b/src/pipewire/data-loop.c
@@ -40,11 +40,11 @@ int pw_data_loop_wait(struct pw_data_loop *this, int timeout)
 	int res;
 
 	while (true) {
-		if (!this->running) {
+		if (SPA_UNLIKELY(!this->running)) {
 			res = -ECANCELED;
 			break;
 		}
-		if ((res = pw_loop_iterate(this->loop, timeout)) < 0) {
+		if (SPA_UNLIKELY((res = pw_loop_iterate(this->loop, timeout)) < 0)) {
 			if (res == -EINTR)
 				continue;
 		}
@@ -77,8 +77,8 @@ static void *do_loop(void *user_data)
 
 	pthread_cleanup_push(thread_cleanup, this);
 
-	while (this->running) {
-		if ((res = pw_loop_iterate(this->loop, -1)) < 0) {
+	while (SPA_LIKELY(this->running)) {
+		if (SPA_UNLIKELY((res = pw_loop_iterate(this->loop, -1)) < 0)) {
 			if (res == -EINTR)
 				continue;
 			pw_log_error("%p: iterate error %d (%s)",
@@ -274,12 +274,7 @@ int pw_data_loop_invoke(struct pw_data_loop *loop,
 		spa_invoke_func_t func, uint32_t seq, const void *data, size_t size,
 		bool block, void *user_data)
 {
-	int res;
-	if (loop->running)
-		res = pw_loop_invoke(loop->loop, func, seq, data, size, block, user_data);
-	else
-		res = func(loop->loop->loop, false, seq, data, size, user_data);
-	return res;
+	return pw_loop_invoke(loop->loop, func, seq, data, size, block, user_data);
 }
 
 /** Set a thread utils implementation.
diff --git a/src/pipewire/impl-link.c b/src/pipewire/impl-link.c
index 095f1eabcde99540d28cd8433ccfabcce5bd830c..26f817d25908bfba23ea8b553a285e6631d0d514 100644
--- a/src/pipewire/impl-link.c
+++ b/src/pipewire/impl-link.c
@@ -281,8 +281,10 @@ static int do_negotiate(struct pw_impl_link *this)
 	/* find a common format for the ports */
 	if ((res = pw_context_find_format(context,
 					output, input, NULL, 0, NULL,
-					&format, &b, &error)) < 0)
+					&format, &b, &error)) < 0) {
+		format = NULL;
 		goto error;
+	}
 
 	format = spa_pod_copy(format);
 	spa_pod_fixate(format);
@@ -1262,7 +1264,7 @@ struct pw_impl_link *pw_context_create_link(struct pw_context *context,
 		impl->inode = input_node;
 	}
 
-	this->rt.target.signal = impl->inode->rt.target.signal;
+	this->rt.target.signal_func = impl->inode->rt.target.signal_func;
 	this->rt.target.data = impl->inode->rt.target.data;
 
 	pw_log_debug("%p: constructed out:%p:%d.%d -> in:%p:%d.%d", impl,
diff --git a/src/pipewire/impl-node.c b/src/pipewire/impl-node.c
index e654b979d83da7a22fd0f1f787f085c85e6e1cb0..55851a68ef40cd77fda5bd2d55ffd3405a7782a5 100644
--- a/src/pipewire/impl-node.c
+++ b/src/pipewire/impl-node.c
@@ -1047,7 +1047,7 @@ static inline int resume_node(struct pw_impl_node *this, int status)
 		if (pw_node_activation_state_dec(state, 1)) {
 			a->status = PW_NODE_ACTIVATION_TRIGGERED;
 			a->signal_time = nsec;
-			t->signal(t->data);
+			t->signal_func(t->data);
 		}
 	}
 	return 0;
@@ -1145,7 +1145,7 @@ static void node_on_fd_events(struct spa_source *source)
 				this->name, this->info.id, cmd - 1);
 
 		pw_log_trace_fp("%p: got process", this);
-		this->rt.target.signal(this->rt.target.data);
+		this->rt.target.signal_func(this->rt.target.data);
 	}
 }
 
@@ -1262,9 +1262,9 @@ struct pw_impl_node *pw_context_create_node(struct pw_context *context,
 	this->rt.activation = this->activation->map->ptr;
 	this->rt.target.activation = this->rt.activation;
 	this->rt.target.node = this;
-	this->rt.target.signal = process_node;
+	this->rt.target.signal_func = process_node;
 	this->rt.target.data = this;
-	this->rt.driver_target.signal = process_node;
+	this->rt.driver_target.signal_func = process_node;
 
 	reset_position(this, &this->rt.activation->position);
 	this->rt.activation->sync_timeout = DEFAULT_SYNC_TIMEOUT;
@@ -1613,7 +1613,7 @@ static int node_ready(void *data, int status)
 						state->pending, state->required);
 				dump_states(node);
 			}
-			node->rt.target.signal(node->rt.target.data);
+			node->rt.target.signal_func(node->rt.target.data);
 		}
 
 		if (node->current_pending) {
diff --git a/src/pipewire/impl-port.c b/src/pipewire/impl-port.c
index 081651bdd43cd997351a6287c1c452089eb2fa74..8b0cb38b02f0df565faae6005a5b5d530b5ed0d0 100644
--- a/src/pipewire/impl-port.c
+++ b/src/pipewire/impl-port.c
@@ -209,6 +209,7 @@ SPA_EXPORT
 int pw_impl_port_init_mix(struct pw_impl_port *port, struct pw_impl_port_mix *mix)
 {
 	uint32_t port_id;
+	struct pw_impl_node *node = port->node;
 	int res = 0;
 
 	port_id = pw_map_insert_new(&port->mix_port_map, mix);
@@ -252,6 +253,13 @@ int pw_impl_port_init_mix(struct pw_impl_port *port, struct pw_impl_port_mix *mi
 			port->n_mix, port->port_id, mix->port.port_id,
 			mix->io, spa_strerror(res));
 
+	if (port->n_mix == 1) {
+		pw_log_debug("%p: setting port io", port);
+		spa_node_port_set_io(node->node,
+				     port->direction, port->port_id,
+				     SPA_IO_Buffers,
+				     &port->rt.io, sizeof(port->rt.io));
+	}
 	return res;
 
 error_remove_port:
@@ -266,6 +274,7 @@ int pw_impl_port_release_mix(struct pw_impl_port *port, struct pw_impl_port_mix
 {
 	int res = 0;
 	uint32_t port_id = mix->port.port_id;
+	struct pw_impl_node *node = port->node;
 
 	pw_map_remove(&port->mix_port_map, port_id);
 	spa_list_remove(&mix->link);
@@ -280,6 +289,13 @@ int pw_impl_port_release_mix(struct pw_impl_port *port, struct pw_impl_port_mix
 	pw_log_debug("%p: release mix %d %d.%d", port,
 			port->n_mix, port->port_id, mix->port.port_id);
 
+	if (port->n_mix == 0) {
+		pw_log_debug("%p: clearing port io", port);
+		spa_node_port_set_io(node->node,
+				     port->direction, port->port_id,
+				     SPA_IO_Buffers,
+				     NULL, sizeof(port->rt.io));
+	}
 	return res;
 }
 
@@ -1025,12 +1041,7 @@ int pw_impl_port_add(struct pw_impl_port *port, struct pw_impl_node *node)
 	if (control) {
 		pw_log_debug("%p: setting node control", port);
 	} else {
-		pw_log_debug("%p: setting node io", port);
-		spa_node_port_set_io(node->node,
-				     port->direction, port->port_id,
-				     SPA_IO_Buffers,
-				     &port->rt.io, sizeof(port->rt.io));
-
+		pw_log_debug("%p: setting mixer io", port);
 		spa_node_port_set_io(port->mix,
 			     pw_direction_reverse(port->direction), 0,
 			     SPA_IO_Buffers,
diff --git a/src/pipewire/keys.h b/src/pipewire/keys.h
index 55c37b0f128d79d868b4a92c97c2f3fbd0a8a297..3d1feded42cd62e35d87c634d746d2ec8dbe3e40 100644
--- a/src/pipewire/keys.h
+++ b/src/pipewire/keys.h
@@ -76,6 +76,8 @@ extern "C" {
 /* config */
 #define PW_KEY_CONFIG_PREFIX		"config.prefix"		/**< a config prefix directory */
 #define PW_KEY_CONFIG_NAME		"config.name"		/**< a config file name */
+#define PW_KEY_CONFIG_OVERRIDE_PREFIX	"config.override.prefix"	/**< a config override prefix directory */
+#define PW_KEY_CONFIG_OVERRIDE_NAME	"config.override.name"	/**< a config override file name */
 
 /* context */
 #define PW_KEY_CONTEXT_PROFILE_MODULES	"context.profile.modules"	/**< a context profile for modules, deprecated */
@@ -170,7 +172,10 @@ extern "C" {
 #define PW_KEY_NODE_FORCE_RATE		"node.force-rate"	/**< force a rate while the node is
 								  *  active */
 
-#define PW_KEY_NODE_DONT_RECONNECT	"node.dont-reconnect"	/**< don't reconnect this node */
+#define PW_KEY_NODE_DONT_RECONNECT	"node.dont-reconnect"	/**< don't reconnect this node. The node is
+								  *  initially linked to node.target or
+								  *  target.object or the default node. If the
+								  *  targets is removed, the node is destroyed */
 #define PW_KEY_NODE_ALWAYS_PROCESS	"node.always-process"	/**< process even when unlinked */
 #define PW_KEY_NODE_WANT_DRIVER		"node.want-driver"	/**< the node wants to be grouped with a driver
 								  *  node in order to schedule the graph. */
@@ -249,6 +254,7 @@ extern "C" {
 								  *  "isa", "pci", "usb", "firewire",
 								  *  "bluetooth" */
 #define PW_KEY_DEVICE_SUBSYSTEM		"device.subsystem"	/**< device subsystem */
+#define PW_KEY_DEVICE_SYSFS_PATH	"device.sysfs.path"	/**< device sysfs path */
 #define PW_KEY_DEVICE_ICON		"device.icon"		/**< icon for the device. A base64 blob
 								  *  containing PNG image data */
 #define PW_KEY_DEVICE_ICON_NAME		"device.icon-name"	/**< an XDG icon name for the device.
diff --git a/src/pipewire/mem.c b/src/pipewire/mem.c
index 42ac7c104db96d03f4747cacba3a75ca0cc191bc..ae9e1e46ce59c6a12cd03c92266da8e266a857d2 100644
--- a/src/pipewire/mem.c
+++ b/src/pipewire/mem.c
@@ -44,7 +44,7 @@
 PW_LOG_TOPIC_EXTERN(log_mem);
 #define PW_LOG_TOPIC_DEFAULT log_mem
 
-#if !defined(__FreeBSD__) && !defined(HAVE_MEMFD_CREATE)
+#if !defined(__FreeBSD__) && !defined(__MidnightBSD__) && !defined(HAVE_MEMFD_CREATE)
 /*
  * No glibc wrappers exist for memfd_create(2), so provide our own.
  *
@@ -61,7 +61,7 @@ static inline int memfd_create(const char *name, unsigned int flags)
 #define HAVE_MEMFD_CREATE 1
 #endif
 
-#ifdef __FreeBSD__
+#if defined(__FreeBSD__) || defined(__MidnightBSD__)
 #define MAP_LOCKED 0
 #endif
 
@@ -485,13 +485,18 @@ struct pw_memblock * pw_mempool_alloc(struct pw_mempool *pool, enum pw_memblock_
 	spa_list_init(&b->memmaps);
 
 #ifdef HAVE_MEMFD_CREATE
-	b->this.fd = memfd_create("pipewire-memfd", MFD_CLOEXEC | MFD_ALLOW_SEALING);
+	char name[128];
+	snprintf(name, sizeof(name),
+		 "pipewire-memfd:flags=0x%08x,type=%" PRIu32 ",size=%zu",
+		 (unsigned int) flags, type, size);
+
+	b->this.fd = memfd_create(name, MFD_CLOEXEC | MFD_ALLOW_SEALING);
 	if (b->this.fd == -1) {
 		res = -errno;
 		pw_log_error("%p: Failed to create memfd: %m", pool);
 		goto error_free;
 	}
-#elif defined(__FreeBSD__)
+#elif defined(__FreeBSD__) || defined(__MidnightBSD__)
 	b->this.fd = shm_open(SHM_ANON, O_CREAT | O_RDWR | O_CLOEXEC, 0);
 	if (b->this.fd == -1) {
 		res = -errno;
@@ -499,7 +504,11 @@ struct pw_memblock * pw_mempool_alloc(struct pw_mempool *pool, enum pw_memblock_
 		goto error_free;
 	}
 #else
-	char filename[] = "/dev/shm/pipewire-tmpfile.XXXXXX";
+	char filename[128];
+	snprintf(filename, sizeof(filename),
+		 "/dev/shm/pipewire-tmpfile:flags=0x%08x,type=%" PRIu32 ",size=%zu:XXXXXX",
+		 (unsigned int) flags, type, size);
+
 	b->this.fd = mkostemp(filename, O_CLOEXEC);
 	if (b->this.fd == -1) {
 		res = -errno;
diff --git a/src/pipewire/meson.build b/src/pipewire/meson.build
index f96a022bceb320bd793bf5a32b0c8ebb3282ce2c..b19631a9261d96e2bb6737842d13bf188c71de67 100644
--- a/src/pipewire/meson.build
+++ b/src/pipewire/meson.build
@@ -94,7 +94,7 @@ libpipewire_c_args = [
   '-DOLD_MEDIA_SESSION_WORKAROUND=1'
 ]
 
-if build_machine.system() != 'freebsd'
+if host_machine.system() != 'freebsd' and host_machine.system() != 'midnightbsd'
   libpipewire_c_args += [
     '-D_POSIX_C_SOURCE'
   ]
diff --git a/src/pipewire/pipewire.c b/src/pipewire/pipewire.c
index f0edb186781397e9284d30dba27d9dce95271c44..255760c90df8ec5206905def6d341c7379044f57 100644
--- a/src/pipewire/pipewire.c
+++ b/src/pipewire/pipewire.c
@@ -27,7 +27,7 @@
 #include <unistd.h>
 #include <limits.h>
 #include <stdio.h>
-#ifndef __FreeBSD__
+#if !defined(__FreeBSD__) && !defined(__MidnightBSD__)
 #include <sys/prctl.h>
 #endif
 #include <pwd.h>
@@ -746,7 +746,7 @@ static void init_prgname(void)
 	static char name[PATH_MAX];
 
 	spa_memzero(name, sizeof(name));
-#if defined(__linux__) || defined(__FreeBSD_kernel__)
+#if defined(__linux__) || defined(__FreeBSD_kernel__) || defined(__MidnightBSD_kernel__)
 	{
 		if (readlink("/proc/self/exe", name, sizeof(name)-1) > 0) {
 			prgname = strrchr(name, '/') + 1;
@@ -754,7 +754,7 @@ static void init_prgname(void)
 		}
 	}
 #endif
-#if defined __FreeBSD__
+#if defined __FreeBSD__ || defined(__MidnightBSD__)
 	{
 		ssize_t len;
 
@@ -764,7 +764,7 @@ static void init_prgname(void)
 		}
 	}
 #endif
-#ifndef __FreeBSD__
+#if !defined(__FreeBSD__) && !defined(__MidnightBSD__)
 	{
 		if (prctl(PR_GET_NAME, (unsigned long) name, 0, 0, 0) == 0) {
 			prgname = name;
@@ -826,8 +826,7 @@ bool pw_check_option(const char *option, const char *value)
 		return global_support.no_color == spa_atob(value);
 	else if (spa_streq(option, "no-config"))
 		return global_support.no_config == spa_atob(value);
-	else
-		return false;
+	return false;
 }
 
 /** Get the client name
@@ -841,15 +840,11 @@ const char *pw_get_client_name(void)
 	const char *cc;
 	static char cname[256];
 
-	if ((cc = pw_get_application_name()))
+	if ((cc = pw_get_application_name()) || (cc = pw_get_prgname()))
 		return cc;
-	else if ((cc = pw_get_prgname()))
-		return cc;
-	else {
-		if (snprintf(cname, sizeof(cname), "pipewire-pid-%zd", (size_t) getpid()) < 0)
-			return NULL;
-		return cname;
-	}
+	else if (snprintf(cname, sizeof(cname), "pipewire-pid-%zd", (size_t) getpid()) < 0)
+		return NULL;
+	return cname;
 }
 
 /** Reverse the direction */
diff --git a/src/pipewire/private.h b/src/pipewire/private.h
index 52629f4a33a7d221e507c29fc90aa3d2f897801c..144e21bf1a84d7a0c3a4e9e0a44173ba8b74e1fa 100644
--- a/src/pipewire/private.h
+++ b/src/pipewire/private.h
@@ -40,7 +40,7 @@ extern "C" {
 #include <spa/utils/result.h>
 #include <spa/utils/type-info.h>
 
-#ifdef __FreeBSD__
+#if defined(__FreeBSD__) || defined(__MidnightBSD__)
 struct ucred {
 };
 #endif
@@ -573,7 +573,7 @@ struct pw_node_target {
 	struct spa_list link;
 	struct pw_impl_node *node;
 	struct pw_node_activation *activation;
-	int (*signal) (void *data);
+	int (*signal_func) (void *data);
 	void *data;
 	unsigned int active:1;
 };
@@ -1153,16 +1153,6 @@ int pw_context_find_format(struct pw_context *context,
 			struct spa_pod_builder *builder,
 			char **error);
 
-/** Find a ports compatible with \a other_port and the format filters */
-struct pw_impl_port *
-pw_context_find_port(struct pw_context *context,
-		  struct pw_impl_port *other_port,
-		  uint32_t id,
-		  struct pw_properties *props,
-		  uint32_t n_format_filters,
-		  struct spa_pod **format_filters,
-		  char **error);
-
 int pw_context_debug_port_params(struct pw_context *context,
 		struct spa_node *node, enum spa_direction direction,
 		uint32_t port_id, uint32_t id, int err, const char *debug, ...);
diff --git a/src/pipewire/settings.c b/src/pipewire/settings.c
index 92d297ca336433652822bff7f8498b1d1d537cd6..c512e96585c943787457a203b6256f1025f8293f 100644
--- a/src/pipewire/settings.c
+++ b/src/pipewire/settings.c
@@ -41,7 +41,7 @@
 #define NAME "settings"
 
 #define DEFAULT_CLOCK_RATE			48000u
-#define DEFAULT_CLOCK_RATES			"[ 44100 48000 ]"
+#define DEFAULT_CLOCK_RATES			"[ 48000 ]"
 #define DEFAULT_CLOCK_QUANTUM			1024u
 #define DEFAULT_CLOCK_MIN_QUANTUM		32u
 #define DEFAULT_CLOCK_MAX_QUANTUM		2048u
diff --git a/src/pipewire/stream.c b/src/pipewire/stream.c
index 58dc80ab74c648b936bb2f813347f5a2b238f4d1..89c2794e29748bb264a7530b65282c10c0d2083e 100644
--- a/src/pipewire/stream.c
+++ b/src/pipewire/stream.c
@@ -313,7 +313,7 @@ static int update_params(struct stream *impl, uint32_t id,
 }
 
 
-static inline int push_queue(struct stream *stream, struct queue *queue, struct buffer *buffer)
+static inline int queue_push(struct stream *stream, struct queue *queue, struct buffer *buffer)
 {
 	uint32_t index;
 
@@ -330,7 +330,13 @@ static inline int push_queue(struct stream *stream, struct queue *queue, struct
 	return 0;
 }
 
-static inline struct buffer *pop_queue(struct stream *stream, struct queue *queue)
+static inline bool queue_is_empty(struct stream *stream, struct queue *queue)
+{
+	uint32_t index;
+	return spa_ringbuffer_get_read_index(&queue->ring, &index) < 1;
+}
+
+static inline struct buffer *queue_pop(struct stream *stream, struct queue *queue)
 {
 	uint32_t index, id;
 	struct buffer *buffer;
@@ -568,7 +574,7 @@ static inline uint32_t update_requested(struct stream *impl)
 		buffer->this.requested = impl->quantum;
 		res = 1;
 	}
-	pw_log_trace_fp("%p: update buffer:%u size:%u", impl, id, r->size);
+	pw_log_trace_fp("%p: update buffer:%u size:%"PRIu64, impl, id, buffer->this.requested);
 	return res;
 }
 
@@ -788,7 +794,7 @@ static void clear_buffers(struct pw_stream *stream)
 	if (impl->direction == SPA_DIRECTION_INPUT) {
 		struct buffer *b;
 
-		while ((b = pop_queue(impl, &impl->dequeued))) {
+		while ((b = queue_pop(impl, &impl->dequeued))) {
 			if (b->busy)
 				ATOMIC_DEC(b->busy->count);
 		}
@@ -927,7 +933,7 @@ static int impl_port_use_buffers(void *object,
 
 		if (impl->direction == SPA_DIRECTION_OUTPUT) {
 			pw_log_trace("%p: recycle buffer %d", stream, b->id);
-			push_queue(impl, &impl->dequeued, b);
+			queue_push(impl, &impl->dequeued, b);
 		}
 
 		SPA_FLAG_SET(b->flags, BUFFER_FLAG_ADDED);
@@ -945,7 +951,7 @@ static int impl_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffe
 	struct stream *d = object;
 	pw_log_trace("%p: recycle buffer %d", d, buffer_id);
 	if (buffer_id < d->n_buffers)
-		push_queue(d, &d->queued, &d->buffers[buffer_id]);
+		queue_push(d, &d->queued, &d->buffers[buffer_id]);
 	return 0;
 }
 
@@ -984,7 +990,7 @@ static int impl_node_process_input(void *object)
 	if (io->status == SPA_STATUS_HAVE_DATA &&
 	    (b = get_buffer(stream, io->buffer_id)) != NULL) {
 		/* push new buffer */
-		if (push_queue(impl, &impl->dequeued, b) == 0) {
+		if (queue_push(impl, &impl->dequeued, b) == 0) {
 			copy_position(impl, impl->dequeued.incount);
 			if (b->busy)
 				ATOMIC_INC(b->busy->count);
@@ -993,7 +999,7 @@ static int impl_node_process_input(void *object)
 	}
 	if (io->status != SPA_STATUS_NEED_DATA) {
 		/* pop buffer to recycle */
-		if ((b = pop_queue(impl, &impl->queued))) {
+		if ((b = queue_pop(impl, &impl->queued))) {
 			pw_log_trace_fp("%p: recycle buffer %d", stream, b->id);
 		} else if (io->status == -EPIPE)
 			return io->status;
@@ -1013,28 +1019,36 @@ static int impl_node_process_output(void *object)
 	struct spa_io_buffers *io = impl->io;
 	struct buffer *b;
 	int res;
-	uint32_t index;
-	bool recycled;
+	bool ask_more;
 
 again:
 	pw_log_trace_fp("%p: process out status:%d id:%d", stream,
 			io->status, io->buffer_id);
 
-	recycled = false;
+	ask_more = false;
 	if ((res = io->status) != SPA_STATUS_HAVE_DATA) {
 		/* recycle old buffer */
 		if ((b = get_buffer(stream, io->buffer_id)) != NULL) {
 			pw_log_trace_fp("%p: recycle buffer %d", stream, b->id);
-			push_queue(impl, &impl->dequeued, b);
-			recycled = true;
+			queue_push(impl, &impl->dequeued, b);
 		}
 
 		/* pop new buffer */
-		if ((b = pop_queue(impl, &impl->queued)) != NULL) {
+		if ((b = queue_pop(impl, &impl->queued)) != NULL) {
 			impl->drained = false;
 			io->buffer_id = b->id;
 			res = io->status = SPA_STATUS_HAVE_DATA;
 			pw_log_trace_fp("%p: pop %d %p", stream, b->id, io);
+			/* we have a buffer, if we are not rt and don't follow
+			 * any rate matching and there are no more
+			 * buffers queued and there is a buffer to dequeue, ask for
+			 * more buffers so that we have one in the next round.
+			 * If we are using rate matching we need to wait until the
+			 * rate matching node (audioconvert) has been scheduled to
+			 * update the values. */
+			ask_more = !impl->process_rt && impl->rate_match == NULL &&
+				queue_is_empty(impl, &impl->queued) &&
+				!queue_is_empty(impl, &impl->dequeued);
 		} else if (impl->draining || impl->drained) {
 			impl->draining = true;
 			impl->drained = true;
@@ -1045,7 +1059,12 @@ again:
 			io->buffer_id = SPA_ID_INVALID;
 			res = io->status = SPA_STATUS_NEED_DATA;
 			pw_log_trace_fp("%p: no more buffers %p", stream, io);
+			ask_more = true;
 		}
+	} else {
+		ask_more = !impl->process_rt &&
+			queue_is_empty(impl, &impl->queued) &&
+			!queue_is_empty(impl, &impl->dequeued);
 	}
 
 	copy_position(impl, impl->queued.outcount);
@@ -1053,18 +1072,13 @@ again:
 	if (!impl->draining && !impl->driving) {
 		/* we're not draining, not a driver check if we need to get
 		 * more buffers */
-		if (!impl->process_rt && (recycled || res == SPA_STATUS_NEED_DATA)) {
-			/* not realtime and we have a free buffer, trigger process so that we have
-			 * data in the next round. */
-			if (update_requested(impl) > 0)
-				call_process(impl);
-		} else if (res == SPA_STATUS_NEED_DATA) {
-			/* realtime and we don't have a buffer, trigger process and try
-			 * again when there is something in the queue now */
+		if (ask_more) {
 			if (update_requested(impl) > 0)
 				call_process(impl);
-			if (impl->draining ||
-			    spa_ringbuffer_get_read_index(&impl->queued.ring, &index) > 0)
+			/* realtime, we can try again now if there is something.
+			 * non-realtime, we will have to try in the next round */
+			if (impl->process_rt &&
+			    (impl->draining || !queue_is_empty(impl, &impl->queued)))
 				goto again;
 		}
 	}
@@ -1155,7 +1169,7 @@ static int node_event_param(void *object, int seq,
 		struct control *c;
 		const struct spa_pod *type, *pod;
 		uint32_t iid, choice, n_vals, container = SPA_ID_INVALID;
-		float *vals, bool_range[3] = { 1.0, 0.0, 1.0 }, dbl[3];
+		float *vals, bool_range[3] = { 1.0f, 0.0f, 1.0f }, dbl[3];
 
 		if (spa_pod_parse_object(param,
 					SPA_TYPE_OBJECT_PropInfo, NULL,
@@ -1182,8 +1196,6 @@ static int node_event_param(void *object, int seq,
 			return -EINVAL;
 		}
 
-		spa_list_append(&stream->controls, &c->link);
-
 		pod = spa_pod_get_values(type, &n_vals, &choice);
 
 		c->type = SPA_POD_TYPE(pod);
@@ -1204,22 +1216,28 @@ static int node_event_param(void *object, int seq,
 			vals[0] = SPA_POD_VALUE(struct spa_pod_bool, pod);
 			n_vals = 3;
 		}
-		else
+		else {
+			free(c);
 			return -ENOTSUP;
+		}
 
 		c->container = container != SPA_ID_INVALID ? container : c->type;
 
 		switch (choice) {
 		case SPA_CHOICE_None:
-			if (n_vals < 1)
+			if (n_vals < 1) {
+				free(c);
 				return -EINVAL;
+			}
 			c->control.n_values = 1;
 			c->control.max_values = 1;
 			c->control.values[0] = c->control.def = c->control.min = c->control.max = vals[0];
 			break;
 		case SPA_CHOICE_Range:
-			if (n_vals < 3)
+			if (n_vals < 3) {
+				free(c);
 				return -EINVAL;
+			}
 			c->control.n_values = 1;
 			c->control.max_values = 1;
 			c->control.values[0] = vals[0];
@@ -1228,10 +1246,12 @@ static int node_event_param(void *object, int seq,
 			c->control.max = vals[2];
 			break;
 		default:
+			free(c);
 			return -ENOTSUP;
 		}
 
 		c->id = iid;
+		spa_list_append(&stream->controls, &c->link);
 		pw_log_debug("%p: add control %d (%s) container:%d (def:%f min:%f max:%f)",
 				stream, c->id, c->control.name, c->container,
 				c->control.def, c->control.min, c->control.max);
@@ -1241,11 +1261,9 @@ static int node_event_param(void *object, int seq,
 	{
 		struct spa_pod_prop *prop;
 		struct spa_pod_object *obj = (struct spa_pod_object *) param;
-		union {
-			float f;
-			double d;
-			bool b;
-		} value;
+		float value_f;
+		double value_d;
+		bool value_b;
 		float *values;
 		uint32_t i, n_values;
 
@@ -1258,24 +1276,24 @@ static int node_event_param(void *object, int seq,
 
 			switch (c->container) {
 			case SPA_TYPE_Float:
-				if (spa_pod_get_float(&prop->value, &value.f) < 0)
+				if (spa_pod_get_float(&prop->value, &value_f) < 0)
 					continue;
 				n_values = 1;
-				values = &value.f;
+				values = &value_f;
 				break;
 			case SPA_TYPE_Double:
-				if (spa_pod_get_double(&prop->value, &value.d) < 0)
+				if (spa_pod_get_double(&prop->value, &value_d) < 0)
 					continue;
 				n_values = 1;
-				value.f = value.d;
-				values = &value.f;
+				value_f = value_d;
+				values = &value_f;
 				break;
 			case SPA_TYPE_Bool:
-				if (spa_pod_get_bool(&prop->value, &value.b) < 0)
+				if (spa_pod_get_bool(&prop->value, &value_b) < 0)
 					continue;
-				value.f = value.b ? 1.0 : 0.0;
+				value_f = value_b ? 1.0f : 0.0f;
 				n_values = 1;
-				values = &value.f;
+				values = &value_f;
 				break;
 			case SPA_TYPE_Array:
 				if ((values = spa_pod_get_array(&prop->value, &n_values)) == NULL ||
@@ -2223,7 +2241,7 @@ struct pw_buffer *pw_stream_dequeue_buffer(struct pw_stream *stream)
 	struct buffer *b;
 	int res;
 
-	if ((b = pop_queue(impl, &impl->dequeued)) == NULL) {
+	if ((b = queue_pop(impl, &impl->dequeued)) == NULL) {
 		res = -errno;
 		pw_log_trace_fp("%p: no more buffers: %m", stream);
 		errno = -res;
@@ -2234,7 +2252,7 @@ struct pw_buffer *pw_stream_dequeue_buffer(struct pw_stream *stream)
 	if (b->busy && impl->direction == SPA_DIRECTION_OUTPUT) {
 		if (ATOMIC_INC(b->busy->count) > 1) {
 			ATOMIC_DEC(b->busy->count);
-			push_queue(impl, &impl->dequeued, b);
+			queue_push(impl, &impl->dequeued, b);
 			pw_log_trace_fp("%p: buffer busy", stream);
 			errno = EBUSY;
 			return NULL;
@@ -2254,7 +2272,7 @@ int pw_stream_queue_buffer(struct pw_stream *stream, struct pw_buffer *buffer)
 		ATOMIC_DEC(b->busy->count);
 
 	pw_log_trace_fp("%p: queue buffer %d", stream, b->id);
-	if ((res = push_queue(impl, &impl->queued, b)) < 0)
+	if ((res = queue_push(impl, &impl->queued, b)) < 0)
 		return res;
 
 	if (impl->direction == SPA_DIRECTION_OUTPUT &&
@@ -2275,9 +2293,9 @@ do_flush(struct spa_loop *loop,
 
 	pw_log_trace_fp("%p: flush", impl);
 	do {
-		b = pop_queue(impl, &impl->queued);
+		b = queue_pop(impl, &impl->queued);
 		if (b != NULL)
-			push_queue(impl, &impl->dequeued, b);
+			queue_push(impl, &impl->dequeued, b);
 	}
 	while (b);
 
diff --git a/src/pipewire/stream.h b/src/pipewire/stream.h
index 1470404311f8e09981a5de75a79835992638a6c0..ca3aac86cdd6ca154b6d4e6b39d321f318154f18 100644
--- a/src/pipewire/stream.h
+++ b/src/pipewire/stream.h
@@ -161,6 +161,15 @@ extern "C" {
  * \section sec_stream_disconnect Disconnect
  *
  * Use \ref pw_stream_disconnect() to disconnect a stream after use.
+ *
+ * \section sec_stream_configuration Configuration
+ *
+ * \subsection ssec_config_properties Stream Properties
+ *
+ * \subsection ssec_config_rules Stream Rules
+ *
+ * \section sec_stream_environment Environment Variables
+ *
  */
 /** \defgroup pw_stream Stream
  *
@@ -427,9 +436,16 @@ int pw_stream_update_properties(struct pw_stream *stream, const struct spa_dict
 int
 pw_stream_connect(struct pw_stream *stream,		/**< a \ref pw_stream */
 		  enum pw_direction direction,		/**< the stream direction */
-		  uint32_t target_id,			/**< the target object id to connect to or
-							  *  PW_ID_ANY to let the manager
-							  *  select a target. */
+		  uint32_t target_id,			/**< should have the value PW_ID_ANY.
+							  * To select a specific target
+							  * node, specify the
+							  * PW_KEY_OBJECT_SERIAL or the
+							  * PW_KEY_NODE_NAME value of the target
+							  * node in the PW_KEY_TARGET_OBJECT
+							  * property of the stream.
+							  * Specifying target nodes by
+							  * their id is deprecated.
+							  */
 		  enum pw_stream_flags flags,		/**< stream flags */
 		  const struct spa_pod **params,	/**< an array with params. The params
 							  *  should ideally contain supported
diff --git a/src/pipewire/thread.c b/src/pipewire/thread.c
index f699ebf5bbb43b26ecc573bf72074a4486a049c0..72be387bd82f96826346d45df51615d1ef183bad 100644
--- a/src/pipewire/thread.c
+++ b/src/pipewire/thread.c
@@ -62,9 +62,9 @@ error:
 	return NULL;
 }
 
-#ifdef __FreeBSD__
+#if defined(__FreeBSD__) || defined(__MidnightBSD__)
 #include <sys/param.h>
-#if __FreeBSD_version < 1202000
+#if __FreeBSD_version < 1202000 || defined(__MidnightBSD__)
 int pthread_setname_np(pthread_t thread, const char *name)
 {
 	pthread_set_name_np(thread, name);
diff --git a/src/pipewire/utils.c b/src/pipewire/utils.c
index a644049fd2afb2e20c1a3182f357d3531f109522..072f472777842b3e25aaee44887b763093766fcb 100644
--- a/src/pipewire/utils.c
+++ b/src/pipewire/utils.c
@@ -154,6 +154,7 @@ SPA_EXPORT
 ssize_t pw_getrandom(void *buf, size_t buflen, unsigned int flags)
 {
 	ssize_t bytes;
+	int read_errno;
 
 #ifdef HAVE_GETRANDOM
 	bytes = getrandom(buf, buflen, flags);
@@ -165,7 +166,9 @@ ssize_t pw_getrandom(void *buf, size_t buflen, unsigned int flags)
 	if (fd < 0)
 		return -1;
 	bytes = read(fd, buf, buflen);
+	read_errno = errno;
 	close(fd);
+	errno = read_errno;
 	return bytes;
 }
 
diff --git a/src/pipewire/utils.h b/src/pipewire/utils.h
index b320db22f97fafd0ae17d02737e155c06b811c44..c04d6ea136fa851edeedef68e2226ede375173dd 100644
--- a/src/pipewire/utils.h
+++ b/src/pipewire/utils.h
@@ -86,6 +86,7 @@ pw_strip(char *str, const char *whitespace);
 	})
 #endif
 
+SPA_WARN_UNUSED_RESULT
 ssize_t pw_getrandom(void *buf, size_t buflen, unsigned int flags);
 
 void* pw_reallocarray(void *ptr, size_t nmemb, size_t size);
diff --git a/src/tests/meson.build b/src/tests/meson.build
index 3e03a4c7561413e3b96fbe2fdf79cfe23c52fd87..f7c54f2b76a4f836c8a388d700eb2247f0fe280f 100644
--- a/src/tests/meson.build
+++ b/src/tests/meson.build
@@ -3,6 +3,7 @@ test_apps = [
   'test-interfaces',
   # 'test-remote',
   'test-stream',
+  'test-filter',
 ]
 
 foreach a : test_apps
diff --git a/src/tests/test-filter.c b/src/tests/test-filter.c
new file mode 100644
index 0000000000000000000000000000000000000000..93f00377e78cde05aac0ae7cfe2ec17f2cd88f7b
--- /dev/null
+++ b/src/tests/test-filter.c
@@ -0,0 +1,375 @@
+/* PipeWire
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <pipewire/pipewire.h>
+#include <pipewire/main-loop.h>
+#include <pipewire/filter.h>
+
+#include <spa/utils/string.h>
+
+#define TEST_FUNC(a,b,func)	\
+do {				\
+	a.func = b.func;	\
+	spa_assert_se(SPA_PTRDIFF(&a.func, &a) == SPA_PTRDIFF(&b.func, &b)); \
+} while(0)
+
+static void test_abi(void)
+{
+	static const struct {
+		uint32_t version;
+		void (*destroy) (void *data);
+		void (*state_changed) (void *data, enum pw_filter_state old,
+			enum pw_filter_state state, const char *error);
+		void (*io_changed) (void *data, void *port_data, uint32_t id, void *area, uint32_t size);
+		void (*param_changed) (void *data, void *port_data, uint32_t id, const struct spa_pod *param);
+		void (*add_buffer) (void *data, void *port_data, struct pw_buffer *buffer);
+		void (*remove_buffer) (void *data, void *port_data, struct pw_buffer *buffer);
+		void (*process) (void *data, struct spa_io_position *position);
+		void (*drained) (void *data);
+		void (*command) (void *data, const struct spa_command *command);
+	} test = { PW_VERSION_FILTER_EVENTS, NULL };
+
+	struct pw_filter_events ev;
+
+	TEST_FUNC(ev, test, destroy);
+	TEST_FUNC(ev, test, state_changed);
+	TEST_FUNC(ev, test, io_changed);
+	TEST_FUNC(ev, test, param_changed);
+	TEST_FUNC(ev, test, add_buffer);
+	TEST_FUNC(ev, test, remove_buffer);
+	TEST_FUNC(ev, test, process);
+	TEST_FUNC(ev, test, drained);
+	TEST_FUNC(ev, test, command);
+
+	spa_assert_se(PW_VERSION_FILTER_EVENTS == 1);
+	spa_assert_se(sizeof(ev) == sizeof(test));
+
+	spa_assert_se(PW_FILTER_STATE_ERROR == -1);
+	spa_assert_se(PW_FILTER_STATE_UNCONNECTED == 0);
+	spa_assert_se(PW_FILTER_STATE_CONNECTING == 1);
+	spa_assert_se(PW_FILTER_STATE_PAUSED == 2);
+	spa_assert_se(PW_FILTER_STATE_STREAMING == 3);
+
+	spa_assert_se(pw_filter_state_as_string(PW_FILTER_STATE_ERROR) != NULL);
+	spa_assert_se(pw_filter_state_as_string(PW_FILTER_STATE_UNCONNECTED) != NULL);
+	spa_assert_se(pw_filter_state_as_string(PW_FILTER_STATE_CONNECTING) != NULL);
+	spa_assert_se(pw_filter_state_as_string(PW_FILTER_STATE_PAUSED) != NULL);
+	spa_assert_se(pw_filter_state_as_string(PW_FILTER_STATE_STREAMING) != NULL);
+}
+
+static void filter_destroy_error(void *data)
+{
+	spa_assert_not_reached();
+}
+static void filter_state_changed_error(void *data, enum pw_filter_state old,
+		enum pw_filter_state state, const char *error)
+{
+	spa_assert_not_reached();
+}
+static void filter_io_changed_error(void *data, void *port_data, uint32_t id, void *area, uint32_t size)
+{
+	spa_assert_not_reached();
+}
+static void filter_param_changed_error(void *data, void *port_data, uint32_t id, const struct spa_pod *format)
+{
+	spa_assert_not_reached();
+}
+static void filter_add_buffer_error(void *data, void *port_data, struct pw_buffer *buffer)
+{
+	spa_assert_not_reached();
+}
+static void filter_remove_buffer_error(void *data, void *port_data, struct pw_buffer *buffer)
+{
+	spa_assert_not_reached();
+}
+static void filter_process_error(void *data, struct spa_io_position *position)
+{
+	spa_assert_not_reached();
+}
+static void filter_drained_error(void *data)
+{
+	spa_assert_not_reached();
+}
+
+static const struct pw_filter_events filter_events_error =
+{
+	PW_VERSION_FILTER_EVENTS,
+        .destroy = filter_destroy_error,
+        .state_changed = filter_state_changed_error,
+	.io_changed = filter_io_changed_error,
+	.param_changed = filter_param_changed_error,
+	.add_buffer = filter_add_buffer_error,
+	.remove_buffer = filter_remove_buffer_error,
+	.process = filter_process_error,
+	.drained = filter_drained_error
+};
+
+static int destroy_count = 0;
+static void filter_destroy_count(void *data)
+{
+	destroy_count++;
+}
+static void test_create(void)
+{
+	struct pw_main_loop *loop;
+	struct pw_context *context;
+	struct pw_core *core;
+	struct pw_filter *filter;
+	struct pw_filter_events filter_events = filter_events_error;
+	struct spa_hook listener = { 0, };
+	const char *error = NULL;
+
+	loop = pw_main_loop_new(NULL);
+	context = pw_context_new(pw_main_loop_get_loop(loop), NULL, 12);
+	spa_assert_se(context != NULL);
+	core = pw_context_connect_self(context, NULL, 0);
+	spa_assert_se(core != NULL);
+	filter = pw_filter_new(core, "test", NULL);
+	spa_assert_se(filter != NULL);
+	pw_filter_add_listener(filter, &listener, &filter_events, filter);
+
+	/* check state */
+	spa_assert_se(pw_filter_get_state(filter, &error) == PW_FILTER_STATE_UNCONNECTED);
+	spa_assert_se(error == NULL);
+	/* check name */
+	spa_assert_se(spa_streq(pw_filter_get_name(filter), "test"));
+
+	/* check id, only when connected */
+	spa_assert_se(pw_filter_get_node_id(filter) == SPA_ID_INVALID);
+
+	/* check destroy */
+	destroy_count = 0;
+	filter_events.destroy = filter_destroy_count;
+	pw_filter_destroy(filter);
+	spa_assert_se(destroy_count == 1);
+
+	pw_context_destroy(context);
+	pw_main_loop_destroy(loop);
+}
+
+static void test_properties(void)
+{
+	struct pw_main_loop *loop;
+	struct pw_context *context;
+	struct pw_core *core;
+	const struct pw_properties *props;
+	struct pw_filter *filter;
+	struct pw_filter_events filter_events = filter_events_error;
+	struct spa_hook listener = { { NULL }, };
+	struct spa_dict_item items[3];
+
+	loop = pw_main_loop_new(NULL);
+	context = pw_context_new(pw_main_loop_get_loop(loop), NULL, 12);
+	spa_assert_se(context != NULL);
+	core = pw_context_connect_self(context, NULL, 0);
+	spa_assert_se(core != NULL);
+	filter = pw_filter_new(core, "test",
+			pw_properties_new("foo", "bar",
+					  "biz", "fuzz",
+					  NULL));
+	spa_assert_se(filter != NULL);
+	pw_filter_add_listener(filter, &listener, &filter_events, filter);
+
+	props = pw_filter_get_properties(filter, NULL);
+	spa_assert_se(props != NULL);
+	spa_assert_se(spa_streq(pw_properties_get(props, "foo"), "bar"));
+	spa_assert_se(spa_streq(pw_properties_get(props, "biz"), "fuzz"));
+	spa_assert_se(pw_properties_get(props, "buzz") == NULL);
+
+	/* remove foo */
+	items[0] = SPA_DICT_ITEM_INIT("foo", NULL);
+	/* change biz */
+	items[1] = SPA_DICT_ITEM_INIT("biz", "buzz");
+	/* add buzz */
+	items[2] = SPA_DICT_ITEM_INIT("buzz", "frizz");
+	pw_filter_update_properties(filter, NULL, &SPA_DICT_INIT(items, 3));
+
+	spa_assert_se(props == pw_filter_get_properties(filter, NULL));
+	spa_assert_se(pw_properties_get(props, "foo") == NULL);
+	spa_assert_se(spa_streq(pw_properties_get(props, "biz"), "buzz"));
+	spa_assert_se(spa_streq(pw_properties_get(props, "buzz"), "frizz"));
+
+	/* check destroy */
+	destroy_count = 0;
+	filter_events.destroy = filter_destroy_count;
+	pw_context_destroy(context);
+	spa_assert_se(destroy_count == 1);
+
+	pw_main_loop_destroy(loop);
+}
+
+struct roundtrip_data
+{
+	struct pw_main_loop *loop;
+	int pending;
+	int done;
+};
+
+static void core_event_done(void *object, uint32_t id, int seq)
+{
+	struct roundtrip_data *data = object;
+	if (id == PW_ID_CORE && seq == data->pending) {
+		data->done = 1;
+		printf("done %d\n", seq);
+		pw_main_loop_quit(data->loop);
+	}
+}
+
+static int roundtrip(struct pw_core *core, struct pw_main_loop *loop)
+{
+	struct spa_hook core_listener;
+	struct roundtrip_data data = { .loop = loop };
+	const struct pw_core_events core_events = {
+	PW_VERSION_CORE_EVENTS,
+		.done = core_event_done,
+	};
+	spa_zero(core_listener);
+	pw_core_add_listener(core, &core_listener,
+			&core_events, &data);
+
+	data.pending = pw_core_sync(core, PW_ID_CORE, 0);
+	printf("sync %d\n", data.pending);
+
+	while (!data.done) {
+		pw_main_loop_run(loop);
+	}
+	spa_hook_remove(&core_listener);
+	return 0;
+}
+
+static int node_count = 0;
+static int port_count = 0;
+static void registry_event_global(void *data, uint32_t id,
+		uint32_t permissions, const char *type, uint32_t version,
+		const struct spa_dict *props)
+{
+	printf("object: id:%u type:%s/%d\n", id, type, version);
+	if (spa_streq(type, PW_TYPE_INTERFACE_Port))
+		port_count++;
+	else if (spa_streq(type, PW_TYPE_INTERFACE_Node))
+		node_count++;
+
+}
+static void registry_event_global_remove(void *data, uint32_t id)
+{
+	printf("object: id:%u\n", id);
+}
+
+struct port {
+	struct pw_filter *filter;
+};
+
+static void test_create_port(void)
+{
+	struct pw_main_loop *loop;
+	struct pw_context *context;
+	struct pw_core *core;
+	struct pw_registry *registry;
+	struct pw_filter *filter;
+	struct spa_hook registry_listener = { 0, };
+	static const struct pw_registry_events registry_events = {
+		PW_VERSION_REGISTRY_EVENTS,
+		.global = registry_event_global,
+		.global_remove = registry_event_global_remove,
+	};
+	int res;
+	struct port *port;
+	enum pw_filter_state state;
+
+	loop = pw_main_loop_new(NULL);
+	context = pw_context_new(pw_main_loop_get_loop(loop), NULL, 12);
+	spa_assert_se(context != NULL);
+	core = pw_context_connect_self(context, NULL, 0);
+	spa_assert_se(core != NULL);
+	filter = pw_filter_new(core, "test", NULL);
+	spa_assert_se(filter != NULL);
+
+	registry = pw_core_get_registry(core, PW_VERSION_REGISTRY, 0);
+	spa_assert_se(registry != NULL);
+        pw_registry_add_listener(registry, &registry_listener,
+                                       &registry_events, NULL);
+
+	state = pw_filter_get_state(filter, NULL);
+	printf("state %s\n", pw_filter_state_as_string(state));
+	res = pw_filter_connect(filter, PW_FILTER_FLAG_RT_PROCESS, NULL, 0);
+	spa_assert_se(res >= 0);
+
+	printf("wait connect\n");
+	while (true) {
+		state = pw_filter_get_state(filter, NULL);
+		printf("state %s\n", pw_filter_state_as_string(state));
+		spa_assert_se(state != PW_FILTER_STATE_ERROR);
+
+		if (state == PW_FILTER_STATE_PAUSED)
+			break;
+
+		roundtrip(core, loop);
+	}
+	spa_assert_se(node_count == 1);
+
+	printf("add port\n");
+	/* make an audio DSP output port */
+	port = pw_filter_add_port(filter,
+			PW_DIRECTION_OUTPUT,
+			PW_FILTER_PORT_FLAG_MAP_BUFFERS,
+			sizeof(struct port),
+			pw_properties_new(
+				PW_KEY_FORMAT_DSP, "32 bit float mono audio",
+				PW_KEY_PORT_NAME, "output",
+				NULL),
+			NULL, 0);
+
+	printf("wait port\n");
+	roundtrip(core, loop);
+
+	spa_assert_se(port_count == 1);
+	printf("port added\n");
+
+	printf("remove port\n");
+	pw_filter_remove_port(port);
+	roundtrip(core, loop);
+
+	printf("destroy\n");
+	/* check destroy */
+	pw_filter_destroy(filter);
+
+	pw_proxy_destroy((struct pw_proxy*)registry);
+
+	pw_context_destroy(context);
+	pw_main_loop_destroy(loop);
+}
+
+int main(int argc, char *argv[])
+{
+	pw_init(&argc, &argv);
+
+	test_abi();
+	test_create();
+	test_properties();
+	test_create_port();
+
+	pw_deinit();
+
+	return 0;
+}
diff --git a/src/tests/test-stream.c b/src/tests/test-stream.c
index 3c4b722684718275458fc747bf5f3bd039b6a1fb..1e54fed9045d630eab03f1ab588b52b3736f1f9b 100644
--- a/src/tests/test-stream.c
+++ b/src/tests/test-stream.c
@@ -251,5 +251,7 @@ int main(int argc, char *argv[])
 	test_create();
 	test_properties();
 
+	pw_deinit();
+
 	return 0;
 }
diff --git a/src/tools/dsffile.c b/src/tools/dsffile.c
index a16f38447443c5d8956da200603419011b421da3..9c6ce0c3a670f7aa9f8942464d96b0c965d4d977 100644
--- a/src/tools/dsffile.c
+++ b/src/tools/dsffile.c
@@ -216,13 +216,15 @@ dsf_file_read(struct dsf_file *f, void *data, size_t samples, const struct dsf_l
 	uint8_t *d = data;
 	int step = SPA_ABS(layout->interleave);
 	bool rev = layout->lsb != f->info.lsb;
-	size_t total, block, offset, pos;
+	size_t total, block, offset, pos, scale;
 
 	block = f->offset / f->info.blocksize;
 	offset = block * f->info.blocksize * f->info.channels;
 	pos = f->offset % f->info.blocksize;
+	scale = SPA_CLAMP(f->info.rate / (44100u * 64u), 1u, 4u);
 
 	samples *= step;
+	samples *= scale;
 
 	for (total = 0; total < samples && offset + pos < f->info.length; total++) {
 		const uint8_t *s = f->p + offset + pos;
diff --git a/src/tools/meson.build b/src/tools/meson.build
index 02514c253168946afa983a978d3f64deefd4a1df..9f058da9762573060ca83043808211e6c7f78366 100644
--- a/src/tools/meson.build
+++ b/src/tools/meson.build
@@ -17,13 +17,11 @@ foreach t : tools_sources
   )
 endforeach
 
-if readline_dep.found()
-  executable('pw-cli',
-    'pw-cli.c',
-    install: true,
-    dependencies: [pipewire_dep, readline_dep]
-  )
-endif
+executable('pw-cli',
+  'pw-cli.c',
+  install: true,
+  dependencies: [pipewire_dep, readline_dep]
+)
 
 if ncurses_dep.found()
   executable('pw-top',
diff --git a/src/tools/pw-cat.c b/src/tools/pw-cat.c
index e9082e5aab460460806d698de131272352af8ad3..ea502d6473069ec02241c154c557ec1fc02d5186 100644
--- a/src/tools/pw-cat.c
+++ b/src/tools/pw-cat.c
@@ -126,7 +126,6 @@ struct data {
 	unsigned int rate;
 	int channels;
 	struct channelmap channelmap;
-	unsigned int samplesize;
 	unsigned int stride;
 	enum unit latency_unit;
 	unsigned int latency_value;
@@ -154,125 +153,42 @@ struct data {
 	} dsf;
 };
 
-static inline int
-sf_str_to_fmt(const char *str)
-{
-	if (!str)
-		return -1;
-
-	if (spa_streq(str, "s8"))
-		return SF_FORMAT_PCM_S8;
-	if (spa_streq(str, "u8"))
-		return SF_FORMAT_PCM_U8;
-	if (spa_streq(str, "s16"))
-		return SF_FORMAT_PCM_16;
-	if (spa_streq(str, "s24"))
-		return SF_FORMAT_PCM_24;
-	if (spa_streq(str, "s32"))
-		return SF_FORMAT_PCM_32;
-	if (spa_streq(str, "f32"))
-		return SF_FORMAT_FLOAT;
-	if (spa_streq(str, "f64"))
-		return SF_FORMAT_DOUBLE;
-
-	return -1;
-}
-
-static inline const char *
-sf_fmt_to_str(int format)
-{
-	int sub_type = (format & SF_FORMAT_SUBMASK);
-
-	if (sub_type == SF_FORMAT_PCM_U8)
-		return "u8";
-	if (sub_type == SF_FORMAT_PCM_S8)
-		return "s8";
-	if (sub_type == SF_FORMAT_PCM_16)
-		return "s16";
-	if (sub_type == SF_FORMAT_PCM_24)
-		return "s24";
-	if (sub_type == SF_FORMAT_PCM_32)
-		return "s32";
-	if (sub_type == SF_FORMAT_FLOAT)
-		return "f32";
-	if (sub_type == SF_FORMAT_DOUBLE)
-		return "f64";
-	return "(invalid)";
-}
+#define STR_FMTS "(ulaw|alaw|u8|s8|s16|s32|f32|f64)"
 
-#define STR_FMTS "(u8|s8|s16|s32|f32|f64)"
-
-/* 0 = native, 1 = le, 2 = be */
-static inline int
-sf_format_endianess(int format)
-{
-	return 0;		/* native */
-}
+static const struct format_info {
+	const char *name;
+	int sf_format;
+	uint32_t spa_format;
+	uint32_t width;
+} format_info[] = {
+	{  "ulaw", SF_FORMAT_ULAW, SPA_AUDIO_FORMAT_ULAW, 1 },
+	{  "alaw", SF_FORMAT_ULAW, SPA_AUDIO_FORMAT_ALAW, 1 },
+	{  "s8", SF_FORMAT_PCM_S8, SPA_AUDIO_FORMAT_S8, 1 },
+	{  "u8", SF_FORMAT_PCM_U8, SPA_AUDIO_FORMAT_U8, 1 },
+	{  "s16", SF_FORMAT_PCM_16, SPA_AUDIO_FORMAT_S16, 2 },
+	{  "s24", SF_FORMAT_PCM_24, SPA_AUDIO_FORMAT_S24, 3 },
+	{  "s32", SF_FORMAT_PCM_32, SPA_AUDIO_FORMAT_S32, 4 },
+	{  "f32", SF_FORMAT_FLOAT, SPA_AUDIO_FORMAT_F32, 4 },
+	{  "f64", SF_FORMAT_DOUBLE, SPA_AUDIO_FORMAT_F32, 8 },
+};
 
-static inline enum spa_audio_format
-sf_format_to_pw(int format)
+static const struct format_info *format_info_by_name(const char *str)
 {
-	int endianness;
-
-	endianness = sf_format_endianess(format);
-	if (endianness < 0)
-		return SPA_AUDIO_FORMAT_UNKNOWN;
-
-	switch (format & SF_FORMAT_SUBMASK) {
-	case SF_FORMAT_PCM_U8:
-		return SPA_AUDIO_FORMAT_U8;
-	case SF_FORMAT_PCM_S8:
-		return SPA_AUDIO_FORMAT_S8;
-	case SF_FORMAT_ULAW:
-		return SPA_AUDIO_FORMAT_ULAW;
-	case SF_FORMAT_ALAW:
-		return SPA_AUDIO_FORMAT_ALAW;
-	case SF_FORMAT_PCM_16:
-		return endianness == 1 ? SPA_AUDIO_FORMAT_S16_LE :
-		       endianness == 2 ? SPA_AUDIO_FORMAT_S16_BE :
-		                         SPA_AUDIO_FORMAT_S16;
-	case SF_FORMAT_PCM_24:
-	case SF_FORMAT_PCM_32:
-		return endianness == 1 ? SPA_AUDIO_FORMAT_S32_LE :
-		       endianness == 2 ? SPA_AUDIO_FORMAT_S32_BE :
-		                         SPA_AUDIO_FORMAT_S32;
-	case SF_FORMAT_DOUBLE:
-		return endianness == 1 ? SPA_AUDIO_FORMAT_F64_LE :
-		       endianness == 2 ? SPA_AUDIO_FORMAT_F64_BE :
-		                         SPA_AUDIO_FORMAT_F64;
-	case SF_FORMAT_FLOAT:
-	default:
-		return endianness == 1 ? SPA_AUDIO_FORMAT_F32_LE :
-		       endianness == 2 ? SPA_AUDIO_FORMAT_F32_BE :
-		                         SPA_AUDIO_FORMAT_F32;
-		break;
-	}
-
-	return SPA_AUDIO_FORMAT_UNKNOWN;
+	uint32_t i;
+	for (i = 0; i < SPA_N_ELEMENTS(format_info); i++)
+		if (spa_streq(str, format_info[i].name))
+			return &format_info[i];
+	return NULL;
 }
 
-static inline int
-sf_format_samplesize(int format)
+static const struct format_info *format_info_by_sf_format(int format)
 {
+	uint32_t i;
 	int sub_type = (format & SF_FORMAT_SUBMASK);
-
-	switch (sub_type) {
-	case SF_FORMAT_PCM_S8:
-	case SF_FORMAT_PCM_U8:
-	case SF_FORMAT_ULAW:
-	case SF_FORMAT_ALAW:
-		return 1;
-	case SF_FORMAT_PCM_16:
-		return 2;
-	case SF_FORMAT_PCM_32:
-		return 4;
-	case SF_FORMAT_DOUBLE:
-		return 8;
-	case SF_FORMAT_FLOAT:
-	default:
-		return 4;
-	}
-	return -1;
+	for (i = 0; i < SPA_N_ELEMENTS(format_info); i++)
+		if (format_info[i].sf_format == sub_type)
+			return &format_info[i];
+	return NULL;
 }
 
 static int sf_playback_fill_x8(struct data *d, void *dest, unsigned int n_frames)
@@ -280,7 +196,7 @@ static int sf_playback_fill_x8(struct data *d, void *dest, unsigned int n_frames
 	sf_count_t rn;
 
 	rn = sf_read_raw(d->file, dest, n_frames * d->stride);
-	return (int)rn;
+	return (int)rn / d->stride;
 }
 
 static int sf_playback_fill_s16(struct data *d, void *dest, unsigned int n_frames)
@@ -320,10 +236,8 @@ static int sf_playback_fill_f64(struct data *d, void *dest, unsigned int n_frame
 }
 
 static inline fill_fn
-sf_fmt_playback_fill_fn(int format)
+playback_fill_fn(uint32_t fmt)
 {
-	enum spa_audio_format fmt = sf_format_to_pw(format);
-
 	switch (fmt) {
 	case SPA_AUDIO_FORMAT_S8:
 	case SPA_AUDIO_FORMAT_U8:
@@ -364,7 +278,7 @@ static int sf_record_fill_x8(struct data *d, void *src, unsigned int n_frames)
 	sf_count_t rn;
 
 	rn = sf_write_raw(d->file, src, n_frames * d->stride);
-	return (int)rn;
+	return (int)rn / d->stride;
 }
 
 static int sf_record_fill_s16(struct data *d, void *src, unsigned int n_frames)
@@ -404,10 +318,8 @@ static int sf_record_fill_f64(struct data *d, void *src, unsigned int n_frames)
 }
 
 static inline fill_fn
-sf_fmt_record_fill_fn(int format)
+record_fill_fn(uint32_t fmt)
 {
-	enum spa_audio_format fmt = sf_format_to_pw(format);
-
 	switch (fmt) {
 	case SPA_AUDIO_FORMAT_S8:
 	case SPA_AUDIO_FORMAT_U8:
@@ -1076,29 +988,6 @@ static int setup_dsffile(struct data *data)
 	return 0;
 }
 
-struct format_info {
-	const char *name;
-	uint32_t spa_format;
-	uint32_t width;
-} format_info[] = {
-	{  "s8", SPA_AUDIO_FORMAT_S8, 1 },
-	{  "u8", SPA_AUDIO_FORMAT_U8, 1 },
-	{  "s16", SPA_AUDIO_FORMAT_S16, 2 },
-	{  "s24", SPA_AUDIO_FORMAT_S24, 3 },
-	{  "s32", SPA_AUDIO_FORMAT_S32, 4 },
-	{  "f32", SPA_AUDIO_FORMAT_F32, 4 },
-	{  "f64", SPA_AUDIO_FORMAT_F32, 8 },
-};
-
-static struct format_info *format_info_by_name(const char *str)
-{
-	uint32_t i;
-	for (i = 0; i < SPA_N_ELEMENTS(format_info); i++)
-		if (spa_streq(str, format_info[i].name))
-			return &format_info[i];
-	return NULL;
-}
-
 static int stdout_record(struct data *d, void *src, unsigned int n_frames)
 {
 	return fwrite(src, d->stride, n_frames, stdout);
@@ -1111,7 +1000,7 @@ static int stdin_play(struct data *d, void *src, unsigned int n_frames)
 
 static int setup_pipe(struct data *data)
 {
-	struct format_info *info;
+	const struct format_info *info;
 
 	if (data->format == NULL)
 		data->format = DEFAULT_FORMAT;
@@ -1129,6 +1018,12 @@ static int setup_pipe(struct data *data)
 	data->spa_format = info->spa_format;
 	data->stride = info->width * data->channels;
 	data->fill = data->mode == mode_playback ?  stdin_play : stdout_record;
+
+	if (data->verbose)
+		printf("PIPE: rate=%u channels=%u fmt=%s samplesize=%u stride=%u\n",
+				data->rate, data->channels,
+				info->name, info->width, data->stride);
+
 	return 0;
 }
 
@@ -1221,9 +1116,8 @@ static void format_from_filename(SF_INFO *info, const char *filename)
 
 static int setup_sndfile(struct data *data)
 {
+	const struct format_info *fi = NULL;
 	SF_INFO info;
-	const char *s;
-	unsigned int nom = 0;
 
 	spa_zero(info);
 	/* for record, you fill in the info first */
@@ -1237,14 +1131,14 @@ static int setup_sndfile(struct data *data)
 		if (data->channelmap.n_channels == 0)
 			channelmap_default(&data->channelmap, data->channels);
 
-		memset(&info, 0, sizeof(info));
-		info.samplerate = data->rate;
-		info.channels = data->channels;
-		info.format = sf_str_to_fmt(data->format);
-		if (info.format == -1) {
+		if ((fi = format_info_by_name(data->format)) == NULL) {
 			fprintf(stderr, "error: unknown format \"%s\"\n", data->format);
 			return -EINVAL;
 		}
+		memset(&info, 0, sizeof(info));
+		info.samplerate = data->rate;
+		info.channels = data->channels;
+		info.format = fi->sf_format;
 		format_from_filename(&info, data->filename);
 	}
 
@@ -1291,13 +1185,46 @@ static int setup_sndfile(struct data *data)
 			}
 		}
 		fill_properties(data);
+
+		/* try native format first, else decode to float */
+		if ((fi = format_info_by_sf_format(info.format)) == NULL)
+			fi = format_info_by_sf_format(SF_FORMAT_FLOAT);
+
 	}
-	data->samplesize = sf_format_samplesize(info.format);
-	data->stride = data->samplesize * data->channels;
-	data->spa_format = sf_format_to_pw(info.format);
+	if (fi == NULL)
+		return -EIO;
+
+	if (data->verbose)
+		printf("PCM: fmt:%s rate:%u channels:%u width:%u\n",
+				fi->name, data->rate, data->channels, fi->width);
+
+	/* we read and write S24 as S32 with sndfile */
+	if (fi->spa_format == SPA_AUDIO_FORMAT_S24)
+		fi = format_info_by_sf_format(SF_FORMAT_PCM_32);
+
+	data->spa_format = fi->spa_format;
+	data->stride = fi->width * data->channels;
 	data->fill = data->mode == mode_playback ?
-			sf_fmt_playback_fill_fn(info.format) :
-			sf_fmt_record_fill_fn(info.format);
+			playback_fill_fn(data->spa_format) :
+			record_fill_fn(data->spa_format);
+
+	if (data->fill == NULL) {
+		fprintf(stderr, "PCM: unhandled format %d\n", data->spa_format);
+		return -EINVAL;
+	}
+	return 0;
+}
+
+static int setup_properties(struct data *data)
+{
+	const char *s;
+	unsigned int nom = 0;
+
+	if (data->quality >= 0)
+		pw_properties_setf(data->props, "resample.quality", "%d", data->quality);
+
+	if (data->rate)
+		pw_properties_setf(data->props, PW_KEY_NODE_RATE, "1/%u", data->rate);
 
 	data->latency_unit = unit_none;
 
@@ -1348,20 +1275,11 @@ static int setup_sndfile(struct data *data)
 	}
 
 	if (data->verbose)
-		printf("PCM: rate=%u channels=%u fmt=%s samplesize=%u stride=%u latency=%u (%.3fs)\n",
-				data->rate, data->channels,
-				sf_fmt_to_str(info.format),
-				data->samplesize,
-				data->stride, nom, (double)nom/data->rate);
-
+		printf("rate:%d latency:%u (%.3fs)\n",
+				data->rate, nom, data->rate ? (double)nom/data->rate : 0.0f);
 	if (nom)
 		pw_properties_setf(data->props, PW_KEY_NODE_LATENCY, "%u/%u", nom, data->rate);
 
-	pw_properties_setf(data->props, PW_KEY_NODE_RATE, "1/%u", data->rate);
-
-	if (data->quality >= 0)
-		pw_properties_setf(data->props, "resample.quality", "%d", data->quality);
-
 	return 0;
 }
 
@@ -1521,9 +1439,7 @@ int main(int argc, char *argv[])
 		case OPT_VOLUME:
 			data.volume = atof(optarg);
 			break;
-
 		default:
-			fprintf(stderr, "error: unknown option '%c'\n", c);
 			goto error_usage;
 		}
 	}
@@ -1634,7 +1550,6 @@ int main(int argc, char *argv[])
 			break;
 		}
 	}
-
 	if (ret < 0) {
 		fprintf(stderr, "error: open failed: %s\n", spa_strerror(ret));
 		switch (ret) {
@@ -1645,6 +1560,8 @@ int main(int argc, char *argv[])
 			goto error_usage;
 		}
 	}
+	ret = setup_properties(&data);
+
 	switch (data.data_type) {
 	case TYPE_PCM:
 	{
@@ -1746,6 +1663,7 @@ error_connect_fail:
 		pw_stream_destroy(data.stream);
 	}
 error_no_stream:
+error_bad_file:
 	spa_hook_remove(&data.core_listener);
 	pw_core_disconnect(data.core);
 error_ctx_connect_failed:
@@ -1754,7 +1672,6 @@ error_no_context:
 	pw_main_loop_destroy(data.loop);
 error_no_props:
 error_no_main_loop:
-error_bad_file:
 	pw_properties_free(data.props);
 	if (data.file)
 		sf_close(data.file);
diff --git a/src/tools/pw-cli.c b/src/tools/pw-cli.c
index 9ac59e282390425da3e079cfe91cb8dd891a4f1e..4d6655b062b37249d9951aed04db8a27973db6d8 100644
--- a/src/tools/pw-cli.c
+++ b/src/tools/pw-cli.c
@@ -22,19 +22,23 @@
  * DEALINGS IN THE SOFTWARE.
  */
 
+#include "config.h"
+
 #include <unistd.h>
 #include <errno.h>
 #include <stdio.h>
 #include <signal.h>
 #include <string.h>
 #include <ctype.h>
-#ifndef __FreeBSD__
+#if !defined(__FreeBSD__) && !defined(__MidnightBSD__)
 #include <alloca.h>
 #endif
 #include <getopt.h>
 #include <fnmatch.h>
+#ifdef HAVE_READLINE
 #include <readline/readline.h>
 #include <readline/history.h>
+#endif
 #include <locale.h>
 
 #if !defined(FNM_EXTMATCH)
@@ -222,7 +226,6 @@ static bool do_set_param(struct data *data, const char *cmd, char *args, char **
 static bool do_permissions(struct data *data, const char *cmd, char *args, char **error);
 static bool do_get_permissions(struct data *data, const char *cmd, char *args, char **error);
 static bool do_send_command(struct data *data, const char *cmd, char *args, char **error);
-static bool do_dump(struct data *data, const char *cmd, char *args, char **error);
 static bool do_quit(struct data *data, const char *cmd, char *args, char **error);
 
 #define DUMP_NAMES "Core|Module|Device|Node|Port|Factory|Client|Link|Session|Endpoint|EndpointStream"
@@ -247,8 +250,6 @@ static const struct command command_list[] = {
 	{ "permissions", "sp", "Set permissions for a client <client-id> <object> <permission>", do_permissions },
 	{ "get-permissions", "gp", "Get permissions of a client <client-id>", do_get_permissions },
 	{ "send-command", "c", "Send a command <object-id>", do_send_command },
-	{ "dump", "D", "Dump objects in ways that are cleaner for humans to understand "
-		 "[short|deep|resolve|notype] [-sdrt] [all|"DUMP_NAMES"|<id>]", do_dump },
 	{ "quit", "q", "Quit", do_quit },
 };
 
@@ -265,7 +266,10 @@ static bool do_help(struct data *data, const char *cmd, char *args, char **error
 
 	printf("Available commands:\n");
 	for (i = 0; i < SPA_N_ELEMENTS(command_list); i++) {
-		printf("\t%-20.20s\t%s\n", command_list[i].name, command_list[i].description);
+		char cmd[256];
+		snprintf(cmd, sizeof(cmd), "%s | %s",
+				command_list[i].name, command_list[i].alias);
+		printf("\t%-20.20s\t%s\n", cmd, command_list[i].description);
 	}
 	return true;
 }
@@ -307,7 +311,12 @@ static void on_core_info(void *_data, const struct pw_core_info *info)
 static void set_prompt(struct remote_data *rd)
 {
 	snprintf(prompt, sizeof(prompt), "%s>> ", rd->name);
+#ifdef HAVE_READLINE
 	rl_set_prompt(prompt);
+#else
+	printf("%s", prompt);
+	fflush(stdout);
+#endif
 }
 
 static void on_core_done(void *_data, uint32_t id, int seq)
@@ -1920,20 +1929,6 @@ static bool do_send_command(struct data *data, const char *cmd, char *args, char
 	return true;
 }
 
-static const char *
-pw_interface_short(const char *type)
-{
-	size_t ilen;
-
-	ilen = strlen(PW_TYPE_INFO_INTERFACE_BASE);
-
-	if (!type || strlen(type) <= ilen ||
-	    memcmp(type, PW_TYPE_INFO_INTERFACE_BASE, ilen))
-		return NULL;
-
-	return type + ilen;
-}
-
 static struct global *
 obj_global(struct remote_data *rd, uint32_t id)
 {
@@ -1992,20 +1987,6 @@ global_props(struct global *global)
 	return NULL;
 }
 
-static struct spa_dict *
-obj_props(struct remote_data *rd, uint32_t id)
-{
-	struct global *global;
-
-	if (!rd)
-		return NULL;
-
-	global = obj_global(rd, id);
-	if (!global)
-		return NULL;
-	return global_props(global);
-}
-
 static const char *
 global_lookup(struct global *global, const char *key)
 {
@@ -2017,16 +1998,6 @@ global_lookup(struct global *global, const char *key)
 	return spa_dict_lookup(dict, key);
 }
 
-static const char *
-obj_lookup(struct remote_data *rd, uint32_t id, const char *key)
-{
-	struct spa_dict *dict;
-
-	dict = obj_props(rd, id);
-	if (!dict)
-		return NULL;
-	return spa_dict_lookup(dict, key);
-}
 
 static int
 children_of(struct remote_data *rd, uint32_t parent_id,
@@ -2126,67 +2097,6 @@ children_of(struct remote_data *rd, uint32_t parent_id,
 	return count;
 }
 
-#ifndef BIT
-#define BIT(x) (1U << (x))
-#endif
-
-enum dump_flags {
-	is_default = 0,
-	is_short = BIT(0),
-	is_deep = BIT(1),
-	is_resolve = BIT(2),
-	is_notype = BIT(3)
-};
-
-static const char * const dump_types[] = {
-	PW_TYPE_INTERFACE_Core,
-	PW_TYPE_INTERFACE_Module,
-	PW_TYPE_INTERFACE_Device,
-	PW_TYPE_INTERFACE_Node,
-	PW_TYPE_INTERFACE_Port,
-	PW_TYPE_INTERFACE_Factory,
-	PW_TYPE_INTERFACE_Client,
-	PW_TYPE_INTERFACE_Link,
-	PW_TYPE_INTERFACE_Session,
-	PW_TYPE_INTERFACE_Endpoint,
-	PW_TYPE_INTERFACE_EndpointStream,
-};
-
-int dump_type_index(const char *type)
-{
-	unsigned int i;
-
-	if (!type)
-		return -1;
-
-	for (i = 0; i < SPA_N_ELEMENTS(dump_types); i++) {
-		if (spa_streq(dump_types[i], type))
-			return (int)i;
-	}
-
-	return -1;
-}
-
-static inline unsigned int dump_type_count(void)
-{
-	return SPA_N_ELEMENTS(dump_types);
-}
-
-static const char *name_to_dump_type(const char *name)
-{
-	unsigned int i;
-
-	if (!name)
-		return NULL;
-
-	for (i = 0; i < SPA_N_ELEMENTS(dump_types); i++) {
-		if (!strcasecmp(name, pw_interface_short(dump_types[i])))
-			return dump_types[i];
-	}
-
-	return NULL;
-}
-
 #define INDENT(_level) \
 	({ \
 		int __level = (_level); \
@@ -2196,817 +2106,6 @@ static const char *name_to_dump_type(const char *name)
 		(const char *)_indent; \
 	})
 
-static void
-dump(struct data *data, struct global *global,
-     enum dump_flags flags, int level);
-
-static void
-dump_properties(struct data *data, struct global *global,
-		enum dump_flags flags, int level)
-{
-	struct remote_data *rd = data->current;
-	struct spa_dict *props;
-	const struct spa_dict_item *item;
-	const char *ind;
-	int id;
-	const char *extra;
-
-	if (!global)
-		return;
-
-	props = global_props(global);
-	if (!props || !props->n_items)
-		return;
-
-	ind = INDENT(level + 2);
-	spa_dict_for_each(item, props) {
-		printf("%s%s = \"%s\"",
-				ind, item->key, item->value);
-
-		extra = NULL;
-		if (spa_streq(global->type, PW_TYPE_INTERFACE_Port) && spa_streq(item->key, PW_KEY_NODE_ID)) {
-			id = atoi(item->value);
-			if (id >= 0)
-				extra = obj_lookup(rd, id, PW_KEY_NODE_NAME);
-		} else if (spa_streq(global->type, PW_TYPE_INTERFACE_Factory) && spa_streq(item->key, PW_KEY_MODULE_ID)) {
-			id = atoi(item->value);
-			if (id >= 0)
-				extra = obj_lookup(rd, id, PW_KEY_MODULE_NAME);
-		} else if (spa_streq(global->type, PW_TYPE_INTERFACE_Device) && spa_streq(item->key, PW_KEY_FACTORY_ID)) {
-			id = atoi(item->value);
-			if (id >= 0)
-				extra = obj_lookup(rd, id, PW_KEY_FACTORY_NAME);
-		} else if (spa_streq(global->type, PW_TYPE_INTERFACE_Device) && spa_streq(item->key, PW_KEY_CLIENT_ID)) {
-			id = atoi(item->value);
-			if (id >= 0)
-				extra = obj_lookup(rd, id, PW_KEY_CLIENT_NAME);
-		}
-
-		if (extra)
-			printf(" (\"%s\")", extra);
-
-		printf("\n");
-	}
-}
-
-static void
-dump_params(struct data *data, struct global *global,
-	    struct spa_param_info *params, uint32_t n_params,
-	    enum dump_flags flags, int level)
-{
-	uint32_t i;
-	const char *ind;
-
-	if (params == NULL || n_params == 0)
-		return;
-
-	ind = INDENT(level + 1);
-	for (i = 0; i < n_params; i++) {
-		const struct spa_type_info *type_info = spa_type_param;
-
-		printf("%s  %d (%s) %c%c\n", ind,
-			params[i].id,
-			spa_debug_type_find_name(type_info, params[i].id),
-			params[i].flags & SPA_PARAM_INFO_READ ? 'r' : '-',
-			params[i].flags & SPA_PARAM_INFO_WRITE ? 'w' : '-');
-	}
-}
-
-
-static void
-dump_global_common(struct data *data, struct global *global,
-	    enum dump_flags flags, int level)
-{
-	const char *ind;
-
-	if (!(flags & is_short)) {
-		ind = INDENT(level + 1);
-		printf("%sid: %"PRIu32"\n", ind, global->id);
-		printf("%spermissions: "PW_PERMISSION_FORMAT"\n", ind,
-			PW_PERMISSION_ARGS(global->permissions));
-		printf("%stype: %s/%d\n", ind,
-				global->type, global->version);
-	} else {
-		ind = INDENT(level);
-		printf("%s%"PRIu32":", ind, global->id);
-		if (!(flags & is_notype))
-			printf(" %s", pw_interface_short(global->type));
-	}
-}
-
-static bool
-dump_core(struct data *data, struct global *global,
-	  enum dump_flags flags, int level)
-{
-	struct proxy_data *pd = pw_proxy_get_user_data(global->proxy);
-	struct pw_core_info *info;
-	const char *ind;
-
-	if (!pd->info)
-		return false;
-
-	dump_global_common(data, global, flags, level);
-
-	info = pd->info;
-	if (!(flags & is_short)) {
-		ind = INDENT(level + 1);
-		printf("%scookie: %u\n", ind, info->cookie);
-		printf("%suser-name: \"%s\"\n", ind, info->user_name);
-		printf("%shost-name: \"%s\"\n", ind, info->host_name);
-		printf("%sversion: \"%s\"\n", ind, info->version);
-		printf("%sname: \"%s\"\n", ind, info->name);
-		printf("%sproperties:\n", ind);
-		dump_properties(data, global, flags, level);
-	} else {
-		printf(" u=\"%s\" h=\"%s\" v=\"%s\" n=\"%s\"",
-				info->user_name, info->host_name, info->version, info->name);
-		printf("\n");
-	}
-
-	return true;
-}
-
-static bool
-dump_module(struct data *data, struct global *global,
-	    enum dump_flags flags, int level)
-{
-	struct remote_data *rd = global->rd;
-	struct proxy_data *pd = pw_proxy_get_user_data(global->proxy);
-	struct pw_module_info *info;
-	const char *args, *desc;
-	const char *ind;
-	uint32_t *factories = NULL;
-	int i, factory_count;
-	struct global *global_factory;
-
-	if (!pd->info)
-		return false;
-
-	info = pd->info;
-
-	dump_global_common(data, global, flags, level);
-
-	if (!(flags & is_short)) {
-		ind = INDENT(level + 1);
-		printf("%sname: \"%s\"\n", ind, info->name);
-		printf("%sfilename: \"%s\"\n", ind, info->filename);
-		printf("%sargs: \"%s\"\n", ind, info->args);
-		printf("%sproperties:\n", ind);
-		dump_properties(data, global, flags, level);
-	} else {
-		desc = spa_dict_lookup(info->props, PW_KEY_MODULE_DESCRIPTION);
-		args = info->args && strcmp(info->args, "(null)") ? info->args : NULL;
-		printf(" n=\"%s\" f=\"%s\"" "%s%s%s" "%s%s%s",
-				info->name, info->filename,
-				args ? " a=\"" : "",
-				args ? args : "",
-				args ? "\"" : "",
-				desc ? " d=\"" : "",
-				desc ? desc : "",
-				desc ? "\"" : "");
-		printf("\n");
-	}
-
-	if (!(flags & is_deep))
-		return true;
-
-	factory_count = children_of(rd, global->id, PW_TYPE_INTERFACE_Factory, &factories);
-	if (factory_count >= 0) {
-		ind = INDENT(level + 1);
-		printf("%sfactories:\n", ind);
-		for (i = 0; i < factory_count; i++) {
-			global_factory = obj_global(rd, factories[i]);
-			if (!global_factory)
-				continue;
-			dump(data, global_factory, flags | is_notype, level + 1);
-		}
-		free(factories);
-	}
-
-	return true;
-}
-
-static bool
-dump_device(struct data *data, struct global *global,
-	    enum dump_flags flags, int level)
-{
-	struct remote_data *rd = data->current;
-	struct proxy_data *pd = pw_proxy_get_user_data(global->proxy);
-	struct pw_device_info *info;
-	const char *media_class, *api, *desc, *name;
-	const char *alsa_path, *alsa_card_id;
-	const char *ind;
-	uint32_t *nodes = NULL;
-	int i, node_count;
-	struct global *global_node;
-
-	if (!pd->info)
-		return false;
-
-	info = pd->info;
-
-	dump_global_common(data, global, flags, level);
-
-	if (!(flags & is_short)) {
-		ind = INDENT(level + 1);
-		printf("%sproperties:\n", ind);
-		dump_properties(data, global, flags, level);
-		printf("%sparams:\n", ind);
-		dump_params(data, global, info->params, info->n_params, flags, level);
-	} else {
-		media_class = spa_dict_lookup(info->props, PW_KEY_MEDIA_CLASS);
-		name = spa_dict_lookup(info->props, PW_KEY_DEVICE_NAME);
-		desc = spa_dict_lookup(info->props, PW_KEY_DEVICE_DESCRIPTION);
-		api = spa_dict_lookup(info->props, PW_KEY_DEVICE_API);
-
-		printf("%s%s%s" "%s%s%s" "%s%s%s" "%s%s%s",
-				media_class ? " c=\"" : "",
-				media_class ? media_class : "",
-				media_class ? "\"" : "",
-				name ? " n=\"" : "",
-				name ? name : "",
-				name ? "\"" : "",
-				desc ? " d=\"" : "",
-				desc ? desc : "",
-				desc ? "\"" : "",
-				api ? " a=\"" : "",
-				api ? api : "",
-				api ? "\"" : "");
-
-		if (media_class && spa_streq(media_class, "Audio/Device") &&
-		    api && spa_streq(api, "alsa:pcm")) {
-
-			alsa_path = spa_dict_lookup(info->props, SPA_KEY_API_ALSA_PATH);
-			alsa_card_id = spa_dict_lookup(info->props, SPA_KEY_API_ALSA_CARD_ID);
-
-			printf("%s%s%s" "%s%s%s",
-					alsa_path ? " p=\"" : "",
-					alsa_path ? alsa_path : "",
-					alsa_path ? "\"" : "",
-					alsa_card_id ? " id=\"" : "",
-					alsa_card_id ? alsa_card_id : "",
-					alsa_card_id ? "\"" : "");
-		}
-
-		printf("\n");
-	}
-
-	if (!(flags & is_deep))
-		return true;
-
-	node_count = children_of(rd, global->id, PW_TYPE_INTERFACE_Node, &nodes);
-	if (node_count >= 0) {
-		ind = INDENT(level + 1);
-		printf("%snodes:\n", ind);
-		for (i = 0; i < node_count; i++) {
-			global_node = obj_global(rd, nodes[i]);
-			if (!global_node)
-				continue;
-			dump(data, global_node, flags | is_notype, level + 1);
-		}
-		free(nodes);
-	}
-
-	return true;
-}
-
-static bool
-dump_node(struct data *data, struct global *global,
-	  enum dump_flags flags, int level)
-{
-	struct remote_data *rd = data->current;
-	struct proxy_data *pd = pw_proxy_get_user_data(global->proxy);
-	struct pw_node_info *info;
-	const char *name, *path;
-	const char *ind;
-	uint32_t *ports = NULL;
-	int i, port_count;
-	struct global *global_port;
-
-	if (!pd->info)
-		return false;
-
-	dump_global_common(data, global, flags, level);
-
-	info = pd->info;
-
-	if (!(flags & is_short)) {
-		ind = INDENT(level + 1);
-		printf("%sinput ports: %u/%u\n", ind, info->n_input_ports, info->max_input_ports);
-		printf("%soutput ports: %u/%u\n", ind, info->n_output_ports, info->max_output_ports);
-		printf("%sstate: \"%s\"", ind, pw_node_state_as_string(info->state));
-		if (info->state == PW_NODE_STATE_ERROR && info->error)
-			printf(" \"%s\"\n", info->error);
-		else
-			printf("\n");
-		printf("%sproperties:\n", ind);
-		dump_properties(data, global, flags, level);
-		printf("%sparams:\n", ind);
-		dump_params(data, global, info->params, info->n_params, flags, level);
-	} else {
-		name = spa_dict_lookup(info->props, PW_KEY_NODE_NAME);
-		path = spa_dict_lookup(info->props, SPA_KEY_OBJECT_PATH);
-
-		printf(" s=\"%s\"", pw_node_state_as_string(info->state));
-
-		if (info->max_input_ports) {
-			printf(" i=%u/%u", info->n_input_ports, info->max_input_ports);
-		}
-		if (info->max_output_ports) {
-			printf(" o=%u/%u", info->n_output_ports, info->max_output_ports);
-		}
-
-		printf("%s%s%s" "%s%s%s",
-				name ? " n=\"" : "",
-				name ? name : "",
-				name ? "\"" : "",
-				path ? " p=\"" : "",
-				path ? path : "",
-				path ? "\"" : "");
-
-		printf("\n");
-	}
-
-	if (!(flags & is_deep))
-		return true;
-
-	port_count = children_of(rd, global->id, PW_TYPE_INTERFACE_Port, &ports);
-	if (port_count >= 0) {
-		ind = INDENT(level + 1);
-		printf("%sports:\n", ind);
-		for (i = 0; i < port_count; i++) {
-			global_port = obj_global(rd, ports[i]);
-			if (!global_port)
-				continue;
-			dump(data, global_port, flags | is_notype, level + 1);
-		}
-		free(ports);
-	}
-	return true;
-}
-
-static bool
-dump_port(struct data *data, struct global *global,
-	  enum dump_flags flags, int level)
-{
-	struct remote_data *rd = data->current;
-	struct proxy_data *pd = pw_proxy_get_user_data(global->proxy);
-	struct pw_port_info *info;
-	const char *ind;
-	const char *name, *format;
-
-	if (!pd->info)
-		return false;
-
-	dump_global_common(data, global, flags, level);
-
-	info = pd->info;
-
-	if (!(flags & is_short)) {
-		ind = INDENT(level + 1);
-		printf("%sdirection: \"%s\"\n", ind,
-				pw_direction_as_string(info->direction));
-		printf("%sproperties:\n", ind);
-		dump_properties(data, global, flags, level);
-		printf("%sparams:\n", ind);
-		dump_params(data, global, info->params, info->n_params, flags, level);
-	} else {
-		printf(" d=\"%s\"", pw_direction_as_string(info->direction));
-
-		name = spa_dict_lookup(info->props, PW_KEY_PORT_NAME);
-		format = spa_dict_lookup(info->props, PW_KEY_FORMAT_DSP);
-
-		printf("%s%s%s" "%s%s%s",
-				name ? " n=\"" : "",
-				name ? name : "",
-				name ? "\"" : "",
-				format ? " f=\"" : "",
-				format ? format : "",
-				format ? "\"" : "");
-
-		printf("\n");
-	}
-
-	(void)rd;
-
-	return true;
-}
-
-static bool
-dump_factory(struct data *data, struct global *global,
-	     enum dump_flags flags, int level)
-{
-	struct remote_data *rd = data->current;
-	struct proxy_data *pd = pw_proxy_get_user_data(global->proxy);
-	struct pw_factory_info *info;
-	const char *ind;
-	const char *module_id, *module_name;
-
-	if (!pd->info)
-		return false;
-
-	dump_global_common(data, global, flags, level);
-
-	info = pd->info;
-
-	if (!(flags & is_short)) {
-		ind = INDENT(level + 1);
-		printf("%sname: \"%s\"\n", ind, info->name);
-		printf("%sproperties:\n", ind);
-		dump_properties(data, global, flags, level);
-	} else {
-		printf(" n=\"%s\"", info->name);
-
-		module_id = spa_dict_lookup(info->props, PW_KEY_MODULE_ID);
-		module_name = module_id ? obj_lookup(rd, atoi(module_id), PW_KEY_MODULE_NAME) : NULL;
-
-		printf("%s%s%s",
-				module_name ? " m=\"" : "",
-				module_name ? module_name : "",
-				module_name ? "\"" : "");
-
-		printf("\n");
-	}
-
-	return true;
-}
-
-static bool
-dump_client(struct data *data, struct global *global,
-	    enum dump_flags flags, int level)
-{
-	struct remote_data *rd = data->current;
-	struct proxy_data *pd = pw_proxy_get_user_data(global->proxy);
-	struct pw_client_info *info;
-	const char *ind;
-	const char *app_name, *app_pid;
-
-	if (!pd->info)
-		return false;
-
-	dump_global_common(data, global, flags, level);
-
-	info = pd->info;
-
-	if (!(flags & is_short)) {
-		ind = INDENT(level + 1);
-		printf("%sproperties:\n", ind);
-		dump_properties(data, global, flags, level);
-	} else {
-		app_name = spa_dict_lookup(info->props, PW_KEY_APP_NAME);
-		app_pid = spa_dict_lookup(info->props, PW_KEY_APP_PROCESS_ID);
-
-		printf("%s%s%s" "%s%s%s",
-				app_name ? " ap=\"" : "",
-				app_name ? app_name : "",
-				app_name ? "\"" : "",
-				app_pid ? " ai=\"" : "",
-				app_pid ? app_pid : "",
-				app_pid ? "\"" : "");
-
-		printf("\n");
-	}
-
-	(void)rd;
-
-	return true;
-}
-
-static bool
-dump_link(struct data *data, struct global *global,
-	  enum dump_flags flags, int level)
-{
-	struct remote_data *rd = data->current;
-	struct proxy_data *pd = pw_proxy_get_user_data(global->proxy);
-	struct pw_link_info *info;
-	const char *ind;
-	const char *in_node_name, *in_port_name;
-	const char *out_node_name, *out_port_name;
-
-	if (!pd->info)
-		return false;
-
-	dump_global_common(data, global, flags, level);
-
-	info = pd->info;
-
-	if (!(flags & is_short)) {
-		ind = INDENT(level + 1);
-		printf("%soutput-node-id: %u\n", ind, info->output_node_id);
-		printf("%soutput-port-id: %u\n", ind, info->output_port_id);
-		printf("%sinput-node-id: %u\n", ind, info->input_node_id);
-		printf("%sinput-port-id: %u\n", ind, info->input_port_id);
-
-		printf("%sstate: \"%s\"", ind,
-				pw_link_state_as_string(info->state));
-		if (info->state == PW_LINK_STATE_ERROR && info->error)
-			printf(" \"%s\"\n", info->error);
-		else
-			printf("\n");
-		printf("%sformat:\n", ind);
-		if (info->format)
-			spa_debug_pod(8 * (level + 1) + 2, NULL, info->format);
-		else
-			printf("%s\tnone\n", ind);
-
-		printf("%sproperties:\n", ind);
-		dump_properties(data, global, flags, level);
-	} else {
-		out_node_name = obj_lookup(rd, info->output_node_id, PW_KEY_NODE_NAME);
-		in_node_name = obj_lookup(rd, info->input_node_id, PW_KEY_NODE_NAME);
-		out_port_name = obj_lookup(rd, info->output_port_id, PW_KEY_PORT_NAME);
-		in_port_name = obj_lookup(rd, info->input_port_id, PW_KEY_PORT_NAME);
-
-		printf(" s=\"%s\"", pw_link_state_as_string(info->state));
-
-		if (out_node_name && out_port_name)
-			printf(" on=\"%s\"" " op=\"%s\"",
-					out_node_name, out_port_name);
-		if (in_node_name && in_port_name)
-			printf(" in=\"%s\"" " ip=\"%s\"",
-					in_node_name, in_port_name);
-
-		printf("\n");
-	}
-
-	(void)rd;
-
-	return true;
-}
-
-static bool
-dump_session(struct data *data, struct global *global,
-	     enum dump_flags flags, int level)
-{
-	struct remote_data *rd = data->current;
-	struct proxy_data *pd = pw_proxy_get_user_data(global->proxy);
-	struct pw_session_info *info;
-	const char *ind;
-
-	if (!pd->info)
-		return false;
-
-	dump_global_common(data, global, flags, level);
-
-	info = pd->info;
-
-	if (!(flags & is_short)) {
-		ind = INDENT(level + 1);
-		printf("%sproperties:\n", ind);
-		dump_properties(data, global, flags, level);
-		printf("%sparams:\n", ind);
-		dump_params(data, global, info->params, info->n_params, flags, level);
-	} else {
-		printf("\n");
-	}
-
-	(void)rd;
-
-	return true;
-}
-
-static bool
-dump_endpoint(struct data *data, struct global *global,
-	      enum dump_flags flags, int level)
-{
-	struct remote_data *rd = data->current;
-	struct proxy_data *pd = pw_proxy_get_user_data(global->proxy);
-	struct pw_endpoint_info *info;
-	const char *ind;
-	const char *direction;
-
-	if (!pd->info)
-		return false;
-
-	dump_global_common(data, global, flags, level);
-
-	info = pd->info;
-
-	switch(info->direction) {
-	case PW_DIRECTION_OUTPUT:
-		direction = "source";
-		break;
-	case PW_DIRECTION_INPUT:
-		direction = "sink";
-		break;
-	default:
-		direction = "invalid";
-		break;
-	}
-
-	if (!(flags & is_short)) {
-		ind = INDENT(level + 1);
-		printf("%sname: %s\n", ind, info->name);
-		printf("%smedia-class: %s\n", ind, info->media_class);
-		printf("%sdirection: %s\n", ind, direction);
-		printf("%sflags: 0x%x\n", ind, info->flags);
-		printf("%sstreams: %u\n", ind, info->n_streams);
-		printf("%ssession: %u\n", ind, info->session_id);
-		printf("%sproperties:\n", ind);
-		dump_properties(data, global, flags, level);
-		printf("%sparams:\n", ind);
-		dump_params(data, global, info->params, info->n_params, flags, level);
-	} else {
-		printf(" n=\"%s\" c=\"%s\" d=\"%s\" s=%u si=%"PRIu32"",
-				info->name, info->media_class, direction,
-				info->n_streams, info->session_id);
-		printf("\n");
-	}
-
-	(void)rd;
-
-	return true;
-}
-
-static bool
-dump_endpoint_stream(struct data *data, struct global *global,
-		     enum dump_flags flags, int level)
-{
-	struct remote_data *rd = data->current;
-	struct proxy_data *pd = pw_proxy_get_user_data(global->proxy);
-	struct pw_endpoint_stream_info *info;
-	const char *ind;
-
-	if (!pd->info)
-		return false;
-
-	dump_global_common(data, global, flags, level);
-
-	info = pd->info;
-
-	if (!(flags & is_short)) {
-		ind = INDENT(level + 1);
-		printf("%sid: %u\n", ind, info->id);
-		printf("%sendpoint-id: %u\n", ind, info->endpoint_id);
-		printf("%sname: %s\n", ind, info->name);
-		printf("%sproperties:\n", ind);
-		dump_properties(data, global, flags, level);
-		printf("%sparams:\n", ind);
-		dump_params(data, global, info->params, info->n_params, flags, level);
-	} else {
-		printf(" n=\"%s\" i=%"PRIu32" ei=%"PRIu32"",
-				info->name, info->id, info->endpoint_id);
-		printf("\n");
-	}
-
-	(void)rd;
-
-	return true;
-}
-
-static void
-dump(struct data *data, struct global *global,
-     enum dump_flags flags, int level)
-{
-	if (!global)
-		return;
-
-	if (spa_streq(global->type, PW_TYPE_INTERFACE_Core))
-		dump_core(data, global, flags, level);
-
-	if (spa_streq(global->type, PW_TYPE_INTERFACE_Module))
-		dump_module(data, global, flags, level);
-
-	if (spa_streq(global->type, PW_TYPE_INTERFACE_Device))
-		dump_device(data, global, flags, level);
-
-	if (spa_streq(global->type, PW_TYPE_INTERFACE_Node))
-		dump_node(data, global, flags, level);
-
-	if (spa_streq(global->type, PW_TYPE_INTERFACE_Port))
-		dump_port(data, global, flags, level);
-
-	if (spa_streq(global->type, PW_TYPE_INTERFACE_Factory))
-		dump_factory(data, global, flags, level);
-
-	if (spa_streq(global->type, PW_TYPE_INTERFACE_Client))
-		dump_client(data, global, flags, level);
-
-	if (spa_streq(global->type, PW_TYPE_INTERFACE_Link))
-		dump_link(data, global, flags, level);
-
-	if (spa_streq(global->type, PW_TYPE_INTERFACE_Session))
-		dump_session(data, global, flags, level);
-
-	if (spa_streq(global->type, PW_TYPE_INTERFACE_Endpoint))
-		dump_endpoint(data, global, flags, level);
-
-	if (spa_streq(global->type, PW_TYPE_INTERFACE_EndpointStream))
-		dump_endpoint_stream(data, global, flags, level);
-}
-
-static bool do_dump(struct data *data, const char *cmd, char *args, char **error)
-{
-	struct remote_data *rd = data->current;
-	union pw_map_item *item;
-	struct global *global;
-	char *aa[32], **a;
-	char c;
-	int i, n, idx;
-	enum dump_flags flags = is_default;
-	bool match;
-	unsigned int type_mask;
-
-	n = pw_split_ip(args, WHITESPACE, SPA_N_ELEMENTS(aa), aa);
-	if (n < 0)
-		goto usage;
-
-	a = aa;
-	while (n > 0 &&
-		(spa_streq(a[0], "short") ||
-		 spa_streq(a[0], "deep") ||
-		 spa_streq(a[0], "resolve") ||
-		 spa_streq(a[0], "notype"))) {
-		if (spa_streq(a[0], "short"))
-			flags |= is_short;
-		else if (spa_streq(a[0], "deep"))
-			flags |= is_deep;
-		else if (spa_streq(a[0], "resolve"))
-			flags |= is_resolve;
-		else if (spa_streq(a[0], "notype"))
-			flags |= is_notype;
-		n--;
-		a++;
-	}
-
-	while (n > 0 && a[0][0] == '-') {
-		for (i = 1; (c = a[0][i]) != '\0'; i++) {
-			if (c == 's')
-				flags |= is_short;
-			else if (c == 'd')
-				flags |= is_deep;
-			else if (c == 'r')
-				flags |= is_resolve;
-			else if (c == 't')
-				flags |= is_notype;
-			else
-				goto usage;
-		}
-		n--;
-		a++;
-	}
-
-	if (n == 0 || spa_streq(a[0], "all")) {
-		type_mask = (1U << dump_type_count()) - 1;
-		flags &= ~is_notype;
-	} else {
-		type_mask = 0;
-		for (i = 0; i < n; i++) {
-			/* skip direct IDs */
-			if (isdigit(a[i][0]))
-				continue;
-			idx = dump_type_index(name_to_dump_type(a[i]));
-			if (idx < 0)
-				goto usage;
-			type_mask |= 1U << idx;
-		}
-
-		/* single bit set? disable type */
-		if ((type_mask & (type_mask - 1)) == 0)
-			flags |= is_notype;
-	}
-
-	pw_array_for_each(item, &rd->globals.items) {
-		if (pw_map_item_is_free(item) || item->data == NULL)
-			continue;
-
-		global = item->data;
-
-		/* unknown type, ignore completely */
-		idx = dump_type_index(global->type);
-		if (idx < 0)
-			continue;
-
-		match = false;
-
-		/* first check direct ids */
-		for (i = 0; i < n; i++) {
-			/* skip non direct IDs */
-			if (!isdigit(a[i][0]))
-				continue;
-			if (atoi(a[i]) == (int)global->id) {
-				match = true;
-				break;
-			}
-		}
-
-		/* if type match */
-		if (!match && (type_mask & (1U << idx)))
-			match = true;
-
-		if (!match)
-			continue;
-
-		dump(data, global, flags, 0);
-	}
-
-	return true;
-usage:
-	*error = spa_aprintf("%s [short|deep|resolve|notype] [-sdrt] [all|%s|<id>]",
-			cmd, DUMP_NAMES);
-	return false;
-}
-
 static bool parse(struct data *data, char *buf, char **error)
 {
 	char *a[2];
@@ -3040,18 +2139,20 @@ static bool parse(struct data *data, char *buf, char **error)
 }
 
 /* We need a global variable, readline doesn't have a closure arg */
-static struct data *readline_dataptr;
+static struct data *input_dataptr;
 
-static void readline_process_line(char *line)
+static void input_process_line(char *line)
 {
-	struct data *d = readline_dataptr;
+	struct data *d = input_dataptr;
 	char *error;
 
 	if (!line)
 		line = strdup("quit");
 
 	if (line[0] != '\0') {
+#ifdef HAVE_READLINE
 		add_history(line);
+#endif
 		if (!parse(d, line, &error)) {
 			fprintf(stderr, "Error: \"%s\"\n", error);
 			free(error);
@@ -3065,8 +2166,21 @@ static void do_input(void *data, int fd, uint32_t mask)
 	struct data *d = data;
 
 	if (mask & SPA_IO_IN) {
-		readline_dataptr = d;
+		input_dataptr = d;
+#ifdef HAVE_READLINE
 		rl_callback_read_char();
+#else
+		{
+			char *line = NULL;
+			size_t s = 0;
+
+			if (getline(&line, &s, stdin) < 0) {
+				free(line);
+				line = NULL;
+			}
+			input_process_line(line);
+		}
+#endif
 
 		if (d->current == NULL)
 			pw_main_loop_quit(d->loop);
@@ -3078,6 +2192,7 @@ static void do_input(void *data, int fd, uint32_t mask)
 	}
 }
 
+#ifdef HAVE_READLINE
 static char *
 readline_match_command(const char *text, int state)
 {
@@ -3119,13 +2234,14 @@ readline_command_completion(const char *text, int start, int end)
 static void readline_init()
 {
 	rl_attempted_completion_function = readline_command_completion;
-	rl_callback_handler_install(">> ", readline_process_line);
+	rl_callback_handler_install(">> ", input_process_line);
 }
 
 static void readline_cleanup()
 {
 	rl_callback_handler_remove();
 }
+#endif
 
 static void do_quit_on_signal(void *data, int signal_number)
 {
@@ -3152,13 +2268,14 @@ int main(int argc, char *argv[])
 	struct pw_loop *l;
 	char *opt_remote = NULL;
 	char *error;
-	bool daemon = false;
+	bool daemon = false, monitor = false;
 	struct remote_data *rd;
 	static const struct option long_options[] = {
-		{ "help",	no_argument,		 NULL, 'h' },
-		{ "version",	no_argument,		 NULL, 'V' },
-		{ "daemon",	no_argument,		 NULL, 'd' },
-		{ "remote",	required_argument,	 NULL, 'r' },
+		{ "help",	no_argument,		NULL, 'h' },
+		{ "version",	no_argument,		NULL, 'V' },
+		{ "monitor",	no_argument,		NULL, 'm' },
+		{ "daemon",	no_argument,		NULL, 'd' },
+		{ "remote",	required_argument,	NULL, 'r' },
 		{ NULL,	0, NULL, 0}
 	};
 	int c, i;
@@ -3168,7 +2285,7 @@ int main(int argc, char *argv[])
 	setlocale(LC_ALL, "");
 	pw_init(&argc, &argv);
 
-	while ((c = getopt_long(argc, argv, "hVdr:", long_options, NULL)) != -1) {
+	while ((c = getopt_long(argc, argv, "hVmdr:", long_options, NULL)) != -1) {
 		switch (c) {
 		case 'h':
 			show_help(&data, argv[0], false);
@@ -3184,6 +2301,9 @@ int main(int argc, char *argv[])
 		case 'd':
 			daemon = true;
 			break;
+		case 'm':
+			monitor = true;
+			break;
 		case 'r':
 			opt_remote = optarg;
 			break;
@@ -3228,13 +2348,17 @@ int main(int argc, char *argv[])
 		printf("Welcome to PipeWire version %s. Type 'help' for usage.\n",
 				pw_get_library_version());
 
+#ifdef HAVE_READLINE
 		readline_init();
+#endif
 
 		pw_loop_add_io(l, STDIN_FILENO, SPA_IO_IN|SPA_IO_HUP, false, do_input, &data);
 
 		pw_main_loop_run(data.loop);
 
+#ifdef HAVE_READLINE
 		readline_cleanup();
+#endif
 	} else {
 		char buf[4096], *p, *error;
 
@@ -3250,9 +2374,11 @@ int main(int argc, char *argv[])
 			fprintf(stderr, "Error: \"%s\"\n", error);
 			free(error);
 		}
-		if (!data.quit && data.current) {
+		while (!data.quit && data.current) {
 			data.current->prompt_pending = pw_core_sync(data.current->core, 0, 0);
 			pw_main_loop_run(data.loop);
+			if (!monitor)
+				break;
 		}
 	}
 	spa_list_consume(rd, &data.remotes, link)
diff --git a/test/test-spa-json.c b/test/test-spa-json.c
index 8187787fd11fa9adb6f1ab76595732c4bc14f086..2a57039734cc5293af18791f0683a804e355078e 100644
--- a/test/test-spa-json.c
+++ b/test/test-spa-json.c
@@ -86,7 +86,7 @@ static void expect_float(struct spa_json *it, float val)
 {
 	const char *value;
 	int len;
-	float f;
+	float f = 0.0f;
 	pwtest_int_gt((len = spa_json_next(it, &value)), 0);
 	check_type(TYPE_FLOAT, value, len);
 	pwtest_int_gt(spa_json_parse_float(value, len, &f), 0);
@@ -281,6 +281,36 @@ PWTEST(json_float)
 	return PWTEST_PASS;
 }
 
+PWTEST(json_float_check)
+{
+	struct {
+		const char *str;
+		int res;
+	} val[] = {
+		{ "0.0", 1 },
+		{ ".0", 1 },
+		{ "+.0E0", 1 },
+		{ "-.0e0", 1 },
+
+		{ "0,0", 0 },
+		{ "0.0.5", 0 },
+		{ "0x0", 0 },
+		{ "0x0.0", 0 },
+		{ "E10", 0 },
+		{ "e20", 0 },
+		{ " 0.0", 0 },
+		{ "0.0 ", 0 },
+		{ " 0.0 ", 0 },
+	};
+	unsigned i;
+	float v;
+
+	for (i = 0; i < SPA_N_ELEMENTS(val); i++) {
+		pwtest_int_eq(spa_json_parse_float(val[i].str, strlen(val[i].str), &v), val[i].res);
+	}
+	return PWTEST_PASS;
+}
+
 PWTEST(json_int)
 {
 	int v;
@@ -296,6 +326,7 @@ PWTEST_SUITE(spa_json)
 	pwtest_add(json_array, PWTEST_NOARG);
 	pwtest_add(json_overflow, PWTEST_NOARG);
 	pwtest_add(json_float, PWTEST_NOARG);
+	pwtest_add(json_float_check, PWTEST_NOARG);
 	pwtest_add(json_int, PWTEST_NOARG);
 
 	return PWTEST_PASS;
diff --git a/test/test-spa-utils.c b/test/test-spa-utils.c
index 0a750391d1f379c9425b5163b8746e42d0219b1f..2f198f90a7c2b898c1df8ad95ff5a4c2f0c37cec 100644
--- a/test/test-spa-utils.c
+++ b/test/test-spa-utils.c
@@ -419,6 +419,12 @@ PWTEST(utils_hook)
 	}
 	pwtest_int_eq(count, 4);
 	pwtest_int_eq(hook_free_count, 4);
+
+	/* remove a zeroed hook */
+	struct spa_hook hook;
+	spa_zero(hook);
+	spa_hook_remove(&hook);
+
 	return PWTEST_PASS;
 }