diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 61e276c4d35cbae9930cb5dfd68ec98ff78b416d..2ab6a08f301941238644453fa3febd236109252f 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -25,7 +25,7 @@ include:
 .fedora:
   variables:
     # Update this tag when you want to trigger a rebuild
-    FDO_DISTRIBUTION_TAG: '2022-03-05.0'
+    FDO_DISTRIBUTION_TAG: '2022-11-07.0'
     FDO_DISTRIBUTION_VERSION: '35'
     FDO_DISTRIBUTION_PACKAGES: >-
       alsa-lib-devel
@@ -52,6 +52,7 @@ include:
       libv4l-devel
       libva-devel
       libX11-devel
+      ModemManager-devel
       openssl-devel
       pulseaudio-libs-devel
       python3-docutils
@@ -68,6 +69,7 @@ include:
       python3-pip
       pulseaudio-utils
       openal-soft
+      readline-devel
     FDO_DISTRIBUTION_EXEC: >-
       pip3 install meson
 
@@ -105,7 +107,7 @@ include:
 .alpine:
   variables:
     # Update this tag when you want to trigger a rebuild
-    FDO_DISTRIBUTION_TAG: '2022-01-28.2'
+    FDO_DISTRIBUTION_TAG: '2022-09-07.0'
     FDO_DISTRIBUTION_VERSION: '3.15'
     FDO_DISTRIBUTION_PACKAGES: >-
       alsa-lib-dev
@@ -128,6 +130,7 @@ include:
       libusb-dev
       libx11-dev
       meson
+      modemmanager-dev
       ncurses-dev
       pulseaudio-dev
       readline-dev
@@ -176,7 +179,6 @@ include:
     - ninja $NINJA_ARGS -C "$BUILD_DIR"
     - ninja $NINJA_ARGS -C "$BUILD_DIR" test
     - ninja $NINJA_ARGS -C "$BUILD_DIR" install
-    - ./check_missing_headers.sh
   artifacts:
     name: pipewire-$CI_COMMIT_SHA
     when: always
@@ -223,6 +225,8 @@ build_on_ubuntu:
     - .fdo.distribution-image@ubuntu
     - .build
   stage: build
+  variables:
+    MESON_OPTIONS: "-Dsession-managers=[]"
 
 .build_on_fedora:
   extends:
@@ -248,6 +252,7 @@ build_on_fedora:
         -Dvulkan=enabled
         -Dsdl2=enabled
         -Dsndfile=enabled
+        -Dsession-managers=[]
   artifacts:
     name: pipewire-$CI_COMMIT_SHA
     when: always
@@ -262,15 +267,24 @@ build_on_alpine:
     - .fdo.distribution-image@alpine
     - .build
   stage: build
+  variables:
+    MESON_OPTIONS: "-Dsession-managers=[]"
 
 # build with all auto() options enabled
 build_all:
   extends:
     - .build_on_fedora
   variables:
-    # Fedora doesn't have libfreeaptx, lc3plus, or roc
+    # Fedora doesn't have libfreeaptx, lc3plus, lc3, or roc
     # libcamera has no stable API, so let's not chase that target
-    MESON_OPTIONS: "-Dauto_features=enabled -Dbluez5-codec-aptx=disabled -Dbluez5-codec-lc3plus=disabled -Droc=disabled -Dlibcamera=disabled"
+    MESON_OPTIONS: >-
+        -Dauto_features=enabled
+        -Dbluez5-codec-aptx=disabled
+        -Dbluez5-codec-lc3plus=disabled
+        -Dbluez5-codec-lc3=disabled
+        -Droc=disabled
+        -Dlibcamera=disabled
+        -Dsession-managers=[]
   parallel:
     matrix:
       - CC: [gcc, clang]
@@ -280,7 +294,7 @@ build_with_no_commandline_options:
   extends:
     - .build_on_fedora
   variables:
-    MESON_OPTIONS: ""
+    MESON_OPTIONS: "-Dsession-managers=[]"
   parallel:
     matrix:
       - CC: [gcc, clang]
@@ -296,7 +310,7 @@ build_with_custom_options:
         MESON_OPTION_VALUE: [enabled, disabled]
   script:
     - echo "Building with -D$MESON_OPTION=$MESON_OPTION_VALUE"
-    - meson "$BUILD_DIR" . --prefix="$PREFIX" "-D$MESON_OPTION=$MESON_OPTION_VALUE"
+    - meson "$BUILD_DIR" . --prefix="$PREFIX" "-D$MESON_OPTION=$MESON_OPTION_VALUE" -Dsession-managers=[]
     - ninja $NINJA_ARGS -C "$BUILD_DIR"
     - ninja $NINJA_ARGS -C "$BUILD_DIR" test
 
@@ -307,7 +321,7 @@ build_release:
   extends:
     - .build_on_fedora
   variables:
-    MESON_OPTIONS: "-Dtest=enabled -Dbuildtype=release -Db_ndebug=true"
+    MESON_OPTIONS: "-Dtest=enabled -Dbuildtype=release -Db_ndebug=true -Dsession-managers=[]"
   parallel:
     matrix:
       - CC: [gcc, clang]
@@ -363,6 +377,8 @@ valgrind:
     - echo "Building with meson options $MESON_OPTIONS"
     - meson "$BUILD_DIR" . --prefix="$PREFIX" $MESON_OPTIONS
     - meson test -C "$BUILD_DIR" --setup=valgrind
+  variables:
+    MESON_OPTIONS: "-Dsession-managers=[]"
 
 build_with_coverity:
   extends:
@@ -383,6 +399,7 @@ build_with_coverity:
         -Dvulkan=enabled
         -Dsdl2=enabled
         -Dsndfile=enabled
+        -Dsession-managers=[]
     - cov-configure --config coverity_conf.xml
         --comptype gcc --compiler cc --template
         --xml-option=append_arg@C:--ppp_translator
@@ -432,6 +449,18 @@ doccheck:
           git grep -q -e "\\\subpage $page" || (echo "\\page $page is missing \\subpage entry in doc/pipewire-modules.dox" && false)
       done
 
+check_missing_headers:
+  extends:
+    - .fedora
+    - .not_coverity
+    - .fdo.distribution-image@fedora
+  stage: analysis
+  dependencies:
+    - build_on_fedora
+  script:
+    - export PREFIX=`find -name prefix-*`
+    - ./.gitlab/ci/check_missing_headers.sh
+
 pages:
   extends:
     - .not_coverity
diff --git a/check_missing_headers.sh b/.gitlab/ci/check_missing_headers.sh
similarity index 100%
rename from check_missing_headers.sh
rename to .gitlab/ci/check_missing_headers.sh
diff --git a/NEWS b/NEWS
index 72d442f54fffd7b342c03ad877f571490e17d987..f2c768108514ee639799e07b6ae68111d1b2720b 100644
--- a/NEWS
+++ b/NEWS
@@ -1,3 +1,292 @@
+# PipeWire 0.3.61 (2022-11-24)
+
+This is a bugfix release that is API and ABI compatible with previous
+0.3.x releases.
+
+## Highlights
+  - Fix a bug in audioadapter that could cause crashes when switching
+    bluetooth profiles.
+  - Fix sound in QEMU, deadbeef and openal again.
+  - libcamera plugin fixes, dynamic add and remove should now work with
+    the next wireplumber version.
+  - Fix a regression in pw-midiplay where the first buffer would not
+    play and some events would be missing.
+  - The network module now doesn't export other network sources
+    anymore.
+  - pulse-server now detects clients that keep underrunning for a long time
+    and will pause them to save power.
+  - Many more bugfixes and improvements.
+
+
+## PipeWire
+  - Optimize away some useless graph recalculations.
+  - Increase alternative sample rates from 16 to 32.
+  - FreeBSD and musl build fixes.
+  - Silence some module loading errors when the error can be ignored.
+  - Fix initial buffer requested size for pw-stream when operating in
+    async mode. This also indirectly fixes the first buffer in
+    pw-midiplay. (#2843)
+
+## Modules
+  - Set the network property on pulse-tunnel streams so that they are
+    not exported anymore. (#2384)
+  - Filter-chain has optimized mix functions now.
+
+## SPA
+  - Handle some errors in libcamera better.
+  - Fix libcamera remove events. Fix the id allocation for devices.
+  - Fix a bug in audioadapter where it would not renegotiate after
+    a port reconfiguration, leading to crashes, especially when
+    automatically switching profiles in bluetooth. (#2764)
+  - Do ALSA probing in 44100Hz again. Some devices seem to fail
+    otherwise for some unknown reason. (#2718)
+  - Force playback start when the ALSA buffer is full. This fixes sound
+    in QEMU. (#2830)
+  - Support Digital 5.1 AC3 for Asus Xonar SE.
+  - Improve format renegotiation in audioadapter. This makes the ALSA
+    plugin work again for deadbeef. (#2832)
+  - Fix latency reporting on adapter DSP ports.
+
+## pulse-server
+  - Fix a bug where openal based applications would hang. (#2821)
+  - Improve zeroconf publish. Only publish on the address of the first
+    running server. This avoids duplicate entries for IPv4 and IPv6.
+    Add support for republish entries when new servers are started.
+  - Add a pulse.idle.timeout option (default to 5 seconds) to pause
+    streams that have been underrunning for this amount of time. Badly
+    behaving clients will then not keep the graph and device busy so
+    that devices can be suspended to save battery. This should give
+    better default behaviour with speech-dispatcher. (#2839)
+
+## JACK
+  - Add an option to configure the filter character.
+  - Fix connect_callbacks. It was only called once for output ports.
+    (#2841)
+  - Add option to set node.passive on jack clients. Make some quirks
+    for qsynth to make it suspend and fade out better.
+
+
+Older versions:
+
+# PipeWire 0.3.60 (2022-11-10)
+
+This is a bugfix release that is API and ABI compatible with previous
+0.3.x releases.
+
+## Highlights
+  - The filter-chain now handles errors better and has fixes for many
+    crasher bugs.
+  - A new RTP module was added with a sender and receiver. It uses SAP
+    to announce and consume RTP streams and is compatible with the
+    PulseAudio RTP modules.
+  - Many small bluetooth improvements and fixes.
+  - The alsa plugin will now only start playback when there is data. This
+    results in better sync and lower latency between capture and playback.
+  - The v4l2 and libcamera plugins have seen a lot of improvements. They
+    support control properties now. Also pw-v4l2 has seen many improvements
+    and mostly passes the v4l2-compliance test now.
+  - Many more bugfixes and improvements.
+
+
+## PipeWire
+  - Code cleanups, compiler warning fixes.
+  - Add some extra checks to avoid scheduling an inactive node.
+  - Rework the sequence of events to start and stop nodes.
+  - Improve param enumeration.
+  - An option was added to give priority to the Buffer params of the
+    consumer. This makes it possible to use the default values of the
+    consumer (instead of the producer) when capturing from a source.
+  - The graph rate selection was improved to pick a rate closest to the
+    requested one (instead of picking the default).
+
+## Modules
+  - Fix some crashes in filter-chain. (#2737)
+  - X11 Bell module will now be loaded by default when available.
+  - A new RTP module was added with a sender and receiver. It uses SAP
+    to announce and consume RTP streams and is compatible with the
+    PulseAudio RTP modules.
+  - Improve RAOP compatibility.
+  - The echo-cancel module now uses the resampler prefill option to align
+    input and output samples without buffering. Better latency control
+    when starting and stopping has been implemented.
+  - The pulse tunnel will now write aligned samples to pulseaudio even
+    when the ringbuffer wraps around. This fixes playback issues with
+    multichannel sinks.
+  - Add a delay option to module-loopback using a ringbuffer.
+  - Implement echo-cancel params.
+  - The filter-chain module has better error reporting.
+  - The LADSPA search path was extended with some more common paths.
+  - The echo-canceler input can now also be a monitor of a sink. This
+    improves compatibility with some proton games that expect a real
+    sink instead of a virtual one.
+
+## Tools
+  - Better error reporting in pw-link.
+  - pw-top now also shows IEC958 passthrough formats and JPEG/H264 video
+    formats.
+  - pw-top refreshes the screen faster.
+  - pw-top now prints the state of the node and shows less info for
+    inactive nodes.
+  - pw-dump now uses the new seq field in the spa_param_info to discard
+    old param updates and avoid duplicate params in the output.
+
+## Bluetooth
+  - Add ModemManager support in the native backend.
+  - Clean up GetManagedObjects handling.
+  - Handle QoS from the endpoints in the codec.
+  - Increase the socket buffer to have more control over the rate and QoS.
+  - Simplify the packet flushing code.
+  - Stop processing nodes before destroying them.
+  - Fix timers when a source switches drivers.
+  - Codecs can now share endpoints. This reduces the amount of endpoints and
+    avoids problems with devices that can't handle a large amount of
+    codec endpoints.
+  - Report batery status to UPower for HFP AG.
+  - Fix bitpool increase.
+
+## SPA
+  - The audioresampler now avoids clicks and pops between activating and
+    deactivating the adaptive resampler when used by the stream API.
+  - Use default locale to parse float parameters.
+  - The upmix functions now have SSE optimizations.
+  - Avoid recalculating the complete channelmix setup when only the
+    volume changes.
+  - The alsa plugin will now only start playback when there is data. This
+    results in better sync and lower latency between capture and playback.
+  - The ALSA MIDI sequencer will now pull data from the graph even when it
+    did not output anything. Fixes some graph stalls with the sequencer in
+    some cases. (#2775)
+  - v4l2 and libcamera sources now recycle buffers when nothing is consuming
+    them. This avoids stalling the graph.
+  - libcamera now suggests a more appropriate frame size than the smallest
+    poster frame.
+  - Improve state changes in audioconvert. (#2764)
+  - A new seq field was added to spa_param_info to keep track of pending
+    param updates.
+  - Support speaker output only on RealTek ALC4080. (#2744)
+  - The v4l2 source now supports setting controls.
+  - The libcamera plugin now supports enumerating and setting controls.
+  - A new unit test for 6.1 channel mapping was added. (#2809) More debug
+    info was added to audioconvert for the channel matrix.
+  - Audioconvert will now also upmix a rear-center channel when needed.
+
+## pulse-server
+  - Add support for the RTP send and recv modules with the new native
+    RTP module.
+  - Add option to set latency for pulse-tunnel streams and
+    module-zeroconf-discover.
+  - The socket will now be given the same permissions as what pulseaudio
+    did (0777).
+  - Implement module-loopback latency_msec correctly with the new delay
+    parameter.
+  - sysfs.path is now filled with the same data as pulseaudio.
+  - The manager now uses the new seq field in the spa_param_info.
+  - Fix a bug where in some cases the read pointer would get out of sync
+    and cause too large requests. (#2799)
+
+## ALSA
+  - The alsa plugin now reuses the stream in prepare which results in
+    better performance.
+  - Some deadlocks have been fixed in the ALSA plugin.
+  - The ALSA plugin reports more accurate timing information in some cases.
+
+## V4l2
+  - The v4l2 compatibility layer has received a lot of updates.
+  - Improved node names and format enumeration.
+  - Support for multiple /dev/videoX devices, each mapped to a unique
+    PipeWire node.
+  - Passes the v4l2-compliance test now with both the v4l2 and libcamera
+    backend in PipeWire.
+  - Improved mmap support for inline buffer memory. This makes it possible to
+    consume PipeWire streams.
+  - Negotiation works more reliably now.
+
+## JACK
+  - Implement jack_acquire_real_time_scheduling() and
+    jack_drop_real_time_scheduling() by keeping the thread utils in a global
+    state.
+  - Fix jack_client_thread_id() to return NULL when the client is not active,
+    just like jack1 and jack2.
+  - An option was added to let the jack_set_buffer_size() function update the
+    global metadata. A quirk was added so that jack_bufsize uses this new feature
+    to make the buffer size settings persistent and global, just like jack.
+  - jack_port_register() and jack_port_unregister() can be called on an
+    active client so make this thread safe. (#2652)
+
+# PipeWire 0.3.59 (2022-09-30)
+
+This is a bugfix release that is API and ABI compatible with previous
+0.3.x releases.
+
+## Highlights
+  - Fix possible wrong samplerate in loopback streams after suspend and
+    rate switch.
+  - module-filter-chain can now adapt to the graph samplerate.
+  - Fix some potential stuttering and crackling in pulse-server.
+  - Add Bluetooth LE support. This requires experimental kernel and bluez
+    support.
+  - The ALSA plugin has more options to control the buffer size. This can
+    be used to work around high latency in davinci resolve.
+  - Many bugfixes and improvements.
+
+
+## PipeWire
+  - Add audio capture example with volume meter.
+  - Fix a case where a rate switch would not suspend all the nodes of the
+    driver first. This could cause wrong samplerates in streams.
+  - Fix a case where a node would be Paused while still added to the
+    graph, causing potential crashes. (#2701)
+
+## Modules
+  - module-filter-chain and module-loopback now use the resample.prefill
+    option to avoid buffering extra samples and causing unwanted latency
+    when resampling is activated.
+  - module-filter-chain can now adapt to the graph samplerate.
+  - Improve module-raop to support the ALAC codec as raw PCM.
+  - Improve RTSP parsing to improve compatibility.
+
+## Tools
+  - Fix 100% CPU in pw-cli monitor mode. (#2709)
+  - spa-acp-tool can now be exited with ctrl-D.
+
+## SPA
+  - Various libcamera fixes and improvements.
+  - Set stride on audioconvert output buffers.
+  - Make sure we always place the last requested size from the resampler
+    on the buffers in pw-stream.
+  - Add resample.prefill option in the resampler to fill the history with
+    0 so that we don't have smaller buffers at the start.
+  - Make sure that when an overflow corrupts a POD, that it will always
+    stay corrupted.
+  - Rate limit some ALSA warnings and reduce some unwanted warnings.
+  - Don't recalculate the audioconverter state for each pause/play. (#2701)
+  - Fix some POD parsing inconsistencies and potential overflows.
+  - Add support for Asus Xonar SE.
+  - Fix Flush command handling. It should not stop playback. (#2726)
+  - Refactor the peaks function and add some unit tests and optimizations.
+  - The channelmix has an optimized nXm converter and new unit tests.
+  - Normalization in the channelmixer was fixed.
+
+## pulse-server
+  - The requested latency of record streams was reduced to fix some
+    stuttering in Teamspeak. (#2702)
+  - Tweak the max amount of bytes sent to a client. (#2711) (#2715)
+  - Improve maxlength calculations, this fixes some crackling noise with
+    high samplerate and channel counts in some players (audacious).
+
+## Bluetooth
+  - Merge Bluetooth LE support.
+  - Make sure we are backward compatible with WirePlumber.
+  - Fix some HFP and HSP AT command parsing. (#2463)
+  - Use HFP by default over HSP.
+
+## ALSA
+  - Increase max number of periods.
+  - The parameters handling was improved. There is now an option to set the
+    buffer-bytes of the ALSA plugin.
+  - PIPEWIRE_ALSA can now be used as an environment variable to restrict the
+    plugin formats and buffer size.
+
 # PipeWire 0.3.58 (2022-09-15)
 
 This is a bugfix release that is API and ABI compatible with previous
@@ -64,9 +353,6 @@ This is a bugfix release that is API and ABI compatible with previous
     wakeups. (#1697)
 
 
-Older versions:
-
-
 # PipeWire 0.3.57 (2022-09-02)
 
 This is a bugfix release that is API and ABI compatible with previous
diff --git a/doc/pipewire-modules.dox b/doc/pipewire-modules.dox
index 0b2f1f6980add4cbb34cfd7ac1bdc9d12cf23919..dfccbbc3c05428c3d681ca00d35973cfa528320f 100644
--- a/doc/pipewire-modules.dox
+++ b/doc/pipewire-modules.dox
@@ -73,6 +73,8 @@ List of known modules:
 - \subpage page_module_raop_discover
 - \subpage page_module_roc_sink
 - \subpage page_module_roc_source
+- \subpage page_module_rtp_sink
+- \subpage page_module_rtp_source
 - \subpage page_module_rt
 - \subpage page_module_session_manager
 - \subpage page_module_x11_bell
diff --git a/man/pipewire.1.rst.in b/man/pipewire.1.rst.in
index fd07017059bba520c9c224caf2022547c8c0cb08..8b63839696510960f4128cae1d980027d33bfb95 100644
--- a/man/pipewire.1.rst.in
+++ b/man/pipewire.1.rst.in
@@ -47,6 +47,8 @@ PipeWire is available from @PACKAGE_URL@
 SEE ALSO
 ========
 
+``pw-top(1)``,
+``pw-dump(1)``,
 ``pw-mon(1)``,
 ``pw-cat(1)``,
 ``pw-cli(1)``,
diff --git a/man/pw-profiler.1.rst.in b/man/pw-profiler.1.rst.in
index 119d8ceea4ca8004881be87ebe296a1bd8a9d005..6fb57c8eb29856b6c56366592b43099f4290acf1 100644
--- a/man/pw-profiler.1.rst.in
+++ b/man/pw-profiler.1.rst.in
@@ -27,6 +27,8 @@ When this program is stopped, a set of **gnuplot** files and a script to generat
 SVG files from the .plot files is generated, along with a .html file to
 visualize the profiling results in a browser.
 
+This function uses the same data used by *pw-top*.
+
 OPTIONS
 =======
 
@@ -52,3 +54,4 @@ SEE ALSO
 ========
 
 ``pipewire(1)``,
+``pw-top(1)``,
diff --git a/man/pw-top.1.rst.in b/man/pw-top.1.rst.in
index afd3573475a8dba839761cbff1a1d395f508cd28..ab8569b1fff750bc9b50ccf5b9a5d5194e081f1b 100644
--- a/man/pw-top.1.rst.in
+++ b/man/pw-top.1.rst.in
@@ -27,13 +27,15 @@ a tree-like representation.
 The columns presented are as follows:
 
 S
-  Measurement status.
-  ! representing inactive - no connections
-
-  Blank representing active
+  Node status.
+    E = ERROR
+    C = CREATING
+    S = SUSPENDED
+    I = IDLE
+    R = RUNNING
 
 ID
-  The ID of the pipewire node/device, as found in *pw-dump*
+  The ID of the pipewire node/device, as found in *pw-dump* and *pw-cli*
 
 QUANT
   The current quantum (for drivers) and the suggested quantum for follower
@@ -135,6 +137,8 @@ FORMAT
 
   For raw audio formats, the layout is <sampleformat> <channels> <samplerate>.
 
+  For IEC958 passthrough audio formats, the layout is IEC958 <codec> <samplerate>.
+
   For DSD formats, the layout is <dsd-rate> <channels>.
 
   For Video formats, the layout is <pixelformat> <width>x<height>.
@@ -168,4 +172,7 @@ SEE ALSO
 ========
 
 ``pipewire(1)``,
+``pw-dump(1)``,
+``pw-cli(1)``,
+``pw-profiler(1)``,
 
diff --git a/meson.build b/meson.build
index e7a1d8590183df74c7dfe50b8749b8a530915783..5420c10edf1d7f5d4c523d09c4f3999ec1a201af 100644
--- a/meson.build
+++ b/meson.build
@@ -1,9 +1,9 @@
 project('pipewire', ['c' ],
-  version : '0.3.58',
+  version : '0.3.61',
   license : [ 'MIT', 'LGPL-2.1-or-later', 'GPL-2.0-only' ],
   meson_version : '>= 0.59.0',
   default_options : [ 'warning_level=3',
-                      'c_std=gnu99',
+                      'c_std=gnu11',
                       'cpp_std=c++17',
                       'b_pie=true',
                       #'b_sanitize=address,undefined',
@@ -67,6 +67,7 @@ cc = meson.get_compiler('c')
 
 common_flags = [
   '-fvisibility=hidden',
+  '-fno-strict-aliasing',
   '-Werror=suggest-attribute=format',
   '-Wsign-compare',
   '-Wpointer-arith',
@@ -97,7 +98,7 @@ have_cpp = add_languages('cpp', native: false, required : false)
 
 if have_cpp
   cxx = meson.get_compiler('cpp')
-  cxx_flags = common_flags
+  cxx_flags = common_flags + [ '-Wno-c99-designator' ]
   add_project_arguments(cxx.get_supported_arguments(cxx_flags), language: 'cpp')
 endif
 
@@ -259,10 +260,10 @@ cdata.set('HAVE_DBUS', dbus_dep.found())
 sdl_dep = dependency('sdl2', required : get_option('sdl2'))
 summary({'SDL2 (video examples)': sdl_dep.found()}, bool_yn: true, section: 'Misc dependencies')
 drm_dep = dependency('libdrm', required : false)
-readline_dep = dependency('readline', required : false)
+readline_dep = dependency('readline', required : get_option('readline'))
 
 if not readline_dep.found()
-  readline_dep = cc.find_library('readline', required: false)
+  readline_dep = cc.find_library('readline', required : get_option('readline'))
 endif
 
 summary({'readline (for pw-cli)': readline_dep.found()}, bool_yn: true, section: 'Misc dependencies')
diff --git a/meson_options.txt b/meson_options.txt
index e6a5623e02039d23cd0fa070f44939ada40fdf49..f306ecbf92449ae98b273e963467cb9cc4699330 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -92,6 +92,10 @@ option('bluez5-backend-hfp-native',
        description: 'Enable HFP in native backend in bluez5 spa plugin',
        type: 'feature',
        value: 'enabled')
+option('bluez5-backend-native-mm',
+       description: 'Enable ModemManager in native backend in bluez5 spa plugin',
+       type: 'feature',
+       value: 'disabled')
 option('bluez5-backend-ofono',
        description: 'Enable oFono HFP backend in bluez5 spa plugin (no dependency on oFono)',
        type: 'feature',
@@ -120,6 +124,10 @@ option('bluez5-codec-opus',
         description: 'Enable Opus open source codec implementation',
         type: 'feature',
         value: 'auto')
+option('bluez5-codec-lc3',
+        description: 'Enable LC3 open source codec implementation',
+        type: 'feature',
+        value: 'disabled')
 option('control',
        description: 'Enable control spa plugin integration',
        type: 'feature',
@@ -224,7 +232,7 @@ option('libusb',
 option('session-managers',
        description : 'Session managers to build (can be [] for none or an absolute path)',
        type : 'array',
-       value : ['media-session'])
+       value : ['wireplumber'])
 option('raop',
        description: 'Enable module for Remote Audio Output Protocol',
        type: 'feature',
@@ -257,3 +265,7 @@ option('flatpak',
        description: 'Enable Flatpak support',
        type: 'feature',
        value: 'enabled')
+option('readline',
+       description: 'Enable code that depends on libreadline',
+       type: 'feature',
+       value: 'auto')
diff --git a/pipewire-alsa/alsa-plugins/pcm_pipewire.c b/pipewire-alsa/alsa-plugins/pcm_pipewire.c
index e3c58ccef4f87d21e9c87173f76687634a1b68c0..37a907b24daccda8b90217c49c976207d4492e40 100644
--- a/pipewire-alsa/alsa-plugins/pcm_pipewire.c
+++ b/pipewire-alsa/alsa-plugins/pcm_pipewire.c
@@ -64,6 +64,26 @@ PW_LOG_TOPIC_STATIC(alsa_log_topic, "alsa.pcm");
 
 #define MIN_PERIOD	64
 
+#define MIN_PERIOD_BYTES	(128)
+#define MAX_PERIOD_BYTES	(2*1024*1024)
+
+#define MIN_BUFFER_BYTES	(2*MIN_PERIOD_BYTES)
+#define MAX_BUFFER_BYTES	(2*MAX_PERIOD_BYTES)
+
+struct params {
+	const char *node_name;
+	const char *server_name;
+	const char *playback_node;
+	const char *capture_node;
+	const char *role;
+	snd_pcm_format_t format;
+	int rate;
+	int channels;
+	int period_bytes;
+	int buffer_bytes;
+	uint32_t flags;
+};
+
 typedef struct {
 	snd_pcm_ioplug_t io;
 
@@ -103,7 +123,9 @@ typedef struct {
 	struct spa_hook stream_listener;
 
 	int64_t delay;
-	uint64_t now;
+	uint64_t transfered;
+	uint64_t buffered;
+	int64_t now;
 	uintptr_t seq;
 
 	struct spa_audio_info_raw format;
@@ -143,21 +165,15 @@ static int check_active(snd_pcm_ioplug_t *io)
 static int update_active(snd_pcm_ioplug_t *io)
 {
 	snd_pcm_pipewire_t *pw = io->private_data;
-	bool active;
-
-	active = check_active(io);
-
-	if (pw->active != active) {
-		uint64_t val;
+	pw->active = check_active(io);
+	uint64_t val;
 
-		pw->active = active;
+	if (pw->active || pw->error < 0)
+		spa_system_eventfd_write(pw->system, io->poll_fd, 1);
+	else
+		spa_system_eventfd_read(pw->system, io->poll_fd, &val);
 
-		if (active)
-			spa_system_eventfd_write(pw->system, io->poll_fd, 1);
-		else
-			spa_system_eventfd_read(pw->system, io->poll_fd, &val);
-	}
-	return active;
+	return pw->active;
 }
 
 static void snd_pcm_pipewire_free(snd_pcm_pipewire_t *pw)
@@ -178,6 +194,7 @@ static void snd_pcm_pipewire_free(snd_pcm_pipewire_t *pw)
 		pw_thread_loop_destroy(pw->main_loop);
 	free(pw->node_name);
 	free(pw->target);
+	free(pw->role);
 	snd_output_close(pw->output);
 	fclose(pw->log_file);
 	free(pw);
@@ -212,8 +229,10 @@ static int snd_pcm_pipewire_poll_revents(snd_pcm_ioplug_t *io,
 		return pw->error;
 
 	*revents = pfds[0].revents & ~(POLLIN | POLLOUT);
-	if (pfds[0].revents & POLLIN && check_active(io))
+	if (pfds[0].revents & POLLIN && check_active(io)) {
 		*revents |= (io->stream == SND_PCM_STREAM_PLAYBACK) ? POLLOUT : POLLIN;
+		update_active(io);
+	}
 
 	return 0;
 }
@@ -245,7 +264,7 @@ static int snd_pcm_pipewire_delay(snd_pcm_ioplug_t *io, snd_pcm_sframes_t *delay
 	do {
 		seq1 = SEQ_READ(pw->seq);
 
-		delay = pw->delay;
+		delay = pw->delay + pw->transfered;
 		now = pw->now;
 		if (io->stream == SND_PCM_STREAM_PLAYBACK)
 			avail = snd_pcm_ioplug_hw_avail(io, pw->hw_ptr, io->appl_ptr);
@@ -429,9 +448,8 @@ static void on_stream_process(void *data)
 	pw_stream_get_time_n(pw->stream, &pwt, sizeof(pwt));
 
 	delay = pwt.delay;
-	if (pwt.rate.num != 0) {
+	if (pwt.rate.num != 0)
 		delay = delay * io->rate * pwt.rate.num / pwt.rate.denom;
-	}
 
 	before = hw_avail = snd_pcm_ioplug_hw_avail(io, pw->hw_ptr, io->appl_ptr);
 
@@ -446,12 +464,20 @@ static void on_stream_process(void *data)
 
 	SEQ_WRITE(pw->seq);
 
+	if (pw->now != pwt.now) {
+		pw->transfered = pw->buffered;
+		pw->buffered = 0;
+	}
+
 	xfer = snd_pcm_pipewire_process(pw, b, &hw_avail, want);
 
 	pw->delay = delay;
 	/* the buffer is now queued in the stream and consumed */
 	if (io->stream == SND_PCM_STREAM_PLAYBACK)
-		pw->delay += xfer;
+		pw->transfered += xfer;
+
+	/* more then requested data transfered, use them in next iteration */
+	pw->buffered = (want == 0 || pw->transfered < want) ?  0 : (pw->transfered % want);
 
 	pw->now = pwt.now;
 	SEQ_WRITE(pw->seq);
@@ -542,11 +568,6 @@ static int snd_pcm_pipewire_prepare(snd_pcm_ioplug_t *io)
 		goto done;
 	pw->hw_params_changed = false;
 
-	if (pw->stream != NULL) {
-		pw_stream_destroy(pw->stream);
-		pw->stream = NULL;
-	}
-
 	props = pw_properties_new(NULL, NULL);
 	if (props == NULL)
 		goto error;
@@ -572,13 +593,20 @@ static int snd_pcm_pipewire_prepare(snd_pcm_ioplug_t *io)
 		pw_properties_get(props, PW_KEY_MEDIA_ROLE) == NULL)
 		pw_properties_setf(props, PW_KEY_MEDIA_ROLE, "%s", pw->role);
 
+	params[0] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat, &pw->format);
+
+	if (pw->stream != NULL) {
+		pw_stream_update_properties(pw->stream, &props->dict);
+		pw_stream_update_params(pw->stream, params, 1);
+		goto done;
+	}
+
 	pw->stream = pw_stream_new(pw->core, pw->node_name, props);
 	if (pw->stream == NULL)
 		goto error;
 
 	pw_stream_add_listener(pw->stream, &pw->stream_listener, &stream_events, pw);
 
-	params[0] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat, &pw->format);
 	pw->error = 0;
 
 	pw_stream_connect(pw->stream,
@@ -846,10 +874,9 @@ static const struct chmap_info chmap_info[] = {
 
 static enum snd_pcm_chmap_position channel_to_chmap(enum spa_audio_channel channel)
 {
-	uint32_t i;
-	for (i = 0; i < SPA_N_ELEMENTS(chmap_info); i++)
-		if (chmap_info[i].channel == channel)
-			return chmap_info[i].pos;
+	SPA_FOR_EACH_ELEMENT_VAR(chmap_info, info)
+		if (info->channel == channel)
+			return info->pos;
 	return SND_CHMAP_UNKNOWN;
 }
 
@@ -942,8 +969,7 @@ static snd_pcm_ioplug_callback_t pipewire_pcm_callback = {
 	.query_chmaps = snd_pcm_pipewire_query_chmaps,
 };
 
-static int pipewire_set_hw_constraint(snd_pcm_pipewire_t *pw, int rate,
-		snd_pcm_format_t format, int channels, int period_bytes)
+static int pipewire_set_hw_constraint(snd_pcm_pipewire_t *pw, struct params *p)
 {
 	unsigned int access_list[] = {
 		SND_PCM_ACCESS_MMAP_INTERLEAVED,
@@ -975,26 +1001,38 @@ static int pipewire_set_hw_constraint(snd_pcm_pipewire_t *pw, int rate,
 	int max_channels;
 	int min_period_bytes;
 	int max_period_bytes;
+	int min_buffer_bytes;
+	int max_buffer_bytes;
 	int err;
 
-	if (rate > 0) {
-		min_rate = max_rate = rate;
+	if (p->rate > 0) {
+		min_rate = max_rate = SPA_CLAMP(p->rate, 1, MAX_RATE);
 	} else {
 		min_rate = 1;
 		max_rate = MAX_RATE;
 	}
-	if (channels > 0) {
-		min_channels = max_channels = channels;
+	if (p->channels > 0) {
+		min_channels = max_channels = SPA_CLAMP(p->channels, 1, MAX_CHANNELS);
 	} else {
 		min_channels = 1;
 		max_channels = MAX_CHANNELS;
 	}
-	if (period_bytes > 0) {
-		min_period_bytes = max_period_bytes = period_bytes;
+	if (p->period_bytes > 0) {
+		min_period_bytes = max_period_bytes = SPA_CLAMP(p->period_bytes,
+				MIN_PERIOD_BYTES, MAX_PERIOD_BYTES);
+	} else {
+		min_period_bytes = MIN_PERIOD_BYTES;
+		max_period_bytes = MAX_PERIOD_BYTES;
+	}
+	if (p->buffer_bytes > 0) {
+		min_buffer_bytes = max_buffer_bytes = SPA_CLAMP(p->buffer_bytes,
+				MIN_BUFFER_BYTES, MAX_BUFFER_BYTES);
 	} else {
-		min_period_bytes = 128;
-		max_period_bytes = 2*1024*1024;
+		min_buffer_bytes = MIN_BUFFER_BYTES;
+		max_buffer_bytes = MAX_BUFFER_BYTES;
 	}
+	if (min_period_bytes * 2 > max_buffer_bytes)
+		min_period_bytes = max_period_bytes = max_buffer_bytes / 2;
 
 	if ((err = snd_pcm_ioplug_set_param_list(&pw->io, SND_PCM_IOPLUG_HW_ACCESS,
 						   SPA_N_ELEMENTS(access_list), access_list)) < 0 ||
@@ -1003,22 +1041,22 @@ static int pipewire_set_hw_constraint(snd_pcm_pipewire_t *pw, int rate,
 		(err = snd_pcm_ioplug_set_param_minmax(&pw->io, SND_PCM_IOPLUG_HW_RATE,
 						   min_rate, max_rate)) < 0 ||
 		(err = snd_pcm_ioplug_set_param_minmax(&pw->io, SND_PCM_IOPLUG_HW_BUFFER_BYTES,
-						   MIN_BUFFERS*min_period_bytes,
-						   MIN_BUFFERS*max_period_bytes)) < 0 ||
+						   min_buffer_bytes,
+						   max_buffer_bytes)) < 0 ||
 		(err = snd_pcm_ioplug_set_param_minmax(&pw->io,
 						   SND_PCM_IOPLUG_HW_PERIOD_BYTES,
 						   min_period_bytes,
 						   max_period_bytes)) < 0 ||
 		(err = snd_pcm_ioplug_set_param_minmax(&pw->io, SND_PCM_IOPLUG_HW_PERIODS,
-						   MIN_BUFFERS, MAX_BUFFERS)) < 0) {
+						   MIN_BUFFERS, 1024)) < 0) {
 		pw_log_warn("Can't set param list: %s", snd_strerror(err));
 		return err;
 	}
 
-	if (format != SND_PCM_FORMAT_UNKNOWN) {
+	if (p->format != SND_PCM_FORMAT_UNKNOWN) {
 		err = snd_pcm_ioplug_set_param_list(&pw->io,
 				SND_PCM_IOPLUG_HW_FORMAT,
-				1, (unsigned int *)&format);
+				1, (unsigned int *)&p->format);
 		if (err < 0) {
 			pw_log_warn("Can't set param list: %s", snd_strerror(err));
 			return err;
@@ -1075,60 +1113,77 @@ static cookie_io_functions_t io_funcs = {
 	.write = log_write,
 };
 
-static int snd_pcm_pipewire_open(snd_pcm_t **pcmp, const char *name,
-				const char *node_name,
-				const char *server_name,
-				const char *playback_node,
-				const char *capture_node,
-				const char *role,
-				snd_pcm_stream_t stream,
-				int mode,
-				uint32_t flags,
-				int rate,
-				snd_pcm_format_t format,
-				int channels,
-				int period_bytes)
+static int snd_pcm_pipewire_open(snd_pcm_t **pcmp,
+		struct params *p, snd_pcm_stream_t stream, int mode)
 {
 	snd_pcm_pipewire_t *pw;
 	int err;
 	const char *str;
 	struct pw_properties *props = NULL;
 	struct pw_loop *loop;
+	uint32_t val;
 
 	assert(pcmp);
 	pw = calloc(1, sizeof(*pw));
 	if (!pw)
 		return -ENOMEM;
 
+	props = pw_properties_new(NULL, NULL);
+	if (props == NULL) {
+		err = -errno;
+		goto error;
+	}
+
+	str = getenv("PIPEWIRE_ALSA");
+	if (str != NULL) {
+		pw_properties_update_string(props, str, strlen(str));
+		if ((str = pw_properties_get(props, "alsa.format")))
+			p->format = snd_pcm_format_value(str);
+		if ((str = pw_properties_get(props, "alsa.rate")) &&
+		    spa_atou32(str, &val, 0))
+			p->rate = val;
+		if ((str = pw_properties_get(props, "alsa.channels")) &&
+		    spa_atou32(str, &val, 0))
+			p->channels = val;
+		if ((str = pw_properties_get(props, "alsa.period-bytes")) &&
+		    spa_atou32(str, &val, 0))
+			p->period_bytes = val;
+		if ((str = pw_properties_get(props, "alsa.buffer-bytes")) &&
+		    spa_atou32(str, &val, 0))
+			p->buffer_bytes = val;
+	}
+
 	str = getenv("PIPEWIRE_REMOTE");
 	if (str != NULL && str[0] != '\0')
-		server_name = str;
+		p->server_name = str;
 
 	str = getenv("PIPEWIRE_NODE");
 
 	pw_log_debug("%p: open name:%s stream:%s mode:%d flags:%08x rate:%d format:%s "
-			"channels:%d period-bytes:%d target:'%s'", pw, name,
-			snd_pcm_stream_name(stream), mode, flags, rate,
-			snd_pcm_format_name(format), channels, period_bytes, str);
+			"channels:%d period-bytes:%d buffer-bytes:%d target:'%s'", pw, p->node_name,
+			snd_pcm_stream_name(stream), mode, p->flags, p->rate,
+			snd_pcm_format_name(p->format), p->channels, p->period_bytes,
+			p->buffer_bytes, str);
 
 	pw->fd = -1;
 	pw->io.poll_fd = -1;
-	pw->flags = flags;
+	pw->flags = p->flags;
 	pw->log_file = fopencookie(pw, "w", io_funcs);
 	if (pw->log_file == NULL) {
 		pw_log_error("can't create log file: %m");
-		return -errno;
+		err = -errno;
+		goto error;
 	}
 	if ((err = snd_output_stdio_attach(&pw->output, pw->log_file, 0)) < 0) {
 		pw_log_error("can't attach log file: %s", snd_strerror(err));
-		return err;
+		goto error;
 	}
 
-	if (node_name == NULL)
+	if (p->node_name == NULL)
 		pw->node_name = spa_aprintf("ALSA %s",
 			       stream == SND_PCM_STREAM_PLAYBACK ? "Playback" : "Capture");
 	else
-		pw->node_name = strdup(node_name);
+		pw->node_name = strdup(p->node_name);
 
 	if (pw->node_name == NULL) {
 		err = -errno;
@@ -1140,12 +1195,12 @@ static int snd_pcm_pipewire_open(snd_pcm_t **pcmp, const char *name,
 		pw->target = strdup(str);
 	else {
 		if (stream == SND_PCM_STREAM_PLAYBACK)
-			pw->target = playback_node ? strdup(playback_node) : NULL;
+			pw->target = p->playback_node ? strdup(p->playback_node) : NULL;
 		else
-			pw->target = capture_node ? strdup(capture_node) : NULL;
+			pw->target = p->capture_node ? strdup(p->capture_node) : NULL;
 	}
 
-	pw->role = (role && *role) ? strdup(role) : NULL;
+	pw->role = (p->role && *p->role) ? strdup(p->role) : NULL;
 
 	pw->main_loop = pw_thread_loop_new("alsa-pipewire", NULL);
 	if (pw->main_loop == NULL) {
@@ -1163,13 +1218,11 @@ static int snd_pcm_pipewire_open(snd_pcm_t **pcmp, const char *name,
 		goto error;
 	}
 
-	props = pw_properties_new(NULL, NULL);
-
 	pw_properties_setf(props, PW_KEY_APP_NAME, "PipeWire ALSA [%s]",
 			pw_get_prgname());
 
-	if (server_name)
-		pw_properties_set(props, PW_KEY_REMOTE_NAME, server_name);
+	if (p->server_name)
+		pw_properties_set(props, PW_KEY_REMOTE_NAME, p->server_name);
 
 	if ((err = pw_thread_loop_start(pw->main_loop)) < 0)
 		goto error;
@@ -1201,15 +1254,13 @@ static int snd_pcm_pipewire_open(snd_pcm_t **pcmp, const char *name,
 #endif
 	pw->io.flags |= SND_PCM_IOPLUG_FLAG_MONOTONIC;
 
-	if ((err = snd_pcm_ioplug_create(&pw->io, name, stream, mode)) < 0)
+	if ((err = snd_pcm_ioplug_create(&pw->io, p->node_name, stream, mode)) < 0)
 		goto error;
 
-
-	if ((err = pipewire_set_hw_constraint(pw, rate, format, channels,
-					period_bytes)) < 0)
+	if ((err = pipewire_set_hw_constraint(pw, p)) < 0)
 		goto error;
 
-	pw_log_debug("%p: opened name:%s stream:%s mode:%d", pw, name,
+	pw_log_debug("%p: opened name:%s stream:%s mode:%d", pw, p->node_name,
 			snd_pcm_stream_name(pw->io.stream), mode);
 
 	*pcmp = pw->io.pcm;
@@ -1217,7 +1268,7 @@ static int snd_pcm_pipewire_open(snd_pcm_t **pcmp, const char *name,
 	return 0;
 
 error:
-	pw_log_debug("%p: failed to open %s :%s", pw, name, spa_strerror(err));
+	pw_log_debug("%p: failed to open %s :%s", pw, p->node_name, spa_strerror(err));
 	pw_properties_free(props);
 	snd_pcm_pipewire_free(pw);
 	return err;
@@ -1228,16 +1279,7 @@ SPA_EXPORT
 SND_PCM_PLUGIN_DEFINE_FUNC(pipewire)
 {
 	snd_config_iterator_t i, next;
-	const char *node_name = NULL;
-	const char *server_name = NULL;
-	const char *playback_node = NULL;
-	const char *capture_node = NULL;
-	const char *role = NULL;
-	snd_pcm_format_t format = SND_PCM_FORMAT_UNKNOWN;
-	int rate = 0;
-	int channels = 0;
-	int period_bytes = 0;
-	uint32_t flags = 0;
+	struct params params;
 	int err;
 
 	pw_init(NULL, NULL);
@@ -1245,6 +1287,8 @@ SND_PCM_PLUGIN_DEFINE_FUNC(pipewire)
 		return -ENOTSUP;
 
 	PW_LOG_TOPIC_INIT(alsa_log_topic);
+	spa_zero(params);
+	params.format = SND_PCM_FORMAT_UNKNOWN;
 
 	snd_config_for_each(i, next, conf) {
 		snd_config_t *n = snd_config_iterator_entry(i);
@@ -1254,35 +1298,35 @@ SND_PCM_PLUGIN_DEFINE_FUNC(pipewire)
 		if (spa_streq(id, "comment") || spa_streq(id, "type") || spa_streq(id, "hint"))
 			continue;
 		if (spa_streq(id, "name")) {
-			snd_config_get_string(n, &node_name);
+			snd_config_get_string(n, &params.node_name);
 			continue;
 		}
 		if (spa_streq(id, "server")) {
-			snd_config_get_string(n, &server_name);
+			snd_config_get_string(n, &params.server_name);
 			continue;
 		}
 		if (spa_streq(id, "playback_node")) {
-			snd_config_get_string(n, &playback_node);
+			snd_config_get_string(n, &params.playback_node);
 			continue;
 		}
 		if (spa_streq(id, "capture_node")) {
-			snd_config_get_string(n, &capture_node);
+			snd_config_get_string(n, &params.capture_node);
 			continue;
 		}
 		if (spa_streq(id, "role")) {
-			snd_config_get_string(n, &role);
+			snd_config_get_string(n, &params.role);
 			continue;
 		}
 		if (spa_streq(id, "exclusive")) {
 			if (snd_config_get_bool(n))
-				flags |= PW_STREAM_FLAG_EXCLUSIVE;
+				params.flags |= PW_STREAM_FLAG_EXCLUSIVE;
 			continue;
 		}
 		if (spa_streq(id, "rate")) {
 			long val;
 
 			if (snd_config_get_integer(n, &val) == 0)
-				rate = val;
+				params.rate = val;
 			else
 				SNDERR("%s: invalid type", id);
 			continue;
@@ -1291,8 +1335,8 @@ SND_PCM_PLUGIN_DEFINE_FUNC(pipewire)
 			const char *str;
 
 			if (snd_config_get_string(n, &str) == 0) {
-				format = snd_pcm_format_value(str);
-				if (format == SND_PCM_FORMAT_UNKNOWN)
+				params.format = snd_pcm_format_value(str);
+				if (*str && params.format == SND_PCM_FORMAT_UNKNOWN)
 					SNDERR("%s: invalid value %s", id, str);
 			} else {
 				SNDERR("%s: invalid type", id);
@@ -1303,7 +1347,7 @@ SND_PCM_PLUGIN_DEFINE_FUNC(pipewire)
 			long val;
 
 			if (snd_config_get_integer(n, &val) == 0)
-				channels = val;
+				params.channels = val;
 			else
 				SNDERR("%s: invalid type", id);
 			continue;
@@ -1312,7 +1356,16 @@ SND_PCM_PLUGIN_DEFINE_FUNC(pipewire)
 			long val;
 
 			if (snd_config_get_integer(n, &val) == 0)
-				period_bytes = val;
+				params.period_bytes = val;
+			else
+				SNDERR("%s: invalid type", id);
+			continue;
+		}
+		if (spa_streq(id, "buffer_bytes")) {
+			long val;
+
+			if (snd_config_get_integer(n, &val) == 0)
+				params.buffer_bytes = val;
 			else
 				SNDERR("%s: invalid type", id);
 			continue;
@@ -1321,9 +1374,7 @@ SND_PCM_PLUGIN_DEFINE_FUNC(pipewire)
 		return -EINVAL;
 	}
 
-	err = snd_pcm_pipewire_open(pcmp, name, node_name, server_name, playback_node,
-			capture_node, role, stream, mode, flags, rate, format,
-			channels, period_bytes);
+	err = snd_pcm_pipewire_open(pcmp, &params, stream, mode);
 
 	return err;
 }
diff --git a/pipewire-alsa/conf/50-pipewire.conf b/pipewire-alsa/conf/50-pipewire.conf
index f7e58472bda1e0b003c5a1046a762068b1e2d1f2..a3a08a61c98a00e71f2cde5b2c54369c7dd42330 100644
--- a/pipewire-alsa/conf/50-pipewire.conf
+++ b/pipewire-alsa/conf/50-pipewire.conf
@@ -4,9 +4,14 @@ defaults.pipewire.server "pipewire-0"
 defaults.pipewire.node "-1"
 defaults.pipewire.exclusive false
 defaults.pipewire.role ""
+defaults.pipewire.rate 0
+defaults.pipewire.format ""
+defaults.pipewire.channels 0
+defaults.pipewire.period_bytes 0
+defaults.pipewire.buffer_bytes 0
 
 pcm.pipewire {
-	@args [ SERVER NODE EXCLUSIVE ROLE ]
+	@args [ SERVER NODE EXCLUSIVE ROLE RATE FORMAT CHANNELS PERIOD_BYTES BUFFER_BYTES ]
 	@args.SERVER {
 		type string
 		default {
@@ -35,7 +40,41 @@ pcm.pipewire {
 			name defaults.pipewire.role
 		}
 	}
-
+	@args.RATE {
+		type integer
+		default {
+			@func refer
+			name defaults.pipewire.rate
+		}
+	}
+	@args.FORMAT {
+		type string
+		default {
+			@func refer
+			name defaults.pipewire.format
+		}
+	}
+	@args.CHANNELS {
+		type integer
+		default {
+			@func refer
+			name defaults.pipewire.channels
+		}
+	}
+	@args.PERIOD_BYTES {
+		type integer
+		default {
+			@func refer
+			name defaults.pipewire.period_bytes
+		}
+	}
+	@args.BUFFER_BYTES {
+		type integer
+		default {
+			@func refer
+			name defaults.pipewire.buffer_bytes
+		}
+	}
 
 	type pipewire
 	server $SERVER
@@ -43,6 +82,11 @@ pcm.pipewire {
 	capture_node $NODE
 	exclusive $EXCLUSIVE
 	role $ROLE
+	rate $RATE
+	format $FORMAT
+	channels $CHANNELS
+	period_bytes $PERIOD_BYTES
+	buffer_bytes $BUFFER_BYTES
 	hint {
 		show on
 		description "PipeWire Sound Server"
diff --git a/pipewire-jack/jack/uuid.h b/pipewire-jack/jack/uuid.h
index faa354b56f4d7492063fdfdbd4c111bc5778f73b..406c1195a85fc23ef02c85b1e0ba1e4f6fd12172 100644
--- a/pipewire-jack/jack/uuid.h
+++ b/pipewire-jack/jack/uuid.h
@@ -1,18 +1,18 @@
 /*
     Copyright (C) 2013 Paul Davis
-    
+
     This program 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.
-    
+
     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 Lesser General Public License for more details.
-    
+
     You should have received a copy of the GNU Lesser General Public License
-    along with this program; if not, write to the Free Software 
+    along with this program; if not, write to the Free Software
     Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 
 */
@@ -30,7 +30,7 @@ extern "C" {
 #define JACK_UUID_STRING_SIZE (JACK_UUID_SIZE+1) /* includes trailing null */
 #define JACK_UUID_EMPTY_INITIALIZER 0
 
-extern jack_uuid_t jack_client_uuid_generate ();
+extern jack_uuid_t jack_client_uuid_generate (void);
 extern jack_uuid_t jack_port_uuid_generate (uint32_t port_id);
 
 extern uint32_t jack_uuid_to_index (jack_uuid_t);
diff --git a/pipewire-jack/src/pipewire-jack-extensions.h b/pipewire-jack/src/pipewire-jack-extensions.h
index f75f5d31e14bb4c0c971f47ec349eb820d80e5a7..8e38e9e6f001d411546aea9cd6c92f51a6140a8b 100644
--- a/pipewire-jack/src/pipewire-jack-extensions.h
+++ b/pipewire-jack/src/pipewire-jack-extensions.h
@@ -24,6 +24,7 @@
 
 #ifndef PIPEWIRE_JACK_EXTENSIONS_H
 #define PIPEWIRE_JACK_EXTENSIONS_H
+#include <stdint.h>
 
 #ifdef __cplusplus
 extern "C" {
diff --git a/pipewire-jack/src/pipewire-jack.c b/pipewire-jack/src/pipewire-jack.c
index 7100a41b088b5d0c84aca37d99503ac17120db1b..98eede7de469ad72c999a1f40ea5977f661f2a49 100644
--- a/pipewire-jack/src/pipewire-jack.c
+++ b/pipewire-jack/src/pipewire-jack.c
@@ -98,6 +98,7 @@ struct globals {
 	pthread_mutex_t lock;
 	struct pw_array descriptions;
 	struct spa_list free_objects;
+	struct spa_thread_utils *thread_utils;
 };
 
 static struct globals globals;
@@ -311,6 +312,7 @@ struct client {
         struct spa_hook proxy_listener;
 
 	struct metadata *metadata;
+	struct metadata *settings;
 
 	uint32_t node_id;
 	uint32_t serial;
@@ -401,6 +403,8 @@ struct client {
 	int self_connect_mode;
 	int rt_max;
 	unsigned int fix_midi_events:1;
+	unsigned int global_buffer_size:1;
+	char filter_char;
 
 	jack_position_t jack_position;
 	jack_transport_state_t jack_state;
@@ -610,13 +614,9 @@ static void free_port(struct client *c, struct port *p)
 {
 	struct mix *m;
 
-	if (!p->valid)
-		return;
-
 	spa_list_consume(m, &p->mix, port_link)
 		free_mix(c, m);
 
-	p->valid = false;
 	pw_map_remove(&c->ports[p->direction], p->port_id);
 	free_object(c, p->object);
 	pw_properties_free(p->props);
@@ -1056,7 +1056,7 @@ static inline void *get_buffer_output(struct port *p, uint32_t frames, uint32_t
 	struct buffer *b;
 	struct spa_data *d;
 
-	if (frames == 0)
+	if (frames == 0 || !p->valid)
 		return NULL;
 
 	if (SPA_UNLIKELY((mix = p->global_mix) == NULL))
@@ -2484,7 +2484,7 @@ static int client_node_port_set_mix_info(void *data,
 		else
 			mix->peer_port = l->port_link.our_input;
 
-		pw_log_info("peer port %p %p %p", mix->peer_port,
+		pw_log_debug("peer port %p %p %p", mix->peer_port,
 				l->port_link.our_output, l->port_link.our_input);
 
 		if (!l->port_link.is_complete) {
@@ -2576,11 +2576,18 @@ static int impl_acquire_rt(void *object, struct spa_thread *thread, int priority
 	return spa_thread_utils_acquire_rt(c->context.old_thread_utils, thread, priority);
 }
 
+static int impl_drop_rt(void *object, struct spa_thread *thread)
+{
+	struct client *c = (struct client *) object;
+	return spa_thread_utils_drop_rt(c->context.old_thread_utils, thread);
+}
+
 static struct spa_thread_utils_methods thread_utils_impl = {
 	SPA_VERSION_THREAD_UTILS_METHODS,
 	.create = impl_create,
 	.join = impl_join,
 	.acquire_rt = impl_acquire_rt,
+	.drop_rt = impl_drop_rt,
 };
 
 static jack_port_type_id_t string_to_type(const char *port_type)
@@ -2721,6 +2728,24 @@ static const struct pw_proxy_events metadata_proxy_events = {
 	.destroy = metadata_proxy_destroy,
 };
 
+static void settings_proxy_removed(void *data)
+{
+	struct client *c = data;
+	pw_proxy_destroy((struct pw_proxy*)c->settings->proxy);
+}
+
+static void settings_proxy_destroy(void *data)
+{
+	struct client *c = data;
+	spa_hook_remove(&c->settings->proxy_listener);
+	c->settings = NULL;
+}
+
+static const struct pw_proxy_events settings_proxy_events = {
+	PW_VERSION_PROXY_EVENTS,
+	.removed = settings_proxy_removed,
+	.destroy = settings_proxy_destroy,
+};
 static void proxy_removed(void *data)
 {
 	struct object *o = data;
@@ -2769,12 +2794,12 @@ static const struct pw_port_events port_events = {
 #define FILTER_NAME	" ()[].:*$"
 #define FILTER_PORT	" ()[].*$"
 
-static void filter_name(char *str, const char *filter)
+static void filter_name(char *str, const char *filter, char filter_char)
 {
 	char *p;
 	for (p = str; *p; p++) {
 		if (strchr(filter, *p) != NULL)
-			*p = ' ';
+			*p = filter_char;
 	}
 }
 
@@ -2839,7 +2864,7 @@ static void registry_event_global(void *data, uint32_t id,
 			snprintf(tmp, sizeof(tmp), "%s", str);
 
 		if (c->filter_name)
-			filter_name(tmp, FILTER_NAME);
+			filter_name(tmp, FILTER_NAME, c->filter_char);
 
 		ot = find_node(c, tmp);
 		if (ot != NULL && o->node.client_id != ot->node.client_id) {
@@ -2956,7 +2981,7 @@ static void registry_event_global(void *data, uint32_t id,
 				snprintf(tmp, sizeof(tmp), "%s:%s", ot->node.name, str);
 
 			if (c->filter_name)
-				filter_name(tmp, FILTER_PORT);
+				filter_name(tmp, FILTER_PORT, c->filter_char);
 
 			op = find_port_by_name(c, tmp);
 			if (op != NULL)
@@ -3033,24 +3058,34 @@ static void registry_event_global(void *data, uint32_t id,
 
 		if (c->metadata != NULL)
 			goto exit;
-		if ((str = spa_dict_lookup(props, PW_KEY_METADATA_NAME)) != NULL &&
-		    !spa_streq(str, "default"))
+		if ((str = spa_dict_lookup(props, PW_KEY_METADATA_NAME)) == NULL)
 			goto exit;
 
-		proxy = pw_registry_bind(c->registry,
-				id, type, PW_VERSION_METADATA, sizeof(struct metadata));
-
-		c->metadata = pw_proxy_get_user_data(proxy);
-		c->metadata->proxy = (struct pw_metadata*)proxy;
-		c->metadata->default_audio_sink[0] = '\0';
-		c->metadata->default_audio_source[0] = '\0';
-
-		pw_proxy_add_listener(proxy,
-				&c->metadata->proxy_listener,
-				&metadata_proxy_events, c);
-		pw_metadata_add_listener(proxy,
-				&c->metadata->listener,
-				&metadata_events, c);
+		if (spa_streq(str, "default")) {
+			proxy = pw_registry_bind(c->registry,
+					id, type, PW_VERSION_METADATA, sizeof(struct metadata));
+
+			c->metadata = pw_proxy_get_user_data(proxy);
+			c->metadata->proxy = (struct pw_metadata*)proxy;
+			c->metadata->default_audio_sink[0] = '\0';
+			c->metadata->default_audio_source[0] = '\0';
+
+			pw_proxy_add_listener(proxy,
+					&c->metadata->proxy_listener,
+					&metadata_proxy_events, c);
+			pw_metadata_add_listener(proxy,
+					&c->metadata->listener,
+					&metadata_events, c);
+		} else if (spa_streq(str, "settings")) {
+			proxy = pw_registry_bind(c->registry,
+					id, type, PW_VERSION_METADATA, sizeof(struct metadata));
+
+			c->settings = pw_proxy_get_user_data(proxy);
+			c->settings->proxy = (struct pw_metadata*)proxy;
+			pw_proxy_add_listener(proxy,
+					&c->settings->proxy_listener,
+					&settings_proxy_events, c);
+		}
 		goto exit;
 	}
 	else {
@@ -3303,6 +3338,8 @@ jack_client_t * jack_client_open (const char *client_name,
 	if (client->context.old_thread_utils == NULL)
 		client->context.old_thread_utils = pw_thread_utils_get();
 
+	globals.thread_utils = client->context.old_thread_utils;
+
 	client->context.thread_utils.iface = SPA_INTERFACE_INIT(
 			SPA_TYPE_INTERFACE_ThreadUtils,
 			SPA_VERSION_THREAD_UTILS,
@@ -3352,6 +3389,8 @@ jack_client_t * jack_client_open (const char *client_name,
 		pw_properties_set(client->props, PW_KEY_NODE_LATENCY, str);
 	if ((str = getenv("PIPEWIRE_RATE")) != NULL)
 		pw_properties_set(client->props, PW_KEY_NODE_RATE, str);
+	if ((str = getenv("PIPEWIRE_LINK_PASSIVE")) != NULL)
+		pw_properties_set(client->props, PW_KEY_NODE_PASSIVE, str);
 
 	if ((str = pw_properties_get(client->props, PW_KEY_NODE_LATENCY)) != NULL) {
 		uint32_t num, denom;
@@ -3408,9 +3447,13 @@ jack_client_t * jack_client_open (const char *client_name,
 	client->merge_monitor = pw_properties_get_bool(client->props, "jack.merge-monitor", false);
 	client->short_name = pw_properties_get_bool(client->props, "jack.short-name", false);
 	client->filter_name = pw_properties_get_bool(client->props, "jack.filter-name", false);
+	client->filter_char = ' ';
+	if ((str = pw_properties_get(client->props, "jack.filter-char")) != NULL && str[0] != '\0')
+		client->filter_char = str[0];
 	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->global_buffer_size = pw_properties_get_bool(client->props, "jack.global-buffer-size", false);
 
 	client->self_connect_mode = SELF_CONNECT_ALLOW;
 	if ((str = pw_properties_get(client->props, "jack.self-connect-mode")) != NULL) {
@@ -3509,10 +3552,11 @@ int jack_client_close (jack_client_t *client)
 		pw_proxy_destroy((struct pw_proxy*)c->registry);
 	}
 	if (c->metadata && c->metadata->proxy) {
-		spa_hook_remove(&c->metadata->listener);
-		spa_hook_remove(&c->metadata->proxy_listener);
 		pw_proxy_destroy((struct pw_proxy*)c->metadata->proxy);
 	}
+	if (c->settings && c->settings->proxy) {
+		pw_proxy_destroy((struct pw_proxy*)c->settings->proxy);
+	}
 
 	if (c->core) {
 		spa_hook_remove(&c->core_listener);
@@ -3773,14 +3817,10 @@ SPA_EXPORT
 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, (pthread_t){0});
 
-	thr = pw_data_loop_get_thread(c->loop);
-	if (thr == NULL)
-		return pthread_self();
-	return (pthread_t) thr;
+	return (jack_native_thread_t)pw_data_loop_get_thread(c->loop);
 }
 
 SPA_EXPORT
@@ -4137,15 +4177,22 @@ int jack_set_buffer_size (jack_client_t *client, jack_nframes_t nframes)
 	pw_log_info("%p: buffer-size %u", client, nframes);
 
 	pw_thread_loop_lock(c->context.loop);
-	pw_properties_setf(c->props, PW_KEY_NODE_FORCE_QUANTUM, "%u", nframes);
+	if (c->global_buffer_size && c->settings && c->settings->proxy) {
+		char val[256];
+		snprintf(val, sizeof(val), "%u", nframes == 1 ? 0: nframes);
+		pw_metadata_set_property(c->settings->proxy, 0,
+				"clock.force-quantum", "", val);
+	} else {
+		pw_properties_setf(c->props, PW_KEY_NODE_FORCE_QUANTUM, "%u", nframes);
 
-	c->info.change_mask |= SPA_NODE_CHANGE_MASK_PROPS;
-	c->info.props = &c->props->dict;
+		c->info.change_mask |= SPA_NODE_CHANGE_MASK_PROPS;
+		c->info.props = &c->props->dict;
 
-	pw_client_node_update(c->node,
-                                    PW_CLIENT_NODE_UPDATE_INFO,
-				    0, NULL, &c->info);
-	c->info.change_mask = 0;
+		pw_client_node_update(c->node,
+	                                    PW_CLIENT_NODE_UPDATE_INFO,
+					    0, NULL, &c->info);
+		c->info.change_mask = 0;
+	}
 	pw_thread_loop_unlock(c->context.loop);
 
 	return 0;
@@ -4369,6 +4416,15 @@ jack_port_t * jack_port_register (jack_client_t *client,
 	return (jack_port_t *) o;
 }
 
+static int
+do_invalidate_port(struct spa_loop *loop,
+                  bool async, uint32_t seq, const void *data, size_t size, void *user_data)
+{
+	struct port *p = user_data;
+	p->valid = false;
+	return 0;
+}
+
 SPA_EXPORT
 int jack_port_unregister (jack_client_t *client, jack_port_t *port)
 {
@@ -4389,6 +4445,9 @@ int jack_port_unregister (jack_client_t *client, jack_port_t *port)
 		res = -EINVAL;
 		goto done;
 	}
+	pw_data_loop_invoke(c->loop,
+			do_invalidate_port, 1, NULL, 0, !c->data_locked, p);
+
 	pw_log_info("%p: port %p unregister \"%s\"", client, port, o->port.name);
 
 	pw_client_node_port_update(c->node,
@@ -4566,6 +4625,8 @@ void * jack_port_get_buffer (jack_port_t *port, jack_nframes_t frames)
 
 		return SPA_PTROFF(d->data, offset, void);
 	}
+	if (!p->valid)
+		return NULL;
 
 	ptr = p->get_buffer(p, frames);
 	pw_log_trace_fp("%p: port %p buffer %p empty:%u", p->client, p, ptr, p->empty_out);
@@ -5113,7 +5174,7 @@ int jack_connect (jack_client_t *client,
 	items[props.n_items++] = SPA_DICT_ITEM_INIT(PW_KEY_LINK_INPUT_NODE, val[2]);
 	items[props.n_items++] = SPA_DICT_ITEM_INIT(PW_KEY_LINK_INPUT_PORT, val[3]);
 	items[props.n_items++] = SPA_DICT_ITEM_INIT(PW_KEY_OBJECT_LINGER, "true");
-	if ((str = getenv("PIPEWIRE_LINK_PASSIVE")) != NULL &&
+	if ((str = pw_properties_get(c->props, PW_KEY_NODE_PASSIVE)) != NULL &&
 	    pw_properties_parse_bool(str))
 		items[props.n_items++] = SPA_DICT_ITEM_INIT(PW_KEY_LINK_PASSIVE, "true");
 
@@ -5715,7 +5776,7 @@ jack_nframes_t jack_time_to_frames(const jack_client_t *client, jack_time_t usec
 }
 
 SPA_EXPORT
-jack_time_t jack_get_time()
+jack_time_t jack_get_time(void)
 {
 	struct timespec ts;
 	clock_gettime(CLOCK_MONOTONIC, &ts);
@@ -6115,15 +6176,21 @@ int jack_client_max_real_time_priority (jack_client_t *client)
 SPA_EXPORT
 int jack_acquire_real_time_scheduling (jack_native_thread_t thread, int priority)
 {
-	pw_log_info("acquire");
-	return pw_thread_utils_acquire_rt((struct spa_thread*)thread, priority);
+	struct spa_thread *t = (struct spa_thread*)thread;
+	pw_log_info("acquire %p", t);
+	spa_return_val_if_fail(globals.thread_utils != NULL, -1);
+	spa_return_val_if_fail(t != NULL, -1);
+	return spa_thread_utils_acquire_rt(globals.thread_utils, t, priority);
 }
 
 SPA_EXPORT
 int jack_drop_real_time_scheduling (jack_native_thread_t thread)
 {
-	pw_log_info("drop");
-	return pw_thread_utils_drop_rt((struct spa_thread*)thread);
+	struct spa_thread *t = (struct spa_thread*)thread;
+	pw_log_info("drop %p", t);
+	spa_return_val_if_fail(globals.thread_utils != NULL, -1);
+	spa_return_val_if_fail(t != NULL, -1);
+	return spa_thread_utils_drop_rt(globals.thread_utils, t);
 }
 
 /**
diff --git a/pipewire-jack/src/uuid.c b/pipewire-jack/src/uuid.c
index 88d8bd881b919910745f0e48d26d41c54327e9b7..8584e11e2c3d91ce3ebbc8399ed7beec885a8926 100644
--- a/pipewire-jack/src/uuid.c
+++ b/pipewire-jack/src/uuid.c
@@ -33,7 +33,7 @@
 #include <pipewire/pipewire.h>
 
 SPA_EXPORT
-jack_uuid_t jack_client_uuid_generate ()
+jack_uuid_t jack_client_uuid_generate (void)
 {
 	static uint32_t uuid_cnt = 0;
 	jack_uuid_t uuid = 0x2; /* JackUUIDClient */;
diff --git a/pipewire-v4l2/src/pipewire-v4l2.c b/pipewire-v4l2/src/pipewire-v4l2.c
index d2e13dbf741e56163abc6dff0803b85c1b5f1827..59640d2c17c0285e233063ec6a9a272cc03fb67c 100644
--- a/pipewire-v4l2/src/pipewire-v4l2.c
+++ b/pipewire-v4l2/src/pipewire-v4l2.c
@@ -57,6 +57,7 @@ PW_LOG_TOPIC_STATIC(v4l2_log_topic, "v4l2");
 #define DEFAULT_CARD		"PipeWire Camera"
 #define DEFAULT_BUS_INFO	"PipeWire"
 
+#define MAX_DEV		32
 struct file_map {
 	void *addr;
 	struct file *file;
@@ -64,6 +65,8 @@ struct file_map {
 
 struct fd_map {
 	int fd;
+#define FD_MAP_DUP	(1<<0)
+	uint32_t flags;
 	struct file *file;
 };
 
@@ -73,6 +76,7 @@ struct globals {
 	pthread_mutex_t lock;
 	struct pw_array fd_maps;
 	struct pw_array file_maps;
+	uint32_t dev_map[MAX_DEV];
 };
 
 static struct globals globals;
@@ -93,6 +97,9 @@ struct buffer {
 struct file {
 	int ref;
 
+	uint32_t dev_id;
+	uint32_t serial;
+
 	struct pw_properties *props;
 	struct pw_thread_loop *loop;
 	struct pw_loop *l;
@@ -114,23 +121,27 @@ struct file {
 	struct pw_stream *stream;
 	struct spa_hook stream_listener;
 
+	enum v4l2_priority priority;
+
 	struct v4l2_format v4l2_format;
 	uint32_t reqbufs;
 
+	int reqbufs_fd;
 	struct buffer buffers[MAX_BUFFERS];
 	uint32_t n_buffers;
 	uint32_t size;
 
+	uint32_t sequence;
+
 	struct pw_array buffer_maps;
 
 	uint32_t last_fourcc;
 
 	unsigned int running:1;
+	unsigned int closed:1;
 	int fd;
 };
 
-#define MAX_PARAMS	32
-
 struct global_info {
 	const char *type;
 	uint32_t version;
@@ -156,8 +167,8 @@ struct global {
 
 	int changed;
 	void *info;
+	struct spa_list pending_list;
 	struct spa_list param_list;
-	int param_seq[MAX_PARAMS];
 
 	union {
 		struct {
@@ -173,6 +184,7 @@ struct global {
 struct param {
 	struct spa_list link;
 	uint32_t id;
+	int32_t seq;
 	struct spa_pod *param;
 };
 
@@ -192,7 +204,7 @@ static uint32_t clear_params(struct spa_list *param_list, uint32_t id)
 }
 
 static struct param *add_param(struct spa_list *params,
-		int seq, int *param_seq, uint32_t id, const struct spa_pod *param)
+		int seq, uint32_t id, const struct spa_pod *param)
 {
 	struct param *p;
 
@@ -204,24 +216,12 @@ static struct param *add_param(struct spa_list *params,
 		id = SPA_POD_OBJECT_ID(param);
 	}
 
-	if (id >= MAX_PARAMS) {
-		pw_log_error("too big param id %d", id);
-		errno = EINVAL;
-		return NULL;
-	}
-
-	if (seq != param_seq[id]) {
-		pw_log_debug("ignoring param %d, seq:%d != current_seq:%d",
-				id, seq, param_seq[id]);
-		errno = EBUSY;
-		return NULL;
-	}
-
 	p = malloc(sizeof(*p) + (param != NULL ? SPA_POD_SIZE(param) : 0));
 	if (p == NULL)
 		return NULL;
 
 	p->id = id;
+	p->seq = seq;
 	if (param != NULL) {
 		p->param = SPA_PTROFF(p, sizeof(*p), struct spa_pod);
 		memcpy(p->param, param, SPA_POD_SIZE(param));
@@ -234,6 +234,39 @@ static struct param *add_param(struct spa_list *params,
 	return p;
 }
 
+static void update_params(struct file *file)
+{
+	struct param *p, *t;
+	struct global *node;
+	struct pw_node_info *info;
+	uint32_t i;
+
+	if ((node = file->node) == NULL)
+		return;
+	if ((info = node->info) == NULL)
+		return;
+
+	for (i = 0; i < info->n_params; i++) {
+		spa_list_for_each_safe(p, t, &node->pending_list, link) {
+			if (p->id == info->params[i].id &&
+			    p->seq != info->params[i].seq &&
+			    p->param != NULL) {
+				spa_list_remove(&p->link);
+				free(p);
+			}
+		}
+	}
+
+	spa_list_consume(p, &node->pending_list, link) {
+		spa_list_remove(&p->link);
+		if (p->param == NULL) {
+			clear_params(&node->param_list, p->id);
+			free(p);
+		} else {
+			spa_list_append(&node->param_list, &p->link);
+		}
+	}
+}
 #define ATOMIC_DEC(s)                   __atomic_sub_fetch(&(s), 1, __ATOMIC_SEQ_CST)
 #define ATOMIC_INC(s)                   __atomic_add_fetch(&(s), 1, __ATOMIC_SEQ_CST)
 
@@ -247,6 +280,8 @@ static struct file *make_file(void)
 
 	file->ref = 1;
 	file->fd = -1;
+	file->reqbufs_fd = -1;
+	file->priority = V4L2_PRIORITY_DEFAULT;
 	spa_list_init(&file->globals);
 	pw_array_init(&file->buffer_maps, sizeof(struct buffer_map) * MAX_BUFFERS);
 	return file;
@@ -254,6 +289,8 @@ static struct file *make_file(void)
 
 static void free_file(struct file *file)
 {
+	pw_log_info("file:%d", file->fd);
+
 	if (file->loop)
 		pw_thread_loop_stop(file->loop);
 
@@ -282,49 +319,99 @@ static void free_file(struct file *file)
 
 static void unref_file(struct file *file)
 {
+	pw_log_debug("file:%d ref:%d", file->fd, file->ref);
 	if (ATOMIC_DEC(file->ref) <= 0)
 		free_file(file);
 }
 
-static int add_fd_map(int fd, struct file *file)
+static int add_fd_map(int fd, struct file *file, uint32_t flags)
 {
 	struct fd_map *map;
 	pthread_mutex_lock(&globals.lock);
 	map = pw_array_add(&globals.fd_maps, sizeof(*map));
 	if (map != NULL) {
 		map->fd = fd;
+		map->flags = flags;
 		map->file = file;
 		ATOMIC_INC(file->ref);
+		pw_log_debug("fd:%d -> file:%d ref:%d", fd, file->fd, file->ref);
 	}
 	pthread_mutex_unlock(&globals.lock);
 	return 0;
 }
 
+static uint32_t find_dev_for_serial(uint32_t serial)
+{
+	uint32_t i, res = SPA_ID_INVALID;
+	pthread_mutex_lock(&globals.lock);
+	for (i = 0; i < SPA_N_ELEMENTS(globals.dev_map); i++) {
+		if (globals.dev_map[i] == serial) {
+			res = i;
+			break;
+		}
+	}
+	pthread_mutex_unlock(&globals.lock);
+	return res;
+}
+
+static bool add_dev_for_serial(uint32_t dev, uint32_t serial)
+{
+	pthread_mutex_lock(&globals.lock);
+	globals.dev_map[dev] = serial;
+	pthread_mutex_unlock(&globals.lock);
+	return true;
+}
+
 /* must be called with `globals.lock` held */
 static struct fd_map *find_fd_map_unlocked(int fd)
 {
 	struct fd_map *map;
-
 	pw_array_for_each(map, &globals.fd_maps) {
 		if (map->fd == fd) {
 			ATOMIC_INC(map->file->ref);
+			pw_log_debug("fd:%d find:%d ref:%d", map->fd, fd, map->file->ref);
 			return map;
 		}
 	}
-
 	return NULL;
 }
 
-static struct file *find_file(int fd)
+static struct file *find_file(int fd, uint32_t *flags)
 {
 	pthread_mutex_lock(&globals.lock);
 
 	struct fd_map *map = find_fd_map_unlocked(fd);
 	struct file *file = NULL;
 
-	if (map != NULL)
+	if (map != NULL) {
 		file = map->file;
+		*flags = map->flags;
+	}
+
+	pthread_mutex_unlock(&globals.lock);
+
+	return file;
+}
+
+static struct file *find_file_by_dev(uint32_t dev)
+{
+	struct fd_map *map = NULL, *tmp;
+	struct file *file = NULL;
 
+	pthread_mutex_lock(&globals.lock);
+	pw_array_for_each(tmp, &globals.fd_maps) {
+		if (tmp->file->dev_id == dev) {
+			if (tmp->file->closed)
+				tmp->file->fd = tmp->fd;
+			ATOMIC_INC(tmp->file->ref);
+			map = tmp;
+			pw_log_debug("dev:%d find:%d ref:%d",
+					tmp->file->dev_id, dev, tmp->file->ref);
+			break;
+		}
+	}
+	if (map != NULL)
+		file = map->file;
 	pthread_mutex_unlock(&globals.lock);
 
 	return file;
@@ -339,6 +426,7 @@ static struct file *remove_fd_map(int fd)
 
 	if (map != NULL) {
 		file = map->file;
+		pw_log_debug("fd:%d find:%d", map->fd, fd);
 		pw_array_remove(&globals.fd_maps, map);
 	}
 
@@ -350,7 +438,7 @@ static struct file *remove_fd_map(int fd)
 	return file;
 }
 
-static int add_file_map(void *addr, struct file *file)
+static int add_file_map(struct file *file, void *addr)
 {
 	struct file_map *map;
 	pthread_mutex_lock(&globals.lock);
@@ -448,15 +536,17 @@ static void on_sync_reply(void *data, uint32_t id, int seq)
 		return;
 
 	file->last_seq = seq;
-	if (file->pending_seq == seq)
+	if (file->pending_seq == seq) {
+		update_params(file);
 		pw_thread_loop_signal(file->loop, false);
+	}
 }
 
 static void on_error(void *data, uint32_t id, int seq, int res, const char *message)
 {
 	struct file *file = data;
 
-	pw_log_warn("%p: error id:%u seq:%d res:%d (%s): %s", file,
+	pw_log_warn("file:%d: error id:%u seq:%d res:%d (%s): %s", file->fd,
 			id, seq, res, spa_strerror(res), message);
 
 	if (id == PW_ID_CORE) {
@@ -485,6 +575,8 @@ static void node_event_info(void *object, const struct pw_node_info *info)
 	uint32_t i;
 
 	info = g->info = pw_node_info_merge(g->info, info, g->changed == 0);
+	if (info == NULL)
+		return;
 
 	pw_log_debug("update %d %"PRIu64, g->id, info->change_mask);
 
@@ -512,21 +604,14 @@ static void node_event_info(void *object, const struct pw_node_info *info)
 				continue;
 			info->params[i].user = 0;
 
-			if (id >= MAX_PARAMS) {
-				pw_log_error("too big param id %d", id);
-				continue;
-			}
-
-			if (id != SPA_PARAM_EnumFormat)
-				continue;
-
+			add_param(&g->pending_list, info->params[i].seq, id, NULL);
 			if (!(info->params[i].flags & SPA_PARAM_INFO_READ))
 				continue;
 
 			res = pw_node_enum_params((struct pw_node*)g->proxy,
-					++g->param_seq[id], id, 0, -1, NULL);
+					++info->params[i].seq, id, 0, -1, NULL);
 			if (SPA_RESULT_IS_ASYNC(res))
-				g->param_seq[id] = res;
+				info->params[i].seq = res;
 		}
 	}
 	do_resync(file);
@@ -538,8 +623,8 @@ static void node_event_param(void *object, int seq,
 {
 	struct global *g = object;
 
-	pw_log_debug("update param %d %d %d %d", g->id, id, seq, g->param_seq[id]);
-	add_param(&g->param_list, seq, g->param_seq, id, param);
+	pw_log_debug("update param %d %d %d", g->id, id, seq);
+	add_param(&g->pending_list, seq, id, param);
 }
 
 static const struct pw_node_events node_events = {
@@ -566,6 +651,10 @@ static void proxy_destroy(void *data)
 	struct global *g = data;
 	spa_list_remove(&g->link);
 	g->proxy = NULL;
+	if (g->file)
+		g->file->node = NULL;
+	clear_params(&g->param_list, SPA_ID_INVALID);
+	clear_params(&g->pending_list, SPA_ID_INVALID);
 }
 
 static const struct pw_proxy_events proxy_events = {
@@ -582,20 +671,31 @@ static void registry_event_global(void *data, uint32_t id,
 	const struct global_info *info = NULL;
 	struct pw_proxy *proxy;
 	const char *str;
-
-	pw_log_debug("got %d %s", id, type);
+	uint32_t serial = SPA_ID_INVALID, dev;
 
 	if (spa_streq(type, PW_TYPE_INTERFACE_Node)) {
+
 		if (file->node != NULL)
 			return;
 
-		if (props == NULL ||
-		    ((str = spa_dict_lookup(props, PW_KEY_MEDIA_CLASS)) == NULL) ||
+		pw_log_info("got %d %s", id, type);
+
+		if (props == NULL)
+			return;
+		if (((str = spa_dict_lookup(props, PW_KEY_MEDIA_CLASS)) == NULL) ||
 		    ((!spa_streq(str, "Video/Sink")) &&
 		     (!spa_streq(str, "Video/Source"))))
 			return;
 
-		pw_log_debug("found node %d type:%s", id, str);
+		if (((str = spa_dict_lookup(props, PW_KEY_OBJECT_SERIAL)) == NULL) ||
+		    !spa_atou32(str, &serial, 10))
+			return;
+
+		dev = find_dev_for_serial(serial);
+		if (dev != SPA_ID_INVALID && dev != file->dev_id)
+			return;
+
+		pw_log_info("found node:%d serial:%d type:%s", id, serial, str);
 		info = &node_info;
 	}
 	if (info) {
@@ -612,6 +712,7 @@ static void registry_event_global(void *data, uint32_t id,
 		g->permissions = permissions;
 		g->props = props ? pw_properties_new_dict(props) : NULL;
 		g->proxy = proxy;
+		spa_list_init(&g->pending_list);
 		spa_list_init(&g->param_list);
 		spa_list_append(&file->globals, &g->link);
 
@@ -627,6 +728,7 @@ static void registry_event_global(void *data, uint32_t id,
 		if (info->init)
 			info->init(g);
 
+		file->serial = serial;
 		file->node = g;
 
 		do_resync(file);
@@ -660,17 +762,58 @@ static const struct pw_registry_events registry_events = {
 	.global_remove = registry_event_global_remove,
 };
 
-static int v4l2_openat(int dirfd, const char *path, int oflag, mode_t mode)
+static int do_dup(int oldfd, uint32_t flags)
 {
 	int res;
 	struct file *file;
+	uint32_t fl;
 
-	if (!spa_strstartswith(path, "/dev/video0"))
+	res = globals.old_fops.dup(oldfd);
+	if (res < 0)
+		return res;
+
+	if ((file = find_file(oldfd, &fl)) != NULL) {
+		add_fd_map(res, file, flags | fl);
+		unref_file(file);
+		pw_log_info("fd:%d %08x -> %d (%s)", oldfd, flags,
+				res, strerror(res < 0 ? errno : 0));
+	}
+	return res;
+}
+
+static int v4l2_dup(int oldfd)
+{
+	return do_dup(oldfd, FD_MAP_DUP);
+}
+
+static int v4l2_openat(int dirfd, const char *path, int oflag, mode_t mode)
+{
+	int res, flags;
+	struct file *file;
+	bool passthrough = true;
+	uint32_t dev_id = SPA_ID_INVALID;
+
+	if (spa_strstartswith(path, "/dev/video")) {
+		if (spa_atou32(path+10, &dev_id, 10) && dev_id < MAX_DEV)
+			passthrough = false;
+	}
+	if (passthrough)
 		return globals.old_fops.openat(dirfd, path, oflag, mode);
 
+	pw_log_info("path:%s oflag:%d mode:%d", path, oflag, mode);
+
+	if ((file = find_file_by_dev(dev_id)) != NULL) {
+		res = do_dup(file->fd, 0);
+		unref_file(file);
+		if (res >= 0)
+			fcntl(res, F_SETFL, oflag);
+		return res;
+	}
+
 	if ((file = make_file()) == NULL)
 		goto error;
 
+	file->dev_id = dev_id;
 	file->props = pw_properties_new(
 			PW_KEY_CLIENT_API, "v4l2",
 			NULL);
@@ -711,47 +854,41 @@ static int v4l2_openat(int dirfd, const char *path, int oflag, mode_t mode)
 		goto error_unlock;
 	}
 	if (file->node == NULL) {
-		errno = -ENOENT;
+		errno = ENOENT;
 		goto error_unlock;
 	}
 	pw_thread_loop_unlock(file->loop);
 
-	res = file->fd = spa_system_eventfd_create(file->l->system,
-		SPA_FD_CLOEXEC | SPA_FD_NONBLOCK);
+	flags = SPA_FD_CLOEXEC;
+	if (oflag & O_NONBLOCK)
+		flags |= SPA_FD_NONBLOCK;
+
+	res = spa_system_eventfd_create(file->l->system, flags);
 	if (res < 0)
 		goto error;
 
+	file->fd = res;
+
 	pw_log_info("path:%s oflag:%d mode:%d -> %d (%s)", path, oflag, mode,
 			res, strerror(res < 0 ? errno : 0));
 
-	add_fd_map(res, file);
+	add_fd_map(res, file, 0);
+	add_dev_for_serial(file->dev_id, file->serial);
+	unref_file(file);
 
 	return res;
 
 error_unlock:
 	pw_thread_loop_unlock(file->loop);
 error:
+	res = -errno;
 	if (file)
 		free_file(file);
-	return -1;
-}
-
-static int v4l2_dup(int oldfd)
-{
-	int res;
-	struct file *file;
 
-	res = globals.old_fops.dup(oldfd);
-	if (res < 0)
-		return res;
-
-	if ((file = find_file(oldfd)) != NULL) {
-		add_fd_map(res, file);
-		unref_file(file);
-		pw_log_info("fd:%d -> %d (%s)", oldfd,
-				res, strerror(res < 0 ? errno : 0));
-	}
-	return res;
+	pw_log_info("path:%s oflag:%d mode:%d -> %d (%s)", path, oflag, mode,
+			-1, spa_strerror(res));
+	errno = -res;
+	return -1;
 }
 
 static int v4l2_close(int fd)
@@ -761,9 +898,12 @@ static int v4l2_close(int fd)
 	if ((file = remove_fd_map(fd)) == NULL)
 		return globals.old_fops.close(fd);
 
+	pw_log_info("fd:%d file:%d", fd, file->fd);
+
 	if (fd != file->fd)
 		spa_system_close(file->l->system, fd);
 
+	file->closed = true;
 	unref_file(file);
 
 	return 0;
@@ -774,10 +914,24 @@ static int v4l2_close(int fd)
 static int vidioc_querycap(struct file *file, struct v4l2_capability *arg)
 {
 	int res = 0;
+	const char *str = NULL;
+	struct pw_node_info *info;
+
+	if (file->node == NULL)
+		return -EIO;
+
+	info = file->node->info;
+
+	if (info != NULL && info->props != NULL) {
+		str = spa_dict_lookup(info->props, PW_KEY_NODE_DESCRIPTION);
+	}
+	if (str == NULL)
+		str = DEFAULT_CARD;
 
 	spa_scnprintf((char*)arg->driver, sizeof(arg->driver), "%s", DEFAULT_DRIVER);
-	spa_scnprintf((char*)arg->card, sizeof(arg->card), "%s", DEFAULT_CARD);
-	spa_scnprintf((char*)arg->bus_info, sizeof(arg->bus_info), "%s:%d", DEFAULT_BUS_INFO, 1);
+	spa_scnprintf((char*)arg->card, sizeof(arg->card), "%s", str);
+	spa_scnprintf((char*)arg->bus_info, sizeof(arg->bus_info), "platform:%s-%d",
+			DEFAULT_BUS_INFO, file->node->id);
 
 	arg->version = KERNEL_VERSION(5, 2, 0);
 	arg->device_caps = V4L2_CAP_VIDEO_CAPTURE
@@ -786,7 +940,7 @@ static int vidioc_querycap(struct file *file, struct v4l2_capability *arg)
 	arg->capabilities = arg->device_caps | V4L2_CAP_DEVICE_CAPS;
 	memset(arg->reserved, 0, sizeof(arg->reserved));
 
-	pw_log_info("file:%p -> %d (%s)", file, res, spa_strerror(res));
+	pw_log_info("file:%d -> %d (%s)", file->fd, res, spa_strerror(res));
 	return res;
 }
 
@@ -799,17 +953,17 @@ struct format_info {
 	const char *desc;
 };
 
-#define MAKE_FORMAT(fcc,mt,mst,bpp,fmt)	\
-	{ V4L2_PIX_FMT_ ## fcc, SPA_MEDIA_TYPE_ ## mt, SPA_MEDIA_SUBTYPE_ ## mst, SPA_VIDEO_FORMAT_ ## fmt, bpp, #fcc }
+#define MAKE_FORMAT(fcc,mt,mst,bpp,fmt,...)	\
+	{ V4L2_PIX_FMT_ ## fcc, SPA_MEDIA_TYPE_ ## mt, SPA_MEDIA_SUBTYPE_ ## mst, SPA_VIDEO_FORMAT_ ## fmt, bpp, __VA_ARGS__ }
 
 static const struct format_info format_info[] = {
 	/* RGB formats */
-	MAKE_FORMAT(RGB332, video, raw, 4, UNKNOWN),
+	MAKE_FORMAT(RGB332, video, raw, 4, UNKNOWN, "8-bit RGB 3-3-2"),
 	MAKE_FORMAT(ARGB555, video, raw, 4, UNKNOWN),
-	MAKE_FORMAT(XRGB555, video, raw, 4, RGB15),
+	MAKE_FORMAT(XRGB555, video, raw, 4, RGB15, "16-bit XRGB 1-5-5-5"),
 	MAKE_FORMAT(ARGB555X, video, raw, 4, UNKNOWN),
-	MAKE_FORMAT(XRGB555X, video, raw, 4, BGR15),
-	MAKE_FORMAT(RGB565, video, raw, 4, RGB16),
+	MAKE_FORMAT(XRGB555X, video, raw, 4, BGR15, "16-bit XRGB 1-5-5-5 BE"),
+	MAKE_FORMAT(RGB565, video, raw, 4, RGB16, "16-bit RGB 5-6-5"),
 	MAKE_FORMAT(RGB565X, video, raw, 4, UNKNOWN),
 	MAKE_FORMAT(BGR666, video, raw, 4, UNKNOWN),
 	MAKE_FORMAT(BGR24, video, raw, 4, BGR),
@@ -843,13 +997,13 @@ static const struct format_info format_info[] = {
 	MAKE_FORMAT(UV8, video, raw, 2, UNKNOWN),
 
 	/* Luminance+Chrominance formats */
-	MAKE_FORMAT(YVU410, video, raw, 1, YVU9),
-	MAKE_FORMAT(YVU420, video, raw, 1, YV12),
+	MAKE_FORMAT(YVU410, video, raw, 1, YVU9, "Planar YVU 4:1:0"),
+	MAKE_FORMAT(YVU420, video, raw, 1, YV12, "Planar YVU 4:2:0"),
 	MAKE_FORMAT(YVU420M, video, raw, 1, UNKNOWN),
-	MAKE_FORMAT(YUYV, video, raw, 2, YUY2),
+	MAKE_FORMAT(YUYV, video, raw, 2, YUY2, "YUYV 4:2:2"),
 	MAKE_FORMAT(YYUV, video, raw, 2, UNKNOWN),
-	MAKE_FORMAT(YVYU, video, raw, 2, YVYU),
-	MAKE_FORMAT(UYVY, video, raw, 2, UYVY),
+	MAKE_FORMAT(YVYU, video, raw, 2, YVYU, "YVYU 4:2:2"),
+	MAKE_FORMAT(UYVY, video, raw, 2, UYVY, "UYVY 4:2:2"),
 	MAKE_FORMAT(VYUY, video, raw, 2, UNKNOWN),
 	MAKE_FORMAT(YUV422P, video, raw, 1, Y42B),
 	MAKE_FORMAT(YUV411P, video, raw, 1, Y41B),
@@ -859,24 +1013,24 @@ static const struct format_info format_info[] = {
 	MAKE_FORMAT(YUV565, video, raw, 1, UNKNOWN),
 	MAKE_FORMAT(YUV32, video, raw, 1, UNKNOWN),
 	MAKE_FORMAT(YUV410, video, raw, 1, YUV9),
-	MAKE_FORMAT(YUV420, video, raw, 1, I420),
-	MAKE_FORMAT(YUV420M, video, raw, 1, I420),
+	MAKE_FORMAT(YUV420, video, raw, 1, I420, "Planar YUV 4:2:0"),
+	MAKE_FORMAT(YUV420M, video, raw, 1, I420, "Planar YUV 4:2:0 (N-C)"),
 	MAKE_FORMAT(HI240, video, raw, 1, UNKNOWN),
 	MAKE_FORMAT(HM12, video, raw, 1, UNKNOWN),
 	MAKE_FORMAT(M420, video, raw, 1, UNKNOWN),
 
 	/* two planes -- one Y, one Cr + Cb interleaved  */
-	MAKE_FORMAT(NV12, video, raw, 1, NV12),
-	MAKE_FORMAT(NV12M, video, raw, 1,  NV12),
-	MAKE_FORMAT(NV12MT, video, raw, 1,  NV12_64Z32),
+	MAKE_FORMAT(NV12, video, raw, 1, NV12, "Y/CbCr 4:2:0"),
+	MAKE_FORMAT(NV12M, video, raw, 1,  NV12, "Y/CbCr 4:2:0 (N-C)"),
+	MAKE_FORMAT(NV12MT, video, raw, 1,  NV12_64Z32, "Y/CbCr 4:2:0 (64x32 MB, N-C)"),
 	MAKE_FORMAT(NV12MT_16X16, video, raw, 1, UNKNOWN),
-	MAKE_FORMAT(NV21, video, raw, 1, NV21),
-	MAKE_FORMAT(NV21M, video, raw, 1, NV21),
-	MAKE_FORMAT(NV16, video, raw, 1, NV16),
-	MAKE_FORMAT(NV16M, video, raw, 1, NV16),
-	MAKE_FORMAT(NV61, video, raw, 1, NV61),
-	MAKE_FORMAT(NV61M, video, raw, 1, NV61),
-	MAKE_FORMAT(NV24, video, raw, 1, NV24),
+	MAKE_FORMAT(NV21, video, raw, 1, NV21, "Y/CrCb 4:2:0"),
+	MAKE_FORMAT(NV21M, video, raw, 1, NV21, "Y/CrCb 4:2:0 (N-C)"),
+	MAKE_FORMAT(NV16, video, raw, 1, NV16, "Y/CbCr 4:2:2"),
+	MAKE_FORMAT(NV16M, video, raw, 1, NV16, "Y/CbCr 4:2:2 (N-C)"),
+	MAKE_FORMAT(NV61, video, raw, 1, NV61, "Y/CrCb 4:2:2"),
+	MAKE_FORMAT(NV61M, video, raw, 1, NV61, "Y/CrCb 4:2:2 (N-C)"),
+	MAKE_FORMAT(NV24, video, raw, 1, NV24, "Y/CbCr 4:4:4"),
 	MAKE_FORMAT(NV42, video, raw, 1, UNKNOWN),
 
 	/* Bayer formats - see http://www.siliconimaging.com/RGB%20Bayer.htm */
@@ -886,14 +1040,14 @@ static const struct format_info format_info[] = {
 	MAKE_FORMAT(SRGGB8, video, bayer, 1, UNKNOWN),
 
 	/* compressed formats */
-	MAKE_FORMAT(MJPEG, video, mjpg, 1, ENCODED),
-	MAKE_FORMAT(JPEG, video, mjpg, 1, ENCODED),
-	MAKE_FORMAT(PJPG, video, mjpg, 1, ENCODED),
+	MAKE_FORMAT(MJPEG, video, mjpg, 1, ENCODED, "Motion-JPEG"),
+	MAKE_FORMAT(JPEG, video, mjpg, 1, ENCODED, "JFIF JPEG"),
+	MAKE_FORMAT(PJPG, video, mjpg, 1, ENCODED, "GSPCA PJPG"),
 	MAKE_FORMAT(DV, video, dv, 1, ENCODED),
 	MAKE_FORMAT(MPEG, video, mpegts, 1, ENCODED),
-	MAKE_FORMAT(H264, video, h264, 1, ENCODED),
-	MAKE_FORMAT(H264_NO_SC, video, h264, 1, ENCODED),
-	MAKE_FORMAT(H264_MVC, video, h264, 1, ENCODED),
+	MAKE_FORMAT(H264, video, h264, 1, ENCODED, "H.264"),
+	MAKE_FORMAT(H264_NO_SC, video, h264, 1, ENCODED, "H.264 (No Start Codes)"),
+	MAKE_FORMAT(H264_MVC, video, h264, 1, ENCODED, "H.264 MVC"),
 	MAKE_FORMAT(H263, video, h263, 1, ENCODED),
 	MAKE_FORMAT(MPEG1, video, mpeg1, 1, ENCODED),
 	MAKE_FORMAT(MPEG2, video, mpeg2, 1, ENCODED),
@@ -913,23 +1067,19 @@ static const struct format_info format_info[] = {
 static const struct format_info *format_info_from_media_type(uint32_t type,
 		uint32_t subtype, uint32_t format)
 {
-	size_t i;
-	for (i = 0; i < SPA_N_ELEMENTS(format_info); i++) {
-		if ((format_info[i].media_type == type) &&
-		    (format_info[i].media_subtype == subtype) &&
-		    (format == 0 || format_info[i].format == format))
-			return &format_info[i];
-	}
+	SPA_FOR_EACH_ELEMENT_VAR(format_info, i)
+		if ((i->media_type == type) &&
+		    (i->media_subtype == subtype) &&
+		    (format == 0 || i->format == format))
+			return i;
 	return NULL;
 }
 
 static const struct format_info *format_info_from_fourcc(uint32_t fourcc)
 {
-	size_t i;
-	for (i = 0; i < SPA_N_ELEMENTS(format_info); i++) {
-		if (format_info[i].fourcc == fourcc)
-			return &format_info[i];
-	}
+	SPA_FOR_EACH_ELEMENT_VAR(format_info, i)
+		if (i->fourcc == fourcc)
+			return i;
 	return NULL;
 }
 
@@ -940,7 +1090,7 @@ static int format_to_info(const struct v4l2_format *arg, struct spa_video_info *
 	pw_log_info("type: %u", arg->type);
 	pw_log_info("width: %u", arg->fmt.pix.width);
 	pw_log_info("height: %u", arg->fmt.pix.height);
-	pw_log_info("fmt: %u", arg->fmt.pix.pixelformat);
+	pw_log_info("fmt: %.4s", (char*)&arg->fmt.pix.pixelformat);
 
 	if (arg->type != V4L2_BUF_TYPE_VIDEO_CAPTURE)
 		return -EINVAL;
@@ -1064,22 +1214,30 @@ static int info_to_fmt(const struct spa_video_info *info, struct v4l2_format *fm
 	case SPA_MEDIA_SUBTYPE_raw:
 		fmt->fmt.pix.width = info->info.raw.size.width;
 		fmt->fmt.pix.height = info->info.raw.size.height;
+		fmt->fmt.pix.colorspace = V4L2_COLORSPACE_SRGB;
 		break;
 	case SPA_MEDIA_SUBTYPE_mjpg:
 	case SPA_MEDIA_SUBTYPE_jpeg:
 		fmt->fmt.pix.width = info->info.mjpg.size.width;
 		fmt->fmt.pix.height = info->info.mjpg.size.height;
+		fmt->fmt.pix.colorspace = V4L2_COLORSPACE_JPEG;
 		break;
 	case SPA_MEDIA_SUBTYPE_h264:
 		fmt->fmt.pix.width = info->info.h264.size.width;
 		fmt->fmt.pix.height = info->info.h264.size.height;
+		fmt->fmt.pix.colorspace = V4L2_COLORSPACE_SRGB;
 		break;
 	default:
 		return -EINVAL;
 	}
+	if (fmt->fmt.pix.width == 0 ||
+	    fmt->fmt.pix.height == 0)
+		return -EINVAL;
+
 	fmt->fmt.pix.bytesperline = SPA_ROUND_UP_N(fmt->fmt.pix.width, 4) * fi->bpp;
 	fmt->fmt.pix.sizeimage = fmt->fmt.pix.bytesperline *
 		SPA_ROUND_UP_N(fmt->fmt.pix.height, 2);
+
 	return 0;
 }
 
@@ -1108,7 +1266,7 @@ static void on_stream_param_changed(void *data, uint32_t id, const struct spa_po
 	uint32_t n_params = 0;
 	uint8_t buffer[4096];
 	struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
-	uint32_t buffers, size;
+	uint32_t buffers, size, stride;
 	struct v4l2_format fmt;
 
 	if (param == NULL || id != SPA_PARAM_Format)
@@ -1119,18 +1277,20 @@ static void on_stream_param_changed(void *data, uint32_t id, const struct spa_po
 
 	file->v4l2_format = fmt;
 
-	buffers = SPA_CLAMP(file->reqbufs, 2u, MAX_BUFFERS);
-	size = 0;
+	buffers = SPA_CLAMP(file->reqbufs, 1u, MAX_BUFFERS);
+	size = fmt.fmt.pix.sizeimage;
+	stride = fmt.fmt.pix.bytesperline;
 
 	params[n_params++] = spa_pod_builder_add_object(&b,
 			SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers,
 			SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(buffers,
-							2, MAX_BUFFERS),
+							1, MAX_BUFFERS),
 			SPA_PARAM_BUFFERS_blocks,  SPA_POD_Int(1),
 			SPA_PARAM_BUFFERS_size,    SPA_POD_CHOICE_RANGE_Int(size, 0, INT_MAX),
-			SPA_PARAM_BUFFERS_stride,  SPA_POD_CHOICE_RANGE_Int(0, 0, INT_MAX),
+			SPA_PARAM_BUFFERS_stride,  SPA_POD_CHOICE_RANGE_Int(stride, 0, INT_MAX),
 			SPA_PARAM_BUFFERS_dataType, SPA_POD_CHOICE_FLAGS_Int((1<<SPA_DATA_MemFd)));
 
+
 	pw_stream_update_params(file->stream, params, n_params);
 
 }
@@ -1140,7 +1300,7 @@ static void on_stream_state_changed(void *data, enum pw_stream_state old,
 {
 	struct file *file = data;
 
-	pw_log_info("%p: state %s", file, pw_stream_state_as_string(state));
+	pw_log_info("file:%d: state %s", file->fd, pw_stream_state_as_string(state));
 
 	switch (state) {
 	case PW_STREAM_STATE_ERROR:
@@ -1165,7 +1325,8 @@ static void on_stream_add_buffer(void *data, struct pw_buffer *b)
 
 	file->size = d->maxsize;
 
-	pw_log_info("%p: id:%d fd:%"PRIi64" size:%u", file, id, d->fd, file->size);
+	pw_log_info("file:%d: id:%d fd:%"PRIi64" size:%u offset:%u", file->fd,
+			id, d->fd, file->size, id * file->size);
 
 	spa_zero(vb);
 	vb.index = id;
@@ -1192,7 +1353,8 @@ static void on_stream_remove_buffer(void *data, struct pw_buffer *b)
 static void on_stream_process(void *data)
 {
 	struct file *file = data;
-        spa_system_eventfd_write(file->l->system, file->fd, 1);
+	pw_log_debug("file:%d", file->fd);
+	spa_system_eventfd_write(file->l->system, file->fd, 1);
 }
 
 static const struct pw_stream_events stream_events = {
@@ -1251,7 +1413,7 @@ static int vidioc_enum_framesizes(struct file *file, struct v4l2_frmsizeenum *ar
 		arg->discrete.width = size.width;
 		arg->discrete.height = size.height;
 
-		pw_log_debug("count:%d %d %dx%d", count, fi->fourcc,
+		pw_log_debug("count:%d %.4s %dx%d", count, (char*)&fi->fourcc,
 				size.width, size.height);
 		if (count == arg->index) {
 			found = true;
@@ -1327,10 +1489,13 @@ static int vidioc_enum_fmt(struct file *file, struct v4l2_fmtdesc *arg)
 
 		if (fi->fourcc == last_fourcc)
 			continue;
-		pw_log_info("count:%d %d %d", count, fi->fourcc, last_fourcc);
+		pw_log_info("count:%d fourcc:%.4s last:%.4s", count,
+				(char*)&fi->fourcc, (char*)&last_fourcc);
 
 		arg->flags = fi->format == SPA_VIDEO_FORMAT_ENCODED ? V4L2_FMT_FLAG_COMPRESSED : 0;
 		arg->pixelformat = fi->fourcc;
+		snprintf((char*)arg->description, sizeof(arg->description), "%s",
+				fi->desc ? fi->desc : "Unknown");
 		last_fourcc = fi->fourcc;
 		if (count == arg->index) {
 			found = true;
@@ -1343,7 +1508,7 @@ static int vidioc_enum_fmt(struct file *file, struct v4l2_fmtdesc *arg)
 	if (!found)
 		return -EINVAL;
 
-	pw_log_info("format: %u", arg->pixelformat);
+	pw_log_info("format: %.4s", (char*)&arg->pixelformat);
 	pw_log_info("flags: %u", arg->type);
 	memset(arg->reserved, 0, sizeof(arg->reserved));
 
@@ -1352,21 +1517,50 @@ static int vidioc_enum_fmt(struct file *file, struct v4l2_fmtdesc *arg)
 
 static int vidioc_g_fmt(struct file *file, struct v4l2_format *arg)
 {
+	struct param *p;
+	struct global *g = file->node;
+	int res;
+
 	if (arg->type != V4L2_BUF_TYPE_VIDEO_CAPTURE)
 		return -EINVAL;
 
-	*arg = file->v4l2_format;
-	return 0;
+	pw_thread_loop_lock(file->loop);
+	if (file->v4l2_format.fmt.pix.pixelformat != 0) {
+		*arg = file->v4l2_format;
+	} else {
+		struct v4l2_format tmp;
+		bool found = false;
+
+		spa_list_for_each(p, &g->param_list, link) {
+			if (p->id != SPA_PARAM_EnumFormat || p->param == NULL)
+				continue;
+
+			if (param_to_fmt(p->param, &tmp) < 0)
+				continue;
+
+			found = true;
+			break;
+		}
+		if (!found) {
+			res = -EINVAL;
+			goto exit_unlock;
+		}
+		*arg = file->v4l2_format = tmp;
+	}
+	res = 0;
+exit_unlock:
+	pw_thread_loop_unlock(file->loop);
+	return res;
 }
 
 static int score_diff(struct v4l2_format *fmt, struct v4l2_format *tmp)
 {
-	int score = 0;
+	int score = 0, w, h;
 	if (fmt->fmt.pix.pixelformat != tmp->fmt.pix.pixelformat)
 		score += 20000;
-	score += SPA_ABS((int)fmt->fmt.pix.width - (int)tmp->fmt.pix.width);
-	score += SPA_ABS((int)fmt->fmt.pix.height - (int)tmp->fmt.pix.height);
-	return score;
+	w = SPA_ABS((int)fmt->fmt.pix.width - (int)tmp->fmt.pix.width);
+	h = SPA_ABS((int)fmt->fmt.pix.height - (int)tmp->fmt.pix.height);
+	return score + (w*w) + (h*h);
 }
 
 static int try_format(struct file *file, struct v4l2_format *fmt)
@@ -1377,6 +1571,9 @@ static int try_format(struct file *file, struct v4l2_format *fmt)
 	int best = -1;
 
 	pw_log_info("in: type: %u", fmt->type);
+	if (fmt->type != V4L2_BUF_TYPE_VIDEO_CAPTURE)
+		return -EINVAL;
+
 	pw_log_info("in: format: %.4s", (char*)&fmt->fmt.pix.pixelformat);
 	pw_log_info("in: width: %u", fmt->fmt.pix.width);
 	pw_log_info("in: height: %u", fmt->fmt.pix.height);
@@ -1385,7 +1582,10 @@ static int try_format(struct file *file, struct v4l2_format *fmt)
 		struct v4l2_format tmp;
 		int score;
 
-		if (p->id != SPA_PARAM_EnumFormat || p->param == NULL)
+		if (p->param == NULL)
+			continue;
+
+		if (p->id != SPA_PARAM_EnumFormat && p->id != SPA_PARAM_Format)
 			continue;
 
 		if (param_to_fmt(p->param, &tmp) < 0)
@@ -1398,6 +1598,10 @@ static int try_format(struct file *file, struct v4l2_format *fmt)
 		pw_log_debug("check: height: %u", tmp.fmt.pix.height);
 		pw_log_debug("check: score: %d best:%d", score, best);
 
+		if (p->id == SPA_PARAM_Format) {
+			best_fmt = tmp;
+			break;
+		}
 		if (best == -1 || score < best) {
 			best = score;
 			best_fmt = tmp;
@@ -1415,6 +1619,7 @@ static int try_format(struct file *file, struct v4l2_format *fmt)
 static int disconnect_stream(struct file *file)
 {
 	if (file->stream != NULL) {
+		pw_log_info("file:%d disconnect", file->fd);
 		pw_stream_destroy(file->stream);
 		file->stream = NULL;
 		file->n_buffers = 0;
@@ -1530,34 +1735,60 @@ static int vidioc_try_fmt(struct file *file, struct v4l2_format *arg)
 
 static int vidioc_enuminput(struct file *file, struct v4l2_input *arg)
 {
-	if (arg->index != 0)
-		return -EINVAL;
-
+	uint32_t index = arg->index;
 	spa_zero(*arg);
-	spa_scnprintf((char*)arg->name, sizeof(arg->name), "%s", DEFAULT_CARD);
-        arg->type = V4L2_INPUT_TYPE_CAMERA;
-
-        return 0;
+	arg->index = index;
+	switch (arg->index) {
+	case 0:
+		spa_scnprintf((char*)arg->name, sizeof(arg->name), "%s", DEFAULT_CARD);
+	        arg->type = V4L2_INPUT_TYPE_CAMERA;
+		break;
+	default:
+		return -EINVAL;
+	}
+	return 0;
 }
 static int vidioc_g_input(struct file *file, int *arg)
 {
 	*arg = 0;
-        return 0;
+	return 0;
 }
 static int vidioc_s_input(struct file *file, int *arg)
 {
 	if (*arg != 0)
 		return -EINVAL;
-        return 0;
+	return 0;
+}
+
+static int vidioc_g_priority(struct file *file, enum v4l2_priority *arg)
+{
+	*arg = file->priority;
+	pw_log_info("file:%d prio:%d", file->fd, *arg);
+	return 0;
+}
+static int vidioc_s_priority(struct file *file, int fd, enum v4l2_priority *arg)
+{
+	if (*arg > V4L2_PRIORITY_RECORD)
+		return -EINVAL;
+
+	if (file->fd != fd && file->priority > *arg)
+		return -EINVAL;
+
+	pw_log_info("file:%d (%d) prio:%d", file->fd, fd, *arg);
+	file->priority = *arg;
+	return 0;
 }
 
-static int vidioc_reqbufs(struct file *file, struct v4l2_requestbuffers *arg)
+static int vidioc_reqbufs(struct file *file, int fd, struct v4l2_requestbuffers *arg)
 {
 	int res;
 
 	pw_log_info("count: %u", arg->count);
 	pw_log_info("type: %u", arg->type);
 	pw_log_info("memory: %u", arg->memory);
+#ifdef V4L2_MEMORY_FLAG_NON_COHERENT
+	pw_log_info("flags: %08x", arg->flags);
+#endif
 
 	if (arg->type != V4L2_BUF_TYPE_VIDEO_CAPTURE)
 		return -EINVAL;
@@ -1566,17 +1797,25 @@ static int vidioc_reqbufs(struct file *file, struct v4l2_requestbuffers *arg)
 
 	pw_thread_loop_lock(file->loop);
 
+	if (file->n_buffers > 0 && file->reqbufs_fd != fd) {
+		pw_log_info("%u fd:%d != %d", file->n_buffers, file->reqbufs_fd, fd);
+		res = -EBUSY;
+		goto exit_unlock;
+	}
 	if (arg->count == 0) {
 		if (pw_array_get_len(&file->buffer_maps, struct buffer_map) != 0) {
+			pw_log_info("fd:%d have maps", fd);
 			res = -EBUSY;
 			goto exit_unlock;
 		}
 		if (file->running) {
+			pw_log_info("fd:%d running", fd);
 			res = -EBUSY;
 			goto exit_unlock;
 		}
-		file->reqbufs = 0;
 		res = disconnect_stream(file);
+		file->reqbufs = 0;
+		file->reqbufs_fd = -1;
 	} else {
 		file->reqbufs = arg->count;
 
@@ -1584,7 +1823,11 @@ static int vidioc_reqbufs(struct file *file, struct v4l2_requestbuffers *arg)
 			goto exit_unlock;
 
 		arg->count = file->n_buffers;
+		file->reqbufs_fd = fd;
 	}
+#ifdef V4L2_MEMORY_FLAG_NON_COHERENT
+	arg->flags = 0;
+#endif
 #ifdef V4L2_BUF_CAP_SUPPORTS_MMAP
 	arg->capabilities = V4L2_BUF_CAP_SUPPORTS_MMAP;
 #endif
@@ -1593,6 +1836,8 @@ static int vidioc_reqbufs(struct file *file, struct v4l2_requestbuffers *arg)
 	pw_log_info("result count: %u", arg->count);
 
 exit_unlock:
+	if (res < 0)
+		pw_log_info("error : %s", spa_strerror(res));
 	pw_thread_loop_unlock(file->loop);
 	return res;
 }
@@ -1645,42 +1890,52 @@ static int vidioc_qbuf(struct file *file, struct v4l2_buffer *arg)
 	arg->flags = buf->v4l2.flags;
 
 	pw_stream_queue_buffer(file->stream, buf->buf);
-	pw_log_debug("file:%p %d -> %d (%s)", file, arg->index, res, spa_strerror(res));
-
 exit:
+	pw_log_debug("file:%d %d -> %d (%s)", file->fd, arg->index, res, spa_strerror(res));
 	pw_thread_loop_unlock(file->loop);
 
 	return res;
 }
-static int vidioc_dqbuf(struct file *file, struct v4l2_buffer *arg)
+static int vidioc_dqbuf(struct file *file, int fd, struct v4l2_buffer *arg)
 {
 	int res = 0;
 	struct pw_buffer *b;
 	struct buffer *buf;
 	uint64_t val;
 	struct spa_data *d;
+	struct timespec ts;
 
 	if (arg->type != V4L2_BUF_TYPE_VIDEO_CAPTURE)
 		return -EINVAL;
 	if (arg->memory != V4L2_MEMORY_MMAP)
 		return -EINVAL;
 
+	pw_log_debug("file:%d (%d) %d", file->fd, fd,
+			arg->index);
+
 	pw_thread_loop_lock(file->loop);
 	if (arg->index >= file->n_buffers) {
 		res = -EINVAL;
 		goto exit_unlock;
 	}
-	if (!file->running) {
-		res = -EINVAL;
-		goto exit_unlock;
-	}
 
-	b = pw_stream_dequeue_buffer(file->stream);
-	if (b == NULL) {
-		res = -EAGAIN;
-		goto exit_unlock;
+	while (true) {
+		if (!file->running) {
+			res = -EINVAL;
+			goto exit_unlock;
+		}
+
+		b = pw_stream_dequeue_buffer(file->stream);
+		if (b != NULL)
+			break;
+
+		pw_thread_loop_unlock(file->loop);
+		res = spa_system_eventfd_read(file->l->system, fd, &val);
+		pw_thread_loop_lock(file->loop);
+		if (res < 0)
+			goto exit_unlock;
 	}
-	spa_system_eventfd_read(file->l->system, file->fd, &val);
+
 
 	buf = b->user_data;
 	d = &buf->buf->buffer->datas[0];
@@ -1689,13 +1944,20 @@ static int vidioc_dqbuf(struct file *file, struct v4l2_buffer *arg)
 	SPA_FLAG_UPDATE(buf->v4l2.flags, V4L2_BUF_FLAG_ERROR,
 		SPA_FLAG_IS_SET(d->chunk->flags, SPA_CHUNK_FLAG_CORRUPTED));
 
+	SPA_FLAG_SET(buf->v4l2.flags, V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC);
+	clock_gettime(CLOCK_MONOTONIC, &ts);
+	buf->v4l2.timestamp.tv_sec = ts.tv_sec;
+	buf->v4l2.timestamp.tv_usec = ts.tv_nsec / 1000;
+
+	buf->v4l2.field = V4L2_FIELD_NONE;
 	buf->v4l2.bytesused = d->chunk->size;
+	buf->v4l2.sequence = file->sequence++;
 	*arg = buf->v4l2;
 
 exit_unlock:
+	pw_log_debug("file:%d (%d) %d -> %d (%s)", file->fd, fd,
+			arg->index, res, spa_strerror(res));
 	pw_thread_loop_unlock(file->loop);
-
-	pw_log_debug("file:%p %d -> %d (%s)", file, arg->index, res, spa_strerror(res));
 	return res;
 }
 
@@ -1703,8 +1965,6 @@ static int vidioc_streamon(struct file *file, int *arg)
 {
 	int res;
 
-	pw_log_info("file:%p -> %d", file, *arg);
-
 	if (*arg != V4L2_BUF_TYPE_VIDEO_CAPTURE)
 		return -EINVAL;
 
@@ -1723,28 +1983,34 @@ static int vidioc_streamon(struct file *file, int *arg)
 exit_unlock:
 	pw_thread_loop_unlock(file->loop);
 
-	pw_log_info("file:%p -> %d (%s)", file, res, spa_strerror(res));
+	pw_log_info("file:%d -> %d (%s)", file->fd, res, spa_strerror(res));
 	return res;
 }
 static int vidioc_streamoff(struct file *file, int *arg)
 {
 	int res;
+	uint32_t i;
 
 	if (*arg != V4L2_BUF_TYPE_VIDEO_CAPTURE)
 		return -EINVAL;
 
 	pw_thread_loop_lock(file->loop);
+	for (i = 0; i < file->n_buffers; i++) {
+		struct buffer *buf = &file->buffers[i];
+		SPA_FLAG_CLEAR(buf->v4l2.flags, V4L2_BUF_FLAG_QUEUED);
+	}
 	if (!file->running) {
 		res = 0;
 		goto exit_unlock;
 	}
 	res = pw_stream_set_active(file->stream, false);
 	file->running = false;
+	file->sequence = 0;
 
 exit_unlock:
 	pw_thread_loop_unlock(file->loop);
 
-	pw_log_info("file:%p -> %d (%s)", file, res, spa_strerror(res));
+	pw_log_info("file:%d -> %d (%s)", file->fd, res, spa_strerror(res));
 	return res;
 }
 
@@ -1752,8 +2018,9 @@ static int v4l2_ioctl(int fd, unsigned long int request, void *arg)
 {
 	int res;
 	struct file *file;
+	uint32_t flags;
 
-	if ((file = find_file(fd)) == NULL)
+	if ((file = find_file(fd, &flags)) == NULL)
 		return globals.old_fops.ioctl(fd, request, arg);
 
 #if defined(__FreeBSD__) || defined(__MidnightBSD__)
@@ -1765,6 +2032,9 @@ static int v4l2_ioctl(int fd, unsigned long int request, void *arg)
 		goto done;
 	}
 
+	if (flags & FD_MAP_DUP)
+		fd = file->fd;
+
 	switch (request & 0xffffffff) {
 	case VIDIOC_QUERYCAP:
 		res = vidioc_querycap(file, (struct v4l2_capability *)arg);
@@ -1793,8 +2063,14 @@ static int v4l2_ioctl(int fd, unsigned long int request, void *arg)
 	case VIDIOC_S_INPUT:
 		res = vidioc_s_input(file, (int *)arg);
 		break;
+	case VIDIOC_G_PRIORITY:
+		res = vidioc_g_priority(file, (enum v4l2_priority *)arg);
+		break;
+	case VIDIOC_S_PRIORITY:
+		res = vidioc_s_priority(file, fd, (enum v4l2_priority *)arg);
+		break;
 	case VIDIOC_REQBUFS:
-		res = vidioc_reqbufs(file, (struct v4l2_requestbuffers *)arg);
+		res = vidioc_reqbufs(file, fd, (struct v4l2_requestbuffers *)arg);
 		break;
 	case VIDIOC_QUERYBUF:
 		res = vidioc_querybuf(file, (struct v4l2_buffer *)arg);
@@ -1803,7 +2079,7 @@ static int v4l2_ioctl(int fd, unsigned long int request, void *arg)
 		res = vidioc_qbuf(file, (struct v4l2_buffer *)arg);
 		break;
 	case VIDIOC_DQBUF:
-		res = vidioc_dqbuf(file, (struct v4l2_buffer *)arg);
+		res = vidioc_dqbuf(file, fd, (struct v4l2_buffer *)arg);
 		break;
 	case VIDIOC_STREAMON:
 		res = vidioc_streamon(file, (int *)arg);
@@ -1820,8 +2096,8 @@ done:
 		errno = -res;
 		res = -1;
 	}
-	pw_log_debug("fd:%d request:%lx nr:%d arg:%p -> %d (%s)",
-			fd, request, (int)_IOC_NR(request), arg,
+	pw_log_debug("file:%d fd:%d request:%lx nr:%d arg:%p -> %d (%s)",
+			file->fd, fd, request, (int)_IOC_NR(request), arg,
 			res, strerror(res < 0 ? errno : 0));
 
 	unref_file(file);
@@ -1838,8 +2114,9 @@ static void *v4l2_mmap(void *addr, size_t length, int prot,
 	struct pw_map_range range;
 	struct buffer *buf;
 	struct spa_data *data;
+	uint32_t fl;
 
-	if ((file = find_file(fd)) == NULL)
+	if ((file = find_file(fd, &fl)) == NULL)
 		return globals.old_fops.mmap(addr, length, prot, flags, fd, offset);
 
 	pw_thread_loop_lock(file->loop);
@@ -1864,14 +2141,19 @@ static void *v4l2_mmap(void *addr, size_t length, int prot,
 	if (!SPA_FLAG_IS_SET(data->flags, SPA_DATA_FLAG_WRITABLE))
 		prot &= ~PROT_WRITE;
 
-	res = globals.old_fops.mmap(addr, range.size, prot, flags, data->fd, range.offset);
+	if (data->data == NULL)
+		res = globals.old_fops.mmap(addr, range.size, prot, flags, data->fd, range.offset);
+	else
+		res = data->data;
 
-	add_file_map(file, addr);
-	add_buffer_map(file, addr, id);
+	add_file_map(file, res);
+	add_buffer_map(file, res, id);
 	SPA_FLAG_SET(buf->v4l2.flags, V4L2_BUF_FLAG_MAPPED);
 
-	pw_log_info("addr:%p length:%u prot:%d flags:%d fd:%"PRIi64" offset:%u -> %p (%s)" ,
-			addr, range.size, prot, flags, data->fd, range.offset,
+	pw_log_info("file:%d addr:%p length:%zu prot:%d flags:%d fd:%"PRIi64
+			" offset:%"PRIi64" (%u - %u) -> %p (%s)" ,
+			file->fd, addr, length, prot, flags, data->fd, offset,
+			range.offset, range.size,
 			res, strerror(res == MAP_FAILED ? errno : 0));
 
 error_unlock:
@@ -1884,7 +2166,9 @@ static int v4l2_munmap(void *addr, size_t length)
 {
 	int res;
 	struct buffer_map *bmap;
+	struct buffer *buf;
 	struct file *file;
+	struct spa_data *data;
 
 	if ((file = remove_file_map(addr)) == NULL)
 		return globals.old_fops.munmap(addr, length);
@@ -1896,12 +2180,18 @@ static int v4l2_munmap(void *addr, size_t length)
 		res = -EINVAL;
 		goto exit_unlock;
 	}
-	res = globals.old_fops.munmap(addr, length);
+	buf = &file->buffers[bmap->id];
+	data = &buf->buf->buffer->datas[0];
+
+	if (data->data == NULL)
+		res = globals.old_fops.munmap(addr, length);
+	else
+		res = 0;
 
 	pw_log_info("addr:%p length:%zu -> %d (%s)", addr, length,
 			res, strerror(res < 0 ? errno : 0));
 
-	file->buffers[bmap->id].v4l2.flags &= ~V4L2_BUF_FLAG_MAPPED;
+	buf->v4l2.flags &= ~V4L2_BUF_FLAG_MAPPED;
 	remove_buffer_map(file, bmap);
 
 exit_unlock:
diff --git a/pipewire-v4l2/src/pw-v4l2.in b/pipewire-v4l2/src/pw-v4l2.in
index a383ba2305e400bd609cbdb8730d97cab5bdd59f..248d9ee60086984252ed9e4e180e7bee02b37c20 100755
--- a/pipewire-v4l2/src/pw-v4l2.in
+++ b/pipewire-v4l2/src/pw-v4l2.in
@@ -54,10 +54,16 @@ done
 
 shift $(( OPTIND - 1 ))
 
+if [ "$PW_UNINSTALLED" = 1 ] ; then
+   PW_V4L2_LD_PRELOAD="$PW_BUILDDIR"'/pipewire-v4l2/src/libpw-v4l2.so'
+else
+   PW_V4L2_LD_PRELOAD='@LIBV4L2_PATH@/libpw-v4l2.so'
+fi
+
 if [ "$LD_PRELOAD" = "" ] ; then
-   LD_PRELOAD='@LIBV4L2_PATH@/libpw-v4l2.so'
+   LD_PRELOAD="$PW_V4L2_LD_PRELOAD"
 else
-   LD_PRELOAD="$LD_PRELOAD "'@LIBV4L2_PATH@/libpw-v4l2.so'
+   LD_PRELOAD="$LD_PRELOAD $PW_V4L2_LD_PRELOAD"
 fi
 
 export LD_PRELOAD
diff --git a/po/cs.po b/po/cs.po
index 1020942eee6c36b441f27b99836ef744427bb998..b148320a896f9298dbced45dd1848b6210715101 100644
--- a/po/cs.po
+++ b/po/cs.po
@@ -8,10 +8,10 @@
 msgid ""
 msgstr ""
 "Project-Id-Version: pipewire.master-tx\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: 2021-10-12 14:18+0200\n"
+"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/-/"
+"issues\n"
+"POT-Creation-Date: 2022-09-15 15:26+0000\n"
+"PO-Revision-Date: 2022-10-21 16:44+0200\n"
 "Last-Translator: Daniel Rusek <mail@asciiwolf.com>\n"
 "Language-Team: čeština <gnome-cs-list@gnome.org>\n"
 "Language: cs\n"
@@ -19,9 +19,9 @@ msgstr ""
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
 "Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n"
-"X-Generator: Poedit 3.0\n"
+"X-Generator: Poedit 3.1.1\n"
 
-#: src/daemon/pipewire.c:43
+#: src/daemon/pipewire.c:46
 #, c-format
 msgid ""
 "%s [options]\n"
@@ -42,36 +42,52 @@ msgstr "Multimediální systém PipeWire"
 msgid "Start the PipeWire Media System"
 msgstr "Spustit multimediální systém PipeWire"
 
-#: src/examples/media-session/alsa-monitor.c:526
-#: spa/plugins/alsa/acp/compat.c:187
-msgid "Built-in Audio"
-msgstr "Vnitřní zvukový systém"
+#: 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"
 
-#: src/examples/media-session/alsa-monitor.c:530
-#: spa/plugins/alsa/acp/compat.c:192
-msgid "Modem"
-msgstr "Modem"
+#: src/modules/module-fallback-sink.c:51
+#| msgid "Game Output"
+msgid "Dummy Output"
+msgstr "Předstíraný výstup"
 
-#: src/examples/media-session/alsa-monitor.c:539
+#: src/modules/module-pulse-tunnel.c:662
+#, c-format
+msgid "Tunnel for %s@%s"
+msgstr "Tunel pro %s@%s"
+
+#: src/modules/module-zeroconf-discover.c:332
 msgid "Unknown device"
 msgstr "Neznámé zařízení"
 
-#: src/tools/pw-cat.c:991
+#: 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:348
+#, c-format
+msgid "%s on %s"
+msgstr "%s na %s"
+
+#: 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 [volby] <soubor>\n"
+"%s [volby] [<soubor>|-]\n"
 "  -h, --help                            Zobrazit tuto nápovědu\n"
 "      --version                         Zobrazit verzi\n"
 "  -v, --verbose                         Povolit podrobné operace\n"
 "\n"
 
-#: src/tools/pw-cat.c:998
+#: src/tools/pw-cat.c:791
 #, c-format
 msgid ""
 "  -R, --remote                          Remote daemon name\n"
@@ -85,7 +101,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                          Název vzdáleného démonu\n"
@@ -102,10 +118,10 @@ msgstr ""
 "                                          nebo přímé vzorky (256)\n"
 "                                          frekvence je stejná jako u "
 "zdrojového souboru\n"
-"      --list-targets                    Zobrazit dostupné cíle pro --target\n"
+"  -P  --properties                      Nastavit vlastnosti uzlu\n"
 "\n"
 
-#: src/tools/pw-cat.c:1016
+#: src/tools/pw-cat.c:809
 #, c-format
 msgid ""
 "      --rate                            Sample rate (req. for rec) (default "
@@ -141,19 +157,21 @@ msgstr ""
 "je %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                        Playback mód\n"
 "  -r, --record                          Recording mód\n"
 "  -m, --midi                            Midi mód\n"
+"  -d, --dsd                             DSD mód\n"
 "\n"
 
-#: src/tools/pw-cli.c:2932
+#: src/tools/pw-cli.c:2255
 #, c-format
 msgid ""
 "%s [options] [command]\n"
@@ -171,199 +189,195 @@ msgstr ""
 "  -r, --remote                          Název vzdáleného démonu\n"
 "\n"
 
-#: spa/plugins/alsa/acp/acp.c:290
+#: spa/plugins/alsa/acp/acp.c:321
 msgid "Pro Audio"
 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:444 spa/plugins/alsa/acp/alsa-mixer.c:4648
+#: spa/plugins/bluez5/bluez5-device.c:1236
 msgid "Off"
 msgstr "Vypnuto"
 
-#: spa/plugins/alsa/acp/channelmap.h:466
-msgid "(invalid)"
-msgstr "(neplatné)"
-
-#: spa/plugins/alsa/acp/alsa-mixer.c:2709
+#: spa/plugins/alsa/acp/alsa-mixer.c:2652
 msgid "Input"
 msgstr "Vstup"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2710
+#: spa/plugins/alsa/acp/alsa-mixer.c:2653
 msgid "Docking Station Input"
 msgstr "Vstup dokovací stanice"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2711
+#: spa/plugins/alsa/acp/alsa-mixer.c:2654
 msgid "Docking Station Microphone"
 msgstr "Mikrofon dokovací stanice"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2712
+#: spa/plugins/alsa/acp/alsa-mixer.c:2655
 msgid "Docking Station Line In"
 msgstr "Linkový vstup dokovací stanice"
 
-#: 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 "Linkový vstup"
 
-#: 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:1454
 msgid "Microphone"
 msgstr "Mikrofon"
 
-#: 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 "Přední mikrofon"
 
-#: 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 "Zadní mikrofon"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2717
+#: spa/plugins/alsa/acp/alsa-mixer.c:2660
 msgid "External Microphone"
 msgstr "Externí mikrofon"
 
-#: 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 "Interní mikrofon"
 
-#: 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 "Rádio"
 
-#: 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 "Obraz"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2721
+#: spa/plugins/alsa/acp/alsa-mixer.c:2664
 msgid "Automatic Gain Control"
 msgstr "Automatické řízení zesílení"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2722
+#: spa/plugins/alsa/acp/alsa-mixer.c:2665
 msgid "No Automatic Gain Control"
 msgstr "Bez automatického řízení zesílení"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2723
+#: spa/plugins/alsa/acp/alsa-mixer.c:2666
 msgid "Boost"
 msgstr "Zdůraznění"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2724
+#: spa/plugins/alsa/acp/alsa-mixer.c:2667
 msgid "No Boost"
 msgstr "Bez zdůraznění"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2725
+#: spa/plugins/alsa/acp/alsa-mixer.c:2668
 msgid "Amplifier"
 msgstr "Zesilovač"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2726
+#: spa/plugins/alsa/acp/alsa-mixer.c:2669
 msgid "No Amplifier"
 msgstr "Bez zesilovače"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2727
+#: spa/plugins/alsa/acp/alsa-mixer.c:2670
 msgid "Bass Boost"
 msgstr "Zdůraznění basů"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2728
+#: spa/plugins/alsa/acp/alsa-mixer.c:2671
 msgid "No Bass Boost"
 msgstr "Bez zdůraznění basů"
 
-#: 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:1460
 msgid "Speaker"
 msgstr "Reproduktor"
 
-#: 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 "Sluchátka"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2797
+#: spa/plugins/alsa/acp/alsa-mixer.c:2740
 msgid "Analog Input"
 msgstr "Analogový vstup"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2801
+#: spa/plugins/alsa/acp/alsa-mixer.c:2744
 msgid "Dock Microphone"
 msgstr "Dokovací mikrofon"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2803
+#: spa/plugins/alsa/acp/alsa-mixer.c:2746
 msgid "Headset Microphone"
 msgstr "Mikrofon náhlavní soupravy"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2807
+#: spa/plugins/alsa/acp/alsa-mixer.c:2750
 msgid "Analog Output"
 msgstr "Analogový výstup"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2809
+#: spa/plugins/alsa/acp/alsa-mixer.c:2752
 msgid "Headphones 2"
 msgstr "Sluchátka 2"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2810
+#: spa/plugins/alsa/acp/alsa-mixer.c:2753
 msgid "Headphones Mono Output"
 msgstr "Sluchátkový výstup mono"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2811
+#: spa/plugins/alsa/acp/alsa-mixer.c:2754
 msgid "Line Out"
 msgstr "Linkový výstup"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2812
+#: spa/plugins/alsa/acp/alsa-mixer.c:2755
 msgid "Analog Mono Output"
 msgstr "Analogový výstup mono"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2813
+#: spa/plugins/alsa/acp/alsa-mixer.c:2756
 msgid "Speakers"
 msgstr "Reproduktory"
 
-#: 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 "Digitální výstup (S/PDIF)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2816
+#: spa/plugins/alsa/acp/alsa-mixer.c:2759
 msgid "Digital Input (S/PDIF)"
 msgstr "Digitální vstup (S/PDIF)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2817
+#: spa/plugins/alsa/acp/alsa-mixer.c:2760
 msgid "Multichannel Input"
 msgstr "Vícekanálový vstup"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2818
+#: spa/plugins/alsa/acp/alsa-mixer.c:2761
 msgid "Multichannel Output"
 msgstr "Vícekanálový výstup"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2819
+#: spa/plugins/alsa/acp/alsa-mixer.c:2762
 msgid "Game Output"
 msgstr "Herní výstup"
 
-#: 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 "Komunikační výstup"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2822
+#: spa/plugins/alsa/acp/alsa-mixer.c:2765
 msgid "Chat Input"
 msgstr "Komunikační vstup"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2823
+#: spa/plugins/alsa/acp/alsa-mixer.c:2766
 msgid "Virtual Surround 7.1"
 msgstr "Virtuální surround 7.1"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4527
+#: spa/plugins/alsa/acp/alsa-mixer.c:4471
 msgid "Analog Mono"
 msgstr "Analogové mono"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4528
+#: spa/plugins/alsa/acp/alsa-mixer.c:4472
 msgid "Analog Mono (Left)"
 msgstr "Analogové mono (levé)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4529
+#: spa/plugins/alsa/acp/alsa-mixer.c:4473
 msgid "Analog Mono (Right)"
 msgstr "Analogové mono (pravé)"
 
@@ -372,147 +386,147 @@ msgstr "Analogové mono (pravé)"
 #. * 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 "Analogové stereo"
 
-#: 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 "Stereo"
 
-#: 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:1442
 msgid "Headset"
 msgstr "Náhlavní souprava"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4541
-#: spa/plugins/alsa/acp/alsa-mixer.c:4699
+#: spa/plugins/alsa/acp/alsa-mixer.c:4485
+#: spa/plugins/alsa/acp/alsa-mixer.c:4643
 msgid "Speakerphone"
 msgstr "Hlasitý odposlech"
 
-#: 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 "Více kanálů"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4544
+#: spa/plugins/alsa/acp/alsa-mixer.c:4488
 msgid "Analog Surround 2.1"
 msgstr "Analogový Surround 2.1"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4545
+#: spa/plugins/alsa/acp/alsa-mixer.c:4489
 msgid "Analog Surround 3.0"
 msgstr "Analogový Surround 3.0"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4546
+#: spa/plugins/alsa/acp/alsa-mixer.c:4490
 msgid "Analog Surround 3.1"
 msgstr "Analogový Surround 3.1"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4547
+#: spa/plugins/alsa/acp/alsa-mixer.c:4491
 msgid "Analog Surround 4.0"
 msgstr "Analogový Surround 4.0"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4548
+#: spa/plugins/alsa/acp/alsa-mixer.c:4492
 msgid "Analog Surround 4.1"
 msgstr "Analogový Surround 4.1"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4549
+#: spa/plugins/alsa/acp/alsa-mixer.c:4493
 msgid "Analog Surround 5.0"
 msgstr "Analogový Surround 5.0"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4550
+#: spa/plugins/alsa/acp/alsa-mixer.c:4494
 msgid "Analog Surround 5.1"
 msgstr "Analogový Surround 5.1"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4551
+#: spa/plugins/alsa/acp/alsa-mixer.c:4495
 msgid "Analog Surround 6.0"
 msgstr "Analogový Surround 6.0"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4552
+#: spa/plugins/alsa/acp/alsa-mixer.c:4496
 msgid "Analog Surround 6.1"
 msgstr "Analogový Surround 6.1"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4553
+#: spa/plugins/alsa/acp/alsa-mixer.c:4497
 msgid "Analog Surround 7.0"
 msgstr "Analogový Surround 7.0"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4554
+#: spa/plugins/alsa/acp/alsa-mixer.c:4498
 msgid "Analog Surround 7.1"
 msgstr "Analogový Surround 7.1"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4555
+#: spa/plugins/alsa/acp/alsa-mixer.c:4499
 msgid "Digital Stereo (IEC958)"
 msgstr "Digitální stereo (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 "Digitální Surround 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 "Digitální Surround 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 "Digitální Surround 5.1 (IEC958/DTS)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4559
+#: spa/plugins/alsa/acp/alsa-mixer.c:4503
 msgid "Digital Stereo (HDMI)"
 msgstr "Digitální stereo (HDMI)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4560
+#: spa/plugins/alsa/acp/alsa-mixer.c:4504
 msgid "Digital Surround 5.1 (HDMI)"
 msgstr "Digitální Surround 5.1 (HDMI)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4561
+#: spa/plugins/alsa/acp/alsa-mixer.c:4505
 msgid "Chat"
 msgstr "Chat"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4562
+#: spa/plugins/alsa/acp/alsa-mixer.c:4506
 msgid "Game"
 msgstr "Hra"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4696
+#: spa/plugins/alsa/acp/alsa-mixer.c:4640
 msgid "Analog Mono Duplex"
 msgstr "Analogové duplexní mono"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4697
+#: spa/plugins/alsa/acp/alsa-mixer.c:4641
 msgid "Analog Stereo Duplex"
 msgstr "Analogové duplexní stereo"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4700
+#: spa/plugins/alsa/acp/alsa-mixer.c:4644
 msgid "Digital Stereo Duplex (IEC958)"
 msgstr "Digitální duplexní stereo (IEC958)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4701
+#: spa/plugins/alsa/acp/alsa-mixer.c:4645
 msgid "Multichannel Duplex"
 msgstr "Vícekanálový duplex"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4702
+#: spa/plugins/alsa/acp/alsa-mixer.c:4646
 msgid "Stereo Duplex"
 msgstr "Duplexní stereo"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4703
+#: spa/plugins/alsa/acp/alsa-mixer.c:4647
 msgid "Mono Chat + 7.1 Surround"
 msgstr "Mono Chat + 7.1 Surround"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4806
+#: spa/plugins/alsa/acp/alsa-mixer.c:4754
 #, c-format
 msgid "%s Output"
 msgstr "Výstup %s"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4813
+#: spa/plugins/alsa/acp/alsa-mixer.c:4761
 #, c-format
 msgid "%s Input"
 msgstr "Vstup %s"
 
-#: spa/plugins/alsa/acp/alsa-util.c:1175 spa/plugins/alsa/acp/alsa-util.c:1269
+#: 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 "
@@ -540,16 +554,16 @@ msgstr[2] ""
 "S největší pravděpodobností se jedná o chybu v ovladači ALSA „%s“. Nahlaste "
 "prosím tento problém vývojářům ALSA."
 
-#: spa/plugins/alsa/acp/alsa-util.c:1241
+#: spa/plugins/alsa/acp/alsa-util.c:1253
 #, 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] ""
@@ -568,7 +582,7 @@ msgstr[2] ""
 "S největší pravděpodobností se jedná o chybu v ovladači ALSA „%s“. Nahlaste "
 "prosím tento problém vývojářům ALSA."
 
-#: spa/plugins/alsa/acp/alsa-util.c:1288
+#: spa/plugins/alsa/acp/alsa-util.c:1300
 #, c-format
 msgid ""
 "snd_pcm_avail_delay() returned strange values: delay %lu is less than avail "
@@ -581,7 +595,7 @@ msgstr ""
 "S největší pravděpodobností se jedná o chybu v ovladači ALSA „%s“. Nahlaste "
 "prosím tento problém vývojářům ALSA."
 
-#: spa/plugins/alsa/acp/alsa-util.c:1331
+#: spa/plugins/alsa/acp/alsa-util.c:1343
 #, c-format
 msgid ""
 "snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu byte "
@@ -609,61 +623,109 @@ msgstr[2] ""
 "S největší pravděpodobností se jedná o chybu v ovladači ALSA „%s“. Nahlaste "
 "prosím tento problém vývojářům ALSA."
 
-#: spa/plugins/bluez5/bluez5-device.c:1010
+#: spa/plugins/alsa/acp/channelmap.h:457
+msgid "(invalid)"
+msgstr "(neplatné)"
+
+#: spa/plugins/alsa/acp/compat.c:189
+msgid "Built-in Audio"
+msgstr "Vnitřní zvukový systém"
+
+#: spa/plugins/alsa/acp/compat.c:194
+msgid "Modem"
+msgstr "Modem"
+
+#: spa/plugins/bluez5/bluez5-device.c:1247
 msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
 msgstr "Zvuková brána (A2DP Source & HSP/HFP AG)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1033
+#: spa/plugins/bluez5/bluez5-device.c:1272
 #, c-format
 msgid "High Fidelity Playback (A2DP Sink, codec %s)"
 msgstr "High Fidelity Playback (A2DP Sink, kodek %s)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1035
+#: spa/plugins/bluez5/bluez5-device.c:1275
 #, c-format
 msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
 msgstr "High Fidelity Duplex (A2DP Source/Sink, kodek %s)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1041
+#: spa/plugins/bluez5/bluez5-device.c:1283
 msgid "High Fidelity Playback (A2DP Sink)"
 msgstr "High Fidelity Playback (A2DP Sink)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1043
+#: spa/plugins/bluez5/bluez5-device.c:1285
 msgid "High Fidelity Duplex (A2DP Source/Sink)"
 msgstr "High Fidelity Duplex (A2DP Source/Sink)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1070
+#: spa/plugins/bluez5/bluez5-device.c:1322
+#, c-format
+#| msgid "High Fidelity Playback (A2DP Sink, codec %s)"
+msgid "High Fidelity Playback (BAP Sink, codec %s)"
+msgstr "High Fidelity Playback (BAP Sink, kodek %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1326
+#, c-format
+#| msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
+msgid "High Fidelity Input (BAP Source, codec %s)"
+msgstr "High Fidelity Input (BAP Source, kodek %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1330
+#, c-format
+#| msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
+msgid "High Fidelity Duplex (BAP Source/Sink, codec %s)"
+msgstr "High Fidelity Duplex (BAP Source/Sink, kodek %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1359
 #, c-format
 msgid "Headset Head Unit (HSP/HFP, codec %s)"
 msgstr "Jednotka náhlavní soupravy (HSP/HFP, kodek %s)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1074
+#: spa/plugins/bluez5/bluez5-device.c:1364
 msgid "Headset Head Unit (HSP/HFP)"
 msgstr "Jednotka náhlavní soupravy (HSP/HFP)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1140
+#: spa/plugins/bluez5/bluez5-device.c:1443
+#: spa/plugins/bluez5/bluez5-device.c:1448
+#: spa/plugins/bluez5/bluez5-device.c:1455
+#: spa/plugins/bluez5/bluez5-device.c:1461
+#: spa/plugins/bluez5/bluez5-device.c:1467
+#: spa/plugins/bluez5/bluez5-device.c:1473
+#: spa/plugins/bluez5/bluez5-device.c:1479
+#: spa/plugins/bluez5/bluez5-device.c:1485
+#: spa/plugins/bluez5/bluez5-device.c:1491
 msgid "Handsfree"
 msgstr "Handsfree"
 
-#: spa/plugins/bluez5/bluez5-device.c:1155
+#: spa/plugins/bluez5/bluez5-device.c:1449
+#| msgid "Handsfree"
+msgid "Handsfree (HFP)"
+msgstr "Handsfree (HFP)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1466
 msgid "Headphone"
 msgstr "Sluchátko"
 
-#: spa/plugins/bluez5/bluez5-device.c:1160
+#: spa/plugins/bluez5/bluez5-device.c:1472
 msgid "Portable"
 msgstr "Přenosné zařízení"
 
-#: spa/plugins/bluez5/bluez5-device.c:1165
+#: spa/plugins/bluez5/bluez5-device.c:1478
 msgid "Car"
 msgstr "Auto"
 
-#: spa/plugins/bluez5/bluez5-device.c:1170
+#: spa/plugins/bluez5/bluez5-device.c:1484
 msgid "HiFi"
 msgstr "HiFi"
 
-#: spa/plugins/bluez5/bluez5-device.c:1175
+#: spa/plugins/bluez5/bluez5-device.c:1490
 msgid "Phone"
 msgstr "Telefon"
 
-#: spa/plugins/bluez5/bluez5-device.c:1181
+#: spa/plugins/bluez5/bluez5-device.c:1497
 msgid "Bluetooth"
 msgstr "Bluetooth"
+
+#: spa/plugins/bluez5/bluez5-device.c:1498
+#| msgid "Bluetooth"
+msgid "Bluetooth (HFP)"
+msgstr "Bluetooth (HFP)"
diff --git a/po/hr.po b/po/hr.po
index e8c8c02119c0e65560f6d0f8ee121c4080d50b75..dbde6091c85380224e7bf18779582bb816f1ebb7 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-06-30 12:50+0200\n"
-"PO-Revision-Date: 2022-06-30 13:14+0200\n"
+"POT-Creation-Date: 2022-10-01 14:01+0200\n"
+"PO-Revision-Date: 2022-10-01 14:12+0200\n"
 "Last-Translator: gogo <trebelnik2@gmail.com>\n"
 "Language-Team: Croatian <https://translate.fedoraproject.org/projects/"
 "pipewire/pipewire/hr/>\n"
@@ -16,9 +16,9 @@ msgstr ""
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
-"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
-"X-Generator: Poedit 2.3\n"
+"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
+"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
+"X-Generator: Poedit 3.0.1\n"
 "X-Launchpad-Export-Date: 2017-04-20 21:04+0000\n"
 
 #: src/daemon/pipewire.c:46
@@ -44,7 +44,7 @@ msgstr "Tunel do %s/%s"
 msgid "Dummy Output"
 msgstr "Lažni izlaz"
 
-#: src/modules/module-pulse-tunnel.c:648
+#: src/modules/module-pulse-tunnel.c:662
 #, c-format
 msgid "Tunnel for %s@%s"
 msgstr "Tunel za %s@%s"
@@ -164,7 +164,7 @@ msgstr ""
 "  -d, --dsd                             DSD način\n"
 "\n"
 
-#: src/tools/pw-cli.c:3165
+#: src/tools/pw-cli.c:2250
 #, c-format
 msgid ""
 "%s [options] [command]\n"
@@ -187,8 +187,8 @@ msgstr ""
 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
+#: spa/plugins/alsa/acp/acp.c:444 spa/plugins/alsa/acp/alsa-mixer.c:4648
+#: spa/plugins/bluez5/bluez5-device.c:1236
 msgid "Off"
 msgstr "Isključeno"
 
@@ -215,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:1330
+#: spa/plugins/bluez5/bluez5-device.c:1454
 msgid "Microphone"
 msgstr "Mikrofon"
 
@@ -281,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:1335
+#: spa/plugins/bluez5/bluez5-device.c:1460
 msgid "Speaker"
 msgstr "Zvučnik"
 
@@ -396,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:1320
+#: spa/plugins/bluez5/bluez5-device.c:1442
 msgid "Headset"
 msgstr "Slušalice s mikrofonom"
 
@@ -520,7 +520,8 @@ 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: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 "
@@ -548,16 +549,16 @@ msgstr[2] ""
 "Najvjerojatnije je ovo greška ALSA upravljačkog programa '%s'. Prijavite "
 "problem ALSA razvijateljima."
 
-#: 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"
-"%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] ""
@@ -566,17 +567,17 @@ msgstr[0] ""
 "Najvjerojatnije je ovo greška ALSA upravljačkog programa '%s'. Prijavite "
 "problem ALSA razvijateljima."
 msgstr[1] ""
-"snd_pcm_delay() je vratio vrijednost koja je iznimno velika: %li bajta (%s"
-"%lu ms).\n"
+"snd_pcm_delay() je vratio vrijednost koja je iznimno velika: %li bajta "
+"(%s%lu ms).\n"
 "Najvjerojatnije je ovo greška ALSA upravljačkog programa '%s'. Prijavite "
 "problem ALSA razvijateljima."
 msgstr[2] ""
-"snd_pcm_delay() je vratio vrijednost koja je iznimno velika: %li bajta (%s"
-"%lu ms).\n"
+"snd_pcm_delay() je vratio vrijednost koja je iznimno velika: %li bajta "
+"(%s%lu ms).\n"
 "Najvjerojatnije je ovo greška ALSA upravljačkog programa '%s'. Prijavite "
 "problem ALSA razvijateljima."
 
-#: 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 "
@@ -589,7 +590,7 @@ msgstr ""
 "Najvjerojatnije je ovo greška ALSA upravljačkog programa '%s'. Prijavite "
 "problem ALSA razvijateljima."
 
-#: 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 "
@@ -629,65 +630,96 @@ msgstr "Ugrađeni zvuk"
 msgid "Modem"
 msgstr "Modem"
 
-#: spa/plugins/bluez5/bluez5-device.c:1172
+#: spa/plugins/bluez5/bluez5-device.c:1247
 msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
 msgstr "Zvučni pristupnik (A2DP izvor i HSP/HFP AG)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1197
+#: spa/plugins/bluez5/bluez5-device.c:1272
 #, 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:1200
+#: spa/plugins/bluez5/bluez5-device.c:1275
 #, 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:1208
+#: spa/plugins/bluez5/bluez5-device.c:1283
 msgid "High Fidelity Playback (A2DP Sink)"
 msgstr "Reprodukcija visoke autentičnosti (A2DP slivnik)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1210
+#: spa/plugins/bluez5/bluez5-device.c:1285
 msgid "High Fidelity Duplex (A2DP Source/Sink)"
 msgstr "Telefonija visoke autentičnosti (A2DP izvor/slivnik)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1238
+#: spa/plugins/bluez5/bluez5-device.c:1322
+#, c-format
+msgid "High Fidelity Playback (BAP Sink, codec %s)"
+msgstr "Reprodukcija visoke autentičnosti (BAP slivnik, kôdek %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1326
+#, c-format
+msgid "High Fidelity Input (BAP Source, codec %s)"
+msgstr "Ulaz visoke autentičnosti (BAP izvor, kôdek %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1330
+#, c-format
+msgid "High Fidelity Duplex (BAP Source/Sink, codec %s)"
+msgstr "Telefonija visoke autentičnosti (BAP izvor/slivnik, kôdek %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1359
 #, 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:1243
+#: spa/plugins/bluez5/bluez5-device.c:1364
 msgid "Headset Head Unit (HSP/HFP)"
 msgstr "Jedinica slušalice s mikrofonom (HSP/HFP)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1325
+#: spa/plugins/bluez5/bluez5-device.c:1443
+#: spa/plugins/bluez5/bluez5-device.c:1448
+#: spa/plugins/bluez5/bluez5-device.c:1455
+#: spa/plugins/bluez5/bluez5-device.c:1461
+#: spa/plugins/bluez5/bluez5-device.c:1467
+#: spa/plugins/bluez5/bluez5-device.c:1473
+#: spa/plugins/bluez5/bluez5-device.c:1479
+#: spa/plugins/bluez5/bluez5-device.c:1485
+#: spa/plugins/bluez5/bluez5-device.c:1491
 msgid "Handsfree"
 msgstr "Bez-ruku"
 
-#: spa/plugins/bluez5/bluez5-device.c:1340
+#: spa/plugins/bluez5/bluez5-device.c:1449
+msgid "Handsfree (HFP)"
+msgstr "Bez-ruku (HFP)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1466
 msgid "Headphone"
-msgstr "Slušalice"
+msgstr "Slušalica"
 
-#: spa/plugins/bluez5/bluez5-device.c:1345
+#: spa/plugins/bluez5/bluez5-device.c:1472
 msgid "Portable"
 msgstr "Prijenosnik"
 
-#: spa/plugins/bluez5/bluez5-device.c:1350
+#: spa/plugins/bluez5/bluez5-device.c:1478
 msgid "Car"
 msgstr "Automobil"
 
-#: spa/plugins/bluez5/bluez5-device.c:1355
+#: spa/plugins/bluez5/bluez5-device.c:1484
 msgid "HiFi"
 msgstr "HiFi"
 
-#: spa/plugins/bluez5/bluez5-device.c:1360
+#: spa/plugins/bluez5/bluez5-device.c:1490
 msgid "Phone"
 msgstr "Telefon"
 
-#: spa/plugins/bluez5/bluez5-device.c:1366
+#: spa/plugins/bluez5/bluez5-device.c:1497
 msgid "Bluetooth"
 msgstr "Bluetooth"
 
+#: spa/plugins/bluez5/bluez5-device.c:1498
+msgid "Bluetooth (HFP)"
+msgstr "Bluetooth (HFP)"
+
 #~ msgid "PipeWire Media System"
 #~ msgstr "PipeWire medijski sustav"
 
diff --git a/po/hu.po b/po/hu.po
index dcb2521679eab68102fdee5cde77befa6b23837e..7fb3b3d74944cb07fa3b6c878aa6eafd519272b9 100644
--- a/po/hu.po
+++ b/po/hu.po
@@ -1,18 +1,18 @@
-# Hungarian translation of PipeWire
-# Copyright (C) 2012, 2016. Free Software Foundation, Inc.
+# Hungarian translation for PipeWire.
+# Copyright (C) 2012, 2016, 2022. Free Software Foundation, Inc.
 # This file is distributed under the same license as the PipeWire package.
 #
-# KAMI <kami911@gmail.com>, 2012.
+# KAMI <kami911 at gmail dot com>, 2012.
 # Gabor Kelemen <kelemeng at ubuntu dot com>, 2016.
-# Balázs Úr <urbalazs at gmail dot com>, 2016.
+# Balázs Úr <ur.balazs at fsf dot hu>, 2016, 2022.
 msgid ""
 msgstr ""
 "Project-Id-Version: PipeWire master\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: 2020-07-21 15:29+0000\n"
-"Last-Translator: Balázs Meskó <meskobalazs@mailbox.org>\n"
+"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/-/"
+"issues\n"
+"POT-Creation-Date: 2022-09-15 15:26+0000\n"
+"PO-Revision-Date: 2022-09-21 22:35+0200\n"
+"Last-Translator: Balázs Úr <ur.balazs at fsf dot hu>\n"
 "Language-Team: Hungarian <https://translate.fedoraproject.org/projects/"
 "pipewire/pipewire/hu/>\n"
 "Language: hu\n"
@@ -20,12 +20,9 @@ msgstr ""
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
 "Plural-Forms: nplurals=2; plural=n != 1;\n"
-"X-Generator: Weblate 4.1.1\n"
-"X-Poedit-Language: Hungarian\n"
-"X-Poedit-Country: HUNGARY\n"
-"X-Poedit-SourceCharset: utf-8\n"
+"X-Generator: Lokalize 19.12.3\n"
 
-#: src/daemon/pipewire.c:43
+#: src/daemon/pipewire.c:46
 #, c-format
 msgid ""
 "%s [options]\n"
@@ -33,40 +30,64 @@ msgid ""
 "      --version                         Show version\n"
 "  -c, --config                          Load config (Default %s)\n"
 msgstr ""
+"%s [kapcsolók]\n"
+"  -h, --help                            Ezen súgó megjelenítése\n"
+"      --version                         Verzió megjelenítése\n"
+"  -c, --config                          Beállítás betöltése (alapérték: %s)\n"
 
 #: src/daemon/pipewire.desktop.in:4
 msgid "PipeWire Media System"
-msgstr ""
+msgstr "PipeWire médiarendszer"
 
 #: src/daemon/pipewire.desktop.in:5
 msgid "Start the PipeWire Media System"
-msgstr ""
+msgstr "A PipeWire médiarendszer indítása"
 
-#: src/examples/media-session/alsa-monitor.c:526
-#: spa/plugins/alsa/acp/compat.c:187
-msgid "Built-in Audio"
-msgstr "Belső hangforrás"
+#: 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 "Alagút ide: %s/%s"
 
-#: src/examples/media-session/alsa-monitor.c:530
-#: spa/plugins/alsa/acp/compat.c:192
-msgid "Modem"
-msgstr "Modem"
+#: src/modules/module-fallback-sink.c:51
+msgid "Dummy Output"
+msgstr "Üres kimenet"
+
+#: src/modules/module-pulse-tunnel.c:662
+#, c-format
+msgid "Tunnel for %s@%s"
+msgstr "Alagút ehhez: %s@%s"
 
-#: src/examples/media-session/alsa-monitor.c:539
+#: src/modules/module-zeroconf-discover.c:332
 msgid "Unknown device"
-msgstr ""
+msgstr "Ismeretlen eszköz"
+
+#: src/modules/module-zeroconf-discover.c:344
+#, c-format
+msgid "%s on %s@%s"
+msgstr "%s ezen: %s@%s"
 
-#: src/tools/pw-cat.c:991
+#: src/modules/module-zeroconf-discover.c:348
+#, c-format
+msgid "%s on %s"
+msgstr "%s ezen: %s"
+
+#: 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 [kapcsolók] [<fájl>|-]\n"
+"  -h, --help                            Ezen súgó megjelenítése\n"
+"      --version                         Verzió megjelenítése\n"
+"  -v, --verbose                         Részletes műveletek engedélyezése\n"
+"\n"
 
-#: src/tools/pw-cat.c:998
+#: src/tools/pw-cat.c:791
 #, c-format
 msgid ""
 "  -R, --remote                          Remote daemon name\n"
@@ -80,11 +101,30 @@ 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                          Távoli démon neve\n"
+"      --media-type                      Médiatípus beállítása (alapérték: "
+"%s)\n"
+"      --media-category                  Médiakategória beállítása\n"
+"                                          (alapérték: %s)\n"
+"      --media-role                      Médiaszerep beállítása (alapérték: "
+"%s)\n"
+"      --target                          Csomópont céljának beállítása\n"
+"                                          (alapérték: %s), a 0 azt jelenti,\n"
+"                                          hogy ne linkeljen\n"
+"      --latency                         Csomópont késleltetésének "
+"beállítása\n"
+"                                          (alapérték: %s)\n"
+"                                          Xegység (egység = s, ms, us, ns)\n"
+"                                          vagy közvetlen minták (256)\n"
+"                                          a gyakoriság a forrásfájl egyike\n"
+"  -P  --properties                      Csomópont tulajdonságainak "
+"beállítása\n"
+"\n"
 
-#: src/tools/pw-cat.c:1016
+#: src/tools/pw-cat.c:809
 #, c-format
 msgid ""
 "      --rate                            Sample rate (req. for rec) (default "
@@ -103,16 +143,39 @@ msgid ""
 "%d)\n"
 "\n"
 msgstr ""
+"      --rate                            Mintavételi gyakoriság (kötelező a\n"
+"                                          rögzítéshez) (alapérték: %u)\n"
+"      --channels                        Csatornák száma (kötelező a\n"
+"                                          rögzítéshez) (alapérték: %u)\n"
+"      --channel-map                     Csatornaleképezés\n"
+"                                          ezek egyike: „stereo”, "
+"„surround-51”\n"
+"                                          stb. vagy csatornanevek vesszővel\n"
+"                                          tagolt listája, például: „FL,FR”\n"
+"      --format                          Mintavételi formátum: %s (kötelező "
+"a\n"
+"                                          rögzítéshez) (alapérték: %s)\n"
+"      --volume                          Adatfolyam hangereje 0-1.0\n"
+"                                          (alapérték: %.3f)\n"
+"  -q  --quality                         Újramintavételezési minőség (0-15)\n"
+"                                          (alapérték: %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                        Lejátszási mód\n"
+"  -r, --record                          Rögzítési mód\n"
+"  -m, --midi                            Midi mód\n"
+"  -d, --dsd                             DSD mód\n"
+"\n"
 
-#: src/tools/pw-cli.c:2932
+#: src/tools/pw-cli.c:2255
 #, c-format
 msgid ""
 "%s [options] [command]\n"
@@ -122,360 +185,353 @@ msgid ""
 "  -r, --remote                          Remote daemon name\n"
 "\n"
 msgstr ""
+"%s [kapcsolók] [parancs]\n"
+"  -h, --help                            Ezen súgó megjelenítése\n"
+"      --version                         Verzió megjelenítése\n"
+"  -d, --daemon                          Indítás démonként (alapérték: "
+"hamis)\n"
+"  -r, --remote                          Távoli démon neve\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:444 spa/plugins/alsa/acp/alsa-mixer.c:4648
+#: spa/plugins/bluez5/bluez5-device.c:1236
 msgid "Off"
-msgstr "Kikapcsolva"
+msgstr "Ki"
 
-#: spa/plugins/alsa/acp/channelmap.h:466
-msgid "(invalid)"
-msgstr "(Érvénytelen)"
-
-#: spa/plugins/alsa/acp/alsa-mixer.c:2709
+#: spa/plugins/alsa/acp/alsa-mixer.c:2652
 msgid "Input"
 msgstr "Bemenet"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2710
+#: spa/plugins/alsa/acp/alsa-mixer.c:2653
 msgid "Docking Station Input"
 msgstr "Dokkolóállomás bemenet"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2711
+#: spa/plugins/alsa/acp/alsa-mixer.c:2654
 msgid "Docking Station Microphone"
 msgstr "Dokkolóállomás mikrofon"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2712
+#: spa/plugins/alsa/acp/alsa-mixer.c:2655
 msgid "Docking Station Line In"
 msgstr "Dokkolóállomás vonalbemenet"
 
-#: 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 "Vonalbemenet"
 
-#: 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:1454
 msgid "Microphone"
 msgstr "Mikrofon"
 
-#: 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 "Első mikrofon"
+msgstr "Elülső mikrofon"
 
-#: 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 "Hátsó mikrofon"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2717
+#: spa/plugins/alsa/acp/alsa-mixer.c:2660
 msgid "External Microphone"
 msgstr "Külső mikrofon"
 
-#: 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 "Belső mikrofon"
 
-#: 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 "Rádió"
 
-#: 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 "Videó"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2721
+#: spa/plugins/alsa/acp/alsa-mixer.c:2664
 msgid "Automatic Gain Control"
 msgstr "Automatikus erősítésszabályzás"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2722
+#: spa/plugins/alsa/acp/alsa-mixer.c:2665
 msgid "No Automatic Gain Control"
 msgstr "Nincs automatikus erősítésszabályzás"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2723
+#: spa/plugins/alsa/acp/alsa-mixer.c:2666
 msgid "Boost"
 msgstr "Erősítés"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2724
+#: spa/plugins/alsa/acp/alsa-mixer.c:2667
 msgid "No Boost"
 msgstr "Nincs erősítés"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2725
+#: spa/plugins/alsa/acp/alsa-mixer.c:2668
 msgid "Amplifier"
 msgstr "Erősítő"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2726
+#: spa/plugins/alsa/acp/alsa-mixer.c:2669
 msgid "No Amplifier"
 msgstr "Nincs erősítő"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2727
+#: spa/plugins/alsa/acp/alsa-mixer.c:2670
 msgid "Bass Boost"
 msgstr "Basszuskiemelés"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2728
+#: spa/plugins/alsa/acp/alsa-mixer.c:2671
 msgid "No Bass Boost"
 msgstr "Nincs basszuskiemelés"
 
-#: 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:1460
 msgid "Speaker"
 msgstr "Hangszóró"
 
-#: 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 "Analóg fejhallgató"
+msgstr "Fejhallgató"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2797
+#: spa/plugins/alsa/acp/alsa-mixer.c:2740
 msgid "Analog Input"
 msgstr "Analóg bemenet"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2801
+#: spa/plugins/alsa/acp/alsa-mixer.c:2744
 msgid "Dock Microphone"
 msgstr "Dokkolóállomás mikrofon"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2803
+#: spa/plugins/alsa/acp/alsa-mixer.c:2746
 msgid "Headset Microphone"
 msgstr "Fejhallgató mikrofon"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2807
+#: spa/plugins/alsa/acp/alsa-mixer.c:2750
 msgid "Analog Output"
 msgstr "Analóg kimenet"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2809
-#, fuzzy
+#: spa/plugins/alsa/acp/alsa-mixer.c:2752
 msgid "Headphones 2"
-msgstr "Analóg fejhallgató"
+msgstr "2. fejhallgató"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2810
-#, fuzzy
+#: spa/plugins/alsa/acp/alsa-mixer.c:2753
 msgid "Headphones Mono Output"
-msgstr "Analóg mono kimenet"
+msgstr "Fejhallató monó kimenet"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2811
+#: spa/plugins/alsa/acp/alsa-mixer.c:2754
 msgid "Line Out"
 msgstr "Vonalkimenet"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2812
+#: spa/plugins/alsa/acp/alsa-mixer.c:2755
 msgid "Analog Mono Output"
-msgstr "Analóg mono kimenet"
+msgstr "Analóg monó kimenet"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2813
+#: spa/plugins/alsa/acp/alsa-mixer.c:2756
 msgid "Speakers"
 msgstr "Hangszórók"
 
-#: 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 "Digitális kimenet (S/PDIF)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2816
+#: spa/plugins/alsa/acp/alsa-mixer.c:2759
 msgid "Digital Input (S/PDIF)"
 msgstr "Digitális bemenet (S/PDIF)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2817
+#: spa/plugins/alsa/acp/alsa-mixer.c:2760
 msgid "Multichannel Input"
 msgstr "Többcsatornás bemenet"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2818
+#: spa/plugins/alsa/acp/alsa-mixer.c:2761
 msgid "Multichannel Output"
 msgstr "Többcsatornás kimenet"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2819
-#, fuzzy
+#: spa/plugins/alsa/acp/alsa-mixer.c:2762
 msgid "Game Output"
-msgstr "%s kimenet"
+msgstr "Játék kimenet"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2820
-#: spa/plugins/alsa/acp/alsa-mixer.c:2821
-#, fuzzy
+#: spa/plugins/alsa/acp/alsa-mixer.c:2763
+#: spa/plugins/alsa/acp/alsa-mixer.c:2764
 msgid "Chat Output"
-msgstr "%s kimenet"
+msgstr "Csevegés kimenet"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2822
-#, fuzzy
+#: spa/plugins/alsa/acp/alsa-mixer.c:2765
 msgid "Chat Input"
-msgstr "%s bemenet"
+msgstr "Csevegés bemenet"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2823
-#, fuzzy
+#: spa/plugins/alsa/acp/alsa-mixer.c:2766
 msgid "Virtual Surround 7.1"
-msgstr "Virtuális térhatású nyelő"
+msgstr "Virtuális térhatás 7.1"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4527
+#: spa/plugins/alsa/acp/alsa-mixer.c:4471
 msgid "Analog Mono"
-msgstr "Analóg mono"
+msgstr "Analóg monó"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4528
-#, fuzzy
+#: spa/plugins/alsa/acp/alsa-mixer.c:4472
 msgid "Analog Mono (Left)"
-msgstr "Analóg mono"
+msgstr "Analóg monó (bal)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4529
-#, fuzzy
+#: spa/plugins/alsa/acp/alsa-mixer.c:4473
 msgid "Analog Mono (Right)"
-msgstr "Analóg mono"
+msgstr "Analóg monó (jobb)"
 
 #. 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 "Analóg sztereó"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4531
+#: spa/plugins/alsa/acp/alsa-mixer.c:4475
 msgid "Mono"
-msgstr "Mono"
+msgstr "Monó"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4532
+#: spa/plugins/alsa/acp/alsa-mixer.c:4476
 msgid "Stereo"
 msgstr "Sztereó"
 
-#: 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:1442
 msgid "Headset"
 msgstr "Fejhallgató"
 
-#: 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 "Hangszóró"
+msgstr "Mikrofonos fejhallgató"
 
-#: 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 "Többcsatornás"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4544
+#: spa/plugins/alsa/acp/alsa-mixer.c:4488
 msgid "Analog Surround 2.1"
 msgstr "Analóg térhatású 2.1"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4545
+#: spa/plugins/alsa/acp/alsa-mixer.c:4489
 msgid "Analog Surround 3.0"
 msgstr "Analóg térhatású 3.0"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4546
+#: spa/plugins/alsa/acp/alsa-mixer.c:4490
 msgid "Analog Surround 3.1"
 msgstr "Analóg térhatású 3.1"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4547
+#: spa/plugins/alsa/acp/alsa-mixer.c:4491
 msgid "Analog Surround 4.0"
 msgstr "Analóg térhatású 4.0"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4548
+#: spa/plugins/alsa/acp/alsa-mixer.c:4492
 msgid "Analog Surround 4.1"
 msgstr "Analóg térhatású 4.1"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4549
+#: spa/plugins/alsa/acp/alsa-mixer.c:4493
 msgid "Analog Surround 5.0"
 msgstr "Analóg térhatású 5.0"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4550
+#: spa/plugins/alsa/acp/alsa-mixer.c:4494
 msgid "Analog Surround 5.1"
 msgstr "Analóg térhatású 5.1"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4551
+#: spa/plugins/alsa/acp/alsa-mixer.c:4495
 msgid "Analog Surround 6.0"
 msgstr "Analóg térhatású 6.0"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4552
+#: spa/plugins/alsa/acp/alsa-mixer.c:4496
 msgid "Analog Surround 6.1"
 msgstr "Analóg térhatású 6.1"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4553
+#: spa/plugins/alsa/acp/alsa-mixer.c:4497
 msgid "Analog Surround 7.0"
 msgstr "Analóg térhatású 7.0"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4554
+#: spa/plugins/alsa/acp/alsa-mixer.c:4498
 msgid "Analog Surround 7.1"
 msgstr "Analóg térhatású 7.1"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4555
+#: spa/plugins/alsa/acp/alsa-mixer.c:4499
 msgid "Digital Stereo (IEC958)"
 msgstr "Digitális sztereó (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 "Digitális térhatású 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 "Digitális térhatású 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 "Digitális térhatású 5.1 (IEC958/DTS)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4559
+#: spa/plugins/alsa/acp/alsa-mixer.c:4503
 msgid "Digital Stereo (HDMI)"
-msgstr "Digitális térhatású (HDMI)"
+msgstr "Digitális sztereó (HDMI)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4560
+#: spa/plugins/alsa/acp/alsa-mixer.c:4504
 msgid "Digital Surround 5.1 (HDMI)"
 msgstr "Digitális térhatású 5.1 (HDMI)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4561
+#: spa/plugins/alsa/acp/alsa-mixer.c:4505
 msgid "Chat"
-msgstr ""
+msgstr "Csevegés"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4562
+#: spa/plugins/alsa/acp/alsa-mixer.c:4506
 msgid "Game"
-msgstr ""
+msgstr "Játék"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4696
+#: spa/plugins/alsa/acp/alsa-mixer.c:4640
 msgid "Analog Mono Duplex"
-msgstr "Analóg mono duplex"
+msgstr "Analóg monó kétirányú"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4697
+#: spa/plugins/alsa/acp/alsa-mixer.c:4641
 msgid "Analog Stereo Duplex"
-msgstr "Analóg sztereó duplex"
+msgstr "Analóg sztereó kétirányú"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4700
+#: spa/plugins/alsa/acp/alsa-mixer.c:4644
 msgid "Digital Stereo Duplex (IEC958)"
-msgstr "Analóg sztereó duplex (IEC958)"
+msgstr "Digitális sztereó kétirányú (IEC958)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4701
+#: spa/plugins/alsa/acp/alsa-mixer.c:4645
 msgid "Multichannel Duplex"
-msgstr "Többcsatornás duplex"
+msgstr "Többcsatornás kétirányú"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4702
-#, fuzzy
+#: spa/plugins/alsa/acp/alsa-mixer.c:4646
 msgid "Stereo Duplex"
-msgstr "Analóg sztereó duplex"
+msgstr "Sztereó kétirányú"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4703
+#: spa/plugins/alsa/acp/alsa-mixer.c:4647
 msgid "Mono Chat + 7.1 Surround"
-msgstr ""
+msgstr "Monó csevegés + 7.1 térhatású"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4806
+#: spa/plugins/alsa/acp/alsa-mixer.c:4754
 #, c-format
 msgid "%s Output"
 msgstr "%s kimenet"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4813
+#: spa/plugins/alsa/acp/alsa-mixer.c:4761
 #, c-format
 msgid "%s Input"
 msgstr "%s bemenet"
 
-#: spa/plugins/alsa/acp/alsa-util.c:1175 spa/plugins/alsa/acp/alsa-util.c:1269
-#, fuzzy, c-format
+#: 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 "
 "ms).\n"
@@ -487,18 +543,18 @@ msgid_plural ""
 "Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
 "to the ALSA developers."
 msgstr[0] ""
-"A „snd_pcm_avail()” függvény visszatérési értéke váratlanul nagy értékű: %lu "
+"Az „snd_pcm_avail()” függvény különlegesen nagy értéket adott vissza: %lu "
 "bájt (%lu ms).\n"
 "Ez valószínűleg egy hiba eredménye az ALSA „%s” illesztőprogramban. Jelentse "
 "ezt a problémát az ALSA fejlesztői felé."
 msgstr[1] ""
-"A „snd_pcm_avail()” függvény visszatérési értéke váratlanul nagy értékű: %lu "
+"Az „snd_pcm_avail()” függvény különlegesen nagy értéket adott vissza: %lu "
 "bájt (%lu ms).\n"
 "Ez valószínűleg egy hiba eredménye az ALSA „%s” illesztőprogramban. Jelentse "
 "ezt a problémát az ALSA fejlesztői felé."
 
-#: spa/plugins/alsa/acp/alsa-util.c:1241
-#, fuzzy, c-format
+#: spa/plugins/alsa/acp/alsa-util.c:1253
+#, c-format
 msgid ""
 "snd_pcm_delay() returned a value that is exceptionally large: %li byte (%s"
 "%lu ms).\n"
@@ -510,17 +566,17 @@ msgid_plural ""
 "Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
 "to the ALSA developers."
 msgstr[0] ""
-"A „snd_pcm_delay()” függvény visszatérési értéke váratlanul nagy értékű: %li "
+"Az „snd_pcm_delay()” függvény különlegesen nagy értéket adott vissza: %li "
 "bájt (%s%lu ms).\n"
 "Ez valószínűleg egy hiba eredménye az ALSA „%s” illesztőprogramban. Jelentse "
 "ezt a problémát az ALSA fejlesztői felé."
 msgstr[1] ""
-"A „snd_pcm_delay()” függvény visszatérési értéke váratlanul nagy értékű: %li "
+"Az „snd_pcm_delay()” függvény különlegesen nagy értéket adott vissza: %li "
 "bájt (%s%lu ms).\n"
 "Ez valószínűleg egy hiba eredménye az ALSA „%s” illesztőprogramban. Jelentse "
 "ezt a problémát az ALSA fejlesztői felé."
 
-#: spa/plugins/alsa/acp/alsa-util.c:1288
+#: spa/plugins/alsa/acp/alsa-util.c:1300
 #, c-format
 msgid ""
 "snd_pcm_avail_delay() returned strange values: delay %lu is less than avail "
@@ -528,13 +584,13 @@ msgid ""
 "Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
 "to the ALSA developers."
 msgstr ""
-"A „snd_pcm_avail_delay()” függvény furcsa értékeket adott vissza: a "
+"Az „snd_pcm_avail_delay()” függvény furcsa értékeket adott vissza: a "
 "késleltetés (%lu) kisebb, mint az elérhető %lu.\n"
 "Ez valószínűleg egy hiba eredménye az ALSA „%s” illesztőprogramban. Jelentse "
 "ezt a problémát az ALSA fejlesztői felé."
 
-#: spa/plugins/alsa/acp/alsa-util.c:1331
-#, fuzzy, c-format
+#: spa/plugins/alsa/acp/alsa-util.c:1343
+#, c-format
 msgid ""
 "snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu byte "
 "(%lu ms).\n"
@@ -546,73 +602,114 @@ msgid_plural ""
 "Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
 "to the ALSA developers."
 msgstr[0] ""
-"A „snd_pcm_mmap_begin()” függvény visszatérési értéke kivételesen nagy: %lu "
-"bájt (%lu ms).\n"
+"Az „snd_pcm_mmap_begin()” függvény különlegesen nagy értéket adott vissza: "
+"%lu bájt (%lu ms).\n"
 "Ez valószínűleg egy hiba eredménye az ALSA „%s” illesztőprogramban. Jelentse "
 "ezt a problémát az ALSA fejlesztői felé."
 msgstr[1] ""
-"A „snd_pcm_mmap_begin()” függvény visszatérési értéke kivételesen nagy: %lu "
-"bájt (%lu ms).\n"
+"Az „snd_pcm_mmap_begin()” függvény különlegesen nagy értéket adott vissza: "
+"%lu bájt (%lu ms).\n"
 "Ez valószínűleg egy hiba eredménye az ALSA „%s” illesztőprogramban. Jelentse "
 "ezt a problémát az ALSA fejlesztői felé."
 
-#: spa/plugins/bluez5/bluez5-device.c:1010
+#: spa/plugins/alsa/acp/channelmap.h:457
+msgid "(invalid)"
+msgstr "(érvénytelen)"
+
+#: spa/plugins/alsa/acp/compat.c:189
+msgid "Built-in Audio"
+msgstr "Beépített hangforrás"
+
+#: spa/plugins/alsa/acp/compat.c:194
+msgid "Modem"
+msgstr "Modem"
+
+#: spa/plugins/bluez5/bluez5-device.c:1247
 msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
-msgstr ""
+msgstr "Hang átjáró (A2DP forrás és HSP/HFP AG)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1033
+#: spa/plugins/bluez5/bluez5-device.c:1272
 #, c-format
 msgid "High Fidelity Playback (A2DP Sink, codec %s)"
-msgstr ""
+msgstr "Magas hűségű lejátszás (A2DP fogadó, %s kodek)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1035
+#: spa/plugins/bluez5/bluez5-device.c:1275
 #, c-format
 msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
-msgstr ""
+msgstr "Magas hűségű kétirányú (A2DP forrás/fogadó, %s kodek)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1041
+#: spa/plugins/bluez5/bluez5-device.c:1283
 msgid "High Fidelity Playback (A2DP Sink)"
-msgstr ""
+msgstr "Magas hűségű lejátszás (A2DP fogadó)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1043
+#: spa/plugins/bluez5/bluez5-device.c:1285
 msgid "High Fidelity Duplex (A2DP Source/Sink)"
-msgstr ""
+msgstr "Magas hűségű kétirányú (A2DP forrás/fogadó)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1322
+#, c-format
+msgid "High Fidelity Playback (BAP Sink, codec %s)"
+msgstr "Magas hűségű lejátszás (BAP fogadó, %s kodek)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1326
+#, c-format
+msgid "High Fidelity Input (BAP Source, codec %s)"
+msgstr "Magas hűségű bemenet (BAP forrás, %s kodek)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1330
+#, c-format
+msgid "High Fidelity Duplex (BAP Source/Sink, codec %s)"
+msgstr "Magas hűségű kétirányú (BAP forrás/fogadó, %s kodek)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1070
+#: spa/plugins/bluez5/bluez5-device.c:1359
 #, c-format
 msgid "Headset Head Unit (HSP/HFP, codec %s)"
-msgstr ""
+msgstr "Fejhallgató fejegység (HSP/HFP, %s kodek)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1074
+#: spa/plugins/bluez5/bluez5-device.c:1364
 msgid "Headset Head Unit (HSP/HFP)"
-msgstr ""
-
-# FIXME: utánanézni
-#: spa/plugins/bluez5/bluez5-device.c:1140
+msgstr "Fejhallgató fejegység (HSP/HFP)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1443
+#: spa/plugins/bluez5/bluez5-device.c:1448
+#: spa/plugins/bluez5/bluez5-device.c:1455
+#: spa/plugins/bluez5/bluez5-device.c:1461
+#: spa/plugins/bluez5/bluez5-device.c:1467
+#: spa/plugins/bluez5/bluez5-device.c:1473
+#: spa/plugins/bluez5/bluez5-device.c:1479
+#: spa/plugins/bluez5/bluez5-device.c:1485
+#: spa/plugins/bluez5/bluez5-device.c:1491
 msgid "Handsfree"
 msgstr "Kihangosító"
 
-#: spa/plugins/bluez5/bluez5-device.c:1155
+#: spa/plugins/bluez5/bluez5-device.c:1449
+msgid "Handsfree (HFP)"
+msgstr "Kihangosító (HFP)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1466
 msgid "Headphone"
-msgstr "Fülhallgató"
+msgstr "Fejhallgató"
 
-#: spa/plugins/bluez5/bluez5-device.c:1160
+#: spa/plugins/bluez5/bluez5-device.c:1472
 msgid "Portable"
 msgstr "Hordozható"
 
-#: spa/plugins/bluez5/bluez5-device.c:1165
+#: spa/plugins/bluez5/bluez5-device.c:1478
 msgid "Car"
 msgstr "Autó"
 
-#: spa/plugins/bluez5/bluez5-device.c:1170
+#: spa/plugins/bluez5/bluez5-device.c:1484
 msgid "HiFi"
 msgstr "Hi-Fi"
 
-#: spa/plugins/bluez5/bluez5-device.c:1175
+#: spa/plugins/bluez5/bluez5-device.c:1490
 msgid "Phone"
 msgstr "Telefon"
 
-#: spa/plugins/bluez5/bluez5-device.c:1181
-#, fuzzy
+#: spa/plugins/bluez5/bluez5-device.c:1497
 msgid "Bluetooth"
-msgstr "Bluetooth bemenet"
+msgstr "Bluetooth"
+
+#: spa/plugins/bluez5/bluez5-device.c:1498
+msgid "Bluetooth (HFP)"
+msgstr "Bluetooth (HFP)"
diff --git a/po/it.po b/po/it.po
index f4a1c112a505d7d0073bb78826c3a2af0cb9ec29..bdd47ba8d7d530e92d9248f3f477272e200f882d 100644
--- a/po/it.po
+++ b/po/it.po
@@ -34,11 +34,11 @@ msgstr ""
 
 #: src/daemon/pipewire.desktop.in:4
 msgid "PipeWire Media System"
-msgstr "Sistema Multimediale PipeWire"
+msgstr "Sistema multimediale PipeWire"
 
 #: src/daemon/pipewire.desktop.in:5
 msgid "Start the PipeWire Media System"
-msgstr "Avvia il Sistema Multimediale PipeWire"
+msgstr "Avvia il sistema multimediale PipeWire"
 
 #: src/examples/media-session/alsa-monitor.c:526
 #: spa/plugins/alsa/acp/compat.c:187
@@ -144,16 +144,16 @@ msgstr "Ingresso docking station"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:2711
 msgid "Docking Station Microphone"
-msgstr "Microfono docking station"
+msgstr "Microfono della docking station"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:2712
 msgid "Docking Station Line In"
-msgstr "Linea in docking station"
+msgstr "Linea di ingresso nella docking station"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:2713
 #: spa/plugins/alsa/acp/alsa-mixer.c:2804
 msgid "Line In"
-msgstr "Line-In"
+msgstr "Linea di ingresso"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:2714
 #: spa/plugins/alsa/acp/alsa-mixer.c:2798
@@ -258,7 +258,7 @@ msgstr "Uscita mono cuffie"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:2811
 msgid "Line Out"
-msgstr "Line-Out"
+msgstr "Linea di uscita"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:2812
 msgid "Analog Mono Output"
@@ -350,7 +350,7 @@ msgstr "Vivavoce"
 #: spa/plugins/alsa/acp/alsa-mixer.c:4542
 #: spa/plugins/alsa/acp/alsa-mixer.c:4543
 msgid "Multichannel"
-msgstr "Multi canale"
+msgstr "Multicanale"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:4544
 msgid "Analog Surround 2.1"
@@ -442,7 +442,7 @@ msgstr "Duplex stereo digitale (IEC958)"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:4701
 msgid "Multichannel Duplex"
-msgstr "Duplex multi canale"
+msgstr "Duplex multicanale"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:4702
 msgid "Stereo Duplex"
@@ -477,11 +477,11 @@ msgid_plural ""
 msgstr[0] ""
 "snd_pcm_avail() ha restituito un valore molto grande: %lu byte (%lu ms).\n"
 "Molto probabilmente si tratta di un bug nel driver ALSA «%s». Segnalare "
-"questo problema agli sviluppatori ALSA."
+"questo problema ai suoi sviluppatori."
 msgstr[1] ""
 "snd_pcm_avail() ha restituito un valore molto grande: %lu byte (%lu ms).\n"
 "Molto probabilmente si tratta di un bug nel driver ALSA «%s». Segnalare "
-"questo problema agli sviluppatori ALSA."
+"questo problema ai suoi sviluppatori."
 
 #: spa/plugins/alsa/acp/alsa-util.c:1241
 #, c-format
@@ -498,11 +498,11 @@ msgid_plural ""
 msgstr[0] ""
 "snd_pcm_delay() ha restituito un valore molto grande: %li byte (%s%lu ms).\n"
 "Molto probabilmente si tratta di un bug nel driver ALSA «%s». Segnalare "
-"questo problema agli sviluppatori ALSA."
+"questo problema ai suoi sviluppatori."
 msgstr[1] ""
 "snd_pcm_delay() ha restituito un valore molto grande: %li byte (%s%lu ms).\n"
 "Molto probabilmente si tratta di un bug nel driver ALSA «%s». Segnalare "
-"questo problema agli sviluppatori ALSA."
+"questo problema ai suoi sviluppatori."
 
 #: spa/plugins/alsa/acp/alsa-util.c:1288
 #, c-format
@@ -515,7 +515,7 @@ msgstr ""
 "snd_pcm_avail() ha restituito dei valori strani: delay %lu è minore di avail "
 "%lu.\n"
 "Molto probabilmente si tratta di un bug nel driver ALSA «%s». Segnalare "
-"questo problema agli sviluppatori ALSA."
+"questo problema ai suoi sviluppatori."
 
 #: spa/plugins/alsa/acp/alsa-util.c:1331
 #, c-format
@@ -533,12 +533,12 @@ msgstr[0] ""
 "snd_pcm_mmap_begin() ha restituito un valore molto grande: %lu byte (%lu "
 "ms).\n"
 "Molto probabilmente si tratta di un bug nel driver ALSA «%s». Segnalare "
-"questo problema agli sviluppatori ALSA."
+"questo problema ai suoi sviluppatori."
 msgstr[1] ""
 "snd_pcm_mmap_begin() ha restituito un valore molto grande: %lu byte (%lu "
 "ms).\n"
 "Molto probabilmente si tratta di un bug nel driver ALSA «%s». Segnalare "
-"questo problema agli sviluppatori ALSA."
+"questo problema ai suoi sviluppatori."
 
 #: spa/plugins/bluez5/bluez5-device.c:1010
 msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
@@ -547,20 +547,20 @@ msgstr "Gateway Audio (Sorgente A2DP & HSP/HFP AG)"
 #: spa/plugins/bluez5/bluez5-device.c:1033
 #, c-format
 msgid "High Fidelity Playback (A2DP Sink, codec %s)"
-msgstr "Riproduzione ad Alta Fedeltà (A2DP Sink, codec %s)"
+msgstr "Riproduzione ad alta fedeltà (A2DP Sink, codec %s)"
 
 #: spa/plugins/bluez5/bluez5-device.c:1035
 #, c-format
 msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
-msgstr "Duplex ad Alta Fedeltà (Sorgente/Sink, A2DP codec %s)"
+msgstr "Duplex ad alta fedeltà (Sorgente/Sink, A2DP codec %s)"
 
 #: spa/plugins/bluez5/bluez5-device.c:1041
 msgid "High Fidelity Playback (A2DP Sink)"
-msgstr "Riproduzione ad Alta Fedeltà (A2DP Sink)"
+msgstr "Riproduzione ad alta fedeltà (A2DP Sink)"
 
 #: spa/plugins/bluez5/bluez5-device.c:1043
 msgid "High Fidelity Duplex (A2DP Source/Sink)"
-msgstr "Duplex ad Alta Fedeltà (A2DP Source/Sink)"
+msgstr "Duplex ad alta fedeltà (A2DP Source/Sink)"
 
 #: spa/plugins/bluez5/bluez5-device.c:1070
 #, c-format
@@ -573,7 +573,7 @@ msgstr ""
 
 #: spa/plugins/bluez5/bluez5-device.c:1140
 msgid "Handsfree"
-msgstr "Sistema mani-libere"
+msgstr "Vivavoce"
 
 #: spa/plugins/bluez5/bluez5-device.c:1155
 msgid "Headphone"
diff --git a/po/ka.po b/po/ka.po
index 1e78ffdfea21d246f9cd1d99e0391df811962d14..47f80678e3a6d34f79d8b645992be2ae01c37ddc 100644
--- a/po/ka.po
+++ b/po/ka.po
@@ -1,7 +1,7 @@
-# SOME DESCRIPTIVE TITLE.
-# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# Georgian translation for pipewire.
+# Copyright (C) 2022 pipewire'S authors
 # This file is distributed under the same license as the pipewire package.
-# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+# Temuri Doghonadze <temuri.doghonadze@gmail.com>, 2022.
 #
 msgid ""
 msgstr ""
@@ -9,15 +9,15 @@ msgstr ""
 "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"
+"PO-Revision-Date: 2022-11-20 11:50+0100\n"
 "Last-Translator: Temuri Doghonadze <temuri.doghonadze@gmail.com>\n"
-"Language-Team: Georgian <(nothing)>\n"
+"Language-Team: \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"
+"X-Generator: Poedit 3.2\n"
 
 #: src/daemon/pipewire.c:46
 #, c-format
@@ -42,7 +42,7 @@ msgstr "გვირაბი %s/%s -მდე"
 msgid "Dummy Output"
 msgstr "ნულოვანი გამოყვანა"
 
-#: src/modules/module-pulse-tunnel.c:648
+#: src/modules/module-pulse-tunnel.c:662
 #, c-format
 msgid "Tunnel for %s@%s"
 msgstr "გვირაბი %s@%s-სთვის"
@@ -153,7 +153,7 @@ msgstr ""
 "   -d, --dsd                             DSD რეჟიმი\n"
 "\n"
 
-#: src/tools/pw-cli.c:3165
+#: src/tools/pw-cli.c:2250
 #, c-format
 msgid ""
 "%s [options] [command]\n"
@@ -174,8 +174,8 @@ msgstr ""
 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
+#: spa/plugins/alsa/acp/acp.c:444 spa/plugins/alsa/acp/alsa-mixer.c:4648
+#: spa/plugins/bluez5/bluez5-device.c:1236
 msgid "Off"
 msgstr "გამორთული"
 
@@ -202,7 +202,7 @@ 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
+#: spa/plugins/bluez5/bluez5-device.c:1454
 msgid "Microphone"
 msgstr "მიკროფონი"
 
@@ -268,7 +268,7 @@ msgid "No Bass Boost"
 msgstr "Bass-ის გაძლიერების გარეშე"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:2672
-#: spa/plugins/bluez5/bluez5-device.c:1335
+#: spa/plugins/bluez5/bluez5-device.c:1460
 msgid "Speaker"
 msgstr "დინამიკი"
 
@@ -383,7 +383,7 @@ msgstr "სტერეო"
 
 #: 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:1442
 msgid "Headset"
 msgstr "ყურსაცვამები & მიკროფონი"
 
@@ -507,7 +507,7 @@ msgstr "%s გამოყვანა"
 msgid "%s Input"
 msgstr "%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 "
@@ -530,7 +530,7 @@ msgstr[1] ""
 "ყველაზე ხშირად ეს ALSA-ს დრაივერის (%s) შეცდომის გამო ხდება. დაუკავშირდით "
 "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"
@@ -553,7 +553,7 @@ msgstr[1] ""
 "ყველაზე ხშირად ეს ALSA-ს დრაივერის (%s) შეცდომის გამო ხდება. დაუკავშირდით "
 "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 "
@@ -566,7 +566,7 @@ msgstr ""
 "ყველაზე ხშირად ეს ALSA-ს დრაივერის (%s) შეცდომის გამო ხდება. დაუკავშირდით "
 "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 "
@@ -601,61 +601,92 @@ msgstr "ჩაშენებული აუდიო"
 msgid "Modem"
 msgstr "მოდემი"
 
-#: spa/plugins/bluez5/bluez5-device.c:1172
+#: spa/plugins/bluez5/bluez5-device.c:1247
 msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
 msgstr "Audio Gateway (A2DP წყარო & HSP/HFP AG)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1197
+#: spa/plugins/bluez5/bluez5-device.c:1272
 #, c-format
 msgid "High Fidelity Playback (A2DP Sink, codec %s)"
 msgstr "მაღალი ხარისხის ხმა (A2DP Sink, კოდეკი %s)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1200
+#: spa/plugins/bluez5/bluez5-device.c:1275
 #, c-format
 msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
 msgstr "მაღალი ხარისხის დუპლექსი (A2DP წყარო/Sink, კოდეკი %s)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1208
+#: spa/plugins/bluez5/bluez5-device.c:1283
 msgid "High Fidelity Playback (A2DP Sink)"
 msgstr "მაღალი ხარისხის ხმა (A2DP Sink)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1210
+#: spa/plugins/bluez5/bluez5-device.c:1285
 msgid "High Fidelity Duplex (A2DP Source/Sink)"
 msgstr "მაღალი ხარისხის დუპლექსი(A2DP წყარო/Sink)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1238
+#: spa/plugins/bluez5/bluez5-device.c:1322
+#, c-format
+msgid "High Fidelity Playback (BAP Sink, codec %s)"
+msgstr "მაღალი ხარისხის დაკვრა (BAP Sink, კოდეკი %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1326
+#, c-format
+msgid "High Fidelity Input (BAP Source, codec %s)"
+msgstr "მაღალი ხარისხის შეყვანა (BAP წყარო, კოდეკი %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1330
+#, c-format
+msgid "High Fidelity Duplex (BAP Source/Sink, codec %s)"
+msgstr "მაღალი ხარისხის დუპლექსი (BAP წყარო/Sink, კოდეკი %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1359
 #, c-format
 msgid "Headset Head Unit (HSP/HFP, codec %s)"
 msgstr "Headset Head Unit (HSP/HFP, კოდეკი %s)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1243
+#: spa/plugins/bluez5/bluez5-device.c:1364
 msgid "Headset Head Unit (HSP/HFP)"
 msgstr "Headset Head Unit (HSP/HFP)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1325
+#: spa/plugins/bluez5/bluez5-device.c:1443
+#: spa/plugins/bluez5/bluez5-device.c:1448
+#: spa/plugins/bluez5/bluez5-device.c:1455
+#: spa/plugins/bluez5/bluez5-device.c:1461
+#: spa/plugins/bluez5/bluez5-device.c:1467
+#: spa/plugins/bluez5/bluez5-device.c:1473
+#: spa/plugins/bluez5/bluez5-device.c:1479
+#: spa/plugins/bluez5/bluez5-device.c:1485
+#: spa/plugins/bluez5/bluez5-device.c:1491
 msgid "Handsfree"
 msgstr "ხელის გარეშე სამართავი"
 
-#: spa/plugins/bluez5/bluez5-device.c:1340
+#: spa/plugins/bluez5/bluez5-device.c:1449
+msgid "Handsfree (HFP)"
+msgstr "ხელის გარეშე სამართავი (HFP)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1466
 msgid "Headphone"
 msgstr "ყურსაცვამი"
 
-#: spa/plugins/bluez5/bluez5-device.c:1345
+#: spa/plugins/bluez5/bluez5-device.c:1472
 msgid "Portable"
 msgstr "გადატანადი"
 
-#: spa/plugins/bluez5/bluez5-device.c:1350
+#: spa/plugins/bluez5/bluez5-device.c:1478
 msgid "Car"
 msgstr "მანქანა"
 
-#: spa/plugins/bluez5/bluez5-device.c:1355
+#: spa/plugins/bluez5/bluez5-device.c:1484
 msgid "HiFi"
 msgstr "HiFi"
 
-#: spa/plugins/bluez5/bluez5-device.c:1360
+#: spa/plugins/bluez5/bluez5-device.c:1490
 msgid "Phone"
 msgstr "ტელეფონი"
 
-#: spa/plugins/bluez5/bluez5-device.c:1366
+#: spa/plugins/bluez5/bluez5-device.c:1497
 msgid "Bluetooth"
 msgstr "Bluetooth"
+
+#: spa/plugins/bluez5/bluez5-device.c:1498
+msgid "Bluetooth (HFP)"
+msgstr "Bluetooth (HFP)"
diff --git a/po/pipewire.pot b/po/pipewire.pot
index 26d49f3d46b9729260ed2b821e0c36f70f0d5037..af26ba80b02380f3a18658a41989c57ae33004ad 100644
--- a/po/pipewire.pot
+++ b/po/pipewire.pot
@@ -37,7 +37,7 @@ msgstr ""
 msgid "Dummy Output"
 msgstr ""
 
-#: src/modules/module-pulse-tunnel.c:648
+#: src/modules/module-pulse-tunnel.c:662
 #, c-format
 msgid "Tunnel for %s@%s"
 msgstr ""
@@ -113,7 +113,7 @@ msgid ""
 "\n"
 msgstr ""
 
-#: src/tools/pw-cli.c:3165
+#: src/tools/pw-cli.c:2250
 #, c-format
 msgid ""
 "%s [options] [command]\n"
@@ -128,8 +128,8 @@ msgstr ""
 msgid "Pro Audio"
 msgstr ""
 
-#: spa/plugins/alsa/acp/acp.c:446 spa/plugins/alsa/acp/alsa-mixer.c:4648
-#: spa/plugins/bluez5/bluez5-device.c:1161
+#: spa/plugins/alsa/acp/acp.c:444 spa/plugins/alsa/acp/alsa-mixer.c:4648
+#: spa/plugins/bluez5/bluez5-device.c:1236
 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:1330
+#: spa/plugins/bluez5/bluez5-device.c:1454
 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:1335
+#: spa/plugins/bluez5/bluez5-device.c:1460
 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:1320
+#: spa/plugins/bluez5/bluez5-device.c:1442
 msgid "Headset"
 msgstr ""
 
@@ -461,8 +461,8 @@ msgstr ""
 msgid "%s Input"
 msgstr ""
 
-#: 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 "
@@ -477,22 +477,22 @@ msgid_plural ""
 msgstr[0] ""
 msgstr[1] ""
 
-#: 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"
-"%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] ""
 msgstr[1] ""
 
-#: 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 "
@@ -501,7 +501,7 @@ msgid ""
 "to the ALSA developers."
 msgstr ""
 
-#: 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 "
@@ -528,61 +528,92 @@ msgstr ""
 msgid "Modem"
 msgstr ""
 
-#: spa/plugins/bluez5/bluez5-device.c:1172
+#: spa/plugins/bluez5/bluez5-device.c:1247
 msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
 msgstr ""
 
-#: spa/plugins/bluez5/bluez5-device.c:1197
+#: spa/plugins/bluez5/bluez5-device.c:1272
 #, c-format
 msgid "High Fidelity Playback (A2DP Sink, codec %s)"
 msgstr ""
 
-#: spa/plugins/bluez5/bluez5-device.c:1200
+#: spa/plugins/bluez5/bluez5-device.c:1275
 #, c-format
 msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
 msgstr ""
 
-#: spa/plugins/bluez5/bluez5-device.c:1208
+#: spa/plugins/bluez5/bluez5-device.c:1283
 msgid "High Fidelity Playback (A2DP Sink)"
 msgstr ""
 
-#: spa/plugins/bluez5/bluez5-device.c:1210
+#: spa/plugins/bluez5/bluez5-device.c:1285
 msgid "High Fidelity Duplex (A2DP Source/Sink)"
 msgstr ""
 
-#: spa/plugins/bluez5/bluez5-device.c:1238
+#: spa/plugins/bluez5/bluez5-device.c:1322
+#, c-format
+msgid "High Fidelity Playback (BAP Sink, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1326
+#, c-format
+msgid "High Fidelity Input (BAP Source, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1330
+#, c-format
+msgid "High Fidelity Duplex (BAP Source/Sink, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1359
 #, c-format
 msgid "Headset Head Unit (HSP/HFP, codec %s)"
 msgstr ""
 
-#: spa/plugins/bluez5/bluez5-device.c:1243
+#: spa/plugins/bluez5/bluez5-device.c:1364
 msgid "Headset Head Unit (HSP/HFP)"
 msgstr ""
 
-#: spa/plugins/bluez5/bluez5-device.c:1325
+#: spa/plugins/bluez5/bluez5-device.c:1443
+#: spa/plugins/bluez5/bluez5-device.c:1448
+#: spa/plugins/bluez5/bluez5-device.c:1455
+#: spa/plugins/bluez5/bluez5-device.c:1461
+#: spa/plugins/bluez5/bluez5-device.c:1467
+#: spa/plugins/bluez5/bluez5-device.c:1473
+#: spa/plugins/bluez5/bluez5-device.c:1479
+#: spa/plugins/bluez5/bluez5-device.c:1485
+#: spa/plugins/bluez5/bluez5-device.c:1491
 msgid "Handsfree"
 msgstr ""
 
-#: spa/plugins/bluez5/bluez5-device.c:1340
+#: spa/plugins/bluez5/bluez5-device.c:1449
+msgid "Handsfree (HFP)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1466
 msgid "Headphone"
 msgstr ""
 
-#: spa/plugins/bluez5/bluez5-device.c:1345
+#: spa/plugins/bluez5/bluez5-device.c:1472
 msgid "Portable"
 msgstr ""
 
-#: spa/plugins/bluez5/bluez5-device.c:1350
+#: spa/plugins/bluez5/bluez5-device.c:1478
 msgid "Car"
 msgstr ""
 
-#: spa/plugins/bluez5/bluez5-device.c:1355
+#: spa/plugins/bluez5/bluez5-device.c:1484
 msgid "HiFi"
 msgstr ""
 
-#: spa/plugins/bluez5/bluez5-device.c:1360
+#: spa/plugins/bluez5/bluez5-device.c:1490
 msgid "Phone"
 msgstr ""
 
-#: spa/plugins/bluez5/bluez5-device.c:1366
+#: spa/plugins/bluez5/bluez5-device.c:1497
 msgid "Bluetooth"
 msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1498
+msgid "Bluetooth (HFP)"
+msgstr ""
diff --git a/po/pl.po b/po/pl.po
index b84b59b9c0538a3ac7d5699944418ebadcb68a19..032979a9724d28bac04ab9b758f2c79e598b4d5b 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-08-27 13:57+0000\n"
-"PO-Revision-Date: 2022-08-27 16:00+0200\n"
+"POT-Creation-Date: 2022-09-15 15:26+0000\n"
+"PO-Revision-Date: 2022-09-25 15:20+0200\n"
 "Last-Translator: Piotr DrÄ…g <piotrdrag@gmail.com>\n"
 "Language-Team: Polish <community-poland@mozilla.org>\n"
 "Language: pl\n"
@@ -51,7 +51,7 @@ msgstr "Tunel do %s/%s"
 msgid "Dummy Output"
 msgstr "Głuche wyjście"
 
-#: src/modules/module-pulse-tunnel.c:648
+#: src/modules/module-pulse-tunnel.c:662
 #, c-format
 msgid "Tunnel for %s@%s"
 msgstr "Tunel dla %s@%s"
@@ -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:1188
+#: spa/plugins/bluez5/bluez5-device.c:1236
 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:1360
+#: spa/plugins/bluez5/bluez5-device.c:1454
 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:1366
+#: spa/plugins/bluez5/bluez5-device.c:1460
 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:1348
+#: spa/plugins/bluez5/bluez5-device.c:1442
 msgid "Headset"
 msgstr "Słuchawki z mikrofonem"
 
@@ -627,77 +627,92 @@ msgstr "Wbudowany dźwięk"
 msgid "Modem"
 msgstr "Modem"
 
-#: spa/plugins/bluez5/bluez5-device.c:1199
+#: spa/plugins/bluez5/bluez5-device.c:1247
 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:1224
+#: spa/plugins/bluez5/bluez5-device.c:1272
 #, 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:1227
+#: spa/plugins/bluez5/bluez5-device.c:1275
 #, 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:1235
+#: spa/plugins/bluez5/bluez5-device.c:1283
 msgid "High Fidelity Playback (A2DP Sink)"
 msgstr "Odtwarzanie o wysokiej dokładności (odpływ A2DP)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1237
+#: spa/plugins/bluez5/bluez5-device.c:1285
 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:1265
+#: spa/plugins/bluez5/bluez5-device.c:1322
+#, c-format
+msgid "High Fidelity Playback (BAP Sink, codec %s)"
+msgstr "Odtwarzanie o wysokiej dokładności (odpływ BAP, kodek %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1326
+#, c-format
+msgid "High Fidelity Input (BAP Source, codec %s)"
+msgstr "Wejście o wysokiej dokładności (źródło BAP, kodek %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1330
+#, c-format
+msgid "High Fidelity Duplex (BAP Source/Sink, codec %s)"
+msgstr "Dupleks o wysokiej dokładności (źródło/odpływ BAP, kodek %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1359
 #, 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:1270
+#: spa/plugins/bluez5/bluez5-device.c:1364
 msgid "Headset Head Unit (HSP/HFP)"
 msgstr "Jednostka główna słuchawek z mikrofonem (HSP/HFP)"
 
-#: 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
+#: spa/plugins/bluez5/bluez5-device.c:1443
+#: spa/plugins/bluez5/bluez5-device.c:1448
+#: spa/plugins/bluez5/bluez5-device.c:1455
+#: spa/plugins/bluez5/bluez5-device.c:1461
+#: spa/plugins/bluez5/bluez5-device.c:1467
+#: spa/plugins/bluez5/bluez5-device.c:1473
+#: spa/plugins/bluez5/bluez5-device.c:1479
+#: spa/plugins/bluez5/bluez5-device.c:1485
+#: spa/plugins/bluez5/bluez5-device.c:1491
 msgid "Handsfree"
 msgstr "Zestaw głośnomówiący"
 
-#: spa/plugins/bluez5/bluez5-device.c:1355
+#: spa/plugins/bluez5/bluez5-device.c:1449
 msgid "Handsfree (HFP)"
 msgstr "Zestaw głośnomówiący (HFP)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1372
+#: spa/plugins/bluez5/bluez5-device.c:1466
 msgid "Headphone"
 msgstr "Słuchawki"
 
-#: spa/plugins/bluez5/bluez5-device.c:1378
+#: spa/plugins/bluez5/bluez5-device.c:1472
 msgid "Portable"
 msgstr "Przenośne"
 
-#: spa/plugins/bluez5/bluez5-device.c:1384
+#: spa/plugins/bluez5/bluez5-device.c:1478
 msgid "Car"
 msgstr "Samochód"
 
-#: spa/plugins/bluez5/bluez5-device.c:1390
+#: spa/plugins/bluez5/bluez5-device.c:1484
 msgid "HiFi"
 msgstr "HiFi"
 
-#: spa/plugins/bluez5/bluez5-device.c:1396
+#: spa/plugins/bluez5/bluez5-device.c:1490
 msgid "Phone"
 msgstr "Telefon"
 
-#: spa/plugins/bluez5/bluez5-device.c:1403
+#: spa/plugins/bluez5/bluez5-device.c:1497
 msgid "Bluetooth"
 msgstr "Bluetooth"
 
-#: spa/plugins/bluez5/bluez5-device.c:1404
+#: spa/plugins/bluez5/bluez5-device.c:1498
 msgid "Bluetooth (HFP)"
 msgstr "Bluetooth (HFP)"
diff --git a/po/pt_BR.po b/po/pt_BR.po
index e695bef5e0aab4862072b8adbbace8c563a9745a..9b70d6f0b104fbe95307fb3ff72e065a29a2ded6 100644
--- a/po/pt_BR.po
+++ b/po/pt_BR.po
@@ -1,18 +1,19 @@
 # Brazilian Portuguese translation for pipewire
-# Copyright (C) 2021 Rafael Fontenelle <rafaelff@gnome.org>
+# Copyright (C) 2022 Rafael Fontenelle <rafaelff@gnome.org>
 # This file is distributed under the same license as the pipewire package.
 # Fabian Affolter <fab@fedoraproject.org>, 2008.
 # Igor Pires Soares <igor@projetofedora.org>, 2009, 2012.
 # Rafael Fontenelle <rafaelff@gnome.org>, 2013-2021.
+# Matheus Barbosa <mdpb.matheus@gmail.com>, 2022.
 #
 msgid ""
 msgstr ""
 "Project-Id-Version: pipewire\n"
 "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/-/"
 "issues\n"
-"POT-Creation-Date: 2021-08-01 15:31+0000\n"
-"PO-Revision-Date: 2021-08-01 17:02-0300\n"
-"Last-Translator: Rafael Fontenelle <rafaelff@gnome.org>\n"
+"POT-Creation-Date: 2022-09-30 03:27+0000\n"
+"PO-Revision-Date: 2022-01-25 19:49-0300\n"
+"Last-Translator: Matheus Barbosa <mdpb.matheus@gmail.com>\n"
 "Language-Team: Brazilian Portuguese <gnome-pt_br-list@gnome.org>\n"
 "Language: pt_BR\n"
 "MIME-Version: 1.0\n"
@@ -21,7 +22,7 @@ msgstr ""
 "Plural-Forms: nplurals=2; plural=(n > 1)\n"
 "X-Generator: Gtranslator 40.0\n"
 
-#: src/daemon/pipewire.c:45
+#: src/daemon/pipewire.c:46
 #, c-format
 msgid ""
 "%s [options]\n"
@@ -43,58 +44,51 @@ msgstr "Sistema de Mídia PipeWire"
 msgid "Start the PipeWire Media System"
 msgstr "Inicia o Sistema de Mídia PipeWire"
 
-#: src/examples/media-session/alsa-monitor.c:586
-#: spa/plugins/alsa/acp/compat.c:189
-msgid "Built-in Audio"
-msgstr "Áudio interno"
-
-#: src/examples/media-session/alsa-monitor.c:590
-#: spa/plugins/alsa/acp/compat.c:194
-msgid "Modem"
-msgstr "Modem"
-
-#: src/examples/media-session/alsa-monitor.c:599
-#: src/modules/module-zeroconf-discover.c:296
-msgid "Unknown device"
-msgstr "Dispositivo desconhecido"
-
-#: src/modules/module-protocol-pulse/modules/module-tunnel-sink.c:173
-#: src/modules/module-protocol-pulse/modules/module-tunnel-source.c:173
+#: 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 para %s/%s"
 
-#: src/modules/module-pulse-tunnel.c:534
+#: src/modules/module-fallback-sink.c:51
+msgid "Dummy Output"
+msgstr "Saída de falsa"
+
+#: src/modules/module-pulse-tunnel.c:662
 #, c-format
 msgid "Tunnel for %s@%s"
 msgstr "Túnel para %s@%s"
 
-#: src/modules/module-zeroconf-discover.c:308
+#: src/modules/module-zeroconf-discover.c:332
+msgid "Unknown device"
+msgstr "Dispositivo desconhecido"
+
+#: src/modules/module-zeroconf-discover.c:344
 #, c-format
 msgid "%s on %s@%s"
 msgstr "%s em %s@%s"
 
-#: src/modules/module-zeroconf-discover.c:312
+#: src/modules/module-zeroconf-discover.c:348
 #, c-format
 msgid "%s on %s"
 msgstr "%s em %s"
 
-#: src/tools/pw-cat.c:1000
+#: 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 [opções] <arquivo>\n"
+"%s [opções] [<arquivo>|-]\n"
 "  -h, --help                            Mostra esta ajuda\n"
 "      --version                         Mostra a versão\n"
 "  -v, --verbose                         Habilita operações verbosas\n"
 "\n"
 
-#: src/tools/pw-cat.c:1007
+#: src/tools/pw-cat.c:791
 #, c-format
 msgid ""
 "  -R, --remote                          Remote daemon name\n"
@@ -108,7 +102,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                          Nome do daemon remoto\n"
@@ -124,11 +118,10 @@ msgstr ""
 "                                          Xunit (unidade = s, ms, us, ns)\n"
 "                                          ou amostras diretas (256)\n"
 "                                          a taxa é um dos arquivos fontes\n"
-"      --list-targets                    Lista alvos disponíveis para --"
-"target\n"
+"      --properties                      Define as propriedades do nó\n"
 "\n"
 
-#: src/tools/pw-cat.c:1025
+#: src/tools/pw-cat.c:809
 #, c-format
 msgid ""
 "      --rate                            Sample rate (req. for rec) (default "
@@ -165,19 +158,21 @@ msgstr ""
 "(padrão: %d)\n"
 "\n"
 
-#: src/tools/pw-cat.c:1042
+#: 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 reprodução\n"
 "  -r, --record                          Modo de gravação\n"
-"  -m, --midi                            Modo midi\n"
+"  -m, --midi                            Modo Midi\n"
+"  -d, --dsd                             Modo DSD\n"
 "\n"
 
-#: src/tools/pw-cli.c:2954
+#: src/tools/pw-cli.c:2255
 #, c-format
 msgid ""
 "%s [options] [command]\n"
@@ -194,12 +189,12 @@ msgstr ""
 "  -r, --remote                          Nome do daemon remoto\n"
 "\n"
 
-#: spa/plugins/alsa/acp/acp.c:306
+#: spa/plugins/alsa/acp/acp.c:321
 msgid "Pro Audio"
 msgstr "Pro Audio"
 
-#: spa/plugins/alsa/acp/acp.c:429 spa/plugins/alsa/acp/alsa-mixer.c:4648
-#: spa/plugins/bluez5/bluez5-device.c:1043
+#: spa/plugins/alsa/acp/acp.c:444 spa/plugins/alsa/acp/alsa-mixer.c:4648
+#: spa/plugins/bluez5/bluez5-device.c:1236
 msgid "Off"
 msgstr "Desligado"
 
@@ -226,7 +221,7 @@ msgstr "Entrada de linha"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:2657
 #: spa/plugins/alsa/acp/alsa-mixer.c:2741
-#: spa/plugins/bluez5/bluez5-device.c:1198
+#: spa/plugins/bluez5/bluez5-device.c:1454
 msgid "Microphone"
 msgstr "Microfone"
 
@@ -298,7 +293,7 @@ msgid "No Bass Boost"
 msgstr "Sem reforço de graves"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:2672
-#: spa/plugins/bluez5/bluez5-device.c:1203
+#: spa/plugins/bluez5/bluez5-device.c:1460
 msgid "Speaker"
 msgstr "Auto-falante"
 
@@ -414,7 +409,7 @@ msgstr "Estéreo"
 # Fone de ouvido não se encaixa como tradução aqui, pois há ou pode haver microfone junto.
 #: spa/plugins/alsa/acp/alsa-mixer.c:4484
 #: spa/plugins/alsa/acp/alsa-mixer.c:4642
-#: spa/plugins/bluez5/bluez5-device.c:1188
+#: spa/plugins/bluez5/bluez5-device.c:1442
 msgid "Headset"
 msgstr "Headset"
 
@@ -528,17 +523,17 @@ msgstr "Duplex estéreo"
 msgid "Mono Chat + 7.1 Surround"
 msgstr "Bate-papo monofônico + surround 7.1"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4750
+#: spa/plugins/alsa/acp/alsa-mixer.c:4754
 #, c-format
 msgid "%s Output"
 msgstr "Saída %s"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4757
+#: spa/plugins/alsa/acp/alsa-mixer.c:4761
 #, c-format
 msgid "%s Input"
 msgstr "Entrada %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 "
@@ -561,7 +556,7 @@ msgstr[1] ""
 "É mais provável que isso seja um erro no driver “%s” do ALSA. Por favor, "
 "relate esse problema aos desenvolvedores do 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"
@@ -584,7 +579,7 @@ msgstr[1] ""
 "É mais provável que isso seja um erro no driver “%s” do ALSA. Por favor, "
 "relate esse problema aos desenvolvedores do 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 "
@@ -597,7 +592,7 @@ msgstr ""
 "É mais provável que isso seja um erro no driver “%s” do ALSA. Por favor, "
 "relate esse problema aos desenvolvedores do 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 "
@@ -620,66 +615,106 @@ msgstr[1] ""
 "É mais provável que isso seja um erro no driver “%s” do ALSA. Por favor, "
 "relate esse problema aos desenvolvedores do ALSA."
 
-#: spa/plugins/alsa/acp/channelmap.h:466
+#: spa/plugins/alsa/acp/channelmap.h:457
 msgid "(invalid)"
 msgstr "(inválido)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1053
+#: spa/plugins/alsa/acp/compat.c:189
+msgid "Built-in Audio"
+msgstr "Áudio interno"
+
+#: spa/plugins/alsa/acp/compat.c:194
+msgid "Modem"
+msgstr "Modem"
+
+#: spa/plugins/bluez5/bluez5-device.c:1247
 msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
 msgstr "Gateway de áudio (fonte A2DP & HSP/HFP AG)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1076
+#: spa/plugins/bluez5/bluez5-device.c:1272
 #, c-format
 msgid "High Fidelity Playback (A2DP Sink, codec %s)"
 msgstr "Reprodução de alta-fidelidade (destino  A2DP, codec %s)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1078
+#: spa/plugins/bluez5/bluez5-device.c:1275
 #, c-format
 msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
 msgstr "Duplex de alta-fidelidade (fonte/destino A2DP, codec %s)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1084
+#: spa/plugins/bluez5/bluez5-device.c:1283
 msgid "High Fidelity Playback (A2DP Sink)"
 msgstr "Reprodução de alta-fidelidade (destino A2DP)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1086
+#: spa/plugins/bluez5/bluez5-device.c:1285
 msgid "High Fidelity Duplex (A2DP Source/Sink)"
 msgstr "Duplex de alta-fidelidade (fonte/destino A2DP)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1113
+#: spa/plugins/bluez5/bluez5-device.c:1322
+#, c-format
+msgid "High Fidelity Playback (BAP Sink, codec %s)"
+msgstr "Reprodução de alta-fidelidade (destino BAP, codec %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1326
+#, c-format
+msgid "High Fidelity Input (BAP Source, codec %s)"
+msgstr "Entrada de alta-fidelidade (fonte BAP, codec %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1330
+#, c-format
+msgid "High Fidelity Duplex (BAP Source/Sink, codec %s)"
+msgstr "Duplex de alta-fidelidade (fonte/destino BAP, codec %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1359
 #, c-format
 msgid "Headset Head Unit (HSP/HFP, codec %s)"
 msgstr "Unidade de headset (HSP/HFP, codec %s)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1117
+#: spa/plugins/bluez5/bluez5-device.c:1364
 msgid "Headset Head Unit (HSP/HFP)"
 msgstr "Unidade de headset (HSP/HFP)"
 
-# Desconheço tradução comum para esta palavra.
-#: spa/plugins/bluez5/bluez5-device.c:1193
+# Supostamente relacionado a HFP, hands-free profile, mas não encontrei tradução comum
+#: spa/plugins/bluez5/bluez5-device.c:1443
+#: spa/plugins/bluez5/bluez5-device.c:1448
+#: spa/plugins/bluez5/bluez5-device.c:1455
+#: spa/plugins/bluez5/bluez5-device.c:1461
+#: spa/plugins/bluez5/bluez5-device.c:1467
+#: spa/plugins/bluez5/bluez5-device.c:1473
+#: spa/plugins/bluez5/bluez5-device.c:1479
+#: spa/plugins/bluez5/bluez5-device.c:1485
+#: spa/plugins/bluez5/bluez5-device.c:1491
 msgid "Handsfree"
 msgstr "Handsfree"
 
-#: spa/plugins/bluez5/bluez5-device.c:1208
+# Supostamente relacionado a HFP, hands-free profile, mas não encontrei tradução comum
+#: spa/plugins/bluez5/bluez5-device.c:1449
+msgid "Handsfree (HFP)"
+msgstr "Handsfree (HFP)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1466
 msgid "Headphone"
 msgstr "Fones de ouvido"
 
-#: spa/plugins/bluez5/bluez5-device.c:1213
+#: spa/plugins/bluez5/bluez5-device.c:1472
 msgid "Portable"
 msgstr "Portátil"
 
-#: spa/plugins/bluez5/bluez5-device.c:1218
+#: spa/plugins/bluez5/bluez5-device.c:1478
 msgid "Car"
 msgstr "Carro"
 
-#: spa/plugins/bluez5/bluez5-device.c:1223
+#: spa/plugins/bluez5/bluez5-device.c:1484
 msgid "HiFi"
 msgstr "HiFi"
 
-#: spa/plugins/bluez5/bluez5-device.c:1228
+#: spa/plugins/bluez5/bluez5-device.c:1490
 msgid "Phone"
 msgstr "Telefone"
 
-#: spa/plugins/bluez5/bluez5-device.c:1234
+#: spa/plugins/bluez5/bluez5-device.c:1497
 msgid "Bluetooth"
 msgstr "Bluetooth"
+
+#: spa/plugins/bluez5/bluez5-device.c:1498
+msgid "Bluetooth (HFP)"
+msgstr "Bluetooth (HFP)"
diff --git a/po/sv.po b/po/sv.po
index f9a57802245ab89f1446ec91f7aa8c2e28e66010..86bf70163ae396fa41d838117b37747f708f0076 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-07-19 15:27+0000\n"
-"PO-Revision-Date: 2022-07-10 10:22+0200\n"
+"POT-Creation-Date: 2022-10-20 15:27+0000\n"
+"PO-Revision-Date: 2022-09-16 12:58+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.1\n"
+"X-Generator: Poedit 3.1.1\n"
 
 #: src/daemon/pipewire.c:46
 #, c-format
@@ -61,26 +61,26 @@ msgstr "Tunnel till %s/%s"
 msgid "Dummy Output"
 msgstr "Attrapputgång"
 
-#: src/modules/module-pulse-tunnel.c:648
+#: src/modules/module-pulse-tunnel.c:681
 #, c-format
 msgid "Tunnel for %s@%s"
 msgstr "Tunnel för %s@%s"
 
-#: src/modules/module-zeroconf-discover.c:332
+#: src/modules/module-zeroconf-discover.c:335
 msgid "Unknown device"
 msgstr "Okänd enhet"
 
-#: src/modules/module-zeroconf-discover.c:344
+#: src/modules/module-zeroconf-discover.c:347
 #, c-format
 msgid "%s on %s@%s"
 msgstr "%s på %s@%s"
 
-#: src/modules/module-zeroconf-discover.c:348
+#: src/modules/module-zeroconf-discover.c:351
 #, c-format
 msgid "%s on %s"
 msgstr "%s på %s"
 
-#: src/tools/pw-cat.c:784
+#: src/tools/pw-cat.c:782
 #, 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:791
+#: src/tools/pw-cat.c:789
 #, 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:809
+#: src/tools/pw-cat.c:807
 #, c-format
 msgid ""
 "      --rate                            Sample rate (req. for rec) (default "
@@ -160,7 +160,7 @@ msgstr ""
 "%d)\n"
 "\n"
 
-#: src/tools/pw-cat.c:826
+#: src/tools/pw-cat.c:824
 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:3165
+#: src/tools/pw-cli.c:2250
 #, c-format
 msgid ""
 "%s [options] [command]\n"
@@ -195,8 +195,8 @@ msgstr ""
 msgid "Pro Audio"
 msgstr "Professionellt ljud"
 
-#: spa/plugins/alsa/acp/acp.c:446 spa/plugins/alsa/acp/alsa-mixer.c:4648
-#: spa/plugins/bluez5/bluez5-device.c:1188
+#: spa/plugins/alsa/acp/acp.c:444 spa/plugins/alsa/acp/alsa-mixer.c:4648
+#: spa/plugins/bluez5/bluez5-device.c:1237
 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:1360
+#: spa/plugins/bluez5/bluez5-device.c:1455
 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:1366
+#: spa/plugins/bluez5/bluez5-device.c:1461
 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:1348
+#: spa/plugins/bluez5/bluez5-device.c:1443
 msgid "Headset"
 msgstr "Headset"
 
@@ -528,7 +528,7 @@ msgstr "%s-utgång"
 msgid "%s Input"
 msgstr "%s-ingång"
 
-#: 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 "
@@ -551,7 +551,7 @@ msgstr[1] ""
 "Förmodligen är detta ett fel i ALSA-drivrutinen ”%s”. Vänligen rapportera "
 "problemet till ALSA-utvecklarna."
 
-#: 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"
@@ -574,7 +574,7 @@ msgstr[1] ""
 "Förmodligen är detta ett fel i ALSA-drivrutinen ”%s”. Vänligen rapportera "
 "problemet till ALSA-utvecklarna."
 
-#: 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 "
@@ -587,7 +587,7 @@ msgstr ""
 "Förmodligen är detta ett fel i ALSA-drivrutinen ”%s”. Vänligen rapportera "
 "problemet till ALSA-utvecklarna."
 
-#: 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 "
@@ -622,77 +622,92 @@ msgstr "Inbyggt ljud"
 msgid "Modem"
 msgstr "Modem"
 
-#: spa/plugins/bluez5/bluez5-device.c:1199
+#: spa/plugins/bluez5/bluez5-device.c:1248
 msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
 msgstr "Audio gateway (A2DP-källa & HSP/HFP AG)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1224
+#: spa/plugins/bluez5/bluez5-device.c:1273
 #, 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:1227
+#: spa/plugins/bluez5/bluez5-device.c:1276
 #, 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:1235
+#: spa/plugins/bluez5/bluez5-device.c:1284
 msgid "High Fidelity Playback (A2DP Sink)"
 msgstr "High fidelity-uppspelning (A2DP-utgång)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1237
+#: spa/plugins/bluez5/bluez5-device.c:1286
 msgid "High Fidelity Duplex (A2DP Source/Sink)"
 msgstr "High fidelity duplex (A2DP-källa/utgång)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1265
+#: spa/plugins/bluez5/bluez5-device.c:1323
+#, c-format
+msgid "High Fidelity Playback (BAP Sink, codec %s)"
+msgstr "High fidelity-uppspelning (BAP-utgång, kodek %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1327
+#, c-format
+msgid "High Fidelity Input (BAP Source, codec %s)"
+msgstr "High fidelity-ingång (BAP-källa, kodek %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1331
+#, c-format
+msgid "High Fidelity Duplex (BAP Source/Sink, codec %s)"
+msgstr "High fidelity duplex (BAP-källa/utgång, kodek %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1360
 #, c-format
 msgid "Headset Head Unit (HSP/HFP, codec %s)"
 msgstr "Headset-huvudenhet (HSP/HFP, kodek %s)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1270
+#: spa/plugins/bluez5/bluez5-device.c:1365
 msgid "Headset Head Unit (HSP/HFP)"
 msgstr "Headset-huvudenhet (HSP/HFP)"
 
-#: 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
+#: spa/plugins/bluez5/bluez5-device.c:1444
+#: spa/plugins/bluez5/bluez5-device.c:1449
+#: spa/plugins/bluez5/bluez5-device.c:1456
+#: spa/plugins/bluez5/bluez5-device.c:1462
+#: spa/plugins/bluez5/bluez5-device.c:1468
+#: spa/plugins/bluez5/bluez5-device.c:1474
+#: spa/plugins/bluez5/bluez5-device.c:1480
+#: spa/plugins/bluez5/bluez5-device.c:1486
+#: spa/plugins/bluez5/bluez5-device.c:1492
 msgid "Handsfree"
 msgstr "Handsfree"
 
-#: spa/plugins/bluez5/bluez5-device.c:1355
+#: spa/plugins/bluez5/bluez5-device.c:1450
 msgid "Handsfree (HFP)"
 msgstr "Handsfree (HFP)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1372
+#: spa/plugins/bluez5/bluez5-device.c:1467
 msgid "Headphone"
 msgstr "Hörlurar"
 
-#: spa/plugins/bluez5/bluez5-device.c:1378
+#: spa/plugins/bluez5/bluez5-device.c:1473
 msgid "Portable"
 msgstr "Bärbar"
 
-#: spa/plugins/bluez5/bluez5-device.c:1384
+#: spa/plugins/bluez5/bluez5-device.c:1479
 msgid "Car"
 msgstr "Bil"
 
-#: spa/plugins/bluez5/bluez5-device.c:1390
+#: spa/plugins/bluez5/bluez5-device.c:1485
 msgid "HiFi"
 msgstr "HiFi"
 
-#: spa/plugins/bluez5/bluez5-device.c:1396
+#: spa/plugins/bluez5/bluez5-device.c:1491
 msgid "Phone"
 msgstr "Telefon"
 
-#: spa/plugins/bluez5/bluez5-device.c:1403
+#: spa/plugins/bluez5/bluez5-device.c:1498
 msgid "Bluetooth"
 msgstr "Bluetooth"
 
-#: spa/plugins/bluez5/bluez5-device.c:1404
+#: spa/plugins/bluez5/bluez5-device.c:1499
 msgid "Bluetooth (HFP)"
 msgstr "Bluetooth (HFP)"
diff --git a/po/tr.po b/po/tr.po
index a1017944907316438d19b8a59c31565ae3969618..766f48f48bb909805a8157bbe9ab3017f8484b70 100644
--- a/po/tr.po
+++ b/po/tr.po
@@ -11,8 +11,8 @@ msgstr ""
 "Project-Id-Version: PipeWire master\n"
 "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/"
 "issues/new\n"
-"POT-Creation-Date: 2022-04-03 12:56+0200\n"
-"PO-Revision-Date: 2022-05-14 18:35+0300\n"
+"POT-Creation-Date: 2022-06-30 12:50+0200\n"
+"PO-Revision-Date: 2022-10-23 10:40+0300\n"
 "Last-Translator: OÄŸuz Ersen <oguz@ersen.moe>\n"
 "Language-Team: Turkish <tr>\n"
 "Language: tr\n"
@@ -35,8 +35,8 @@ msgstr ""
 "      --version                         Sürümü göster\n"
 "  -c, --config                          Yapılandırmayı yükle (Öntanımlı %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 "%s/%s tüneli"
@@ -45,41 +45,41 @@ msgstr "%s/%s tüneli"
 msgid "Dummy Output"
 msgstr "Temsili Çıkış"
 
-#: src/modules/module-pulse-tunnel.c:545
+#: src/modules/module-pulse-tunnel.c:662
 #, c-format
 msgid "Tunnel for %s@%s"
 msgstr "%s@%s için tünel"
 
-#: src/modules/module-zeroconf-discover.c:313
+#: src/modules/module-zeroconf-discover.c:332
 msgid "Unknown device"
 msgstr "Bilinmeyen aygıt"
 
-#: src/modules/module-zeroconf-discover.c:325
+#: src/modules/module-zeroconf-discover.c:344
 #, c-format
 msgid "%s on %s@%s"
 msgstr "%s, %s@%s"
 
-#: src/modules/module-zeroconf-discover.c:329
+#: src/modules/module-zeroconf-discover.c:348
 #, c-format
 msgid "%s on %s"
 msgstr "%s, %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 [seçenekler] <dosya>\n"
+"%s [seçenekler] [<dosya>|-]\n"
 "  -h, --help                            Bu yardımı göster\n"
 "      --version                         Sürümü göster\n"
 "  -v, --verbose                         Ayrıntılı işlemleri etkinleştir\n"
 "\n"
 
-#: src/tools/pw-cat.c:1094
+#: src/tools/pw-cat.c:791
 #, c-format
 msgid ""
 "  -R, --remote                          Remote daemon name\n"
@@ -93,7 +93,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                          Uzak arka plan programı adı\n"
@@ -109,11 +109,10 @@ msgstr ""
 "                                          Xbirim (birim = s, ms, us, ns)\n"
 "                                          veya doğrudan örneklemeler (256)\n"
 "                                          oran kaynak dosyadan biridir\n"
-"      --list-targets                    --target için kullanılabilir "
-"hedefleri listele\n"
+"  -P  --properties                      Düğüm özelliklerini ayarla\n"
 "\n"
 
-#: src/tools/pw-cat.c:1112
+#: src/tools/pw-cat.c:809
 #, c-format
 msgid ""
 "      --rate                            Sample rate (req. for rec) (default "
@@ -149,7 +148,7 @@ msgstr ""
 "15) (öntanımlı %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"
@@ -163,7 +162,7 @@ msgstr ""
 "  -d, --dsd                             DSD modu\n"
 "\n"
 
-#: src/tools/pw-cli.c:3051
+#: src/tools/pw-cli.c:2250
 #, c-format
 msgid ""
 "%s [options] [command]\n"
@@ -186,7 +185,7 @@ msgid "Pro Audio"
 msgstr "Profesyonel Ses"
 
 #: spa/plugins/alsa/acp/acp.c:444 spa/plugins/alsa/acp/alsa-mixer.c:4648
-#: spa/plugins/bluez5/bluez5-device.c:1159
+#: spa/plugins/bluez5/bluez5-device.c:1236
 msgid "Off"
 msgstr "Kapalı"
 
@@ -213,7 +212,7 @@ msgstr "Hat GiriÅŸi"
 
 #: 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:1454
 msgid "Microphone"
 msgstr "Mikrofon"
 
@@ -279,7 +278,7 @@ msgid "No Bass Boost"
 msgstr "Bas Artırma Yok"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:2672
-#: spa/plugins/bluez5/bluez5-device.c:1333
+#: spa/plugins/bluez5/bluez5-device.c:1460
 msgid "Speaker"
 msgstr "Hoparlör"
 
@@ -394,7 +393,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:1442
 msgid "Headset"
 msgstr "Kulaklık"
 
@@ -518,7 +517,7 @@ msgstr "%s Çıkışı"
 msgid "%s Input"
 msgstr "%s GiriÅŸi"
 
-#: 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 "
@@ -535,7 +534,7 @@ msgstr[0] ""
 "Büyük ihtimalle bu bir ALSA sürücüsü '%s' hatasıdır. Lütfen bu sorunu ALSA "
 "geliÅŸtiricilerine bildirin."
 
-#: 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 "
@@ -552,7 +551,7 @@ msgstr[0] ""
 "Büyük ihtimalle bu bir ALSA sürücüsü '%s' hatasıdır. Lütfen bu sorunu ALSA "
 "geliÅŸtiricilerine bildirin."
 
-#: 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 "
@@ -565,7 +564,7 @@ msgstr ""
 "Büyük ihtimalle bu bir ALSA sürücüsü '%s' hatasıdır. Lütfen bu sorunu ALSA "
 "geliÅŸtiricilerine bildirin."
 
-#: 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 "
@@ -583,7 +582,7 @@ msgstr[0] ""
 "Büyük ihtimalle bu bir ALSA sürücüsü '%s' hatasıdır. Lütfen bu sorunu ALSA "
 "geliÅŸtiricilerine bildirin."
 
-#: spa/plugins/alsa/acp/channelmap.h:464
+#: spa/plugins/alsa/acp/channelmap.h:457
 msgid "(invalid)"
 msgstr "(geçersiz)"
 
@@ -595,65 +594,96 @@ msgstr "Dahili Ses"
 msgid "Modem"
 msgstr "Modem"
 
-#: spa/plugins/bluez5/bluez5-device.c:1170
+#: spa/plugins/bluez5/bluez5-device.c:1247
 msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
 msgstr "Ses Geçidi (A2DP Kaynak & HSP/HFP AG)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1195
+#: spa/plugins/bluez5/bluez5-device.c:1272
 #, c-format
 msgid "High Fidelity Playback (A2DP Sink, codec %s)"
 msgstr "Yüksek Kaliteli Çalma (A2DP Alıcı, çözücü %s)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1198
+#: spa/plugins/bluez5/bluez5-device.c:1275
 #, c-format
 msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
 msgstr "Yüksek Kaliteli İkili (A2DP Kaynak/Alıcı, çözücü %s)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1206
+#: spa/plugins/bluez5/bluez5-device.c:1283
 msgid "High Fidelity Playback (A2DP Sink)"
 msgstr "Yüksek Kaliteli Çalma (A2DP Alıcı)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1208
+#: spa/plugins/bluez5/bluez5-device.c:1285
 msgid "High Fidelity Duplex (A2DP Source/Sink)"
 msgstr "Yüksek Kaliteli İkili (A2DP Kaynak/Alıcı)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1236
+#: spa/plugins/bluez5/bluez5-device.c:1322
+#, c-format
+msgid "High Fidelity Playback (BAP Sink, codec %s)"
+msgstr "Yüksek Kaliteli Çalma (BAP Alıcı, çözücü %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1326
+#, c-format
+msgid "High Fidelity Input (BAP Source, codec %s)"
+msgstr "Yüksek Kaliteli Giriş (BAP Kaynak, çözücü %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1330
+#, c-format
+msgid "High Fidelity Duplex (BAP Source/Sink, codec %s)"
+msgstr "Yüksek Kaliteli İkili (BAP Kaynak/Alıcı, çözücü %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1359
 #, c-format
 msgid "Headset Head Unit (HSP/HFP, codec %s)"
 msgstr "Kulaklık Ana Birimi (HSP/HFP, çözücü %s)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1241
+#: spa/plugins/bluez5/bluez5-device.c:1364
 msgid "Headset Head Unit (HSP/HFP)"
 msgstr "Kulaklık Ana Birimi (HSP/HFP)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1323
+#: spa/plugins/bluez5/bluez5-device.c:1443
+#: spa/plugins/bluez5/bluez5-device.c:1448
+#: spa/plugins/bluez5/bluez5-device.c:1455
+#: spa/plugins/bluez5/bluez5-device.c:1461
+#: spa/plugins/bluez5/bluez5-device.c:1467
+#: spa/plugins/bluez5/bluez5-device.c:1473
+#: spa/plugins/bluez5/bluez5-device.c:1479
+#: spa/plugins/bluez5/bluez5-device.c:1485
+#: spa/plugins/bluez5/bluez5-device.c:1491
 msgid "Handsfree"
 msgstr "Ahizesiz"
 
-#: spa/plugins/bluez5/bluez5-device.c:1338
+#: spa/plugins/bluez5/bluez5-device.c:1449
+msgid "Handsfree (HFP)"
+msgstr "Ahizesiz (HFP)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1466
 msgid "Headphone"
 msgstr "Kulaklık"
 
-#: spa/plugins/bluez5/bluez5-device.c:1343
+#: spa/plugins/bluez5/bluez5-device.c:1472
 msgid "Portable"
 msgstr "Taşınabilir"
 
-#: spa/plugins/bluez5/bluez5-device.c:1348
+#: spa/plugins/bluez5/bluez5-device.c:1478
 msgid "Car"
 msgstr "Araba"
 
-#: spa/plugins/bluez5/bluez5-device.c:1353
+#: spa/plugins/bluez5/bluez5-device.c:1484
 msgid "HiFi"
 msgstr "Yüksek Kalite"
 
-#: spa/plugins/bluez5/bluez5-device.c:1358
+#: spa/plugins/bluez5/bluez5-device.c:1490
 msgid "Phone"
 msgstr "Telefon"
 
-#: spa/plugins/bluez5/bluez5-device.c:1364
+#: spa/plugins/bluez5/bluez5-device.c:1497
 msgid "Bluetooth"
 msgstr "Bluetooth"
 
+#: spa/plugins/bluez5/bluez5-device.c:1498
+msgid "Bluetooth (HFP)"
+msgstr "Bluetooth (HFP)"
+
 #~ msgid "PipeWire Media System"
 #~ msgstr "PipeWire Ortam Sistemi"
 
diff --git a/pw-uninstalled.sh b/pw-uninstalled.sh
index 7bc948ddde7c71f75e1638bc4c67d3d768bec128..1bb6c55c23b9a45f100497091db501870d15f358 100755
--- a/pw-uninstalled.sh
+++ b/pw-uninstalled.sh
@@ -41,7 +41,7 @@ export SPA_PLUGIN_DIR="${BUILDDIR}/spa/plugins"
 export SPA_DATA_DIR="${SCRIPT_DIR}/spa/plugins"
 # the directory with pipewire modules
 export PIPEWIRE_MODULE_DIR="${BUILDDIR}/src/modules"
-export PATH="${BUILDDIR}/src/daemon:${BUILDDIR}/src/tools:${BUILDDIR}/src/media-session:${BUILDDIR}/src/examples:${PATH}"
+export PATH="${BUILDDIR}/src/daemon:${BUILDDIR}/src/tools:${BUILDDIR}/src/media-session:${BUILDDIR}/src/examples:${BUILDDIR}/pipewire-v4l2/src:${PATH}"
 export LD_LIBRARY_PATH="${BUILDDIR}/src/pipewire/:${BUILDDIR}/pipewire-jack/src/${LD_LIBRARY_PATH+":$LD_LIBRARY_PATH"}"
 export GST_PLUGIN_PATH="${BUILDDIR}/src/gst/${GST_PLUGIN_PATH+":${GST_PLUGIN_PATH}"}"
 # the directory with card profiles and paths
@@ -50,6 +50,7 @@ export ACP_PROFILES_DIR="${SCRIPT_DIR}/spa/plugins/alsa/mixer/profile-sets"
 # ALSA plugin directory
 export ALSA_PLUGIN_DIR="${BUILDDIR}/pipewire-alsa/alsa-plugins"
 
+export PW_BUILDDIR=$BUILDDIR
 export PW_UNINSTALLED=1
 export PKG_CONFIG_PATH="${BUILDDIR}/meson-uninstalled/:${PKG_CONFIG_PATH}"
 
diff --git a/spa/include/spa/buffer/alloc.h b/spa/include/spa/buffer/alloc.h
index 6417073838d71ed03c9fede73078ed7113a677e2..ec855415323dc09305bb6464a2bf7b829270eb35 100644
--- a/spa/include/spa/buffer/alloc.h
+++ b/spa/include/spa/buffer/alloc.h
@@ -161,8 +161,9 @@ static inline int spa_buffer_alloc_fill_info(struct spa_buffer_alloc_info *info,
 	*target += info->chunk_size;
 
 	for (i = 0, size = 0; i < n_datas; i++) {
+		int64_t align = data_aligns[i];
 		info->max_align = SPA_MAX(info->max_align, data_aligns[i]);
-		size = SPA_ROUND_UP_N(size, data_aligns[i]);
+		size = SPA_ROUND_UP_N(size, align);
 		size += datas[i].maxsize;
 	}
 	info->data_size = size;
diff --git a/spa/include/spa/buffer/meta.h b/spa/include/spa/buffer/meta.h
index ae72ef970e5dc13a3b967f57e7398163fbd6c4ce..e270c56cacbaf13d9d9a93b5f1cc2ae127b067f4 100644
--- a/spa/include/spa/buffer/meta.h
+++ b/spa/include/spa/buffer/meta.h
@@ -64,9 +64,15 @@ struct spa_meta {
 	void *data;		/**< pointer to metadata */
 };
 
-#define spa_meta_first(m)	((m)->data)
-#define spa_meta_end(m)		SPA_PTROFF((m)->data,(m)->size,void)
-#define spa_meta_check(p,m)	(SPA_PTROFF(p,sizeof(*p),void) <= spa_meta_end(m))
+static inline void *spa_meta_first(const struct spa_meta *m) {
+	return m->data;
+}
+#define spa_meta_first spa_meta_first
+static inline void *spa_meta_end(const struct spa_meta *m) {
+	return SPA_PTROFF(m->data,m->size,void);
+}
+#define spa_meta_end spa_meta_end
+#define spa_meta_check(p,m)	(SPA_PTROFF(p,sizeof(*(p)),void) <= spa_meta_end(m))
 
 /**
  * Describes essential buffer header metadata such as flags and
@@ -92,11 +98,14 @@ struct spa_meta_region {
 	struct spa_region region;
 };
 
-#define spa_meta_region_is_valid(m)	((m)->region.size.width != 0 && (m)->region.size.height != 0)
+static inline bool spa_meta_region_is_valid(const struct spa_meta_region *m) {
+	return m->region.size.width != 0 && m->region.size.height != 0;
+}
+#define spa_meta_region_is_valid spa_meta_region_is_valid
 
 /** iterate all the items in a metadata */
 #define spa_meta_for_each(pos,meta)					\
-	for (pos = (__typeof(pos))spa_meta_first(meta);			\
+	for ((pos) = (__typeof(pos))spa_meta_first(meta);			\
 	    spa_meta_check(pos, meta);					\
             (pos)++)
 
diff --git a/spa/include/spa/debug/log.h b/spa/include/spa/debug/log.h
index 8311e95460ce0221cb60b5fe82c6c70a4fea6cd6..bfe2d5ff84dbd061b1086dd4d21183f5aef615bd 100644
--- a/spa/include/spa/debug/log.h
+++ b/spa/include/spa/debug/log.h
@@ -36,10 +36,10 @@ extern "C" {
  */
 
 #ifndef spa_debug
-#define spa_debug(fmt,...)	({ printf(fmt"\n", ## __VA_ARGS__); })
+#define spa_debug(fmt,...)	({ printf((fmt"\n"), ## __VA_ARGS__); })
 #endif
 #ifndef spa_debugn
-#define spa_debugn(fmt,...)	({ printf(fmt, ## __VA_ARGS__); })
+#define spa_debugn(fmt,...)	({ printf((fmt), ## __VA_ARGS__); })
 #endif
 
 /**
diff --git a/spa/include/spa/graph/graph.h b/spa/include/spa/graph/graph.h
index ba402481c96a7ca62f5d6e443e7a5f15b35448c2..0e887cd3c6d0ad436cb7eb40c709c262351faa2b 100644
--- a/spa/include/spa/graph/graph.h
+++ b/spa/include/spa/graph/graph.h
@@ -121,12 +121,12 @@ struct spa_graph_node {
 	int __res = 0;							\
 	spa_callbacks_call_res(&(n)->callbacks,				\
 			struct spa_graph_node_callbacks, __res,		\
-			method, version, ##__VA_ARGS__);		\
+			method, (version), ##__VA_ARGS__);		\
 	__res;								\
 })
 
-#define spa_graph_node_process(n)		spa_graph_node_call(n, process, 0, n)
-#define spa_graph_node_reuse_buffer(n,p,i)	spa_graph_node_call(n, reuse_buffer, 0, n, p, i)
+#define spa_graph_node_process(n)		spa_graph_node_call((n), process, 0, (n))
+#define spa_graph_node_reuse_buffer(n,p,i)	spa_graph_node_call((n), reuse_buffer, 0, (n), (p), (i))
 
 struct spa_graph_port {
 	struct spa_list link;		/**< link in node port list */
diff --git a/spa/include/spa/interfaces/audio/aec.h b/spa/include/spa/interfaces/audio/aec.h
index 9fb109df21b11a5b59373370017a20ca18edaf49..c5dcb68aedf5ae900df87b5d9db77aa1bd10cf4f 100644
--- a/spa/include/spa/interfaces/audio/aec.h
+++ b/spa/include/spa/interfaces/audio/aec.h
@@ -23,6 +23,7 @@
  */
 
 
+#include <spa/pod/builder.h>
 #include <spa/utils/dict.h>
 #include <spa/utils/hook.h>
 #include <spa/param/audio/raw.h>
@@ -60,7 +61,7 @@ struct spa_audio_aec_events {
 };
 
 struct spa_audio_aec_methods {
-#define SPA_VERSION_AUDIO_AEC_METHODS	1
+#define SPA_VERSION_AUDIO_AEC_METHODS	2
         uint32_t version;
 
 	int (*add_listener) (void *object,
@@ -75,15 +76,20 @@ struct spa_audio_aec_methods {
 	int (*activate) (void *object);
 	/* since 0.3.58, version 1:1 */
 	int (*deactivate) (void *object);
+
+	/* version 1:2 */
+	int (*enum_props) (void* object, int index, struct spa_pod_builder* builder);
+	int (*get_params) (void* object, struct spa_pod_builder* builder);
+	int (*set_params) (void *object, const struct spa_pod *args);
 };
 
 #define spa_audio_aec_method(o,method,version,...)			\
 ({									\
 	int _res = -ENOTSUP;						\
-	struct spa_audio_aec *_o = o;					\
+	struct spa_audio_aec *_o = (o);					\
 	spa_interface_call_res(&_o->iface,				\
 			struct spa_audio_aec_methods, _res,		\
-			method, version, ##__VA_ARGS__);		\
+			method, (version), ##__VA_ARGS__);		\
 	_res;								\
 })
 
@@ -93,6 +99,9 @@ struct spa_audio_aec_methods {
 #define spa_audio_aec_set_props(o,...)		spa_audio_aec_method(o, set_props, 0, __VA_ARGS__)
 #define spa_audio_aec_activate(o)		spa_audio_aec_method(o, activate, 1)
 #define spa_audio_aec_deactivate(o)		spa_audio_aec_method(o, deactivate, 1)
+#define spa_audio_aec_enum_props(o,...)		spa_audio_aec_method(o, enum_props, 2, __VA_ARGS__)
+#define spa_audio_aec_get_params(o,...)		spa_audio_aec_method(o, get_params, 2, __VA_ARGS__)
+#define spa_audio_aec_set_params(o,...)		spa_audio_aec_method(o, set_params, 2, __VA_ARGS__)
 
 #ifdef __cplusplus
 }  /* extern "C" */
diff --git a/spa/include/spa/monitor/device.h b/spa/include/spa/monitor/device.h
index 481874d184130342c7ecc16b8fb434fa56b71bd8..59fea51484c03ddba5d1ac03ac67fd82ebae7116 100644
--- a/spa/include/spa/monitor/device.h
+++ b/spa/include/spa/monitor/device.h
@@ -71,7 +71,7 @@ struct spa_device_info {
 	uint32_t n_params;			/**< number of elements in params */
 };
 
-#define SPA_DEVICE_INFO_INIT()	(struct spa_device_info){ SPA_VERSION_DEVICE_INFO, }
+#define SPA_DEVICE_INFO_INIT()	((struct spa_device_info){ SPA_VERSION_DEVICE_INFO, })
 
 /**
  * Information about a device object
@@ -92,7 +92,7 @@ struct spa_device_object_info {
 	const struct spa_dict *props;		/**< extra object properties */
 };
 
-#define SPA_DEVICE_OBJECT_INFO_INIT()	(struct spa_device_object_info){ SPA_VERSION_DEVICE_OBJECT_INFO, }
+#define SPA_DEVICE_OBJECT_INFO_INIT()	((struct spa_device_object_info){ SPA_VERSION_DEVICE_OBJECT_INFO, })
 
 /** the result of spa_device_enum_params() */
 #define SPA_RESULT_TYPE_DEVICE_PARAMS	1
@@ -243,10 +243,10 @@ struct spa_device_methods {
 #define spa_device_method(o,method,version,...)				\
 ({									\
 	int _res = -ENOTSUP;						\
-	struct spa_device *_o = o;					\
+	struct spa_device *_o = (o);					\
 	spa_interface_call_res(&_o->iface,				\
 			struct spa_device_methods, _res,		\
-			method, version, ##__VA_ARGS__);		\
+			method, (version), ##__VA_ARGS__);		\
 	_res;								\
 })
 
diff --git a/spa/include/spa/node/io.h b/spa/include/spa/node/io.h
index 74635c79a5c8ee0972ed92d1db0c64795492ed01..9211f65977e45f9b788d517ac4adf687eec2dafa 100644
--- a/spa/include/spa/node/io.h
+++ b/spa/include/spa/node/io.h
@@ -100,7 +100,7 @@ struct spa_io_buffers {
 	uint32_t buffer_id;		/**< a buffer id */
 };
 
-#define SPA_IO_BUFFERS_INIT  (struct spa_io_buffers) { SPA_STATUS_OK, SPA_ID_INVALID, }
+#define SPA_IO_BUFFERS_INIT  ((struct spa_io_buffers) { SPA_STATUS_OK, SPA_ID_INVALID, })
 
 /**
  * IO area to exchange a memory region
@@ -110,7 +110,7 @@ struct spa_io_memory {
 	uint32_t size;			/**< the size of \a data */
 	void *data;			/**< a memory pointer */
 };
-#define SPA_IO_MEMORY_INIT  (struct spa_io_memory) { SPA_STATUS_OK, 0, NULL, }
+#define SPA_IO_MEMORY_INIT  ((struct spa_io_memory) { SPA_STATUS_OK, 0, NULL, })
 
 /** A range, suitable for input ports that can suggest a range to output ports */
 struct spa_io_range {
diff --git a/spa/include/spa/node/node.h b/spa/include/spa/node/node.h
index 17be05d67f371088ab86dfa258d973acb7179c4f..6efa6d01fd75ab3139ed163167c74fc902af8e9b 100644
--- a/spa/include/spa/node/node.h
+++ b/spa/include/spa/node/node.h
@@ -85,7 +85,7 @@ struct spa_node_info {
 	uint32_t n_params;			/**< number of items in \a params */
 };
 
-#define SPA_NODE_INFO_INIT()	(struct spa_node_info) { 0, }
+#define SPA_NODE_INFO_INIT()	((struct spa_node_info) { 0, })
 
 /**
  * Port information structure
@@ -124,7 +124,7 @@ struct spa_port_info {
 	uint32_t n_params;			/**< number of items in \a params */
 };
 
-#define SPA_PORT_INFO_INIT()	(struct spa_port_info) { 0, }
+#define SPA_PORT_INFO_INIT()	((struct spa_port_info) { 0, })
 
 #define SPA_RESULT_TYPE_NODE_ERROR	1
 #define SPA_RESULT_TYPE_NODE_PARAMS	2
diff --git a/spa/include/spa/param/audio/dsd.h b/spa/include/spa/param/audio/dsd.h
index 3228f2565df89c1c4ea8351dae04d9c511f0b8ee..3b317f2ed44e989d804441697e780ce1e295894a 100644
--- a/spa/include/spa/param/audio/dsd.h
+++ b/spa/include/spa/param/audio/dsd.h
@@ -68,7 +68,7 @@ struct spa_audio_info_dsd {
 	uint32_t position[SPA_AUDIO_MAX_CHANNELS];	/*< channel position from enum spa_audio_channel */
 };
 
-#define SPA_AUDIO_INFO_DSD_INIT(...)		(struct spa_audio_info_dsd) { __VA_ARGS__ }
+#define SPA_AUDIO_INFO_DSD_INIT(...)		((struct spa_audio_info_dsd) { __VA_ARGS__ })
 
 /**
  * \}
diff --git a/spa/include/spa/param/audio/format-utils.h b/spa/include/spa/param/audio/format-utils.h
index 6ee8f933590f34e1b8eebb34182b1bd13a0b35c2..55ccec3297d7c52581dafb2598d59eecb2f926b2 100644
--- a/spa/include/spa/param/audio/format-utils.h
+++ b/spa/include/spa/param/audio/format-utils.h
@@ -47,9 +47,9 @@ spa_format_audio_raw_parse(const struct spa_pod *format, struct spa_audio_info_r
 	info->flags = 0;
 	res = spa_pod_parse_object(format,
 			SPA_TYPE_OBJECT_Format, NULL,
-			SPA_FORMAT_AUDIO_format,	SPA_POD_Id(&info->format),
-			SPA_FORMAT_AUDIO_rate,		SPA_POD_Int(&info->rate),
-			SPA_FORMAT_AUDIO_channels,	SPA_POD_Int(&info->channels),
+			SPA_FORMAT_AUDIO_format,	SPA_POD_OPT_Id(&info->format),
+			SPA_FORMAT_AUDIO_rate,		SPA_POD_OPT_Int(&info->rate),
+			SPA_FORMAT_AUDIO_channels,	SPA_POD_OPT_Int(&info->channels),
 			SPA_FORMAT_AUDIO_position,	SPA_POD_OPT_Pod(&position));
 	if (position == NULL ||
 	    !spa_pod_copy_array(position, SPA_TYPE_Id, info->position, SPA_AUDIO_MAX_CHANNELS))
@@ -64,7 +64,7 @@ spa_format_audio_dsp_parse(const struct spa_pod *format, struct spa_audio_info_d
 	int res;
 	res = spa_pod_parse_object(format,
 			SPA_TYPE_OBJECT_Format, NULL,
-			SPA_FORMAT_AUDIO_format,	SPA_POD_Id(&info->format));
+			SPA_FORMAT_AUDIO_format,	SPA_POD_OPT_Id(&info->format));
 	return res;
 }
 
@@ -74,8 +74,8 @@ spa_format_audio_iec958_parse(const struct spa_pod *format, struct spa_audio_inf
 	int res;
 	res = spa_pod_parse_object(format,
 			SPA_TYPE_OBJECT_Format, NULL,
-			SPA_FORMAT_AUDIO_iec958Codec,	SPA_POD_Id(&info->codec),
-			SPA_FORMAT_AUDIO_rate,		SPA_POD_Int(&info->rate));
+			SPA_FORMAT_AUDIO_iec958Codec,	SPA_POD_OPT_Id(&info->codec),
+			SPA_FORMAT_AUDIO_rate,		SPA_POD_OPT_Int(&info->rate));
 	return res;
 }
 
@@ -87,10 +87,10 @@ spa_format_audio_dsd_parse(const struct spa_pod *format, struct spa_audio_info_d
 	info->flags = 0;
 	res = spa_pod_parse_object(format,
 			SPA_TYPE_OBJECT_Format, NULL,
-			SPA_FORMAT_AUDIO_bitorder,	SPA_POD_Id(&info->bitorder),
-			SPA_FORMAT_AUDIO_interleave,	SPA_POD_Int(&info->interleave),
-			SPA_FORMAT_AUDIO_rate,		SPA_POD_Int(&info->rate),
-			SPA_FORMAT_AUDIO_channels,	SPA_POD_Int(&info->channels),
+			SPA_FORMAT_AUDIO_bitorder,	SPA_POD_OPT_Id(&info->bitorder),
+			SPA_FORMAT_AUDIO_interleave,	SPA_POD_OPT_Int(&info->interleave),
+			SPA_FORMAT_AUDIO_rate,		SPA_POD_OPT_Int(&info->rate),
+			SPA_FORMAT_AUDIO_channels,	SPA_POD_OPT_Int(&info->channels),
 			SPA_FORMAT_AUDIO_position,	SPA_POD_OPT_Pod(&position));
 	if (position == NULL ||
 	    !spa_pod_copy_array(position, SPA_TYPE_Id, info->position, SPA_AUDIO_MAX_CHANNELS))
diff --git a/spa/include/spa/param/audio/iec958.h b/spa/include/spa/param/audio/iec958.h
index fb46c561e1be4db3266de6782a9ee7c52aa5ec75..94451387fce5d6944764b94e0a6e654f4ae6fa6d 100644
--- a/spa/include/spa/param/audio/iec958.h
+++ b/spa/include/spa/param/audio/iec958.h
@@ -56,7 +56,7 @@ struct spa_audio_info_iec958 {
 	uint32_t rate;				/*< sample rate */
 };
 
-#define SPA_AUDIO_INFO_IEC958_INIT(...)		(struct spa_audio_info_iec958) { __VA_ARGS__ }
+#define SPA_AUDIO_INFO_IEC958_INIT(...)		((struct spa_audio_info_iec958) { __VA_ARGS__ })
 
 /**
  * \}
diff --git a/spa/include/spa/param/audio/raw.h b/spa/include/spa/param/audio/raw.h
index a34915c422b32e47af5ec10ef785d8100c7518a5..5690db1db0cce703d2d5895f938b46c09eacdd39 100644
--- a/spa/include/spa/param/audio/raw.h
+++ b/spa/include/spa/param/audio/raw.h
@@ -294,7 +294,7 @@ struct spa_audio_info_raw {
 	uint32_t position[SPA_AUDIO_MAX_CHANNELS];	/*< channel position from enum spa_audio_channel */
 };
 
-#define SPA_AUDIO_INFO_RAW_INIT(...)		(struct spa_audio_info_raw) { __VA_ARGS__ }
+#define SPA_AUDIO_INFO_RAW_INIT(...)		((struct spa_audio_info_raw) { __VA_ARGS__ })
 
 #define SPA_KEY_AUDIO_FORMAT		"audio.format"		/**< an audio format as string,
 								  *  Ex. "S16LE" */
@@ -311,7 +311,7 @@ struct spa_audio_info_dsp {
 	enum spa_audio_format format;		/*< format, one of the DSP formats in enum spa_audio_format_dsp */
 };
 
-#define SPA_AUDIO_INFO_DSP_INIT(...)		(struct spa_audio_info_dsp) { __VA_ARGS__ }
+#define SPA_AUDIO_INFO_DSP_INIT(...)		((struct spa_audio_info_dsp) { __VA_ARGS__ })
 
 /**
  * \}
diff --git a/spa/include/spa/param/bluetooth/audio.h b/spa/include/spa/param/bluetooth/audio.h
index f1037657c5df72c91222a0a8bdc9221afff77878..27f3197178501ce03a2469cf4b35db05f6da235d 100644
--- a/spa/include/spa/param/bluetooth/audio.h
+++ b/spa/include/spa/param/bluetooth/audio.h
@@ -58,6 +58,9 @@ enum spa_bluetooth_audio_codec {
 	/* HFP */
 	SPA_BLUETOOTH_AUDIO_CODEC_CVSD = 0x100,
 	SPA_BLUETOOTH_AUDIO_CODEC_MSBC,
+
+	/* BAP */
+	SPA_BLUETOOTH_AUDIO_CODEC_LC3 = 0x200,
 };
 
 /**
diff --git a/spa/include/spa/param/bluetooth/type-info.h b/spa/include/spa/param/bluetooth/type-info.h
index 8286b970ad2576556093b756d16fd46358e6c447..729bd58cefb9870337904d2084933577c61b5ec3 100644
--- a/spa/include/spa/param/bluetooth/type-info.h
+++ b/spa/include/spa/param/bluetooth/type-info.h
@@ -62,6 +62,8 @@ static const struct spa_type_info spa_type_bluetooth_audio_codec[] = {
 	{ 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 },
 
+	{ SPA_BLUETOOTH_AUDIO_CODEC_LC3, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "lc3", NULL },
+
 	{ 0, 0, NULL, NULL },
 };
 
diff --git a/spa/include/spa/param/latency-utils.h b/spa/include/spa/param/latency-utils.h
index 57a6828813ef285a308ac249fc6687afa6f396e3..e072574c3c72caf9ee942283f7ddf79b6a153932 100644
--- a/spa/include/spa/param/latency-utils.h
+++ b/spa/include/spa/param/latency-utils.h
@@ -50,7 +50,7 @@ struct spa_latency_info {
 	uint64_t max_ns;
 };
 
-#define SPA_LATENCY_INFO(dir,...) (struct spa_latency_info) { .direction = (dir), ## __VA_ARGS__ }
+#define SPA_LATENCY_INFO(dir,...) ((struct spa_latency_info) { .direction = (dir), ## __VA_ARGS__ })
 
 static inline int
 spa_latency_info_compare(const struct spa_latency_info *a, struct spa_latency_info *b)
@@ -146,7 +146,7 @@ struct spa_process_latency_info {
 	uint64_t ns;
 };
 
-#define SPA_PROCESS_LATENCY_INFO_INIT(...)	(struct spa_process_latency_info) { __VA_ARGS__ }
+#define SPA_PROCESS_LATENCY_INFO_INIT(...)	((struct spa_process_latency_info) { __VA_ARGS__ })
 
 static inline int
 spa_process_latency_parse(const struct spa_pod *latency, struct spa_process_latency_info *info)
diff --git a/spa/include/spa/param/param.h b/spa/include/spa/param/param.h
index 6059ee3618b49e263e67c2535fa71f1f79f4ebf2..90db8440366b00518913c71467a3c44ec5987d13 100644
--- a/spa/include/spa/param/param.h
+++ b/spa/include/spa/param/param.h
@@ -72,10 +72,12 @@ struct spa_param_info {
 	uint32_t flags;
 	uint32_t user;			/**< private user field. You can use this to keep
 					  *  state. */
-	uint32_t padding[5];
+	int32_t seq;			/**< private seq field. You can use this to keep
+					  *  state of a pending update. */
+	uint32_t padding[4];
 };
 
-#define SPA_PARAM_INFO(id,flags) (struct spa_param_info){ (id), (flags) }
+#define SPA_PARAM_INFO(id,flags) ((struct spa_param_info){ (id), (flags) })
 
 /** properties for SPA_TYPE_OBJECT_ParamBuffers */
 enum spa_param_buffers {
diff --git a/spa/include/spa/param/video/format-utils.h b/spa/include/spa/param/video/format-utils.h
index 9abf67021d032bd39ee675cb2ca5cc39ee375b99..339e7e523e68143553cc38da83aed73caf2a8ab3 100644
--- a/spa/include/spa/param/video/format-utils.h
+++ b/spa/include/spa/param/video/format-utils.h
@@ -44,10 +44,10 @@ spa_format_video_raw_parse(const struct spa_pod *format,
 {
 	return spa_pod_parse_object(format,
 		SPA_TYPE_OBJECT_Format, NULL,
-		SPA_FORMAT_VIDEO_format,		SPA_POD_Id(&info->format),
+		SPA_FORMAT_VIDEO_format,		SPA_POD_OPT_Id(&info->format),
 		SPA_FORMAT_VIDEO_modifier,		SPA_POD_OPT_Long(&info->modifier),
-		SPA_FORMAT_VIDEO_size,			SPA_POD_Rectangle(&info->size),
-		SPA_FORMAT_VIDEO_framerate,		SPA_POD_Fraction(&info->framerate),
+		SPA_FORMAT_VIDEO_size,			SPA_POD_OPT_Rectangle(&info->size),
+		SPA_FORMAT_VIDEO_framerate,		SPA_POD_OPT_Fraction(&info->framerate),
 		SPA_FORMAT_VIDEO_maxFramerate,		SPA_POD_OPT_Fraction(&info->max_framerate),
 		SPA_FORMAT_VIDEO_views,			SPA_POD_OPT_Int(&info->views),
 		SPA_FORMAT_VIDEO_interlaceMode,		SPA_POD_OPT_Id(&info->interlace_mode),
@@ -67,7 +67,7 @@ spa_format_video_dsp_parse(const struct spa_pod *format,
 {
 	return spa_pod_parse_object(format,
 		SPA_TYPE_OBJECT_Format, NULL,
-		SPA_FORMAT_VIDEO_format,		SPA_POD_Id(&info->format),
+		SPA_FORMAT_VIDEO_format,		SPA_POD_OPT_Id(&info->format),
 		SPA_FORMAT_VIDEO_modifier,		SPA_POD_OPT_Long(&info->modifier));
 }
 
diff --git a/spa/include/spa/param/video/raw.h b/spa/include/spa/param/video/raw.h
index dae4e738eb6ce11aecf289d7077376fc8ad60fc0..47fbd19b3c14f6131b6c7090924f992e4dce169d 100644
--- a/spa/include/spa/param/video/raw.h
+++ b/spa/include/spa/param/video/raw.h
@@ -206,14 +206,14 @@ struct spa_video_info_raw {
 	enum spa_video_color_primaries color_primaries;		/**< color primaries. used to convert between R'G'B' and CIE XYZ */
 };
 
-#define SPA_VIDEO_INFO_RAW_INIT(...)	(struct spa_video_info_raw) { __VA_ARGS__ }
+#define SPA_VIDEO_INFO_RAW_INIT(...)	((struct spa_video_info_raw) { __VA_ARGS__ })
 
 struct spa_video_info_dsp {
 	enum spa_video_format format;
 	int64_t modifier;
 };
 
-#define SPA_VIDEO_INFO_DSP_INIT(...)	(struct spa_video_info_dsp) { __VA_ARGS__ }
+#define SPA_VIDEO_INFO_DSP_INIT(...)	((struct spa_video_info_dsp) { __VA_ARGS__ })
 
 /**
  * \}
diff --git a/spa/include/spa/pod/builder.h b/spa/include/spa/pod/builder.h
index 624b543c401df2e7d50bd9c2df20cee332b19278..860673a7cef6d0e8f45b4f17071d8bc8acd71bfc 100644
--- a/spa/include/spa/pod/builder.h
+++ b/spa/include/spa/pod/builder.h
@@ -69,7 +69,7 @@ struct spa_pod_builder {
 	struct spa_callbacks callbacks;
 };
 
-#define SPA_POD_BUILDER_INIT(buffer,size)  (struct spa_pod_builder){ buffer, size, 0, {}, {} }
+#define SPA_POD_BUILDER_INIT(buffer,size)  ((struct spa_pod_builder){ (buffer), (size), 0, {}, {} })
 
 static inline void
 spa_pod_builder_get_state(struct spa_pod_builder *builder, struct spa_pod_builder_state *state)
@@ -143,8 +143,10 @@ static inline int spa_pod_builder_raw(struct spa_pod_builder *builder, const voi
 
 	if (offset + size > builder->size) {
 		res = -ENOSPC;
-		spa_callbacks_call_res(&builder->callbacks, struct spa_pod_builder_callbacks, res,
-				overflow, 0, offset + size);
+		if (offset <= builder->size)
+			spa_callbacks_call_res(&builder->callbacks,
+					struct spa_pod_builder_callbacks, res,
+					overflow, 0, offset + size);
 	}
 	if (res == 0 && data)
 		memcpy(SPA_PTROFF(builder->data, offset, void), data, size);
@@ -212,7 +214,7 @@ spa_pod_builder_primitive(struct spa_pod_builder *builder, const struct spa_pod
 	return res;
 }
 
-#define SPA_POD_INIT(size,type) (struct spa_pod) { size, type }
+#define SPA_POD_INIT(size,type) ((struct spa_pod) { (size), (type) })
 
 #define SPA_POD_INIT_None() SPA_POD_INIT(0, SPA_TYPE_None)
 
@@ -229,7 +231,7 @@ static inline int spa_pod_builder_child(struct spa_pod_builder *builder, uint32_
 	return spa_pod_builder_raw(builder, &p, sizeof(p));
 }
 
-#define SPA_POD_INIT_Bool(val) (struct spa_pod_bool){ { sizeof(uint32_t), SPA_TYPE_Bool }, val ? 1 : 0, 0 }
+#define SPA_POD_INIT_Bool(val) ((struct spa_pod_bool){ { sizeof(uint32_t), SPA_TYPE_Bool }, (val) ? 1 : 0, 0 })
 
 static inline int spa_pod_builder_bool(struct spa_pod_builder *builder, bool val)
 {
@@ -237,7 +239,7 @@ static inline int spa_pod_builder_bool(struct spa_pod_builder *builder, bool val
 	return spa_pod_builder_primitive(builder, &p.pod);
 }
 
-#define SPA_POD_INIT_Id(val) (struct spa_pod_id){ { sizeof(uint32_t), SPA_TYPE_Id }, (uint32_t)val, 0 }
+#define SPA_POD_INIT_Id(val) ((struct spa_pod_id){ { sizeof(uint32_t), SPA_TYPE_Id }, (val), 0 })
 
 static inline int spa_pod_builder_id(struct spa_pod_builder *builder, uint32_t val)
 {
@@ -245,7 +247,7 @@ static inline int spa_pod_builder_id(struct spa_pod_builder *builder, uint32_t v
 	return spa_pod_builder_primitive(builder, &p.pod);
 }
 
-#define SPA_POD_INIT_Int(val) (struct spa_pod_int){ { sizeof(int32_t), SPA_TYPE_Int }, (int32_t)val, 0 }
+#define SPA_POD_INIT_Int(val) ((struct spa_pod_int){ { sizeof(int32_t), SPA_TYPE_Int }, (val), 0 })
 
 static inline int spa_pod_builder_int(struct spa_pod_builder *builder, int32_t val)
 {
@@ -253,7 +255,7 @@ static inline int spa_pod_builder_int(struct spa_pod_builder *builder, int32_t v
 	return spa_pod_builder_primitive(builder, &p.pod);
 }
 
-#define SPA_POD_INIT_Long(val) (struct spa_pod_long){ { sizeof(int64_t), SPA_TYPE_Long }, (int64_t)val }
+#define SPA_POD_INIT_Long(val) ((struct spa_pod_long){ { sizeof(int64_t), SPA_TYPE_Long }, (val) })
 
 static inline int spa_pod_builder_long(struct spa_pod_builder *builder, int64_t val)
 {
@@ -261,7 +263,7 @@ static inline int spa_pod_builder_long(struct spa_pod_builder *builder, int64_t
 	return spa_pod_builder_primitive(builder, &p.pod);
 }
 
-#define SPA_POD_INIT_Float(val) (struct spa_pod_float){ { sizeof(float), SPA_TYPE_Float }, val, 0 }
+#define SPA_POD_INIT_Float(val) ((struct spa_pod_float){ { sizeof(float), SPA_TYPE_Float }, (val), 0 })
 
 static inline int spa_pod_builder_float(struct spa_pod_builder *builder, float val)
 {
@@ -269,7 +271,7 @@ static inline int spa_pod_builder_float(struct spa_pod_builder *builder, float v
 	return spa_pod_builder_primitive(builder, &p.pod);
 }
 
-#define SPA_POD_INIT_Double(val) (struct spa_pod_double){ { sizeof(double), SPA_TYPE_Double }, val }
+#define SPA_POD_INIT_Double(val) ((struct spa_pod_double){ { sizeof(double), SPA_TYPE_Double }, (val) })
 
 static inline int spa_pod_builder_double(struct spa_pod_builder *builder, double val)
 {
@@ -277,7 +279,7 @@ static inline int spa_pod_builder_double(struct spa_pod_builder *builder, double
 	return spa_pod_builder_primitive(builder, &p.pod);
 }
 
-#define SPA_POD_INIT_String(len) (struct spa_pod_string){ { len, SPA_TYPE_String } }
+#define SPA_POD_INIT_String(len) ((struct spa_pod_string){ { (len), SPA_TYPE_String } })
 
 static inline int
 spa_pod_builder_write_string(struct spa_pod_builder *builder, const char *str, uint32_t len)
@@ -307,7 +309,7 @@ static inline int spa_pod_builder_string(struct spa_pod_builder *builder, const
 	return spa_pod_builder_string_len(builder, str ? str : "", len);
 }
 
-#define SPA_POD_INIT_Bytes(len) (struct spa_pod_bytes){ { len, SPA_TYPE_Bytes } }
+#define SPA_POD_INIT_Bytes(len) ((struct spa_pod_bytes){ { (len), SPA_TYPE_Bytes } })
 
 static inline int
 spa_pod_builder_bytes(struct spa_pod_builder *builder, const void *bytes, uint32_t len)
@@ -327,7 +329,7 @@ spa_pod_builder_reserve_bytes(struct spa_pod_builder *builder, uint32_t len)
 	return SPA_POD_BODY(spa_pod_builder_deref(builder, offset));
 }
 
-#define SPA_POD_INIT_Pointer(type,value) (struct spa_pod_pointer){ { sizeof(struct spa_pod_pointer_body), SPA_TYPE_Pointer }, { type, 0, value } }
+#define SPA_POD_INIT_Pointer(type,value) ((struct spa_pod_pointer){ { sizeof(struct spa_pod_pointer_body), SPA_TYPE_Pointer }, { (type), 0, (value) } })
 
 static inline int
 spa_pod_builder_pointer(struct spa_pod_builder *builder, uint32_t type, const void *val)
@@ -336,7 +338,7 @@ spa_pod_builder_pointer(struct spa_pod_builder *builder, uint32_t type, const vo
 	return spa_pod_builder_primitive(builder, &p.pod);
 }
 
-#define SPA_POD_INIT_Fd(fd) (struct spa_pod_fd){ { sizeof(int64_t), SPA_TYPE_Fd }, fd }
+#define SPA_POD_INIT_Fd(fd) ((struct spa_pod_fd){ { sizeof(int64_t), SPA_TYPE_Fd }, (fd) })
 
 static inline int spa_pod_builder_fd(struct spa_pod_builder *builder, int64_t fd)
 {
@@ -344,7 +346,7 @@ static inline int spa_pod_builder_fd(struct spa_pod_builder *builder, int64_t fd
 	return spa_pod_builder_primitive(builder, &p.pod);
 }
 
-#define SPA_POD_INIT_Rectangle(val) (struct spa_pod_rectangle){ { sizeof(struct spa_rectangle), SPA_TYPE_Rectangle }, val }
+#define SPA_POD_INIT_Rectangle(val) ((struct spa_pod_rectangle){ { sizeof(struct spa_rectangle), SPA_TYPE_Rectangle }, (val) })
 
 static inline int
 spa_pod_builder_rectangle(struct spa_pod_builder *builder, uint32_t width, uint32_t height)
@@ -353,7 +355,7 @@ spa_pod_builder_rectangle(struct spa_pod_builder *builder, uint32_t width, uint3
 	return spa_pod_builder_primitive(builder, &p.pod);
 }
 
-#define SPA_POD_INIT_Fraction(val) (struct spa_pod_fraction){ { sizeof(struct spa_fraction), SPA_TYPE_Fraction }, val }
+#define SPA_POD_INIT_Fraction(val) ((struct spa_pod_fraction){ { sizeof(struct spa_fraction), SPA_TYPE_Fraction }, (val) })
 
 static inline int
 spa_pod_builder_fraction(struct spa_pod_builder *builder, uint32_t num, uint32_t denom)
@@ -389,12 +391,12 @@ spa_pod_builder_array(struct spa_pod_builder *builder,
 }
 
 #define SPA_POD_INIT_CHOICE_BODY(type, flags, child_size, child_type)				\
-	(struct spa_pod_choice_body) { type, flags, { child_size, child_type }}
+	((struct spa_pod_choice_body) { (type), (flags), { (child_size), (child_type) }})
 
 #define SPA_POD_INIT_Choice(type, ctype, child_type, n_vals, ...)				\
-	(struct { struct spa_pod_choice choice; ctype vals[n_vals];})				\
-	{ { { n_vals * sizeof(ctype) + sizeof(struct spa_pod_choice_body), SPA_TYPE_Choice },	\
-		{ type, 0, { sizeof(ctype), child_type } } }, { __VA_ARGS__ } }
+	((struct { struct spa_pod_choice choice; ctype vals[(n_vals)];})			\
+	{ { { (n_vals) * sizeof(ctype) + sizeof(struct spa_pod_choice_body), SPA_TYPE_Choice },	\
+		{ (type), 0, { sizeof(ctype), (child_type) } } }, { __VA_ARGS__ } })
 
 static inline int
 spa_pod_builder_push_choice(struct spa_pod_builder *builder, struct spa_pod_frame *frame,
@@ -409,7 +411,7 @@ spa_pod_builder_push_choice(struct spa_pod_builder *builder, struct spa_pod_fram
 	return res;
 }
 
-#define SPA_POD_INIT_Struct(size) (struct spa_pod_struct){ { size, SPA_TYPE_Struct } }
+#define SPA_POD_INIT_Struct(size) ((struct spa_pod_struct){ { (size), SPA_TYPE_Struct } })
 
 static inline int
 spa_pod_builder_push_struct(struct spa_pod_builder *builder, struct spa_pod_frame *frame)
@@ -421,7 +423,7 @@ spa_pod_builder_push_struct(struct spa_pod_builder *builder, struct spa_pod_fram
 	return res;
 }
 
-#define SPA_POD_INIT_Object(size,type,id,...)	(struct spa_pod_object){ { size, SPA_TYPE_Object }, { type, id }, ##__VA_ARGS__ }
+#define SPA_POD_INIT_Object(size,type,id,...)	((struct spa_pod_object){ { (size), SPA_TYPE_Object }, { (type), (id) }, ##__VA_ARGS__ })
 
 static inline int
 spa_pod_builder_push_object(struct spa_pod_builder *builder, struct spa_pod_frame *frame,
@@ -436,7 +438,7 @@ spa_pod_builder_push_object(struct spa_pod_builder *builder, struct spa_pod_fram
 }
 
 #define SPA_POD_INIT_Prop(key,flags,size,type)	\
-	(struct spa_pod_prop){ key, flags, { size, type } }
+	((struct spa_pod_prop){ (key), (flags), { (size), (type) } })
 
 static inline int
 spa_pod_builder_prop(struct spa_pod_builder *builder, uint32_t key, uint32_t flags)
@@ -446,7 +448,7 @@ spa_pod_builder_prop(struct spa_pod_builder *builder, uint32_t key, uint32_t fla
 }
 
 #define SPA_POD_INIT_Sequence(size,unit)	\
-	(struct spa_pod_sequence){ { size, SPA_TYPE_Sequence}, {unit, 0 } }
+	((struct spa_pod_sequence){ { (size), SPA_TYPE_Sequence}, {(unit), 0 } })
 
 static inline int
 spa_pod_builder_push_sequence(struct spa_pod_builder *builder, struct spa_pod_frame *frame, uint32_t unit)
@@ -650,26 +652,29 @@ static inline int spa_pod_builder_add(struct spa_pod_builder *builder, ...)
 
 #define spa_pod_builder_add_object(b,type,id,...)				\
 ({										\
+	struct spa_pod_builder *_b = (b);					\
 	struct spa_pod_frame _f;						\
-	spa_pod_builder_push_object(b, &_f, type, id);				\
-	spa_pod_builder_add(b, ##__VA_ARGS__, 0);				\
-	spa_pod_builder_pop(b, &_f);						\
+	spa_pod_builder_push_object(_b, &_f, type, id);				\
+	spa_pod_builder_add(_b, ##__VA_ARGS__, 0);				\
+	spa_pod_builder_pop(_b, &_f);						\
 })
 
 #define spa_pod_builder_add_struct(b,...)					\
 ({										\
+	struct spa_pod_builder *_b = (b);					\
 	struct spa_pod_frame _f;						\
-	spa_pod_builder_push_struct(b, &_f);					\
-	spa_pod_builder_add(b, ##__VA_ARGS__, NULL);				\
-	spa_pod_builder_pop(b, &_f);						\
+	spa_pod_builder_push_struct(_b, &_f);					\
+	spa_pod_builder_add(_b, ##__VA_ARGS__, NULL);				\
+	spa_pod_builder_pop(_b, &_f);						\
 })
 
 #define spa_pod_builder_add_sequence(b,unit,...)				\
 ({										\
+	struct spa_pod_builder *_b = (b);					\
 	struct spa_pod_frame _f;						\
-	spa_pod_builder_push_sequence(b, &_f, unit);				\
-	spa_pod_builder_add(b, ##__VA_ARGS__, 0, 0);				\
-	spa_pod_builder_pop(b, &_f);						\
+	spa_pod_builder_push_sequence(_b, &_f, unit);				\
+	spa_pod_builder_add(_b, ##__VA_ARGS__, 0, 0);				\
+	spa_pod_builder_pop(_b, &_f);						\
 })
 
 /** Copy a pod structure */
diff --git a/spa/include/spa/pod/command.h b/spa/include/spa/pod/command.h
index 0c292eeae5e2aa3fab0e9fe174b1028e6b41c8c1..3cd30a0e4e23c5e8e7cab37c14c90005027b168f 100644
--- a/spa/include/spa/pod/command.h
+++ b/spa/include/spa/pod/command.h
@@ -47,12 +47,12 @@ struct spa_command {
 };
 
 #define SPA_COMMAND_TYPE(cmd)		((cmd)->body.body.type)
-#define SPA_COMMAND_ID(cmd,type)	(SPA_COMMAND_TYPE(cmd) == type ? \
+#define SPA_COMMAND_ID(cmd,type)	(SPA_COMMAND_TYPE(cmd) == (type) ? \
 						(cmd)->body.body.id : SPA_ID_INVALID)
 
-#define SPA_COMMAND_INIT_FULL(t,size,type,id,...) (t)			\
-	{ { size, SPA_TYPE_Object },					\
-	  { { type, id }, ##__VA_ARGS__ } }				\
+#define SPA_COMMAND_INIT_FULL(t,size,type,id,...) ((t)			\
+	{ { (size), SPA_TYPE_Object },					\
+	  { { (type), (id) }, ##__VA_ARGS__ } })
 
 #define SPA_COMMAND_INIT(type,id)					\
 	SPA_COMMAND_INIT_FULL(struct spa_command,			\
diff --git a/spa/include/spa/pod/event.h b/spa/include/spa/pod/event.h
index 2ecfb0eabb643816ef1d5596c079073929967c61..2328af778b9f0a3433a497a7177d48ec339952a6 100644
--- a/spa/include/spa/pod/event.h
+++ b/spa/include/spa/pod/event.h
@@ -46,12 +46,12 @@ struct spa_event {
 };
 
 #define SPA_EVENT_TYPE(ev)	((ev)->body.body.type)
-#define SPA_EVENT_ID(ev,type)	(SPA_EVENT_TYPE(ev) == type ? \
+#define SPA_EVENT_ID(ev,type)	(SPA_EVENT_TYPE(ev) == (type) ? \
 					(ev)->body.body.id : SPA_ID_INVALID)
 
-#define SPA_EVENT_INIT_FULL(t,size,type,id,...) (t)			\
-	{ { size, SPA_TYPE_OBJECT },					\
-	  { { type, id }, ##__VA_ARGS__ } }				\
+#define SPA_EVENT_INIT_FULL(t,size,type,id,...) ((t)			\
+	{ { (size), SPA_TYPE_OBJECT },					\
+	  { { (type), (id) }, ##__VA_ARGS__ } })			\
 
 #define SPA_EVENT_INIT(type,id)						\
 	SPA_EVENT_INIT_FULL(struct spa_event,				\
diff --git a/spa/include/spa/pod/parser.h b/spa/include/spa/pod/parser.h
index c0a3099da64fd5f6a3f1afc4d2b36252187e3501..e0546751f455bf2a1e5fc297aa487a87b7d77e6f 100644
--- a/spa/include/spa/pod/parser.h
+++ b/spa/include/spa/pod/parser.h
@@ -53,7 +53,7 @@ struct spa_pod_parser {
 	struct spa_pod_parser_state state;
 };
 
-#define SPA_POD_PARSER_INIT(buffer,size)  (struct spa_pod_parser){ buffer, size, 0, {} }
+#define SPA_POD_PARSER_INIT(buffer,size)  ((struct spa_pod_parser){ (buffer), (size), 0, {} })
 
 static inline void spa_pod_parser_init(struct spa_pod_parser *parser,
 				       const void *data, uint32_t size)
@@ -82,12 +82,20 @@ spa_pod_parser_reset(struct spa_pod_parser *parser, struct spa_pod_parser_state
 static inline struct spa_pod *
 spa_pod_parser_deref(struct spa_pod_parser *parser, uint32_t offset, uint32_t size)
 {
-	if (offset + 8 <= size) {
-		struct spa_pod *pod = SPA_PTROFF(parser->data, offset, struct spa_pod);
-		if (offset + SPA_POD_SIZE(pod) <= size)
-			return pod;
+	/* Cast to uint64_t to avoid wraparound.  Add 8 for the pod itself. */
+	const uint64_t long_offset = (uint64_t)offset + 8;
+	if (long_offset <= size && (offset & 7) == 0) {
+		/* Use void* because creating a misaligned pointer is undefined. */
+		void *pod = SPA_PTROFF(parser->data, offset, void);
+		/*
+		 * Check that the pointer is aligned and that the size (rounded
+		 * to the next multiple of 8) is in bounds.
+		 */
+		if (SPA_IS_ALIGNED(pod, __alignof__(struct spa_pod)) &&
+		    long_offset + SPA_ROUND_UP_N((uint64_t)SPA_POD_BODY_SIZE(pod), 8) <= size)
+			return (struct spa_pod *)pod;
 	}
-        return NULL;
+	return NULL;
 }
 
 static inline struct spa_pod *spa_pod_parser_frame(struct spa_pod_parser *parser, struct spa_pod_frame *frame)
@@ -285,10 +293,15 @@ static inline bool spa_pod_parser_can_collect(const struct spa_pod *pod, char ty
 	if (pod == NULL)
 		return false;
 
-	if (spa_pod_is_choice(pod) &&
-	    SPA_POD_CHOICE_TYPE(pod) == SPA_CHOICE_None &&
-	    spa_pod_parser_can_collect(SPA_POD_CHOICE_CHILD(pod), type))
-		return true;
+	if (SPA_POD_TYPE(pod) == SPA_TYPE_Choice) {
+		if (!spa_pod_is_choice(pod))
+			return false;
+		if (type == 'V')
+			return true;
+		if (SPA_POD_CHOICE_TYPE(pod) != SPA_CHOICE_None)
+			return false;
+		pod = SPA_POD_CHOICE_CHILD(pod);
+	}
 
 	switch (type) {
 	case 'P':
@@ -328,7 +341,6 @@ static inline bool spa_pod_parser_can_collect(const struct spa_pod *pod, char ty
 	case 'O':
 		return spa_pod_is_object(pod) || spa_pod_is_none(pod);
 	case 'V':
-		return spa_pod_is_choice(pod);
 	default:
 		return false;
 	}
@@ -355,7 +367,7 @@ do {											\
 		break;									\
 	case 's':									\
 		*va_arg(args, char**) =							\
-			(pod == NULL || (SPA_POD_TYPE(pod) == SPA_TYPE_None)		\
+			((pod) == NULL || (SPA_POD_TYPE(pod) == SPA_TYPE_None)		\
 				? NULL							\
 				: (char *)SPA_POD_CONTENTS(struct spa_pod_string, pod));	\
 		break;									\
@@ -407,8 +419,8 @@ do {											\
 	{										\
 		const struct spa_pod **d = va_arg(args, const struct spa_pod**);	\
 		if (d)									\
-			*d = (pod == NULL || (SPA_POD_TYPE(pod) == SPA_TYPE_None)	\
-				? NULL : pod);						\
+			*d = ((pod) == NULL || (SPA_POD_TYPE(pod) == SPA_TYPE_None)	\
+				? NULL : (pod));						\
 		break;									\
 	}										\
 	default:									\
@@ -493,8 +505,7 @@ static inline int spa_pod_parser_getv(struct spa_pod_parser *parser, va_list arg
 			}
 			SPA_POD_PARSER_SKIP(*format, args);
 		} else {
-			if (pod->type == SPA_TYPE_Choice && *format != 'V' &&
-			    SPA_POD_CHOICE_TYPE(pod) == SPA_CHOICE_None)
+			if (pod->type == SPA_TYPE_Choice && *format != 'V')
 				pod = SPA_POD_CHOICE_CHILD(pod);
 
 			SPA_POD_PARSER_COLLECT(pod, *format, args);
diff --git a/spa/include/spa/pod/pod.h b/spa/include/spa/pod/pod.h
index 2d2eaad6a18b4df69657844fdeb91f53c68566eb..1864b19b9f4c50bd2ba584ff427b9f0468d2fdc9 100644
--- a/spa/include/spa/pod/pod.h
+++ b/spa/include/spa/pod/pod.h
@@ -39,7 +39,7 @@ extern "C" {
 
 #define SPA_POD_BODY_SIZE(pod)			(((struct spa_pod*)(pod))->size)
 #define SPA_POD_TYPE(pod)			(((struct spa_pod*)(pod))->type)
-#define SPA_POD_SIZE(pod)			(sizeof(struct spa_pod) + SPA_POD_BODY_SIZE(pod))
+#define SPA_POD_SIZE(pod)			((uint64_t)sizeof(struct spa_pod) + SPA_POD_BODY_SIZE(pod))
 #define SPA_POD_CONTENTS_SIZE(type,pod)		(SPA_POD_SIZE(pod)-sizeof(type))
 
 #define SPA_POD_CONTENTS(type,pod)		SPA_PTROFF((pod),sizeof(type),void)
@@ -52,7 +52,7 @@ struct spa_pod {
 	uint32_t type;		/* a basic id of enum spa_type */
 };
 
-#define SPA_POD_VALUE(type,pod)			(((type*)pod)->value)
+#define SPA_POD_VALUE(type,pod)			(((type*)(pod))->value)
 
 struct spa_pod_bool {
 	struct spa_pod pod;
diff --git a/spa/include/spa/support/log-impl.h b/spa/include/spa/support/log-impl.h
index 124153d073dfb39a722c87e626ee77f48705d893..e1ee21dd86f317bfea7a40babe71c5d39c9f16cc 100644
--- a/spa/include/spa/support/log-impl.h
+++ b/spa/include/spa/support/log-impl.h
@@ -121,7 +121,7 @@ struct {					\
 
 #define SPA_LOG_IMPL_INIT(name)				\
 	{ { { SPA_TYPE_INTERFACE_Log, SPA_VERSION_LOG,	\
-	      SPA_CALLBACKS_INIT(&name.methods, &name) },	\
+	      SPA_CALLBACKS_INIT(&(name).methods, &(name)) },	\
 	    SPA_LOG_LEVEL_INFO,	},			\
 	  { SPA_VERSION_LOG_METHODS,			\
 	    spa_log_impl_log,				\
diff --git a/spa/include/spa/support/log.h b/spa/include/spa/support/log.h
index 1475eedf2416bb71f06b52a608503b7470eb6bad..008fb34175fea0e49ff273181cb48d3a7c09b220 100644
--- a/spa/include/spa/support/log.h
+++ b/spa/include/spa/support/log.h
@@ -212,7 +212,7 @@ struct spa_log_methods {
 
 
 #define SPA_LOG_TOPIC(v, t) \
-   (struct spa_log_topic){ .version = v, .topic = (t)}
+   (struct spa_log_topic){ .version = (v), .topic = (t)}
 
 #define spa_log_topic_init(l, topic)				\
 do {								\
@@ -231,10 +231,10 @@ do {								\
 ({								\
 	struct spa_log *_log = l;				\
 	enum spa_log_level _lev = _log ? _log->level : SPA_LOG_LEVEL_NONE;		\
-	struct spa_log_topic *_t = (struct spa_log_topic *)topic; \
+	struct spa_log_topic *_t = (struct spa_log_topic *)(topic); \
 	if (_t && _t->has_custom_level)							\
 		_lev = _t->level;				\
-	_lev >= lev;						\
+	_lev >= (lev);						\
 })
 
 /* Transparently calls to version 0 log if v1 is not supported */
@@ -269,26 +269,32 @@ do {								\
 	}								\
 })
 
+#define spa_logt_lev(l,lev,t,...)					\
+	spa_log_logt(l,lev,t,__FILE__,__LINE__,__func__,__VA_ARGS__)
+
+#define spa_log_lev(l,lev,...)					\
+	spa_logt_lev(l,lev,SPA_LOG_TOPIC_DEFAULT,__VA_ARGS__)
+
 #define spa_log_log(l,lev,...)					\
 	spa_log_logt(l,lev,SPA_LOG_TOPIC_DEFAULT,__VA_ARGS__)
 
 #define spa_log_logv(l,lev,...)					\
 	spa_log_logtv(l,lev,SPA_LOG_TOPIC_DEFAULT,__VA_ARGS__)
 
-#define spa_log_error(l,...)	spa_log_log(l,SPA_LOG_LEVEL_ERROR,__FILE__,__LINE__,__func__,__VA_ARGS__)
-#define spa_log_warn(l,...)	spa_log_log(l,SPA_LOG_LEVEL_WARN,__FILE__,__LINE__,__func__,__VA_ARGS__)
-#define spa_log_info(l,...)	spa_log_log(l,SPA_LOG_LEVEL_INFO,__FILE__,__LINE__,__func__,__VA_ARGS__)
-#define spa_log_debug(l,...)	spa_log_log(l,SPA_LOG_LEVEL_DEBUG,__FILE__,__LINE__,__func__,__VA_ARGS__)
-#define spa_log_trace(l,...)	spa_log_log(l,SPA_LOG_LEVEL_TRACE,__FILE__,__LINE__,__func__,__VA_ARGS__)
+#define spa_log_error(l,...)	spa_log_lev(l,SPA_LOG_LEVEL_ERROR,__VA_ARGS__)
+#define spa_log_warn(l,...)	spa_log_lev(l,SPA_LOG_LEVEL_WARN,__VA_ARGS__)
+#define spa_log_info(l,...)	spa_log_lev(l,SPA_LOG_LEVEL_INFO,__VA_ARGS__)
+#define spa_log_debug(l,...)	spa_log_lev(l,SPA_LOG_LEVEL_DEBUG,__VA_ARGS__)
+#define spa_log_trace(l,...)	spa_log_lev(l,SPA_LOG_LEVEL_TRACE,__VA_ARGS__)
 
-#define spa_logt_error(l,t,...)	spa_log_logt(l,SPA_LOG_LEVEL_ERROR,t,__FILE__,__LINE__,__func__,__VA_ARGS__)
-#define spa_logt_warn(l,t,...)	spa_log_logt(l,SPA_LOG_LEVEL_WARN,t,__FILE__,__LINE__,__func__,__VA_ARGS__)
-#define spa_logt_info(l,t,...)	spa_log_logt(l,SPA_LOG_LEVEL_INFO,t,__FILE__,__LINE__,__func__,__VA_ARGS__)
-#define spa_logt_debug(l,t,...)	spa_log_logt(l,SPA_LOG_LEVEL_DEBUG,t,__FILE__,__LINE__,__func__,__VA_ARGS__)
-#define spa_logt_trace(l,t,...)	spa_log_logt(l,SPA_LOG_LEVEL_TRACE,t,__FILE__,__LINE__,__func__,__VA_ARGS__)
+#define spa_logt_error(l,t,...)	spa_logt_lev(l,SPA_LOG_LEVEL_ERROR,t,__VA_ARGS__)
+#define spa_logt_warn(l,t,...)	spa_logt_lev(l,SPA_LOG_LEVEL_WARN,t,__VA_ARGS__)
+#define spa_logt_info(l,t,...)	spa_logt_lev(l,SPA_LOG_LEVEL_INFO,t,__VA_ARGS__)
+#define spa_logt_debug(l,t,...)	spa_logt_lev(l,SPA_LOG_LEVEL_DEBUG,t,__VA_ARGS__)
+#define spa_logt_trace(l,t,...)	spa_logt_lev(l,SPA_LOG_LEVEL_TRACE,t,__VA_ARGS__)
 
 #ifndef FASTPATH
-#define spa_log_trace_fp(l,...)	spa_log_log(l,SPA_LOG_LEVEL_TRACE,__FILE__,__LINE__,__func__,__VA_ARGS__)
+#define spa_log_trace_fp(l,...)	spa_log_lev(l,SPA_LOG_LEVEL_TRACE,__VA_ARGS__)
 #else
 #define spa_log_trace_fp(l,...)
 #endif
@@ -296,18 +302,16 @@ do {								\
 #define spa_log_hexdump(l,lev,indent,data,len)						\
 ({											\
 	char str[512];									\
-	uint8_t *buf = (uint8_t *)data;							\
-	size_t i;									\
+	uint8_t *buf = (uint8_t *)(data);						\
+	size_t i, j = (len);								\
 	int pos = 0;									\
 											\
-	for (i = 0; i < len; i++) {							\
+	for (i = 0; i < j; i++) {							\
 		if (i % 16 == 0)							\
 			pos = 0;							\
 		pos += sprintf(str + pos, "%02x ", buf[i]);				\
-		if (i % 16 == 15 || i == len - 1) {					\
-			spa_log_log(l,lev,__FILE__,__LINE__,__func__,			\
-			            "%*s" "%s",indent,"", str);				\
-		}									\
+		if (i % 16 == 15 || i == j - 1)						\
+			spa_log_lev(l,lev, "%*s" "%s",indent,"", str);			\
 	}										\
 })
 
diff --git a/spa/include/spa/support/plugin.h b/spa/include/spa/support/plugin.h
index e66bdc99760809c00cf2f5c499aac77f89d02a4d..80cadda61ee49de149f15994d61b83b34cb12594 100644
--- a/spa/include/spa/support/plugin.h
+++ b/spa/include/spa/support/plugin.h
@@ -105,7 +105,7 @@ static inline void *spa_support_find(const struct spa_support *support,
 	return NULL;
 }
 
-#define SPA_SUPPORT_INIT(type,data) (struct spa_support) { (type), (data) }
+#define SPA_SUPPORT_INIT(type,data) ((struct spa_support) { (type), (data) })
 
 struct spa_handle_factory {
 	/** The version of this structure */
diff --git a/spa/include/spa/support/system.h b/spa/include/spa/support/system.h
index 8076ceb4bbc71da6d46917750be6d0363595efdf..79127d9062cc9d089914aef0ce9b9d9664dd970c 100644
--- a/spa/include/spa/support/system.h
+++ b/spa/include/spa/support/system.h
@@ -119,7 +119,7 @@ struct spa_system_methods {
 
 #define spa_system_method_r(o,method,version,...)			\
 ({									\
-	int _res = -ENOTSUP;						\
+	volatile int _res = -ENOTSUP;					\
 	struct spa_system *_o = o;					\
 	spa_interface_call_res(&_o->iface,				\
 			struct spa_system_methods, _res,		\
diff --git a/spa/include/spa/utils/defs.h b/spa/include/spa/utils/defs.h
index cf15cc2446cd2fd7d7850aaa0960c7a48799afb1..c602c9348827805dbafc63bf2732e0d5df833158 100644
--- a/spa/include/spa/utils/defs.h
+++ b/spa/include/spa/utils/defs.h
@@ -27,8 +27,18 @@
 
 #ifdef __cplusplus
 extern "C" {
+# if __cplusplus >= 201103L
+#  define SPA_STATIC_ASSERT static_assert
+# endif
 #else
-#include <stdbool.h>
+# include <stdbool.h>
+# if __STDC_VERSION__ >= 201112L
+#  define SPA_STATIC_ASSERT _Static_assert
+# endif
+#endif
+#ifndef SPA_STATIC_ASSERT
+#define SPA_STATIC_ASSERT(a, b)						\
+	((void)sizeof(struct { int spa_static_assertion_failed : 2 * !!(a) - 1; }))
 #endif
 #include <inttypes.h>
 #include <signal.h>
@@ -72,10 +82,19 @@ extern "C" {
 #endif
 
 #define SPA_FLAG_MASK(field,mask,flag)	(((field) & (mask)) == (flag))
-#define SPA_FLAG_IS_SET(field,flag)	SPA_FLAG_MASK(field,flag,flag)
+#define SPA_FLAG_IS_SET(field,flag)	SPA_FLAG_MASK(field, flag, flag)
+
 #define SPA_FLAG_SET(field,flag)	((field) |= (flag))
-#define SPA_FLAG_CLEAR(field,flag)	((field) &= ~(flag))
-#define SPA_FLAG_UPDATE(field,flag,val)	((val) ? SPA_FLAG_SET(field,flag) : SPA_FLAG_CLEAR(field,flag))
+#define SPA_FLAG_CLEAR(field, flag)					\
+({									\
+	SPA_STATIC_ASSERT(__builtin_constant_p(flag) ?			\
+	              (__typeof__(flag))(__typeof__(field))(__typeof__(flag))(flag) == (flag) : \
+		      sizeof(field) >= sizeof(flag),			\
+			"truncation problem when masking " #field	\
+			" with ~" #flag);				\
+	((field) &= ~(__typeof__(field))(flag));			\
+})
+#define SPA_FLAG_UPDATE(field,flag,val)	((val) ? SPA_FLAG_SET((field),(flag)) : SPA_FLAG_CLEAR((field),(flag)))
 
 enum spa_direction {
 	SPA_DIRECTION_INPUT = 0,
@@ -84,25 +103,25 @@ enum spa_direction {
 
 #define SPA_DIRECTION_REVERSE(d)	((d) ^ 1)
 
-#define SPA_RECTANGLE(width,height) (struct spa_rectangle){ width, height }
+#define SPA_RECTANGLE(width,height) ((struct spa_rectangle){ (width), (height) })
 struct spa_rectangle {
 	uint32_t width;
 	uint32_t height;
 };
 
-#define SPA_POINT(x,y) (struct spa_point){ x, y }
+#define SPA_POINT(x,y) ((struct spa_point){ (x), (y) })
 struct spa_point {
 	int32_t x;
 	int32_t y;
 };
 
-#define SPA_REGION(x,y,width,height) (struct spa_region){ SPA_POINT(x,y), SPA_RECTANGLE(width,height) }
+#define SPA_REGION(x,y,width,height) ((struct spa_region){ SPA_POINT(x,y), SPA_RECTANGLE(width,height) })
 struct spa_region {
 	struct spa_point position;
 	struct spa_rectangle size;
 };
 
-#define SPA_FRACTION(num,denom) (struct spa_fraction){ num, denom }
+#define SPA_FRACTION(num,denom) ((struct spa_fraction){ (num), (denom) })
 struct spa_fraction {
 	uint32_t num;
 	uint32_t denom;
@@ -120,23 +139,26 @@ struct spa_fraction {
  * ```
  */
 #define SPA_FOR_EACH_ELEMENT(arr, ptr) \
-	for (ptr = arr; (void*)ptr < SPA_PTROFF(arr, sizeof(arr), void); ptr++)
+	for ((ptr) = arr; (void*)(ptr) < SPA_PTROFF(arr, sizeof(arr), void); (ptr)++)
+
+#define SPA_FOR_EACH_ELEMENT_VAR(arr, var) \
+	for (__typeof__((arr)[0])* (var) = arr; (void*)(var) < SPA_PTROFF(arr, sizeof(arr), void); (var)++)
 
 #define SPA_ABS(a)			\
 ({					\
 	__typeof__(a) _a = (a);		\
 	SPA_LIKELY(_a >= 0) ? _a : -_a;	\
 })
-#define SPA_MIN(a,b)		\
-({				\
-	__typeof__(a) _min_a = (a);	\
-	__typeof__(b) _min_b = (b);	\
+#define SPA_MIN(a,b)					\
+({							\
+	__typeof__(a) _min_a = (a);			\
+	__typeof__(b) _min_b = (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);	\
+#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;	\
 })
 #define SPA_CLAMP(v,low,high)				\
@@ -156,7 +178,7 @@ struct spa_fraction {
 #define SPA_SWAP(a,b)					\
 ({							\
 	__typeof__(a) _t = (a);				\
-	a = b; b = _t;					\
+	(a) = b; (b) = _t;				\
 })
 
 #define SPA_TYPECHECK(type,x)		\
@@ -180,7 +202,7 @@ struct spa_fraction {
 #define SPA_MEMBER(b,o,t) SPA_PTROFF(b,o,t)
 #define SPA_MEMBER_ALIGN(b,o,a,t) SPA_PTROFF_ALIGN(b,o,a,t)
 
-#define SPA_CONTAINER_OF(p,t,m) ((t*)((uintptr_t)p - offsetof(t,m)))
+#define SPA_CONTAINER_OF(p,t,m) ((t*)((uintptr_t)(p) - offsetof(t,m)))
 
 #define SPA_PTRDIFF(p1,p2) ((intptr_t)(p1) - (intptr_t)(p2))
 
@@ -194,7 +216,7 @@ struct spa_fraction {
 #define SPA_IDX_INVALID  ((unsigned int)-1)
 #define SPA_ID_INVALID  ((uint32_t)0xffffffff)
 
-#define SPA_NSEC_PER_SEC  (1000000000ll)
+#define SPA_NSEC_PER_SEC  (1000000000LL)
 #define SPA_NSEC_PER_MSEC (1000000ll)
 #define SPA_NSEC_PER_USEC (1000ll)
 #define SPA_USEC_PER_SEC  (1000000ll)
@@ -236,11 +258,21 @@ struct spa_fraction {
 #define SPA_RESTRICT
 #endif
 
-#define SPA_ROUND_DOWN(num,value)	((num) - ((num) % (value)))
-#define SPA_ROUND_UP(num,value)		((((num) + (value) - 1) / (value)) * (value))
+#define SPA_ROUND_DOWN(num,value)		\
+({						\
+	__typeof__(num) _num = (num);		\
+	((_num) - ((_num) % (value)));		\
+})
+#define SPA_ROUND_UP(num,value)			\
+({						\
+	__typeof__(value) _v = (value);		\
+	((((num) + (_v) - 1) / (_v)) * (_v));	\
+})
+
+#define SPA_ROUND_MASK(num,mask)	((__typeof__(num))((mask)-1))
 
-#define SPA_ROUND_DOWN_N(num,align)	((num) & ~((align) - 1))
-#define SPA_ROUND_UP_N(num,align)	SPA_ROUND_DOWN_N((num) + ((align) - 1),align)
+#define SPA_ROUND_DOWN_N(num,align)	((num) & ~SPA_ROUND_MASK(num, align))
+#define SPA_ROUND_UP_N(num,align)	((((num)-1) | SPA_ROUND_MASK(num, align))+1)
 
 #define SPA_PTR_ALIGNMENT(p,align)	((intptr_t)(p) & ((align)-1))
 #define SPA_IS_ALIGNED(p,align)		(SPA_PTR_ALIGNMENT(p,align) == 0)
diff --git a/spa/include/spa/utils/dict.h b/spa/include/spa/utils/dict.h
index 558c6fd016f3c60167c8f60da3310f2023bf8fef..f9a0b5b6b55a813215a4f39047b41a5a082e1990 100644
--- a/spa/include/spa/utils/dict.h
+++ b/spa/include/spa/utils/dict.h
@@ -48,7 +48,7 @@ struct spa_dict_item {
 	const char *value;
 };
 
-#define SPA_DICT_ITEM_INIT(key,value) (struct spa_dict_item) { key, value }
+#define SPA_DICT_ITEM_INIT(key,value) ((struct spa_dict_item) { (key), (value) })
 
 struct spa_dict {
 #define SPA_DICT_FLAG_SORTED	(1<<0)		/**< items are sorted */
@@ -57,8 +57,8 @@ struct spa_dict {
 	const struct spa_dict_item *items;
 };
 
-#define SPA_DICT_INIT(items,n_items) (struct spa_dict) { 0, n_items, items }
-#define SPA_DICT_INIT_ARRAY(items) (struct spa_dict) { 0, SPA_N_ELEMENTS(items), items }
+#define SPA_DICT_INIT(items,n_items) ((struct spa_dict) { 0, (n_items), (items) })
+#define SPA_DICT_INIT_ARRAY(items) ((struct spa_dict) { 0, SPA_N_ELEMENTS(items), (items) })
 
 #define spa_dict_for_each(item, dict)				\
 	for ((item) = (dict)->items;				\
diff --git a/spa/include/spa/utils/hook.h b/spa/include/spa/utils/hook.h
index 953b97445ed05f0986358af25f828693db695467..edcb7164323275ce3d3e7ac565fd6bcbdeb104dd 100644
--- a/spa/include/spa/utils/hook.h
+++ b/spa/include/spa/utils/hook.h
@@ -142,7 +142,7 @@ struct spa_callbacks {
  * Initialize the set of functions \a funcs as a \ref spa_callbacks, together
  * with \a _data.
  */
-#define SPA_CALLBACKS_INIT(_funcs,_data) (struct spa_callbacks){ _funcs, _data, }
+#define SPA_CALLBACKS_INIT(_funcs,_data) ((struct spa_callbacks){ (_funcs), (_data), })
 
 /** \struct spa_interface
  */
@@ -166,7 +166,7 @@ struct spa_interface {
  *
  */
 #define SPA_INTERFACE_INIT(_type,_version,_funcs,_data) \
-	(struct spa_interface){ _type, _version, SPA_CALLBACKS_INIT(_funcs,_data), }
+	((struct spa_interface){ (_type), (_version), SPA_CALLBACKS_INIT(_funcs,_data), })
 
 /**
  * Invoke method named \a method in the \a callbacks.
diff --git a/spa/include/spa/utils/json.h b/spa/include/spa/utils/json.h
index 19093169b87fb43da4d72bec9a9f7e1f487faaed..0f93149e6ed966f65ce3424a5a802dc2658c01d2 100644
--- a/spa/include/spa/utils/json.h
+++ b/spa/include/spa/utils/json.h
@@ -58,20 +58,20 @@ struct spa_json {
 	uint32_t depth;
 };
 
-#define SPA_JSON_INIT(data,size) (struct spa_json) { (data), (data)+(size), }
+#define SPA_JSON_INIT(data,size) ((struct spa_json) { (data), (data)+(size), })
 
 static inline void spa_json_init(struct spa_json * iter, const char *data, size_t size)
 {
 	*iter =  SPA_JSON_INIT(data, size);
 }
-#define SPA_JSON_ENTER(iter) (struct spa_json) { (iter)->cur, (iter)->end, (iter), }
+#define SPA_JSON_ENTER(iter) ((struct spa_json) { (iter)->cur, (iter)->end, (iter), })
 
 static inline void spa_json_enter(struct spa_json * iter, struct spa_json * sub)
 {
 	*sub = SPA_JSON_ENTER(iter);
 }
 
-#define SPA_JSON_SAVE(iter) (struct spa_json) { (iter)->cur, (iter)->end, }
+#define SPA_JSON_SAVE(iter) ((struct spa_json) { (iter)->cur, (iter)->end, })
 
 /** Get the next token. \a value points to the token and the return value
  * is the length. */
diff --git a/spa/include/spa/utils/list.h b/spa/include/spa/utils/list.h
index 2300905edaf254418f54b543fc4f94084bbce183..b57657a0ff15d983e81cb6f870f52554d893ea9f 100644
--- a/spa/include/spa/utils/list.h
+++ b/spa/include/spa/utils/list.h
@@ -44,7 +44,7 @@ struct spa_list {
 	struct spa_list *prev;
 };
 
-#define SPA_LIST_INIT(list) (struct spa_list){ list, list }
+#define SPA_LIST_INIT(list) ((struct spa_list){ (list), (list) })
 
 static inline void spa_list_init(struct spa_list *list)
 {
@@ -98,25 +98,25 @@ static inline void spa_list_remove(struct spa_list *elem)
 	(&(pos)->member == (head))
 
 #define spa_list_next(pos, member)					\
-	SPA_CONTAINER_OF((pos)->member.next, __typeof__(*pos), member)
+	SPA_CONTAINER_OF((pos)->member.next, __typeof__(*(pos)), member)
 
 #define spa_list_prev(pos, member)					\
-	SPA_CONTAINER_OF((pos)->member.prev, __typeof__(*pos), member)
+	SPA_CONTAINER_OF((pos)->member.prev, __typeof__(*(pos)), member)
 
 #define spa_list_consume(pos, head, member)				\
-	for (pos = spa_list_first(head, __typeof__(*pos), member);	\
+	for ((pos) = spa_list_first(head, __typeof__(*(pos)), member);	\
 	     !spa_list_is_empty(head);					\
-	     pos = spa_list_first(head, __typeof__(*pos), member))
+	     (pos) = spa_list_first(head, __typeof__(*(pos)), member))
 
 #define spa_list_for_each_next(pos, head, curr, member)			\
-	for (pos = spa_list_first(curr, __typeof__(*pos), member);	\
+	for ((pos) = spa_list_first(curr, __typeof__(*(pos)), member);	\
 	     !spa_list_is_end(pos, head, member);			\
-	     pos = spa_list_next(pos, member))
+	     (pos) = spa_list_next(pos, member))
 
 #define spa_list_for_each_prev(pos, head, curr, member)			\
-	for (pos = spa_list_last(curr, __typeof__(*pos), member);	\
+	for ((pos) = spa_list_last(curr, __typeof__(*(pos)), member);	\
 	     !spa_list_is_end(pos, head, member);			\
-	     pos = spa_list_prev(pos, member))
+	     (pos) = spa_list_prev(pos, member))
 
 #define spa_list_for_each(pos, head, member)				\
 	spa_list_for_each_next(pos, head, head, member)
@@ -125,16 +125,16 @@ static inline void spa_list_remove(struct spa_list *elem)
 	spa_list_for_each_prev(pos, head, head, member)
 
 #define spa_list_for_each_safe_next(pos, tmp, head, curr, member)	\
-	for (pos = spa_list_first(curr, __typeof__(*pos), member);	\
-	     tmp = spa_list_next(pos, member),				\
+	for ((pos) = spa_list_first(curr, __typeof__(*(pos)), member);	\
+	     (tmp) = spa_list_next(pos, member),				\
 	     !spa_list_is_end(pos, head, member);			\
-	     pos = tmp)
+	     (pos) = (tmp))
 
 #define spa_list_for_each_safe_prev(pos, tmp, head, curr, member)	\
-	for (pos = spa_list_last(curr, __typeof__(*pos), member);	\
-	     tmp = spa_list_prev(pos, member),				\
+	for ((pos) = spa_list_last(curr, __typeof__(*(pos)), member);	\
+	     (tmp) = spa_list_prev(pos, member),				\
 	     !spa_list_is_end(pos, head, member);			\
-	     pos = tmp)
+	     (pos) = (tmp))
 
 #define spa_list_for_each_safe(pos, tmp, head, member)			\
 	spa_list_for_each_safe_next(pos, tmp, head, head, member)
@@ -146,11 +146,11 @@ static inline void spa_list_remove(struct spa_list *elem)
         spa_list_prepend(head, &(cursor).member)
 
 #define spa_list_for_each_cursor(pos, cursor, head, member)             \
-        for(pos = spa_list_first(&(cursor).member, __typeof__(*(pos)), member); \
+        for((pos) = spa_list_first(&(cursor).member, __typeof__(*(pos)), member); \
             spa_list_remove(&(pos)->member),                            \
             spa_list_append(&(cursor).member, &(pos)->member),          \
             !spa_list_is_end(pos, head, member);                        \
-            pos = spa_list_next(&cursor, member))
+            (pos) = spa_list_next(&(cursor), member))
 
 #define spa_list_cursor_end(cursor, member)                             \
         spa_list_remove(&(cursor).member)
diff --git a/spa/include/spa/utils/names.h b/spa/include/spa/utils/names.h
index 7d60b07ed80579d8eaf3da9e92733542a322e020..f8104b7421e96742307ed36be5b81a61347542cf 100644
--- a/spa/include/spa/utils/names.h
+++ b/spa/include/spa/utils/names.h
@@ -114,13 +114,15 @@ extern "C" {
 /** keys for bluez5 factory names */
 #define SPA_NAME_API_BLUEZ5_ENUM_DBUS	"api.bluez5.enum.dbus"		/**< a dbus Device interface */
 #define SPA_NAME_API_BLUEZ5_DEVICE	"api.bluez5.device"		/**< a Device interface */
-#define SPA_NAME_API_BLUEZ5_A2DP_SINK	"api.bluez5.a2dp.sink"		/**< a playback Node interface for A2DP profiles */
-#define SPA_NAME_API_BLUEZ5_A2DP_SOURCE	"api.bluez5.a2dp.source"	/**< a capture Node interface for A2DP profiles */
+#define SPA_NAME_API_BLUEZ5_MEDIA_SINK	"api.bluez5.media.sink"		/**< a playback Node interface for A2DP/BAP profiles */
+#define SPA_NAME_API_BLUEZ5_MEDIA_SOURCE	"api.bluez5.media.source"	/**< a capture Node interface for A2DP/BAP profiles */
+#define SPA_NAME_API_BLUEZ5_A2DP_SINK	"api.bluez5.a2dp.sink"		/**< alias for media.sink */
+#define SPA_NAME_API_BLUEZ5_A2DP_SOURCE	"api.bluez5.a2dp.source"	/**< alias for media.source */
 #define SPA_NAME_API_BLUEZ5_SCO_SINK	"api.bluez5.sco.sink"		/**< a playback Node interface for HSP/HFP profiles */
 #define SPA_NAME_API_BLUEZ5_SCO_SOURCE	"api.bluez5.sco.source"		/**< a capture Node interface for HSP/HFP profiles */
 
 /** keys for codec factory names */
-#define SPA_NAME_API_CODEC_BLUEZ5_A2DP	"api.codec.bluez5.a2dp"		/**< Bluez5 A2DP codec plugin */
+#define SPA_NAME_API_CODEC_BLUEZ5_MEDIA	"api.codec.bluez5.media"	/**< Bluez5 Media codec plugin */
 
 /** keys for v4l2 factory names */
 #define SPA_NAME_API_V4L2_ENUM_UDEV	"api.v4l2.enum.udev"		/**< a v4l2 udev Device interface */
diff --git a/spa/include/spa/utils/result.h b/spa/include/spa/utils/result.h
index 67ee401a1447146cd6e61c1ef919180c0123d141..05a1ef96b4e3de4dc2e54fc904aef3a318bb7c9a 100644
--- a/spa/include/spa/utils/result.h
+++ b/spa/include/spa/utils/result.h
@@ -55,7 +55,7 @@ extern "C" {
 
 #define spa_strerror(err)		\
 ({					\
-	int _err = -err;		\
+	int _err = -(err);		\
 	if (SPA_RESULT_IS_ASYNC(err))	\
 		_err = EINPROGRESS;	\
 	strerror(_err);			\
diff --git a/spa/include/spa/utils/ringbuffer.h b/spa/include/spa/utils/ringbuffer.h
index 19bfb8675551c66195901ac8c193125c61d91cec..ed14939a78b9a876a0230e46b2fc9d445e0ef9eb 100644
--- a/spa/include/spa/utils/ringbuffer.h
+++ b/spa/include/spa/utils/ringbuffer.h
@@ -53,7 +53,7 @@ struct spa_ringbuffer {
 	uint32_t writeindex;	/*< the current write index */
 };
 
-#define SPA_RINGBUFFER_INIT()	(struct spa_ringbuffer) { 0, 0 }
+#define SPA_RINGBUFFER_INIT()	((struct spa_ringbuffer) { 0, 0 })
 
 /**
  * Initialize a spa_ringbuffer with \a size.
diff --git a/spa/meson.build b/spa/meson.build
index 44b86eaf51968fd526b43e0266a0dea096d9aea8..64cc7b31072d0cbcb04d497eb40c20543764f4bb 100644
--- a/spa/meson.build
+++ b/spa/meson.build
@@ -64,6 +64,12 @@ if get_option('spa-plugins').allowed()
     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')
+    lc3_dep = dependency('lc3', required : get_option('bluez5-codec-lc3'))
+    summary({'LC3': lc3_dep.found()}, bool_yn: true, section: 'Bluetooth audio codecs')
+    if get_option('bluez5-backend-hsp-native').allowed() or get_option('bluez5-backend-hfp-native').allowed()
+      mm_dep = dependency('ModemManager', version : '>= 1.10.0', required : get_option('bluez5-backend-native-mm'))
+      summary({'ModemManager': mm_dep.found()}, bool_yn: true, section: 'Bluetooth backends')
+    endif
   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/alsa/90-pipewire-alsa.rules b/spa/plugins/alsa/90-pipewire-alsa.rules
index f451275b692d9fc158b224cd9336389213d8b114..5dd292683d21a8a23ed3cdf6d82a432ad9b351a2 100644
--- a/spa/plugins/alsa/90-pipewire-alsa.rules
+++ b/spa/plugins/alsa/90-pipewire-alsa.rules
@@ -181,6 +181,9 @@ ATTRS{idVendor}=="1395", ATTRS{idProduct}=="008a", ENV{ACP_PROFILE_SET}="usb-gam
 # have any digital outputs.
 ATTRS{idVendor}=="0a12", ATTRS{idProduct}=="4007", ENV{ACP_PROFILE_SET}="analog-only.conf"
 
+# Asus Xonar SE
+ATTRS{idVendor}=="0b05", ATTRS{idProduct}=="189d", ENV{ACP_PROFILE_SET}="asus-xonar-se.conf"
+
 GOTO="pipewire_end"
 
 LABEL="pipewire_check_pci"
diff --git a/spa/plugins/alsa/acp-tool.c b/spa/plugins/alsa/acp-tool.c
index bddefab53b800f6f21e3da5d5354e0e319140bf4..0fc92a4f32b1c22e31c990e8d352d818bffffcd2 100644
--- a/spa/plugins/alsa/acp-tool.c
+++ b/spa/plugins/alsa/acp-tool.c
@@ -584,6 +584,9 @@ static int handle_input(struct data *data)
 		return -errno;
 	buf[r] = 0;
 
+	if (r == 0)
+		return -EPIPE;
+
 	if ((p = strchr(buf, '#')))
                 *p = '\0';
 
@@ -679,8 +682,12 @@ static int do_prompt(struct data *data)
 		if (err < 0)
 			return -errno;
 
-		if (pfds[0].revents & POLLIN)
-			handle_input(data);
+		if (pfds[0].revents & POLLIN) {
+			if ((err = handle_input(data)) < 0) {
+				if (err == -EPIPE)
+					break;
+			}
+		}
 
 		if (count < 2)
 			continue;
diff --git a/spa/plugins/alsa/acp/acp.c b/spa/plugins/alsa/acp/acp.c
index f23232ed3ab9b249180dd4f09bae79ba4d88837a..4bbe5ee927e0eb29e34768beb381dae555ac133a 100644
--- a/spa/plugins/alsa/acp/acp.c
+++ b/spa/plugins/alsa/acp/acp.c
@@ -34,6 +34,8 @@ void *_acp_log_data;
 
 struct spa_i18n *acp_i18n;
 
+#define DEFAULT_RATE	44100
+
 #define VOLUME_ACCURACY (PA_VOLUME_NORM/100)  /* don't require volume adjustments to be perfectly correct. don't necessarily extend granularity in software unless the differences get greater than this level */
 
 static const uint32_t channel_table[PA_CHANNEL_POSITION_MAX] = {
@@ -312,7 +314,7 @@ static int add_pro_profile(pa_card *impl, uint32_t index)
 	snd_pcm_uframes_t try_period_size, try_buffer_size;
 
 	ss.format = PA_SAMPLE_S32LE;
-	ss.rate = 48000;
+	ss.rate = DEFAULT_RATE;
 	ss.channels = 64;
 
 	ap = pa_xnew0(pa_alsa_profile, 1);
@@ -624,7 +626,7 @@ static int report_jack_state(snd_mixer_elem_t *melem, unsigned int mask)
 	pa_card *impl = snd_mixer_elem_get_callback_private(melem);
 	snd_hctl_elem_t *elem = snd_mixer_elem_get_private(melem);
 	snd_ctl_elem_value_t *elem_value;
-	bool plugged_in;
+	bool plugged_in, any_input_port_available;
 	void *state;
 	pa_alsa_jack *jack;
 	struct temp_port_avail *tp, *tports;
@@ -735,6 +737,31 @@ static int report_jack_state(snd_mixer_elem_t *melem, unsigned int mask)
 	if (impl->card.active_profile_index != ACP_INVALID_INDEX)
 		active_available = impl->card.profiles[impl->card.active_profile_index]->available;
 
+	/* First round - detect, if we have any input port available.
+	   If the hardware can report the state for all I/O jacks, only speakers
+	   may be plugged in. */
+	any_input_port_available = false;
+	PA_HASHMAP_FOREACH(profile, impl->profiles, state) {
+		pa_device_port *port;
+		void *state2;
+
+		if (profile->profile.flags & ACP_PROFILE_OFF)
+			continue;
+
+		PA_HASHMAP_FOREACH(port, impl->ports, state2) {
+			if (!pa_hashmap_get(port->profiles, profile->profile.name))
+				continue;
+
+			if (port->port.direction == ACP_DIRECTION_CAPTURE &&
+			    port->port.available != ACP_AVAILABLE_NO) {
+				any_input_port_available = true;
+				goto input_port_found;
+			}
+		}
+	}
+input_port_found:
+
+	/* Second round */
 	PA_HASHMAP_FOREACH(profile, impl->profiles, state) {
 		pa_device_port *port;
 		void *state2;
@@ -768,7 +795,7 @@ static int report_jack_state(snd_mixer_elem_t *melem, unsigned int mask)
 
 		if (has_input_port && !has_output_port && found_available_input_port)
 			available = ACP_AVAILABLE_YES;
-		if (has_output_port && !has_input_port && found_available_output_port)
+		if (has_output_port && (!has_input_port || !any_input_port_available) && found_available_output_port)
 			available = ACP_AVAILABLE_YES;
 		if (has_output_port && has_input_port && found_available_output_port && found_available_input_port)
 			available = ACP_AVAILABLE_YES;
@@ -1546,7 +1573,7 @@ struct acp_card *acp_card_new(uint32_t index, const struct acp_dict *props)
 	}
 
 	impl->ucm.default_sample_spec.format = PA_SAMPLE_S16NE;
-	impl->ucm.default_sample_spec.rate = 48000;
+	impl->ucm.default_sample_spec.rate = DEFAULT_RATE;
 	impl->ucm.default_sample_spec.channels = 2;
 	pa_channel_map_init_extend(&impl->ucm.default_channel_map,
 			impl->ucm.default_sample_spec.channels, PA_CHANNEL_MAP_ALSA);
diff --git a/spa/plugins/alsa/acp/acp.h b/spa/plugins/alsa/acp/acp.h
index 8db9f8f2783810cd5f78605069a3a48d562d201f..b61ad720097a22409848deeaf9d6fb24196e9a5e 100644
--- a/spa/plugins/alsa/acp/acp.h
+++ b/spa/plugins/alsa/acp/acp.h
@@ -49,7 +49,7 @@ struct acp_dict_item {
 	const char *key;
 	const char *value;
 };
-#define ACP_DICT_ITEM_INIT(key,value) (struct acp_dict_item) { key, value }
+#define ACP_DICT_ITEM_INIT(key,value) ((struct acp_dict_item) { (key), (value) })
 
 struct acp_dict {
 	uint32_t flags;
@@ -115,8 +115,8 @@ struct acp_format {
 	uint32_t map[ACP_MAX_CHANNELS];
 };
 
-#define ACP_DICT_INIT(items,n_items) (struct acp_dict) { 0, n_items, items }
-#define ACP_DICT_INIT_ARRAY(items) (struct acp_dict) { 0, sizeof(items)/sizeof((items)[0]), items }
+#define ACP_DICT_INIT(items,n_items) ((struct acp_dict) { 0, (n_items), (items) })
+#define ACP_DICT_INIT_ARRAY(items) ((struct acp_dict) { 0, sizeof(items)/sizeof((items)[0]), (items) })
 
 #define acp_dict_for_each(item, dict)				\
 	for ((item) = (dict)->items;				\
diff --git a/spa/plugins/alsa/acp/array.h b/spa/plugins/alsa/acp/array.h
index ef3b60362540d7adab07ddc03a7e967c886cece0..8a976ca7988accd54942b6e7a85b73d907dd38f8 100644
--- a/spa/plugins/alsa/acp/array.h
+++ b/spa/plugins/alsa/acp/array.h
@@ -39,7 +39,7 @@ typedef struct pa_array {
 	size_t extend;		/**< number of bytes to extend with */
 } pa_array;
 
-#define PW_ARRAY_INIT(extend) (struct pa_array) { NULL, 0, 0, extend }
+#define PW_ARRAY_INIT(extend) ((struct pa_array) { NULL, 0, 0, (extend) })
 
 #define pa_array_get_len_s(a,s)			((a)->size / (s))
 #define pa_array_get_unchecked_s(a,idx,s,t)	(t*)((uint8_t*)(a)->data + (int)((idx)*(s)))
diff --git a/spa/plugins/alsa/alsa-acp-device.c b/spa/plugins/alsa/alsa-acp-device.c
index 5e56d72f815363b63c1a9c1d1896344610394ce5..8e71ebc86c9cd8f1d2f560530da3105327e4185c 100644
--- a/spa/plugins/alsa/alsa-acp-device.c
+++ b/spa/plugins/alsa/alsa-acp-device.c
@@ -216,7 +216,7 @@ static int emit_info(struct impl *this, bool full)
 {
 	int err = 0;
 	struct spa_dict_item *items;
-	uint32_t i, n_items;
+	uint32_t n_items;
 	const struct acp_dict_item *it;
 	struct acp_card *card = this->card;
 	char path[128];
@@ -241,10 +241,10 @@ static int emit_info(struct impl *this, bool full)
 #undef ADD_ITEM
 
 		if (this->info.change_mask & SPA_DEVICE_CHANGE_MASK_PARAMS) {
-			for (i = 0; i < SPA_N_ELEMENTS(this->params); i++) {
-				if (this->params[i].user > 0) {
-					this->params[i].flags ^= SPA_PARAM_INFO_SERIAL;
-					this->params[i].user = 0;
+			SPA_FOR_EACH_ELEMENT_VAR(this->params, p) {
+				if (p->user > 0) {
+					p->flags ^= SPA_PARAM_INFO_SERIAL;
+					p->user = 0;
 				}
 			}
 		}
diff --git a/spa/plugins/alsa/alsa-pcm.c b/spa/plugins/alsa/alsa-pcm.c
index 6bcb7d851eb8fa2729507ec8c8953e26a7ffa6f1..5e0a60b375e8658f8d4c682543393c6e6c9c0292 100644
--- a/spa/plugins/alsa/alsa-pcm.c
+++ b/spa/plugins/alsa/alsa-pcm.c
@@ -511,6 +511,9 @@ int spa_alsa_init(struct state *state, const struct spa_dict *info)
 	}
 	CHECK(snd_output_stdio_attach(&state->output, state->log_file, 0), "attach failed");
 
+	state->rate_limit.interval = 2 * SPA_NSEC_PER_SEC;
+	state->rate_limit.burst = 1;
+
 	return 0;
 }
 
@@ -635,12 +638,10 @@ static const struct format_info format_info[] = {
 
 static snd_pcm_format_t spa_format_to_alsa(uint32_t format, bool *planar)
 {
-	size_t i;
-
-	for (i = 0; i < SPA_N_ELEMENTS(format_info); i++) {
-		*planar = format_info[i].spa_pformat == format;
-		if (format_info[i].spa_format == format || *planar)
-			return format_info[i].format;
+	SPA_FOR_EACH_ELEMENT_VAR(format_info, i) {
+		*planar = i->spa_pformat == format;
+		if (i->spa_format == format || *planar)
+			return i->format;
 	}
 	return SND_PCM_FORMAT_UNKNOWN;
 }
@@ -966,7 +967,7 @@ static int enum_pcm_formats(struct state *state, uint32_t index, uint32_t *next,
 		struct spa_pod **result, struct spa_pod_builder *b)
 {
 	int res, err;
-	size_t i, j;
+	size_t j;
 	snd_pcm_t *hndl;
 	snd_pcm_hw_params_t *params;
 	struct spa_pod_frame f[2];
@@ -1017,8 +1018,10 @@ static int enum_pcm_formats(struct state *state, uint32_t index, uint32_t *next,
 	spa_pod_builder_push_choice(b, &f[1], SPA_CHOICE_None, 0);
 	choice = (struct spa_pod_choice*)spa_pod_builder_frame(b, &f[1]);
 
-	for (i = 1, j = 0; i < SPA_N_ELEMENTS(format_info); i++) {
-		const struct format_info *fi = &format_info[i];
+	j = 0;
+	SPA_FOR_EACH_ELEMENT_VAR(format_info, fi) {
+		if (fi->format == SND_PCM_FORMAT_UNKNOWN)
+			continue;
 
 		if (snd_pcm_format_mask_test(fmask, fi->format)) {
 			if ((snd_pcm_access_mask_test(amask, SND_PCM_ACCESS_MMAP_NONINTERLEAVED) ||
@@ -1772,22 +1775,24 @@ recover:
 	state->alsa_started = false;
 
 	if (state->stream == SND_PCM_STREAM_PLAYBACK)
-		spa_alsa_silence(state, state->start_delay + state->threshold * 2 + state->headroom);
+		spa_alsa_silence(state, state->start_delay + state->threshold + state->headroom);
 
 	return do_start(state);
 }
 
 static int get_avail(struct state *state, uint64_t current_time)
 {
-	int res;
+	int res, missed;
 	snd_pcm_sframes_t avail;
 
 	if (SPA_UNLIKELY((avail = snd_pcm_avail(state->hndl)) < 0)) {
 		if ((res = alsa_recover(state, avail)) < 0)
 			return res;
 		if ((avail = snd_pcm_avail(state->hndl)) < 0) {
-			spa_log_warn(state->log, "%s: snd_pcm_avail after recover: %s",
-					state->props.device, snd_strerror(avail));
+			if ((missed = ratelimit_test(&state->rate_limit, current_time)) >= 0) {
+				spa_log_warn(state->log, "%s: (%d missed) snd_pcm_avail after recover: %s",
+						state->props.device, missed, snd_strerror(avail));
+			}
 			avail = state->threshold * 2;
 		}
 	} else {
@@ -1799,7 +1804,7 @@ static int get_avail(struct state *state, uint64_t current_time)
 #if 0
 static int get_avail_htimestamp(struct state *state, uint64_t current_time)
 {
-	int res;
+	int res, missed;
 	snd_pcm_uframes_t avail;
 	snd_htimestamp_t tstamp;
 	uint64_t then;
@@ -1808,8 +1813,10 @@ static int get_avail_htimestamp(struct state *state, uint64_t current_time)
 		if ((res = alsa_recover(state, avail)) < 0)
 			return res;
 		if ((res = snd_pcm_htimestamp(state->hndl, &avail, &tstamp)) < 0) {
-			spa_log_warn(state->log, "%s: snd_pcm_htimestamp error: %s",
-				state->props.device, snd_strerror(res));
+			if ((missed = ratelimit_test(&state->rate_limit, current_time)) >= 0) {
+				spa_log_warn(state->log, "%s: (%d missed) snd_pcm_htimestamp error: %s",
+					state->props.device, missed, snd_strerror(res));
+			}
 			avail = state->threshold * 2;
 		}
 	} else {
@@ -1880,15 +1887,14 @@ static int update_time(struct state *state, uint64_t current_time, snd_pcm_sfram
 				state, follower, state->last_threshold, state->threshold, diff, err);
 		state->last_threshold = state->threshold;
 		state->alsa_sync = true;
+		state->alsa_sync_warning = false;
 	}
 	if (err > state->max_error) {
 		err = state->max_error;
 		state->alsa_sync = true;
-		state->alsa_sync_warning = (diff == 0);
 	} else if (err < -state->max_error) {
 		err = -state->max_error;
 		state->alsa_sync = true;
-		state->alsa_sync_warning = (diff == 0);
 	}
 
 	if (!follower || state->matching)
@@ -1985,7 +1991,7 @@ int spa_alsa_write(struct state *state)
 	const snd_pcm_channel_area_t *my_areas;
 	snd_pcm_uframes_t written, frames, offset, off, to_write, total_written, max_write;
 	snd_pcm_sframes_t commitres;
-	int res = 0;
+	int res = 0, missed;
 	size_t frame_size = state->frame_size;
 
 	check_position_config(state);
@@ -2005,13 +2011,18 @@ int spa_alsa_write(struct state *state)
 			return res;
 
 		if (SPA_UNLIKELY(state->alsa_sync)) {
-			if (SPA_UNLIKELY(state->alsa_sync_warning)) {
-				spa_log_warn(state->log, "%s: follower delay:%ld target:%ld thr:%u, resync",
-						state->props.device, delay, target, state->threshold);
-				state->alsa_sync_warning = false;
-			} else
-				spa_log_info(state->log, "%s: follower delay:%ld target:%ld thr:%u, resync",
-						state->props.device, delay, target, state->threshold);
+			enum spa_log_level lev;
+
+			if (SPA_UNLIKELY(state->alsa_sync_warning))
+				lev = SPA_LOG_LEVEL_WARN;
+			else
+				lev = SPA_LOG_LEVEL_INFO;
+
+			if ((missed = ratelimit_test(&state->rate_limit, current_time)) >= 0) {
+				spa_log_lev(state->log, lev, "%s: follower delay:%ld target:%ld thr:%u, "
+						"resync (%d missed)", state->props.device, delay,
+						target, state->threshold, missed);
+			}
 
 			if (delay > target)
 				snd_pcm_rewind(state->hndl, delay - target);
@@ -2019,7 +2030,8 @@ int spa_alsa_write(struct state *state)
 				spa_alsa_silence(state, target - delay);
 			delay = target;
 			state->alsa_sync = false;
-		}
+		} else
+			state->alsa_sync_warning = true;
 	}
 
 	total_written = 0;
@@ -2116,7 +2128,7 @@ again:
 
 	state->sample_count += total_written;
 
-	if (SPA_UNLIKELY(!state->alsa_started && total_written > 0))
+	if (SPA_UNLIKELY(!state->alsa_started && (total_written > 0 || frames == 0)))
 		do_start(state);
 
 	return 0;
@@ -2212,7 +2224,7 @@ int spa_alsa_read(struct state *state)
 	const snd_pcm_channel_area_t *my_areas;
 	snd_pcm_uframes_t read, frames, offset;
 	snd_pcm_sframes_t commitres;
-	int res = 0;
+	int res = 0, missed;
 
 	check_position_config(state);
 
@@ -2221,7 +2233,6 @@ int spa_alsa_read(struct state *state)
 	if (state->following && state->alsa_started) {
 		uint64_t current_time;
 		snd_pcm_uframes_t avail, delay, target;
-		uint32_t threshold = state->threshold;
 
 		current_time = state->position->clock.nsec;
 
@@ -2234,13 +2245,18 @@ int spa_alsa_read(struct state *state)
 			return res;
 
 		if (state->alsa_sync) {
-			if (SPA_UNLIKELY(state->alsa_sync_warning)) {
-				spa_log_warn(state->log, "%s: follower delay:%lu target:%lu thr:%u, resync",
-						state->props.device, delay, target, threshold);
-				state->alsa_sync_warning = false;
-			} else
-				spa_log_info(state->log, "%s: follower delay:%lu target:%lu thr:%u, resync",
-						state->props.device, delay, target, threshold);
+			enum spa_log_level lev;
+
+			if (SPA_UNLIKELY(state->alsa_sync_warning))
+				lev = SPA_LOG_LEVEL_WARN;
+			else
+				lev = SPA_LOG_LEVEL_INFO;
+
+			if ((missed = ratelimit_test(&state->rate_limit, current_time)) >= 0) {
+				spa_log_lev(state->log, lev, "%s: follower delay:%ld target:%ld thr:%u, "
+						"resync (%d missed)", state->props.device, delay,
+						target, state->threshold, missed);
+			}
 
 			if (delay < target)
 				max_read = target - delay;
@@ -2248,7 +2264,8 @@ int spa_alsa_read(struct state *state)
 				snd_pcm_forward(state->hndl, delay - target);
 			delay = target;
 			state->alsa_sync = false;
-		}
+		} else
+			state->alsa_sync_warning = true;
 
 		if (avail < state->read_size)
 			max_read = 0;
@@ -2338,7 +2355,7 @@ static int handle_play(struct state *state, uint64_t current_time,
 {
 	int res;
 
-	if (SPA_UNLIKELY(delay > target + state->max_error)) {
+	if (state->alsa_started && SPA_UNLIKELY(delay > target + state->max_error)) {
 		spa_log_trace(state->log, "%p: early wakeup %lu %lu", state, delay, target);
 		if (delay > target * 3)
 			delay = target * 3;
@@ -2537,13 +2554,14 @@ int spa_alsa_start(struct state *state)
 
 	reset_buffers(state);
 	state->alsa_sync = true;
+	state->alsa_sync_warning = false;
 	state->alsa_recovering = false;
 	state->alsa_started = false;
 
+	/* start capture now, playback will start after first write */
 	if (state->stream == SND_PCM_STREAM_PLAYBACK)
-		spa_alsa_silence(state, state->start_delay + state->threshold * 2 + state->headroom);
-
-	if ((err = do_start(state)) < 0)
+		spa_alsa_silence(state, state->start_delay + state->threshold + state->headroom);
+	else if ((err = do_start(state)) < 0)
 		return err;
 
 	set_timers(state);
diff --git a/spa/plugins/alsa/alsa-pcm.h b/spa/plugins/alsa/alsa-pcm.h
index 91385f40d05342b80937672dac4d19a142e2bf10..c630de3acc349b5fc919f72c602cabefa4997a82 100644
--- a/spa/plugins/alsa/alsa-pcm.h
+++ b/spa/plugins/alsa/alsa-pcm.h
@@ -87,7 +87,6 @@ struct channel_map {
 	uint32_t pos[SPA_AUDIO_MAX_CHANNELS];
 };
 
-
 struct card {
 	struct spa_list link;
 	int ref;
@@ -98,6 +97,13 @@ struct card {
 	uint32_t rate;
 };
 
+struct ratelimit {
+	uint64_t interval;
+	uint64_t begin;
+	unsigned burst;
+	unsigned n_printed, n_missed;
+};
+
 struct state {
 	struct spa_handle handle;
 	struct spa_node node;
@@ -107,6 +113,7 @@ struct state {
 	struct spa_loop *data_loop;
 
 	FILE *log_file;
+	struct ratelimit rate_limit;
 
 	uint32_t card_index;
 	struct card *card;
@@ -342,6 +349,23 @@ static inline uint32_t spa_alsa_get_iec958_codecs(struct state *state, uint32_t
 	return i;
 }
 
+static inline int ratelimit_test(struct ratelimit *r, uint64_t now)
+{
+	unsigned missed = 0;
+	if (r->begin + r->interval < now) {
+		missed = r->n_missed;
+		r->begin = now;
+		r->n_printed = 0;
+		r->n_missed = 0;
+	} else if (r->n_printed >= r->burst) {
+		r->n_missed++;
+		return -1;
+	}
+	r->n_printed++;
+	return missed;
+}
+
+
 #ifdef __cplusplus
 } /* extern "C" */
 #endif
diff --git a/spa/plugins/alsa/alsa-seq-bridge.c b/spa/plugins/alsa/alsa-seq-bridge.c
index bf29301472d7c0639a35e0dcec91d2e468987c23..4ec8e3c6ed33f2d4895a5ed8c1fbd1eb6f337a6b 100644
--- a/spa/plugins/alsa/alsa-seq-bridge.c
+++ b/spa/plugins/alsa/alsa-seq-bridge.c
@@ -566,7 +566,7 @@ impl_node_port_enum_params(void *object, int seq,
 		if (result.index > 0)
 			return 0;
 		param = spa_pod_builder_add_object(&b,
-			SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat,
+			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));
 		break;
diff --git a/spa/plugins/alsa/alsa-seq.c b/spa/plugins/alsa/alsa-seq.c
index 381bbc5e10d3661f4a800939587ccfdd1e7d2078..407b88f84cb08a85f464a5e6856583cd5f9f04b8 100644
--- a/spa/plugins/alsa/alsa-seq.c
+++ b/spa/plugins/alsa/alsa-seq.c
@@ -586,11 +586,11 @@ static int process_read(struct seq_state *state)
 			continue;
 
 		if (prepare_buffer(state, port) >= 0) {
-			port->buffer->buf->datas[0].chunk->offset = 0;
-			port->buffer->buf->datas[0].chunk->size = port->builder.state.offset,
-
 			spa_pod_builder_pop(&port->builder, &port->frame);
 
+			port->buffer->buf->datas[0].chunk->offset = 0;
+			port->buffer->buf->datas[0].chunk->size = port->builder.state.offset;
+
 			/* move buffer to ready queue */
 			spa_list_remove(&port->buffer->link);
 			SPA_FLAG_SET(port->buffer->flags, BUFFER_FLAG_OUT);
@@ -702,23 +702,28 @@ static int process_write(struct seq_state *state)
 	return res;
 }
 
-static int update_time(struct seq_state *state, uint64_t nsec, bool follower)
+static void update_position(struct seq_state *state)
 {
-	snd_seq_queue_status_t *status;
-	const snd_seq_real_time_t* queue_time;
-	uint64_t queue_real;
-	double err, corr;
-	uint64_t queue_elapsed;
-
 	if (state->position) {
 		struct spa_io_clock *clock = &state->position->clock;
 		state->rate = clock->rate;
+		if (state->rate.num == 0 || state->rate.denom == 0)
+			state->rate = SPA_FRACTION(1, 48000);
 		state->duration = clock->duration;
 	} else {
 		state->rate = SPA_FRACTION(1, 48000);
 		state->duration = 1024;
 	}
 	state->threshold = state->duration;
+}
+
+static int update_time(struct seq_state *state, uint64_t nsec, bool follower)
+{
+	snd_seq_queue_status_t *status;
+	const snd_seq_real_time_t* queue_time;
+	uint64_t queue_real;
+	double err, corr;
+	uint64_t queue_elapsed;
 
 	corr = 1.0 - (state->dll.z2 + state->dll.z3);
 
@@ -776,6 +781,8 @@ int spa_alsa_seq_process(struct seq_state *state)
 {
 	int res;
 
+	update_position(state);
+
 	res = process_recycle(state);
 
 	if (state->following && state->position) {
@@ -800,11 +807,13 @@ static void alsa_on_timeout_event(struct spa_source *source)
 
 	spa_log_trace(state->log, "timeout %"PRIu64, state->current_time);
 
+	update_position(state);
+
 	update_time(state, state->current_time, false);
 
 	res = process_read(state);
-	if (res > 0)
-		spa_node_call_ready(&state->callbacks, res);
+	if (res >= 0)
+		spa_node_call_ready(&state->callbacks, res | SPA_STATUS_NEED_DATA);
 
 	set_timeout(state, state->next_time);
 }
@@ -878,15 +887,7 @@ int spa_alsa_seq_start(struct seq_state *state)
 	while (snd_seq_drain_output(state->event.hndl) > 0)
 		sleep(1);
 
-	if (state->position) {
-		struct spa_io_clock *clock = &state->position->clock;
-		state->rate = clock->rate;
-		state->duration = clock->duration;
-	} else {
-		state->rate = SPA_FRACTION(1, 48000);
-		state->duration = 1024;
-	}
-	state->threshold = state->duration;
+	update_position(state);
 
 	state->started = true;
 
diff --git a/spa/plugins/alsa/alsa-udev.c b/spa/plugins/alsa/alsa-udev.c
index 8ee217d9dff3e380769770e81b7227d686e5a08b..f89d86375b953034a0c99e769183ca0ec456635b 100644
--- a/spa/plugins/alsa/alsa-udev.c
+++ b/spa/plugins/alsa/alsa-udev.c
@@ -256,7 +256,7 @@ static int check_device_pcm_class(const char *devname)
 	/* Check device class */
 	spa_scnprintf(path, sizeof(path), "/sys/class/sound/%s/pcm_class",
 			devname);
-	f = fopen(path, "r");
+	f = fopen(path, "re");
 	if (f == NULL)
 		return -errno;
 	sz = fread(buf, 1, sizeof(buf) - 1, f);
@@ -361,7 +361,7 @@ static int check_device_available(struct impl *this, struct device *device, int
 			spa_scnprintf(path, sizeof(path), "/proc/asound/card%u/%s/%s/status",
 					(unsigned int)device->id, entry->d_name, entry_pcm->d_name);
 
-			f = fopen(path, "r");
+			f = fopen(path, "re");
 			if (f == NULL)
 				goto done;
 			sz = fread(buf, 1, 6, f);
@@ -465,7 +465,7 @@ static int emit_object_info(struct impl *this, struct device *device)
 	if (str && *str) {
 		items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_BUS_PATH, str);
 	}
-	if ((str = udev_device_get_syspath(dev)) && *str) {
+	if ((str = udev_device_get_devpath(dev)) && *str) {
 		items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_SYSFS_PATH, str);
 	}
 	if ((str = udev_device_get_property_value(dev, "ID_ID")) && *str) {
@@ -651,9 +651,9 @@ static void impl_on_notify_events(struct spa_source *source)
 {
 	bool deleted = false;
 	struct impl *this = source->data;
-	struct {
+	union {
 		struct inotify_event e;
-		char name[NAME_MAX+1];
+		char name[NAME_MAX+1+sizeof(struct inotify_event)];
 	} buf;
 
 	while (true) {
@@ -670,17 +670,20 @@ static void impl_on_notify_events(struct spa_source *source)
 		e = SPA_PTROFF(&buf, len, void);
 
 		for (p = &buf; p < e;
-		    p = SPA_PTROFF(p, sizeof(struct inotify_event) + event->len, void)) {
+		     p = SPA_PTROFF(p, sizeof(struct inotify_event) + event->len, void)) {
 			unsigned int id;
 			struct device *device;
 
 			event = (const struct inotify_event *) p;
+			spa_assert_se(SPA_PTRDIFF(e, p) >= (ptrdiff_t)sizeof(struct inotify_event) &&
+			              SPA_PTRDIFF(e, p) - sizeof(struct inotify_event) >= event->len &&
+			              "bad event from kernel");
 
 			/* Device becomes accessible or not busy */
 			if ((event->mask & (IN_ATTRIB | IN_CLOSE_WRITE))) {
 				bool access;
 				if (sscanf(event->name, "controlC%u", &id) != 1 &&
-						sscanf(event->name, "pcmC%uD", &id) != 1)
+				    sscanf(event->name, "pcmC%uD", &id) != 1)
 					continue;
 				if ((device = find_device(this, id)) == NULL)
 					continue;
diff --git a/spa/plugins/alsa/mixer/profile-sets/asus-xonar-se.conf b/spa/plugins/alsa/mixer/profile-sets/asus-xonar-se.conf
new file mode 100644
index 0000000000000000000000000000000000000000..3e42ea311b22a71a94dddb980de3d1fae3811bc7
--- /dev/null
+++ b/spa/plugins/alsa/mixer/profile-sets/asus-xonar-se.conf
@@ -0,0 +1,93 @@
+# This file is part of PulseAudio.
+#
+# 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/>.
+
+; ASUS Xonar SE card.
+; This card has two devices for each rear and front panel jacks.
+;
+; See default.conf for an explanation on the directives used here.
+
+[General]
+auto-profiles = yes
+
+[Mapping analog-stereo-front]
+description = Analog Stereo Front
+device-strings = hw:%f,1
+channel-map = left,right
+paths-output = analog-output analog-output-headphones
+paths-input = analog-input-mic analog-input-headphone-mic analog-input-headset-mic
+priority = 15
+
+[Mapping analog-stereo-rear]
+description = Analog Stereo Rear
+device-strings = hw:%f,0
+channel-map = left,right
+paths-output = analog-output analog-output-speaker
+paths-input = analog-input analog-input-mic analog-input-linein
+priority = 14
+
+[Mapping analog-surround-21]
+device-strings = surround21:%f
+channel-map = front-left,front-right,lfe
+paths-output = analog-output-speaker
+priority = 13
+direction = output
+
+[Mapping analog-surround-40]
+device-strings = surround40:%f
+channel-map = front-left,front-right,rear-left,rear-right
+paths-output = analog-output-speaker
+priority = 12
+direction = output
+
+[Mapping analog-surround-41]
+device-strings = surround41:%f
+channel-map = front-left,front-right,rear-left,rear-right,lfe
+paths-output = analog-output-speaker
+priority = 13
+direction = output
+
+[Mapping analog-surround-50]
+device-strings = surround50:%f
+channel-map = front-left,front-right,rear-left,rear-right,front-center
+paths-output = analog-output-speaker
+priority = 12
+direction = output
+
+[Mapping analog-surround-51]
+device-strings = surround51:%f
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+paths-output = analog-output-speaker
+priority = 13
+direction = output
+
+[Mapping iec958-stereo]
+device-strings = iec958:%f
+channel-map = left,right
+paths-output = iec958-stereo-output
+priority = 5
+
+[Mapping iec958-ac3-surround-40]
+device-strings = a52:%f
+channel-map = front-left,front-right,rear-left,rear-right
+paths-output = iec958-stereo-output
+priority = 2
+direction = output
+
+[Mapping iec958-ac3-surround-51]
+device-strings = a52:%f
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+paths-output = iec958-stereo-output
+priority = 3
+direction = output
diff --git a/spa/plugins/audioconvert/audioadapter.c b/spa/plugins/audioconvert/audioadapter.c
index dcaed9ad3d8ee32644b0a7929b35eb88ecef4f1e..14bf3e6d970f926d34f36cee4b39018697939e2f 100644
--- a/spa/plugins/audioconvert/audioadapter.c
+++ b/spa/plugins/audioconvert/audioadapter.c
@@ -336,7 +336,7 @@ static int negotiate_buffers(struct impl *this)
 	struct spa_data *datas;
 	uint32_t follower_flags, conv_flags;
 
-	spa_log_debug(this->log, "%p: %d", this, this->n_buffers);
+	spa_log_debug(this->log, "%p: n_buffers:%d", this, this->n_buffers);
 
 	if (this->target == this->follower)
 		return 0;
@@ -438,6 +438,9 @@ static int configure_format(struct impl *this, uint32_t flags, const struct spa_
 {
 	int res;
 
+	if (format == NULL && !this->have_format)
+		return 0;
+
 	spa_log_debug(this->log, "%p: configure format:", this);
 	if (format && spa_log_level_enabled(this->log, SPA_LOG_LEVEL_DEBUG))
 		spa_debug_format(0, NULL, format);
@@ -556,30 +559,6 @@ static int reconfigure_mode(struct impl *this, bool passthrough,
 	return 0;
 }
 
-static int format_audio_raw_parse_opt(const struct spa_pod *format, struct spa_audio_info_raw *info)
-{
-	struct spa_pod *position = NULL;
-	uint32_t media_type, media_subtype;
-	int res;
-	if ((res = spa_format_parse(format, &media_type, &media_subtype)) < 0)
-		return res;
-	if (media_type != SPA_MEDIA_TYPE_audio ||
-	    media_subtype != SPA_MEDIA_SUBTYPE_raw)
-		return -ENOTSUP;
-
-	spa_zero(*info);
-	res = spa_pod_parse_object(format,
-			SPA_TYPE_OBJECT_Format, NULL,
-			SPA_FORMAT_AUDIO_format, SPA_POD_OPT_Id(&info->format),
-			SPA_FORMAT_AUDIO_channels, SPA_POD_OPT_Int(&info->channels),
-			SPA_FORMAT_AUDIO_position, SPA_POD_OPT_Pod(&position));
-	if (position == NULL ||
-	    !spa_pod_copy_array(position, SPA_TYPE_Id, info->position, SPA_AUDIO_MAX_CHANNELS))
-		SPA_FLAG_SET(info->flags, SPA_AUDIO_FLAG_UNPOSITIONED);
-
-	return res;
-}
-
 static int impl_node_set_param(void *object, uint32_t id, uint32_t flags,
 			       const struct spa_pod *param)
 {
@@ -627,8 +606,18 @@ static int impl_node_set_param(void *object, uint32_t id, uint32_t flags,
 
 		if (format) {
 			struct spa_audio_info info;
-			if (format_audio_raw_parse_opt(format, &info.info.raw) >= 0)
+
+			spa_zero(info);
+			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) {
+				info.info.raw.rate = 0;
 				this->default_format = info;
+			}
 		}
 
 		switch (mode) {
@@ -738,6 +727,8 @@ static int negotiate_format(struct impl *this)
 	struct spa_pod_builder b = { 0 };
 	int res;
 
+	spa_log_debug(this->log, "%p: have_format:%d", this, this->have_format);
+
 	if (this->have_format)
 		return 0;
 
@@ -746,7 +737,6 @@ static int negotiate_format(struct impl *this)
 
 	spa_pod_builder_init(&b, buffer, sizeof(buffer));
 
-	spa_log_debug(this->log, "%p: negiotiate", this);
 
 	spa_node_send_command(this->follower,
 			&SPA_NODE_COMMAND_INIT(SPA_NODE_COMMAND_ParamBegin));
@@ -813,19 +803,24 @@ static int impl_node_send_command(void *object, const struct spa_command *comman
 
 	switch (SPA_NODE_COMMAND_ID(command)) {
 	case SPA_NODE_COMMAND_Start:
+		spa_log_debug(this->log, "%p: starting %d", this, this->started);
+		if (this->started)
+			return 0;
 		if ((res = negotiate_format(this)) < 0)
 			return res;
 		if ((res = negotiate_buffers(this)) < 0)
 			return res;
+		this->started = true;
 		break;
 	case SPA_NODE_COMMAND_Suspend:
-		configure_format(this, 0, NULL);
-		SPA_FALLTHROUGH
+		spa_log_debug(this->log, "%p: suspending", this);
+		break;
+	case SPA_NODE_COMMAND_Pause:
+		spa_log_debug(this->log, "%p: pausing", this);
+		break;
 	case SPA_NODE_COMMAND_Flush:
+		spa_log_debug(this->log, "%p: flushing", this);
 		this->io_buffers.status = SPA_STATUS_OK;
-		SPA_FALLTHROUGH
-	case SPA_NODE_COMMAND_Pause:
-		this->started = false;
 		break;
 	default:
 		break;
@@ -848,7 +843,17 @@ static int impl_node_send_command(void *object, const struct spa_command *comman
 	}
 	switch (SPA_NODE_COMMAND_ID(command)) {
 	case SPA_NODE_COMMAND_Start:
-		this->started = true;
+		spa_log_debug(this->log, "%p: started", this);
+		break;
+	case SPA_NODE_COMMAND_Suspend:
+		configure_format(this, 0, NULL);
+		SPA_FALLTHROUGH
+	case SPA_NODE_COMMAND_Pause:
+		this->started = false;
+		spa_log_debug(this->log, "%p: stopped", this);
+		break;
+	case SPA_NODE_COMMAND_Flush:
+		spa_log_debug(this->log, "%p: flushed", this);
 		break;
 	}
 	return res;
@@ -892,10 +897,11 @@ static void convert_node_info(void *data, const struct spa_node_info *info)
 				(this->params[idx].flags & SPA_PARAM_INFO_SERIAL) |
 				(info->params[i].flags & SPA_PARAM_INFO_READWRITE);
 
-			if (!this->add_listener) {
-				this->params[idx].user++;
-				spa_log_debug(this->log, "param %d changed", info->params[i].id);
-			}
+			if (this->add_listener)
+				continue;
+
+			this->params[idx].user++;
+			spa_log_debug(this->log, "param %d changed", info->params[i].id);
 		}
 	}
 	emit_node_info(this, false);
@@ -1000,10 +1006,11 @@ static void follower_info(void *data, const struct spa_node_info *info)
 				(this->params[idx].flags & SPA_PARAM_INFO_SERIAL) |
 				(info->params[i].flags & SPA_PARAM_INFO_READWRITE);
 
-			if (!this->add_listener) {
-				this->params[idx].user++;
-				spa_log_debug(this->log, "param %d changed", info->params[i].id);
-			}
+			if (this->add_listener)
+				continue;
+
+			this->params[idx].user++;
+			spa_log_debug(this->log, "param %d changed", info->params[i].id);
 		}
 	}
 	emit_node_info(this, false);
@@ -1080,26 +1087,32 @@ static void follower_port_info(void *data,
 			default:
 				continue;
 			}
+
 			if (!this->add_listener &&
 			    this->follower_params_flags[idx] == info->params[i].flags)
 				continue;
 
+			this->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS;
 			this->follower_params_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 (this->add_listener)
+				continue;
+
 			if (idx == IDX_Latency) {
 				res = recalc_latency(this, direction, port_id);
 				spa_log_debug(this->log, "latency: %d (%s)", res,
 						spa_strerror(res));
 			}
-
-			this->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS;
-			if (!this->add_listener) {
-				this->params[idx].user++;
-				spa_log_debug(this->log, "param %d changed", info->params[i].id);
+			if (idx == IDX_EnumFormat) {
+				spa_log_debug(this->log, "new formats");
+				configure_format(this, 0, NULL);
 			}
+
+			this->params[idx].user++;
+			spa_log_debug(this->log, "param %d changed", info->params[i].id);
 		}
 	}
 	emit_node_info(this, false);
@@ -1150,6 +1163,11 @@ static int follower_ready(void *data, int status)
 
 	spa_log_trace_fp(this->log, "%p: ready %d", this, status);
 
+	if (!this->started) {
+		spa_log_warn(this->log, "%p: ready stopped node", this);
+		return -EIO;
+	}
+
 	if (this->target != this->follower) {
 		this->driver = true;
 
@@ -1396,6 +1414,11 @@ static int impl_node_process(void *object)
 	struct impl *this = object;
 	int status = 0, fstatus, retry = 8;
 
+	if (!this->started) {
+		spa_log_warn(this->log, "%p: scheduling stopped node", this);
+		return -EIO;
+	}
+
 	spa_log_trace_fp(this->log, "%p: process convert:%p driver:%d",
 			this, this->convert, this->driver);
 
diff --git a/spa/plugins/audioconvert/audioconvert.c b/spa/plugins/audioconvert/audioconvert.c
index bb6614d13d2615d5651eff04d20e69d233fa17c2..8e5237bdbf29bb2a0b7e87a8b2fd4ac91fcc478e 100644
--- a/spa/plugins/audioconvert/audioconvert.c
+++ b/spa/plugins/audioconvert/audioconvert.c
@@ -90,8 +90,8 @@ struct props {
 	struct volumes monitor;
 	unsigned int have_soft_volume:1;
 	unsigned int mix_disabled:1;
-	unsigned int resample_quality;
 	unsigned int resample_disabled:1;
+	unsigned int resample_quality;
 	double rate;
 };
 
@@ -105,10 +105,11 @@ static void props_reset(struct props *props)
 	init_volumes(&props->channel);
 	init_volumes(&props->soft);
 	init_volumes(&props->monitor);
+	props->have_soft_volume = false;
 	props->mix_disabled = false;
-	props->rate = 1.0;
-	props->resample_quality = RESAMPLE_DEFAULT_QUALITY;
 	props->resample_disabled = false;
+	props->resample_quality = RESAMPLE_DEFAULT_QUALITY;
+	props->rate = 1.0;
 }
 
 struct buffer {
@@ -215,9 +216,11 @@ struct impl {
 	uint32_t in_offset;
 	uint32_t out_offset;
 	unsigned int started:1;
-	unsigned int peaks:1;
+	unsigned int setup:1;
+	unsigned int resample_peaks:1;
 	unsigned int is_passthrough:1;
 	unsigned int drained:1;
+	unsigned int rate_adjust:1;
 
 	uint32_t empty_size;
 	float *empty;
@@ -239,16 +242,15 @@ static void set_volume(struct impl *this);
 static void emit_node_info(struct impl *this, bool full)
 {
 	uint64_t old = full ? this->info.change_mask : 0;
-	uint32_t i;
 
 	if (full)
 		this->info.change_mask = this->info_all;
 	if (this->info.change_mask) {
 		if (this->info.change_mask & SPA_NODE_CHANGE_MASK_PARAMS) {
-			for (i = 0; i < SPA_N_ELEMENTS(this->params); i++) {
-				if (this->params[i].user > 0) {
-					this->params[i].flags ^= SPA_PARAM_INFO_SERIAL;
-					this->params[i].user = 0;
+			SPA_FOR_EACH_ELEMENT_VAR(this->params, p) {
+				if (p->user > 0) {
+					p->flags ^= SPA_PARAM_INFO_SERIAL;
+					p->user = 0;
 				}
 			}
 		}
@@ -260,7 +262,6 @@ static void emit_node_info(struct impl *this, bool full)
 static void emit_port_info(struct impl *this, struct port *port, bool full)
 {
 	uint64_t old = full ? port->info.change_mask : 0;
-	uint32_t i;
 
 	if (full)
 		port->info.change_mask = port->info_all;
@@ -280,10 +281,10 @@ static void emit_port_info(struct impl *this, struct port *port, bool full)
 		port->info.props = &SPA_DICT_INIT(items, n_items);
 
 		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_FOR_EACH_ELEMENT_VAR(port->params, p) {
+				if (p->user > 0) {
+					p->flags ^= SPA_PARAM_INFO_SERIAL;
+					p->user = 0;
 				}
 			}
 		}
@@ -430,7 +431,6 @@ static int impl_node_enum_params(void *object, int seq,
 	{
 		struct props *p = &this->props;
 		struct spa_pod_frame f[2];
-		uint32_t i;
 
 		switch (result.index) {
 		case 0:
@@ -594,9 +594,9 @@ static int impl_node_enum_params(void *object, int seq,
 
 			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_FOR_EACH_ELEMENT_VAR(channelmix_upmix_info, i) {
+				spa_pod_builder_string(&b, i->label);
+				spa_pod_builder_string(&b, i->description);
 			}
 			spa_pod_builder_pop(&b, &f[1]);
 			param = spa_pod_builder_pop(&b, &f[0]);
@@ -644,9 +644,9 @@ static int impl_node_enum_params(void *object, int seq,
 				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_FOR_EACH_ELEMENT_VAR(dither_method_info, i) {
+				spa_pod_builder_string(&b, i->label);
+				spa_pod_builder_string(&b, i->description);
 			}
 			spa_pod_builder_pop(&b, &f[1]);
 			param = spa_pod_builder_pop(&b, &f[0]);
@@ -825,8 +825,11 @@ static int parse_prop_params(struct impl *this, struct spa_pod *params)
 		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_dtoa(value, sizeof(value),
 					SPA_POD_VALUE(struct spa_pod_float, pod));
+		} else if (spa_pod_is_double(pod)) {
+			spa_dtoa(value, sizeof(value),
+					SPA_POD_VALUE(struct spa_pod_double, pod));
 		} else if (spa_pod_is_int(pod)) {
 			snprintf(value, sizeof(value), "%d",
 					SPA_POD_VALUE(struct spa_pod_int, pod));
@@ -834,12 +837,17 @@ static int parse_prop_params(struct impl *this, struct spa_pod *params)
 			snprintf(value, sizeof(value), "%s",
 					SPA_POD_VALUE(struct spa_pod_bool, pod) ?
 					"true" : "false");
+		} else if (spa_pod_is_none(pod)) {
+			spa_zero(value);
 		} else
 			continue;
 
 		spa_log_info(this->log, "key:'%s' val:'%s'", name, value);
 		changed += audioconvert_set_param(this, name, value);
 	}
+	if (changed) {
+		channelmix_init(&this->mix);
+	}
 	return changed;
 }
 
@@ -907,6 +915,11 @@ static int apply_props(struct impl *this, const struct spa_pod *param)
 			break;
 		case SPA_PROP_rate:
 			spa_pod_get_double(&prop->value, &p->rate);
+			if (!this->rate_adjust && p->rate != 1.0) {
+				this->rate_adjust = true;
+				spa_log_info(this->log, "%p: activating adaptive resampler",
+						this);
+			}
 			break;
 		case SPA_PROP_params:
 			changed += parse_prop_params(this, &prop->value);
@@ -921,7 +934,6 @@ static int apply_props(struct impl *this, const struct spa_pod *param)
 		else if (have_channel_volume)
 			p->have_soft_volume = false;
 
-		channelmix_init(&this->mix);
 		set_volume(this);
 	}
 	return changed;
@@ -967,6 +979,7 @@ static int reconfigure_mode(struct impl *this, enum spa_param_port_config_mode m
 	}
 
 	this->monitor = monitor;
+	this->setup = false;
 	dir->control = control;
 	dir->have_profile = true;
 	dir->mode = mode;
@@ -1060,7 +1073,10 @@ static int impl_node_set_param(void *object, uint32_t id, uint32_t flags,
 			if (spa_format_audio_raw_parse(format, &info.info.raw) < 0)
 				return -EINVAL;
 
-			if (info.info.raw.channels > SPA_AUDIO_MAX_CHANNELS)
+			if (info.info.raw.format == 0 ||
+			    info.info.raw.rate == 0 ||
+			    info.info.raw.channels == 0 ||
+			    info.info.raw.channels > SPA_AUDIO_MAX_CHANNELS)
 				return -EINVAL;
 
 			infop = &info;
@@ -1276,12 +1292,23 @@ static void set_volume(struct impl *this)
 	this->params[IDX_Props].user++;
 }
 
+static char *format_position(char *str, size_t len, uint32_t channels, uint32_t *position)
+{
+	uint32_t i, idx = 0;
+	for (i = 0; i < channels; i++)
+		idx += snprintf(str + idx, len - idx, "%s%s", i == 0 ? "" : " ",
+				spa_debug_type_find_short_name(spa_type_audio_channel,
+					position[i]));
+	return str;
+}
+
 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;
+	char str[1024];
 	int res;
 
 	src_chan = in->format.info.raw.channels;
@@ -1296,6 +1323,11 @@ static int setup_channelmix(struct impl *this)
 		dst_mask |= 1ULL << (p < 64 ? p : 0);
 	}
 
+	spa_log_info(this->log, "in  %s (%016"PRIx64")", format_position(str, sizeof(str),
+				src_chan, in->format.info.raw.position), src_mask);
+	spa_log_info(this->log, "out %s (%016"PRIx64")", format_position(str, sizeof(str),
+				dst_chan, out->format.info.raw.position), dst_mask);
+
 	if (src_mask & 1)
 		src_mask = default_mask(src_chan);
 	if (dst_mask & 1)
@@ -1353,7 +1385,9 @@ static int setup_resample(struct impl *this)
 	this->resample.quality = this->props.resample_quality;
 	this->resample.cpu_flags = this->cpu_flags;
 
-	if (this->peaks)
+	this->rate_adjust = this->props.rate != 1.0;
+
+	if (this->resample_peaks)
 		res = resample_peaks_init(&this->resample);
 	else
 		res = resample_native_init(&this->resample);
@@ -1463,6 +1497,12 @@ static int setup_convert(struct impl *this)
 	in = &this->dir[SPA_DIRECTION_INPUT];
 	out = &this->dir[SPA_DIRECTION_OUTPUT];
 
+	spa_log_debug(this->log, "%p: setup:%d in_format:%d out_format:%d", this,
+			this->setup, in->have_format, out->have_format);
+
+	if (this->setup)
+		return 0;
+
 	if (!in->have_format || !out->have_format)
 		return -EINVAL;
 
@@ -1506,6 +1546,7 @@ static int setup_convert(struct impl *this)
 		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);
 	}
+	this->setup = true;
 
 	emit_node_info(this, false);
 
@@ -1537,13 +1578,14 @@ static int impl_node_send_command(void *object, const struct spa_command *comman
 		this->started = true;
 		break;
 	case SPA_NODE_COMMAND_Suspend:
-		SPA_FALLTHROUGH;
-	case SPA_NODE_COMMAND_Flush:
-		reset_node(this);
+		this->setup = false;
 		SPA_FALLTHROUGH;
 	case SPA_NODE_COMMAND_Pause:
 		this->started = false;
 		break;
+	case SPA_NODE_COMMAND_Flush:
+		reset_node(this);
+		break;
 	default:
 		return -ENOTSUP;
 	}
@@ -1604,7 +1646,6 @@ static int port_enum_formats(void *object,
 			     struct spa_pod_builder *builder)
 {
 	struct impl *this = object;
-	struct port *port = GET_PORT(this, direction, port_id);
 
 	switch (index) {
 	case 0:
@@ -1618,11 +1659,7 @@ static int port_enum_formats(void *object,
 				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 {
+		} else {
 			uint32_t rate = this->io_position ?
 				this->io_position->clock.rate.denom : DEFAULT_RATE;
 
@@ -1832,7 +1869,7 @@ static int port_set_latency(void *object,
 			   const struct spa_pod *latency)
 {
 	struct impl *this = object;
-	struct port *port;
+	struct port *port, *oport;
 	enum spa_direction other = SPA_DIRECTION_REVERSE(direction);
 	uint32_t i;
 
@@ -1854,10 +1891,10 @@ static int port_set_latency(void *object,
 	}
 
 	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);
+		oport = GET_PORT(this, other, i);
+		oport->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+		oport->params[IDX_Latency].user++;
+		emit_port_info(this, oport, false);
 	}
 	port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
 	port->params[IDX_Latency].user++;
@@ -1929,9 +1966,13 @@ static int port_set_format(void *object,
 				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);
+			if (info.info.raw.format == 0 ||
+			    info.info.raw.rate == 0 ||
+			    info.info.raw.channels == 0 ||
+			    info.info.raw.channels > SPA_AUDIO_MAX_CHANNELS) {
+				spa_log_error(this->log, "invalid format:%d rate:%d channels:%d",
+						info.info.raw.format, info.info.raw.rate,
+						info.info.raw.channels);
 				return -EINVAL;
 			}
 			port->stride = calc_width(&info);
@@ -1943,6 +1984,7 @@ static int port_set_format(void *object,
 			}
 			this->dir[direction].format = info;
 			this->dir[direction].have_format = true;
+			this->setup = false;
 		}
 		port->format = info;
 		port->have_format = true;
@@ -2260,8 +2302,7 @@ static uint32_t resample_update_rate_match(struct impl *this, bool passthrough,
 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 ||
+		!this->rate_adjust && (this->io_rate_match == NULL ||
 		 !SPA_FLAG_IS_SET(this->io_rate_match->flags, SPA_IO_RATE_MATCH_FLAG_ACTIVE));
 }
 
@@ -2467,6 +2508,7 @@ static int impl_node_process(void *object)
 							volume, mon_max);
 
 					bd->chunk->size = mon_max * port->stride;
+					bd->chunk->stride = port->stride;
 
 					spa_log_trace_fp(this->log, "%p: monitor %d %d", this,
 							remap, mon_max);
@@ -2667,9 +2709,10 @@ static int impl_node_process(void *object)
 			for (j = 0; j < port->blocks; j++) {
 				bd = &buf->buf->datas[j];
 				bd->chunk->size = this->out_offset * port->stride;
+				bd->chunk->stride = 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);
+				spa_log_trace_fp(this->log, "out: offs:%d stride:%d size:%d",
+						this->out_offset, port->stride, bd->chunk->size);
 			}
 			io->status = SPA_STATUS_HAVE_DATA;
 			io->buffer_id = buf->id;
@@ -2678,7 +2721,7 @@ static int impl_node_process(void *object)
 		this->drained = draining;
 		this->out_offset = 0;
 	}
-	else if (n_samples == 0 && this->peaks) {
+	else if (n_samples == 0 && this->resample_peaks) {
 		for (i = 0; i < dir->n_ports; i++) {
 			port = GET_OUT_PORT(this, i);
 			if (port->is_monitor || port->is_control)
@@ -2841,15 +2884,20 @@ impl_init(const struct spa_handle_factory *factory,
 		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);
+			this->resample_peaks = spa_atob(s);
+		else if (spa_streq(k, "resample.prefill"))
+			SPA_FLAG_UPDATE(this->resample.options,
+				RESAMPLE_OPTION_PREFILL, 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 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, SPA_KEY_AUDIO_POSITION)) {
+			if (s != NULL)
+	                        this->props.n_channels = parse_position(this->props.channel_map, s, strlen(s));
+		}
 		else
 			audioconvert_set_param(this, k, s);
 	}
diff --git a/spa/plugins/audioconvert/benchmark-fmt-ops.c b/spa/plugins/audioconvert/benchmark-fmt-ops.c
index 491b0730bb08294bb63d1b6c796db42d2a96feb7..2a0d4e80b0f5f1168d8152b8390561389d55281f 100644
--- a/spa/plugins/audioconvert/benchmark-fmt-ops.c
+++ b/spa/plugins/audioconvert/benchmark-fmt-ops.c
@@ -105,21 +105,17 @@ static void run_test1(const char *name, const char *impl, bool in_packed, bool o
 static void run_testc(const char *name, const char *impl, bool in_packed, bool out_packed, convert_func_t func,
 		int channel_count)
 {
-	size_t i;
-	for (i = 0; i < SPA_N_ELEMENTS(sample_sizes); i++) {
+	SPA_FOR_EACH_ELEMENT_VAR(sample_sizes, s) {
 		run_test1(name, impl, in_packed, out_packed, func, channel_count,
-				(sample_sizes[i] + (channel_count -1)) / channel_count);
+				(*s + (channel_count -1)) / channel_count);
 	}
 }
 
 static void run_test(const char *name, const char *impl, bool in_packed, bool out_packed, convert_func_t func)
 {
-	size_t i, j;
-
-	for (i = 0; i < SPA_N_ELEMENTS(sample_sizes); i++) {
-		for (j = 0; j < SPA_N_ELEMENTS(channel_counts); j++) {
-			run_test1(name, impl, in_packed, out_packed, func, channel_counts[j],
-				(sample_sizes[i] + (channel_counts[j] -1)) / channel_counts[j]);
+	SPA_FOR_EACH_ELEMENT_VAR(sample_sizes, s) {
+		SPA_FOR_EACH_ELEMENT_VAR(channel_counts, c) {
+			run_test1(name, impl, in_packed, out_packed, func, *c, (*s + (*c -1)) / *c);
 		}
 	}
 }
diff --git a/spa/plugins/audioconvert/channelmix-ops-c.c b/spa/plugins/audioconvert/channelmix-ops-c.c
index 9a28830042ad2f5fd3610304e46f6a9df7359c9d..f12f35f85aeb322202328b382bb11b06628b6c5e 100644
--- a/spa/plugins/audioconvert/channelmix-ops-c.c
+++ b/spa/plugins/audioconvert/channelmix-ops-c.c
@@ -46,6 +46,16 @@ static inline void vol_c(float *d, const float *s, float vol, uint32_t n_samples
 			d[n] = s[n] * vol;
 	}
 }
+static inline void conv_c(float *d, const float **s, float *c, uint32_t n_c, uint32_t n_samples)
+{
+	uint32_t n, j;
+	for (n = 0; n < n_samples; n++) {
+		float sum = 0.0f;
+		for (j = 0; j < n_c; j++)
+			sum += s[j][n] * c[j];
+		d[n] = sum;
+	}
+}
 
 static inline void avg_c(float *d, const float *s0, const float *s1, uint32_t n_samples)
 {
@@ -78,7 +88,7 @@ void
 channelmix_f32_n_m_c(struct channelmix *mix, void * SPA_RESTRICT dst[],
 		const void * SPA_RESTRICT src[], uint32_t n_samples)
 {
-	uint32_t i, j, n, n_dst = mix->dst_chan, n_src = mix->src_chan;
+	uint32_t i, j, n_dst = mix->dst_chan, n_src = mix->src_chan;
 	float **d = (float **) dst;
 	const float **s = (const float **) src;
 
@@ -94,16 +104,27 @@ channelmix_f32_n_m_c(struct channelmix *mix, void * SPA_RESTRICT dst[],
 			clear_c(d[i], n_samples);
 	}
 	else {
-		for (n = 0; n < n_samples; n++) {
-			for (i = 0; i < n_dst; i++) {
-				float sum = 0.0f;
-				for (j = 0; j < n_src; j++)
-					sum += s[j][n] * mix->matrix[i][j];
-				d[i][n] = sum;
+		for (i = 0; i < n_dst; i++) {
+			float *di = d[i];
+			float mj[n_src];
+			const float *sj[n_src];
+			uint32_t n_j = 0;
+
+			for (j = 0; j < n_src; j++) {
+				if (mix->matrix[i][j] == 0.0f)
+					continue;
+				mj[n_j] = mix->matrix[i][j];
+				sj[n_j++] = s[j];
+			}
+			if (n_j == 0) {
+				clear_c(di, n_samples);
+			} else if (n_j == 1) {
+				lr4_process(&mix->lr4[i], di, sj[0], mj[0], n_samples);
+			} else {
+				conv_c(di, sj, mj, n_j, n_samples);
+				lr4_process(&mix->lr4[i], di, di, 1.0f, n_samples);
 			}
 		}
-		for (i = 0; i < n_dst; i++)
-			lr4_process(&mix->lr4[i], d[i], d[i], 1.0f, n_samples);
 	}
 }
 
diff --git a/spa/plugins/audioconvert/channelmix-ops-sse.c b/spa/plugins/audioconvert/channelmix-ops-sse.c
index 37a02e22648b102a5b16f91c058f6c5b9426cfed..8311fb430648946f77fcca9c1bf34569c45f27ec 100644
--- a/spa/plugins/audioconvert/channelmix-ops-sse.c
+++ b/spa/plugins/audioconvert/channelmix-ops-sse.c
@@ -68,6 +68,97 @@ static inline void vol_sse(float *d, const float *s, float vol, uint32_t n_sampl
 	}
 }
 
+static inline void conv_sse(float *d, const float **s, float *c, uint32_t n_c, uint32_t n_samples)
+{
+	__m128 mi[n_c], sum[2];
+	uint32_t n, j, unrolled;
+	bool aligned = true;
+
+	for (j = 0; j < n_c; j++) {
+		mi[j] = _mm_set1_ps(c[j]);
+		aligned &= SPA_IS_ALIGNED(s[j], 16);
+	}
+
+	if (aligned && SPA_IS_ALIGNED(d, 16))
+		unrolled = n_samples & ~7;
+	else
+		unrolled = 0;
+
+	for (n = 0; n < unrolled; n += 8) {
+		sum[0] = sum[1] = _mm_setzero_ps();
+		for (j = 0; j < n_c; j++) {
+			sum[0] = _mm_add_ps(sum[0], _mm_mul_ps(_mm_load_ps(&s[j][n + 0]), mi[j]));
+			sum[1] = _mm_add_ps(sum[1], _mm_mul_ps(_mm_load_ps(&s[j][n + 4]), mi[j]));
+		}
+		_mm_store_ps(&d[n + 0], sum[0]);
+		_mm_store_ps(&d[n + 4], sum[1]);
+	}
+	for (; n < n_samples; n++) {
+		sum[0] = _mm_setzero_ps();
+		for (j = 0; j < n_c; j++)
+			sum[0] = _mm_add_ss(sum[0], _mm_mul_ss(_mm_load_ss(&s[j][n]), mi[j]));
+		_mm_store_ss(&d[n], sum[0]);
+	}
+}
+
+static inline void avg_sse(float *d, const float *s0, const float *s1, uint32_t n_samples)
+{
+	uint32_t n, unrolled;
+	__m128 half = _mm_set1_ps(0.5f);
+
+	if (SPA_IS_ALIGNED(d, 16) &&
+	    SPA_IS_ALIGNED(s0, 16) &&
+	    SPA_IS_ALIGNED(s1, 16))
+		unrolled = n_samples & ~7;
+	else
+		unrolled = 0;
+
+	for (n = 0; n < unrolled; n += 8) {
+		_mm_store_ps(&d[n + 0],
+				_mm_mul_ps(
+					_mm_add_ps(
+						_mm_load_ps(&s0[n + 0]),
+						_mm_load_ps(&s1[n + 0])),
+					half));
+		_mm_store_ps(&d[n + 4],
+				_mm_mul_ps(
+					_mm_add_ps(
+						_mm_load_ps(&s0[n + 4]),
+						_mm_load_ps(&s1[n + 4])),
+					half));
+	}
+
+	for (; n < n_samples; n++)
+		_mm_store_ss(&d[n],
+				_mm_mul_ss(
+					_mm_add_ss(
+						_mm_load_ss(&s0[n]),
+						_mm_load_ss(&s1[n])),
+					half));
+}
+
+static inline void sub_sse(float *d, const float *s0, const float *s1, uint32_t n_samples)
+{
+	uint32_t n, unrolled;
+
+	if (SPA_IS_ALIGNED(d, 16) &&
+	    SPA_IS_ALIGNED(s0, 16) &&
+	    SPA_IS_ALIGNED(s1, 16))
+		unrolled = n_samples & ~7;
+	else
+		unrolled = 0;
+
+	for (n = 0; n < unrolled; n += 8) {
+		_mm_store_ps(&d[n + 0],
+			_mm_sub_ps(_mm_load_ps(&s0[n + 0]), _mm_load_ps(&s1[n + 0])));
+		_mm_store_ps(&d[n + 4],
+			_mm_sub_ps(_mm_load_ps(&s0[n + 4]), _mm_load_ps(&s1[n + 4])));
+	}
+	for (; n < n_samples; n++)
+		_mm_store_ss(&d[n],
+			_mm_sub_ss(_mm_load_ss(&s0[n]), _mm_load_ss(&s1[n])));
+}
+
 void channelmix_copy_sse(struct channelmix *mix, void * SPA_RESTRICT dst[],
 		const void * SPA_RESTRICT src[], uint32_t n_samples)
 {
@@ -78,6 +169,167 @@ void channelmix_copy_sse(struct channelmix *mix, void * SPA_RESTRICT dst[],
 		vol_sse(d[i], s[i], mix->matrix[i][i], n_samples);
 }
 
+void
+channelmix_f32_n_m_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;
+	uint32_t i, j, n_dst = mix->dst_chan, n_src = mix->src_chan;
+
+	for (i = 0; i < n_dst; i++) {
+		float *di = d[i];
+		float mj[n_src];
+		const float *sj[n_src];
+		uint32_t n_j = 0;
+
+		for (j = 0; j < n_src; j++) {
+			if (mix->matrix[i][j] == 0.0f)
+				continue;
+			mj[n_j] = mix->matrix[i][j];
+			sj[n_j++] = s[j];
+		}
+		if (n_j == 0) {
+			clear_sse(di, n_samples);
+		} else if (n_j == 1) {
+			if (mix->lr4[i].active)
+				lr4_process(&mix->lr4[i], di, sj[0], mj[0], n_samples);
+			else
+				vol_sse(di, sj[0], mj[0], n_samples);
+		} else {
+			conv_sse(di, sj, mj, n_j, n_samples);
+			lr4_process(&mix->lr4[i], di, di, 1.0f, n_samples);
+		}
+	}
+}
+
+void
+channelmix_f32_2_3p1_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;
+	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 v2 = (mix->matrix[2][0] + mix->matrix[2][1]) * 0.5f;
+	const float v3 = (mix->matrix[3][0] + mix->matrix[3][1]) * 0.5f;
+
+	if (SPA_FLAG_IS_SET(mix->flags, CHANNELMIX_FLAG_ZERO)) {
+		for (i = 0; i < n_dst; i++)
+			clear_sse(d[i], n_samples);
+	}
+	else {
+		if (mix->widen == 0.0f) {
+			vol_sse(d[0], s[0], v0, n_samples);
+			vol_sse(d[1], s[1], v1, n_samples);
+			avg_sse(d[2], s[0], s[1], n_samples);
+		} else {
+			const __m128 mv0 = _mm_set1_ps(mix->matrix[0][0]);
+			const __m128 mv1 = _mm_set1_ps(mix->matrix[1][1]);
+			const __m128 mw = _mm_set1_ps(mix->widen);
+			const __m128 mh = _mm_set1_ps(0.5f);
+			__m128 t0[1], t1[1], w[1], c[1];
+
+			if (SPA_IS_ALIGNED(s[0], 16) &&
+			    SPA_IS_ALIGNED(s[1], 16) &&
+			    SPA_IS_ALIGNED(d[0], 16) &&
+			    SPA_IS_ALIGNED(d[1], 16) &&
+			    SPA_IS_ALIGNED(d[2], 16))
+				unrolled = n_samples & ~3;
+			else
+				unrolled = 0;
+
+			for(n = 0; n < unrolled; n += 4) {
+				t0[0] = _mm_load_ps(&s[0][n]);
+				t1[0] = _mm_load_ps(&s[1][n]);
+				c[0] = _mm_add_ps(t0[0], t1[0]);
+				w[0] = _mm_mul_ps(c[0], mw);
+				_mm_store_ps(&d[0][n], _mm_mul_ps(_mm_sub_ps(t0[0], w[0]), mv0));
+				_mm_store_ps(&d[1][n], _mm_mul_ps(_mm_sub_ps(t1[0], w[0]), mv1));
+				_mm_store_ps(&d[2][n], _mm_mul_ps(c[0], mh));
+			}
+			for (; n < n_samples; n++) {
+				t0[0] = _mm_load_ss(&s[0][n]);
+				t1[0] = _mm_load_ss(&s[1][n]);
+				c[0] = _mm_add_ss(t0[0], t1[0]);
+				w[0] = _mm_mul_ss(c[0], mw);
+				_mm_store_ss(&d[0][n], _mm_mul_ss(_mm_sub_ss(t0[0], w[0]), mv0));
+				_mm_store_ss(&d[1][n], _mm_mul_ss(_mm_sub_ss(t1[0], w[0]), mv1));
+				_mm_store_ss(&d[2][n], _mm_mul_ss(c[0], mh));
+			}
+		}
+		lr4_process(&mix->lr4[3], d[3], d[2], v3, n_samples);
+		lr4_process(&mix->lr4[2], d[2], d[2], v2, n_samples);
+	}
+}
+
+void
+channelmix_f32_2_5p1_sse(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 v4 = mix->matrix[4][0];
+	const float v5 = mix->matrix[5][1];
+
+	if (SPA_FLAG_IS_SET(mix->flags, CHANNELMIX_FLAG_ZERO)) {
+		for (i = 0; i < n_dst; i++)
+			clear_sse(d[i], n_samples);
+	}
+	else {
+		channelmix_f32_2_3p1_sse(mix, dst, src, n_samples);
+
+		if (mix->upmix != CHANNELMIX_UPMIX_PSD) {
+			vol_sse(d[4], s[0], v4, n_samples);
+			vol_sse(d[5], s[1], v5, n_samples);
+		} else {
+			sub_sse(d[4], 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[5], d[4], -v5, n_samples);
+			delay_convolve_run(mix->buffer[0], &mix->pos[0], BUFFER_SIZE, mix->delay,
+					mix->taps, mix->n_taps, d[4], d[4], v4, n_samples);
+		}
+	}
+}
+
+void
+channelmix_f32_2_7p1_sse(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 v4 = mix->matrix[4][0];
+	const float v5 = mix->matrix[5][1];
+	const float v6 = mix->matrix[6][0];
+	const float v7 = mix->matrix[7][1];
+
+	if (SPA_FLAG_IS_SET(mix->flags, CHANNELMIX_FLAG_ZERO)) {
+		for (i = 0; i < n_dst; i++)
+			clear_sse(d[i], n_samples);
+	}
+	else {
+		channelmix_f32_2_3p1_sse(mix, dst, src, n_samples);
+
+		vol_sse(d[4], s[0], v4, n_samples);
+		vol_sse(d[5], s[1], v5, n_samples);
+
+		if (mix->upmix != CHANNELMIX_UPMIX_PSD) {
+			vol_sse(d[6], s[0], v6, n_samples);
+			vol_sse(d[7], s[1], v7, n_samples);
+		} else {
+			sub_sse(d[6], 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[7], d[6], -v7, n_samples);
+			delay_convolve_run(mix->buffer[0], &mix->pos[0], BUFFER_SIZE, mix->delay,
+					mix->taps, mix->n_taps, d[6], d[6], v6, n_samples);
+		}
+	}
+}
 /* FL+FR+FC+LFE -> FL+FR */
 void
 channelmix_f32_3p1_2_sse(struct channelmix *mix, void * SPA_RESTRICT dst[],
diff --git a/spa/plugins/audioconvert/channelmix-ops.c b/spa/plugins/audioconvert/channelmix-ops.c
index 56faa7ea6cadeda7748873e4ec770fcdf6ac33e9..cfc449f0b0d739d0292ef6d68229c68e98817aa4 100644
--- a/spa/plugins/audioconvert/channelmix-ops.c
+++ b/spa/plugins/audioconvert/channelmix-ops.c
@@ -30,6 +30,7 @@
 #include <spa/support/cpu.h>
 #include <spa/support/log.h>
 #include <spa/utils/defs.h>
+#include <spa/debug/types.h>
 
 #include "channelmix-ops.h"
 #include "hilbert.h"
@@ -69,8 +70,17 @@ static const struct channelmix_info {
 	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),
+#if defined (HAVE_SSE)
+	MAKE(2, MASK_STEREO, 4, MASK_3_1, channelmix_f32_2_3p1_sse, SPA_CPU_FLAG_SSE),
+#endif
 	MAKE(2, MASK_STEREO, 4, MASK_3_1, channelmix_f32_2_3p1_c),
+#if defined (HAVE_SSE)
+	MAKE(2, MASK_STEREO, 6, MASK_5_1, channelmix_f32_2_5p1_sse, SPA_CPU_FLAG_SSE),
+#endif
 	MAKE(2, MASK_STEREO, 6, MASK_5_1, channelmix_f32_2_5p1_c),
+#if defined (HAVE_SSE)
+	MAKE(2, MASK_STEREO, 8, MASK_7_1, channelmix_f32_2_7p1_sse, SPA_CPU_FLAG_SSE),
+#endif
 	MAKE(2, MASK_STEREO, 8, MASK_7_1, channelmix_f32_2_7p1_c),
 #if defined (HAVE_SSE)
 	MAKE(4, MASK_3_1, 2, MASK_STEREO, channelmix_f32_3p1_2_sse, SPA_CPU_FLAG_SSE),
@@ -94,6 +104,9 @@ static const struct channelmix_info {
 	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),
 
+#if defined (HAVE_SSE)
+	MAKE(ANY, 0, ANY, 0, channelmix_f32_n_m_sse, SPA_CPU_FLAG_SSE),
+#endif
 	MAKE(ANY, 0, ANY, 0, channelmix_f32_n_m_c),
 };
 #undef MAKE
@@ -105,19 +118,18 @@ static const struct channelmix_info {
 static const struct channelmix_info *find_channelmix_info(uint32_t src_chan, uint64_t src_mask,
 		uint32_t dst_chan, uint64_t dst_mask, uint32_t cpu_flags)
 {
-	size_t i;
-	for (i = 0; i < SPA_N_ELEMENTS(channelmix_table); i++) {
-		if (!MATCH_CPU_FLAGS(channelmix_table[i].cpu_flags, cpu_flags))
+	SPA_FOR_EACH_ELEMENT_VAR(channelmix_table, info) {
+		if (!MATCH_CPU_FLAGS(info->cpu_flags, cpu_flags))
 			continue;
 
 		if (src_chan == dst_chan && src_mask == dst_mask)
-			return &channelmix_table[i];
+			return info;
 
-		if (MATCH_CHAN(channelmix_table[i].src_chan, src_chan) &&
-		    MATCH_CHAN(channelmix_table[i].dst_chan, dst_chan) &&
-		    MATCH_MASK(channelmix_table[i].src_mask, src_mask) &&
-		    MATCH_MASK(channelmix_table[i].dst_mask, dst_mask))
-			return &channelmix_table[i];
+		if (MATCH_CHAN(info->src_chan, src_chan) &&
+		    MATCH_CHAN(info->dst_chan, dst_chan) &&
+		    MATCH_MASK(info->src_mask, src_mask) &&
+		    MATCH_MASK(info->dst_mask, dst_mask))
+			return info;
 	}
 	return NULL;
 }
@@ -142,6 +154,8 @@ static int make_matrix(struct channelmix *mix)
 	float matrix[SPA_AUDIO_MAX_CHANNELS][SPA_AUDIO_MAX_CHANNELS] = {{ 0.0f }};
 	uint64_t src_mask = mix->src_mask;
 	uint64_t dst_mask = mix->dst_mask;
+	uint32_t src_chan = mix->src_chan;
+	uint32_t dst_chan = mix->dst_chan;
 	uint64_t unassigned, keep;
 	uint32_t i, j, ic, jc, matrix_encoding = MATRIX_NORMAL;
 	float clev = SQRT1_2;
@@ -157,7 +171,7 @@ static int make_matrix(struct channelmix *mix)
 	/* move the MONO mask to FRONT so that the lower bits can be shifted
 	 * away. */
 	if ((src_mask & (1Ull << SPA_AUDIO_CHANNEL_MONO)) != 0) {
-		if (mix->src_chan == 1)
+		if (src_chan == 1)
 			src_mask = 0;
 		else
 			src_mask |= (1ULL << SPA_AUDIO_CHANNEL_FC);
@@ -171,19 +185,19 @@ static int make_matrix(struct channelmix *mix)
 
 	/* unknown channels or just 1 channel */
 	if (src_mask == 0 || dst_mask == 0) {
-		if (mix->src_chan == 1) {
+		if (src_chan == 1) {
 			/* one FC/MONO src goes everywhere */
-			spa_log_debug(mix->log, "distribute FC/MONO");
+			spa_log_debug(mix->log, "distribute FC/MONO (%f)", 1.0f);
 			for (i = 0; i < SPA_AUDIO_MAX_CHANNELS; i++)
 				matrix[i][0]= 1.0f;
-		} else if (mix->dst_chan == 1) {
+		} else if (dst_chan == 1) {
 			/* one FC/MONO dst get average of everything */
-			spa_log_debug(mix->log, "average FC/MONO");
+			spa_log_debug(mix->log, "average FC/MONO (%f)", 1.0f / src_chan);
 			for (i = 0; i < SPA_AUDIO_MAX_CHANNELS; i++)
-				matrix[0][i]= 1.0f / mix->src_chan;
+				matrix[0][i]= 1.0f / src_chan;
 		} else {
 			/* just pair channels */
-			spa_log_debug(mix->log, "pairing channels");
+			spa_log_debug(mix->log, "pairing channels (%f)", 1.0f);
 			for (i = 0; i < SPA_AUDIO_MAX_CHANNELS; i++)
 				matrix[i][i]= 1.0f;
 		}
@@ -197,7 +211,7 @@ static int make_matrix(struct channelmix *mix)
 		spa_log_debug(mix->log, "matching channels");
 		for (i = 0; i < SPA_AUDIO_MAX_CHANNELS; i++) {
 			if ((src_mask & dst_mask & (1ULL << i))) {
-				spa_log_debug(mix->log, "matched %u", i);
+				spa_log_debug(mix->log, "matched channel %u (%f)", i, 1.0f);
 				matrix[i][i]= 1.0f;
 			}
 		}
@@ -222,11 +236,12 @@ static int make_matrix(struct channelmix *mix)
 
 	if (unassigned & FRONT){
 		if ((dst_mask & STEREO) == STEREO){
-			spa_log_debug(mix->log, "assign FC to STEREO");
 			if(src_mask & STEREO) {
+				spa_log_debug(mix->log, "assign FC to STEREO (%f)", clev);
 				_MATRIX(FL,FC) += clev;
 				_MATRIX(FR,FC) += clev;
 			} else {
+				spa_log_debug(mix->log, "assign FC to STEREO (%f)", SQRT1_2);
 				_MATRIX(FL,FC) += SQRT1_2;
 				_MATRIX(FR,FC) += SQRT1_2;
 			}
@@ -237,11 +252,13 @@ static int make_matrix(struct channelmix *mix)
 
 	if (unassigned & STEREO){
 		if (dst_mask & FRONT) {
-			spa_log_debug(mix->log, "assign STEREO to FC");
+			spa_log_debug(mix->log, "assign STEREO to FC (%f)", SQRT1_2);
 			_MATRIX(FC,FL) += SQRT1_2;
 			_MATRIX(FC,FR) += SQRT1_2;
-			if (src_mask & FRONT)
+			if (src_mask & FRONT) {
+				spa_log_debug(mix->log, "assign FC to FC (%f)", clev * SQRT2);
 				_MATRIX(FC,FC) = clev * SQRT2;
+			}
 			keep &= ~FRONT;
 		} else {
 			spa_log_warn(mix->log, "can't assign STEREO");
@@ -250,11 +267,11 @@ static int make_matrix(struct channelmix *mix)
 
 	if (unassigned & _MASK(RC)) {
 		if (dst_mask & REAR){
-			spa_log_debug(mix->log, "assign RC to RL+RR");
+			spa_log_debug(mix->log, "assign RC to RL+RR (%f)", SQRT1_2);
 			_MATRIX(RL,RC) += SQRT1_2;
 			_MATRIX(RR,RC) += SQRT1_2;
 		} else if (dst_mask & SIDE) {
-			spa_log_debug(mix->log, "assign RC to SL+SR");
+			spa_log_debug(mix->log, "assign RC to SL+SR (%f)", SQRT1_2);
 			_MATRIX(SL,RC) += SQRT1_2;
 			_MATRIX(SR,RC) += SQRT1_2;
 		} else if(dst_mask & STEREO) {
@@ -273,7 +290,7 @@ static int make_matrix(struct channelmix *mix)
 				_MATRIX(FR,RC) += slev * SQRT1_2;
 			}
 		} else if (dst_mask & FRONT) {
-			spa_log_debug(mix->log, "assign RC to FC");
+			spa_log_debug(mix->log, "assign RC to FC (%f)", slev * SQRT1_2);
 			_MATRIX(FC,RC) += slev * SQRT1_2;
 		} else {
 			spa_log_warn(mix->log, "can't assign RC");
@@ -294,8 +311,9 @@ static int make_matrix(struct channelmix *mix)
 				_MATRIX(SL,RL) += 1.0f;
 				_MATRIX(SR,RR) += 1.0f;
 			}
+			keep &= ~SIDE;
 		} else if (dst_mask & STEREO) {
-			spa_log_debug(mix->log, "assign RL+RR to FL+FR %f", slev);
+			spa_log_debug(mix->log, "assign RL+RR to FL+FR (%f)", slev);
 			if (matrix_encoding == MATRIX_DOLBY) {
 				_MATRIX(FL,RL) -= slev * SQRT1_2;
 				_MATRIX(FL,RR) -= slev * SQRT1_2;
@@ -311,7 +329,8 @@ static int make_matrix(struct channelmix *mix)
 				_MATRIX(FR,RR) += slev;
 			}
 		} else if (dst_mask & FRONT) {
-			spa_log_debug(mix->log, "assign RL+RR to FC");
+			spa_log_debug(mix->log, "assign RL+RR to FC (%f)",
+					slev * SQRT1_2);
 			_MATRIX(FC,RL)+= slev * SQRT1_2;
 			_MATRIX(FC,RR)+= slev * SQRT1_2;
 		} else {
@@ -321,36 +340,42 @@ static int make_matrix(struct channelmix *mix)
 
 	if (unassigned & SIDE) {
 		if (dst_mask & REAR) {
-			spa_log_debug(mix->log, "assign SL+SR to RL+RR");
 			if (src_mask & _MASK(RL)) {
+				spa_log_debug(mix->log, "assign SL+SR to RL+RR (%f)", SQRT1_2);
 				_MATRIX(RL,SL) += SQRT1_2;
 				_MATRIX(RR,SR) += SQRT1_2;
 			} else {
+				spa_log_debug(mix->log, "assign SL+SR to RL+RR (%f)", 1.0f);
 				_MATRIX(RL,SL) += 1.0f;
 				_MATRIX(RR,SR) += 1.0f;
 			}
+			keep &= ~REAR;
 		} else if (dst_mask & _MASK(RC)) {
-			spa_log_debug(mix->log, "assign SL+SR to RC");
+			spa_log_debug(mix->log, "assign SL+SR to RC (%f)", SQRT1_2);
 			_MATRIX(RC,SL)+= SQRT1_2;
 			_MATRIX(RC,SR)+= SQRT1_2;
 		} else if (dst_mask & STEREO) {
-			spa_log_debug(mix->log, "assign SL+SR to FL+FR");
 			if (matrix_encoding == MATRIX_DOLBY) {
+				spa_log_debug(mix->log, "assign SL+SR to FL+FR (%f)",
+						slev * SQRT1_2);
 				_MATRIX(FL,SL) -= slev * SQRT1_2;
 				_MATRIX(FL,SR) -= slev * SQRT1_2;
 				_MATRIX(FR,SL) += slev * SQRT1_2;
 				_MATRIX(FR,SR) += slev * SQRT1_2;
 			} else if (matrix_encoding == MATRIX_DPLII) {
+				spa_log_debug(mix->log, "assign SL+SR to FL+FR (%f / %f)",
+						slev * SQRT3_2, slev * SQRT1_2);
 				_MATRIX(FL,SL) -= slev * SQRT3_2;
 				_MATRIX(FL,SR) -= slev * SQRT1_2;
 				_MATRIX(FR,SL) += slev * SQRT1_2;
 				_MATRIX(FR,SR) += slev * SQRT3_2;
 			} else {
+				spa_log_debug(mix->log, "assign SL+SR to FL+FR (%f)", slev);
 				_MATRIX(FL,SL) += slev;
 				_MATRIX(FR,SR) += slev;
 			}
 		} else if (dst_mask & FRONT) {
-			spa_log_debug(mix->log, "assign SL+SR to FC");
+			spa_log_debug(mix->log, "assign SL+SR to FC (%f)", slev * SQRT1_2);
 			_MATRIX(FC,SL) += slev * SQRT1_2;
 			_MATRIX(FC,SR) += slev * SQRT1_2;
 		} else {
@@ -360,11 +385,11 @@ static int make_matrix(struct channelmix *mix)
 
 	if (unassigned & _MASK(FLC)) {
 		if (dst_mask & STEREO) {
-			spa_log_debug(mix->log, "assign FLC+FRC to FL+FR");
+			spa_log_debug(mix->log, "assign FLC+FRC to FL+FR (%f)", 1.0f);
 			_MATRIX(FL,FLC)+= 1.0f;
 			_MATRIX(FR,FRC)+= 1.0f;
 		} else if(dst_mask & FRONT) {
-			spa_log_debug(mix->log, "assign FLC+FRC to FC");
+			spa_log_debug(mix->log, "assign FLC+FRC to FC (%f)", SQRT1_2);
 			_MATRIX(FC,FLC)+= SQRT1_2;
 			_MATRIX(FC,FRC)+= SQRT1_2;
 		} else {
@@ -374,10 +399,11 @@ static int make_matrix(struct channelmix *mix)
 	if (unassigned & _MASK(LFE) &&
 	    SPA_FLAG_IS_SET(mix->options, CHANNELMIX_OPTION_MIX_LFE)) {
 		if (dst_mask & FRONT) {
-			spa_log_debug(mix->log, "assign LFE to FC");
+			spa_log_debug(mix->log, "assign LFE to FC (%f)", llev);
 			_MATRIX(FC,LFE) += llev;
 		} else if (dst_mask & STEREO) {
-			spa_log_debug(mix->log, "assign LFE to FL+FR");
+			spa_log_debug(mix->log, "assign LFE to FL+FR (%f)",
+					llev * SQRT1_2);
 			_MATRIX(FL,LFE) += llev * SQRT1_2;
 			_MATRIX(FR,LFE) += llev * SQRT1_2;
 		} else {
@@ -392,7 +418,7 @@ static int make_matrix(struct channelmix *mix)
 
 	if (unassigned & STEREO) {
 		if ((src_mask & FRONT) == FRONT) {
-			spa_log_debug(mix->log, "produce STEREO from FC");
+			spa_log_debug(mix->log, "produce STEREO from FC (%f)", clev);
 			_MATRIX(FL,FC) += clev;
 			_MATRIX(FR,FC) += clev;
 		} else {
@@ -401,7 +427,7 @@ static int make_matrix(struct channelmix *mix)
 	}
 	if (unassigned & FRONT) {
 		if ((src_mask & STEREO) == STEREO) {
-			spa_log_debug(mix->log, "produce FC from STEREO");
+			spa_log_debug(mix->log, "produce FC from STEREO (%f)", clev);
 			_MATRIX(FC,FL) += clev;
 			_MATRIX(FC,FR) += clev;
 			filter_fc = true;
@@ -411,12 +437,12 @@ static int make_matrix(struct channelmix *mix)
 	}
 	if (unassigned & _MASK(LFE)) {
 		if ((src_mask & STEREO) == STEREO) {
-			spa_log_debug(mix->log, "produce LFE from STEREO");
+			spa_log_debug(mix->log, "produce LFE from STEREO (%f)", llev);
 			_MATRIX(LFE,FL) += llev;
 			_MATRIX(LFE,FR) += llev;
 			filter_lfe = true;
 		} else if ((src_mask & FRONT) == FRONT) {
-			spa_log_debug(mix->log, "produce LFE from FC");
+			spa_log_debug(mix->log, "produce LFE from FC (%f)", llev);
 			_MATRIX(LFE,FC) += llev;
 			filter_lfe = true;
 		} else {
@@ -425,16 +451,16 @@ static int make_matrix(struct channelmix *mix)
 	}
 	if (unassigned & SIDE) {
 		if ((src_mask & REAR) == REAR) {
-			spa_log_debug(mix->log, "produce SIDE from REAR");
+			spa_log_debug(mix->log, "produce SIDE from REAR (%f)", 1.0f);
 			_MATRIX(SL,RL) += 1.0f;
 			_MATRIX(SR,RR) += 1.0f;
 		} else if ((src_mask & STEREO) == STEREO) {
-			spa_log_debug(mix->log, "produce SIDE from STEREO");
+			spa_log_debug(mix->log, "produce SIDE from STEREO (%f)", slev);
 			_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");
+			spa_log_debug(mix->log, "produce SIDE from FC (%f)", clev);
 			_MATRIX(SL,FC) += clev;
 			_MATRIX(SR,FC) += clev;
 		} else {
@@ -443,34 +469,77 @@ static int make_matrix(struct channelmix *mix)
 	}
 	if (unassigned & REAR) {
 		if ((src_mask & SIDE) == SIDE) {
-			spa_log_debug(mix->log, "produce REAR from SIDE");
+			spa_log_debug(mix->log, "produce REAR from SIDE (%f)", 1.0f);
 			_MATRIX(RL,SL) += 1.0f;
 			_MATRIX(RR,SR) += 1.0f;
 		} else if ((src_mask & STEREO) == STEREO) {
-			spa_log_debug(mix->log, "produce REAR from STEREO");
+			spa_log_debug(mix->log, "produce REAR from STEREO (%f)", slev);
 			_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");
+			spa_log_debug(mix->log, "produce REAR from FC (%f)", clev);
 			_MATRIX(RL,FC) += clev;
 			_MATRIX(RR,FC) += clev;
 		} else {
 			spa_log_debug(mix->log, "won't produce SIDE");
 		}
 	}
+	if (unassigned & _MASK(RC)) {
+		if ((src_mask & REAR) == REAR) {
+			spa_log_debug(mix->log, "produce RC from REAR (%f)", 0.5f);
+			_MATRIX(RC,RL) += 0.5f;
+			_MATRIX(RC,RR) += 0.5f;
+		} else if ((src_mask & SIDE) == SIDE) {
+			spa_log_debug(mix->log, "produce RC from SIDE (%f)", 0.5f);
+			_MATRIX(RC,SL) += 0.5f;
+			_MATRIX(RC,SR) += 0.5f;
+		} else if ((src_mask & STEREO) == STEREO) {
+			spa_log_debug(mix->log, "produce RC from STEREO (%f)", 0.5f);
+			_MATRIX(RC,FL) += 0.5f;
+			_MATRIX(RC,FR) += 0.5f;
+		} else if ((src_mask & FRONT) == FRONT &&
+			mix->upmix == CHANNELMIX_UPMIX_SIMPLE) {
+			spa_log_debug(mix->log, "produce RC from FC (%f)", slev);
+			_MATRIX(RC,FC) += slev;
+		} else {
+			spa_log_debug(mix->log, "won't produce RC");
+		}
+	}
 
 done:
 	for (jc = 0, ic = 0, i = 0; i < SPA_AUDIO_MAX_CHANNELS; i++) {
 		float sum = 0.0f;
+		char str[1024], str2[1024];
+		int idx = 0, idx2 = 0;
 		if ((dst_mask & (1UL << i)) == 0)
 			continue;
 		for (jc = 0, j = 0; j < SPA_AUDIO_MAX_CHANNELS; j++) {
 			if ((src_mask & (1UL << j)) == 0)
 				continue;
+			if (ic >= dst_chan || jc >= src_chan)
+				continue;
+
+			if (i == 0)
+				idx2 += snprintf(str2 + idx2, sizeof(str2) - idx2, "%-4.4s  ",
+						spa_debug_type_find_short_name(spa_type_audio_channel, j + 3));
+
 			mix->matrix_orig[ic][jc++] = matrix[i][j];
 			sum += fabs(matrix[i][j]);
+
+			if (matrix[i][j] == 0.0f)
+				idx += snprintf(str + idx, sizeof(str) - idx, "      ");
+			else
+				idx += snprintf(str + idx, sizeof(str) - idx, "%1.3f ", matrix[i][j]);
 		}
+		if (dst_mask != 0 && src_mask != 0 && sum > 0.0f) {
+			if (i == 0)
+				spa_log_info(mix->log, "     %s", str2);
+			spa_log_info(mix->log, "%-4.4s %s   %f",
+					spa_debug_type_find_short_name(spa_type_audio_channel, i + 3),
+					str, sum);
+		}
+
 		maxsum = SPA_MAX(maxsum, sum);
 		if (i == _CH(LFE) && mix->lfe_cutoff > 0.0f && filter_lfe) {
 			spa_log_info(mix->log, "channel %d is LFE cutoff:%f", ic, mix->lfe_cutoff);
@@ -486,11 +555,10 @@ done:
 	if (SPA_FLAG_IS_SET(mix->options, CHANNELMIX_OPTION_NORMALIZE) &&
 	    maxsum > 1.0f) {
 		spa_log_debug(mix->log, "normalize %f", maxsum);
-		for (i = 0; i < ic; i++)
-			for (j = 0; j < jc; j++)
+		for (i = 0; i < dst_chan; i++)
+			for (j = 0; j < src_chan; j++)
 		                mix->matrix_orig[i][j] /= maxsum;
 	}
-
 	return 0;
 }
 
@@ -524,6 +592,12 @@ static void impl_channelmix_set_volume(struct channelmix *mix, float volume, boo
 				mix->matrix[i][j] = mix->matrix_orig[i][j] * volumes[i];
 			}
 		}
+	} else if (n_channel_volumes == 0) {
+		for (i = 0; i < dst_chan; i++) {
+			for (j = 0; j < src_chan; j++) {
+				mix->matrix[i][j] = mix->matrix_orig[i][j] * vol;
+			}
+		}
 	}
 
 	SPA_FLAG_SET(mix->flags, CHANNELMIX_FLAG_ZERO);
diff --git a/spa/plugins/audioconvert/channelmix-ops.h b/spa/plugins/audioconvert/channelmix-ops.h
index 89737c315fc090a5d003080cfc9bd49bc98c9296..c134a9677994462730222b7b8f23ca5697b65326 100644
--- a/spa/plugins/audioconvert/channelmix-ops.h
+++ b/spa/plugins/audioconvert/channelmix-ops.h
@@ -109,10 +109,9 @@ static const struct channelmix_upmix_info {
 
 static inline uint32_t channelmix_upmix_from_label(const char *label)
 {
-	uint32_t i;
-	for (i = 0; i < SPA_N_ELEMENTS(channelmix_upmix_info); i++) {
-		if (spa_streq(channelmix_upmix_info[i].label, label))
-			return channelmix_upmix_info[i].upmix;
+	SPA_FOR_EACH_ELEMENT_VAR(channelmix_upmix_info, i) {
+		if (spa_streq(i->label, label))
+			return i->upmix;
 	}
 	return CHANNELMIX_UPMIX_NONE;
 }
@@ -147,6 +146,10 @@ DEFINE_FUNCTION(f32_7p1_4, c);
 
 #if defined (HAVE_SSE)
 DEFINE_FUNCTION(copy, sse);
+DEFINE_FUNCTION(f32_n_m, sse);
+DEFINE_FUNCTION(f32_2_3p1, sse);
+DEFINE_FUNCTION(f32_2_5p1, sse);
+DEFINE_FUNCTION(f32_2_7p1, sse);
 DEFINE_FUNCTION(f32_3p1_2, sse);
 DEFINE_FUNCTION(f32_5p1_2, sse);
 DEFINE_FUNCTION(f32_5p1_3p1, sse);
diff --git a/spa/plugins/audioconvert/fmt-ops-c.c b/spa/plugins/audioconvert/fmt-ops-c.c
index f3d91d0356544404020dc902edc2ad6358f4e9cd..92ecb5a0e116c340eae041d2d3dd5d87fdb29a81 100644
--- a/spa/plugins/audioconvert/fmt-ops-c.c
+++ b/spa/plugins/audioconvert/fmt-ops-c.c
@@ -230,38 +230,57 @@ lcnoise(uint32_t *state)
         return (int32_t)(*state);
 }
 
-static inline void update_noise_c(struct convert *conv, uint32_t n_samples)
+void conv_noise_none_c(struct convert *conv, float *noise, uint32_t n_samples)
+{
+	memset(noise, 0, n_samples * sizeof(float));
+}
+
+void conv_noise_rect_c(struct convert *conv, float *noise, uint32_t n_samples)
+{
+	uint32_t n;
+	uint32_t *state = &conv->random[0];
+	const float scale = conv->scale;
+
+	for (n = 0; n < n_samples; n++)
+		noise[n] = lcnoise(state) * scale;
+}
+
+void conv_noise_tri_c(struct convert *conv, float *noise, uint32_t n_samples)
+{
+	uint32_t n;
+	const float scale = conv->scale;
+	uint32_t *state = &conv->random[0];
+
+	for (n = 0; n < n_samples; n++)
+		noise[n] = (lcnoise(state) - lcnoise(state)) * scale;
+}
+
+void conv_noise_tri_hf_c(struct convert *conv, float *noise, uint32_t n_samples)
 {
 	uint32_t n;
-	float *noise = conv->noise, scale = conv->scale;
+	const float 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;
-		}
-		*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;
+	old = *prev;
+	for (n = 0; n < n_samples; n++) {
+		new = lcnoise(state);
+		noise[n] = (new - old) * scale;
+		old = new;
 	}
+	*prev = old;
+}
+
+void conv_noise_pattern_c(struct convert *conv, float *noise, uint32_t n_samples)
+{
+	uint32_t n;
+	const float scale = conv->scale;
+	int32_t *prev = &conv->prev[0], old;
+
+	old = *prev;
+	for (n = 0; n < n_samples; n++)
+		noise[n] = scale * (1-((old++>>10)&1));
+	*prev = old;
 }
 
 #define MAKE_D_noise(dname,dtype,func)						\
@@ -271,7 +290,7 @@ void conv_f32d_to_ ##dname## d_noise_c(struct convert *conv,			\
 {										\
 	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));			\
+	convert_update_noise(conv, noise, SPA_MIN(n_samples, noise_size));	\
 	for (i = 0; i < n_channels; i++) {					\
 		const float *s = src[i];					\
 		dtype *d = dst[i];						\
@@ -292,7 +311,7 @@ void conv_f32d_to_ ##dname## _noise_c(struct convert *conv,			\
 	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));			\
+	convert_update_noise(conv, noise, 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++) {				\
@@ -342,9 +361,10 @@ void conv_f32d_to_ ##dname## d_shaped_c(struct convert *conv,			\
                 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;			\
+	float *noise = conv->noise;						\
+	const float *ns = conv->ns;						\
 	uint32_t n, n_ns = conv->n_ns;						\
-	update_noise_c(conv, SPA_MIN(n_samples, noise_size));			\
+	convert_update_noise(conv, noise, SPA_MIN(n_samples, noise_size));	\
 	for (i = 0; i < n_channels; i++) {					\
 		const float *s = src[i];					\
 		dtype *d = dst[i];						\
@@ -366,9 +386,10 @@ void conv_f32d_to_ ##dname## _shaped_c(struct convert *conv,			\
 {										\
 	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;			\
+	float *noise = conv->noise;						\
+	const float *ns = conv->ns;						\
 	uint32_t n, n_ns = conv->n_ns;						\
-	update_noise_c(conv, SPA_MIN(n_samples, noise_size));			\
+	convert_update_noise(conv, noise, SPA_MIN(n_samples, noise_size));	\
 	for (i = 0; i < n_channels; i++) {					\
 		const float *s = src[i];					\
 		dtype *d = &d0[i];						\
diff --git a/spa/plugins/audioconvert/fmt-ops-sse2.c b/spa/plugins/audioconvert/fmt-ops-sse2.c
index 917bfa0dc80c0a36e68d0b9b5179913d846c5ff4..4e2fce72ffa30f4511890f04e6f50e8cfbc5cd29 100644
--- a/spa/plugins/audioconvert/fmt-ops-sse2.c
+++ b/spa/plugins/audioconvert/fmt-ops-sse2.c
@@ -576,61 +576,64 @@ conv_f32d_to_s32_sse2(struct convert *conv, void * SPA_RESTRICT dst[], const voi
 	i;						\
 })
 
+void conv_noise_rect_sse2(struct convert *conv, float *noise, uint32_t n_samples)
+{
+	uint32_t n;
+	const uint32_t *r = conv->random;
+	__m128 scale = _mm_set1_ps(conv->scale);
+	__m128i in[1];
+	__m128 out[1];
+
+	for (n = 0; n < n_samples; n += 4) {
+		in[0] = _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]);
+	}
+}
 
-static inline void update_noise_sse2(struct convert *conv, uint32_t n_samples)
+void conv_noise_tri_sse2(struct convert *conv, float *noise, 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;
+	const uint32_t *r = conv->random;
 	__m128 scale = _mm_set1_ps(conv->scale);
+	__m128i in[1];
 	__m128 out[1];
-	float *noise = SPA_PTR_ALIGN(conv->noise, 16, float);
+
+	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]);
+	}
+}
+
+void conv_noise_tri_hf_sse2(struct convert *conv, float *noise, uint32_t n_samples)
+{
+	uint32_t n;
+	int32_t *p = conv->prev;
+	const uint32_t *r = conv->random;
+	__m128 scale = _mm_set1_ps(conv->scale);
 	__m128i in[1], old[1], new[1];
+	__m128 out[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;
+	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]);
 }
 
 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)
+		float *noise, 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];
@@ -676,14 +679,16 @@ conv_f32d_to_s32_noise_sse2(struct convert *conv, void * SPA_RESTRICT dst[], con
 {
 	int32_t *d = dst[0];
 	uint32_t i, k, chunk, n_channels = conv->n_channels;
+	float *noise = conv->noise;
 
-	update_noise_sse2(conv, SPA_MIN(n_samples, conv->noise_size));
+	convert_update_noise(conv, noise, 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);
+			conv_f32d_to_s32_1s_noise_sse2(conv, &d[i + k*n_channels],
+					&s[k], noise, n_channels, chunk);
 		}
 	}
 }
@@ -1261,11 +1266,10 @@ conv_f32d_to_s16_sse2(struct convert *conv, void * SPA_RESTRICT dst[], const voi
 
 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 *noise, 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];
@@ -1312,25 +1316,26 @@ conv_f32d_to_s16_noise_sse2(struct convert *conv, void * SPA_RESTRICT dst[], con
 {
 	int16_t *d = dst[0];
 	uint32_t i, k, chunk, n_channels = conv->n_channels;
+	float *noise = conv->noise;
 
-	update_noise_sse2(conv, SPA_MIN(n_samples, conv->noise_size));
+	convert_update_noise(conv, noise, 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);
+			conv_f32d_to_s16_1s_noise_sse2(conv, &d[i + k*n_channels],
+					&s[k], noise, 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 *noise, 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];
@@ -1366,15 +1371,16 @@ conv_f32d_to_s16d_noise_sse2(struct convert *conv, void * SPA_RESTRICT dst[], co
 		uint32_t n_samples)
 {
 	uint32_t i, k, chunk, n_channels = conv->n_channels;
+	float *noise = conv->noise;
 
-	update_noise_sse2(conv, SPA_MIN(n_samples, conv->noise_size));
+	convert_update_noise(conv, noise, 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);
+			conv_f32_to_s16_1_noise_sse2(conv, &d[k], &s[k], noise, chunk);
 		}
 	}
 }
diff --git a/spa/plugins/audioconvert/fmt-ops.c b/spa/plugins/audioconvert/fmt-ops.c
index 443d59eec729b7b0ffb61ad2c554bddde9e1b918..4999a208d7fc3bd7d48cc11ee48864ade431f852 100644
--- a/spa/plugins/audioconvert/fmt-ops.c
+++ b/spa/plugins/audioconvert/fmt-ops.c
@@ -32,7 +32,8 @@
 
 #include "fmt-ops.h"
 
-#define DITHER_SIZE	(1<<10)
+#define NOISE_SIZE	(1<<10)
+#define RANDOM_SIZE	(16)
 
 typedef void (*convert_func_t) (struct convert *conv, void * SPA_RESTRICT dst[],
 		const void * SPA_RESTRICT src[], uint32_t n_samples);
@@ -358,15 +359,53 @@ static struct conv_info conv_table[] =
 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 conv_flags)
 {
-	size_t i;
-
-	for (i = 0; i < SPA_N_ELEMENTS(conv_table); i++) {
-		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_DITHER(conv_table[i].conv_flags, conv_flags))
-			return &conv_table[i];
+	SPA_FOR_EACH_ELEMENT_VAR(conv_table, c) {
+		if (c->src_fmt == src_fmt &&
+		    c->dst_fmt == dst_fmt &&
+		    MATCH_CHAN(c->n_channels, n_channels) &&
+		    MATCH_CPU_FLAGS(c->cpu_flags, cpu_flags) &&
+		    MATCH_DITHER(c->conv_flags, conv_flags))
+			return c;
+	}
+	return NULL;
+}
+
+typedef void (*noise_func_t) (struct convert *conv, float * noise, uint32_t n_samples);
+
+struct noise_info {
+	uint32_t method;
+
+	noise_func_t noise;
+	const char *name;
+
+	uint32_t cpu_flags;
+};
+
+#define MAKE(method,func,...) \
+	{  NOISE_METHOD_ ##method, func, #func , __VA_ARGS__ }
+
+static struct noise_info noise_table[] =
+{
+#if defined (HAVE_SSE2)
+	MAKE(RECTANGULAR, conv_noise_rect_sse2, SPA_CPU_FLAG_SSE2),
+	MAKE(TRIANGULAR, conv_noise_tri_sse2, SPA_CPU_FLAG_SSE2),
+	MAKE(TRIANGULAR_HF, conv_noise_tri_hf_sse2, SPA_CPU_FLAG_SSE2),
+#endif
+	MAKE(NONE, conv_noise_none_c),
+	MAKE(RECTANGULAR, conv_noise_rect_c),
+	MAKE(TRIANGULAR, conv_noise_tri_c),
+	MAKE(TRIANGULAR_HF, conv_noise_tri_hf_c),
+	MAKE(PATTERN, conv_noise_pattern_c),
+};
+#undef MAKE
+
+static const struct noise_info *find_noise_info(uint32_t method,
+		uint32_t cpu_flags)
+{
+	SPA_FOR_EACH_ELEMENT_VAR(noise_table, t) {
+		if (t->method == method &&
+		    MATCH_CPU_FLAGS(t->cpu_flags, cpu_flags))
+			return t;
 	}
 	return NULL;
 }
@@ -374,8 +413,8 @@ 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;
+	free(conv->data);
+	conv->data = NULL;
 }
 
 static bool need_dither(uint32_t format)
@@ -430,17 +469,14 @@ static const struct dither_info {
 
 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];
+	SPA_FOR_EACH_ELEMENT_VAR(dither_info, di) {
 		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 di;
 	}
 	return NULL;
 }
@@ -449,7 +485,8 @@ int convert_init(struct convert *conv)
 {
 	const struct conv_info *info;
 	const struct dither_info *dinfo;
-	uint32_t i, conv_flags;
+	const struct noise_info *ninfo;
+	uint32_t i, conv_flags, data_size[3];
 
 	conv->scale = 1.0f / (float)(INT32_MAX);
 
@@ -494,17 +531,31 @@ int convert_init(struct convert *conv)
 	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)
+	ninfo = find_noise_info(conv->noise_method, conv->cpu_flags);
+	if (ninfo == NULL)
+		return -ENOTSUP;
+
+	conv->noise_size = NOISE_SIZE;
+
+	data_size[0] = SPA_ROUND_UP(conv->noise_size * sizeof(float), FMT_OPS_MAX_ALIGN);
+	data_size[1] = SPA_ROUND_UP(RANDOM_SIZE * sizeof(uint32_t), FMT_OPS_MAX_ALIGN);
+	data_size[2] = SPA_ROUND_UP(RANDOM_SIZE * sizeof(int32_t), FMT_OPS_MAX_ALIGN);
+
+	conv->data = calloc(FMT_OPS_MAX_ALIGN +
+			data_size[0] + data_size[1] + data_size[2], 1);
+	if (conv->data == NULL)
 		return -errno;
 
-	for (i = 0; i < SPA_N_ELEMENTS(conv->random); i++)
+	conv->noise = SPA_PTR_ALIGN(conv->data, FMT_OPS_MAX_ALIGN, float);
+	conv->random = SPA_PTROFF(conv->noise, data_size[0], uint32_t);
+	conv->prev = SPA_PTROFF(conv->random, data_size[1], int32_t);
+
+	for (i = 0; i < RANDOM_SIZE; i++)
 		conv->random[i] = random();
 
 	conv->is_passthrough = conv->src_fmt == conv->dst_fmt;
 	conv->cpu_flags = info->cpu_flags;
+	conv->update_noise = ninfo->noise;
 	conv->process = info->process;
 	conv->free = impl_convert_free;
 	conv->func_name = info->name;
diff --git a/spa/plugins/audioconvert/fmt-ops.h b/spa/plugins/audioconvert/fmt-ops.h
index a4cd1de1125e0ea06b6cc777b03421a3fb6d508d..9280b8cb0f19b1e9f5c0908b90b7569472f16509 100644
--- a/spa/plugins/audioconvert/fmt-ops.h
+++ b/spa/plugins/audioconvert/fmt-ops.h
@@ -226,8 +226,8 @@ struct convert {
 	unsigned int is_passthrough:1;
 
 	float scale;
-	uint32_t random[16 + FMT_OPS_MAX_ALIGN/4];
-	int32_t prev[16 + FMT_OPS_MAX_ALIGN/4];
+	uint32_t *random;
+	int32_t *prev;
 #define NOISE_METHOD_NONE		0
 #define NOISE_METHOD_RECTANGULAR	1
 #define NOISE_METHOD_TRIANGULAR		2
@@ -240,9 +240,12 @@ struct convert {
 	uint32_t n_ns;
 	struct shaper shaper[64];
 
+	void (*update_noise) (struct convert *conv, float *noise, uint32_t n_samples);
 	void (*process) (struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
 			uint32_t n_samples);
 	void (*free) (struct convert *conv);
+
+	void *data;
 };
 
 int convert_init(struct convert *conv);
@@ -268,20 +271,37 @@ static const struct dither_method_info {
 
 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;
+	SPA_FOR_EACH_ELEMENT_VAR(dither_method_info, i) {
+		if (spa_streq(i->label, label))
+			return i->method;
 	}
 	return DITHER_METHOD_NONE;
 }
 
+#define convert_update_noise(conv,...)	(conv)->update_noise(conv, __VA_ARGS__)
 #define convert_process(conv,...)	(conv)->process(conv, __VA_ARGS__)
 #define convert_free(conv)		(conv)->free(conv)
 
-#define DEFINE_FUNCTION(name,arch) \
+#define DEFINE_NOISE_FUNCTION(name,arch)				\
+void conv_noise_##name##_##arch(struct convert *conv, float *noise,	\
+					uint32_t n_samples)
+
+DEFINE_NOISE_FUNCTION(none, c);
+DEFINE_NOISE_FUNCTION(rect, c);
+DEFINE_NOISE_FUNCTION(tri, c);
+DEFINE_NOISE_FUNCTION(tri_hf, c);
+DEFINE_NOISE_FUNCTION(pattern, c);
+#if defined(HAVE_SSE2)
+DEFINE_NOISE_FUNCTION(rect, sse2);
+DEFINE_NOISE_FUNCTION(tri, sse2);
+DEFINE_NOISE_FUNCTION(tri_hf, sse2);
+#endif
+
+#undef DEFINE_NOISE_FUNCTION
+
+#define DEFINE_FUNCTION(name,arch)						\
 void conv_##name##_##arch(struct convert *conv, void * SPA_RESTRICT dst[],	\
-		const void * SPA_RESTRICT src[], uint32_t n_samples)		\
+		const void * SPA_RESTRICT src[], uint32_t n_samples)
 
 DEFINE_FUNCTION(copy8d, c);
 DEFINE_FUNCTION(copy8, c);
diff --git a/spa/plugins/audioconvert/meson.build b/spa/plugins/audioconvert/meson.build
index ab0581e765ec5e44556f712b81f00196fe19f392..f08527a5a60581b9dc3aceaabedaab681b31328e 100644
--- a/spa/plugins/audioconvert/meson.build
+++ b/spa/plugins/audioconvert/meson.build
@@ -12,8 +12,8 @@ audioconvert_c = static_library('audioconvert_c',
     'biquad.c',
     'crossover.c',
     'volume-ops-c.c',
+    'peaks-ops-c.c',
     'resample-native-c.c',
-    'resample-peaks-c.c',
     'fmt-ops-c.c' ],
   c_args : ['-Ofast', '-ffast-math'],
   dependencies : [ spa_dep ],
@@ -24,8 +24,8 @@ simd_dependencies += audioconvert_c
 if have_sse
   audioconvert_sse = static_library('audioconvert_sse',
     ['resample-native-sse.c',
-      'resample-peaks-sse.c',
       'volume-ops-sse.c',
+      'peaks-ops-sse.c',
       'channelmix-ops-sse.c' ],
     c_args : [sse_args, '-Ofast', '-DHAVE_SSE'],
     dependencies : [ spa_dep ],
@@ -101,6 +101,7 @@ endif
 audioconvert_lib = static_library('audioconvert',
   ['fmt-ops.c',
     'channelmix-ops.c',
+    'peaks-ops.c',
     'resample-native.c',
     'resample-peaks.c',
     'volume-ops.c' ],
@@ -132,6 +133,7 @@ test_apps = [
   'test-audioconvert',
   'test-channelmix',
   'test-fmt-ops',
+  'test-peaks',
   'test-resample',
   ]
 
diff --git a/spa/plugins/audioconvert/resample-peaks-sse.c b/spa/plugins/audioconvert/peaks-ops-c.c
similarity index 57%
rename from spa/plugins/audioconvert/resample-peaks-sse.c
rename to spa/plugins/audioconvert/peaks-ops-c.c
index 26adb8b91a4b2035d20ebec13dfed70eac754dc2..45ab1dc694f15d0020044598a09598d81d0c8c92 100644
--- a/spa/plugins/audioconvert/resample-peaks-sse.c
+++ b/spa/plugins/audioconvert/peaks-ops-c.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"),
@@ -24,41 +24,27 @@
 
 #include <math.h>
 
-#include <xmmintrin.h>
+#include "peaks-ops.h"
 
-#include "resample-peaks-impl.h"
-
-static inline float hmax_ps(__m128 val)
+void peaks_min_max_c(struct peaks *peaks, const float * SPA_RESTRICT src,
+		uint32_t n_samples, float *min, float *max)
 {
-	__m128 t = _mm_movehl_ps(val, val);
-	t = _mm_max_ps(t, val);
-	val = _mm_shuffle_ps(t, t, 0x55);
-	val = _mm_max_ss(t, val);
-	return _mm_cvtss_f32(val);
+	uint32_t n;
+	float t, mi = *min, ma = *max;
+	for (n = 0; n < n_samples; n++) {
+		t = src[n];
+		mi = fminf(mi, t);
+		ma = fmaxf(ma, t);
+	}
+	*min = mi;
+	*max = ma;
 }
 
-static inline float find_abs_max_sse(const float *s, uint32_t n_samples, float m)
+float peaks_abs_max_c(struct peaks *peaks, const float * SPA_RESTRICT src,
+		uint32_t n_samples, float max)
 {
-	__m128 in[2], max;
-	uint32_t n, unrolled;
-	const __m128 mask = _mm_set1_ps(-0.0f);
-
-	max = _mm_set1_ps(m);
-
-	unrolled = n_samples & ~7;
-
-	for (n = 0; n < unrolled; n += 8) {
-		in[0] = _mm_loadu_ps(&s[n + 0]);
-		in[1] = _mm_loadu_ps(&s[n + 4]);
-		in[0] = _mm_andnot_ps(mask, in[0]);
-		in[1] = _mm_andnot_ps(mask, in[1]);
-		max = _mm_max_ps(max, in[0]);
-		max = _mm_max_ps(max, in[1]);
-	}
-	for (; n < n_samples; n++)
-		m = fmaxf(fabsf(s[n]), m);
-
-	return fmaxf(hmax_ps(max), m);
+	uint32_t n;
+	for (n = 0; n < n_samples; n++)
+		max = fmaxf(fabsf(src[n]), max);
+	return max;
 }
-
-MAKE_PEAKS(sse);
diff --git a/spa/plugins/audioconvert/peaks-ops-sse.c b/spa/plugins/audioconvert/peaks-ops-sse.c
new file mode 100644
index 0000000000000000000000000000000000000000..7ceb2a8c6e8c33a2e2884cea8245e24b8e574b6a
--- /dev/null
+++ b/spa/plugins/audioconvert/peaks-ops-sse.c
@@ -0,0 +1,122 @@
+/* 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 <math.h>
+
+#include <xmmintrin.h>
+
+#include "peaks-ops.h"
+
+static inline float hmin_ps(__m128 val)
+{
+	__m128 t = _mm_movehl_ps(val, val);
+	t = _mm_min_ps(t, val);
+	val = _mm_shuffle_ps(t, t, 0x55);
+	val = _mm_min_ss(t, val);
+	return _mm_cvtss_f32(val);
+}
+
+static inline float hmax_ps(__m128 val)
+{
+	__m128 t = _mm_movehl_ps(val, val);
+	t = _mm_max_ps(t, val);
+	val = _mm_shuffle_ps(t, t, 0x55);
+	val = _mm_max_ss(t, val);
+	return _mm_cvtss_f32(val);
+}
+
+void peaks_min_max_sse(struct peaks *peaks, const float * SPA_RESTRICT src,
+		uint32_t n_samples, float *min, float *max)
+{
+	uint32_t n;
+	__m128 in;
+	__m128 mi = _mm_set1_ps(*min);
+	__m128 ma = _mm_set1_ps(*max);
+
+	for (n = 0; n < n_samples; n++) {
+		if (SPA_IS_ALIGNED(&src[n], 16))
+			break;
+		in = _mm_set1_ps(src[n]);
+		mi = _mm_min_ps(mi, in);
+		ma = _mm_max_ps(ma, in);
+	}
+	for (; n + 15 < n_samples; n += 16) {
+		in = _mm_load_ps(&src[n + 0]);
+		mi = _mm_min_ps(mi, in);
+		ma = _mm_max_ps(ma, in);
+		in = _mm_load_ps(&src[n + 4]);
+		mi = _mm_min_ps(mi, in);
+		ma = _mm_max_ps(ma, in);
+		in = _mm_load_ps(&src[n + 8]);
+		mi = _mm_min_ps(mi, in);
+		ma = _mm_max_ps(ma, in);
+		in = _mm_load_ps(&src[n + 12]);
+		mi = _mm_min_ps(mi, in);
+		ma = _mm_max_ps(ma, in);
+	}
+	for (; n < n_samples; n++) {
+		in = _mm_set1_ps(src[n]);
+		mi = _mm_min_ps(mi, in);
+		ma = _mm_max_ps(ma, in);
+	}
+	*min = hmin_ps(mi);
+	*max = hmax_ps(ma);
+}
+
+float peaks_abs_max_sse(struct peaks *peaks, const float * SPA_RESTRICT src,
+		uint32_t n_samples, float max)
+{
+	uint32_t n;
+	__m128 in;
+	__m128 ma = _mm_set1_ps(max);
+	const __m128 mask = _mm_set1_ps(-0.0f);
+
+	for (n = 0; n < n_samples; n++) {
+		if (SPA_IS_ALIGNED(&src[n], 16))
+			break;
+		in = _mm_set1_ps(src[n]);
+		in = _mm_andnot_ps(mask, in);
+		ma = _mm_max_ps(ma, in);
+	}
+	for (; n + 15 < n_samples; n += 16) {
+		in = _mm_load_ps(&src[n + 0]);
+		in = _mm_andnot_ps(mask, in);
+		ma = _mm_max_ps(ma, in);
+		in = _mm_load_ps(&src[n + 4]);
+		in = _mm_andnot_ps(mask, in);
+		ma = _mm_max_ps(ma, in);
+		in = _mm_load_ps(&src[n + 8]);
+		in = _mm_andnot_ps(mask, in);
+		ma = _mm_max_ps(ma, in);
+		in = _mm_load_ps(&src[n + 12]);
+		in = _mm_andnot_ps(mask, in);
+		ma = _mm_max_ps(ma, in);
+	}
+	for (; n < n_samples; n++) {
+		in = _mm_set1_ps(src[n]);
+		in = _mm_andnot_ps(mask, in);
+		ma = _mm_max_ps(ma, in);
+	}
+	return hmax_ps(ma);
+}
diff --git a/spa/plugins/audioconvert/peaks-ops.c b/spa/plugins/audioconvert/peaks-ops.c
new file mode 100644
index 0000000000000000000000000000000000000000..b5cb25286588570982d48a66de015fc605d4524e
--- /dev/null
+++ b/spa/plugins/audioconvert/peaks-ops.c
@@ -0,0 +1,89 @@
+/* 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 <string.h>
+#include <stdio.h>
+#include <math.h>
+#include <errno.h>
+
+#include <spa/support/cpu.h>
+#include <spa/support/log.h>
+#include <spa/utils/defs.h>
+
+#include "peaks-ops.h"
+
+typedef void (*peaks_min_max_func_t) (struct peaks *peaks, const float * SPA_RESTRICT src,
+		uint32_t n_samples, float *min, float *max);
+typedef float (*peaks_abs_max_func_t) (struct peaks *peaks, const float * SPA_RESTRICT src,
+			uint32_t n_samples, float max);
+
+#define MAKE(min_max,abs_max,...) \
+	{ min_max, abs_max, #min_max , __VA_ARGS__ }
+
+static const struct peaks_info {
+	peaks_min_max_func_t min_max;
+	peaks_abs_max_func_t abs_max;
+	const char *name;
+	uint32_t cpu_flags;
+} peaks_table[] =
+{
+#if defined (HAVE_SSE)
+	MAKE(peaks_min_max_sse, peaks_abs_max_sse, SPA_CPU_FLAG_SSE),
+#endif
+	MAKE(peaks_min_max_c, peaks_abs_max_c),
+};
+#undef MAKE
+
+#define MATCH_CPU_FLAGS(a,b)	((a) == 0 || ((a) & (b)) == a)
+
+static const struct peaks_info *find_peaks_info(uint32_t cpu_flags)
+{
+	SPA_FOR_EACH_ELEMENT_VAR(peaks_table, t) {
+		if (MATCH_CPU_FLAGS(t->cpu_flags, cpu_flags))
+			return t;
+	}
+	return NULL;
+}
+
+static void impl_peaks_free(struct peaks *peaks)
+{
+	peaks->min_max = NULL;
+	peaks->abs_max = NULL;
+}
+
+int peaks_init(struct peaks *peaks)
+{
+	const struct peaks_info *info;
+
+	info = find_peaks_info(peaks->cpu_flags);
+	if (info == NULL)
+		return -ENOTSUP;
+
+	peaks->cpu_flags = info->cpu_flags;
+	peaks->func_name = info->name;
+	peaks->free = impl_peaks_free;
+	peaks->min_max = info->min_max;
+	peaks->abs_max = info->abs_max;
+	return 0;
+}
diff --git a/spa/plugins/audioconvert/peaks-ops.h b/spa/plugins/audioconvert/peaks-ops.h
new file mode 100644
index 0000000000000000000000000000000000000000..29da794100d6b9851b81bda2322414e62b3865bc
--- /dev/null
+++ b/spa/plugins/audioconvert/peaks-ops.h
@@ -0,0 +1,72 @@
+/* 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 <string.h>
+#include <stdio.h>
+
+#include <spa/utils/defs.h>
+
+struct peaks {
+	uint32_t cpu_flags;
+	const char *func_name;
+
+	struct spa_log *log;
+
+	uint32_t flags;
+
+	void (*min_max) (struct peaks *peaks, const float * SPA_RESTRICT src,
+		uint32_t n_samples, float *min, float *max);
+	float (*abs_max) (struct peaks *peaks, const float * SPA_RESTRICT src,
+			uint32_t n_samples, float max);
+
+	void (*free) (struct peaks *peaks);
+};
+
+int peaks_init(struct peaks *peaks);
+
+#define peaks_min_max(peaks,...)	(peaks)->min_max(peaks, __VA_ARGS__)
+#define peaks_abs_max(peaks,...)	(peaks)->abs_max(peaks, __VA_ARGS__)
+#define peaks_free(peaks)		(peaks)->free(peaks)
+
+#define DEFINE_MIN_MAX_FUNCTION(arch)				\
+void peaks_min_max_##arch(struct peaks *peaks,			\
+		const float * SPA_RESTRICT src,			\
+		uint32_t n_samples, float *min, float *max);
+
+#define DEFINE_ABS_MAX_FUNCTION(arch)				\
+float peaks_abs_max_##arch(struct peaks *peaks,			\
+		const float * SPA_RESTRICT src,			\
+		uint32_t n_samples, float max);
+
+#define PEAKS_OPS_MAX_ALIGN	16
+
+DEFINE_MIN_MAX_FUNCTION(c);
+DEFINE_ABS_MAX_FUNCTION(c);
+
+#if defined (HAVE_SSE)
+DEFINE_MIN_MAX_FUNCTION(sse);
+DEFINE_ABS_MAX_FUNCTION(sse);
+#endif
+
+#undef DEFINE_FUNCTION
diff --git a/spa/plugins/audioconvert/resample-native-impl.h b/spa/plugins/audioconvert/resample-native-impl.h
index a6c87fc3640ea8c6cd0b096ff656be912d1db387..5dfc40e6f1c5e8d4c165503095f8915b43499506 100644
--- a/spa/plugins/audioconvert/resample-native-impl.h
+++ b/spa/plugins/audioconvert/resample-native-impl.h
@@ -94,6 +94,14 @@ DEFINE_RESAMPLER(copy,arch)							\
 	*out_len = ooffs;							\
 }
 
+#define INC(index,phase,n_phases) \
+	index += inc;						\
+	phase += frac;						\
+	if (phase >= n_phases) {				\
+		phase -= n_phases;				\
+		index += 1;					\
+	}
+
 #define MAKE_RESAMPLER_FULL(arch)						\
 DEFINE_RESAMPLER(full,arch)							\
 {										\
@@ -114,17 +122,10 @@ DEFINE_RESAMPLER(full,arch)							\
 		phase = data->phase;						\
 										\
 		for (o = ooffs; o < olen && index + n_taps <= ilen; o++) {	\
-			const float *ip, *taps;					\
-										\
-			ip = &s[index];						\
-			taps = &data->filter[phase * stride];			\
-			index += inc;						\
-			phase += frac;						\
-			if (phase >= n_phases) {				\
-				phase -= n_phases;				\
-				index += 1;					\
-			}							\
-			inner_product_##arch(&d[o], ip, taps, n_taps);		\
+			inner_product_##arch(&d[o], &s[index],			\
+					&data->filter[phase * stride],		\
+					n_taps);				\
+			INC(index, phase, n_phases);				\
 		}								\
 	}									\
 	*in_len = index;							\
@@ -153,24 +154,13 @@ DEFINE_RESAMPLER(inter,arch)							\
 		phase = data->phase;						\
 										\
 		for (o = ooffs; o < olen && index + n_taps <= ilen; o++) {	\
-			const float *ip, *t0, *t1;				\
-			float ph, x;						\
-			uint32_t offset;					\
-										\
-			ip = &s[index];						\
-			ph = (float)phase * n_phases / out_rate;		\
-			offset = floor(ph);					\
-			x = ph - (float)offset;					\
-										\
-			t0 = &data->filter[(offset + 0) * stride];		\
-			t1 = &data->filter[(offset + 1) * stride];		\
-			index += inc;						\
-			phase += frac;						\
-			if (phase >= out_rate) {				\
-				phase -= out_rate;				\
-				index += 1;					\
-			}							\
-			inner_product_ip_##arch(&d[o], ip, t0, t1, x, n_taps);	\
+			float ph = (float)phase * n_phases / out_rate;		\
+			uint32_t offset = floorf(ph);				\
+			inner_product_ip_##arch(&d[o], &s[index],		\
+					&data->filter[(offset + 0) * stride],	\
+					&data->filter[(offset + 1) * stride],	\
+					ph - offset, n_taps);			\
+			INC(index, phase, out_rate);				\
 		}								\
 	}									\
 	*in_len = index;							\
diff --git a/spa/plugins/audioconvert/resample-native.c b/spa/plugins/audioconvert/resample-native.c
index b46a09fc1f0cf0e223b0a70083e107da4824bb4f..05ce54ba3192ed7e5f0f9db9e30ee5ba683d7854 100644
--- a/spa/plugins/audioconvert/resample-native.c
+++ b/spa/plugins/audioconvert/resample-native.c
@@ -66,6 +66,7 @@ static inline double window_blackman(double x, double n_taps)
 		(alpha / 2.0) * cos(2.0 * x);
 	return r;
 }
+
 static inline double window_cosh(double x, double n_taps)
 {
 	double r;
@@ -80,7 +81,7 @@ static inline double window_cosh(double x, double n_taps)
 	return r;
 }
 
-#define window window_cosh
+#define window (1 ? window_cosh : window_blackman)
 
 static int build_filter(float *taps, uint32_t stride, uint32_t n_taps, uint32_t n_phases, double cutoff)
 {
@@ -125,11 +126,10 @@ static struct resample_info resample_table[] =
 #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)
 {
-	size_t i;
-	for (i = 0; i < SPA_N_ELEMENTS(resample_table); i++) {
-		if (resample_table[i].format == format &&
-		    MATCH_CPU_FLAGS(resample_table[i].cpu_flags, cpu_flags))
-			return &resample_table[i];
+	SPA_FOR_EACH_ELEMENT_VAR(resample_table, t) {
+		if (t->format == format &&
+		    MATCH_CPU_FLAGS(t->cpu_flags, cpu_flags))
+			return t;
 	}
 	return NULL;
 }
@@ -304,7 +304,10 @@ static void impl_native_reset (struct resample *r)
 	if (d == NULL)
 		return;
 	memset(d->hist_mem, 0, r->channels * sizeof(float) * d->n_taps * 2);
-	d->hist = (d->n_taps / 2) - 1;
+	if (r->options & RESAMPLE_OPTION_PREFILL)
+		d->hist = d->n_taps - 1;
+	else
+		d->hist = (d->n_taps / 2) - 1;
 	d->phase = 0;
 }
 
diff --git a/spa/plugins/audioconvert/resample-peaks-impl.h b/spa/plugins/audioconvert/resample-peaks-impl.h
deleted file mode 100644
index 9d9d55cff12023bb3fedbcdab4a5d718f7e749c9..0000000000000000000000000000000000000000
--- a/spa/plugins/audioconvert/resample-peaks-impl.h
+++ /dev/null
@@ -1,92 +0,0 @@
-/* Spa
- *
- * Copyright © 2020 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 <spa/utils/defs.h>
-
-#include "resample.h"
-
-struct peaks_data {
-	uint32_t o_count;
-	uint32_t i_count;
-	float max_f[];
-};
-
-#define DEFINE_PEAKS(arch)						\
-void resample_peaks_process_##arch(struct resample *r,			\
-	const void * SPA_RESTRICT src[], uint32_t *in_len,		\
-	void * SPA_RESTRICT dst[], uint32_t *out_len)
-
-#define MAKE_PEAKS(arch)						\
-DEFINE_PEAKS(arch)							\
-{									\
-	struct peaks_data *pd = r->data;				\
-	uint32_t c, i, o, end, chunk, i_count, o_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);			\
-									\
-			m = find_abs_max_##arch(&s[i], chunk - i, m);	\
-									\
-			i += chunk;					\
-									\
-			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;				\
-	}								\
-}
-
-
-DEFINE_PEAKS(c);
-#if defined (HAVE_SSE)
-DEFINE_PEAKS(sse);
-#endif
diff --git a/spa/plugins/audioconvert/resample-peaks.c b/spa/plugins/audioconvert/resample-peaks.c
index 2fa52e875e4a65a55b89842f16759a15fb2a92ff..c151d60f32a9699deeea4db21a0fc1505b6a1151 100644
--- a/spa/plugins/audioconvert/resample-peaks.c
+++ b/spa/plugins/audioconvert/resample-peaks.c
@@ -27,40 +27,70 @@
 
 #include <spa/param/audio/format.h>
 
-#include "resample-peaks-impl.h"
-
-struct resample_info {
-	uint32_t format;
-	uint32_t cpu_flags;
-	void (*process) (struct resample *r,
-			const void * SPA_RESTRICT src[], uint32_t *in_len,
-			void * SPA_RESTRICT dst[], uint32_t *out_len);
+#include "peaks-ops.h"
+#include "resample.h"
+
+struct peaks_data {
+	uint32_t o_count;
+	uint32_t i_count;
+	struct peaks peaks;
+	float max_f[];
 };
 
-static struct resample_info resample_table[] =
+static void resample_peaks_process(struct resample *r,
+	const void * SPA_RESTRICT src[], uint32_t *in_len,
+	void * SPA_RESTRICT dst[], uint32_t *out_len)
 {
-#if defined (HAVE_SSE)
-	{ SPA_AUDIO_FORMAT_F32, SPA_CPU_FLAG_SSE, resample_peaks_process_sse, },
-#endif
-	{ SPA_AUDIO_FORMAT_F32, 0, resample_peaks_process_c, },
-};
+	struct peaks_data *pd = r->data;
+	uint32_t c, i, o, end, chunk, i_count, o_count;
 
-#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)
-{
-	size_t i;
-	for (i = 0; i < SPA_N_ELEMENTS(resample_table); i++) {
-		if (resample_table[i].format == format &&
-		    MATCH_CPU_FLAGS(resample_table[i].cpu_flags, cpu_flags)) {
-			return &resample_table[i];
+	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);
+
+			m = peaks_abs_max(&pd->peaks, &s[i], chunk - i, m);
+
+			i += chunk;
+
+			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;
 	}
-	return NULL;
 }
 
 static void impl_peaks_free(struct resample *r)
 {
-	free(r->data);
+	struct peaks_data *d = r->data;
+	if (d != NULL) {
+		peaks_free(&d->peaks);
+		free(d);
+	}
 	r->data = NULL;
 }
 
@@ -87,27 +117,32 @@ static void impl_peaks_reset (struct resample *r)
 int resample_peaks_init(struct resample *r)
 {
 	struct peaks_data *d;
-	const struct resample_info *info;
+	int res;
 
 	r->free = impl_peaks_free;
 	r->update_rate = impl_peaks_update_rate;
 
-	if ((info = find_resample_info(SPA_AUDIO_FORMAT_F32, r->cpu_flags)) == NULL)
-		return -ENOTSUP;
+	d = calloc(1, sizeof(struct peaks_data) + sizeof(float) * r->channels);
+	if (d == NULL)
+		return -errno;
+
+	d->peaks.log = r->log;
+	d->peaks.cpu_flags = r->cpu_flags;
+	if ((res = peaks_init(&d->peaks)) < 0) {
+		free(d);
+		return res;
+	}
 
-	r->process = info->process;
+	r->data = d;
+	r->process = resample_peaks_process;
 	r->reset = impl_peaks_reset;
 	r->delay = impl_peaks_delay;
 	r->in_len = impl_peaks_in_len;
 
-	d = r->data = calloc(1, sizeof(struct peaks_data) + sizeof(float) * r->channels);
-	if (r->data == NULL)
-		return -errno;
-
 	spa_log_debug(r->log, "peaks %p: in:%d out:%d features:%08x:%08x", r,
-			r->i_rate, r->o_rate, r->cpu_flags, info->cpu_flags);
+			r->i_rate, r->o_rate, r->cpu_flags, d->peaks.cpu_flags);
 
-	r->cpu_flags = info->cpu_flags;
+	r->cpu_flags = d->peaks.cpu_flags;
 	d->i_count = d->o_count = 0;
 	return 0;
 }
diff --git a/spa/plugins/audioconvert/resample.h b/spa/plugins/audioconvert/resample.h
index 0b9180ccb37cdb736d8b32a0edd81fa184d9f27e..b1c89d593bf929f9ea8050cb44e0c1c9d4f760cc 100644
--- a/spa/plugins/audioconvert/resample.h
+++ b/spa/plugins/audioconvert/resample.h
@@ -32,6 +32,8 @@
 
 struct resample {
 	struct spa_log *log;
+#define RESAMPLE_OPTION_PREFILL		(1<<0)
+	uint32_t options;
 	uint32_t cpu_flags;
 	const char *func_name;
 
diff --git a/spa/plugins/audioconvert/test-audioconvert.c b/spa/plugins/audioconvert/test-audioconvert.c
index 883803d06c30d1e562d4b60076984c404748325c..99ba86681fbe0358c34c45672f5c585beb91cfc1 100644
--- a/spa/plugins/audioconvert/test-audioconvert.c
+++ b/spa/plugins/audioconvert/test-audioconvert.c
@@ -681,9 +681,9 @@ static int run_convert(struct context *ctx, struct data *in_data,
 
 			res = memcmp(b->datas[j].data, out_data->data[k], out_data->size);
 			if (res != 0) {
-				fprintf(stderr, "error plane %d\n", j);
+				fprintf(stderr, "error port %d plane %d\n", i, j);
 				spa_debug_mem(0, b->datas[j].data, out_data->size);
-				spa_debug_mem(0, out_data->data[j], out_data->size);
+				spa_debug_mem(0, out_data->data[k], out_data->size);
 			}
 			spa_assert_se(res == 0);
 
@@ -702,7 +702,9 @@ 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_5_6p1[] = { 0.953553438f, 0.953553438f, 0.953553438f, 0.953553438f };
 static const float data_f32p_6[] = { 0.6f, 0.6f, 0.6f, 0.6f };
+static const float data_f32p_6_6p1[] = { 1.053553343f, 1.053553343f, 1.053553343f, 1.053553343f };
 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 };
 
@@ -710,6 +712,14 @@ 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_6p1[] = { 0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f, 0.7f,
+				      0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f, 0.7f,
+				      0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f, 0.7f,
+				      0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f, 0.7f };
+static const float data_f32_6p1_from_5p1[] = { 0.1f, 0.2f, 0.3f, 0.4f, 0.55f, 0.5f, 0.6f,
+				      0.1f, 0.2f, 0.3f, 0.4f, 0.55f, 0.5f, 0.6f,
+				      0.1f, 0.2f, 0.3f, 0.4f, 0.55f, 0.5f, 0.6f,
+				      0.1f, 0.2f, 0.3f, 0.4f, 0.55f, 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,
@@ -740,6 +750,27 @@ struct data dsp_5p1 = {
 	.size = sizeof(float) * 4
 };
 
+struct data dsp_5p1_from_6p1 = {
+	.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_6p1, data_f32p_6_6p1, },
+	.size = sizeof(float) * 4
+};
+
+
 struct data dsp_5p1_remapped = {
 	.mode = SPA_PARAM_PORT_CONFIG_MODE_dsp,
 	.info = SPA_AUDIO_INFO_RAW_INIT(
@@ -760,6 +791,68 @@ struct data dsp_5p1_remapped = {
 	.size = sizeof(float) * 4
 };
 
+struct data dsp_5p1_remapped_from_6p1 = {
+	.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_6p1, data_f32p_6_6p1, data_f32p_3, data_f32p_4, },
+	.size = sizeof(float) * 4
+};
+
+struct data dsp_6p1 = {
+	.mode = SPA_PARAM_PORT_CONFIG_MODE_dsp,
+	.info = SPA_AUDIO_INFO_RAW_INIT(
+		.format = SPA_AUDIO_FORMAT_F32,
+		.rate = 48000,
+		.channels = 7,
+		.position = {
+			SPA_AUDIO_CHANNEL_FL,
+			SPA_AUDIO_CHANNEL_FR,
+			SPA_AUDIO_CHANNEL_FC,
+			SPA_AUDIO_CHANNEL_LFE,
+			SPA_AUDIO_CHANNEL_RC,
+			SPA_AUDIO_CHANNEL_RL,
+			SPA_AUDIO_CHANNEL_RR,
+		}),
+	.ports = 7,
+	.planes = 1,
+	.data = { data_f32p_1, data_f32p_2, data_f32p_3, data_f32p_4, data_f32p_5, data_f32p_6, data_f32p_7 },
+	.size = sizeof(float) * 4
+};
+
+struct data dsp_6p1_side = {
+	.mode = SPA_PARAM_PORT_CONFIG_MODE_dsp,
+	.info = SPA_AUDIO_INFO_RAW_INIT(
+		.format = SPA_AUDIO_FORMAT_F32,
+		.rate = 48000,
+		.channels = 7,
+		.position = {
+			SPA_AUDIO_CHANNEL_FL,
+			SPA_AUDIO_CHANNEL_FR,
+			SPA_AUDIO_CHANNEL_FC,
+			SPA_AUDIO_CHANNEL_LFE,
+			SPA_AUDIO_CHANNEL_RC,
+			SPA_AUDIO_CHANNEL_SL,
+			SPA_AUDIO_CHANNEL_SR,
+		}),
+	.ports = 7,
+	.planes = 1,
+	.data = { data_f32p_1, data_f32p_2, data_f32p_3, data_f32p_4, data_f32p_5, data_f32p_6, data_f32p_7 },
+	.size = sizeof(float) * 4
+};
+
 struct data dsp_7p1_remapped = {
 	.mode = SPA_PARAM_PORT_CONFIG_MODE_dsp,
 	.info = SPA_AUDIO_INFO_RAW_INIT(
@@ -862,6 +955,90 @@ struct data conv_f32p_48000_5p1 = {
 	.size = sizeof(float) * 4
 };
 
+struct data conv_f32_48000_6p1 = {
+	.mode = SPA_PARAM_PORT_CONFIG_MODE_convert,
+	.info = SPA_AUDIO_INFO_RAW_INIT(
+		.format = SPA_AUDIO_FORMAT_F32,
+		.rate = 48000,
+		.channels = 7,
+		.position = {
+			SPA_AUDIO_CHANNEL_FL,
+			SPA_AUDIO_CHANNEL_FR,
+			SPA_AUDIO_CHANNEL_FC,
+			SPA_AUDIO_CHANNEL_LFE,
+			SPA_AUDIO_CHANNEL_RC,
+			SPA_AUDIO_CHANNEL_RL,
+			SPA_AUDIO_CHANNEL_RR,
+		}),
+	.ports = 1,
+	.planes = 1,
+	.data = { data_f32_6p1 },
+	.size = sizeof(data_f32_6p1)
+};
+
+struct data conv_f32_48000_6p1_from_5p1 = {
+	.mode = SPA_PARAM_PORT_CONFIG_MODE_convert,
+	.info = SPA_AUDIO_INFO_RAW_INIT(
+		.format = SPA_AUDIO_FORMAT_F32,
+		.rate = 48000,
+		.channels = 7,
+		.position = {
+			SPA_AUDIO_CHANNEL_FL,
+			SPA_AUDIO_CHANNEL_FR,
+			SPA_AUDIO_CHANNEL_FC,
+			SPA_AUDIO_CHANNEL_LFE,
+			SPA_AUDIO_CHANNEL_RC,
+			SPA_AUDIO_CHANNEL_RL,
+			SPA_AUDIO_CHANNEL_RR,
+		}),
+	.ports = 1,
+	.planes = 1,
+	.data = { data_f32_6p1_from_5p1 },
+	.size = sizeof(data_f32_6p1_from_5p1)
+};
+
+struct data conv_f32_48000_6p1_side = {
+	.mode = SPA_PARAM_PORT_CONFIG_MODE_convert,
+	.info = SPA_AUDIO_INFO_RAW_INIT(
+		.format = SPA_AUDIO_FORMAT_F32,
+		.rate = 48000,
+		.channels = 7,
+		.position = {
+			SPA_AUDIO_CHANNEL_FL,
+			SPA_AUDIO_CHANNEL_FR,
+			SPA_AUDIO_CHANNEL_FC,
+			SPA_AUDIO_CHANNEL_LFE,
+			SPA_AUDIO_CHANNEL_RC,
+			SPA_AUDIO_CHANNEL_SL,
+			SPA_AUDIO_CHANNEL_SR,
+		}),
+	.ports = 1,
+	.planes = 1,
+	.data = { data_f32_6p1 },
+	.size = sizeof(data_f32_6p1)
+};
+
+struct data conv_f32p_48000_6p1 = {
+	.mode = SPA_PARAM_PORT_CONFIG_MODE_convert,
+	.info = SPA_AUDIO_INFO_RAW_INIT(
+		.format = SPA_AUDIO_FORMAT_F32P,
+		.rate = 48000,
+		.channels = 7,
+		.position = {
+			SPA_AUDIO_CHANNEL_FL,
+			SPA_AUDIO_CHANNEL_FR,
+			SPA_AUDIO_CHANNEL_FC,
+			SPA_AUDIO_CHANNEL_LFE,
+			SPA_AUDIO_CHANNEL_RC,
+			SPA_AUDIO_CHANNEL_RL,
+			SPA_AUDIO_CHANNEL_RR,
+		}),
+	.ports = 1,
+	.planes = 7,
+	.data = { data_f32p_1, data_f32p_2, data_f32p_3, data_f32p_4, data_f32p_5, data_f32p_6, data_f32p_7 },
+	.size = sizeof(float) * 4
+};
+
 struct data conv_f32p_48000_5p1_remapped = {
 	.mode = SPA_PARAM_PORT_CONFIG_MODE_convert,
 	.info = SPA_AUDIO_INFO_RAW_INIT(
@@ -918,6 +1095,11 @@ static int test_convert_remap_dsp(struct context *ctx)
 	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);
+	run_convert(ctx, &dsp_6p1, &conv_f32p_48000_6p1);
+	run_convert(ctx, &dsp_6p1, &conv_f32_48000_6p1);
+	run_convert(ctx, &dsp_6p1_side, &conv_f32_48000_6p1_side);
+
+	run_convert(ctx, &dsp_5p1, &conv_f32_48000_6p1_from_5p1);
 	return 0;
 }
 
@@ -933,9 +1115,16 @@ static int test_convert_remap_conv(struct context *ctx)
 	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_6p1, &dsp_6p1);
+	run_convert(ctx, &conv_f32_48000_6p1, &dsp_6p1);
+	run_convert(ctx, &conv_f32_48000_6p1_side, &dsp_6p1_side);
 	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);
+
+	run_convert(ctx, &conv_f32_48000_6p1, &dsp_5p1_from_6p1);
+	run_convert(ctx, &conv_f32_48000_6p1_side, &dsp_5p1_from_6p1);
+	run_convert(ctx, &conv_f32_48000_6p1, &dsp_5p1_remapped_from_6p1);
 	return 0;
 }
 
diff --git a/spa/plugins/audioconvert/test-channelmix.c b/spa/plugins/audioconvert/test-channelmix.c
index 705ddf4de34372443c9ae2250351fbafba544896..9e052e20d85a284bbadf079a4ac99aebd44b6fdf 100644
--- a/spa/plugins/audioconvert/test-channelmix.c
+++ b/spa/plugins/audioconvert/test-channelmix.c
@@ -22,6 +22,8 @@
  * DEALINGS IN THE SOFTWARE.
  */
 
+#include "config.h"
+
 #include <string.h>
 #include <stdio.h>
 #include <stdlib.h>
@@ -32,20 +34,26 @@
 #include <spa/support/log-impl.h>
 #include <spa/debug/mem.h>
 
+static uint32_t cpu_flags;
+
 SPA_LOG_IMPL(logger);
 
 #define MATRIX(...) (float[]) { __VA_ARGS__ }
 
+#include "test-helper.h"
 #include "channelmix-ops.c"
+
+#define CLOSE_ENOUGH(a,b)	(fabs((a)-(b)) < 0.000001f)
+
 static void dump_matrix(struct channelmix *mix, float *coeff)
 {
 	uint32_t i, j;
 
 	for (i = 0; i < mix->dst_chan; i++) {
 		for (j = 0; j < mix->src_chan; j++) {
-			float v = mix->matrix_orig[i][j];
+			float v = mix->matrix[i][j];
 			spa_log_debug(mix->log, "%d %d: %f <-> %f", i, j, v, *coeff);
-			spa_assert_se(fabs(v - *coeff) < 0.000001);
+			spa_assert_se(CLOSE_ENOUGH(v, *coeff));
 			coeff++;
 		}
 	}
@@ -65,7 +73,8 @@ static void test_mix(uint32_t src_chan, uint32_t src_mask, uint32_t dst_chan, ui
 	mix.dst_mask = dst_mask;
 	mix.log = &logger.log;
 
-	channelmix_init(&mix);
+	spa_assert_se(channelmix_init(&mix) == 0);
+	channelmix_set_volume(&mix, 1.0f, false, 0, NULL);
 	dump_matrix(&mix, coeff);
 }
 
@@ -211,6 +220,46 @@ static void test_5p1_N(void)
 			       0.0, 0.0, 0.0, 0.0, 0.0, 1.0));
 }
 
+static void test_6p1_N(void)
+{
+	test_mix(7, _M(FL)|_M(FR)|_M(LFE)|_M(FC)|_M(RC)|_M(SL)|_M(SR), 1, _M(MONO), 0,
+			MATRIX(0.707107, 0.707107, 1.0, 0.0, 0.5, 0.5, 0.5));
+	test_mix(7, _M(FL)|_M(FR)|_M(LFE)|_M(FC)|_M(SL)|_M(SR)|_M(RC),
+		 6, _M(FL)|_M(FR)|_M(LFE)|_M(FC)|_M(SL)|_M(SR), 0,
+			MATRIX(1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
+			       0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0,
+			       0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0,
+			       0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0,
+			       0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.707107,
+			       0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.707107));
+	test_mix(7, _M(FL)|_M(FR)|_M(LFE)|_M(FC)|_M(SL)|_M(SR)|_M(RC),
+		 6, _M(FL)|_M(FR)|_M(LFE)|_M(FC)|_M(RL)|_M(RR), 0,
+			MATRIX(1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
+			       0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0,
+			       0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0,
+			       0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0,
+			       0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.707107,
+			       0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.707107));
+	test_mix(7, _M(FL)|_M(FR)|_M(LFE)|_M(FC)|_M(RC)|_M(RL)|_M(RR),
+		 6, _M(FL)|_M(FR)|_M(LFE)|_M(FC)|_M(RL)|_M(RR), 0,
+			MATRIX(1.0, 0.0, 0.0, 0.0, 0.0,      0.0, 0.0,
+			       0.0, 1.0, 0.0, 0.0, 0.0,      0.0, 0.0,
+			       0.0, 0.0, 1.0, 0.0, 0.0,      0.0, 0.0,
+			       0.0, 0.0, 0.0, 1.0, 0.0,      0.0, 0.0,
+			       0.0, 0.0, 0.0, 0.0, 0.707107, 1.0, 0.0,
+			       0.0, 0.0, 0.0, 0.0, 0.707107, 0.0, 1.0));
+	test_mix(7, _M(FL)|_M(FR)|_M(LFE)|_M(FC)|_M(SL)|_M(SR)|_M(RC),
+		 8, _M(FL)|_M(FR)|_M(LFE)|_M(FC)|_M(SL)|_M(SR)|_M(RL)|_M(RR), 0,
+			MATRIX(1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
+			       0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0,
+			       0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0,
+			       0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0,
+			       0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0,
+			       0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0,
+			       0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.707107,
+			       0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.707107));
+}
+
 static void test_7p1_N(void)
 {
 	test_mix(8, _M(FL)|_M(FR)|_M(LFE)|_M(FC)|_M(SL)|_M(SR)|_M(RL)|_M(RR), 1, _M(MONO), 0,
@@ -220,17 +269,106 @@ static void test_7p1_N(void)
 			       0.0, 1.0, 0.707107, 0.0, 0.0, 0.707107, 0.0, 0.707107));
 }
 
+static void check_samples(float **s1, float **s2, uint32_t n_s, uint32_t n_samples)
+{
+	uint32_t i, j;
+	for (i = 0; i < n_s; i++) {
+		for (j = 0; j < n_samples; j++) {
+			spa_assert_se(CLOSE_ENOUGH(s1[i][j], s2[i][j]));
+		}
+	}
+}
+
+static void run_n_m_impl(struct channelmix *mix, const void **src, uint32_t n_samples)
+{
+	uint32_t dst_chan = mix->dst_chan, i;
+	float dst_c_data[dst_chan][n_samples];
+	float dst_x_data[dst_chan][n_samples];
+	void *dst_c[dst_chan], *dst_x[dst_chan];
+
+	for (i = 0; i < dst_chan; i++) {
+		dst_c[i] = dst_c_data[i];
+		dst_x[i] = dst_x_data[i];
+	}
+
+	channelmix_f32_n_m_c(mix, dst_c, src, n_samples);
+
+	channelmix_f32_n_m_c(mix, dst_x, src, n_samples);
+	check_samples((float**)dst_c, (float**)dst_x, dst_chan, n_samples);
+
+#if defined(HAVE_SSE)
+	if (cpu_flags & SPA_CPU_FLAG_SSE) {
+		channelmix_f32_n_m_sse(mix, dst_x, src, n_samples);
+		check_samples((float**)dst_c, (float**)dst_x, dst_chan, n_samples);
+	}
+#endif
+}
+
+static void test_n_m_impl(void)
+{
+	struct channelmix mix;
+	unsigned int i, j;
+#define N_SAMPLES	251
+	float src_data[16][N_SAMPLES], *src[16];
+
+	spa_log_debug(&logger.log, "start");
+
+	for (i = 0; i < 16; i++) {
+		for (j = 0; j < N_SAMPLES; j++)
+			src_data[i][j] = (drand48() - 0.5f) * 2.5f;
+		src[i] = src_data[i];
+	}
+
+	spa_zero(mix);
+	mix.src_chan = 16;
+	mix.dst_chan = 12;
+	mix.log = &logger.log;
+	mix.cpu_flags = cpu_flags;
+	spa_assert_se(channelmix_init(&mix) == 0);
+	channelmix_set_volume(&mix, 1.0f, false, 0, NULL);
+
+	/* identity matrix */
+	run_n_m_impl(&mix, (const void**)src, N_SAMPLES);
+
+	/* some zero destination */
+	mix.matrix_orig[2][2] = 0.0f;
+	mix.matrix_orig[7][7] = 0.0f;
+	channelmix_set_volume(&mix, 1.0f, false, 0, NULL);
+	run_n_m_impl(&mix, (const void**)src, N_SAMPLES);
+
+	/* random matrix */
+	for (i = 0; i < mix.dst_chan; i++) {
+		for (j = 0; j < mix.src_chan; j++) {
+			mix.matrix_orig[i][j] = drand48() - 0.5f;
+		}
+	}
+	channelmix_set_volume(&mix, 1.0f, false, 0, NULL);
+
+	run_n_m_impl(&mix, (const void**)src, N_SAMPLES);
+}
+
 int main(int argc, char *argv[])
 {
+	struct timespec ts;
+
+	clock_gettime(CLOCK_MONOTONIC, &ts);
+	srand48(SPA_TIMESPEC_TO_NSEC(&ts));
+
 	logger.log.level = SPA_LOG_LEVEL_TRACE;
 
+	cpu_flags = get_cpu_flags();
+	printf("got CPU flags %d\n", cpu_flags);
+
 	test_1_N_MONO();
 	test_1_N_FC();
 	test_N_1();
 	test_3p1_N();
 	test_4_N();
 	test_5p1_N();
+	test_6p1_N();
 	test_7p1_N();
 
+	test_n_m_impl();
+
 	return 0;
 }
diff --git a/spa/plugins/audioconvert/test-peaks.c b/spa/plugins/audioconvert/test-peaks.c
new file mode 100644
index 0000000000000000000000000000000000000000..3f7d093ff908ad4132ddd34338c1015427ac9790
--- /dev/null
+++ b/spa/plugins/audioconvert/test-peaks.c
@@ -0,0 +1,128 @@
+/* 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/support/log-impl.h>
+#include <spa/debug/mem.h>
+
+SPA_LOG_IMPL(logger);
+
+static uint32_t cpu_flags;
+
+#include "test-helper.h"
+
+#include "peaks-ops.c"
+
+static void test_impl(void)
+{
+	struct peaks peaks;
+	unsigned int i;
+	float vals[1038];
+	float min[2] = { 0.0f, 0.0f }, max[2] = { 0.0f, 0.0f }, absmax[2] = { 0.0f, 0.0f };
+
+	for (i = 0; i < SPA_N_ELEMENTS(vals); i++)
+		vals[i] = (drand48() - 0.5f) * 2.5f;
+
+	peaks_min_max_c(&peaks, &vals[1], SPA_N_ELEMENTS(vals) - 1, &min[0], &max[0]);
+	printf("c peaks min:%f max:%f\n", min[0], max[0]);
+
+	absmax[0] = peaks_abs_max_c(&peaks, &vals[1], SPA_N_ELEMENTS(vals) - 1, 0.0f);
+	printf("c peaks abs-max:%f\n", absmax[0]);
+
+#if defined(HAVE_SSE)
+	if (cpu_flags & SPA_CPU_FLAG_SSE) {
+		peaks_min_max_sse(&peaks, &vals[1], SPA_N_ELEMENTS(vals) - 1, &min[1], &max[1]);
+		printf("sse peaks min:%f max:%f\n", min[1], max[1]);
+
+		absmax[1] = peaks_abs_max_sse(&peaks, &vals[1], SPA_N_ELEMENTS(vals) - 1, 0.0f);
+		printf("sse peaks abs-max:%f\n", absmax[1]);
+
+		spa_assert(min[0] == min[1]);
+		spa_assert(max[0] == max[1]);
+		spa_assert(absmax[0] == absmax[1]);
+	}
+#endif
+
+}
+
+static void test_min_max(void)
+{
+	struct peaks peaks;
+	const float vals[] = { 0.0f, 0.5f, -0.5f, 0.0f, 0.6f, -0.8f, -0.5f, 0.0f };
+	float min = 0.0f, max = 0.0f;
+
+	spa_zero(peaks);
+	peaks.log = &logger.log;
+	peaks.cpu_flags = cpu_flags;
+	peaks_init(&peaks);
+
+	peaks_min_max(&peaks, vals, SPA_N_ELEMENTS(vals), &min, &max);
+
+	spa_assert(min == -0.8f);
+	spa_assert(max == 0.6f);
+}
+
+static void test_abs_max(void)
+{
+	struct peaks peaks;
+	const float vals[] = { 0.0f, 0.5f, -0.5f, 0.0f, 0.6f, -0.8f, -0.5f, 0.0f };
+	float max = 0.0f;
+
+	spa_zero(peaks);
+	peaks.log = &logger.log;
+	peaks.cpu_flags = cpu_flags;
+	peaks_init(&peaks);
+
+	max = peaks_abs_max(&peaks, vals, SPA_N_ELEMENTS(vals), max);
+
+	spa_assert(max == 0.8f);
+}
+
+int main(int argc, char *argv[])
+{
+	struct timespec ts;
+
+	clock_gettime(CLOCK_MONOTONIC, &ts);
+	srand48(SPA_TIMESPEC_TO_NSEC(&ts));
+
+	logger.log.level = SPA_LOG_LEVEL_TRACE;
+
+	cpu_flags = get_cpu_flags();
+	printf("got CPU flags %d\n", cpu_flags);
+
+	test_impl();
+
+	test_min_max();
+	test_abs_max();
+
+	return 0;
+}
diff --git a/spa/plugins/audioconvert/test-source.c b/spa/plugins/audioconvert/test-source.c
index 55a65c5d554ab88736174358a579d543efdb293d..b352323fd0e82d1e042aa574cb6f4a0f89bdf5a9 100644
--- a/spa/plugins/audioconvert/test-source.c
+++ b/spa/plugins/audioconvert/test-source.c
@@ -535,8 +535,12 @@ static int calc_width(struct spa_audio_info *info)
 	case SPA_AUDIO_FORMAT_S24:
 	case SPA_AUDIO_FORMAT_S24_OE:
 		return 3;
-	default:
+	case SPA_AUDIO_FORMAT_S32P:
+	case SPA_AUDIO_FORMAT_S32:
+	case SPA_AUDIO_FORMAT_S32_OE:
 		return 4;
+	default:
+		return 0;
 	}
 }
 
@@ -571,6 +575,12 @@ static int port_set_format(void *object,
 			return res;
 
 		port->stride = calc_width(&info);
+		if (port->stride == 0)
+			return -EINVAL;
+		if (info.info.raw.rate == 0 ||
+		    info.info.raw.channels == 0)
+			return -EINVAL;
+
 		if (SPA_AUDIO_FORMAT_IS_PLANAR(info.info.raw.format)) {
 			port->blocks = info.info.raw.channels;
 		}
diff --git a/spa/plugins/audioconvert/volume-ops.c b/spa/plugins/audioconvert/volume-ops.c
index 98887e46f5217b708bc2f76cf0b09fada094ba1d..6890cfa7d4389eabea138cb0b15727d8264afa0a 100644
--- a/spa/plugins/audioconvert/volume-ops.c
+++ b/spa/plugins/audioconvert/volume-ops.c
@@ -56,11 +56,9 @@ static const struct volume_info {
 
 static const struct volume_info *find_volume_info(uint32_t cpu_flags)
 {
-	size_t i;
-	for (i = 0; i < SPA_N_ELEMENTS(volume_table); i++) {
-		if (!MATCH_CPU_FLAGS(volume_table[i].cpu_flags, cpu_flags))
-			continue;
-		return &volume_table[i];
+	SPA_FOR_EACH_ELEMENT_VAR(volume_table, t) {
+		if (MATCH_CPU_FLAGS(t->cpu_flags, cpu_flags))
+			return t;
 	}
 	return NULL;
 }
diff --git a/spa/plugins/audiomixer/audiomixer.c b/spa/plugins/audiomixer/audiomixer.c
index 6aba1a0120361f7713f5d439bcba5b9f9a5735e0..41e7e3f7ee5dcd5c527718c328bcf2c150a64f4f 100644
--- a/spa/plugins/audiomixer/audiomixer.c
+++ b/spa/plugins/audiomixer/audiomixer.c
@@ -566,6 +566,10 @@ static int port_set_format(void *object,
 			if (memcmp(&info, &this->format, sizeof(struct spa_audio_info)))
 				return -EINVAL;
 		} else {
+			if (info.info.raw.format == 0 ||
+			    info.info.raw.channels == 0)
+				return -EINVAL;
+
 			this->ops.fmt = info.info.raw.format;
 			this->ops.n_channels = info.info.raw.channels;
 			this->ops.cpu_flags = this->cpu_flags;
diff --git a/spa/plugins/audiomixer/mix-ops-avx.c b/spa/plugins/audiomixer/mix-ops-avx.c
index a5e3b5b11763874aa123a3dd05e22c82c9ca4691..3c5aa6bf7486a81d7a6430cfecf3ade14c9ced5d 100644
--- a/spa/plugins/audiomixer/mix-ops-avx.c
+++ b/spa/plugins/audiomixer/mix-ops-avx.c
@@ -32,62 +32,6 @@
 
 #include <immintrin.h>
 
-static inline void mix_4(float * dst,
-		const float * SPA_RESTRICT src0,
-		const float * SPA_RESTRICT src1,
-		const float * SPA_RESTRICT src2,
-		uint32_t n_samples)
-{
-	uint32_t n, unrolled;
-
-	if (SPA_IS_ALIGNED(src0, 32) &&
-	    SPA_IS_ALIGNED(src1, 32) &&
-	    SPA_IS_ALIGNED(src2, 32) &&
-	    SPA_IS_ALIGNED(dst, 32))
-		unrolled = n_samples & ~15;
-	else
-		unrolled = 0;
-
-	for (n = 0; n < unrolled; n += 16) {
-		__m256 in1[4], in2[4];
-
-		in1[0] = _mm256_load_ps(&dst[n + 0]);
-		in2[0] = _mm256_load_ps(&dst[n + 8]);
-		in1[1] = _mm256_load_ps(&src0[n + 0]);
-		in2[1] = _mm256_load_ps(&src0[n + 8]);
-		in1[2] = _mm256_load_ps(&src1[n + 0]);
-		in2[2] = _mm256_load_ps(&src1[n + 8]);
-		in1[3] = _mm256_load_ps(&src2[n + 0]);
-		in2[3] = _mm256_load_ps(&src2[n + 8]);
-
-		in1[0] = _mm256_add_ps(in1[0], in1[1]);
-		in2[0] = _mm256_add_ps(in2[0], in2[1]);
-		in1[2] = _mm256_add_ps(in1[2], in1[3]);
-		in2[2] = _mm256_add_ps(in2[2], in2[3]);
-		in1[0] = _mm256_add_ps(in1[0], in1[2]);
-		in2[0] = _mm256_add_ps(in2[0], in2[2]);
-
-		_mm256_store_ps(&dst[n + 0], in1[0]);
-		_mm256_store_ps(&dst[n + 8], in2[0]);
-	}
-	for (; n < n_samples; n++) {
-		__m128 in[4];
-		in[0] = _mm_load_ss(&dst[n]),
-		in[1] = _mm_load_ss(&src0[n]),
-		in[2] = _mm_load_ss(&src1[n]),
-		in[3] = _mm_load_ss(&src2[n]),
-		in[0] = _mm_add_ss(in[0], in[1]);
-		in[2] = _mm_add_ss(in[2], in[3]);
-		in[0] = _mm_add_ss(in[0], in[2]);
-		_mm_store_ss(&dst[n], in[0]);
-	}
-}
-
-
-static inline void mix_2(float * dst, const float * SPA_RESTRICT src, uint32_t n_samples)
-{
-}
-
 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)
diff --git a/spa/plugins/audiomixer/mix-ops.c b/spa/plugins/audiomixer/mix-ops.c
index 816f44d1e4b66de60707ed5a3b3b371bf70d5c09..b4599268758f74a617c769145ca9af1142f8cdad 100644
--- a/spa/plugins/audiomixer/mix-ops.c
+++ b/spa/plugins/audiomixer/mix-ops.c
@@ -98,13 +98,11 @@ static struct mix_info mix_table[] =
 static const struct mix_info *find_mix_info(uint32_t fmt,
 		uint32_t n_channels, uint32_t cpu_flags)
 {
-	size_t i;
-
-	for (i = 0; i < SPA_N_ELEMENTS(mix_table); i++) {
-		if (mix_table[i].fmt == fmt &&
-		    MATCH_CHAN(mix_table[i].n_channels, n_channels) &&
-		    MATCH_CPU_FLAGS(mix_table[i].cpu_flags, cpu_flags))
-			return &mix_table[i];
+	SPA_FOR_EACH_ELEMENT_VAR(mix_table, t) {
+		if (t->fmt == fmt &&
+		    MATCH_CHAN(t->n_channels, n_channels) &&
+		    MATCH_CPU_FLAGS(t->cpu_flags, cpu_flags))
+			return t;
 	}
 	return NULL;
 }
diff --git a/spa/plugins/audiotestsrc/audiotestsrc.c b/spa/plugins/audiotestsrc/audiotestsrc.c
index ff58e6a2aa7225617349eb79ef65e026f1a08e9f..f009b9a29b5a40c78c32d5c7852c3317fb7cabd6 100644
--- a/spa/plugins/audiotestsrc/audiotestsrc.c
+++ b/spa/plugins/audiotestsrc/audiotestsrc.c
@@ -743,6 +743,10 @@ port_set_format(struct impl *this,
 		if (spa_format_audio_raw_parse(format, &info.info.raw) < 0)
 			return -EINVAL;
 
+		if (info.info.raw.rate == 0 ||
+		    info.info.raw.channels == 0)
+			return -EINVAL;
+
 		switch (info.info.raw.format) {
 		case SPA_AUDIO_FORMAT_S16:
 			idx = 0;
diff --git a/spa/plugins/avb/avb-pcm.c b/spa/plugins/avb/avb-pcm.c
index c7ccb05b666753640a1ceafe5f8d793747de81da..8fe9503a379be7f684d7907a6cc29a9c47b95698 100644
--- a/spa/plugins/avb/avb-pcm.c
+++ b/spa/plugins/avb/avb-pcm.c
@@ -439,7 +439,7 @@ static int spa_format_to_aaf(uint32_t format)
 	}
 }
 
-static int frame_size(uint32_t format)
+static int calc_frame_size(uint32_t format)
 {
 	switch(format) {
 	case SPA_AUDIO_FORMAT_F32_BE:
@@ -647,7 +647,7 @@ static int setup_packet(struct state *state, struct spa_audio_info *fmt)
 		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_BIT_DEPTH(pdu, calc_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);
 	}
@@ -690,14 +690,22 @@ int spa_avb_clear_format(struct state *state)
 
 int spa_avb_set_format(struct state *state, struct spa_audio_info *fmt, uint32_t flags)
 {
-	int res;
+	int res, frame_size;
 	struct props *p = &state->props;
 
+	frame_size = calc_frame_size(fmt->info.raw.format);
+	if (frame_size == 0)
+		return -EINVAL;
+
+	if (fmt->info.raw.rate == 0 ||
+	    fmt->info.raw.channels == 0)
+		return -EINVAL;
+
 	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);
+	state->stride = state->channels * frame_size;
 
 	if ((res = setup_socket(state)) < 0)
 		return res;
@@ -877,7 +885,7 @@ static int flush_write(struct state *state, uint64_t current_time)
 		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);
+		n = sendmsg(state->sockfd, &state->msg, MSG_NOSIGNAL);
 		if (n < 0 || n != (ssize_t)state->pdu_size) {
 			spa_log_error(state->log, "sendmdg() failed: %m");
 		}
diff --git a/spa/plugins/bluez5/a2dp-codec-aac.c b/spa/plugins/bluez5/a2dp-codec-aac.c
index c14ff2af9af32e7fbc2f9f103b8dd4abe57e2535..46a87407b686d363126e830d9bbe5e29b4481922 100644
--- a/spa/plugins/bluez5/a2dp-codec-aac.c
+++ b/spa/plugins/bluez5/a2dp-codec-aac.c
@@ -34,7 +34,7 @@
 #include <fdk-aac/aacdecoder_lib.h>
 
 #include "rtp.h"
-#include "a2dp-codecs.h"
+#include "media-codecs.h"
 
 static struct spa_log *log;
 static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.codecs.aac");
@@ -65,7 +65,7 @@ struct impl {
 	int samplesize;
 };
 
-static int codec_fill_caps(const struct a2dp_codec *codec, uint32_t flags,
+static int codec_fill_caps(const struct media_codec *codec, uint32_t flags,
 		uint8_t caps[A2DP_MAX_CAPS_SIZE])
 {
 	static const a2dp_aac_t a2dp_aac = {
@@ -98,7 +98,7 @@ static int codec_fill_caps(const struct a2dp_codec *codec, uint32_t flags,
 	return sizeof(a2dp_aac);
 }
 
-static const struct a2dp_codec_config
+static const struct media_codec_config
 aac_frequencies[] = {
 	{ AAC_SAMPLING_FREQ_48000, 48000, 11 },
 	{ AAC_SAMPLING_FREQ_44100, 44100, 10 },
@@ -114,7 +114,7 @@ aac_frequencies[] = {
 	{ AAC_SAMPLING_FREQ_8000,  8000,  0 },
 };
 
-static const struct a2dp_codec_config
+static const struct media_codec_config
 aac_channel_modes[] = {
 	{ AAC_CHANNELS_2, 2, 1 },
 	{ AAC_CHANNELS_1, 1, 0 },
@@ -130,9 +130,9 @@ static int get_valid_aac_bitrate(a2dp_aac_t *conf)
 	}
 }
 
-static int codec_select_config(const struct a2dp_codec *codec, uint32_t flags,
+static int codec_select_config(const struct media_codec *codec, uint32_t flags,
 		const void *caps, size_t caps_size,
-		const struct a2dp_codec_audio_info *info,
+		const struct media_codec_audio_info *info,
 		const struct spa_dict *settings, uint8_t config[A2DP_MAX_CAPS_SIZE])
 {
 	a2dp_aac_t conf;
@@ -154,7 +154,7 @@ static int codec_select_config(const struct a2dp_codec *codec, uint32_t flags,
 	else
 		return -ENOTSUP;
 
-	if ((i = a2dp_codec_select_config(aac_frequencies,
+	if ((i = media_codec_select_config(aac_frequencies,
 					  SPA_N_ELEMENTS(aac_frequencies),
 					  AAC_GET_FREQUENCY(conf),
 				    	  info ? info->rate : A2DP_CODEC_DEFAULT_RATE
@@ -162,7 +162,7 @@ static int codec_select_config(const struct a2dp_codec *codec, uint32_t flags,
 		return -ENOTSUP;
 	AAC_SET_FREQUENCY(conf, aac_frequencies[i].config);
 
-	if ((i = a2dp_codec_select_config(aac_channel_modes,
+	if ((i = media_codec_select_config(aac_channel_modes,
 					  SPA_N_ELEMENTS(aac_channel_modes),
 					  conf.channels,
 				    	  info ? info->channels : A2DP_CODEC_DEFAULT_CHANNELS
@@ -177,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, uint32_t flags,
+static int codec_enum_config(const struct media_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)
 {
@@ -206,11 +206,11 @@ static int codec_enum_config(const struct a2dp_codec *codec, uint32_t flags,
 	spa_pod_builder_push_choice(b, &f[1], SPA_CHOICE_None, 0);
 	choice = (struct spa_pod_choice*)spa_pod_builder_frame(b, &f[1]);
 	i = 0;
-	for (size_t j = 0; j < SPA_N_ELEMENTS(aac_frequencies); j++) {
-		if (AAC_GET_FREQUENCY(conf) & aac_frequencies[j].config) {
+	SPA_FOR_EACH_ELEMENT_VAR(aac_frequencies, f) {
+		if (AAC_GET_FREQUENCY(conf) & f->config) {
 			if (i++ == 0)
-				spa_pod_builder_int(b, aac_frequencies[j].value);
-			spa_pod_builder_int(b, aac_frequencies[j].value);
+				spa_pod_builder_int(b, f->value);
+			spa_pod_builder_int(b, f->value);
 		}
 	}
 	if (i == 0)
@@ -246,7 +246,7 @@ static int codec_enum_config(const struct a2dp_codec *codec, uint32_t flags,
 	return *param == NULL ? -EIO : 1;
 }
 
-static int codec_validate_config(const struct a2dp_codec *codec, uint32_t flags,
+static int codec_validate_config(const struct media_codec *codec, uint32_t flags,
 			const void *caps, size_t caps_size,
 			struct spa_audio_info *info)
 {
@@ -272,14 +272,15 @@ static int codec_validate_config(const struct a2dp_codec *codec, uint32_t flags,
 	if (!(conf.object_type & (AAC_OBJECT_TYPE_MPEG2_AAC_LC |
 					AAC_OBJECT_TYPE_MPEG4_AAC_LC)))
 		return -EINVAL;
-
-	for (j = 0; j < SPA_N_ELEMENTS(aac_frequencies); ++j) {
-		if (AAC_GET_FREQUENCY(conf) & aac_frequencies[j].config) {
-			info->info.raw.rate = aac_frequencies[j].value;
+	j = 0;
+	SPA_FOR_EACH_ELEMENT_VAR(aac_frequencies, f) {
+		if (AAC_GET_FREQUENCY(conf) & f->config) {
+			info->info.raw.rate = f->value;
+			j++;
 			break;
 		}
 	}
-	if (j == SPA_N_ELEMENTS(aac_frequencies))
+	if (j == 0)
 		return -EINVAL;
 
 	if (conf.channels & AAC_CHANNELS_2) {
@@ -296,7 +297,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, uint32_t flags, const struct spa_dict *settings)
+static void *codec_init_props(const struct media_codec *codec, uint32_t flags, const struct spa_dict *settings)
 {
 	struct props *p = calloc(1, sizeof(struct props));
 	const char *str;
@@ -316,7 +317,7 @@ static void codec_clear_props(void *props)
 	free(props);
 }
 
-static void *codec_init(const struct a2dp_codec *codec, uint32_t flags,
+static void *codec_init(const struct media_codec *codec, uint32_t flags,
 		void *config, size_t config_len, const struct spa_audio_info *info,
 		void *props, size_t mtu)
 {
@@ -609,7 +610,7 @@ static int codec_change_bitrate(struct impl *this, int new_bitrate)
 	if (res != AACENC_OK)
 		return -EINVAL;
 
-	return 0;
+	return this->cur_bitrate;
 }
 
 static int codec_reduce_bitpool(void *data)
@@ -630,7 +631,7 @@ static void codec_set_log(struct spa_log *global_log)
 	spa_log_topic_init(log, &log_topic);
 }
 
-const struct a2dp_codec a2dp_codec_aac = {
+const struct media_codec a2dp_codec_aac = {
 	.id = SPA_BLUETOOTH_AUDIO_CODEC_AAC,
 	.codec_id = A2DP_CODEC_MPEG24,
 	.name = "aac",
@@ -654,7 +655,7 @@ const struct a2dp_codec a2dp_codec_aac = {
 	.set_log = codec_set_log,
 };
 
-A2DP_CODEC_EXPORT_DEF(
+MEDIA_CODEC_EXPORT_DEF(
 	"aac",
 	&a2dp_codec_aac
 );
diff --git a/spa/plugins/bluez5/a2dp-codec-aptx.c b/spa/plugins/bluez5/a2dp-codec-aptx.c
index 721b90599adbae76bd627b8d964f93625cd63011..6938e479c45732a6247e480b0616ff43c5b9b39d 100644
--- a/spa/plugins/bluez5/a2dp-codec-aptx.c
+++ b/spa/plugins/bluez5/a2dp-codec-aptx.c
@@ -35,7 +35,7 @@
 #include <freeaptx.h>
 
 #include "rtp.h"
-#include "a2dp-codecs.h"
+#include "media-codecs.h"
 
 #define APTX_LL_LEVEL1(level) (((level) >> 8) & 0xFF)
 #define APTX_LL_LEVEL2(level) (((level) >> 0) & 0xFF)
@@ -71,19 +71,19 @@ struct msbc_impl {
 	sbc_t msbc;
 };
 
-static inline bool codec_is_hd(const struct a2dp_codec *codec)
+static inline bool codec_is_hd(const struct media_codec *codec)
 {
 	return codec->vendor.codec_id == APTX_HD_CODEC_ID
 		&& codec->vendor.vendor_id == APTX_HD_VENDOR_ID;
 }
 
-static inline bool codec_is_ll(const struct a2dp_codec *codec)
+static inline bool codec_is_ll(const struct media_codec *codec)
 {
 	return (codec->id == SPA_BLUETOOTH_AUDIO_CODEC_APTX_LL) ||
 		(codec->id == SPA_BLUETOOTH_AUDIO_CODEC_APTX_LL_DUPLEX);
 }
 
-static inline size_t codec_get_caps_size(const struct a2dp_codec *codec)
+static inline size_t codec_get_caps_size(const struct media_codec *codec)
 {
 	if (codec_is_hd(codec))
 		return sizeof(a2dp_aptx_hd_t);
@@ -93,7 +93,7 @@ static inline size_t codec_get_caps_size(const struct a2dp_codec *codec)
 		return sizeof(a2dp_aptx_t);
 }
 
-static int codec_fill_caps(const struct a2dp_codec *codec, uint32_t flags,
+static int codec_fill_caps(const struct media_codec *codec, uint32_t flags,
 		uint8_t caps[A2DP_MAX_CAPS_SIZE])
 {
 	size_t actual_conf_size = codec_get_caps_size(codec);
@@ -119,7 +119,7 @@ static int codec_fill_caps(const struct a2dp_codec *codec, uint32_t flags,
 	return actual_conf_size;
 }
 
-static const struct a2dp_codec_config
+static const struct media_codec_config
 aptx_frequencies[] = {
 	{ APTX_SAMPLING_FREQ_48000, 48000, 3 },
 	{ APTX_SAMPLING_FREQ_44100, 44100, 2 },
@@ -127,9 +127,9 @@ aptx_frequencies[] = {
 	{ APTX_SAMPLING_FREQ_16000, 16000, 0 },
 };
 
-static int codec_select_config(const struct a2dp_codec *codec, uint32_t flags,
+static int codec_select_config(const struct media_codec *codec, uint32_t flags,
 		const void *caps, size_t caps_size,
-		const struct a2dp_codec_audio_info *info,
+		const struct media_codec_audio_info *info,
 		const struct spa_dict *settings, uint8_t config[A2DP_MAX_CAPS_SIZE])
 {
 	a2dp_aptx_t conf;
@@ -145,7 +145,7 @@ static int codec_select_config(const struct a2dp_codec *codec, uint32_t flags,
 	    codec->vendor.codec_id != conf.info.codec_id)
 		return -ENOTSUP;
 
-	if ((i = a2dp_codec_select_config(aptx_frequencies,
+	if ((i = media_codec_select_config(aptx_frequencies,
 					  SPA_N_ELEMENTS(aptx_frequencies),
 					  conf.frequency,
 				    	  info ? info->rate : A2DP_CODEC_DEFAULT_RATE
@@ -163,9 +163,9 @@ static int codec_select_config(const struct a2dp_codec *codec, uint32_t flags,
 	return actual_conf_size;
 }
 
-static int codec_select_config_ll(const struct a2dp_codec *codec, uint32_t flags,
+static int codec_select_config_ll(const struct media_codec *codec, uint32_t flags,
 		const void *caps, size_t caps_size,
-		const struct a2dp_codec_audio_info *info,
+		const struct media_codec_audio_info *info,
 		const struct spa_dict *settings, uint8_t config[A2DP_MAX_CAPS_SIZE])
 {
 	a2dp_aptx_ll_ext_t conf = { 0 };
@@ -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, uint32_t flags,
+static int codec_enum_config(const struct media_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)
 {
@@ -315,7 +315,7 @@ static int codec_get_block_size(void *data)
 	return this->codesize;
 }
 
-static void *codec_init(const struct a2dp_codec *codec, uint32_t flags,
+static void *codec_init(const struct media_codec *codec, uint32_t flags,
 		void *config, size_t config_len, const struct spa_audio_info *info,
 		void *props, size_t mtu)
 {
@@ -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, uint32_t flags,
+static int msbc_enum_config(const struct media_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)
 {
@@ -479,7 +479,7 @@ static int msbc_enum_config(const struct a2dp_codec *codec, uint32_t flags,
 	return *param == NULL ? -EIO : 1;
 }
 
-static int msbc_validate_config(const struct a2dp_codec *codec, uint32_t flags,
+static int msbc_validate_config(const struct media_codec *codec, uint32_t flags,
 			const void *caps, size_t caps_size,
 			struct spa_audio_info *info)
 {
@@ -508,7 +508,7 @@ static int msbc_get_block_size(void *data)
 	return MSBC_DECODED_SIZE;
 }
 
-static void *msbc_init(const struct a2dp_codec *codec, uint32_t flags,
+static void *msbc_init(const struct media_codec *codec, uint32_t flags,
 		void *config, size_t config_len, const struct spa_audio_info *info,
 		void *props, size_t mtu)
 {
@@ -610,7 +610,7 @@ static int msbc_decode(void *data,
 }
 
 
-const struct a2dp_codec a2dp_codec_aptx = {
+const struct media_codec a2dp_codec_aptx = {
 	.id = SPA_BLUETOOTH_AUDIO_CODEC_APTX,
 	.codec_id = A2DP_CODEC_VENDOR,
 	.vendor = { .vendor_id = APTX_VENDOR_ID,
@@ -633,7 +633,7 @@ const struct a2dp_codec a2dp_codec_aptx = {
 };
 
 
-const struct a2dp_codec a2dp_codec_aptx_hd = {
+const struct media_codec a2dp_codec_aptx_hd = {
 	.id = SPA_BLUETOOTH_AUDIO_CODEC_APTX_HD,
 	.codec_id = A2DP_CODEC_VENDOR,
 	.vendor = { .vendor_id = APTX_HD_VENDOR_ID,
@@ -671,7 +671,7 @@ const struct a2dp_codec a2dp_codec_aptx_hd = {
 	.increase_bitpool = codec_increase_bitpool
 
 
-const struct a2dp_codec a2dp_codec_aptx_ll_0 = {
+const struct media_codec a2dp_codec_aptx_ll_0 = {
 	APTX_LL_COMMON_DEFS,
 	.id = SPA_BLUETOOTH_AUDIO_CODEC_APTX_LL,
 	.vendor = { .vendor_id = APTX_LL_VENDOR_ID,
@@ -680,7 +680,7 @@ const struct a2dp_codec a2dp_codec_aptx_ll_0 = {
 	.endpoint_name = "aptx_ll_0",
 };
 
-const struct a2dp_codec a2dp_codec_aptx_ll_1 = {
+const struct media_codec a2dp_codec_aptx_ll_1 = {
 	APTX_LL_COMMON_DEFS,
 	.id = SPA_BLUETOOTH_AUDIO_CODEC_APTX_LL,
 	.vendor = { .vendor_id = APTX_LL_VENDOR_ID2,
@@ -690,7 +690,7 @@ const struct a2dp_codec a2dp_codec_aptx_ll_1 = {
 };
 
 /* Voice channel mSBC, not a real A2DP codec */
-static const struct a2dp_codec aptx_ll_msbc = {
+static const struct media_codec aptx_ll_msbc = {
 	.codec_id = A2DP_CODEC_VENDOR,
 	.name = "aptx_ll_msbc",
 	.description = "aptX-LL mSBC",
@@ -715,7 +715,7 @@ static const struct spa_dict_item duplex_info_items[] = {
 };
 static const struct spa_dict duplex_info = SPA_DICT_INIT_ARRAY(duplex_info_items);
 
-const struct a2dp_codec a2dp_codec_aptx_ll_duplex_0 = {
+const struct media_codec a2dp_codec_aptx_ll_duplex_0 = {
 	APTX_LL_COMMON_DEFS,
 	.id = SPA_BLUETOOTH_AUDIO_CODEC_APTX_LL_DUPLEX,
 	.vendor = { .vendor_id = APTX_LL_VENDOR_ID,
@@ -726,7 +726,7 @@ const struct a2dp_codec a2dp_codec_aptx_ll_duplex_0 = {
 	.info = &duplex_info,
 };
 
-const struct a2dp_codec a2dp_codec_aptx_ll_duplex_1 = {
+const struct media_codec a2dp_codec_aptx_ll_duplex_1 = {
 	APTX_LL_COMMON_DEFS,
 	.id = SPA_BLUETOOTH_AUDIO_CODEC_APTX_LL_DUPLEX,
 	.vendor = { .vendor_id = APTX_LL_VENDOR_ID2,
@@ -737,7 +737,7 @@ const struct a2dp_codec a2dp_codec_aptx_ll_duplex_1 = {
 	.info = &duplex_info,
 };
 
-A2DP_CODEC_EXPORT_DEF(
+MEDIA_CODEC_EXPORT_DEF(
 	"aptx",
 	&a2dp_codec_aptx_hd,
 	&a2dp_codec_aptx,
diff --git a/spa/plugins/bluez5/a2dp-codec-faststream.c b/spa/plugins/bluez5/a2dp-codec-faststream.c
index 03911945c3b49d3ac5f08ecaf21f8356629e8220..a579eadd0682bdd9294588c7a882354346188469 100644
--- a/spa/plugins/bluez5/a2dp-codec-faststream.c
+++ b/spa/plugins/bluez5/a2dp-codec-faststream.c
@@ -36,7 +36,7 @@
 
 #include <sbc/sbc.h>
 
-#include "a2dp-codecs.h"
+#include "media-codecs.h"
 
 struct impl {
 	sbc_t sbc;
@@ -51,7 +51,7 @@ struct duplex_impl {
 	sbc_t sbc;
 };
 
-static int codec_fill_caps(const struct a2dp_codec *codec, uint32_t flags,
+static int codec_fill_caps(const struct media_codec *codec, uint32_t flags,
 		uint8_t caps[A2DP_MAX_CAPS_SIZE])
 {
 	const a2dp_faststream_t a2dp_faststream = {
@@ -69,20 +69,20 @@ static int codec_fill_caps(const struct a2dp_codec *codec, uint32_t flags,
 	return sizeof(a2dp_faststream);
 }
 
-static const struct a2dp_codec_config
+static const struct media_codec_config
 frequencies[] = {
 	{ FASTSTREAM_SINK_SAMPLING_FREQ_48000, 48000, 1 },
 	{ FASTSTREAM_SINK_SAMPLING_FREQ_44100, 44100, 0 },
 };
 
-static const struct a2dp_codec_config
+static const struct media_codec_config
 duplex_frequencies[] = {
 	{ FASTSTREAM_SOURCE_SAMPLING_FREQ_16000, 16000, 0 },
 };
 
-static int codec_select_config(const struct a2dp_codec *codec, uint32_t flags,
+static int codec_select_config(const struct media_codec *codec, uint32_t flags,
 		const void *caps, size_t caps_size,
-		const struct a2dp_codec_audio_info *info,
+		const struct media_codec_audio_info *info,
 		const struct spa_dict *settings, uint8_t config[A2DP_MAX_CAPS_SIZE])
 {
 	a2dp_faststream_t conf;
@@ -108,7 +108,7 @@ static int codec_select_config(const struct a2dp_codec *codec, uint32_t flags,
 	if (codec->duplex_codec)
 		conf.direction |= FASTSTREAM_DIRECTION_SOURCE;
 
-	if ((i = a2dp_codec_select_config(frequencies,
+	if ((i = media_codec_select_config(frequencies,
 			SPA_N_ELEMENTS(frequencies),
 			conf.sink_frequency,
 			info ? info->rate : A2DP_CODEC_DEFAULT_RATE
@@ -116,7 +116,7 @@ static int codec_select_config(const struct a2dp_codec *codec, uint32_t flags,
 		return -ENOTSUP;
 	conf.sink_frequency = frequencies[i].config;
 
-	if ((i = a2dp_codec_select_config(duplex_frequencies,
+	if ((i = media_codec_select_config(duplex_frequencies,
 			SPA_N_ELEMENTS(duplex_frequencies),
 			conf.source_frequency,
 			16000
@@ -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, uint32_t flags,
+static int codec_enum_config(const struct media_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)
 {
@@ -209,7 +209,7 @@ static size_t ceil2(size_t v)
 	return v;
 }
 
-static void *codec_init(const struct a2dp_codec *codec, uint32_t flags,
+static void *codec_init(const struct media_codec *codec, uint32_t flags,
 		void *config, size_t config_len, const struct spa_audio_info *info,
 		void *props, size_t mtu)
 {
@@ -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, uint32_t flags,
+static int duplex_enum_config(const struct media_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)
 {
@@ -411,7 +411,7 @@ static int duplex_enum_config(const struct a2dp_codec *codec, uint32_t flags,
 	return *param == NULL ? -EIO : 1;
 }
 
-static int duplex_validate_config(const struct a2dp_codec *codec, uint32_t flags,
+static int duplex_validate_config(const struct media_codec *codec, uint32_t flags,
 			const void *caps, size_t caps_size,
 			struct spa_audio_info *info)
 {
@@ -441,7 +441,7 @@ static int duplex_get_block_size(void *data)
 	return 0;
 }
 
-static void *duplex_init(const struct a2dp_codec *codec, uint32_t flags,
+static void *duplex_init(const struct media_codec *codec, uint32_t flags,
 		void *config, size_t config_len, const struct spa_audio_info *info,
 		void *props, size_t mtu)
 {
@@ -577,7 +577,7 @@ static int duplex_decode(void *data,
 }
 
 /* Voice channel SBC, not a real A2DP codec */
-static const struct a2dp_codec duplex_codec = {
+static const struct media_codec duplex_codec = {
 	.codec_id = A2DP_CODEC_VENDOR,
 	.name = "faststream_sbc",
 	.description = "FastStream duplex SBC",
@@ -614,7 +614,7 @@ static const struct a2dp_codec duplex_codec = {
 	.reduce_bitpool = codec_reduce_bitpool,		\
 	.increase_bitpool = codec_increase_bitpool
 
-static const struct a2dp_codec a2dp_codec_faststream = {
+const struct media_codec a2dp_codec_faststream = {
 	FASTSTREAM_COMMON_DEFS,
 	.id = SPA_BLUETOOTH_AUDIO_CODEC_FASTSTREAM,
 	.name = "faststream",
@@ -625,7 +625,7 @@ static const struct spa_dict_item duplex_info_items[] = {
 };
 static const struct spa_dict duplex_info = SPA_DICT_INIT_ARRAY(duplex_info_items);
 
-const struct a2dp_codec a2dp_codec_faststream_duplex = {
+const struct media_codec a2dp_codec_faststream_duplex = {
 	FASTSTREAM_COMMON_DEFS,
 	.id = SPA_BLUETOOTH_AUDIO_CODEC_FASTSTREAM_DUPLEX,
 	.name = "faststream_duplex",
@@ -633,7 +633,7 @@ const struct a2dp_codec a2dp_codec_faststream_duplex = {
 	.info = &duplex_info,
 };
 
-A2DP_CODEC_EXPORT_DEF(
+MEDIA_CODEC_EXPORT_DEF(
 	"faststream",
 	&a2dp_codec_faststream,
 	&a2dp_codec_faststream_duplex
diff --git a/spa/plugins/bluez5/a2dp-codec-lc3plus.c b/spa/plugins/bluez5/a2dp-codec-lc3plus.c
index 40ca0e0db142fa809560a9599ecbc08d7ef2a1e3..289662442b9da247025e461c42b48ad3bbdef713 100644
--- a/spa/plugins/bluez5/a2dp-codec-lc3plus.c
+++ b/spa/plugins/bluez5/a2dp-codec-lc3plus.c
@@ -41,7 +41,7 @@
 #endif
 
 #include "rtp.h"
-#include "a2dp-codecs.h"
+#include "media-codecs.h"
 
 #define BITRATE_MIN 96000
 #define BITRATE_MAX 512000
@@ -86,7 +86,7 @@ struct impl {
 	int32_t buf[2][LC3PLUS_MAX_SAMPLES];
 };
 
-static int codec_fill_caps(const struct a2dp_codec *codec, uint32_t flags,
+static int codec_fill_caps(const struct media_codec *codec, uint32_t flags,
 		uint8_t caps[A2DP_MAX_CAPS_SIZE])
 {
 	const a2dp_lc3plus_hr_t a2dp_lc3plus_hr = {
@@ -102,9 +102,9 @@ static int codec_fill_caps(const struct a2dp_codec *codec, uint32_t flags,
 	return sizeof(a2dp_lc3plus_hr);
 }
 
-static int codec_select_config(const struct a2dp_codec *codec, uint32_t flags,
+static int codec_select_config(const struct media_codec *codec, uint32_t flags,
 		const void *caps, size_t caps_size,
-		const struct a2dp_codec_audio_info *info,
+		const struct media_codec_audio_info *info,
 		const struct spa_dict *settings, uint8_t config[A2DP_MAX_CAPS_SIZE])
 {
 	a2dp_lc3plus_hr_t conf;
@@ -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, 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)
+static int codec_caps_preference_cmp(const struct media_codec *codec, uint32_t flags, const void *caps1, size_t caps1_size,
+		const void *caps2, size_t caps2_size, const struct media_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, uint32_t fl
 
 	/* 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, uint32_t fl
 #undef PREFER_BOOL
 }
 
-static int codec_enum_config(const struct a2dp_codec *codec, uint32_t flags,
+static int codec_enum_config(const struct media_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)
 {
@@ -263,7 +263,7 @@ static int codec_enum_config(const struct a2dp_codec *codec, uint32_t flags,
 	return *param == NULL ? -EIO : 1;
 }
 
-static int codec_validate_config(const struct a2dp_codec *codec, uint32_t flags,
+static int codec_validate_config(const struct media_codec *codec, uint32_t flags,
 			const void *caps, size_t caps_size,
 			struct spa_audio_info *info)
 {
@@ -342,7 +342,7 @@ static bool check_mtu_vs_frame_dms(struct impl *this)
 	return (size_t)this->mtu >= header_size + ceildiv(payload_size, max_fragments);
 }
 
-static void *codec_init(const struct a2dp_codec *codec, uint32_t flags,
+static void *codec_init(const struct media_codec *codec, uint32_t flags,
 		void *config, size_t config_len, const struct spa_audio_info *info,
 		void *props, size_t mtu)
 {
@@ -747,18 +747,20 @@ static SPA_UNUSED int codec_decode(void *data,
 static int codec_reduce_bitpool(void *data)
 {
 	struct impl *this = data;
-	this->e.next_bitrate = this->bitrate * 3 / 4;
-	return 0;
+	this->e.next_bitrate = SPA_CLAMP(this->bitrate * 3 / 4,
+			BITRATE_MIN * this->channels, BITRATE_MAX * this->channels);
+	return this->e.next_bitrate;
 }
 
 static int codec_increase_bitpool(void *data)
 {
 	struct impl *this = data;
-	this->e.next_bitrate = this->bitrate * 5 / 4;
-	return 0;
+	this->e.next_bitrate = SPA_CLAMP(this->bitrate * 5 / 4,
+			BITRATE_MIN * this->channels, BITRATE_MAX * this->channels);
+	return this->e.next_bitrate;
 }
 
-const struct a2dp_codec a2dp_codec_lc3plus_hr = {
+const struct media_codec a2dp_codec_lc3plus_hr = {
 	.id = SPA_BLUETOOTH_AUDIO_CODEC_LC3PLUS_HR,
 	.name = "lc3plus_hr",
 	.codec_id = A2DP_CODEC_VENDOR,
@@ -782,7 +784,7 @@ const struct a2dp_codec a2dp_codec_lc3plus_hr = {
 	.increase_bitpool = codec_increase_bitpool
 };
 
-A2DP_CODEC_EXPORT_DEF(
+MEDIA_CODEC_EXPORT_DEF(
 	"lc3plus",
 	&a2dp_codec_lc3plus_hr
 );
diff --git a/spa/plugins/bluez5/a2dp-codec-ldac.c b/spa/plugins/bluez5/a2dp-codec-ldac.c
index 3906104e87ab4b03d7bfb7cfffceb5550c10c174..6649c772e2c1ba0cf8c94d326b1c504e4599d62c 100644
--- a/spa/plugins/bluez5/a2dp-codec-ldac.c
+++ b/spa/plugins/bluez5/a2dp-codec-ldac.c
@@ -40,7 +40,7 @@
 #endif
 
 #include "rtp.h"
-#include "a2dp-codecs.h"
+#include "media-codecs.h"
 
 #define LDACBT_EQMID_AUTO -1
 
@@ -79,7 +79,7 @@ struct impl {
 	int frame_count;
 };
 
-static int codec_fill_caps(const struct a2dp_codec *codec, uint32_t flags, uint8_t caps[A2DP_MAX_CAPS_SIZE])
+static int codec_fill_caps(const struct media_codec *codec, uint32_t flags, uint8_t caps[A2DP_MAX_CAPS_SIZE])
 {
 	static const a2dp_ldac_t a2dp_ldac = {
 		.info.vendor_id = LDAC_VENDOR_ID,
@@ -97,7 +97,7 @@ static int codec_fill_caps(const struct a2dp_codec *codec, uint32_t flags, uint8
 	return sizeof(a2dp_ldac);
 }
 
-static const struct a2dp_codec_config
+static const struct media_codec_config
 ldac_frequencies[] = {
 	{ LDACBT_SAMPLING_FREQ_044100, 44100, 3 },
 	{ LDACBT_SAMPLING_FREQ_048000, 48000, 2 },
@@ -105,16 +105,16 @@ ldac_frequencies[] = {
 	{ LDACBT_SAMPLING_FREQ_096000, 96000, 0 },
 };
 
-static const struct a2dp_codec_config
+static const struct media_codec_config
 ldac_channel_modes[] = {
 	{ LDACBT_CHANNEL_MODE_STEREO,       2, 2 },
 	{ LDACBT_CHANNEL_MODE_DUAL_CHANNEL, 2, 1 },
 	{ LDACBT_CHANNEL_MODE_MONO,         1, 0 },
 };
 
-static int codec_select_config(const struct a2dp_codec *codec, uint32_t flags,
+static int codec_select_config(const struct media_codec *codec, uint32_t flags,
 		const void *caps, size_t caps_size,
-		const struct a2dp_codec_audio_info *info,
+		const struct media_codec_audio_info *info,
 		const struct spa_dict *settings, uint8_t config[A2DP_MAX_CAPS_SIZE])
 {
 	a2dp_ldac_t conf;
@@ -129,7 +129,7 @@ static int codec_select_config(const struct a2dp_codec *codec, uint32_t flags,
 	    codec->vendor.codec_id != conf.info.codec_id)
 		return -ENOTSUP;
 
-	if ((i = a2dp_codec_select_config(ldac_frequencies,
+	if ((i = media_codec_select_config(ldac_frequencies,
 					  SPA_N_ELEMENTS(ldac_frequencies),
 					  conf.frequency,
 				    	  info ? info->rate : A2DP_CODEC_DEFAULT_RATE
@@ -137,7 +137,7 @@ static int codec_select_config(const struct a2dp_codec *codec, uint32_t flags,
 		return -ENOTSUP;
 	conf.frequency = ldac_frequencies[i].config;
 
-	if ((i = a2dp_codec_select_config(ldac_channel_modes,
+	if ((i = media_codec_select_config(ldac_channel_modes,
 					  SPA_N_ELEMENTS(ldac_channel_modes),
 				    	  conf.channel_mode,
 				    	  info ? info->channels : A2DP_CODEC_DEFAULT_CHANNELS
@@ -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, uint32_t flags,
+static int codec_enum_config(const struct media_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, uint32_t flags, const struct spa_dict *settings)
+static void *codec_init_props(const struct media_codec *codec, uint32_t flags, const struct spa_dict *settings)
 {
 	struct props *p = calloc(1, sizeof(struct props));
 	const char *str;
@@ -385,7 +385,7 @@ static int codec_set_props(void *props, const struct spa_pod *param)
 	return prev_eqmid != p->eqmid;
 }
 
-static void *codec_init(const struct a2dp_codec *codec, uint32_t flags,
+static void *codec_init(const struct media_codec *codec, uint32_t flags,
 		void *config, size_t config_len, const struct spa_audio_info *info,
 		void *props, size_t mtu)
 {
@@ -570,7 +570,7 @@ static int codec_encode(void *data,
 	return src_used;
 }
 
-const struct a2dp_codec a2dp_codec_ldac = {
+const struct media_codec a2dp_codec_ldac = {
 	.id = SPA_BLUETOOTH_AUDIO_CODEC_LDAC,
 	.codec_id = A2DP_CODEC_VENDOR,
 	.vendor = { .vendor_id = LDAC_VENDOR_ID,
@@ -598,7 +598,7 @@ const struct a2dp_codec a2dp_codec_ldac = {
 	.increase_bitpool = codec_increase_bitpool,
 };
 
-A2DP_CODEC_EXPORT_DEF(
+MEDIA_CODEC_EXPORT_DEF(
 	"ldac",
 	&a2dp_codec_ldac
 );
diff --git a/spa/plugins/bluez5/a2dp-codec-opus.c b/spa/plugins/bluez5/a2dp-codec-opus.c
index 7f1df12df331ed17d565a558ebdc3b9a2b3cf8e5..32ae2908695c5ad0072d1a9d76900275324ab4ab 100644
--- a/spa/plugins/bluez5/a2dp-codec-opus.c
+++ b/spa/plugins/bluez5/a2dp-codec-opus.c
@@ -44,7 +44,7 @@
 #include <opus_multistream.h>
 
 #include "rtp.h"
-#include "a2dp-codecs.h"
+#include "media-codecs.h"
 
 static struct spa_log *log;
 static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.codecs.opus");
@@ -395,7 +395,7 @@ static void parse_settings(struct props *props, const struct spa_dict *settings)
 		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)
+static int set_channel_conf(const struct media_codec *codec, a2dp_opus_05_t *caps, const struct props *props)
 {
 	/*
 	 * Predefined codec profiles
@@ -469,7 +469,7 @@ static int set_channel_conf(const struct a2dp_codec *codec, a2dp_opus_05_t *caps
 	return 0;
 }
 
-static void get_default_bitrates(const struct a2dp_codec *codec, bool bidi, int *min, int *max, int *init)
+static void get_default_bitrates(const struct media_codec *codec, bool bidi, int *min, int *max, int *init)
 {
 	int tmp;
 
@@ -514,7 +514,7 @@ static void get_default_bitrates(const struct a2dp_codec *codec, bool bidi, int
 	};
 }
 
-static int get_mapping(const struct a2dp_codec *codec, const a2dp_opus_05_direction_t *conf,
+static int get_mapping(const struct media_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)
 {
@@ -576,7 +576,7 @@ static int get_mapping(const struct a2dp_codec *codec, const a2dp_opus_05_direct
 	return 0;
 }
 
-static int codec_fill_caps(const struct a2dp_codec *codec, uint32_t flags,
+static int codec_fill_caps(const struct media_codec *codec, uint32_t flags,
 		uint8_t caps[A2DP_MAX_CAPS_SIZE])
 {
 	a2dp_opus_05_t a2dp_opus_05 = {
@@ -613,9 +613,9 @@ static int codec_fill_caps(const struct a2dp_codec *codec, uint32_t flags,
 	return sizeof(a2dp_opus_05);
 }
 
-static int codec_select_config(const struct a2dp_codec *codec, uint32_t flags,
+static int codec_select_config(const struct media_codec *codec, uint32_t flags,
 		const void *caps, size_t caps_size,
-		const struct a2dp_codec_audio_info *info,
+		const struct media_codec_audio_info *info,
 		const struct spa_dict *global_settings, uint8_t config[A2DP_MAX_CAPS_SIZE])
 {
 	struct props props;
@@ -715,8 +715,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, uint32_t flags, 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 media_codec *codec, uint32_t flags, const void *caps1, size_t caps1_size,
+		const void *caps2, size_t caps2_size, const struct media_codec_audio_info *info,
 		const struct spa_dict *global_settings)
 {
 	a2dp_opus_05_t conf1, conf2, cap1, cap2;
@@ -768,12 +768,12 @@ static int codec_caps_preference_cmp(const struct a2dp_codec *codec, uint32_t fl
 #undef PREFER_BOOL
 }
 
-static bool is_duplex_codec(const struct a2dp_codec *codec)
+static bool is_duplex_codec(const struct media_codec *codec)
 {
 	return codec->id == 0;
 }
 
-static bool use_surround_encoder(const struct a2dp_codec *codec, bool is_sink)
+static bool use_surround_encoder(const struct media_codec *codec, bool is_sink)
 {
 	if (codec->id == SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_PRO)
 		return false;
@@ -784,11 +784,11 @@ static bool use_surround_encoder(const struct a2dp_codec *codec, bool is_sink)
 		return !is_sink;
 }
 
-static int codec_enum_config(const struct a2dp_codec *codec, uint32_t flags,
+static int codec_enum_config(const struct media_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);
+	const bool surround_encoder = use_surround_encoder(codec, flags & MEDIA_CODEC_FLAG_SINK);
 	a2dp_opus_05_t conf;
 	a2dp_opus_05_direction_t *dir;
 	struct spa_pod_frame f[1];
@@ -823,11 +823,11 @@ static int codec_enum_config(const struct a2dp_codec *codec, uint32_t flags,
 	return *param == NULL ? -EIO : 1;
 }
 
-static int codec_validate_config(const struct a2dp_codec *codec, uint32_t flags,
+static int codec_validate_config(const struct media_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 bool surround_encoder = use_surround_encoder(codec, flags & MEDIA_CODEC_FLAG_SINK);
 	const a2dp_opus_05_direction_t *dir1, *dir2;
 	const a2dp_opus_05_t *conf;
 
@@ -898,7 +898,7 @@ static int parse_frame_dms(int bitfield)
 	}
 }
 
-static void *codec_init_props(const struct a2dp_codec *codec, uint32_t flags, const struct spa_dict *settings)
+static void *codec_init_props(const struct media_codec *codec, uint32_t flags, const struct spa_dict *settings)
 {
 	struct props *p;
 
@@ -919,11 +919,11 @@ static void codec_clear_props(void *props)
 	free(props);
 }
 
-static void *codec_init(const struct a2dp_codec *codec, uint32_t flags,
+static void *codec_init(const struct media_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);
+	const bool surround_encoder = use_surround_encoder(codec, flags & MEDIA_CODEC_FLAG_SINK);
 	a2dp_opus_05_t *conf = config;
 	a2dp_opus_05_direction_t *dir;
 	struct impl *this = NULL;
@@ -1360,7 +1360,6 @@ static void codec_set_log(struct spa_log *global_log)
 	.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,		\
@@ -1380,44 +1379,50 @@ static void codec_set_log(struct spa_log *global_log)
 	.start_decode = codec_start_decode,			\
 	.decode = codec_decode
 
-const struct a2dp_codec a2dp_codec_opus_05 = {
+const struct media_codec a2dp_codec_opus_05 = {
 	OPUS_05_COMMON_FULL_DEFS,
 	.id = SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05,
 	.name = "opus_05",
 	.description = "Opus",
+	.fill_caps = codec_fill_caps,
 };
 
-const struct a2dp_codec a2dp_codec_opus_05_51 = {
+const struct media_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",
+	.endpoint_name = "opus_05",
+	.fill_caps = NULL,
 };
 
-const struct a2dp_codec a2dp_codec_opus_05_71 = {
+const struct media_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",
+	.endpoint_name = "opus_05",
+	.fill_caps = NULL,
 };
 
 /* Bidi return channel codec: doesn't have endpoints */
-const struct a2dp_codec a2dp_codec_opus_05_return = {
+const struct media_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 = {
+const struct media_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,
+	.fill_caps = codec_fill_caps,
 };
 
-const struct a2dp_codec a2dp_codec_opus_05_pro = {
+const struct media_codec a2dp_codec_opus_05_pro = {
 	OPUS_05_COMMON_DEFS,
 	.id = SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_PRO,
 	.name = "opus_05_pro",
@@ -1425,9 +1430,11 @@ const struct a2dp_codec a2dp_codec_opus_05_pro = {
 	.init_props = codec_init_props,
 	.clear_props = codec_clear_props,
 	.duplex_codec = &a2dp_codec_opus_05_return,
+	.endpoint_name = "opus_05_duplex",
+	.fill_caps = NULL,
 };
 
-A2DP_CODEC_EXPORT_DEF(
+MEDIA_CODEC_EXPORT_DEF(
 	"opus",
 	&a2dp_codec_opus_05,
 	&a2dp_codec_opus_05_51,
diff --git a/spa/plugins/bluez5/a2dp-codec-sbc.c b/spa/plugins/bluez5/a2dp-codec-sbc.c
index 35d55355a1c028bc765909932d3d15b794acb5f6..27a57bded3958bce656ef91bd72953315c66c02e 100644
--- a/spa/plugins/bluez5/a2dp-codec-sbc.c
+++ b/spa/plugins/bluez5/a2dp-codec-sbc.c
@@ -33,7 +33,7 @@
 #include <sbc/sbc.h>
 
 #include "rtp.h"
-#include "a2dp-codecs.h"
+#include "media-codecs.h"
 
 #define MAX_FRAME_COUNT 16
 
@@ -51,7 +51,7 @@ struct impl {
 	int max_bitpool;
 };
 
-static int codec_fill_caps(const struct a2dp_codec *codec, uint32_t flags,
+static int codec_fill_caps(const struct media_codec *codec, uint32_t flags,
 		uint8_t caps[A2DP_MAX_CAPS_SIZE])
 {
 	static const a2dp_sbc_t a2dp_sbc = {
@@ -121,7 +121,7 @@ static uint8_t default_bitpool(uint8_t freq, uint8_t mode, bool xq)
 }
 
 
-static const struct a2dp_codec_config
+static const struct media_codec_config
 sbc_frequencies[] = {
 	{ SBC_SAMPLING_FREQ_48000, 48000, 3 },
 	{ SBC_SAMPLING_FREQ_44100, 44100, 2 },
@@ -129,13 +129,13 @@ sbc_frequencies[] = {
 	{ SBC_SAMPLING_FREQ_16000, 16000, 0 },
 };
 
-static const struct a2dp_codec_config
+static const struct media_codec_config
 sbc_xq_frequencies[] = {
 	{ SBC_SAMPLING_FREQ_44100, 44100, 1 },
 	{ SBC_SAMPLING_FREQ_48000, 48000, 0 },
 };
 
-static const struct a2dp_codec_config
+static const struct media_codec_config
 sbc_channel_modes[] = {
 	{ SBC_CHANNEL_MODE_JOINT_STEREO, 2, 3 },
 	{ SBC_CHANNEL_MODE_STEREO,       2, 2 },
@@ -143,22 +143,22 @@ sbc_channel_modes[] = {
 	{ SBC_CHANNEL_MODE_MONO,         1, 0 },
 };
 
-static const struct a2dp_codec_config
+static const struct media_codec_config
 sbc_xq_channel_modes[] = {
 	{ SBC_CHANNEL_MODE_DUAL_CHANNEL, 2, 2 },
 	{ SBC_CHANNEL_MODE_JOINT_STEREO, 2, 1 },
 	{ SBC_CHANNEL_MODE_STEREO,       2, 0 },
 };
 
-static int codec_select_config(const struct a2dp_codec *codec, uint32_t flags,
+static int codec_select_config(const struct media_codec *codec, uint32_t flags,
 		const void *caps, size_t caps_size,
-		const struct a2dp_codec_audio_info *info,
+		const struct media_codec_audio_info *info,
 		const struct spa_dict *settings, uint8_t config[A2DP_MAX_CAPS_SIZE])
 {
 	a2dp_sbc_t conf;
 	int bitpool, i;
 	size_t n;
-	const struct a2dp_codec_config *configs;
+	const struct media_codec_config *configs;
 	bool xq = false;
 
 
@@ -176,7 +176,7 @@ static int codec_select_config(const struct a2dp_codec *codec, uint32_t flags,
 		configs = sbc_frequencies;
 		n = SPA_N_ELEMENTS(sbc_frequencies);
 	}
-	if ((i = a2dp_codec_select_config(configs, n, conf.frequency,
+	if ((i = media_codec_select_config(configs, n, conf.frequency,
 					  info ? info->rate : A2DP_CODEC_DEFAULT_RATE
 					  )) < 0)
 		return -ENOTSUP;
@@ -189,7 +189,7 @@ static int codec_select_config(const struct a2dp_codec *codec, uint32_t flags,
 		configs = sbc_channel_modes;
 		n = SPA_N_ELEMENTS(sbc_channel_modes);
 	}
-	if ((i = a2dp_codec_select_config(configs, n, conf.channel_mode,
+	if ((i = media_codec_select_config(configs, n, conf.channel_mode,
 					  info ? info->channels : A2DP_CODEC_DEFAULT_CHANNELS
 					  )) < 0)
 		return -ENOTSUP;
@@ -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, 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)
+static int codec_caps_preference_cmp(const struct media_codec *codec, uint32_t flags, const void *caps1, size_t caps1_size,
+		const void *caps2, size_t caps2_size, const struct media_codec_audio_info *info, const struct spa_dict *global_settings)
 {
 	a2dp_sbc_t conf1, conf2;
 	a2dp_sbc_t *conf;
@@ -275,7 +275,7 @@ static int codec_caps_preference_cmp(const struct a2dp_codec *codec, uint32_t fl
 #undef PREFER_BOOL
 }
 
-static int codec_validate_config(const struct a2dp_codec *codec, uint32_t flags,
+static int codec_validate_config(const struct media_codec *codec, uint32_t flags,
 			const void *caps, size_t caps_size,
 			struct spa_audio_info *info)
 {
@@ -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, uint32_t flags,
+static int codec_enum_config(const struct media_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)
 {
@@ -453,7 +453,7 @@ static int codec_get_block_size(void *data)
 	return this->codesize;
 }
 
-static void *codec_init(const struct a2dp_codec *codec, uint32_t flags,
+static void *codec_init(const struct media_codec *codec, uint32_t flags,
 		void *config, size_t config_len, const struct spa_audio_info *info,
 		void *props, size_t mtu)
 {
@@ -638,7 +638,7 @@ static int codec_decode(void *data,
 	return res;
 }
 
-const struct a2dp_codec a2dp_codec_sbc = {
+const struct media_codec a2dp_codec_sbc = {
 	.id = SPA_BLUETOOTH_AUDIO_CODEC_SBC,
 	.codec_id = A2DP_CODEC_SBC,
 	.name = "sbc",
@@ -660,7 +660,7 @@ const struct a2dp_codec a2dp_codec_sbc = {
 	.increase_bitpool = codec_increase_bitpool,
 };
 
-const struct a2dp_codec a2dp_codec_sbc_xq = {
+const struct media_codec a2dp_codec_sbc_xq = {
 	.id = SPA_BLUETOOTH_AUDIO_CODEC_SBC_XQ,
 	.codec_id = A2DP_CODEC_SBC,
 	.name = "sbc_xq",
@@ -682,7 +682,7 @@ const struct a2dp_codec a2dp_codec_sbc_xq = {
 	.increase_bitpool = codec_increase_bitpool,
 };
 
-A2DP_CODEC_EXPORT_DEF(
+MEDIA_CODEC_EXPORT_DEF(
 	"sbc",
 	&a2dp_codec_sbc,
 	&a2dp_codec_sbc_xq
diff --git a/spa/plugins/bluez5/backend-native.c b/spa/plugins/bluez5/backend-native.c
index c38656bbbd5b4e9d188e55c5910b593204c2dcb7..eed23f66e4fdab4b3598a183c014284cd9770ee2 100644
--- a/spa/plugins/bluez5/backend-native.c
+++ b/spa/plugins/bluez5/backend-native.c
@@ -51,6 +51,9 @@
 #include <libusb.h>
 #endif
 
+#include "modemmanager.h"
+#include "upower.h"
+
 static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.native");
 #undef SPA_LOG_TOPIC_DEFAULT
 #define SPA_LOG_TOPIC_DEFAULT &log_topic
@@ -60,12 +63,39 @@ static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.native");
 #define HFP_CODEC_SWITCH_INITIAL_TIMEOUT_MSEC 5000
 #define HFP_CODEC_SWITCH_TIMEOUT_MSEC 20000
 
+#define INTERNATIONAL_NUMBER 145
+#define NATIONAL_NUMBER 129
+
+#define MAX_HF_INDICATORS 16
+
 enum {
 	HFP_AG_INITIAL_CODEC_SETUP_NONE = 0,
 	HFP_AG_INITIAL_CODEC_SETUP_SEND,
 	HFP_AG_INITIAL_CODEC_SETUP_WAIT
 };
 
+#define CIND_INDICATORS "(\"service\",(0-1)),(\"call\",(0-1)),(\"callsetup\",(0-3)),(\"callheld\",(0-2)),(\"signal\",(0-5)),(\"roam\",(0-1)),\"battchg\",(0-5))"
+enum {
+	CIND_SERVICE = 1,
+	CIND_CALL,
+	CIND_CALLSETUP,
+	CIND_CALLHELD,
+	CIND_SIGNAL,
+	CIND_ROAM,
+	CIND_BATTERY_LEVEL,
+	CIND_MAX
+};
+
+struct modem {
+	bool network_has_service;
+	unsigned int signal_strength;
+	bool network_is_roaming;
+	char *operator_name;
+	char *own_number;
+	bool active_call;
+	unsigned int call_setup;
+};
+
 struct impl {
 	struct spa_bt_backend this;
 
@@ -78,7 +108,7 @@ struct impl {
 	struct spa_dbus *dbus;
 	DBusConnection *conn;
 
-#define DEFAULT_ENABLED_PROFILES (SPA_BT_PROFILE_HSP_HS | SPA_BT_PROFILE_HFP_AG)
+#define DEFAULT_ENABLED_PROFILES (SPA_BT_PROFILE_HFP_HF | SPA_BT_PROFILE_HFP_AG)
 	enum spa_bt_profile enabled_profiles;
 
 	struct spa_source sco;
@@ -87,6 +117,13 @@ struct impl {
 
 	struct spa_list rfcomm_list;
 	unsigned int defer_setup_enabled:1;
+
+	struct modem modem;
+	unsigned int battery_level;
+
+	void *modemmanager;
+	struct spa_source *ring_timer;
+	void *upower;
 };
 
 struct transport_data {
@@ -142,9 +179,13 @@ struct rfcomm {
 	unsigned int hfp_ag_initial_codec_setup:2;
 	unsigned int cind_call_active:1;
 	unsigned int cind_call_notify:1;
+	unsigned int extended_error_reporting:1;
+	unsigned int clip_notify:1;
 	enum hfp_hf_state hf_state;
 	enum hsp_hs_state hs_state;
 	unsigned int codec;
+	uint32_t cind_enabled_indicators;
+	char *hf_indicators[MAX_HF_INDICATORS];
 #endif
 };
 
@@ -233,6 +274,11 @@ static void volume_sync_stop_timer(struct rfcomm *rfcomm);
 static void rfcomm_free(struct rfcomm *rfcomm)
 {
 	codec_switch_stop_timer(rfcomm);
+	for (int i = 0; i < MAX_HF_INDICATORS; i++) {
+		if (rfcomm->hf_indicators[i]) {
+			free(rfcomm->hf_indicators[i]);
+		}
+	}
 	spa_list_remove(&rfcomm->link);
 	if (rfcomm->path)
 		free(rfcomm->path);
@@ -259,6 +305,7 @@ static void rfcomm_free(struct rfcomm *rfcomm)
 
 #define RFCOMM_MESSAGE_MAX_LENGTH 256
 
+/* from HF/HS to AG */
 SPA_PRINTF_FUNC(2, 3)
 static ssize_t rfcomm_send_cmd(const struct rfcomm *rfcomm, const char *format, ...)
 {
@@ -279,7 +326,14 @@ static ssize_t rfcomm_send_cmd(const struct rfcomm *rfcomm, const char *format,
 
 	spa_log_debug(backend->log, "RFCOMM >> %s", message);
 
-	message[len] = '\n';
+	/*
+	 * The format of an AT command from the HF to the AG shall be: <AT command><cr>
+	 * - HFP 1.8, 4.34.1
+	 *
+	 * The format for a command from the HS to the AG is thus: AT<cmd>=<value><cr>
+	 * - HSP 1.2, 4.8.1
+	 */
+	message[len] = '\r';
 	/* `message` is no longer null-terminated */
 
 	len = write(rfcomm->source.fd, message, len + 1);
@@ -293,6 +347,7 @@ static ssize_t rfcomm_send_cmd(const struct rfcomm *rfcomm, const char *format,
 	return len;
 }
 
+/* from AG to HF/HS */
 SPA_PRINTF_FUNC(2, 3)
 static ssize_t rfcomm_send_reply(const struct rfcomm *rfcomm, const char *format, ...)
 {
@@ -313,6 +368,18 @@ static ssize_t rfcomm_send_reply(const struct rfcomm *rfcomm, const char *format
 
 	spa_log_debug(backend->log, "RFCOMM >> %s", &message[2]);
 
+	/*
+	 * The format of the OK code from the AG to the HF shall be: <cr><lf>OK<cr><lf>
+	 * The format of the generic ERROR code from the AG to the HF shall be: <cr><lf>ERROR<cr><lf>
+	 * The format of an unsolicited result code from the AG to the HF shall be: <cr><lf><result code><cr><lf>
+	 * - HFP 1.8, 4.34.1
+	 *
+	 * If the command is processed successfully, the resulting response from the AG to the HS is: <cr><lf>OK<cr><lf>
+	 * If the command is not processed successfully, or is not recognized,
+	 * the resulting response from the AG to the HS is: <cr><lf>ERROR<cr><lf>
+	 * The format for an unsolicited result code (such as RING) from the AG to the HS is: <cr><lf><result code><cr><lf>
+	 * - HSP 1.2, 4.8.1
+	 */
 	message[0] = '\r';
 	message[1] = '\n';
 	message[len + 2] = '\r';
@@ -330,6 +397,14 @@ static ssize_t rfcomm_send_reply(const struct rfcomm *rfcomm, const char *format
 	return len;
 }
 
+static void rfcomm_send_error(const struct rfcomm *rfcomm, enum cmee_error error)
+{
+	if (rfcomm->extended_error_reporting)
+		rfcomm_send_reply(rfcomm, "+CME ERROR: %d", error);
+	else
+		rfcomm_send_reply(rfcomm, "ERROR");
+}
+
 static bool rfcomm_volume_enabled(struct rfcomm *rfcomm)
 {
 	return rfcomm->device != NULL
@@ -699,6 +774,7 @@ static bool rfcomm_hfp_ag(struct rfcomm *rfcomm, char* buf)
 	unsigned int selected_codec;
 	unsigned int indicator;
 	unsigned int indicator_value;
+	unsigned int value;
 	int xapl_vendor;
 	int xapl_product;
 	int xapl_features;
@@ -740,6 +816,7 @@ static bool rfcomm_hfp_ag(struct rfcomm *rfcomm, char* buf)
 		}
 
 		/* send reply to HF with the features supported by Audio Gateway (=computer) */
+		ag_features |= mm_supported_features();
 		ag_features |= SPA_BT_HFP_AG_FEATURE_HF_INDICATORS;
 		rfcomm_send_reply(rfcomm, "+BRSF: %u", ag_features);
 		rfcomm_send_reply(rfcomm, "OK");
@@ -747,35 +824,32 @@ static bool rfcomm_hfp_ag(struct rfcomm *rfcomm, char* buf)
 		/* retrieve supported codecs */
 		/* response has the form AT+BAC=<codecID1>,<codecID2>,<codecIDx>
 		   strategy: split the string into tokens */
-		static const char separators[] = "=,";
 
 		char* token;
 		int cntr = 0;
 
-		token = strtok (buf, separators);
-		while (token != NULL)
-		{
+		while ((token = strsep(&buf, "=,"))) {
+			unsigned int codec_id;
+
 			/* skip token 0 i.e. the "AT+BAC=" part */
-			if (cntr > 0) {
-				int codec_id;
-				sscanf (token, "%u", &codec_id);
+			if (cntr > 0 && sscanf(token, "%u", &codec_id) == 1) {
 				spa_log_debug(backend->log, "RFCOMM AT+BAC found codec %u", codec_id);
 				if (codec_id == HFP_AUDIO_CODEC_MSBC) {
 					rfcomm->msbc_supported_by_hfp = true;
 					spa_log_debug(backend->log, "RFCOMM headset supports mSBC codec");
 				}
 			}
-			/* get next token */
-			token = strtok (NULL, separators);
 			cntr++;
 		}
 
 		rfcomm_send_reply(rfcomm, "OK");
 	} else if (spa_strstartswith(buf, "AT+CIND=?")) {
-		rfcomm_send_reply(rfcomm, "+CIND:(\"service\",(0-1)),(\"call\",(0-1)),(\"callsetup\",(0-3)),(\"callheld\",(0-2))");
+		rfcomm_send_reply(rfcomm, "+CIND:%s", CIND_INDICATORS);
 		rfcomm_send_reply(rfcomm, "OK");
 	} else if (spa_strstartswith(buf, "AT+CIND?")) {
-		rfcomm_send_reply(rfcomm, "+CIND: 0,%d,0,0", rfcomm->cind_call_active);
+		rfcomm_send_reply(rfcomm, "+CIND: %d,%d,%d,0,%d,%d,%d", backend->modem.network_has_service,
+		                  backend->modem.active_call, backend->modem.call_setup, backend->modem.signal_strength,
+		                  backend->modem.network_is_roaming, backend->battery_level);
 		rfcomm_send_reply(rfcomm, "OK");
 	} else if (spa_strstartswith(buf, "AT+CMER")) {
 		int mode, keyp, disp, ind;
@@ -808,8 +882,13 @@ static bool rfcomm_hfp_ag(struct rfcomm *rfcomm, char* buf)
 		}
 	} else if (!rfcomm->slc_configured) {
 		spa_log_warn(backend->log, "RFCOMM receive command before SLC completed: %s", buf);
-		rfcomm_send_reply(rfcomm, "ERROR");
-		return false;
+		rfcomm_send_error(rfcomm, CMEE_AG_FAILURE);
+		return true;
+
+	/* *****
+	 * Following commands requires a Service Level Connection
+	 * ***** */
+
 	} else if (sscanf(buf, "AT+BCS=%u", &selected_codec) == 1) {
 		/* parse BCS(=Bluetooth Codec Selection) reply */
 		bool was_switching_codec = rfcomm->hfp_ag_switching_codec && (rfcomm->device != NULL);
@@ -820,7 +899,7 @@ static bool rfcomm_hfp_ag(struct rfcomm *rfcomm, char* buf)
 
 		if (selected_codec != HFP_AUDIO_CODEC_CVSD && selected_codec != HFP_AUDIO_CODEC_MSBC) {
 			spa_log_warn(backend->log, "unsupported codec negotiation: %d", selected_codec);
-			rfcomm_send_reply(rfcomm, "ERROR");
+			rfcomm_send_error(rfcomm, CMEE_AG_FAILURE);
 			if (was_switching_codec)
 				spa_bt_device_emit_codec_switched(rfcomm->device, -EIO);
 			return true;
@@ -838,7 +917,7 @@ static bool rfcomm_hfp_ag(struct rfcomm *rfcomm, char* buf)
 		if (rfcomm->transport == NULL) {
 			spa_log_warn(backend->log, "can't create transport: %m");
 			// TODO: We should manage the missing transport
-			rfcomm_send_reply(rfcomm, "ERROR");
+			rfcomm_send_error(rfcomm, CMEE_AG_FAILURE);
 			if (was_switching_codec)
 				spa_bt_device_emit_codec_switched(rfcomm->device, -ENOMEM);
 			return true;
@@ -851,10 +930,105 @@ static bool rfcomm_hfp_ag(struct rfcomm *rfcomm, char* buf)
 		if (was_switching_codec)
 			spa_bt_device_emit_codec_switched(rfcomm->device, 0);
 	} else if (spa_strstartswith(buf, "AT+BIA=")) {
-		/* We only support 'call' indicator, which HFP 4.35.1 defines as
-		   always active (assuming CMER enabled it), so we don't need to
-		   parse anything here. */
+		/* retrieve indicators activation
+		 * form: AT+BIA=[indrep1],[indrep2],[indrepx] */
+		char *str = buf + 7;
+		unsigned int ind = 1;
+
+		while (*str && ind < CIND_MAX && *str != '\r' && *str != '\n') {
+			if (*str == ',') {
+				ind++;
+				goto next_indicator;
+			}
+
+			/* Ignore updates to mandantory indicators which are always ON */
+			if (ind == CIND_CALL || ind == CIND_CALLSETUP || ind == CIND_CALLHELD)
+				goto next_indicator;
+
+			switch (*str) {
+			case '0':
+				rfcomm->cind_enabled_indicators &= ~(1 << ind);
+				break;
+			case '1':
+				rfcomm->cind_enabled_indicators |= (1 << ind);
+				break;
+			default:
+				spa_log_warn(backend->log, "Unsupported entry in %s: %c", buf, *str);
+			}
+next_indicator:
+			str++;
+		}
+
+		rfcomm_send_reply(rfcomm, "OK");
+	} else if (spa_strstartswith(buf, "AT+CLCC")) {
+		struct spa_list *calls;
+		struct call *call;
+		unsigned int type;
+
+		if (backend->modemmanager) {
+			calls = mm_get_calls(backend->modemmanager);
+			spa_list_for_each(call, calls, link) {
+				if (!call->number) {
+					rfcomm_send_reply(rfcomm, "+CLCC: %u,%u,%u,0,%u", call->index, call->direction, call->state, call->multiparty);
+				} else {
+					if (spa_strstartswith(call->number, "+"))
+						type = INTERNATIONAL_NUMBER;
+					else
+						type = NATIONAL_NUMBER;
+					rfcomm_send_reply(rfcomm, "+CLCC: %u,%u,%u,0,%u,\"%s\",%d", call->index, call->direction, call->state,
+									call->multiparty, call->number, type);
+				}
+			}
+		}
+
+		rfcomm_send_reply(rfcomm, "OK");
+	} else if (sscanf(buf, "AT+CLIP=%u", &value) == 1) {
+		if (value > 1) {
+			spa_log_debug(backend->log, "Unsupported AT+CLIP value: %u", value);
+			rfcomm_send_error(rfcomm, CMEE_AG_FAILURE);
+			return true;
+		}
+
+		rfcomm->clip_notify = value;
+		rfcomm_send_reply(rfcomm, "OK");
+	} else if (sscanf(buf, "AT+CMEE=%u", &value) == 1) {
+		if (value > 1) {
+			spa_log_debug(backend->log, "Unsupported AT+CMEE value: %u", value);
+			rfcomm_send_error(rfcomm, CMEE_AG_FAILURE);
+			return true;
+		}
+
+		rfcomm->extended_error_reporting = value;
 		rfcomm_send_reply(rfcomm, "OK");
+	} else if (spa_strstartswith(buf, "AT+CNUM")) {
+		if (backend->modem.own_number) {
+			unsigned int type;
+			if (spa_strstartswith(backend->modem.own_number, "+"))
+				type = INTERNATIONAL_NUMBER;
+			else
+				type = NATIONAL_NUMBER;
+			rfcomm_send_reply(rfcomm, "+CNUM: ,\"%s\",%u", backend->modem.own_number, type);
+		}
+		rfcomm_send_reply(rfcomm, "OK");
+	} else if (spa_strstartswith(buf, "AT+COPS=")) {
+		unsigned int mode, val;
+
+		if (sscanf(buf, "AT+COPS=%u,%u", &mode, &val) != 2 ||
+		      mode != 3 || val != 0) {
+			rfcomm_send_error(rfcomm, CMEE_AG_FAILURE);
+		} else {
+			rfcomm_send_reply(rfcomm, "OK");
+		}
+	} else if (spa_strstartswith(buf, "AT+COPS?")) {
+		if (!backend->modem.network_has_service) {
+			rfcomm_send_error(rfcomm, CMEE_NO_NETWORK_SERVICE);
+		} else {
+			if (backend->modem.operator_name)
+				rfcomm_send_reply(rfcomm, "+COPS: 0,0,\"%s\"", backend->modem.operator_name);
+			else
+				rfcomm_send_reply(rfcomm, "+COPS: 0,,");
+			rfcomm_send_reply(rfcomm, "OK");
+		}
 	} else if (sscanf(buf, "AT+VGM=%u", &gain) == 1) {
 		if (gain <= SPA_BT_VOLUME_HS_MAX) {
 			if (!rfcomm->broken_mic_hw_volume)
@@ -862,7 +1036,7 @@ static bool rfcomm_hfp_ag(struct rfcomm *rfcomm, char* buf)
 			rfcomm_send_reply(rfcomm, "OK");
 		} else {
 			spa_log_debug(backend->log, "RFCOMM receive unsupported VGM gain: %s", buf);
-			rfcomm_send_reply(rfcomm, "ERROR");
+			rfcomm_send_error(rfcomm, CMEE_OPERATION_NOT_ALLOWED);
 		}
 	} else if (sscanf(buf, "AT+VGS=%u", &gain) == 1) {
 		if (gain <= SPA_BT_VOLUME_HS_MAX) {
@@ -870,7 +1044,7 @@ static bool rfcomm_hfp_ag(struct rfcomm *rfcomm, char* buf)
 			rfcomm_send_reply(rfcomm, "OK");
 		} else {
 			spa_log_debug(backend->log, "RFCOMM receive unsupported VGS gain: %s", buf);
-			rfcomm_send_reply(rfcomm, "ERROR");
+			rfcomm_send_error(rfcomm, CMEE_OPERATION_NOT_ALLOWED);
 		}
 	} else if (spa_strstartswith(buf, "AT+BIND=?")) {
 		rfcomm_send_reply(rfcomm, "+BIND: (2)");
@@ -909,6 +1083,69 @@ static bool rfcomm_hfp_ag(struct rfcomm *rfcomm, char* buf)
 	} else if (spa_strstartswith(buf, "AT+APLSIRI?")) {
 		// This command is sent when we activate Apple extensions
 		rfcomm_send_reply(rfcomm, "OK");
+	} else if (!mm_is_available(backend->modemmanager)) {
+		spa_log_warn(backend->log, "RFCOMM receive command but modem not available: %s", buf);
+		rfcomm_send_error(rfcomm, CMEE_NO_CONNECTION_TO_PHONE);
+		return true;
+
+	/* *****
+	 * Following commands requires a Service Level Connection
+	 * and acces to a modem
+	 * ***** */
+
+	} else if (!backend->modem.network_has_service) {
+		spa_log_warn(backend->log, "RFCOMM receive command but network not available: %s", buf);
+		rfcomm_send_error(rfcomm, CMEE_NO_NETWORK_SERVICE);
+		return true;
+
+	/* *****
+	 * Following commands requires a Service Level Connection,
+	 * acces to a modem and to the network
+	 * ***** */
+
+	} else if (spa_strstartswith(buf, "ATA")) {
+		enum cmee_error error;
+
+		if (!mm_answer_call(backend->modemmanager, rfcomm, &error)) {
+			rfcomm_send_error(rfcomm, error);
+			return true;
+		}
+	} else if (spa_strstartswith(buf, "ATD")) {
+		char number[31], sep;
+		enum cmee_error error;
+
+		if (sscanf(buf, "ATD%30[^;]%c", number, &sep) != 2 || sep != ';') {
+			spa_log_debug(backend->log, "Failed to parse ATD: \"%s\"", buf);
+			rfcomm_send_error(rfcomm, CMEE_AG_FAILURE);
+			return true;
+		}
+
+		if (!mm_do_call(backend->modemmanager, number, rfcomm, &error)) {
+			rfcomm_send_error(rfcomm, error);
+			return true;
+		}
+	} else if (spa_strstartswith(buf, "AT+CHUP")) {
+		enum cmee_error error;
+
+		if (!mm_hangup_call(backend->modemmanager, rfcomm, &error)) {
+			rfcomm_send_error(rfcomm, error);
+			return true;
+		}
+	} else if (spa_strstartswith(buf, "AT+VTS=")) {
+		char *dtmf;
+		enum cmee_error error;
+
+		dtmf = calloc(1, 2);
+		if (sscanf(buf, "AT+VTS=%1s", dtmf) != 1) {
+			spa_log_debug(backend->log, "Failed to parse AT+VTS: \"%s\"", buf);
+			rfcomm_send_error(rfcomm, CMEE_AG_FAILURE);
+			return true;
+		}
+
+		if (!mm_send_dtmf(backend->modemmanager, dtmf, rfcomm, &error)) {
+			rfcomm_send_error(rfcomm, error);
+			return true;
+		}
 	} else {
 		return false;
 	}
@@ -918,29 +1155,16 @@ static bool rfcomm_hfp_ag(struct rfcomm *rfcomm, char* buf)
 
 static bool rfcomm_hfp_hf(struct rfcomm *rfcomm, char* buf)
 {
-	static const char separators[] = "\r\n:";
-
 	struct impl *backend = rfcomm->backend;
-	unsigned int features;
-	unsigned int gain;
-	unsigned int selected_codec;
+	unsigned int features, gain, selected_codec, indicator, value;
 	char* token;
 
-	token = strtok(buf, separators);
-	while (token != NULL)
-	{
-		if (spa_strstartswith(token, "+BRSF")) {
-			/* get next token */
-			token = strtok(NULL, separators);
-			features = atoi(token);
+	while ((token = strsep(&buf, "\r\n"))) {
+		if (sscanf(token, "+BRSF:%u", &features) == 1) {
 			if (((features & (SPA_BT_HFP_AG_FEATURE_CODEC_NEGOTIATION)) != 0) &&
 			    rfcomm->msbc_supported_by_hfp)
 				rfcomm->codec_negotiation_supported = true;
-		} else if (spa_strstartswith(token, "+BCS") && rfcomm->codec_negotiation_supported) {
-			/* get next token */
-			token = strtok(NULL, separators);
-			selected_codec = atoi(token);
-
+		} else if (sscanf(token, "+BCS:%u", &selected_codec) == 1 && rfcomm->codec_negotiation_supported) {
 			if (selected_codec != HFP_AUDIO_CODEC_CVSD && selected_codec != HFP_AUDIO_CODEC_MSBC) {
 				spa_log_warn(backend->log, "unsupported codec negotiation: %d", selected_codec);
 			} else {
@@ -965,29 +1189,59 @@ static bool rfcomm_hfp_hf(struct rfcomm *rfcomm, char* buf)
 					}
 				}
 			}
-		} else if (spa_strstartswith(token, "+CIND")) {
-			/* get next token and discard it */
-			token = strtok(NULL, separators);
-		} else if (spa_strstartswith(token, "+VGM")) {
-			/* get next token */
-			token = strtok(NULL, separators);
-			gain = atoi(token);
-
+		} else if (sscanf(token, "+VGM%*1[:=]%u", &gain) == 1) {
 			if (gain <= SPA_BT_VOLUME_HS_MAX) {
 				rfcomm_emit_volume_changed(rfcomm, SPA_BT_VOLUME_ID_TX, gain);
 			} else {
 				spa_log_debug(backend->log, "RFCOMM receive unsupported VGM gain: %s", token);
 			}
-		} else if (spa_strstartswith(token, "+VGS")) {
-			/* get next token */
-			token = strtok(NULL, separators);
-			gain = atoi(token);
-
+		} else if (sscanf(token, "+VGS%*1[:=]%u", &gain) == 1) {
 			if (gain <= SPA_BT_VOLUME_HS_MAX) {
 				rfcomm_emit_volume_changed(rfcomm, SPA_BT_VOLUME_ID_RX, gain);
 			} else {
 				spa_log_debug(backend->log, "RFCOMM receive unsupported VGS gain: %s", token);
 			}
+		} else if (spa_strstartswith(token, "+CIND: (")) {
+			uint8_t i = 1;
+			while (strstr(token, "\"")) {
+				token += strcspn(token, "\"") + 1;
+				token[strcspn(token, "\"")] = 0;
+				rfcomm->hf_indicators[i] = strdup(token);
+				token += strcspn(token, "\"") + 1;
+				i++;
+				if (i == MAX_HF_INDICATORS) {
+					break;
+				}
+			}
+		} else if (spa_strstartswith(token, "+CIND: ")) {
+			token[strcspn(token, "\r")] = 0;
+			token[strcspn(token, "\n")] = 0;
+			token += strlen("+CIND: ");
+			uint8_t i = 1;
+			while (strlen(token)) {
+				if (i >= MAX_HF_INDICATORS || !rfcomm->hf_indicators[i]) {
+					break;
+				}
+				token[strcspn(token, ",")] = 0;
+				spa_log_info(backend->log, "AG indicator state: %s = %i", rfcomm->hf_indicators[i], atoi(token));
+
+				if (spa_streq(rfcomm->hf_indicators[i], "battchg")) {
+					spa_bt_device_report_battery_level(rfcomm->device, atoi(token) * 100 / 5);
+				}
+
+				token += strcspn(token, "\0") + 1;
+				i++;
+			}
+		} else if (sscanf(token, "+CIEV: %u,%u", &indicator, &value) == 2) {
+			if (indicator >= MAX_HF_INDICATORS || !rfcomm->hf_indicators[indicator]) {
+				spa_log_warn(backend->log, "indicator %u has not been registered, ignoring", indicator);
+			} else {
+				spa_log_info(backend->log, "AG indicator update: %s = %u", rfcomm->hf_indicators[indicator], value);
+
+				if (spa_streq(rfcomm->hf_indicators[indicator], "battchg")) {
+					spa_bt_device_report_battery_level(rfcomm->device, value * 100 / 5);
+				}
+			}
 		} else if (spa_strstartswith(token, "OK")) {
 			switch(rfcomm->hf_state) {
 				case hfp_hf_brsf:
@@ -1008,7 +1262,7 @@ static bool rfcomm_hfp_hf(struct rfcomm *rfcomm, char* buf)
 					rfcomm->hf_state = hfp_hf_cind2;
 					break;
 				case hfp_hf_cind2:
-					rfcomm_send_cmd(rfcomm, "AT+CMER=3,0,0,0");
+					rfcomm_send_cmd(rfcomm, "AT+CMER=3,0,0,1");
 					rfcomm->hf_state = hfp_hf_cmer;
 					break;
 				case hfp_hf_cmer:
@@ -1041,8 +1295,6 @@ static bool rfcomm_hfp_hf(struct rfcomm *rfcomm, char* buf)
 					break;
 			}
 		}
-		/* get next token */
-		token = strtok(NULL, separators);
 	}
 
 	return true;
@@ -1097,7 +1349,7 @@ static void rfcomm_event(struct spa_source *source)
 
 		if (!res) {
 			spa_log_debug(backend->log, "RFCOMM received unsupported command: %s", buf);
-			rfcomm_send_reply(rfcomm, "ERROR");
+			rfcomm_send_error(rfcomm, CMEE_OPERATION_NOT_SUPPORTED);
 		}
 	}
 }
@@ -1866,6 +2118,9 @@ static DBusHandlerResult profile_new_connection(DBusConnection *conn, DBusMessag
 	rfcomm->source.fd = fd;
 	rfcomm->source.mask = SPA_IO_IN;
 	rfcomm->source.rmask = 0;
+	/* By default all indicators are enabled */
+	rfcomm->cind_enabled_indicators = 0xFFFFFFFF;
+	memset(rfcomm->hf_indicators, 0, sizeof rfcomm->hf_indicators);
 
 	for (int i = 0; i < SPA_BT_VOLUME_ID_TERM; ++i) {
 		if (rfcomm->profile & SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY)
@@ -2250,6 +2505,161 @@ static int backend_native_unregister_profiles(void *data)
 	return 0;
 }
 
+static void send_ciev_for_each_rfcomm(struct impl *backend, int indicator, int value)
+{
+	struct rfcomm *rfcomm;
+
+	spa_list_for_each(rfcomm, &backend->rfcomm_list, link) {
+		if (rfcomm->slc_configured &&
+		    ((indicator == CIND_CALL || indicator == CIND_CALLSETUP || indicator == CIND_CALLHELD) ||
+			(rfcomm->cind_call_notify && (rfcomm->cind_enabled_indicators & (1 << indicator)))))
+			rfcomm_send_reply(rfcomm, "+CIEV: %d,%d", indicator, value);
+	}
+}
+
+static void ring_timer_event(void *data, uint64_t expirations)
+{
+	struct impl *backend = data;
+	const char *number;
+	unsigned int type;
+	struct timespec ts;
+	const uint64_t timeout = 1 * SPA_NSEC_PER_SEC;
+	struct rfcomm *rfcomm;
+
+	number = mm_get_incoming_call_number(backend->modemmanager);
+	if (number) {
+		if (spa_strstartswith(number, "+"))
+			type = INTERNATIONAL_NUMBER;
+		else
+			type = NATIONAL_NUMBER;
+	}
+
+	ts.tv_sec = timeout / SPA_NSEC_PER_SEC;
+	ts.tv_nsec = timeout % SPA_NSEC_PER_SEC;
+	spa_loop_utils_update_timer(backend->loop_utils, backend->ring_timer, &ts, NULL, false);
+
+	spa_list_for_each(rfcomm, &backend->rfcomm_list, link) {
+		if (rfcomm->slc_configured) {
+			rfcomm_send_reply(rfcomm, "RING");
+			if (rfcomm->clip_notify && number)
+				rfcomm_send_reply(rfcomm, "+CLIP: \"%s\",%u", number, type);
+		}
+	}
+}
+
+static void set_call_active(bool active, void *user_data)
+{
+	struct impl *backend = user_data;
+
+	if (backend->modem.active_call != active) {
+		backend->modem.active_call = active;
+		send_ciev_for_each_rfcomm(backend, CIND_CALL, active);
+	}
+}
+
+static void set_call_setup(enum call_setup value, void *user_data)
+{
+	struct impl *backend = user_data;
+	enum call_setup old = backend->modem.call_setup;
+
+	if (backend->modem.call_setup != value) {
+		backend->modem.call_setup = value;
+		send_ciev_for_each_rfcomm(backend, CIND_CALLSETUP, value);
+	}
+
+	if (value == CIND_CALLSETUP_INCOMING) {
+		if (backend->ring_timer == NULL)
+			backend->ring_timer = spa_loop_utils_add_timer(backend->loop_utils, ring_timer_event, backend);
+
+		if (backend->ring_timer == NULL) {
+			spa_log_warn(backend->log, "Failed to create ring timer");
+			return;
+		}
+
+		ring_timer_event(backend, 0);
+	} else if (old == CIND_CALLSETUP_INCOMING) {
+		spa_loop_utils_update_timer(backend->loop_utils, backend->ring_timer, NULL, NULL, false);
+	}
+}
+
+void set_battery_level(unsigned int level, void *user_data)
+{
+	struct impl *backend = user_data;
+
+	if (backend->battery_level != level) {
+		backend->battery_level = level;
+		send_ciev_for_each_rfcomm(backend, CIND_BATTERY_LEVEL, level);
+	}
+}
+
+static void set_modem_operator_name(const char *name, void *user_data)
+{
+	struct impl *backend = user_data;
+
+	if (backend->modem.operator_name) {
+		free(backend->modem.operator_name);
+		backend->modem.operator_name = NULL;
+	}
+
+	if (name)
+		backend->modem.operator_name = strdup(name);
+}
+
+static void set_modem_roaming(bool is_roaming, void *user_data)
+{
+	struct impl *backend = user_data;
+
+	if (backend->modem.network_is_roaming != is_roaming) {
+		backend->modem.network_is_roaming = is_roaming;
+		send_ciev_for_each_rfcomm(backend, CIND_ROAM, is_roaming);
+	}
+}
+
+static void set_modem_own_number(const char *number, void *user_data)
+{
+	struct impl *backend = user_data;
+
+	if (backend->modem.own_number) {
+		free(backend->modem.own_number);
+		backend->modem.own_number = NULL;
+	}
+
+	if (number)
+		backend->modem.own_number = strdup(number);
+}
+
+static void set_modem_service(bool available, void *user_data)
+{
+	struct impl *backend = user_data;
+
+	if (backend->modem.network_has_service != available) {
+		backend->modem.network_has_service = available;
+		send_ciev_for_each_rfcomm(backend, CIND_SERVICE, available);
+	}
+}
+
+static void set_modem_signal_strength(unsigned int strength, void *user_data)
+{
+	struct impl *backend = user_data;
+
+	if (backend->modem.signal_strength != strength) {
+		backend->modem.signal_strength = strength;
+		send_ciev_for_each_rfcomm(backend, CIND_SIGNAL, strength);
+	}
+}
+
+static void send_cmd_result(bool success, enum cmee_error error, void *user_data)
+{
+	struct rfcomm *rfcomm = user_data;
+
+	if (success) {
+		rfcomm_send_reply(rfcomm, "OK");
+		return;
+	}
+
+	rfcomm_send_error(rfcomm, error);
+}
+
 static int backend_native_free(void *data)
 {
 	struct impl *backend = data;
@@ -2258,6 +2668,19 @@ static int backend_native_free(void *data)
 
 	sco_close(backend);
 
+	if (backend->modemmanager) {
+		mm_unregister(backend);
+		backend->modemmanager = NULL;
+	}
+
+	if (backend->upower) {
+		upower_unregister(backend->upower);
+		backend->upower = NULL;
+	}
+
+	if (backend->ring_timer)
+		spa_loop_utils_destroy_source(backend->loop_utils, backend->ring_timer);
+
 #ifdef HAVE_BLUEZ_5_BACKEND_HSP_NATIVE
 	dbus_connection_unregister_object_path(backend->conn, PROFILE_HSP_AG);
 	dbus_connection_unregister_object_path(backend->conn, PROFILE_HSP_HS);
@@ -2271,6 +2694,8 @@ static int backend_native_free(void *data)
 	spa_list_consume(rfcomm, &backend->rfcomm_list, link)
 		rfcomm_free(rfcomm);
 
+	if (backend->modem.operator_name)
+		free(backend->modem.operator_name);
 	free(backend);
 
 	return 0;
@@ -2305,6 +2730,17 @@ static const struct spa_bt_backend_implementation backend_impl = {
 	.supports_codec = backend_native_supports_codec,
 };
 
+static const struct mm_ops mm_ops = {
+	.send_cmd_result = send_cmd_result,
+	.set_modem_service = set_modem_service,
+	.set_modem_signal_strength = set_modem_signal_strength,
+	.set_modem_operator_name = set_modem_operator_name,
+	.set_modem_own_number = set_modem_own_number,
+	.set_modem_roaming = set_modem_roaming,
+	.set_call_active = set_call_active,
+	.set_call_setup = set_call_setup,
+};
+
 struct spa_bt_backend *backend_native_new(struct spa_bt_monitor *monitor,
 		void *dbus_connection,
 		const struct spa_dict *info,
@@ -2370,6 +2806,9 @@ struct spa_bt_backend *backend_native_new(struct spa_bt_monitor *monitor,
 	}
 #endif
 
+	backend->modemmanager = mm_register(backend->log, backend->conn, info, &mm_ops, backend);
+	backend->upower = upower_register(backend->log, backend->conn, set_battery_level, backend);
+
 	return &backend->this;
 
 #ifdef HAVE_BLUEZ_5_BACKEND_HFP_NATIVE
diff --git a/spa/plugins/bluez5/bap-codec-caps.h b/spa/plugins/bluez5/bap-codec-caps.h
new file mode 100644
index 0000000000000000000000000000000000000000..7bfac3529caec8483623401d4d4ade9b465080f6
--- /dev/null
+++ b/spa/plugins/bluez5/bap-codec-caps.h
@@ -0,0 +1,142 @@
+/* Spa BAP codec API
+ *
+ * Copyright © 2022 Collabora
+ *
+ * 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_BLUEZ5_BAP_CODEC_CAPS_H_
+#define SPA_BLUEZ5_BAP_CODEC_CAPS_H_
+
+#define BAP_CODEC_LC3           0x06
+
+#define LC3_TYPE_FREQ           0x01
+#define LC3_FREQ_8KHZ           (1 << 0)
+#define LC3_FREQ_11KHZ          (1 << 1)
+#define LC3_FREQ_16KHZ          (1 << 2)
+#define LC3_FREQ_22KHZ          (1 << 3)
+#define LC3_FREQ_24KHZ          (1 << 4)
+#define LC3_FREQ_32KHZ          (1 << 5)
+#define LC3_FREQ_44KHZ          (1 << 6)
+#define LC3_FREQ_48KHZ          (1 << 7)
+#define LC3_FREQ_ANY            (LC3_FREQ_8KHZ | \
+                                 LC3_FREQ_11KHZ | \
+                                 LC3_FREQ_16KHZ | \
+                                 LC3_FREQ_22KHZ | \
+                                 LC3_FREQ_24KHZ | \
+                                 LC3_FREQ_32KHZ | \
+                                 LC3_FREQ_44KHZ | \
+                                 LC3_FREQ_48KHZ)
+
+#define LC3_TYPE_DUR            0x02
+#define LC3_DUR_7_5             (1 << 0)
+#define LC3_DUR_10              (1 << 1)
+#define LC3_DUR_ANY             (LC3_DUR_7_5 | \
+                                 LC3_DUR_10)
+
+#define LC3_TYPE_CHAN           0x03
+#define LC3_CHAN_1              (1 << 0)
+#define LC3_CHAN_2              (1 << 1)
+
+#define LC3_TYPE_FRAMELEN       0x04
+#define LC3_TYPE_BLKS           0x05
+
+/* LC3 config parameters */
+#define LC3_CONFIG_FREQ_8KHZ    0x01
+#define LC3_CONFIG_FREQ_11KHZ   0x02
+#define LC3_CONFIG_FREQ_16KHZ   0x03
+#define LC3_CONFIG_FREQ_22KHZ   0x04
+#define LC3_CONFIG_FREQ_24KHZ   0x05
+#define LC3_CONFIG_FREQ_32KHZ   0x06
+#define LC3_CONFIG_FREQ_44KHZ   0x07
+#define LC3_CONFIG_FREQ_48KHZ   0x08
+
+#define LC3_CONFIG_DURATION_7_5 0x00
+#define LC3_CONFIG_DURATION_10  0x01
+
+#define LC3_CONFIG_CHNL_NOT_ALLOWED 0x00000000
+#define LC3_CONFIG_CHNL_FL          0x00000001 /* front left */
+#define LC3_CONFIG_CHNL_FR          0x00000002 /* front right */
+#define LC3_CONFIG_CHNL_FC          0x00000004 /* front center */
+#define LC3_CONFIG_CHNL_LFE         0x00000008 /* LFE */
+#define LC3_CONFIG_CHNL_BL          0x00000010 /* back left */
+#define LC3_CONFIG_CHNL_BR          0x00000020 /* back right */
+#define LC3_CONFIG_CHNL_FLC         0x00000040 /* front left center */
+#define LC3_CONFIG_CHNL_FRC         0x00000080 /* front right center */
+#define LC3_CONFIG_CHNL_BC          0x00000100 /* back center */
+#define LC3_CONFIG_CHNL_LFE2		0x00000200 /* LFE 2 */
+#define LC3_CONFIG_CHNL_SL          0x00000400 /* side left */
+#define LC3_CONFIG_CHNL_SR          0x00000800 /* side right */
+#define LC3_CONFIG_CHNL_TFL         0x00001000 /* top front left */
+#define LC3_CONFIG_CHNL_TFR         0x00002000 /* top front right */
+#define LC3_CONFIG_CHNL_TFC         0x00004000 /* top front center */
+#define LC3_CONFIG_CHNL_TC          0x00008000 /* top center */
+#define LC3_CONFIG_CHNL_TBL         0x00010000 /* top back left */
+#define LC3_CONFIG_CHNL_TBR         0x00020000 /* top back right */
+#define LC3_CONFIG_CHNL_TSL         0x00040000 /* top side left */
+#define LC3_CONFIG_CHNL_TSR         0x00080000 /* top side right */
+#define LC3_CONFIG_CHNL_TBC         0x00100000 /* top back center */
+#define LC3_CONFIG_CHNL_BFC         0x00200000 /* bottom front center */
+#define LC3_CONFIG_CHNL_BFL         0x00400000 /* bottom front left */
+#define LC3_CONFIG_CHNL_BFR         0x00800000 /* bottom front right */
+#define LC3_CONFIG_CHNL_FLW         0x01000000 /* front left wide */
+#define LC3_CONFIG_CHNL_FRW         0x02000000 /* front right wide */
+#define LC3_CONFIG_CHNL_LS          0x04000000 /* left surround */
+#define LC3_CONFIG_CHNL_RS          0x08000000 /* right surround */
+
+#define LC3_MAX_CHANNELS 28
+
+typedef struct {
+    uint8_t rate;
+	uint8_t frame_duration;
+	uint32_t channels;
+	uint16_t framelen;
+	uint8_t n_blks;
+} __attribute__ ((packed)) bap_lc3_t;
+
+#define BT_ISO_QOS_CIG_UNSET    0xff
+#define BT_ISO_QOS_CIS_UNSET    0xff
+
+#define BT_ISO_QOS_TARGET_LATENCY_LOW		0x01
+#define BT_ISO_QOS_TARGET_LATENCY_BALANCED	0x02
+#define BT_ISO_QOS_TARGET_LATENCY_RELIABILITY	0x03
+
+struct bap_endpoint_qos {
+        uint8_t  framing;
+        uint8_t  phy;
+        uint8_t  retransmission;
+        uint16_t latency;
+        uint32_t delay_min;
+        uint32_t delay_max;
+        uint32_t preferred_delay_min;
+        uint32_t preferred_delay_max;
+};
+
+struct bap_codec_qos {
+	uint32_t interval;
+	uint8_t framing;
+	uint8_t phy;
+	uint16_t sdu;
+	uint8_t retransmission;
+	uint16_t latency;
+	uint32_t delay;
+	uint8_t target_latency;
+};
+
+#endif
diff --git a/spa/plugins/bluez5/bap-codec-lc3.c b/spa/plugins/bluez5/bap-codec-lc3.c
new file mode 100644
index 0000000000000000000000000000000000000000..29e09f1c9410c6d3e9744bb1b495cef79bf034e2
--- /dev/null
+++ b/spa/plugins/bluez5/bap-codec-lc3.c
@@ -0,0 +1,790 @@
+/* Spa BAP LC3 codec
+ *
+ * Copyright © 2020 Wim Taymans
+ * Copyright © 2022 Pauli Virtanen
+ * Copyright © 2022 Collabora
+ *
+ * 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 <unistd.h>
+#include <stddef.h>
+#include <errno.h>
+#include <arpa/inet.h>
+#include <bluetooth/bluetooth.h>
+
+#include <spa/param/audio/format.h>
+#include <spa/param/audio/format-utils.h>
+
+#include <lc3.h>
+
+#include "media-codecs.h"
+#include "bap-codec-caps.h"
+
+struct impl {
+	lc3_encoder_t enc[LC3_MAX_CHANNELS];
+	lc3_decoder_t dec[LC3_MAX_CHANNELS];
+
+	int mtu;
+	int samplerate;
+	int channels;
+	int frame_dus;
+	int framelen;
+	int samples;
+	unsigned int codesize;
+};
+
+struct ltv {
+	uint8_t  len;
+	uint8_t  type;
+	uint8_t  value[0];
+} __packed;
+
+static int write_ltv(uint8_t *dest, uint8_t type, void* value, size_t len)
+{
+	struct ltv *ltv = (struct ltv *)dest;
+
+	ltv->len = len + 1;
+	ltv->type = type;
+	memcpy(ltv->value, value, len);
+
+	return len + 2;
+}
+
+static int write_ltv_uint8(uint8_t *dest, uint8_t type, uint8_t value)
+{
+	return write_ltv(dest, type, &value, sizeof(value));
+}
+
+static int write_ltv_uint16(uint8_t *dest, uint8_t type, uint16_t value)
+{
+	return write_ltv(dest, type, &value, sizeof(value));
+}
+
+static int write_ltv_uint32(uint8_t *dest, uint8_t type, uint32_t value)
+{
+	return write_ltv(dest, type, &value, sizeof(value));
+}
+
+static int codec_fill_caps(const struct media_codec *codec, uint32_t flags,
+		uint8_t caps[A2DP_MAX_CAPS_SIZE])
+{
+	uint8_t *data = caps;
+	uint16_t framelen[2] = {htobs(LC3_MIN_FRAME_BYTES), htobs(LC3_MAX_FRAME_BYTES)};
+
+	data += write_ltv_uint16(data, LC3_TYPE_FREQ,
+	                         htobs(LC3_FREQ_48KHZ | LC3_FREQ_24KHZ | LC3_FREQ_16KHZ | LC3_FREQ_8KHZ));
+	data += write_ltv_uint8(data, LC3_TYPE_DUR, LC3_DUR_ANY);
+	data += write_ltv_uint8(data, LC3_TYPE_CHAN, LC3_CHAN_1 | LC3_CHAN_2);
+	data += write_ltv(data, LC3_TYPE_FRAMELEN, framelen, sizeof(framelen));
+	data += write_ltv_uint8(data, LC3_TYPE_BLKS, 2);
+
+	return data - caps;
+}
+
+static bool parse_capabilities(bap_lc3_t *conf, const uint8_t *data, size_t data_size)
+{
+	uint16_t framelen_min = 0, framelen_max = 0;
+
+	if (!data_size)
+		return false;
+	memset(conf, 0, sizeof(*conf));
+
+	conf->frame_duration = 0xFF;
+
+	while (data_size > 0) {
+		struct ltv *ltv = (struct ltv *)data;
+
+		if (ltv->len > data_size)
+			return false;
+
+		switch (ltv->type) {
+		case LC3_TYPE_FREQ:
+			spa_return_val_if_fail(ltv->len == 3, false);
+			{
+				uint16_t rate = ltv->value[0] + (ltv->value[1] << 8);
+				if (rate & LC3_FREQ_48KHZ)
+					conf->rate = LC3_CONFIG_FREQ_48KHZ;
+				else if (rate & LC3_FREQ_24KHZ)
+					conf->rate = LC3_CONFIG_FREQ_24KHZ;
+				else if (rate & LC3_FREQ_16KHZ)
+					conf->rate = LC3_CONFIG_FREQ_16KHZ;
+				else if (rate & LC3_FREQ_8KHZ)
+					conf->rate = LC3_CONFIG_FREQ_8KHZ;
+				else
+					return false;
+			}
+			break;
+		case LC3_TYPE_DUR:
+			spa_return_val_if_fail(ltv->len == 2, false);
+			{
+				uint8_t duration = ltv->value[0];
+				if (duration & LC3_DUR_10)
+					conf->frame_duration = LC3_CONFIG_DURATION_10;
+				else if (duration & LC3_DUR_7_5)
+					conf->frame_duration = LC3_CONFIG_DURATION_7_5;
+				else
+					return false;
+			}
+			break;
+		case LC3_TYPE_CHAN:
+			spa_return_val_if_fail(ltv->len == 2, false);
+			{
+				uint8_t channels = ltv->value[0];
+				/* Only mono or stereo streams are currently supported,
+				 * in both case Audio location is defined as both Front Left
+				 * and Front Right, difference is done by the n_blks parameter.
+				 */
+				if ((channels & LC3_CHAN_2) || (channels & LC3_CHAN_1))
+					conf->channels = LC3_CONFIG_CHNL_FR | LC3_CONFIG_CHNL_FL;
+				else
+					return false;
+			}
+			break;
+		case LC3_TYPE_FRAMELEN:
+			spa_return_val_if_fail(ltv->len == 5, false);
+			framelen_min = ltv->value[0] + (ltv->value[1] << 8);
+			framelen_max = ltv->value[2] + (ltv->value[3] << 8);
+			break;
+		case LC3_TYPE_BLKS:
+			spa_return_val_if_fail(ltv->len == 2, false);
+			conf->n_blks = ltv->value[0];
+			if (!conf->n_blks)
+				return false;
+			break;
+		default:
+			return false;
+		}
+		data_size -= ltv->len + 1;
+		data += ltv->len + 1;
+	}
+
+	if (framelen_min < LC3_MIN_FRAME_BYTES || framelen_max > LC3_MAX_FRAME_BYTES)
+		return false;
+	if (conf->frame_duration == 0xFF || !conf->rate)
+		return false;
+	if (!conf->channels)
+		conf->channels = LC3_CONFIG_CHNL_FL;
+
+	switch (conf->rate) {
+	case LC3_CONFIG_FREQ_48KHZ:
+		if (conf->frame_duration == LC3_CONFIG_DURATION_7_5)
+			conf->framelen = 117;
+		else
+			conf->framelen = 120;
+		break;
+	case LC3_CONFIG_FREQ_24KHZ:
+		if (conf->frame_duration == LC3_CONFIG_DURATION_7_5)
+			conf->framelen = 45;
+		else
+			conf->framelen = 60;
+		break;
+	case LC3_CONFIG_FREQ_16KHZ:
+		if (conf->frame_duration == LC3_CONFIG_DURATION_7_5)
+			conf->framelen = 30;
+		else
+			conf->framelen = 40;
+		break;
+	case LC3_CONFIG_FREQ_8KHZ:
+		if (conf->frame_duration == LC3_CONFIG_DURATION_7_5)
+			conf->framelen = 26;
+		else
+			conf->framelen = 30;
+		break;
+	default:
+			return false;
+	}
+
+	return true;
+}
+
+static bool parse_conf(bap_lc3_t *conf, const uint8_t *data, size_t data_size)
+{
+	if (!data_size)
+		return false;
+	memset(conf, 0, sizeof(*conf));
+
+	conf->frame_duration = 0xFF;
+
+	while (data_size > 0) {
+		struct ltv *ltv = (struct ltv *)data;
+
+		if (ltv->len > data_size)
+			return false;
+
+		switch (ltv->type) {
+		case LC3_TYPE_FREQ:
+			spa_return_val_if_fail(ltv->len == 2, false);
+			conf->rate = ltv->value[0];
+			break;
+		case LC3_TYPE_DUR:
+			spa_return_val_if_fail(ltv->len == 2, false);
+			conf->frame_duration = ltv->value[0];
+			break;
+		case LC3_TYPE_CHAN:
+			spa_return_val_if_fail(ltv->len == 5, false);
+			conf->channels = ltv->value[0] + (ltv->value[1] << 8) + (ltv->value[2] << 16) + (ltv->value[3] << 24);
+			break;
+		case LC3_TYPE_FRAMELEN:
+			spa_return_val_if_fail(ltv->len == 3, false);
+			conf->framelen = ltv->value[0] + (ltv->value[1] << 8);
+			break;
+		case LC3_TYPE_BLKS:
+			spa_return_val_if_fail(ltv->len == 2, false);
+			conf->n_blks = ltv->value[0];
+			if (!conf->n_blks)
+				return false;
+			break;
+		default:
+			return false;
+		}
+		data_size -= ltv->len + 1;
+		data += ltv->len + 1;
+	}
+
+	if (conf->frame_duration == 0xFF || !conf->rate)
+		return false;
+
+	return true;
+}
+
+static int codec_select_config(const struct media_codec *codec, uint32_t flags,
+		const void *caps, size_t caps_size,
+		const struct media_codec_audio_info *info,
+		const struct spa_dict *settings, uint8_t config[A2DP_MAX_CAPS_SIZE])
+{
+	bap_lc3_t conf;
+	uint8_t *data = config;
+
+	if (caps == NULL)
+		return -EINVAL;
+
+	if (!parse_capabilities(&conf, caps, caps_size))
+		return -ENOTSUP;
+
+	data += write_ltv_uint8(data, LC3_TYPE_FREQ, conf.rate);
+	data += write_ltv_uint8(data, LC3_TYPE_DUR, conf.frame_duration);
+	data += write_ltv_uint32(data, LC3_TYPE_CHAN, htobl(conf.channels));
+	data += write_ltv_uint16(data, LC3_TYPE_FRAMELEN, htobs(conf.framelen));
+	data += write_ltv_uint8(data, LC3_TYPE_BLKS, conf.n_blks);
+
+	return data - config;
+}
+
+static int codec_caps_preference_cmp(const struct media_codec *codec, uint32_t flags, const void *caps1, size_t caps1_size,
+		const void *caps2, size_t caps2_size, const struct media_codec_audio_info *info, const struct spa_dict *global_settings)
+{
+	bap_lc3_t conf1, conf2;
+	bap_lc3_t *conf;
+	int res1, res2;
+	int a, b;
+
+	/* 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);
+
+#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(bap_lc3_t)) ? 1 : 0;
+	b = (res2 > 0 && (size_t)res2 == sizeof(bap_lc3_t)) ? 1 : 0;
+	if (!a || !b)
+		return b - a;
+
+	PREFER_BOOL(conf->channels & LC3_CHAN_2);
+	PREFER_BOOL(conf->rate & (LC3_CONFIG_FREQ_48KHZ | LC3_CONFIG_FREQ_24KHZ | LC3_CONFIG_FREQ_16KHZ | LC3_CONFIG_FREQ_8KHZ));
+	PREFER_BOOL(conf->rate & LC3_CONFIG_FREQ_48KHZ);
+
+	return 0;
+
+#undef PREFER_EXPR
+#undef PREFER_BOOL
+}
+
+static uint8_t channels_to_positions(uint32_t channels, uint8_t n_channels, uint32_t *position)
+{
+	uint8_t n_positions = 0;
+
+	spa_assert(n_channels <= SPA_AUDIO_MAX_CHANNELS);
+
+	/* First check if stream is configure for Mono, i.e. 1 block for both Front
+	 * Left anf Front Right,
+	 * else map LE Audio locations to PipeWire locations in the ascending order
+	 * which will be used as block order in stream.
+	 */
+	if ((channels & (LC3_CONFIG_CHNL_FR | LC3_CONFIG_CHNL_FL)) == (LC3_CONFIG_CHNL_FR | LC3_CONFIG_CHNL_FL) &&
+	     n_channels == 1) {
+		position[0] = SPA_AUDIO_CHANNEL_MONO;
+		n_positions = 1;
+	} else {
+#define CHANNEL_2_SPACHANNEL(channel,spa_channel)	if (channels & channel) position[n_positions++] = spa_channel;
+
+		CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_FL,   SPA_AUDIO_CHANNEL_FL);
+		CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_FR,   SPA_AUDIO_CHANNEL_FR);
+		CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_FC,   SPA_AUDIO_CHANNEL_FC);
+		CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_LFE,  SPA_AUDIO_CHANNEL_LFE);
+		CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_BL,   SPA_AUDIO_CHANNEL_RL);
+		CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_BR,   SPA_AUDIO_CHANNEL_RR);
+		CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_FLC,  SPA_AUDIO_CHANNEL_FLC);
+		CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_FRC,  SPA_AUDIO_CHANNEL_FRC);
+		CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_BC,   SPA_AUDIO_CHANNEL_BC);
+		CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_LFE2, SPA_AUDIO_CHANNEL_LFE2);
+		CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_SL,   SPA_AUDIO_CHANNEL_SL);
+		CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_SR,   SPA_AUDIO_CHANNEL_SR);
+		CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_TFL,  SPA_AUDIO_CHANNEL_TFL);
+		CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_TFR,  SPA_AUDIO_CHANNEL_TFR);
+		CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_TFC,  SPA_AUDIO_CHANNEL_TFC);
+		CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_TC,   SPA_AUDIO_CHANNEL_TC);
+		CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_TBL,  SPA_AUDIO_CHANNEL_TRL);
+		CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_TBR,  SPA_AUDIO_CHANNEL_TRR);
+		CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_TSL,  SPA_AUDIO_CHANNEL_TSL);
+		CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_TSR,  SPA_AUDIO_CHANNEL_TSR);
+		CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_TBC,  SPA_AUDIO_CHANNEL_TRC);
+		CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_BFC,  SPA_AUDIO_CHANNEL_BC);
+		CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_BFL,  SPA_AUDIO_CHANNEL_BLC);
+		CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_BFR,  SPA_AUDIO_CHANNEL_BRC);
+		CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_FLW,  SPA_AUDIO_CHANNEL_FLW);
+		CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_FRW,  SPA_AUDIO_CHANNEL_FRW);
+		CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_LS,   SPA_AUDIO_CHANNEL_LLFE); /* is it the right mapping? */
+		CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_RS,   SPA_AUDIO_CHANNEL_RLFE); /* is it the right mapping? */
+
+#undef CHANNEL_2_SPACHANNEL
+	}
+
+	return n_positions;
+}
+
+static int codec_enum_config(const struct media_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)
+{
+	bap_lc3_t conf;
+	struct spa_pod_frame f[2];
+	struct spa_pod_choice *choice;
+	uint32_t position[SPA_AUDIO_MAX_CHANNELS];
+	uint32_t i = 0;
+	uint8_t res;
+
+	if (!parse_conf(&conf, caps, caps_size))
+		return -EINVAL;
+
+	if (idx > 0)
+		return 0;
+
+	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_S24_32),
+			0);
+	spa_pod_builder_prop(b, SPA_FORMAT_AUDIO_rate, 0);
+
+	spa_pod_builder_push_choice(b, &f[1], SPA_CHOICE_None, 0);
+	choice = (struct spa_pod_choice*)spa_pod_builder_frame(b, &f[1]);
+	i = 0;
+	if (conf.rate & LC3_CONFIG_FREQ_48KHZ) {
+		if (i++ == 0)
+			spa_pod_builder_int(b, 48000);
+		spa_pod_builder_int(b, 48000);
+	}
+	if (conf.rate & LC3_CONFIG_FREQ_24KHZ) {
+		if (i++ == 0)
+			spa_pod_builder_int(b, 24000);
+		spa_pod_builder_int(b, 24000);
+	}
+	if (conf.rate & LC3_CONFIG_FREQ_16KHZ) {
+		if (i++ == 0)
+			spa_pod_builder_int(b, 16000);
+		spa_pod_builder_int(b, 16000);
+	}
+	if (conf.rate & LC3_CONFIG_FREQ_8KHZ) {
+		if (i++ == 0)
+			spa_pod_builder_int(b, 8000);
+		spa_pod_builder_int(b, 8000);
+	}
+	if (i == 0)
+		return -EINVAL;
+	if (i > 1)
+		choice->body.type = SPA_CHOICE_Enum;
+	spa_pod_builder_pop(b, &f[1]);
+
+	res = channels_to_positions(conf.channels, conf.n_blks, position);
+	if (res == 0)
+		return -EINVAL;
+	spa_pod_builder_add(b,
+			SPA_FORMAT_AUDIO_channels, SPA_POD_Int(res),
+			SPA_FORMAT_AUDIO_position, SPA_POD_Array(sizeof(uint32_t),
+				SPA_TYPE_Id, res, position),
+			0);
+
+	*param = spa_pod_builder_pop(b, &f[0]);
+	return *param == NULL ? -EIO : 1;
+}
+
+static int codec_validate_config(const struct media_codec *codec, uint32_t flags,
+			const void *caps, size_t caps_size,
+			struct spa_audio_info *info)
+{
+	bap_lc3_t conf;
+	uint8_t res;
+
+	if (caps == NULL)
+		return -EINVAL;
+
+	if (!parse_conf(&conf, caps, caps_size))
+		return -ENOTSUP;
+
+	spa_zero(*info);
+	info->media_type = SPA_MEDIA_TYPE_audio;
+	info->media_subtype = SPA_MEDIA_SUBTYPE_raw;
+	info->info.raw.format = SPA_AUDIO_FORMAT_S24_32;
+
+	switch (conf.rate) {
+	case LC3_CONFIG_FREQ_48KHZ:
+		info->info.raw.rate = 48000U;
+		break;
+	case LC3_CONFIG_FREQ_24KHZ:
+		info->info.raw.rate = 24000U;
+		break;
+	case LC3_CONFIG_FREQ_16KHZ:
+		info->info.raw.rate = 16000U;
+		break;
+	case LC3_CONFIG_FREQ_8KHZ:
+		info->info.raw.rate = 8000U;
+		break;
+	default:
+		return -EINVAL;
+	}
+
+	res = channels_to_positions(conf.channels, conf.n_blks, info->info.raw.position);
+	if (res == 0)
+		return -EINVAL;
+	info->info.raw.channels = res;
+
+	switch (conf.frame_duration) {
+	case LC3_CONFIG_DURATION_10:
+	case LC3_CONFIG_DURATION_7_5:
+		break;
+	default:
+		return -EINVAL;
+	}
+
+	return 0;
+}
+
+static int codec_get_qos(const struct media_codec *codec,
+		const void *config, size_t config_size,
+		const struct bap_endpoint_qos *endpoint_qos,
+		struct bap_codec_qos *qos)
+{
+	bap_lc3_t conf;
+
+	spa_zero(*qos);
+
+	if (!parse_conf(&conf, config, config_size))
+		return -EINVAL;
+
+	qos->framing = false;
+	if (endpoint_qos->phy & 0x2)
+		qos->phy = 0x2;
+	else if (endpoint_qos->phy & 0x1)
+		qos->phy = 0x1;
+	else
+		qos->phy = 0x2;
+	qos->retransmission = 2; /* default */
+	qos->sdu = conf.framelen * conf.n_blks;
+	qos->latency = 20; /* default */
+	qos->delay = 40000U;
+	qos->interval = (conf.frame_duration == LC3_CONFIG_DURATION_7_5 ? 7500 : 10000);
+	qos->target_latency = BT_ISO_QOS_TARGET_LATENCY_BALANCED;
+
+	switch (conf.rate) {
+		case LC3_CONFIG_FREQ_8KHZ:
+		case LC3_CONFIG_FREQ_16KHZ:
+		case LC3_CONFIG_FREQ_24KHZ:
+		case LC3_CONFIG_FREQ_32KHZ:
+			qos->retransmission = 2;
+			qos->latency = (conf.frame_duration == LC3_CONFIG_DURATION_7_5 ? 8 : 10);
+			break;
+		case LC3_CONFIG_FREQ_48KHZ:
+			qos->retransmission = 5;
+			qos->latency = (conf.frame_duration == LC3_CONFIG_DURATION_7_5 ? 15 : 20);
+			break;
+	}
+
+	/* Clamp to ASE values */
+	if (endpoint_qos->latency >= 0x0005 && endpoint_qos->latency <= 0x0FA0)
+		/* Values outside the range are RFU */
+		qos->latency = SPA_MAX(qos->latency, endpoint_qos->latency);
+
+	if (endpoint_qos->delay_min)
+		qos->delay = SPA_MAX(qos->delay, endpoint_qos->delay_min);
+	if (endpoint_qos->delay_max)
+		qos->delay = SPA_MIN(qos->delay, endpoint_qos->delay_max);
+
+	return 0;
+}
+
+static void *codec_init(const struct media_codec *codec, uint32_t flags,
+		void *config, size_t config_len, const struct spa_audio_info *info,
+		void *props, size_t mtu)
+{
+	bap_lc3_t conf;
+	struct impl *this = NULL;
+	struct spa_audio_info config_info;
+	int res, ich;
+
+	if (info->media_type != SPA_MEDIA_TYPE_audio ||
+	    info->media_subtype != SPA_MEDIA_SUBTYPE_raw ||
+	    info->info.raw.format != SPA_AUDIO_FORMAT_S24_32) {
+		res = -EINVAL;
+		goto error;
+	}
+
+	if ((this = calloc(1, sizeof(struct impl))) == NULL)
+		goto error_errno;
+
+	if ((res = codec_validate_config(codec, flags, config, config_len, &config_info)) < 0)
+		goto error;
+
+	if (!parse_conf(&conf, config, config_len)) {
+		res = -ENOTSUP;
+		goto error;
+	}
+
+	this->mtu = mtu;
+	this->samplerate = config_info.info.raw.rate;
+	this->channels = config_info.info.raw.channels;
+	this->framelen = conf.framelen;
+
+	switch (conf.frame_duration) {
+	case LC3_CONFIG_DURATION_10:
+		this->frame_dus = 10000;
+		break;
+	case LC3_CONFIG_DURATION_7_5:
+		this->frame_dus = 7500;
+		break;
+	default:
+		res = -EINVAL;
+		goto error;
+	}
+
+	this->samples = lc3_frame_samples(this->frame_dus, this->samplerate);
+	if (this->samples < 0) {
+		res = -EINVAL;
+		goto error;
+	}
+	this->codesize = this->samples * this->channels * sizeof(int32_t);
+
+	if (!(flags & MEDIA_CODEC_FLAG_SINK)) {
+		for (ich = 0; ich < this->channels; ich++) {
+			this->enc[ich] = lc3_setup_encoder(this->frame_dus, this->samplerate, 0, calloc(1, lc3_encoder_size(this->frame_dus, this->samplerate)));
+			if (this->enc[ich] == NULL) {
+				res = -EINVAL;
+				goto error;
+			}
+		}
+	} else {
+		for (ich = 0; ich < this->channels; ich++) {
+			this->dec[ich] = lc3_setup_decoder(this->frame_dus, this->samplerate, 0, calloc(1, lc3_decoder_size(this->frame_dus, this->samplerate)));
+			if (this->dec[ich] == NULL) {
+				res = -EINVAL;
+				goto error;
+			}
+		}
+	}
+
+	return this;
+
+error_errno:
+	res = -errno;
+	goto error;
+
+error:
+	if (this) {
+		for (ich = 0; ich < this->channels; ich++) {
+			if (this->enc[ich])
+				free(this->enc[ich]);
+			if (this->dec[ich])
+				free(this->dec[ich]);
+		}
+	}
+	free(this);
+	errno = -res;
+	return NULL;
+}
+
+static void codec_deinit(void *data)
+{
+	struct impl *this = data;
+	int ich;
+
+	for (ich = 0; ich < this->channels; ich++) {
+		if (this->enc[ich])
+			free(this->enc[ich]);
+		if (this->dec[ich])
+			free(this->dec[ich]);
+	}
+	free(this);
+}
+
+static int codec_get_block_size(void *data)
+{
+	struct impl *this = data;
+	return this->codesize;
+}
+
+static int codec_abr_process (void *data, size_t unsent)
+{
+	return -ENOTSUP;
+}
+
+static int codec_start_encode (void *data,
+		void *dst, size_t dst_size, uint16_t seqnum, uint32_t timestamp)
+{
+	return 0;
+}
+
+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;
+	int frame_bytes;
+	int ich, res;
+	int size, processed;
+
+	frame_bytes = lc3_frame_bytes(this->frame_dus, this->samplerate);
+	processed = 0;
+	size = 0;
+
+	if (src_size < (size_t)this->codesize)
+		goto done;
+	if (dst_size < (size_t)frame_bytes)
+		goto done;
+
+	for (ich = 0; ich < this->channels; ich++) {
+		uint8_t *in = (uint8_t *)src + (ich * 4);
+		uint8_t *out = (uint8_t *)dst + ich * this->framelen;
+		res = lc3_encode(this->enc[ich], LC3_PCM_FORMAT_S24, in, this->channels, this->framelen, out);
+		size += this->framelen;
+		if (SPA_UNLIKELY(res != 0))
+			return -EINVAL;
+	}
+	*dst_out = size;
+
+	processed += this->codesize;
+
+done:
+	spa_assert(size <= this->mtu);
+	*need_flush = NEED_FLUSH_ALL;
+
+	return processed;
+}
+
+static SPA_UNUSED int codec_start_decode (void *data,
+		const void *src, size_t src_size, uint16_t *seqnum, uint32_t *timestamp)
+{
+	return 0;
+}
+
+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 ich, res;
+	int consumed;
+	int samples;
+
+	spa_return_val_if_fail((size_t)(this->framelen * this->channels) == src_size, -EINVAL);
+	consumed = 0;
+
+	samples = lc3_frame_samples(this->frame_dus, this->samplerate);
+	if (samples == -1)
+		return -EINVAL;
+	if (dst_size < this->codesize)
+		return -EINVAL;
+
+	for (ich = 0; ich < this->channels; ich++) {
+		uint8_t *in = (uint8_t *)src + ich * this->framelen;
+		uint8_t *out = (uint8_t *)dst + (ich * 4);
+		res = lc3_decode(this->dec[ich], in, this->framelen, LC3_PCM_FORMAT_S24, out, this->channels);
+		if (SPA_UNLIKELY(res < 0))
+			return -EINVAL;
+		consumed += this->framelen;
+	}
+
+	*dst_out = this->codesize;
+
+	return consumed;
+}
+
+static int codec_reduce_bitpool(void *data)
+{
+	return -ENOTSUP;
+}
+
+static int codec_increase_bitpool(void *data)
+{
+	return -ENOTSUP;
+}
+
+const struct media_codec bap_codec_lc3 = {
+	.id = SPA_BLUETOOTH_AUDIO_CODEC_LC3,
+	.name = "lc3",
+	.codec_id = BAP_CODEC_LC3,
+	.bap = true,
+	.description = "LC3",
+	.fill_caps = codec_fill_caps,
+	.select_config = codec_select_config,
+	.enum_config = codec_enum_config,
+	.validate_config = codec_validate_config,
+	.get_qos = codec_get_qos,
+	.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,
+	.start_decode = codec_start_decode,
+	.decode = codec_decode,
+	.reduce_bitpool = codec_reduce_bitpool,
+	.increase_bitpool = codec_increase_bitpool
+};
+
+MEDIA_CODEC_EXPORT_DEF(
+	"lc3",
+	&bap_codec_lc3
+);
diff --git a/spa/plugins/bluez5/bluez5-dbus.c b/spa/plugins/bluez5/bluez5-dbus.c
index 93f8e72171a31e4cafb3886e156035f65ed463e9..ab7e7989a833edc9e083cfa2845210b8bf084c58 100644
--- a/spa/plugins/bluez5/bluez5-dbus.c
+++ b/spa/plugins/bluez5/bluez5-dbus.c
@@ -100,7 +100,7 @@ struct spa_bt_monitor {
 
 	uint32_t id;
 
-	const struct a2dp_codec * const * a2dp_codecs;
+	const struct media_codec * const * media_codecs;
 
 	/*
 	 * Lists of BlueZ objects, kept up-to-date by following DBus events
@@ -113,6 +113,7 @@ struct spa_bt_monitor {
 
 	unsigned int filters_added:1;
 	unsigned int objects_listed:1;
+	DBusPendingCall *get_managed_objects_call;
 
 	struct spa_bt_backend *backend;
 	struct spa_bt_backend *backends[BACKEND_NUM];
@@ -130,7 +131,9 @@ struct spa_bt_monitor {
 	struct spa_dict global_settings;
 
 	/* A reference audio info for A2DP codec configuration. */
-	struct a2dp_codec_audio_info default_audio_info;
+	struct media_codec_audio_info default_audio_info;
+
+	bool le_audio_supported;
 };
 
 /* Stream endpoints owned by BlueZ for each device */
@@ -146,6 +149,7 @@ struct spa_bt_remote_endpoint {
 	uint8_t *capabilities;
 	int capabilities_len;
 	bool delay_reporting;
+	bool acceptor;
 };
 
 /*
@@ -155,7 +159,7 @@ struct spa_bt_remote_endpoint {
  * with the desired capabilities.
  * The codec switch struct tracks candidates still to be tried.
  */
-struct spa_bt_a2dp_codec_switch {
+struct spa_bt_media_codec_switch {
 	struct spa_bt_device *device;
 	struct spa_list device_link;
 
@@ -172,10 +176,10 @@ struct spa_bt_a2dp_codec_switch {
 	 * Called asynchronously, so endpoint paths instead of pointers (which may be
 	 * invalidated in the meantime).
 	 */
-	const struct a2dp_codec **codecs;
+	const struct media_codec **codecs;
 	char **paths;
 
-	const struct a2dp_codec **codec_iter;	/**< outer iterator over codecs */
+	const struct media_codec **codec_iter;	/**< outer iterator over codecs */
 	char **path_iter;			/**< inner iterator over endpoint paths */
 
 	uint16_t retries;
@@ -430,10 +434,17 @@ static void register_battery_provider(struct spa_bt_device *device)
 	}
 }
 
-static int a2dp_codec_to_endpoint(const struct a2dp_codec *codec,
-				   const char * endpoint,
+static int media_codec_to_endpoint(const struct media_codec *codec,
+				   enum spa_bt_media_direction direction,
 				   char** object_path)
 {
+	const char * endpoint;
+
+	if (direction == SPA_BT_MEDIA_SOURCE)
+		endpoint = codec->bap ? BAP_SOURCE_ENDPOINT : A2DP_SOURCE_ENDPOINT;
+	else
+		endpoint = codec->bap ? BAP_SINK_ENDPOINT : A2DP_SINK_ENDPOINT;
+
 	*object_path = spa_aprintf("%s/%s", endpoint,
 		codec->endpoint_name ? codec->endpoint_name : codec->name);
 	if (*object_path == NULL)
@@ -441,10 +452,11 @@ 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, bool *sink)
+static const struct media_codec *media_endpoint_to_codec(struct spa_bt_monitor *monitor, const char *endpoint, bool *sink, const struct media_codec *preferred)
 {
 	const char *ep_name;
-	const struct a2dp_codec * const * const a2dp_codecs = monitor->a2dp_codecs;
+	const struct media_codec * const * const media_codecs = monitor->media_codecs;
+	const struct media_codec *found = NULL;
 	int i;
 
 	if (spa_strstartswith(endpoint, A2DP_SINK_ENDPOINT "/")) {
@@ -453,36 +465,81 @@ static const struct a2dp_codec *a2dp_endpoint_to_codec(struct spa_bt_monitor *mo
 	} else if (spa_strstartswith(endpoint, A2DP_SOURCE_ENDPOINT "/")) {
 		ep_name = endpoint + strlen(A2DP_SOURCE_ENDPOINT "/");
 		*sink = false;
+	} else if (spa_strstartswith(endpoint, BAP_SOURCE_ENDPOINT "/")) {
+		ep_name = endpoint + strlen(BAP_SOURCE_ENDPOINT "/");
+		*sink = false;
+	} else if (spa_strstartswith(endpoint, BAP_SINK_ENDPOINT "/")) {
+		ep_name = endpoint + strlen(BAP_SINK_ENDPOINT "/");
+		*sink = true;
 	} else {
+		*sink = true;
 		return NULL;
 	}
 
-	for (i = 0; a2dp_codecs[i]; i++) {
-		const struct a2dp_codec *codec = a2dp_codecs[i];
+	for (i = 0; media_codecs[i]; i++) {
+		const struct media_codec *codec = media_codecs[i];
 		const char *codec_ep_name =
 			codec->endpoint_name ? codec->endpoint_name : codec->name;
-		if (spa_streq(ep_name, codec_ep_name))
-			return codec;
+
+		if (!spa_streq(ep_name, codec_ep_name))
+			continue;
+		if ((*sink && !codec->decode) || (!*sink && !codec->encode))
+			continue;
+
+		/* Same endpoint may be shared with multiple codec objects,
+		 * which may e.g. correspond to different encoder settings.
+		 * Look up which one we selected.
+		 */
+		if ((preferred && codec == preferred) || found == NULL)
+			found = codec;
 	}
-	return NULL;
+	return found;
 }
 
-static int a2dp_endpoint_to_profile(const char *endpoint)
+static int media_endpoint_to_profile(const char *endpoint)
 {
 
 	if (spa_strstartswith(endpoint, A2DP_SINK_ENDPOINT "/"))
 		return SPA_BT_PROFILE_A2DP_SOURCE;
 	else if (spa_strstartswith(endpoint, A2DP_SOURCE_ENDPOINT "/"))
 		return SPA_BT_PROFILE_A2DP_SINK;
+	else if (spa_strstartswith(endpoint, BAP_SINK_ENDPOINT "/"))
+		return SPA_BT_PROFILE_BAP_SOURCE;
+	else if (spa_strstartswith(endpoint, BAP_SOURCE_ENDPOINT "/"))
+		return SPA_BT_PROFILE_BAP_SINK;
 	else
 		return SPA_BT_PROFILE_NULL;
 }
 
-static bool is_a2dp_codec_enabled(struct spa_bt_monitor *monitor, const struct a2dp_codec *codec)
+static bool is_media_codec_enabled(struct spa_bt_monitor *monitor, const struct media_codec *codec)
 {
 	return spa_dict_lookup(&monitor->enabled_codecs, codec->name) != NULL;
 }
 
+static bool codec_has_direction(const struct media_codec *codec, enum spa_bt_media_direction direction)
+{
+	switch (direction) {
+	case SPA_BT_MEDIA_SOURCE:
+		return codec->encode;
+	case SPA_BT_MEDIA_SINK:
+		return codec->decode;
+	default:
+		spa_assert_not_reached();
+	}
+}
+
+static bool endpoint_should_be_registered(struct spa_bt_monitor *monitor,
+					  const struct media_codec *codec,
+					  enum spa_bt_media_direction direction)
+{
+	/* Codecs with fill_caps == NULL share endpoint with another codec,
+	 * and don't have their own endpoint
+	 */
+	return is_media_codec_enabled(monitor, codec) &&
+		codec_has_direction(codec, direction) &&
+		codec->fill_caps;
+}
+
 static DBusHandlerResult endpoint_select_configuration(DBusConnection *conn, DBusMessage *m, void *userdata)
 {
 	struct spa_bt_monitor *monitor = userdata;
@@ -492,7 +549,7 @@ static DBusHandlerResult endpoint_select_configuration(DBusConnection *conn, DBu
 	DBusMessage *r;
 	DBusError err;
 	int size, res;
-	const struct a2dp_codec *codec;
+	const struct media_codec *codec;
 	bool sink;
 
 	dbus_error_init(&err);
@@ -508,14 +565,21 @@ static DBusHandlerResult endpoint_select_configuration(DBusConnection *conn, DBu
 	spa_log_info(monitor->log, "%p: %s select conf %d", monitor, path, size);
 	spa_log_hexdump(monitor->log, SPA_LOG_LEVEL_DEBUG, 2, cap, (size_t)size);
 
-	codec = a2dp_endpoint_to_codec(monitor, path, &sink);
+	/* For codecs sharing the same endpoint, BlueZ-initiated connections
+	 * always pick the default one. The session manager will
+	 * switch the codec to a saved value after connection, so this generally
+	 * does not matter.
+	 */
+	codec = media_endpoint_to_codec(monitor, path, &sink, NULL);
+	spa_log_debug(monitor->log, "%p: %s codec:%s", monitor, path, codec ? codec->name : "<null>");
+
 	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, sink ? A2DP_CODEC_FLAG_SINK : 0, cap, size, &monitor->default_audio_info,
+		res = codec->select_config(codec, sink ? MEDIA_CODEC_FLAG_SINK : 0, cap, size, &monitor->default_audio_info,
 				&monitor->global_settings, config);
 	else
 		res = -ENOTSUP;
@@ -536,7 +600,7 @@ static DBusHandlerResult endpoint_select_configuration(DBusConnection *conn, DBu
 			DBUS_TYPE_BYTE, &pconf, size, DBUS_TYPE_INVALID))
 		return DBUS_HANDLER_RESULT_NEED_MEMORY;
 
-      exit_send:
+exit_send:
 	if (!dbus_connection_send(conn, r, NULL))
 		return DBUS_HANDLER_RESULT_NEED_MEMORY;
 
@@ -545,6 +609,242 @@ static DBusHandlerResult endpoint_select_configuration(DBusConnection *conn, DBu
 	return DBUS_HANDLER_RESULT_HANDLED;
 }
 
+static void append_basic_variant_dict_entry(DBusMessageIter *dict, const char* key, int variant_type_int, const char* variant_type_str, void* variant);
+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 struct spa_bt_remote_endpoint *remote_endpoint_find(struct spa_bt_monitor *monitor, const char *path);
+
+static DBusHandlerResult endpoint_select_properties(DBusConnection *conn, DBusMessage *m, void *userdata)
+{
+	struct spa_bt_monitor *monitor = userdata;
+	const char *path;
+	DBusMessageIter args, props, iter;
+	DBusMessage *r = NULL;
+	int res;
+	const struct media_codec *codec;
+	bool sink;
+	const char *err_msg = "Unknown error";
+
+	const char *endpoint_path = NULL;
+	uint8_t caps[A2DP_MAX_CAPS_SIZE];
+	uint8_t config[A2DP_MAX_CAPS_SIZE];
+	int caps_size = 0;
+	DBusMessageIter dict;
+	struct bap_endpoint_qos endpoint_qos;
+
+	spa_zero(endpoint_qos);
+
+	if (!dbus_message_iter_init(m, &args) || !spa_streq(dbus_message_get_signature(m), "a{sv}")) {
+		spa_log_error(monitor->log, "Invalid signature for method SelectProperties()");
+		return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+	}
+
+	dbus_message_iter_recurse(&args, &props);
+	if (dbus_message_iter_get_arg_type(&props) != DBUS_TYPE_DICT_ENTRY)
+		return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+
+	path = dbus_message_get_path(m);
+
+	/* TODO: for codecs with shared endpoint, this currently always picks the default
+	 * one. However, currently we don't have BAP codecs with shared endpoint, so
+	 * this does not matter, but in case they are needed later we should pick the
+	 * right one here.
+	 */
+	codec = media_endpoint_to_codec(monitor, path, &sink, NULL);
+	spa_log_debug(monitor->log, "%p: %s codec:%s", monitor, path, codec ? codec->name : "<null>");
+	if (!codec) {
+		spa_log_error(monitor->log, "Unsupported codec");
+		err_msg = "Unsupported codec";
+		goto error;
+	}
+
+	/* Parse transport properties */
+	while (dbus_message_iter_get_arg_type(&props) == DBUS_TYPE_DICT_ENTRY) {
+		const char *key;
+		DBusMessageIter value, entry;
+		int type;
+
+		dbus_message_iter_recurse(&props, &entry);
+		dbus_message_iter_get_basic(&entry, &key);
+
+		dbus_message_iter_next(&entry);
+		dbus_message_iter_recurse(&entry, &value);
+
+		type = dbus_message_iter_get_arg_type(&value);
+
+		if (spa_streq(key, "Capabilities")) {
+			DBusMessageIter array;
+			uint8_t *buf;
+
+			if (type != DBUS_TYPE_ARRAY) {
+				spa_log_error(monitor->log, "Property %s of wrong type %c", key, (char)type);
+				goto error_invalid;
+			}
+
+			dbus_message_iter_recurse(&value, &array);
+			type = dbus_message_iter_get_arg_type(&array);
+			if (type != DBUS_TYPE_BYTE) {
+				spa_log_error(monitor->log, "%s is an array of wrong type %c", key, (char)type);
+				goto error_invalid;
+			}
+
+			dbus_message_iter_get_fixed_array(&array, &buf, &caps_size);
+			memcpy(caps, buf, caps_size);
+
+			spa_log_info(monitor->log, "%p: %s %s size:%d", monitor, path, key, caps_size);
+			spa_log_hexdump(monitor->log, SPA_LOG_LEVEL_DEBUG, ' ', caps, (size_t)caps_size);
+		} else if (spa_streq(key, "Endpoint")) {
+			if (type != DBUS_TYPE_OBJECT_PATH) {
+				spa_log_error(monitor->log, "Property %s of wrong type %c", key, (char)type);
+				goto error_invalid;
+			}
+
+			dbus_message_iter_get_basic(&value, &endpoint_path);
+
+			spa_log_info(monitor->log, "%p: %s %s %s", monitor, path, key, endpoint_path);
+		} else if (type == DBUS_TYPE_BYTE) {
+			uint8_t v;
+			dbus_message_iter_get_basic(&value, &v);
+
+			spa_log_info(monitor->log, "%p: %s %s 0x%x", monitor, path, key, (unsigned int)v);
+
+			if (spa_streq(key, "Framing"))
+				endpoint_qos.framing = v;
+			else if (spa_streq(key, "PHY"))
+				endpoint_qos.phy = v;
+			else
+				spa_log_info(monitor->log, "Unknown property %s", key);
+		} else if (type == DBUS_TYPE_UINT16) {
+			dbus_uint16_t v;
+			dbus_message_iter_get_basic(&value, &v);
+
+			spa_log_info(monitor->log, "%p: %s %s 0x%x", monitor, path, key, (unsigned int)v);
+
+			if (spa_streq(key, "Latency"))
+				endpoint_qos.latency = v;
+			else
+				spa_log_info(monitor->log, "Unknown property %s", key);
+		} else if (type == DBUS_TYPE_UINT32) {
+			dbus_uint32_t v;
+			dbus_message_iter_get_basic(&value, &v);
+
+			spa_log_info(monitor->log, "%p: %s %s 0x%x", monitor, path, key, (unsigned int)v);
+
+			if (spa_streq(key, "MinimumDelay"))
+				endpoint_qos.delay_min = v;
+			else if (spa_streq(key, "MaximumDelay"))
+				endpoint_qos.delay_max = v;
+			else if (spa_streq(key, "PreferredMinimumDelay"))
+				endpoint_qos.preferred_delay_min = v;
+			else if (spa_streq(key, "PreferredMaximumDelay"))
+				endpoint_qos.preferred_delay_max = v;
+			else
+				spa_log_info(monitor->log, "Unknown property %s", key);
+		} else {
+			spa_log_info(monitor->log, "Unknown property %s", key);
+		}
+
+		dbus_message_iter_next(&props);
+	}
+
+	if (codec->bap) {
+		struct spa_bt_remote_endpoint *ep;
+
+		ep = remote_endpoint_find(monitor, endpoint_path);
+		if (!ep) {
+			spa_log_warn(monitor->log, "Unable to find remote endpoint for %s", endpoint_path);
+			goto error_invalid;
+		}
+
+		/* Call of SelectProperties means that local device acts as an initiator
+		 * and therefor remote endpoint is an acceptor
+		 */
+		ep->acceptor = true;
+	}
+
+	/* TODO: determine which device the SelectConfiguration() call is associated
+	 * with; it's known here based on the remote endpoint.
+	 */
+	res = codec->select_config(codec, 0, caps, caps_size, &monitor->default_audio_info, NULL, config);
+
+	if (res < 0 || res != caps_size) {
+		spa_log_error(monitor->log, "can't select config: %d (%s)",
+				res, spa_strerror(res));
+		goto error_invalid;
+	}
+	spa_log_info(monitor->log, "%p: selected conf %d", monitor, caps_size);
+	spa_log_hexdump(monitor->log, SPA_LOG_LEVEL_DEBUG, ' ', (uint8_t *)config, (size_t)caps_size);
+
+	if ((r = dbus_message_new_method_return(m)) == NULL)
+		return DBUS_HANDLER_RESULT_NEED_MEMORY;
+	dbus_message_iter_init_append(r, &iter);
+
+	dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY,
+			DBUS_DICT_ENTRY_BEGIN_CHAR_AS_STRING
+			DBUS_TYPE_STRING_AS_STRING
+			DBUS_TYPE_VARIANT_AS_STRING
+			DBUS_DICT_ENTRY_END_CHAR_AS_STRING,
+			&dict);
+	append_basic_array_variant_dict_entry(&dict, "Capabilities", "ay", "y", DBUS_TYPE_BYTE, &config, caps_size);
+
+	if (codec->get_qos) {
+		struct bap_codec_qos qos;
+		dbus_bool_t framing;
+		const char *phy_str;
+
+		spa_zero(qos);
+
+		res = codec->get_qos(codec, config, caps_size, &endpoint_qos, &qos);
+		if (res < 0) {
+			spa_log_error(monitor->log, "can't select QOS config: %d (%s)",
+					res, spa_strerror(res));
+			goto error_invalid;
+		}
+
+		append_basic_variant_dict_entry(&dict, "Interval", DBUS_TYPE_UINT32, "u", &qos.interval);
+		framing = (qos.framing ? TRUE : FALSE);
+		append_basic_variant_dict_entry(&dict, "Framing", DBUS_TYPE_BOOLEAN, "b", &framing);
+		if (qos.phy == 0x1)
+			phy_str = "1M";
+		else if (qos.phy == 0x2)
+			phy_str = "2M";
+		else
+			spa_assert_not_reached();
+		append_basic_variant_dict_entry(&dict, "PHY", DBUS_TYPE_STRING, "s", &phy_str);
+		append_basic_variant_dict_entry(&dict, "SDU", DBUS_TYPE_UINT16, "q", &qos.sdu);
+		append_basic_variant_dict_entry(&dict, "Retransmissions", DBUS_TYPE_BYTE, "y", &qos.retransmission);
+		append_basic_variant_dict_entry(&dict, "Latency", DBUS_TYPE_UINT16, "q", &qos.latency);
+		append_basic_variant_dict_entry(&dict, "Delay", DBUS_TYPE_UINT32, "u", &qos.delay);
+		append_basic_variant_dict_entry(&dict, "TargetLatency", DBUS_TYPE_BYTE, "y", &qos.target_latency);
+	}
+
+	dbus_message_iter_close_container(&iter, &dict);
+
+	if (r) {
+		if (!dbus_connection_send(conn, r, NULL))
+			return DBUS_HANDLER_RESULT_NEED_MEMORY;
+
+		dbus_message_unref(r);
+	}
+
+	return DBUS_HANDLER_RESULT_HANDLED;
+
+error_invalid:
+	err_msg = "Invalid property";
+	goto error;
+
+error:
+	if (r)
+		dbus_message_unref(r);
+	if ((r = dbus_message_new_error(m, "org.bluez.Error.InvalidArguments", err_msg)) == NULL)
+		return DBUS_HANDLER_RESULT_NEED_MEMORY;
+	if (!dbus_connection_send(conn, r, NULL)) {
+		dbus_message_unref(r);
+		return DBUS_HANDLER_RESULT_NEED_MEMORY;
+	}
+	dbus_message_unref(r);
+	return DBUS_HANDLER_RESULT_HANDLED;
+}
+
 static struct spa_bt_adapter *adapter_find(struct spa_bt_monitor *monitor, const char *path)
 {
 	struct spa_bt_adapter *d;
@@ -570,9 +870,9 @@ static int parse_modalias(const char *modalias, uint16_t *source, uint16_t *vend
 	char *pos;
 	unsigned int src, i, j, k;
 
-	if (strncmp(modalias, "bluetooth:", strlen("bluetooth:")) == 0)
+	if (spa_strstartswith(modalias, "bluetooth:"))
 		src = SOURCE_ID_BLUETOOTH;
-	else if (strncmp(modalias, "usb:", strlen("usb:")) == 0)
+	else if (spa_strstartswith(modalias, "usb:"))
 		src = SOURCE_ID_USB;
 	else
 		return -EINVAL;
@@ -683,6 +983,12 @@ static int adapter_update_props(struct spa_bt_adapter *adapter,
 				if (profile && (adapter->profiles & profile) == 0) {
 					spa_log_debug(monitor->log, "adapter %p: add UUID=%s", adapter, uuid);
 					adapter->profiles |= profile;
+				} else if (strcasecmp(uuid, SPA_BT_UUID_PACS) == 0 &&
+				           (adapter->profiles & SPA_BT_PROFILE_BAP_SINK) == 0) {
+					spa_log_debug(monitor->log, "adapter %p: add UUID=%s", adapter, SPA_BT_UUID_BAP_SINK);
+					adapter->profiles |= SPA_BT_PROFILE_BAP_SINK;
+					spa_log_debug(monitor->log, "adapter %p: add UUID=%s", adapter, SPA_BT_UUID_BAP_SOURCE);
+					adapter->profiles |= SPA_BT_PROFILE_BAP_SOURCE;
 				}
 				dbus_message_iter_next(&iter);
 			}
@@ -690,7 +996,7 @@ static int adapter_update_props(struct spa_bt_adapter *adapter,
 		else
 			spa_log_debug(monitor->log, "adapter %p: unhandled key %s", adapter, key);
 
-	      next:
+next:
 		dbus_message_iter_next(props_iter);
 	}
 	return 0;
@@ -757,7 +1063,7 @@ static int adapter_init_modalias(struct spa_bt_monitor *monitor, struct spa_bt_a
 	if (str == NULL)
 		goto fail;
 	snprintf(path, sizeof(path), "/sys/class/bluetooth/%s/device/modalias", str);
-	if ((f = fopen(path, "rb")) == NULL) {
+	if ((f = fopen(path, "rbe")) == NULL) {
 		res = -errno;
 		goto fail;
 	}
@@ -837,6 +1143,11 @@ static uint32_t adapter_connectable_profiles(struct spa_bt_adapter *adapter)
 	if (profiles & SPA_BT_PROFILE_A2DP_SOURCE)
 		mask |= SPA_BT_PROFILE_A2DP_SINK;
 
+	if (profiles & SPA_BT_PROFILE_BAP_SINK)
+		mask |= SPA_BT_PROFILE_BAP_SOURCE;
+	if (profiles & SPA_BT_PROFILE_BAP_SOURCE)
+		mask |= SPA_BT_PROFILE_BAP_SINK;
+
 	if (profiles & SPA_BT_PROFILE_HSP_AG)
 		mask |= SPA_BT_PROFILE_HSP_HS;
 	if (profiles & SPA_BT_PROFILE_HSP_HS)
@@ -905,7 +1216,7 @@ static struct spa_bt_device *device_create(struct spa_bt_monitor *monitor, const
 
 static int device_stop_timer(struct spa_bt_device *device);
 
-static void a2dp_codec_switch_free(struct spa_bt_a2dp_codec_switch *sw);
+static void media_codec_switch_free(struct spa_bt_media_codec_switch *sw);
 
 static void device_clear_sub(struct spa_bt_device *device)
 {
@@ -916,7 +1227,7 @@ static void device_clear_sub(struct spa_bt_device *device)
 static void device_free(struct spa_bt_device *device)
 {
 	struct spa_bt_remote_endpoint *ep, *tep;
-	struct spa_bt_a2dp_codec_switch *sw;
+	struct spa_bt_media_codec_switch *sw;
 	struct spa_bt_transport *t, *tt;
 	struct spa_bt_monitor *monitor = device->monitor;
 
@@ -946,7 +1257,7 @@ static void device_free(struct spa_bt_device *device)
 	}
 
 	spa_list_consume(sw, &device->codec_switch_list, device_link)
-		a2dp_codec_switch_free(sw);
+		media_codec_switch_free(sw);
 
 	spa_list_remove(&device->link);
 	free(device->path);
@@ -1130,7 +1441,7 @@ int spa_bt_device_add_profile(struct spa_bt_device *device, enum spa_bt_profile
 
 
 static int device_try_connect_profile(struct spa_bt_device *device,
-                                      const char *profile_uuid)
+				      const char *profile_uuid)
 {
 	struct spa_bt_monitor *monitor = device->monitor;
 	DBusMessage *m;
@@ -1141,9 +1452,9 @@ static int device_try_connect_profile(struct spa_bt_device *device,
 	/* Call org.bluez.Device1.ConnectProfile() on device, ignoring result */
 
 	m = dbus_message_new_method_call(BLUEZ_SERVICE,
-	                                 device->path,
-	                                 BLUEZ_DEVICE_INTERFACE,
-	                                 "ConnectProfile");
+					 device->path,
+					 BLUEZ_DEVICE_INTERFACE,
+					 "ConnectProfile");
 	if (m == NULL)
 		return -ENOMEM;
 	dbus_message_append_args(m, DBUS_TYPE_STRING, &profile_uuid, DBUS_TYPE_INVALID);
@@ -1208,6 +1519,10 @@ static int reconnect_device_profiles(struct spa_bt_device *device)
 		device_try_connect_profile(device, SPA_BT_UUID_A2DP_SINK);
 	if (reconnect & SPA_BT_PROFILE_A2DP_SOURCE)
 		device_try_connect_profile(device, SPA_BT_UUID_A2DP_SOURCE);
+	if (reconnect & SPA_BT_PROFILE_BAP_SINK)
+		device_try_connect_profile(device, SPA_BT_UUID_BAP_SINK);
+	if (reconnect & SPA_BT_PROFILE_BAP_SOURCE)
+		device_try_connect_profile(device, SPA_BT_UUID_BAP_SOURCE);
 
 	return reconnect;
 }
@@ -1222,7 +1537,7 @@ static void device_timer_event(struct spa_source *source)
 	uint64_t exp;
 
 	if (spa_system_timerfd_read(monitor->main_system, source->fd, &exp) < 0)
-                spa_log_warn(monitor->log, "error reading timerfd: %s", strerror(errno));
+		spa_log_warn(monitor->log, "error reading timerfd: %s", strerror(errno));
 
 	spa_log_debug(monitor->log, "device %p: timeout %08x %08x",
 			device, device->profiles, device->connected_profiles);
@@ -1278,11 +1593,11 @@ static int device_stop_timer(struct spa_bt_device *device)
 
 	spa_log_debug(monitor->log, "device %p: stop timer", device);
 	spa_loop_remove_source(monitor->main_loop, &device->timer);
-        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(monitor->main_system, device->timer.fd, 0, &ts, NULL);
+	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(monitor->main_system, device->timer.fd, 0, &ts, NULL);
 	spa_system_close(monitor->main_system, device->timer.fd);
 	device->timer.data = NULL;
 	return 0;
@@ -1295,8 +1610,8 @@ int spa_bt_device_check_profiles(struct spa_bt_device *device, bool force)
 	uint32_t connectable_profiles =
 		device->adapter ? adapter_connectable_profiles(device->adapter) : 0;
 	uint32_t direction_masks[3] = {
-		SPA_BT_PROFILE_A2DP_SINK | SPA_BT_PROFILE_HEADSET_HEAD_UNIT,
-		SPA_BT_PROFILE_A2DP_SOURCE,
+		SPA_BT_PROFILE_MEDIA_SINK | SPA_BT_PROFILE_HEADSET_HEAD_UNIT,
+		SPA_BT_PROFILE_MEDIA_SOURCE,
 		SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY,
 	};
 	bool direction_connected = false;
@@ -1347,9 +1662,9 @@ static void device_set_connected(struct spa_bt_device *device, int connected)
 		spa_bt_device_check_profiles(device, false);
 	else {
 		/* Stop codec switch on disconnect */
-		struct spa_bt_a2dp_codec_switch *sw;
+		struct spa_bt_media_codec_switch *sw;
 		spa_list_consume(sw, &device->codec_switch_list, device_link)
-			a2dp_codec_switch_free(sw);
+			media_codec_switch_free(sw);
 
 		if (device->reconnect_state != BT_DEVICE_RECONNECT_INIT)
 			device_stop_timer(device);
@@ -1514,12 +1829,12 @@ static int device_update_props(struct spa_bt_device *device,
 
 				profile = spa_bt_profile_from_uuid(uuid);
 
-				/* Only add A2DP profiles if HSP/HFP backed is none.
+				/* Only add A2DP/BAP profiles if HSP/HFP backed is none.
 				 * This allows BT device to connect instantly instead of waiting for
 				 * profile timeout, because all available profiles are connected.
 				 */
 				if (monitor->backend_selection != BACKEND_NONE || (monitor->backend_selection == BACKEND_NONE &&
-						profile & (SPA_BT_PROFILE_A2DP_SINK | SPA_BT_PROFILE_A2DP_SOURCE))) {
+						profile & (SPA_BT_PROFILE_MEDIA_SINK | SPA_BT_PROFILE_MEDIA_SOURCE))) {
 					if (profile && (device->profiles & profile) == 0) {
 						spa_log_debug(monitor->log, "device %p: add UUID=%s", device, uuid);
 						device->profiles |= profile;
@@ -1535,7 +1850,7 @@ static int device_update_props(struct spa_bt_device *device,
 		else
 			spa_log_debug(monitor->log, "device %p: unhandled key %s type %d", device, key, type);
 
-	      next:
+next:
 		dbus_message_iter_next(props_iter);
 	}
 	return 0;
@@ -1550,7 +1865,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 sink)
+bool spa_bt_device_supports_media_codec(struct spa_bt_device *device, const struct media_codec *codec, bool sink)
 {
 	struct spa_bt_monitor *monitor = device->monitor;
 	struct spa_bt_remote_endpoint *ep;
@@ -1563,7 +1878,7 @@ bool spa_bt_device_supports_a2dp_codec(struct spa_bt_device *device, const struc
 	};
 	size_t i;
 
-	if (!is_a2dp_codec_enabled(device->monitor, codec))
+	if (!is_media_codec_enabled(device->monitor, codec))
 		return false;
 
 	if (!device->adapter->application_registered) {
@@ -1587,13 +1902,17 @@ 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;
+		enum spa_bt_profile expected;
+
+		if (codec->bap)
+			expected = sink ? SPA_BT_PROFILE_BAP_SINK : SPA_BT_PROFILE_BAP_SOURCE;
+		else
+			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,
+		if (media_codec_check_caps(codec, ep->codec, ep->capabilities, ep->capabilities_len,
 						&ep->monitor->default_audio_info, &monitor->global_settings))
 			return true;
 	}
@@ -1601,34 +1920,34 @@ bool spa_bt_device_supports_a2dp_codec(struct spa_bt_device *device, const struc
 	return false;
 }
 
-const struct a2dp_codec **spa_bt_device_get_supported_a2dp_codecs(struct spa_bt_device *device, size_t *count, bool sink)
+const struct media_codec **spa_bt_device_get_supported_media_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;
-	const struct a2dp_codec **supported_codecs;
+	const struct media_codec * const * const media_codecs = monitor->media_codecs;
+	const struct media_codec **supported_codecs;
 	size_t i, j, size;
 
 	*count = 0;
 
 	size = 8;
-	supported_codecs = malloc(size * sizeof(const struct a2dp_codec *));
+	supported_codecs = malloc(size * sizeof(const struct media_codec *));
 	if (supported_codecs == NULL)
 		return NULL;
 
 	j = 0;
-	for (i = 0; a2dp_codecs[i] != NULL; ++i) {
-		if (spa_bt_device_supports_a2dp_codec(device, a2dp_codecs[i], sink)) {
-			supported_codecs[j] = a2dp_codecs[i];
+	for (i = 0; media_codecs[i] != NULL; ++i) {
+		if (spa_bt_device_supports_media_codec(device, media_codecs[i], sink)) {
+			supported_codecs[j] = media_codecs[i];
 			++j;
 		}
 
 		if (j >= size) {
-			const struct a2dp_codec **p;
+			const struct media_codec **p;
 			size = size * 2;
 #ifdef HAVE_REALLOCARRRAY
-			p = reallocarray(supported_codecs, size, sizeof(const struct a2dp_codec *));
+			p = reallocarray(supported_codecs, size, sizeof(const struct media_codec *));
 #else
-			p = realloc(supported_codecs, size * sizeof(const struct a2dp_codec *));
+			p = realloc(supported_codecs, size * sizeof(const struct media_codec *));
 #endif
 			if (p == NULL) {
 				free(supported_codecs);
@@ -1755,7 +2074,7 @@ static int remote_endpoint_update_props(struct spa_bt_remote_endpoint *remote_en
 		else
 			spa_log_debug(monitor->log, "remote_endpoint %p: unhandled key %s", remote_endpoint, key);
 
-	      next:
+next:
 		dbus_message_iter_next(props_iter);
 	}
 	return 0;
@@ -1804,8 +2123,8 @@ struct spa_bt_transport *spa_bt_transport_find(struct spa_bt_monitor *monitor, c
 }
 
 struct spa_bt_transport *spa_bt_transport_find_full(struct spa_bt_monitor *monitor,
-                                                    bool (*callback) (struct spa_bt_transport *t, const void *data),
-                                                    const void *data)
+						    bool (*callback) (struct spa_bt_transport *t, const void *data),
+						    const void *data)
 {
 	struct spa_bt_transport *t;
 
@@ -1832,6 +2151,7 @@ struct spa_bt_transport *spa_bt_transport_create(struct spa_bt_monitor *monitor,
 	t->delay = SPA_BT_UNKNOWN_DELAY;
 	t->user_data = SPA_PTROFF(t, sizeof(struct spa_bt_transport), void);
 	spa_hook_list_init(&t->listener_list);
+	spa_list_init(&t->bap_transport_linked);
 
 	spa_list_append(&monitor->transport_list, &t->link);
 
@@ -1911,6 +2231,8 @@ void spa_bt_transport_free(struct spa_bt_transport *transport)
 	if (device && device->connected_profiles != prev_connected)
 		spa_bt_device_emit_profiles_changed(device, device->profiles, prev_connected);
 
+	spa_list_remove(&transport->bap_transport_linked);
+
 	free(transport->endpoint_path);
 	free(transport->path);
 	free(transport);
@@ -2131,7 +2453,7 @@ static void spa_bt_transport_volume_timer_event(struct spa_source *source)
 	uint64_t exp;
 
 	if (spa_system_timerfd_read(monitor->main_system, source->fd, &exp) < 0)
-                spa_log_warn(monitor->log, "error reading timerfd: %s", strerror(errno));
+		spa_log_warn(monitor->log, "error reading timerfd: %s", strerror(errno));
 
 	spa_bt_transport_volume_changed(transport);
 }
@@ -2170,10 +2492,10 @@ int64_t spa_bt_transport_get_delay_nsec(struct spa_bt_transport *t)
 
 	/* Fallback values when device does not provide information */
 
-	if (t->a2dp_codec == NULL)
+	if (t->media_codec == NULL)
 		return 30 * SPA_NSEC_PER_MSEC;
 
-	switch (t->a2dp_codec->id) {
+	switch (t->media_codec->id) {
 	case SPA_BLUETOOTH_AUDIO_CODEC_SBC:
 	case SPA_BLUETOOTH_AUDIO_CODEC_SBC_XQ:
 		return 200 * SPA_NSEC_PER_MSEC;
@@ -2189,6 +2511,7 @@ int64_t spa_bt_transport_get_delay_nsec(struct spa_bt_transport *t)
 	case SPA_BLUETOOTH_AUDIO_CODEC_APTX_LL_DUPLEX:
 	case SPA_BLUETOOTH_AUDIO_CODEC_FASTSTREAM:
 	case SPA_BLUETOOTH_AUDIO_CODEC_FASTSTREAM_DUPLEX:
+	case SPA_BLUETOOTH_AUDIO_CODEC_LC3:
 		return 40 * SPA_NSEC_PER_MSEC;
 	default:
 		break;
@@ -2229,6 +2552,12 @@ static int transport_update_props(struct spa_bt_transport *transport,
 				case SPA_BT_PROFILE_A2DP_SINK:
 					transport->profile = SPA_BT_PROFILE_A2DP_SOURCE;
 					break;
+				case SPA_BT_PROFILE_BAP_SOURCE:
+					transport->profile = SPA_BT_PROFILE_BAP_SINK;
+					break;
+				case SPA_BT_PROFILE_BAP_SINK:
+					transport->profile = SPA_BT_PROFILE_BAP_SOURCE;
+					break;
 				default:
 					spa_log_warn(monitor->log, "unknown profile %s", value);
 					break;
@@ -2249,6 +2578,16 @@ static int transport_update_props(struct spa_bt_transport *transport,
 						spa_log_warn(monitor->log, "could not find device %s", value);
 				}
 			}
+			else if (spa_streq(key, "Endpoint")) {
+				struct spa_bt_remote_endpoint *ep = remote_endpoint_find(monitor, value);
+				if (!ep) {
+					spa_log_warn(monitor->log, "Unable to find remote endpoint for %s", value);
+					goto next;
+				}
+
+				// If the remote endpoint is an acceptor this transport is an initiator
+				transport->bap_initiator = ep->acceptor;
+			}
 		}
 		else if (spa_streq(key, "Codec")) {
 			uint8_t value;
@@ -2321,7 +2660,48 @@ static int transport_update_props(struct spa_bt_transport *transport,
 			transport->delay = value;
 			spa_bt_transport_emit_delay_changed(transport);
 		}
-	      next:
+		else if (spa_streq(key, "PresentationDelay")) {
+			uint32_t value;
+
+			if (type != DBUS_TYPE_UINT32)
+				goto next;
+			dbus_message_iter_get_basic(&it[1], &value);
+
+			spa_log_debug(monitor->log, "transport %p: %s=%02x", transport, key, value);
+
+			transport->delay = value / 100;
+			spa_bt_transport_emit_delay_changed(transport);
+		}
+		else if (spa_streq(key, "Links")) {
+			DBusMessageIter iter;
+
+			if (!check_iter_signature(&it[1], "ao"))
+				goto next;
+
+			dbus_message_iter_recurse(&it[1], &iter);
+			while (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_INVALID) {
+				const char *transport_path;
+				struct spa_bt_transport *t;
+
+				dbus_message_iter_get_basic(&iter, &transport_path);
+
+				spa_log_debug(monitor->log, "transport %p: Linked with=%s", transport, transport_path);
+				t = spa_bt_transport_find(monitor, transport_path);
+				if (!t) {
+					spa_log_warn(monitor->log, "Unable to find linked transport");
+					dbus_message_iter_next(&iter);
+					continue;
+				}
+
+				if (spa_list_is_empty(&t->bap_transport_linked))
+				    spa_list_append(&transport->bap_transport_linked, &t->bap_transport_linked);
+				else if (spa_list_is_empty(&transport->bap_transport_linked))
+				    spa_list_append(&t->bap_transport_linked, &transport->bap_transport_linked);
+
+				dbus_message_iter_next(&iter);
+			}
+		}
+next:
 		dbus_message_iter_next(props_iter);
 	}
 	return 0;
@@ -2404,10 +2784,27 @@ static int transport_acquire(void *data, bool optional)
 {
 	struct spa_bt_transport *transport = data;
 	struct spa_bt_monitor *monitor = transport->monitor;
-	DBusMessage *m, *r;
+	DBusMessage *m, *r = NULL;
 	DBusError err;
 	int ret = 0;
 	const char *method = optional ? "TryAcquire" : "Acquire";
+	struct spa_bt_transport *t_linked;
+
+	/* For LE Audio, multiple transport from the same device may share the same
+	 * stream (CIS) and group (CIG) but for different direction, e.g. a speaker and
+	 * a microphone. In this case they are linked.
+	 * If one of them has already been acquired this function should not call Acquire
+	 * or TryAcquire but re-use values from the previously acquired transport.
+	 */
+	spa_list_for_each(t_linked, &transport->bap_transport_linked, bap_transport_linked) {
+		if (t_linked->acquired && t_linked->device == transport->device) {
+			transport->fd = t_linked->fd;
+			transport->read_mtu = t_linked->read_mtu;
+			transport->write_mtu = t_linked->write_mtu;
+			spa_log_debug(monitor->log, "transport %p: linked transport %s", transport, t_linked->path);
+			goto done;
+		}
+	}
 
 	m = dbus_message_new_method_call(BLUEZ_SERVICE,
 					 transport->path,
@@ -2451,6 +2848,7 @@ static int transport_acquire(void *data, bool optional)
 		ret = -EIO;
 		goto finish;
 	}
+done:
 	spa_log_debug(monitor->log, "transport %p: %s %s, fd %d MTU %d:%d", transport, method,
 			transport->path, transport->fd, transport->read_mtu, transport->write_mtu);
 
@@ -2459,7 +2857,8 @@ static int transport_acquire(void *data, bool optional)
 	transport_sync_volume(transport);
 
 finish:
-	dbus_message_unref(r);
+	if (r)
+		dbus_message_unref(r);
 	return ret;
 }
 
@@ -2470,12 +2869,30 @@ static int transport_release(void *data)
 	DBusMessage *m, *r;
 	DBusError err;
 	bool is_idle = (transport->state == SPA_BT_TRANSPORT_STATE_IDLE);
+	struct spa_bt_transport *t_linked;
+	bool linked = false;
 
 	spa_log_debug(monitor->log, "transport %p: Release %s",
 			transport, transport->path);
 
 	spa_bt_player_set_state(transport->device->adapter->dummy_player, SPA_BT_PLAYER_STOPPED);
 
+	/* For LE Audio, multiple transport stream (CIS) can be linked together (CIG).
+	 * If they are part of the same device they re-use the same fd, and call to
+	 * release should be done for the last one only.
+	 */
+	spa_list_for_each(t_linked, &transport->bap_transport_linked, bap_transport_linked) {
+		if (t_linked->acquired && t_linked->device == transport->device) {
+			linked = true;
+			break;
+		}
+	}
+	if (linked) {
+		spa_log_info(monitor->log, "Linked transport %s released", transport->path);
+		transport->fd = -1;
+		return 0;
+	}
+
 	close(transport->fd);
 	transport->fd = -1;
 
@@ -2523,23 +2940,21 @@ static const struct spa_bt_transport_implementation transport_impl = {
 	.set_volume = transport_set_volume,
 };
 
-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);
+static void media_codec_switch_reply(DBusPendingCall *pending, void *userdata);
 
-static int a2dp_codec_switch_cmp(const void *a, const void *b);
+static int media_codec_switch_cmp(const void *a, const void *b);
 
-static struct spa_bt_a2dp_codec_switch *a2dp_codec_switch_cmp_sw;  /* global for qsort */
+static struct spa_bt_media_codec_switch *media_codec_switch_cmp_sw;  /* global for qsort */
 
-static int a2dp_codec_switch_start_timer(struct spa_bt_a2dp_codec_switch *sw, uint64_t timeout);
+static int media_codec_switch_start_timer(struct spa_bt_media_codec_switch *sw, uint64_t timeout);
 
-static int a2dp_codec_switch_stop_timer(struct spa_bt_a2dp_codec_switch *sw);
+static int media_codec_switch_stop_timer(struct spa_bt_media_codec_switch *sw);
 
-static void a2dp_codec_switch_free(struct spa_bt_a2dp_codec_switch *sw)
+static void media_codec_switch_free(struct spa_bt_media_codec_switch *sw)
 {
 	char **p;
 
-	a2dp_codec_switch_stop_timer(sw);
+	media_codec_switch_stop_timer(sw);
 
 	if (sw->pending != NULL) {
 		dbus_pending_call_cancel(sw->pending);
@@ -2558,7 +2973,7 @@ static void a2dp_codec_switch_free(struct spa_bt_a2dp_codec_switch *sw)
 	free(sw);
 }
 
-static void a2dp_codec_switch_next(struct spa_bt_a2dp_codec_switch *sw)
+static void media_codec_switch_next(struct spa_bt_media_codec_switch *sw)
 {
 	spa_assert(*sw->codec_iter != NULL && *sw->path_iter != NULL);
 
@@ -2571,13 +2986,13 @@ static void a2dp_codec_switch_next(struct spa_bt_a2dp_codec_switch *sw)
 	sw->retries = CODEC_SWITCH_RETRIES;
 }
 
-static bool a2dp_codec_switch_process_current(struct spa_bt_a2dp_codec_switch *sw)
+static bool media_codec_switch_process_current(struct spa_bt_media_codec_switch *sw)
 {
 	struct spa_bt_remote_endpoint *ep;
 	struct spa_bt_transport *t;
-	const struct a2dp_codec *codec;
+	const struct media_codec *codec;
 	uint8_t config[A2DP_MAX_CAPS_SIZE];
-	char *local_endpoint_base;
+	enum spa_bt_media_direction direction;
 	char *local_endpoint = NULL;
 	int res, config_size;
 	dbus_bool_t dbus_ret;
@@ -2590,43 +3005,43 @@ static bool a2dp_codec_switch_process_current(struct spa_bt_a2dp_codec_switch *s
 
 	codec = *sw->codec_iter;
 
-	spa_log_debug(sw->device->monitor->log, "a2dp codec switch %p: consider codec %s for remote endpoint %s",
+	spa_log_debug(sw->device->monitor->log, "media codec switch %p: consider codec %s for remote endpoint %s",
 	              sw, (*sw->codec_iter)->name, *sw->path_iter);
 
 	ep = device_remote_endpoint_find(sw->device, *sw->path_iter);
 
 	if (ep == NULL || ep->capabilities == NULL || ep->uuid == NULL) {
-		spa_log_debug(sw->device->monitor->log, "a2dp codec switch %p: endpoint %s not valid, try next",
+		spa_log_debug(sw->device->monitor->log, "media codec switch %p: endpoint %s not valid, try next",
 		              sw, *sw->path_iter);
 		goto next;
 	}
 
 	/* Setup and check compatible configuration */
 	if (ep->codec != codec->codec_id) {
-		spa_log_debug(sw->device->monitor->log, "a2dp codec switch %p: different codec, try next", sw);
+		spa_log_debug(sw->device->monitor->log, "media codec switch %p: different codec, try next", sw);
 		goto next;
 	}
 
 	if (!(sw->profile & spa_bt_profile_from_uuid(ep->uuid))) {
-		spa_log_debug(sw->device->monitor->log, "a2dp codec switch %p: wrong uuid (%s) for profile, try next",
+		spa_log_debug(sw->device->monitor->log, "media codec switch %p: wrong uuid (%s) for profile, try next",
 		              sw, ep->uuid);
 		goto next;
 	}
 
-	if (sw->profile & SPA_BT_PROFILE_A2DP_SINK) {
-		local_endpoint_base = A2DP_SOURCE_ENDPOINT;
+	if ((sw->profile & SPA_BT_PROFILE_A2DP_SINK) || (sw->profile & SPA_BT_PROFILE_BAP_SINK) ) {
+		direction = SPA_BT_MEDIA_SOURCE;
 		sink = false;
-	} else if (sw->profile & SPA_BT_PROFILE_A2DP_SOURCE) {
-		local_endpoint_base = A2DP_SINK_ENDPOINT;
+	} else if ((sw->profile & SPA_BT_PROFILE_A2DP_SOURCE) || (sw->profile & SPA_BT_PROFILE_BAP_SOURCE) ) {
+		direction = SPA_BT_MEDIA_SINK;
 		sink = true;
 	} else {
-		spa_log_debug(sw->device->monitor->log, "a2dp codec switch %p: bad profile (%d), try next",
+		spa_log_debug(sw->device->monitor->log, "media codec switch %p: bad profile (%d), try next",
 		              sw, sw->profile);
 		goto next;
 	}
 
-	if (a2dp_codec_to_endpoint(codec, local_endpoint_base, &local_endpoint) < 0) {
-		spa_log_debug(sw->device->monitor->log, "a2dp codec switch %p: no endpoint for codec %s, try next",
+	if (media_codec_to_endpoint(codec, direction, &local_endpoint) < 0) {
+		spa_log_debug(sw->device->monitor->log, "media codec switch %p: no endpoint for codec %s, try next",
 		              sw, codec->name);
 		goto next;
 	}
@@ -2638,36 +3053,39 @@ static bool a2dp_codec_switch_process_current(struct spa_bt_a2dp_codec_switch *s
 		if (t->device->adapter != sw->device->adapter)
 			continue;
 		if (spa_streq(t->endpoint_path, local_endpoint)) {
-			spa_log_debug(sw->device->monitor->log, "a2dp codec switch %p: endpoint %s in use, try next",
+			spa_log_debug(sw->device->monitor->log, "media codec switch %p: endpoint %s in use, try next",
 					sw, local_endpoint);
 			goto next;
 		}
 	}
 
-	res = codec->select_config(codec, sink ? A2DP_CODEC_FLAG_SINK : 0, ep->capabilities, ep->capabilities_len,
+	res = codec->select_config(codec, sink ? MEDIA_CODEC_FLAG_SINK : 0, ep->capabilities, ep->capabilities_len,
 				   &sw->device->monitor->default_audio_info,
 				   &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",
+		spa_log_debug(sw->device->monitor->log, "media codec switch %p: incompatible capabilities (%d), try next",
 		              sw, res);
 		goto next;
 	}
 	config_size = res;
 
-	spa_log_debug(sw->device->monitor->log, "a2dp codec switch %p: configuration %d", sw, config_size);
+	spa_log_debug(sw->device->monitor->log, "media codec switch %p: configuration %d", sw, config_size);
 	for (i = 0; i < config_size; i++)
-		spa_log_debug(sw->device->monitor->log, "a2dp codec switch %p:     %d: %02x", sw, i, config[i]);
+		spa_log_debug(sw->device->monitor->log, "media codec switch %p:     %d: %02x", sw, i, config[i]);
+
+	/* Codecs may share the same endpoint, so indicate which one we are using */
+	sw->device->preferred_codec = codec;
 
 	/* org.bluez.MediaEndpoint1.SetConfiguration on remote endpoint */
 	m = dbus_message_new_method_call(BLUEZ_SERVICE, ep->path, BLUEZ_MEDIA_ENDPOINT_INTERFACE, "SetConfiguration");
 	if (m == NULL) {
-		spa_log_debug(sw->device->monitor->log, "a2dp codec switch %p: dbus allocation failure, try next", sw);
+		spa_log_debug(sw->device->monitor->log, "media codec switch %p: dbus allocation failure, try next", sw);
 		goto next;
 	}
 
 	spa_bt_device_update_last_bluez_action_time(sw->device);
 
-	spa_log_info(sw->device->monitor->log, "a2dp codec switch %p: trying codec %s for endpoint %s, local endpoint %s",
+	spa_log_info(sw->device->monitor->log, "media codec switch %p: trying codec %s for endpoint %s, local endpoint %s",
 	             sw, codec->name, ep->path, local_endpoint);
 
 	dbus_message_iter_init_append(m, &iter);
@@ -2680,16 +3098,16 @@ static bool a2dp_codec_switch_process_current(struct spa_bt_a2dp_codec_switch *s
 	dbus_ret = dbus_connection_send_with_reply(sw->device->monitor->conn, m, &sw->pending, -1);
 
 	if (!dbus_ret || sw->pending == NULL) {
-		spa_log_error(sw->device->monitor->log, "a2dp codec switch %p: dbus call failure, try next", sw);
+		spa_log_error(sw->device->monitor->log, "media codec switch %p: dbus call failure, try next", sw);
 		dbus_message_unref(m);
 		goto next;
 	}
 
-	dbus_ret = dbus_pending_call_set_notify(sw->pending, a2dp_codec_switch_reply, sw, NULL);
+	dbus_ret = dbus_pending_call_set_notify(sw->pending, media_codec_switch_reply, sw, NULL);
 	dbus_message_unref(m);
 
 	if (!dbus_ret) {
-		spa_log_error(sw->device->monitor->log, "a2dp codec switch %p: dbus set notify failure", sw);
+		spa_log_error(sw->device->monitor->log, "media codec switch %p: dbus set notify failure", sw);
 		goto next;
 	}
 
@@ -2701,7 +3119,7 @@ next:
 	return false;
 }
 
-static void a2dp_codec_switch_process(struct spa_bt_a2dp_codec_switch *sw)
+static void media_codec_switch_process(struct spa_bt_media_codec_switch *sw)
 {
 	while (*sw->codec_iter != NULL && *sw->path_iter != NULL) {
 		struct timespec ts;
@@ -2713,80 +3131,80 @@ static void a2dp_codec_switch_process(struct spa_bt_a2dp_codec_switch *sw)
 		threshold = sw->device->last_bluez_action_time + BLUEZ_ACTION_RATE_MSEC * SPA_NSEC_PER_MSEC;
 		if (now < threshold) {
 			/* Wait for timeout */
-			a2dp_codec_switch_start_timer(sw, threshold - now);
+			media_codec_switch_start_timer(sw, threshold - now);
 			return;
 		}
 
 		if (sw->path_iter == sw->paths && (*sw->codec_iter)->caps_preference_cmp) {
 			/* Sort endpoints according to codec preference, when at a new codec. */
-			a2dp_codec_switch_cmp_sw = sw;
-			qsort(sw->paths, sw->num_paths, sizeof(char *), a2dp_codec_switch_cmp);
+			media_codec_switch_cmp_sw = sw;
+			qsort(sw->paths, sw->num_paths, sizeof(char *), media_codec_switch_cmp);
 		}
 
-		if (a2dp_codec_switch_process_current(sw)) {
+		if (media_codec_switch_process_current(sw)) {
 			/* Wait for dbus reply */
 			return;
 		}
 
-		a2dp_codec_switch_next(sw);
+		media_codec_switch_next(sw);
 	};
 
 	/* Didn't find any suitable endpoint. Report failure. */
-	spa_log_info(sw->device->monitor->log, "a2dp codec switch %p: failed to get an endpoint", sw);
+	spa_log_info(sw->device->monitor->log, "media codec switch %p: failed to get an endpoint", sw);
 	spa_bt_device_emit_codec_switched(sw->device, -ENODEV);
 	spa_bt_device_check_profiles(sw->device, false);
-	a2dp_codec_switch_free(sw);
+	media_codec_switch_free(sw);
 }
 
-static bool a2dp_codec_switch_goto_active(struct spa_bt_a2dp_codec_switch *sw)
+static bool media_codec_switch_goto_active(struct spa_bt_media_codec_switch *sw)
 {
 	struct spa_bt_device *device = sw->device;
-	struct spa_bt_a2dp_codec_switch *active_sw;
+	struct spa_bt_media_codec_switch *active_sw;
 
-	active_sw = spa_list_first(&device->codec_switch_list, struct spa_bt_a2dp_codec_switch, device_link);
+	active_sw = spa_list_first(&device->codec_switch_list, struct spa_bt_media_codec_switch, device_link);
 
 	if (active_sw != sw) {
-		struct spa_bt_a2dp_codec_switch *t;
+		struct spa_bt_media_codec_switch *t;
 
 		/* This codec switch has been canceled. Switch to the newest one. */
 		spa_log_debug(sw->device->monitor->log,
-			      "a2dp codec switch %p: canceled, go to new switch", sw);
+			      "media codec switch %p: canceled, go to new switch", sw);
 
 		spa_list_for_each_safe(sw, t, &device->codec_switch_list, device_link) {
 			if (sw != active_sw)
-				a2dp_codec_switch_free(sw);
+				media_codec_switch_free(sw);
 		}
 
-		a2dp_codec_switch_process(active_sw);
+		media_codec_switch_process(active_sw);
 		return false;
 	}
 
 	return true;
 }
 
-static void a2dp_codec_switch_timer_event(struct spa_source *source)
+static void media_codec_switch_timer_event(struct spa_source *source)
 {
-	struct spa_bt_a2dp_codec_switch *sw = source->data;
+	struct spa_bt_media_codec_switch *sw = source->data;
 	struct spa_bt_device *device = sw->device;
 	struct spa_bt_monitor *monitor = device->monitor;
 	uint64_t exp;
 
 	if (spa_system_timerfd_read(monitor->main_system, source->fd, &exp) < 0)
-                spa_log_warn(monitor->log, "error reading timerfd: %s", strerror(errno));
+		spa_log_warn(monitor->log, "error reading timerfd: %s", strerror(errno));
 
-	spa_log_debug(monitor->log, "a2dp codec switch %p: rate limit timer event", sw);
+	spa_log_debug(monitor->log, "media codec switch %p: rate limit timer event", sw);
 
-	a2dp_codec_switch_stop_timer(sw);
+	media_codec_switch_stop_timer(sw);
 
-	if (!a2dp_codec_switch_goto_active(sw))
+	if (!media_codec_switch_goto_active(sw))
 		return;
 
-	a2dp_codec_switch_process(sw);
+	media_codec_switch_process(sw);
 }
 
-static void a2dp_codec_switch_reply(DBusPendingCall *pending, void *user_data)
+static void media_codec_switch_reply(DBusPendingCall *pending, void *user_data)
 {
-	struct spa_bt_a2dp_codec_switch *sw = user_data;
+	struct spa_bt_media_codec_switch *sw = user_data;
 	struct spa_bt_device *device = sw->device;
 	DBusMessage *r;
 
@@ -2798,7 +3216,7 @@ static void a2dp_codec_switch_reply(DBusPendingCall *pending, void *user_data)
 
 	spa_bt_device_update_last_bluez_action_time(device);
 
-	if (!a2dp_codec_switch_goto_active(sw)) {
+	if (!media_codec_switch_goto_active(sw)) {
 		if (r != NULL)
 			dbus_message_unref(r);
 		return;
@@ -2806,14 +3224,14 @@ static void a2dp_codec_switch_reply(DBusPendingCall *pending, void *user_data)
 
 	if (r == NULL) {
 		spa_log_error(sw->device->monitor->log,
-		              "a2dp codec switch %p: empty reply from dbus, trying next",
+		              "media codec switch %p: empty reply from dbus, trying next",
 		              sw);
 		goto next;
 	}
 
 	if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) {
 		spa_log_debug(sw->device->monitor->log,
-		              "a2dp codec switch %p: failed (%s), trying next",
+		              "media codec switch %p: failed (%s), trying next",
 		              sw, dbus_message_get_error_name(r));
 		dbus_message_unref(r);
 		goto next;
@@ -2822,34 +3240,34 @@ static void a2dp_codec_switch_reply(DBusPendingCall *pending, void *user_data)
 	dbus_message_unref(r);
 
 	/* Success */
-	spa_log_info(sw->device->monitor->log, "a2dp codec switch %p: success", sw);
+	spa_log_info(sw->device->monitor->log, "media codec switch %p: success", sw);
 	spa_bt_device_emit_codec_switched(sw->device, 0);
 	spa_bt_device_check_profiles(sw->device, false);
-	a2dp_codec_switch_free(sw);
+	media_codec_switch_free(sw);
 	return;
 
 next:
 	if (sw->retries > 0)
 		--sw->retries;
 	else
-		a2dp_codec_switch_next(sw);
+		media_codec_switch_next(sw);
 
-	a2dp_codec_switch_process(sw);
+	media_codec_switch_process(sw);
 	return;
 }
 
-static int a2dp_codec_switch_start_timer(struct spa_bt_a2dp_codec_switch *sw, uint64_t timeout)
+static int media_codec_switch_start_timer(struct spa_bt_media_codec_switch *sw, uint64_t timeout)
 {
 	struct spa_bt_monitor *monitor = sw->device->monitor;
 	struct itimerspec ts;
 
 	spa_assert(sw->timer.data == NULL);
 
-	spa_log_debug(monitor->log, "a2dp codec switch %p: starting rate limit timer", sw);
+	spa_log_debug(monitor->log, "media codec switch %p: starting rate limit timer", sw);
 
 	if (sw->timer.data == NULL) {
 		sw->timer.data = sw;
-		sw->timer.func = a2dp_codec_switch_timer_event;
+		sw->timer.func = media_codec_switch_timer_event;
 		sw->timer.fd = spa_system_timerfd_create(monitor->main_system,
 				CLOCK_MONOTONIC, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK);
 		sw->timer.mask = SPA_IO_IN;
@@ -2864,7 +3282,7 @@ static int a2dp_codec_switch_start_timer(struct spa_bt_a2dp_codec_switch *sw, ui
 	return 0;
 }
 
-static int a2dp_codec_switch_stop_timer(struct spa_bt_a2dp_codec_switch *sw)
+static int media_codec_switch_stop_timer(struct spa_bt_media_codec_switch *sw)
 {
 	struct spa_bt_monitor *monitor = sw->device->monitor;
 	struct itimerspec ts;
@@ -2872,7 +3290,7 @@ static int a2dp_codec_switch_stop_timer(struct spa_bt_a2dp_codec_switch *sw)
 	if (sw->timer.data == NULL)
 		return 0;
 
-	spa_log_debug(monitor->log, "a2dp codec switch %p: stopping rate limit timer", sw);
+	spa_log_debug(monitor->log, "media codec switch %p: stopping rate limit timer", sw);
 
 	spa_loop_remove_source(monitor->main_loop, &sw->timer);
 	ts.it_value.tv_sec = 0;
@@ -2885,10 +3303,10 @@ static int a2dp_codec_switch_stop_timer(struct spa_bt_a2dp_codec_switch *sw)
 	return 0;
 }
 
-static int a2dp_codec_switch_cmp(const void *a, const void *b)
+static int media_codec_switch_cmp(const void *a, const void *b)
 {
-	struct spa_bt_a2dp_codec_switch *sw = a2dp_codec_switch_cmp_sw;
-	const struct a2dp_codec *codec = *sw->codec_iter;
+	struct spa_bt_media_codec_switch *sw = media_codec_switch_cmp_sw;
+	const struct media_codec *codec = *sw->codec_iter;
 	const char *path1 = *(char **)a, *path2 = *(char **)b;
 	struct spa_bt_remote_endpoint *ep1, *ep2;
 	uint32_t flags;
@@ -2912,7 +3330,10 @@ static int a2dp_codec_switch_cmp(const void *a, const void *b)
 	else if (ep2 == NULL)
 		return -1;
 
-	flags = spa_streq(ep1->uuid, SPA_BT_UUID_A2DP_SOURCE) ? A2DP_CODEC_FLAG_SINK : 0;
+	if (codec->bap)
+		flags = spa_streq(ep1->uuid, SPA_BT_UUID_BAP_SOURCE) ? MEDIA_CODEC_FLAG_SINK : 0;
+	else
+		flags = spa_streq(ep1->uuid, SPA_BT_UUID_A2DP_SOURCE) ? MEDIA_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,
@@ -2920,12 +3341,12 @@ static int a2dp_codec_switch_cmp(const void *a, const void *b)
 }
 
 /* Ensure there's a transport for at least one of the listed codecs */
-int spa_bt_device_ensure_a2dp_codec(struct spa_bt_device *device, const struct a2dp_codec * const *codecs)
+int spa_bt_device_ensure_media_codec(struct spa_bt_device *device, const struct media_codec * const *codecs)
 {
-	struct spa_bt_a2dp_codec_switch *sw;
+	struct spa_bt_media_codec_switch *sw;
 	struct spa_bt_remote_endpoint *ep;
 	struct spa_bt_transport *t;
-	const struct a2dp_codec *preferred_codec = NULL;
+	const struct media_codec *preferred_codec = NULL;
 	size_t i, j, num_codecs, num_eps;
 
 	if (!device->adapter->application_registered) {
@@ -2934,7 +3355,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], true)) {
+		if (spa_bt_device_supports_media_codec(device, codecs[i], true)) {
 			preferred_codec = codecs[i];
 			break;
 		}
@@ -2946,7 +3367,7 @@ int spa_bt_device_ensure_a2dp_codec(struct spa_bt_device *device, const struct a
 	 */
 	if (spa_list_is_empty(&device->codec_switch_list) && preferred_codec != NULL) {
 		spa_list_for_each(t, &device->transport_list, device_link) {
-			if (t->a2dp_codec != preferred_codec)
+			if (t->media_codec != preferred_codec)
 				continue;
 
 			if ((device->connected_profiles & t->profile) != t->profile)
@@ -2959,7 +3380,7 @@ int spa_bt_device_ensure_a2dp_codec(struct spa_bt_device *device, const struct a
 
 	/* Setup and start iteration */
 
-	sw = calloc(1, sizeof(struct spa_bt_a2dp_codec_switch));
+	sw = calloc(1, sizeof(struct spa_bt_media_codec_switch));
 	if (sw == NULL)
 		return -ENOMEM;
 
@@ -2971,17 +3392,17 @@ int spa_bt_device_ensure_a2dp_codec(struct spa_bt_device *device, const struct a
 	while (codecs[num_codecs] != NULL)
 		++num_codecs;
 
-	sw->codecs = calloc(num_codecs + 1, sizeof(const struct a2dp_codec *));
+	sw->codecs = calloc(num_codecs + 1, sizeof(const struct media_codec *));
 	sw->paths = calloc(num_eps + 1, sizeof(char *));
 	sw->num_paths = num_eps;
 
 	if (sw->codecs == NULL || sw->paths == NULL) {
-		a2dp_codec_switch_free(sw);
+		media_codec_switch_free(sw);
 		return -ENOMEM;
 	}
 
 	for (i = 0, j = 0; i < num_codecs; ++i) {
-		if (is_a2dp_codec_enabled(device->monitor, codecs[i])) {
+		if (is_media_codec_enabled(device->monitor, codecs[i])) {
 			sw->codecs[j] = codecs[i];
 			++j;
 		}
@@ -2992,7 +3413,7 @@ int spa_bt_device_ensure_a2dp_codec(struct spa_bt_device *device, const struct a
 	spa_list_for_each(ep, &device->remote_endpoint_list, device_link) {
 		sw->paths[i] = strdup(ep->path);
 		if (sw->paths[i] == NULL) {
-			a2dp_codec_switch_free(sw);
+			media_codec_switch_free(sw);
 			return -ENOMEM;
 		}
 		++i;
@@ -3018,13 +3439,13 @@ int spa_bt_device_ensure_a2dp_codec(struct spa_bt_device *device, const struct a
 		 * to wait to pass in any case, so we don't cancel it either.
 		 */
 		spa_log_debug(sw->device->monitor->log,
-				"a2dp codec switch %p: already in progress, canceling previous",
+				"media codec switch %p: already in progress, canceling previous",
 				sw);
 
 		spa_list_prepend(&device->codec_switch_list, &sw->device_link);
 	} else {
 		spa_list_prepend(&device->codec_switch_list, &sw->device_link);
-		a2dp_codec_switch_process(sw);
+		media_codec_switch_process(sw);
 	}
 
 	return 0;
@@ -3050,7 +3471,7 @@ static DBusHandlerResult endpoint_set_configuration(DBusConnection *conn,
 	DBusMessageIter it[2];
 	DBusMessage *r;
 	struct spa_bt_transport *transport;
-	const struct a2dp_codec *codec;
+	const struct media_codec *codec;
 	int profile;
 	bool sink;
 
@@ -3060,8 +3481,8 @@ 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, &sink);
+	profile = media_endpoint_to_profile(endpoint);
+	codec = media_endpoint_to_codec(monitor, endpoint, &sink, NULL);
 	if (codec == NULL) {
 		spa_log_warn(monitor->log, "unknown SetConfiguration() codec");
 		return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
@@ -3102,7 +3523,7 @@ static DBusHandlerResult endpoint_set_configuration(DBusConnection *conn,
 	free(transport->endpoint_path);
 	transport->endpoint_path = strdup(endpoint);
 	transport->profile = profile;
-	transport->a2dp_codec = codec;
+	transport->media_codec = codec;
 	transport_update_props(transport, &it[1], NULL);
 
 	if (transport->device == NULL || transport->device->adapter == NULL) {
@@ -3110,6 +3531,12 @@ static DBusHandlerResult endpoint_set_configuration(DBusConnection *conn,
 		return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
 	}
 
+	/* If multiple codecs share the endpoint, pick the one we wanted */
+	transport->media_codec = codec = media_endpoint_to_codec(monitor, endpoint, &sink,
+			transport->device->preferred_codec);
+	spa_assert(codec != NULL);
+	spa_log_debug(monitor->log, "%p: %s codec:%s", monitor, path, codec ? codec->name : "<null>");
+
 	spa_bt_device_update_last_bluez_action_time(transport->device);
 
 	if (profile & SPA_BT_PROFILE_A2DP_SOURCE) {
@@ -3122,7 +3549,7 @@ static DBusHandlerResult endpoint_set_configuration(DBusConnection *conn,
 
 	if (codec->validate_config) {
 		struct spa_audio_info info;
-		if (codec->validate_config(codec, sink ? A2DP_CODEC_FLAG_SINK : 0,
+		if (codec->validate_config(codec, sink ? MEDIA_CODEC_FLAG_SINK : 0,
 					transport->configuration, transport->configuration_len,
 					&info) < 0) {
 			spa_log_error(monitor->log, "invalid transport configuration");
@@ -3202,8 +3629,9 @@ static DBusHandlerResult endpoint_release(DBusConnection *conn, DBusMessage *m,
 {
 	DBusMessage *r;
 
-	r = dbus_message_new_error(m, BLUEZ_MEDIA_ENDPOINT_INTERFACE ".Error.NotImplemented",
-                                            "Method not implemented");
+	r = dbus_message_new_error(m,
+				   BLUEZ_MEDIA_ENDPOINT_INTERFACE ".Error.NotImplemented",
+				   "Method not implemented");
 	if (r == NULL)
 		return DBUS_HANDLER_RESULT_NEED_MEMORY;
 	if (!dbus_connection_send(conn, r, NULL))
@@ -3244,6 +3672,8 @@ static DBusHandlerResult endpoint_handler(DBusConnection *c, DBusMessage *m, voi
 		res = endpoint_set_configuration(c, path, m, userdata);
 	else if (dbus_message_is_method_call(m, BLUEZ_MEDIA_ENDPOINT_INTERFACE, "SelectConfiguration"))
 		res = endpoint_select_configuration(c, m, userdata);
+	else if (dbus_message_is_method_call(m, BLUEZ_MEDIA_ENDPOINT_INTERFACE, "SelectProperties"))
+		res = endpoint_select_properties(c, m, userdata);
 	else if (dbus_message_is_method_call(m, BLUEZ_MEDIA_ENDPOINT_INTERFACE, "ClearConfiguration"))
 		res = endpoint_clear_configuration(c, m, userdata);
 	else if (dbus_message_is_method_call(m, BLUEZ_MEDIA_ENDPOINT_INTERFACE, "Release"))
@@ -3260,6 +3690,8 @@ static void bluez_register_endpoint_reply(DBusPendingCall *pending, void *user_d
 	DBusMessage *r;
 
 	r = dbus_pending_call_steal_reply(pending);
+	dbus_pending_call_unref(pending);
+
 	if (r == NULL)
 		return;
 
@@ -3273,9 +3705,8 @@ static void bluez_register_endpoint_reply(DBusPendingCall *pending, void *user_d
 		goto finish;
 	}
 
-	finish:
+finish:
 	dbus_message_unref(r);
-	dbus_pending_call_unref(pending);
 }
 
 static void append_basic_variant_dict_entry(DBusMessageIter *dict, const char* key, int variant_type_int, const char* variant_type_str, void* variant) {
@@ -3303,24 +3734,25 @@ static void append_basic_array_variant_dict_entry(DBusMessageIter *dict, const c
 }
 
 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 *object_path = NULL;
+				   const char *path, enum spa_bt_media_direction direction,
+				   const char *uuid, const struct media_codec *codec)
+{
+	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;
+	bool sink = (direction == SPA_BT_MEDIA_SINK);
 
-	ret = a2dp_codec_to_endpoint(codec, endpoint, &object_path);
+	spa_assert(codec->fill_caps);
+
+	ret = media_codec_to_endpoint(codec, direction, &object_path);
 	if (ret < 0)
 		goto error;
 
-	sink = spa_streq(endpoint, A2DP_SINK_ENDPOINT);
-
-	ret = caps_size = codec->fill_caps(codec, sink ? A2DP_CODEC_FLAG_SINK : 0, caps);
+	ret = caps_size = codec->fill_caps(codec, sink ? MEDIA_CODEC_FLAG_SINK : 0, caps);
 	if (ret < 0)
 		goto error;
 
@@ -3357,37 +3789,10 @@ error:
 	return ret;
 }
 
-static int register_a2dp_endpoint(struct spa_bt_monitor *monitor,
-		const struct a2dp_codec *codec, const char *endpoint)
-{
-	int ret;
-	char* object_path = NULL;
-	const DBusObjectPathVTable vtable_endpoint = {
-		.message_function = endpoint_handler,
-	};
-
-	ret = a2dp_codec_to_endpoint(codec, endpoint, &object_path);
-	if (ret < 0)
-		return ret;
-
-	spa_log_info(monitor->log, "Registering endpoint: %s", object_path);
-
-	if (!dbus_connection_register_object_path(monitor->conn,
-	                                          object_path,
-	                                          &vtable_endpoint, monitor)) {
-		free(object_path);
-		return -EIO;
-	}
-
-	free(object_path);
-	return 0;
-
-}
-
 static int adapter_register_endpoints(struct spa_bt_adapter *a)
 {
 	struct spa_bt_monitor *monitor = a->monitor;
-	const struct a2dp_codec * const * const a2dp_codecs = monitor->a2dp_codecs;
+	const struct media_codec * const * const media_codecs = monitor->media_codecs;
 	int i;
 	int err = 0;
 
@@ -3398,31 +3803,33 @@ static int adapter_register_endpoints(struct spa_bt_adapter *a)
 	 * It doesn't make sense to register codecs other than SBC
 	 * as bluez5 will probably use SBC anyway and we have no control over it
 	 * let's incentivize users to upgrade their bluez5 daemon
-	 * if they want proper a2dp codec support
+	 * if they want proper media codec support
 	 * */
-	spa_log_warn(monitor->log, "Using legacy bluez5 API for A2DP - only SBC will be supported. "
-                               "Please upgrade bluez5.");
+	spa_log_warn(monitor->log,
+		     "Using legacy bluez5 API for A2DP - only SBC will be supported. "
+		     "Please upgrade bluez5.");
 
-	for (i = 0; a2dp_codecs[i]; i++) {
-		const struct a2dp_codec *codec = a2dp_codecs[i];
+	for (i = 0; media_codecs[i]; i++) {
+		const struct media_codec *codec = media_codecs[i];
 
-		if (!is_a2dp_codec_enabled(monitor, codec))
+		if (codec->id != SPA_BLUETOOTH_AUDIO_CODEC_SBC)
 			continue;
 
-		if (!(codec->codec_id == A2DP_CODEC_SBC && spa_streq(codec->name, "sbc")))
-			continue;
-
-		if ((err = bluez_register_endpoint(monitor, a->path,
-						   A2DP_SOURCE_ENDPOINT,
-		                                   SPA_BT_UUID_A2DP_SOURCE,
-		                                   codec)))
-			goto out;
+		if (endpoint_should_be_registered(monitor, codec, SPA_BT_MEDIA_SOURCE)) {
+			if ((err = bluez_register_endpoint(monitor, a->path,
+									SPA_BT_MEDIA_SOURCE,
+									SPA_BT_UUID_A2DP_SOURCE,
+									codec)))
+				goto out;
+		}
 
-		if ((err = bluez_register_endpoint(monitor, a->path,
-						   A2DP_SINK_ENDPOINT,
-		                                   SPA_BT_UUID_A2DP_SINK,
-		                                   codec)))
-			goto out;
+		if (endpoint_should_be_registered(monitor, codec, SPA_BT_MEDIA_SINK)) {
+			if ((err = bluez_register_endpoint(monitor, a->path,
+									SPA_BT_MEDIA_SINK,
+									SPA_BT_UUID_A2DP_SINK,
+									codec)))
+				goto out;
+		}
 
 		a->endpoints_registered = true;
 		break;
@@ -3434,14 +3841,14 @@ static int adapter_register_endpoints(struct spa_bt_adapter *a)
 		err = -ENOSYS;
 	}
 
-	out:
+out:
 	if (err) {
 		spa_log_error(monitor->log, "Failed to register bluez5 endpoints");
 	}
 	return err;
 }
 
-static void append_a2dp_object(DBusMessageIter *iter, const char *endpoint,
+static void append_media_object(DBusMessageIter *iter, const char *endpoint,
 		const char *uuid, uint8_t codec_id, uint8_t *caps, size_t caps_size)
 {
 	const char *interface_name = BLUEZ_MEDIA_ENDPOINT_INTERFACE;
@@ -3475,7 +3882,7 @@ static void append_a2dp_object(DBusMessageIter *iter, const char *endpoint,
 static DBusHandlerResult object_manager_handler(DBusConnection *c, DBusMessage *m, void *user_data)
 {
 	struct spa_bt_monitor *monitor = user_data;
-	const struct a2dp_codec * const * const a2dp_codecs = monitor->a2dp_codecs;
+	const struct media_codec * const * const media_codecs = monitor->media_codecs;
 	const char *path, *interface, *member;
 	char *endpoint;
 	DBusMessage *r;
@@ -3509,38 +3916,52 @@ static DBusHandlerResult object_manager_handler(DBusConnection *c, DBusMessage *
 		dbus_message_iter_init_append(r, &iter);
 		dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{oa{sa{sv}}}", &array);
 
-		for (i = 0; a2dp_codecs[i]; i++) {
-			const struct a2dp_codec *codec = a2dp_codecs[i];
+		for (i = 0; media_codecs[i]; i++) {
+			const struct media_codec *codec = media_codecs[i];
 			uint8_t caps[A2DP_MAX_CAPS_SIZE];
 			int caps_size, ret;
 			uint16_t codec_id = codec->codec_id;
 
-			if (!is_a2dp_codec_enabled(monitor, codec))
+			if (!is_media_codec_enabled(monitor, codec))
+				continue;
+
+			if (codec->bap && !monitor->le_audio_supported) {
+				/* The legacy bluez5 api doesn't support LE Audio
+				 * It doesn't make sense to register unsupported codecs as it prevents
+				 * registration of A2DP codecs
+				 * let's incentivize users to upgrade their bluez5 daemon
+				 * if they want proper media codec support
+				 * */
+				spa_log_warn(monitor->log, "Trying to use legacy bluez5 API for LE Audio - only A2DP will be supported. "
+										"Please upgrade bluez5.");
 				continue;
+			}
 
-			if (codec->decode != NULL) {
-				caps_size = codec->fill_caps(codec, A2DP_CODEC_FLAG_SINK, caps);
+			if (endpoint_should_be_registered(monitor, codec, SPA_BT_MEDIA_SINK)) {
+				caps_size = codec->fill_caps(codec, MEDIA_CODEC_FLAG_SINK, caps);
 				if (caps_size < 0)
 					continue;
 
-				ret = a2dp_codec_to_endpoint(codec, A2DP_SINK_ENDPOINT, &endpoint);
+				ret = media_codec_to_endpoint(codec, SPA_BT_MEDIA_SINK, &endpoint);
 				if (ret == 0) {
-					spa_log_info(monitor->log, "register A2DP sink codec %s: %s", a2dp_codecs[i]->name, endpoint);
-					append_a2dp_object(&array, endpoint, SPA_BT_UUID_A2DP_SINK,
+					spa_log_info(monitor->log, "register media sink codec %s: %s", media_codecs[i]->name, endpoint);
+					append_media_object(&array, endpoint,
+					        codec->bap ? SPA_BT_UUID_BAP_SINK : SPA_BT_UUID_A2DP_SINK,
 							codec_id, caps, caps_size);
 					free(endpoint);
 				}
 			}
 
-			if (codec->encode != NULL) {
+			if (endpoint_should_be_registered(monitor, codec, SPA_BT_MEDIA_SOURCE)) {
 				caps_size = codec->fill_caps(codec, 0, caps);
 				if (caps_size < 0)
 					continue;
 
-				ret = a2dp_codec_to_endpoint(codec, A2DP_SOURCE_ENDPOINT, &endpoint);
+				ret = media_codec_to_endpoint(codec, SPA_BT_MEDIA_SOURCE, &endpoint);
 				if (ret == 0) {
-					spa_log_info(monitor->log, "register A2DP source codec %s: %s", a2dp_codecs[i]->name, endpoint);
-					append_a2dp_object(&array, endpoint, SPA_BT_UUID_A2DP_SOURCE,
+					spa_log_info(monitor->log, "register media source codec %s: %s", media_codecs[i]->name, endpoint);
+					append_media_object(&array, endpoint,
+					        codec->bap ? SPA_BT_UUID_BAP_SOURCE : SPA_BT_UUID_A2DP_SOURCE,
 							codec_id, caps, caps_size);
 					free(endpoint);
 				}
@@ -3566,6 +3987,8 @@ static void bluez_register_application_reply(DBusPendingCall *pending, void *use
 	bool fallback = true;
 
 	r = dbus_pending_call_steal_reply(pending);
+	dbus_pending_call_unref(pending);
+
 	if (r == NULL)
 		return;
 
@@ -3585,71 +4008,100 @@ static void bluez_register_application_reply(DBusPendingCall *pending, void *use
 
 finish:
 	dbus_message_unref(r);
-	dbus_pending_call_unref(pending);
 
 	if (fallback)
 		adapter_register_endpoints(adapter);
 }
 
+static int register_media_endpoint(struct spa_bt_monitor *monitor,
+				   const struct media_codec *codec,
+				   enum spa_bt_media_direction direction)
+{
+	static const DBusObjectPathVTable vtable_endpoint = {
+		.message_function = endpoint_handler,
+	};
+
+	if (!endpoint_should_be_registered(monitor, codec, direction))
+		return 0;
+
+	char *object_path = NULL;
+	int ret = media_codec_to_endpoint(codec, direction, &object_path);
+	if (ret < 0)
+		return ret;
+
+	spa_log_info(monitor->log, "registering endpoint: %s", object_path);
+
+	if (!dbus_connection_register_object_path(monitor->conn,
+						  object_path,
+						  &vtable_endpoint, monitor))
+	{
+		ret = -EIO;
+	}
+
+	free(object_path);
+	return ret;
+}
+
 static int register_media_application(struct spa_bt_monitor * monitor)
 {
-	const struct a2dp_codec * const * const a2dp_codecs = monitor->a2dp_codecs;
+	const struct media_codec * const * const media_codecs = monitor->media_codecs;
 	const DBusObjectPathVTable vtable_object_manager = {
 		.message_function = object_manager_handler,
 	};
 
-	spa_log_info(monitor->log, "Registering media application object: " A2DP_OBJECT_MANAGER_PATH);
+	spa_log_info(monitor->log, "Registering media application object: " MEDIA_OBJECT_MANAGER_PATH);
 
 	if (!dbus_connection_register_object_path(monitor->conn,
-	                                          A2DP_OBJECT_MANAGER_PATH,
+	                                          MEDIA_OBJECT_MANAGER_PATH,
 	                                          &vtable_object_manager, monitor))
 		return -EIO;
 
-	for (int i = 0; a2dp_codecs[i]; i++) {
-		const struct a2dp_codec *codec = a2dp_codecs[i];
-
-		if (!is_a2dp_codec_enabled(monitor, codec))
-			continue;
+	for (int i = 0; media_codecs[i]; i++) {
+		const struct media_codec *codec = media_codecs[i];
 
-		if (codec->encode != NULL)
-			register_a2dp_endpoint(monitor, codec, A2DP_SOURCE_ENDPOINT);
-		if (codec->decode != NULL)
-			register_a2dp_endpoint(monitor, codec, A2DP_SINK_ENDPOINT);
+		register_media_endpoint(monitor, codec, SPA_BT_MEDIA_SOURCE);
+		register_media_endpoint(monitor, codec, SPA_BT_MEDIA_SINK);
 	}
 
 	return 0;
 }
 
-static void unregister_media_application(struct spa_bt_monitor * monitor)
+static void unregister_media_endpoint(struct spa_bt_monitor *monitor,
+				      const struct media_codec *codec,
+				      enum spa_bt_media_direction direction)
 {
-	const struct a2dp_codec * const * const a2dp_codecs = monitor->a2dp_codecs;
-	int ret;
+	if (!endpoint_should_be_registered(monitor, codec, direction))
+		return;
+
 	char *object_path = NULL;
+	int ret = media_codec_to_endpoint(codec, direction, &object_path);
+	if (ret < 0)
+		return;
 
-	for (int i = 0; a2dp_codecs[i]; i++) {
-		const struct a2dp_codec *codec = a2dp_codecs[i];
+	spa_log_info(monitor->log, "unregistering endpoint: %s", object_path);
 
-		if (!is_a2dp_codec_enabled(monitor, codec))
-			continue;
+	if (!dbus_connection_unregister_object_path(monitor->conn, object_path))
+		spa_log_warn(monitor->log, "failed to unregister %s\n", object_path);
 
-		ret = a2dp_codec_to_endpoint(codec, A2DP_SOURCE_ENDPOINT, &object_path);
-		if (ret == 0) {
-			dbus_connection_unregister_object_path(monitor->conn, object_path);
-			free(object_path);
-		}
+	free(object_path);
+}
 
-		ret = a2dp_codec_to_endpoint(codec, A2DP_SINK_ENDPOINT, &object_path);
-		if (ret == 0) {
-			dbus_connection_unregister_object_path(monitor->conn, object_path);
-			free(object_path);
-		}
+static void unregister_media_application(struct spa_bt_monitor * monitor)
+{
+	const struct media_codec * const * const media_codecs = monitor->media_codecs;
+
+	for (int i = 0; media_codecs[i]; i++) {
+		const struct media_codec *codec = media_codecs[i];
+
+		unregister_media_endpoint(monitor, codec, SPA_BT_MEDIA_SOURCE);
+		unregister_media_endpoint(monitor, codec, SPA_BT_MEDIA_SINK);
 	}
 
-	dbus_connection_unregister_object_path(monitor->conn, A2DP_OBJECT_MANAGER_PATH);
+	dbus_connection_unregister_object_path(monitor->conn, MEDIA_OBJECT_MANAGER_PATH);
 }
 
 static int adapter_register_application(struct spa_bt_adapter *a) {
-	const char *object_manager_path = A2DP_OBJECT_MANAGER_PATH;
+	const char *object_manager_path = MEDIA_OBJECT_MANAGER_PATH;
 	struct spa_bt_monitor *monitor = a->monitor;
 	DBusMessage *m;
 	DBusMessageIter i, d;
@@ -3745,6 +4197,48 @@ static void reselect_backend(struct spa_bt_monitor *monitor, bool silent)
 				backend ? backend->name : "none");
 }
 
+static int media_update_props(struct spa_bt_monitor *monitor,
+				DBusMessageIter *props_iter,
+				DBusMessageIter *invalidated_iter)
+{
+	while (dbus_message_iter_get_arg_type(props_iter) != DBUS_TYPE_INVALID) {
+		DBusMessageIter it[2];
+		const char *key;
+
+		dbus_message_iter_recurse(props_iter, &it[0]);
+		dbus_message_iter_get_basic(&it[0], &key);
+		dbus_message_iter_next(&it[0]);
+		dbus_message_iter_recurse(&it[0], &it[1]);
+
+		if (spa_streq(key, "SupportedUUIDs")) {
+			DBusMessageIter iter;
+
+			if (!check_iter_signature(&it[1], "as"))
+				goto next;
+
+			dbus_message_iter_recurse(&it[1], &iter);
+
+			while (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_INVALID) {
+				const char *uuid;
+
+				dbus_message_iter_get_basic(&iter, &uuid);
+
+				if (spa_streq(uuid, SPA_BT_UUID_BAP_SINK)) {
+					monitor->le_audio_supported = true;
+					spa_log_info(monitor->log, "LE Audio supported");
+				}
+				dbus_message_iter_next(&iter);
+			}
+		}
+		else
+			spa_log_debug(monitor->log, "media: unhandled key %s", key);
+
+next:
+		dbus_message_iter_next(props_iter);
+	}
+	return 0;
+}
+
 static void interface_added(struct spa_bt_monitor *monitor,
 			    DBusConnection *conn,
 			    const char *object_path,
@@ -3818,6 +4312,9 @@ static void interface_added(struct spa_bt_monitor *monitor,
 		if (d)
 			spa_bt_device_emit_profiles_changed(d, d->profiles, d->connected_profiles);
 	}
+	else if (spa_streq(interface_name, BLUEZ_MEDIA_INTERFACE)) {
+		media_update_props(monitor, props_iter, NULL);
+	}
 }
 
 static void interfaces_added(struct spa_bt_monitor *monitor, DBusMessageIter *arg_iter)
@@ -3892,7 +4389,12 @@ static void get_managed_objects_reply(DBusPendingCall *pending, void *user_data)
 	DBusMessage *r;
 	DBusMessageIter it[6];
 
+	spa_assert(pending == monitor->get_managed_objects_call);
+	monitor->get_managed_objects_call = NULL;
+
 	r = dbus_pending_call_steal_reply(pending);
+	dbus_pending_call_unref(pending);
+
 	if (r == NULL)
 		return;
 
@@ -3927,14 +4429,16 @@ static void get_managed_objects_reply(DBusPendingCall *pending, void *user_data)
 
 	monitor->objects_listed = true;
 
-      finish:
+finish:
 	dbus_message_unref(r);
-        dbus_pending_call_unref(pending);
 	return;
 }
 
 static void get_managed_objects(struct spa_bt_monitor *monitor)
 {
+	if (monitor->objects_listed || monitor->get_managed_objects_call)
+		return;
+
 	DBusMessage *m;
 	DBusPendingCall *call;
 
@@ -3943,74 +4447,13 @@ static void get_managed_objects(struct spa_bt_monitor *monitor)
 					 "org.freedesktop.DBus.ObjectManager",
 					 "GetManagedObjects");
 
-	dbus_connection_send_with_reply(monitor->conn, m, &call, -1);
-	dbus_pending_call_set_notify(call, get_managed_objects_reply, monitor, NULL);
-        dbus_message_unref(m);
-}
-
-static void check_name_owner_reply(DBusPendingCall *pending, void *user_data)
-{
-	struct spa_bt_monitor *monitor = user_data;
-	DBusMessage *r;
-	DBusError error;
-	bool running;
-
-	r = dbus_pending_call_steal_reply(pending);
-	if (r == NULL)
-		return;
-
-	if (dbus_message_is_error(r, DBUS_ERROR_UNKNOWN_METHOD)) {
-		spa_log_warn(monitor->log, "BlueZ D-Bus ObjectManager not available");
-		goto finish;
-	}
-	if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) {
-		spa_log_error(monitor->log, "NameHasOwner() failed: %s",
-				dbus_message_get_error_name(r));
-		goto finish;
-	}
-	if (!spa_streq(dbus_message_get_signature(r), "b")) {
-		spa_log_error(monitor->log, "Invalid reply signature for NameHasOwner()");
-		goto finish;
-	}
-
-	dbus_error_init(&error);
-	dbus_message_get_args(r, &error, DBUS_TYPE_BOOLEAN, &running, DBUS_TYPE_INVALID);
-
-	if (dbus_error_is_set(&error)) {
-                spa_log_error(monitor->log, "Could not check bluetooth service: %s", error.message);
-		dbus_error_free(&error);
-		goto finish;
-	}
-
-	spa_log_info(monitor->log, "bluetooth service running: %s",
-			running ? "yes" : "no");
-	if (running)
-		get_managed_objects(monitor);
-
-finish:
-	dbus_message_unref(r);
-	dbus_pending_call_unref(pending);
-	return;
-}
-
-static void check_name_owner(struct spa_bt_monitor *monitor)
-{
-	DBusMessage *m;
-	DBusPendingCall *call;
-	const char *service = BLUEZ_SERVICE;
-
-	m = dbus_message_new_method_call("org.freedesktop.DBus",
-					 "/org/freedesktop/DBus",
-					 "org.freedesktop.DBus",
-					 "NameHasOwner");
-	if (m == NULL)
-		return;
-
-	dbus_message_append_args(m, DBUS_TYPE_STRING, &service, DBUS_TYPE_INVALID);
+	dbus_message_set_auto_start(m, false);
 
 	dbus_connection_send_with_reply(monitor->conn, m, &call, -1);
-	dbus_pending_call_set_notify(call, check_name_owner_reply, monitor, NULL);
+	dbus_pending_call_set_notify(call, get_managed_objects_reply, monitor, NULL);
 	dbus_message_unref(m);
+
+	monitor->get_managed_objects_call = call;
 }
 
 static DBusHandlerResult filter_cb(DBusConnection *bus, DBusMessage *m, void *user_data)
@@ -4189,7 +4632,7 @@ static DBusHandlerResult filter_cb(DBusConnection *bus, DBusMessage *m, void *us
 
 			transport_update_props(transport, &it[1], NULL);
 		}
-        }
+	}
 
 fail:
 	dbus_error_free(&err);
@@ -4271,7 +4714,13 @@ impl_device_add_listener(void *object, struct spa_hook *listener,
 	spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
 
 	add_filters(this);
-	check_name_owner(this);
+	get_managed_objects(this);
+
+	struct spa_bt_device *device;
+	spa_list_for_each(device, &this->device_list, link) {
+		if (device->added)
+			emit_device_info(this, device, this->connection_info_supported);
+	}
 
 	spa_hook_list_join(&this->hooks, &save);
 
@@ -4325,6 +4774,11 @@ static int impl_clear(struct spa_handle *handle)
 		monitor->filters_added = false;
 	}
 
+	if (monitor->get_managed_objects_call) {
+		dbus_pending_call_cancel(monitor->get_managed_objects_call);
+		dbus_pending_call_unref(monitor->get_managed_objects_call);
+	}
+
 	spa_list_consume(t, &monitor->transport_list, link)
 		spa_bt_transport_free(t);
 	spa_list_consume(ep, &monitor->remote_endpoint_list, link)
@@ -4361,7 +4815,7 @@ static int impl_clear(struct spa_handle *handle)
 
 	spa_bt_quirks_destroy(monitor->quirks);
 
-	free_a2dp_codecs(monitor->a2dp_codecs);
+	free_media_codecs(monitor->media_codecs);
 
 	return 0;
 }
@@ -4397,6 +4851,10 @@ int spa_bt_profiles_from_json_array(const char *str)
 			profiles |= SPA_BT_PROFILE_A2DP_SINK;
 		} else if (spa_streq(role_name, "a2dp_source")) {
 			profiles |= SPA_BT_PROFILE_A2DP_SOURCE;
+		} else if (spa_streq(role_name, "bap_sink")) {
+			profiles |= SPA_BT_PROFILE_BAP_SINK;
+		} else if (spa_streq(role_name, "bap_source")) {
+			profiles |= SPA_BT_PROFILE_BAP_SOURCE;
 		}
 	}
 
@@ -4405,7 +4863,7 @@ int spa_bt_profiles_from_json_array(const char *str)
 
 static int parse_codec_array(struct spa_bt_monitor *this, const struct spa_dict *info)
 {
-	const struct a2dp_codec * const * const a2dp_codecs = this->a2dp_codecs;
+	const struct media_codec * const * const media_codecs = this->media_codecs;
 	const char *str;
 	struct spa_dict_item *codecs;
 	struct spa_json it, it_array;
@@ -4416,7 +4874,7 @@ static int parse_codec_array(struct spa_bt_monitor *this, const struct spa_dict
 	/* Parse bluez5.codecs property to a dict of enabled codecs */
 
 	num_codecs = 0;
-	while (a2dp_codecs[num_codecs])
+	while (media_codecs[num_codecs])
 		++num_codecs;
 
 	codecs = calloc(num_codecs, sizeof(struct spa_dict_item));
@@ -4438,8 +4896,8 @@ static int parse_codec_array(struct spa_bt_monitor *this, const struct spa_dict
 	while (spa_json_get_string(&it_array, codec_name, sizeof(codec_name)) > 0) {
 		int i;
 
-		for (i = 0; a2dp_codecs[i]; ++i) {
-			const struct a2dp_codec *codec = a2dp_codecs[i];
+		for (i = 0; media_codecs[i]; ++i) {
+			const struct media_codec *codec = media_codecs[i];
 
 			if (!spa_streq(codec->name, codec_name))
 				continue;
@@ -4461,16 +4919,16 @@ static int parse_codec_array(struct spa_bt_monitor *this, const struct spa_dict
 
 	spa_dict_qsort(&this->enabled_codecs);
 
-	for (i = 0; a2dp_codecs[i]; ++i) {
-		const struct a2dp_codec *codec = a2dp_codecs[i];
-		if (!is_a2dp_codec_enabled(this, codec))
+	for (i = 0; media_codecs[i]; ++i) {
+		const struct media_codec *codec = media_codecs[i];
+		if (!is_media_codec_enabled(this, codec))
 			spa_log_debug(this->log, "disabling codec %s", codec->name);
 	}
 	return 0;
 
 fallback:
-	for (i = 0; a2dp_codecs[i]; ++i) {
-		const struct a2dp_codec *codec = a2dp_codecs[i];
+	for (i = 0; media_codecs[i]; ++i) {
+		const struct media_codec *codec = media_codecs[i];
 		spa_log_debug(this->log, "enabling codec %s", codec->name);
 		codecs[i].key = codec->name;
 		codecs[i].value = "true";
@@ -4536,14 +4994,14 @@ impl_init(const struct spa_handle_factory *factory,
 		return -EINVAL;
 	}
 
-	this->a2dp_codecs = NULL;
+	this->media_codecs = NULL;
 	this->quirks = NULL;
 	this->conn = NULL;
 	this->dbus_connection = NULL;
 
-	this->a2dp_codecs = load_a2dp_codecs(this->plugin_loader, this->log);
-	if (this->a2dp_codecs == NULL) {
-		spa_log_error(this->log, "failed to load required A2DP codec plugins");
+	this->media_codecs = load_media_codecs(this->plugin_loader, this->log);
+	if (this->media_codecs == NULL) {
+		spa_log_error(this->log, "failed to load required media codec plugins");
 		res = -EIO;
 		goto fail;
 	}
@@ -4640,15 +5098,15 @@ impl_init(const struct spa_handle_factory *factory,
 	return 0;
 
 fail:
-	if (this->a2dp_codecs)
-		free_a2dp_codecs(this->a2dp_codecs);
+	if (this->media_codecs)
+		free_media_codecs(this->media_codecs);
 	if (this->quirks)
 		spa_bt_quirks_destroy(this->quirks);
 	if (this->conn)
 		dbus_connection_unref(this->conn);
 	if (this->dbus_connection)
 		spa_dbus_connection_destroy(this->dbus_connection);
-	this->a2dp_codecs = NULL;
+	this->media_codecs = NULL;
 	this->quirks = NULL;
 	this->conn = NULL;
 	this->dbus_connection = NULL;
diff --git a/spa/plugins/bluez5/bluez5-device.c b/spa/plugins/bluez5/bluez5-device.c
index 5a23ea4409c1c0842957ae9952b1be1cba2c6d68..f3c2d721f3b05f031f3d6284d1785047b52de317 100644
--- a/spa/plugins/bluez5/bluez5-device.c
+++ b/spa/plugins/bluez5/bluez5-device.c
@@ -51,7 +51,7 @@
 #include <spa/debug/pod.h>
 
 #include "defs.h"
-#include "a2dp-codecs.h"
+#include "media-codecs.h"
 
 static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.device");
 #undef SPA_LOG_TOPIC_DEFAULT
@@ -73,6 +73,8 @@ enum {
 	DEVICE_PROFILE_AG = 1,
 	DEVICE_PROFILE_A2DP = 2,
 	DEVICE_PROFILE_HSP_HFP = 3,
+	DEVICE_PROFILE_BAP = 4,
+	DEVICE_PROFILE_LAST = DEVICE_PROFILE_BAP,
 };
 
 struct props {
@@ -140,11 +142,11 @@ struct impl {
 	unsigned int save_profile:1;
 	uint32_t prev_bt_connected_profiles;
 
-	const struct a2dp_codec **supported_codecs;
+	const struct media_codec **supported_codecs;
 	size_t supported_codec_count;
 
-	struct dynamic_node dyn_a2dp_source;
-	struct dynamic_node dyn_a2dp_sink;
+	struct dynamic_node dyn_media_source;
+	struct dynamic_node dyn_media_sink;
 	struct dynamic_node dyn_sco_source;
 	struct dynamic_node dyn_sco_sink;
 
@@ -167,9 +169,9 @@ static void init_node(struct impl *this, struct node *node, uint32_t id)
 	}
 }
 
-static void get_a2dp_codecs(struct impl *this, enum spa_bluetooth_audio_codec id, const struct a2dp_codec **codecs, size_t size)
+static void get_media_codecs(struct impl *this, enum spa_bluetooth_audio_codec id, const struct media_codec **codecs, size_t size)
 {
-	const struct a2dp_codec * const *c;
+	const struct media_codec * const *c;
 
 	spa_assert(size > 0);
 	spa_assert(this->supported_codecs);
@@ -184,18 +186,18 @@ static void get_a2dp_codecs(struct impl *this, enum spa_bluetooth_audio_codec id
 	*codecs = NULL;
 }
 
-static const struct a2dp_codec *get_supported_a2dp_codec(struct impl *this, enum spa_bluetooth_audio_codec id, size_t *idx)
+static const struct media_codec *get_supported_media_codec(struct impl *this, enum spa_bluetooth_audio_codec id, size_t *idx)
 {
-	const struct a2dp_codec *a2dp_codec = NULL;
+	const struct media_codec *media_codec = NULL;
 	size_t i;
 	for (i = 0; i < this->supported_codec_count; ++i) {
 		if (this->supported_codecs[i]->id == id) {
-			a2dp_codec = this->supported_codecs[i];
+			media_codec = this->supported_codecs[i];
 			if (idx)
 				*idx = i;
 		}
 	}
-	return a2dp_codec;
+	return media_codec;
 }
 
 static unsigned int get_hfp_codec(enum spa_bluetooth_audio_codec id)
@@ -245,10 +247,10 @@ static const char *get_hfp_codec_name(unsigned int codec)
 
 static const char *get_codec_name(struct spa_bt_transport *t, bool a2dp_duplex)
 {
-	if (t->a2dp_codec != NULL) {
-		if (a2dp_duplex && t->a2dp_codec->duplex_codec)
-			return t->a2dp_codec->duplex_codec->name;
-		return t->a2dp_codec->name;
+	if (t->media_codec != NULL) {
+		if (a2dp_duplex && t->media_codec->duplex_codec)
+			return t->media_codec->duplex_codec->name;
+		return t->media_codec->name;
 	}
 	return get_hfp_codec_name(t->codec);
 }
@@ -301,7 +303,7 @@ 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;
+	const struct media_codec *codec = node->transport ? node->transport->media_codec : NULL;
 
 	/*
 	 * For A2DP duplex, the duplex microphone channel sometimes does not appear
@@ -351,6 +353,7 @@ static bool node_update_volume_from_transport(struct node *node, bool reset)
 
 	/* PW is the controller for remote device. */
 	if (impl->profile != DEVICE_PROFILE_A2DP
+	    && impl->profile != DEVICE_PROFILE_BAP
 	    && impl->profile !=  DEVICE_PROFILE_HSP_HFP)
 		return false;
 
@@ -406,16 +409,16 @@ static const struct spa_bt_transport_events transport_events = {
 
 static void get_channels(struct spa_bt_transport *t, bool a2dp_duplex, uint32_t *n_channels, uint32_t *channels)
 {
-	const struct a2dp_codec *codec;
+	const struct media_codec *codec;
 	struct spa_audio_info info = { 0 };
 
-	if (!a2dp_duplex || !t->a2dp_codec || !t->a2dp_codec->duplex_codec) {
+	if (!a2dp_duplex || !t->media_codec || !t->media_codec->duplex_codec) {
 		*n_channels = t->n_channels;
 		memcpy(channels, t->channels, t->n_channels * sizeof(uint32_t));
 		return;
 	}
 
-	codec = t->a2dp_codec->duplex_codec;
+	codec = t->media_codec->duplex_codec;
 
 	if (!codec->validate_config ||
 			codec->validate_config(codec, 0,
@@ -514,7 +517,7 @@ static struct spa_bt_transport *find_transport(struct impl *this, int profile, e
 
 	spa_list_for_each(t, &device->transport_list, device_link) {
 		bool codec_ok = codec == 0 ||
-			(t->a2dp_codec != NULL && t->a2dp_codec->id == codec) ||
+			(t->media_codec != NULL && t->media_codec->id == codec) ||
 			get_hfp_codec_id(t->codec) == codec;
 
 		if ((t->profile & device->connected_profiles) &&
@@ -676,15 +679,15 @@ static int emit_nodes(struct impl *this)
 						1, SPA_NAME_API_BLUEZ5_SCO_SINK, false);
 			}
 		}
-		if (this->bt_dev->connected_profiles & SPA_BT_PROFILE_A2DP_SOURCE) {
+		if (this->bt_dev->connected_profiles & (SPA_BT_PROFILE_A2DP_SOURCE)) {
 			t = find_transport(this, SPA_BT_PROFILE_A2DP_SOURCE, 0);
 			if (t) {
-				this->props.codec = t->a2dp_codec->id;
-				emit_dynamic_node(&this->dyn_a2dp_source, this, t,
+				this->props.codec = t->media_codec->id;
+				emit_dynamic_node(&this->dyn_media_source, this, t,
 						2, SPA_NAME_API_BLUEZ5_A2DP_SOURCE, false);
 
-				if (t->a2dp_codec->duplex_codec) {
-					emit_dynamic_node(&this->dyn_a2dp_sink, this, t,
+				if (t->media_codec->duplex_codec) {
+					emit_dynamic_node(&this->dyn_media_sink, this, t,
 						3, SPA_NAME_API_BLUEZ5_A2DP_SINK, true);
 				}
 			}
@@ -694,11 +697,11 @@ static int emit_nodes(struct impl *this)
 		if (this->bt_dev->connected_profiles & SPA_BT_PROFILE_A2DP_SOURCE) {
 			t = find_transport(this, SPA_BT_PROFILE_A2DP_SOURCE, 0);
 			if (t) {
-				this->props.codec = t->a2dp_codec->id;
-				emit_dynamic_node(&this->dyn_a2dp_source, this, t,
+				this->props.codec = t->media_codec->id;
+				emit_dynamic_node(&this->dyn_media_source, this, t,
 					DEVICE_ID_SOURCE, SPA_NAME_API_BLUEZ5_A2DP_SOURCE, false);
 
-				if (t->a2dp_codec->duplex_codec) {
+				if (t->media_codec->duplex_codec) {
 					emit_node(this, t,
 						DEVICE_ID_SINK, SPA_NAME_API_BLUEZ5_A2DP_SINK, true);
 				}
@@ -708,17 +711,45 @@ static int emit_nodes(struct impl *this)
 		if (this->bt_dev->connected_profiles & SPA_BT_PROFILE_A2DP_SINK) {
 			t = find_transport(this, SPA_BT_PROFILE_A2DP_SINK, this->props.codec);
 			if (t) {
-				this->props.codec = t->a2dp_codec->id;
+				this->props.codec = t->media_codec->id;
 				emit_node(this, t, DEVICE_ID_SINK, SPA_NAME_API_BLUEZ5_A2DP_SINK, false);
 
-				if (t->a2dp_codec->duplex_codec) {
+				if (t->media_codec->duplex_codec) {
 					emit_node(this, t,
 						DEVICE_ID_SOURCE, SPA_NAME_API_BLUEZ5_A2DP_SOURCE, true);
 				}
 			}
 		}
 
-		if (get_supported_a2dp_codec(this, this->props.codec, NULL) == NULL)
+		if (get_supported_media_codec(this, this->props.codec, NULL) == NULL)
+			this->props.codec = 0;
+		break;
+	case DEVICE_PROFILE_BAP:
+		if (this->bt_dev->connected_profiles & (SPA_BT_PROFILE_BAP_SOURCE)) {
+			t = find_transport(this, SPA_BT_PROFILE_BAP_SOURCE, 0);
+			if (t) {
+				this->props.codec = t->media_codec->id;
+				if (t->bap_initiator)
+					emit_node(this, t, DEVICE_ID_SOURCE, SPA_NAME_API_BLUEZ5_MEDIA_SOURCE, false);
+				else
+					emit_dynamic_node(&this->dyn_media_source, this, t,
+						DEVICE_ID_SOURCE, SPA_NAME_API_BLUEZ5_MEDIA_SOURCE, false);
+			}
+		}
+
+		if (this->bt_dev->connected_profiles & (SPA_BT_PROFILE_BAP_SINK)) {
+			t = find_transport(this, SPA_BT_PROFILE_BAP_SINK, this->props.codec);
+			if (t) {
+				this->props.codec = t->media_codec->id;
+				if (t->bap_initiator)
+					emit_node(this, t, DEVICE_ID_SINK, SPA_NAME_API_BLUEZ5_MEDIA_SINK, false);
+				else
+					emit_dynamic_node(&this->dyn_media_sink, this, t,
+						DEVICE_ID_SINK, SPA_NAME_API_BLUEZ5_MEDIA_SINK, false);
+			}
+		}
+
+		if (get_supported_media_codec(this, this->props.codec, NULL) == NULL)
 			this->props.codec = 0;
 		break;
 	case DEVICE_PROFILE_HSP_HFP:
@@ -766,8 +797,8 @@ static void emit_info(struct impl *this, bool full)
 
 static void emit_remove_nodes(struct impl *this)
 {
-	remove_dynamic_node (&this->dyn_a2dp_source);
-	remove_dynamic_node (&this->dyn_a2dp_sink);
+	remove_dynamic_node (&this->dyn_media_source);
+	remove_dynamic_node (&this->dyn_media_sink);
 	remove_dynamic_node (&this->dyn_sco_source);
 	remove_dynamic_node (&this->dyn_sco_sink);
 
@@ -800,6 +831,7 @@ static int set_profile(struct impl *this, uint32_t profile, enum spa_bluetooth_a
 
 	if (this->profile == profile &&
 	    (this->profile != DEVICE_PROFILE_A2DP || codec == this->props.codec) &&
+	    (this->profile != DEVICE_PROFILE_BAP || codec == this->props.codec) &&
 	    (this->profile != DEVICE_PROFILE_HSP_HFP || codec == this->props.codec))
 		return 0;
 
@@ -812,19 +844,20 @@ static int set_profile(struct impl *this, uint32_t profile, enum spa_bluetooth_a
 	this->props.codec = codec;
 
 	/*
-	 * A2DP: ensure there's a transport with the selected codec (0 means any).
+	 * A2DP/BAP: ensure there's a transport with the selected codec (0 means any).
 	 * Don't try to switch codecs when the device is in the A2DP source role, since
 	 * devices do not appear to like that.
 	 */
-	if (profile == DEVICE_PROFILE_A2DP && !(this->bt_dev->connected_profiles & SPA_BT_PROFILE_A2DP_SOURCE)) {
+	if ((profile == DEVICE_PROFILE_A2DP || profile == DEVICE_PROFILE_BAP)
+	    && !(this->bt_dev->connected_profiles & SPA_BT_PROFILE_A2DP_SOURCE)) {
 		int ret;
-		const struct a2dp_codec *codecs[64];
+		const struct media_codec *codecs[64];
 
-		get_a2dp_codecs(this, codec, codecs, SPA_N_ELEMENTS(codecs));
+		get_media_codecs(this, codec, codecs, SPA_N_ELEMENTS(codecs));
 
 		this->switching_codec = true;
 
-		ret = spa_bt_device_ensure_a2dp_codec(this->bt_dev, codecs);
+		ret = spa_bt_device_ensure_media_codec(this->bt_dev, codecs);
 		if (ret < 0) {
 			if (ret != -ENOTSUP)
 				spa_log_error(this->log, "failed to switch codec (%d), setting basic profile", ret);
@@ -873,6 +906,8 @@ static void codec_switched(void *userdata, int status)
 		spa_log_error(this->log, "failed to switch codec (%d), setting fallback profile", status);
 		if (this->profile == DEVICE_PROFILE_A2DP && this->props.codec != 0) {
 			this->props.codec = 0;
+		} else if (this->profile == DEVICE_PROFILE_BAP && this->props.codec != 0) {
+			this->props.codec = 0;
 		} else if (this->profile == DEVICE_PROFILE_HSP_HFP && this->props.codec != 0) {
 			this->props.codec = 0;
 		} else {
@@ -912,9 +947,9 @@ static void profiles_changed(void *userdata, uint32_t prev_profiles, uint32_t pr
 	if (this->switching_codec)
 		return;
 
-	if (this->bt_dev->connected_profiles & SPA_BT_PROFILE_A2DP_SINK) {
+	if (this->bt_dev->connected_profiles & SPA_BT_PROFILE_MEDIA_SINK) {
 		free(this->supported_codecs);
-		this->supported_codecs = spa_bt_device_get_supported_a2dp_codecs(
+		this->supported_codecs = spa_bt_device_get_supported_media_codecs(
 			this->bt_dev, &this->supported_codec_count, true);
 	}
 
@@ -925,16 +960,17 @@ static void profiles_changed(void *userdata, uint32_t prev_profiles, uint32_t pr
 		break;
 	case DEVICE_PROFILE_AG:
 		nodes_changed = (connected_change & (SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY |
-						     SPA_BT_PROFILE_A2DP_SOURCE));
+						     SPA_BT_PROFILE_MEDIA_SOURCE));
 		spa_log_debug(this->log, "profiles changed: AG nodes changed: %d",
 			      nodes_changed);
 		break;
 	case DEVICE_PROFILE_A2DP:
-		if (get_supported_a2dp_codec(this, this->props.codec, NULL) == NULL)
+	case DEVICE_PROFILE_BAP:
+		if (get_supported_media_codec(this, this->props.codec, NULL) == NULL)
 			this->props.codec = 0;
-		nodes_changed = (connected_change & (SPA_BT_PROFILE_A2DP_SINK |
-						     SPA_BT_PROFILE_A2DP_SOURCE));
-		spa_log_debug(this->log, "profiles changed: A2DP nodes changed: %d",
+		nodes_changed = (connected_change & (SPA_BT_PROFILE_MEDIA_SINK |
+						     SPA_BT_PROFILE_MEDIA_SOURCE));
+		spa_log_debug(this->log, "profiles changed: media nodes changed: %d",
 			      nodes_changed);
 		break;
 	case DEVICE_PROFILE_HSP_HFP:
@@ -1019,15 +1055,16 @@ static uint32_t profile_direction_mask(struct impl *this, uint32_t index, enum s
 	struct spa_bt_device *device = this->bt_dev;
 	uint32_t mask;
 	bool have_output = false, have_input = false;
-	const struct a2dp_codec *a2dp_codec;
+	const struct media_codec *media_codec;
 
 	switch (index) {
 	case DEVICE_PROFILE_A2DP:
-		if (device->connected_profiles & SPA_BT_PROFILE_A2DP_SINK)
+	case DEVICE_PROFILE_BAP:
+		if (device->connected_profiles & SPA_BT_PROFILE_MEDIA_SINK)
 			have_output = true;
 
-		a2dp_codec = get_supported_a2dp_codec(this, codec, NULL);
-		if (a2dp_codec && a2dp_codec->duplex_codec)
+		media_codec = get_supported_media_codec(this, codec, NULL);
+		if (media_codec && media_codec->duplex_codec)
 			have_input = true;
 		break;
 	case DEVICE_PROFILE_HSP_HFP:
@@ -1056,19 +1093,27 @@ static uint32_t get_profile_from_index(struct impl *this, uint32_t index, uint32
 	*codec = 0;
 	*next = index + 1;
 
-	if (index <= 3) {
+	if (index <= DEVICE_PROFILE_LAST) {
 		return index;
 	} else if (index != SPA_ID_INVALID) {
 		const struct spa_type_info *info;
+		uint32_t profile;
 
-		*codec = index - 3;
+		*codec = index - DEVICE_PROFILE_LAST;
 		*next = SPA_ID_INVALID;
 
 		for (info = spa_type_bluetooth_audio_codec; info->type; ++info)
 			if (info->type > *codec)
-				*next = SPA_MIN(info->type + 3, *next);
+				*next = SPA_MIN(info->type + DEVICE_PROFILE_LAST, *next);
 
-		return get_hfp_codec(*codec) ? DEVICE_PROFILE_HSP_HFP : DEVICE_PROFILE_A2DP;
+		if (get_hfp_codec(*codec))
+			profile = DEVICE_PROFILE_HSP_HFP;
+		else if (*codec == SPA_BLUETOOTH_AUDIO_CODEC_LC3)
+			profile = DEVICE_PROFILE_BAP;
+		else
+			profile = DEVICE_PROFILE_A2DP;
+
+		return profile;
 	}
 
 	*next = SPA_ID_INVALID;
@@ -1080,18 +1125,18 @@ static uint32_t get_index_from_profile(struct impl *this, uint32_t profile, enum
 	if (profile == DEVICE_PROFILE_OFF || profile == DEVICE_PROFILE_AG)
 		return profile;
 
-	if (profile == DEVICE_PROFILE_A2DP) {
-		if (codec == 0 || (this->bt_dev->connected_profiles & SPA_BT_PROFILE_A2DP_SOURCE))
+	if (profile == DEVICE_PROFILE_A2DP || profile == DEVICE_PROFILE_BAP) {
+		if (codec == 0 || (this->bt_dev->connected_profiles & SPA_BT_PROFILE_MEDIA_SOURCE))
 			return profile;
 
-		return codec + 3;
+		return codec + DEVICE_PROFILE_LAST;
 	}
 
 	if (profile == DEVICE_PROFILE_HSP_HFP) {
 		if (codec == 0 || (this->bt_dev->connected_profiles & SPA_BT_PROFILE_HFP_AG))
 			return profile;
 
-		return codec + 3;
+		return codec + DEVICE_PROFILE_LAST;
 	}
 
 	return SPA_ID_INVALID;
@@ -1129,11 +1174,11 @@ 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->supported_codecs = spa_bt_device_get_supported_media_codecs(
 		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 */
+	/* Prefer BAP, then A2DP, then HFP, then null, but select AG if the device
+	   appears not to have BAP_SINK, A2DP_SINK or any HEAD_UNIT profile */
 
 	/* If default profile is set to HSP/HFP, first try those and exit if found. */
 	if (this->bt_dev->settings != NULL) {
@@ -1144,16 +1189,20 @@ static void set_initial_profile(struct impl *this)
 			return;
 	}
 
-	for (i = SPA_BT_PROFILE_A2DP_SINK; i <= SPA_BT_PROFILE_A2DP_SOURCE; i <<= 1) {
+	for (i = SPA_BT_PROFILE_BAP_SINK; i <= SPA_BT_PROFILE_A2DP_SOURCE; i <<= 1) {
 		if (!(this->bt_dev->connected_profiles & i))
 			continue;
 
 		t = find_transport(this, i, 0);
 		if (t) {
-			this->profile = (i == SPA_BT_PROFILE_A2DP_SOURCE) ?
-				DEVICE_PROFILE_AG : DEVICE_PROFILE_A2DP;
-			this->props.codec = t->a2dp_codec->id;
-			spa_log_debug(this->log, "initial profile A2DP profile:%d codec:%d",
+			if (i == SPA_BT_PROFILE_A2DP_SOURCE || i == SPA_BT_PROFILE_BAP_SOURCE)
+				this->profile =  DEVICE_PROFILE_AG;
+			else if (i == SPA_BT_PROFILE_BAP_SINK)
+				this->profile =  DEVICE_PROFILE_BAP;
+			else
+				this->profile =  DEVICE_PROFILE_A2DP;
+			this->props.codec = t->media_codec->id;
+			spa_log_debug(this->log, "initial profile media profile:%d codec:%d",
 					this->profile, this->props.codec);
 			return;
 		}
@@ -1213,19 +1262,19 @@ static struct spa_pod *build_profile(struct impl *this, struct spa_pod_builder *
 		n_sink++;
 		if (codec) {
 			size_t idx;
-			const struct a2dp_codec *a2dp_codec = get_supported_a2dp_codec(this, codec, &idx);
-			if (a2dp_codec == NULL) {
+			const struct media_codec *media_codec = get_supported_media_codec(this, codec, &idx);
+			if (media_codec == NULL) {
 				errno = EINVAL;
 				return NULL;
 			}
-			name_and_codec = spa_aprintf("%s-%s", name, a2dp_codec->name);
+			name_and_codec = spa_aprintf("%s-%s", name, media_codec->name);
 			name = name_and_codec;
-			if (profile == SPA_BT_PROFILE_A2DP_SINK && !a2dp_codec->duplex_codec) {
+			if (profile == SPA_BT_PROFILE_A2DP_SINK && !media_codec->duplex_codec) {
 				desc_and_codec = spa_aprintf(_("High Fidelity Playback (A2DP Sink, codec %s)"),
-							     a2dp_codec->description);
+							     media_codec->description);
 			} else {
 				desc_and_codec = spa_aprintf(_("High Fidelity Duplex (A2DP Source/Sink, codec %s)"),
-							     a2dp_codec->description);
+							     media_codec->description);
 
 			}
 			desc = desc_and_codec;
@@ -1240,6 +1289,52 @@ static struct spa_pod *build_profile(struct impl *this, struct spa_pod_builder *
 		}
 		break;
 	}
+	case DEVICE_PROFILE_BAP:
+	{
+		uint32_t profile = device->connected_profiles &
+		      (SPA_BT_PROFILE_BAP_SINK | SPA_BT_PROFILE_BAP_SOURCE);
+		size_t idx;
+		const struct media_codec *media_codec;
+
+		if (profile == 0)
+			return NULL;
+
+		if (!codec) {
+			errno = EINVAL;
+			return NULL;
+		}
+
+		if (profile & (SPA_BT_PROFILE_BAP_SINK))
+			n_sink++;
+		if (profile & (SPA_BT_PROFILE_BAP_SOURCE))
+			n_source++;
+
+		name = spa_bt_profile_name(profile);
+
+		media_codec = get_supported_media_codec(this, codec, &idx);
+		if (media_codec == NULL) {
+			errno = EINVAL;
+			return NULL;
+		}
+		name_and_codec = spa_aprintf("%s-%s", name, media_codec->name);
+		name = name_and_codec;
+		switch (profile) {
+		case SPA_BT_PROFILE_BAP_SINK:
+			desc_and_codec = spa_aprintf(_("High Fidelity Playback (BAP Sink, codec %s)"),
+						     media_codec->description);
+			break;
+		case SPA_BT_PROFILE_BAP_SOURCE:
+			desc_and_codec = spa_aprintf(_("High Fidelity Input (BAP Source, codec %s)"),
+						     media_codec->description);
+			break;
+		default:
+			desc_and_codec = spa_aprintf(_("High Fidelity Duplex (BAP Source/Sink, codec %s)"),
+						     media_codec->description);
+		}
+		desc = desc_and_codec;
+		priority = 128 + this->supported_codec_count - idx;  /* order as in codec list */
+		break;
+	}
 	case DEVICE_PROFILE_HSP_HFP:
 	{
 		/* make this device profile visible only if there is a head unit */
@@ -1537,7 +1632,8 @@ static struct spa_pod *build_route(struct impl *this, struct spa_pod_builder *b,
 		spa_pod_builder_array(b, sizeof(uint32_t), SPA_TYPE_Id,
 				node->n_channels, node->channels);
 
-		if (this->profile == DEVICE_PROFILE_A2DP && dev == DEVICE_ID_SINK) {
+		if ((this->profile == DEVICE_PROFILE_A2DP || this->profile == DEVICE_PROFILE_BAP) &&
+		    dev == DEVICE_ID_SINK) {
 			spa_pod_builder_prop(b, SPA_PROP_latencyOffsetNsec, 0);
 			spa_pod_builder_long(b, node->latency_offset);
 		}
@@ -1560,7 +1656,7 @@ static struct spa_pod *build_route(struct impl *this, struct spa_pod_builder *b,
 	return spa_pod_builder_pop(b, &f[0]);
 }
 
-static bool iterate_supported_a2dp_codecs(struct impl *this, int *j, const struct a2dp_codec **codec)
+static bool iterate_supported_media_codecs(struct impl *this, int *j, const struct media_codec **codec)
 {
 	int i;
 
@@ -1582,12 +1678,12 @@ static struct spa_pod *build_prop_info(struct impl *this, struct spa_pod_builder
 {
 	struct spa_pod_frame f[2];
 	struct spa_pod_choice *choice;
-	const struct a2dp_codec *codec;
+	const struct media_codec *codec;
 	size_t n;
 	int j;
 
-#define FOR_EACH_A2DP_CODEC(j, codec) \
-		for (j = -1; iterate_supported_a2dp_codecs(this, &j, &codec);)
+#define FOR_EACH_MEDIA_CODEC(j, codec) \
+		for (j = -1; iterate_supported_media_codecs(this, &j, &codec);)
 #define FOR_EACH_HFP_CODEC(j) \
 		for (j = HFP_AUDIO_CODEC_MSBC; j >= HFP_AUDIO_CODEC_CVSD; --j) \
 			if (spa_bt_device_supports_hfp_codec(this->bt_dev, j) == 1)
@@ -1608,8 +1704,8 @@ static struct spa_pod *build_prop_info(struct impl *this, struct spa_pod_builder
 	spa_pod_builder_push_choice(b, &f[1], SPA_CHOICE_Enum, 0);
 	choice = (struct spa_pod_choice *)spa_pod_builder_frame(b, &f[1]);
 	n = 0;
-	if (this->profile == DEVICE_PROFILE_A2DP) {
-		FOR_EACH_A2DP_CODEC(j, codec) {
+	if (this->profile == DEVICE_PROFILE_A2DP || this->profile == DEVICE_PROFILE_BAP) {
+		FOR_EACH_MEDIA_CODEC(j, codec) {
 			if (n == 0)
 				spa_pod_builder_int(b, codec->id);
 			spa_pod_builder_int(b, codec->id);
@@ -1628,8 +1724,8 @@ static struct spa_pod *build_prop_info(struct impl *this, struct spa_pod_builder
 	spa_pod_builder_pop(b, &f[1]);
 	spa_pod_builder_prop(b, SPA_PROP_INFO_labels, 0);
 	spa_pod_builder_push_struct(b, &f[1]);
-	if (this->profile == DEVICE_PROFILE_A2DP) {
-		FOR_EACH_A2DP_CODEC(j, codec) {
+	if (this->profile == DEVICE_PROFILE_A2DP || this->profile == DEVICE_PROFILE_BAP) {
+		FOR_EACH_MEDIA_CODEC(j, codec) {
 			spa_pod_builder_int(b, codec->id);
 			spa_pod_builder_string(b, codec->description);
 		}
@@ -1642,7 +1738,7 @@ static struct spa_pod *build_prop_info(struct impl *this, struct spa_pod_builder
 	spa_pod_builder_pop(b, &f[1]);
 	return spa_pod_builder_pop(b, &f[0]);
 
-#undef FOR_EACH_A2DP_CODEC
+#undef FOR_EACH_MEDIA_CODEC
 #undef FOR_EACH_HFP_CODEC
 }
 
@@ -1688,6 +1784,7 @@ static int impl_enum_params(void *object, int seq,
 		case DEVICE_PROFILE_OFF:
 		case DEVICE_PROFILE_AG:
 		case DEVICE_PROFILE_A2DP:
+		case DEVICE_PROFILE_BAP:
 		case DEVICE_PROFILE_HSP_HFP:
 			param = build_profile(this, &b, id, result.index, profile, codec, false);
 			if (param == NULL)
@@ -2024,7 +2121,7 @@ static int impl_set_param(void *object,
 		if (codec_id == SPA_ID_INVALID)
 			return 0;
 
-		if (this->profile == DEVICE_PROFILE_A2DP) {
+		if (this->profile == DEVICE_PROFILE_A2DP || this->profile == DEVICE_PROFILE_BAP) {
 			size_t j;
 			for (j = 0; j < this->supported_codec_count; ++j) {
 				if (this->supported_codecs[j]->id == codec_id) {
diff --git a/spa/plugins/bluez5/codec-loader.c b/spa/plugins/bluez5/codec-loader.c
index 172c42309455b473ad22924c1de7de227036d291..f8363f8880a76e6d69589fa40135831146cfca19 100644
--- a/spa/plugins/bluez5/codec-loader.c
+++ b/spa/plugins/bluez5/codec-loader.c
@@ -29,7 +29,7 @@
 #include "defs.h"
 #include "codec-loader.h"
 
-#define A2DP_CODEC_LIB_BASE	"bluez5/libspa-codec-bluez5-"
+#define MEDIA_CODEC_LIB_BASE	"bluez5/libspa-codec-bluez5-"
 
 /* AVDTP allows 0x3E endpoints, can't have more codecs than that */
 #define MAX_CODECS	0x3E
@@ -40,7 +40,7 @@ static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.codecs");
 #define SPA_LOG_TOPIC_DEFAULT &log_topic
 
 struct impl {
-	const struct a2dp_codec *codecs[MAX_CODECS + 1];
+	const struct media_codec *codecs[MAX_CODECS + 1];
 	struct spa_handle *handles[MAX_HANDLES];
 	size_t n_codecs;
 	size_t n_handles;
@@ -48,9 +48,10 @@ struct impl {
 	struct spa_log *log;
 };
 
-static int codec_order(const struct a2dp_codec *c)
+static int codec_order(const struct media_codec *c)
 {
 	static const enum spa_bluetooth_audio_codec order[] = {
+		SPA_BLUETOOTH_AUDIO_CODEC_LC3,
 		SPA_BLUETOOTH_AUDIO_CODEC_LDAC,
 		SPA_BLUETOOTH_AUDIO_CODEC_APTX_HD,
 		SPA_BLUETOOTH_AUDIO_CODEC_APTX,
@@ -78,8 +79,8 @@ static int codec_order(const struct a2dp_codec *c)
 
 static int codec_order_cmp(const void *a, const void *b)
 {
-	const struct a2dp_codec * const *ca = a;
-	const struct a2dp_codec * const *cb = b;
+	const struct media_codec * const *ca = a;
+	const struct media_codec * const *cb = b;
 	int ia = codec_order(*ca);
 	int ib = codec_order(*cb);
 	if (*ca == *cb)
@@ -87,7 +88,7 @@ static int codec_order_cmp(const void *a, const void *b)
 	return (ia == ib) ? (*ca < *cb ? -1 : 1) : ia - ib;
 }
 
-static int load_a2dp_codecs_from(struct impl *impl, const char *factory_name, const char *libname)
+static int load_media_codecs_from(struct impl *impl, const char *factory_name, const char *libname)
 {
 	struct spa_handle *handle = NULL;
 	void *iface;
@@ -108,7 +109,7 @@ static int load_a2dp_codecs_from(struct impl *impl, const char *factory_name, co
 
 	spa_log_debug(impl->log, "loading codecs from %s", factory_name);
 
-	if ((res = spa_handle_get_interface(handle, SPA_TYPE_INTERFACE_Bluez5CodecA2DP, &iface)) < 0) {
+	if ((res = spa_handle_get_interface(handle, SPA_TYPE_INTERFACE_Bluez5CodecMedia, &iface)) < 0) {
 		spa_log_warn(impl->log, "Bluetooth codec plugin %s has no codec interface",
 				factory_name);
 		goto fail;
@@ -116,17 +117,21 @@ static int load_a2dp_codecs_from(struct impl *impl, const char *factory_name, co
 
 	bluez5_codec_a2dp = iface;
 
-	if (bluez5_codec_a2dp->iface.version != SPA_VERSION_BLUEZ5_CODEC_A2DP) {
+	if (bluez5_codec_a2dp->iface.version != SPA_VERSION_BLUEZ5_CODEC_MEDIA) {
 		spa_log_warn(impl->log, "codec plugin %s has incompatible ABI version (%d != %d)",
-				factory_name, bluez5_codec_a2dp->iface.version, SPA_VERSION_BLUEZ5_CODEC_A2DP);
+				factory_name, bluez5_codec_a2dp->iface.version, SPA_VERSION_BLUEZ5_CODEC_MEDIA);
 		res = -ENOENT;
 		goto fail;
 	}
 
 	for (i = 0; bluez5_codec_a2dp->codecs[i]; ++i) {
-		const struct a2dp_codec *c = bluez5_codec_a2dp->codecs[i];
+		const struct media_codec *c = bluez5_codec_a2dp->codecs[i];
+		const char *ep = c->endpoint_name ? c->endpoint_name : c->name;
 		size_t j;
 
+		if (!ep)
+			goto next_codec;
+
 		if (impl->n_codecs >= MAX_CODECS) {
 			spa_log_error(impl->log, "too many A2DP codecs");
 			break;
@@ -134,14 +139,17 @@ static int load_a2dp_codecs_from(struct impl *impl, const char *factory_name, co
 
 		/* Don't load duplicate endpoints */
 		for (j = 0; j < impl->n_codecs; ++j) {
-			const struct a2dp_codec *c2 = impl->codecs[j];
-			const char *ep1 = c->endpoint_name ? c->endpoint_name : c->name;
+			const struct media_codec *c2 = impl->codecs[j];
 			const char *ep2 = c2->endpoint_name ? c2->endpoint_name : c2->name;
-			if (spa_streq(ep1, ep2))
+			if (spa_streq(ep, ep2) && c->fill_caps && c2->fill_caps) {
+				spa_log_debug(impl->log, "media codec %s from %s duplicate endpoint %s",
+						c->name, factory_name, ep);
 				goto next_codec;
+			}
 		}
 
-		spa_log_debug(impl->log, "loaded A2DP codec %s from %s", c->name, factory_name);
+		spa_log_debug(impl->log, "loaded media codec %s from %s, endpoint:%s",
+				c->name, factory_name, ep);
 
 		if (c->set_log)
 			c->set_log(impl->log);
@@ -166,22 +174,23 @@ fail:
 	return res;
 }
 
-const struct a2dp_codec * const *load_a2dp_codecs(struct spa_plugin_loader *loader, struct spa_log *log)
+const struct media_codec * const *load_media_codecs(struct spa_plugin_loader *loader, struct spa_log *log)
 {
 	struct impl *impl;
 	bool has_sbc;
 	size_t i;
 	const struct { const char *factory; const char *lib; } plugins[] = {
-#define A2DP_CODEC_FACTORY_LIB(basename) \
-		{ A2DP_CODEC_FACTORY_NAME(basename), A2DP_CODEC_LIB_BASE basename }
-		A2DP_CODEC_FACTORY_LIB("aac"),
-		A2DP_CODEC_FACTORY_LIB("aptx"),
-		A2DP_CODEC_FACTORY_LIB("faststream"),
-		A2DP_CODEC_FACTORY_LIB("ldac"),
-		A2DP_CODEC_FACTORY_LIB("sbc"),
-		A2DP_CODEC_FACTORY_LIB("lc3plus"),
-		A2DP_CODEC_FACTORY_LIB("opus")
-#undef A2DP_CODEC_FACTORY_LIB
+#define MEDIA_CODEC_FACTORY_LIB(basename) \
+		{ MEDIA_CODEC_FACTORY_NAME(basename), MEDIA_CODEC_LIB_BASE basename }
+		MEDIA_CODEC_FACTORY_LIB("aac"),
+		MEDIA_CODEC_FACTORY_LIB("aptx"),
+		MEDIA_CODEC_FACTORY_LIB("faststream"),
+		MEDIA_CODEC_FACTORY_LIB("ldac"),
+		MEDIA_CODEC_FACTORY_LIB("sbc"),
+		MEDIA_CODEC_FACTORY_LIB("lc3plus"),
+		MEDIA_CODEC_FACTORY_LIB("opus"),
+		MEDIA_CODEC_FACTORY_LIB("lc3")
+#undef MEDIA_CODEC_FACTORY_LIB
 	};
 
 	impl = calloc(sizeof(struct impl), 1);
@@ -194,7 +203,7 @@ const struct a2dp_codec * const *load_a2dp_codecs(struct spa_plugin_loader *load
 	spa_log_topic_init(impl->log, &log_topic);
 
 	for (i = 0; i < SPA_N_ELEMENTS(plugins); ++i)
-		load_a2dp_codecs_from(impl, plugins[i].factory, plugins[i].lib);
+		load_media_codecs_from(impl, plugins[i].factory, plugins[i].lib);
 
 	has_sbc = false;
 	for (i = 0; i < impl->n_codecs; ++i)
@@ -203,19 +212,19 @@ const struct a2dp_codec * const *load_a2dp_codecs(struct spa_plugin_loader *load
 
 	if (!has_sbc) {
 		spa_log_error(impl->log, "failed to load A2DP SBC codec from plugins");
-		free_a2dp_codecs(impl->codecs);
+		free_media_codecs(impl->codecs);
 		errno = ENOENT;
 		return NULL;
 	}
 
-	qsort(impl->codecs, impl->n_codecs, sizeof(const struct a2dp_codec *), codec_order_cmp);
+	qsort(impl->codecs, impl->n_codecs, sizeof(const struct media_codec *), codec_order_cmp);
 
 	return impl->codecs;
 }
 
-void free_a2dp_codecs(const struct a2dp_codec * const *a2dp_codecs)
+void free_media_codecs(const struct media_codec * const *media_codecs)
 {
-	struct impl *impl = SPA_CONTAINER_OF(a2dp_codecs, struct impl, codecs);
+	struct impl *impl = SPA_CONTAINER_OF(media_codecs, struct impl, codecs);
 	size_t i;
 
 	for (i = 0; i < impl->n_handles; ++i)
diff --git a/spa/plugins/bluez5/codec-loader.h b/spa/plugins/bluez5/codec-loader.h
index 5422cc45802cab2b04cd8c4c7786eb6d69c8dad2..b77d9c49f4b0ef696198291e2fd0b5fd07784132 100644
--- a/spa/plugins/bluez5/codec-loader.h
+++ b/spa/plugins/bluez5/codec-loader.h
@@ -31,9 +31,9 @@
 #include <spa/support/plugin-loader.h>
 
 #include "a2dp-codec-caps.h"
-#include "a2dp-codecs.h"
+#include "media-codecs.h"
 
-const struct a2dp_codec * const *load_a2dp_codecs(struct spa_plugin_loader *loader, struct spa_log *log);
-void free_a2dp_codecs(const struct a2dp_codec * const *a2dp_codecs);
+const struct media_codec * const *load_media_codecs(struct spa_plugin_loader *loader, struct spa_log *log);
+void free_media_codecs(const struct media_codec * const *media_codecs);
 
 #endif
diff --git a/spa/plugins/bluez5/defs.h b/spa/plugins/bluez5/defs.h
index 90348afa05f1805507adb52451241fce75105a46..5d194b3d6293d901147c885f7a3b3015c7f89cec 100644
--- a/spa/plugins/bluez5/defs.h
+++ b/spa/plugins/bluez5/defs.h
@@ -141,6 +141,9 @@ extern "C" {
 #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_PACS        "00001850-0000-1000-8000-00805f9b34fb"
+#define SPA_BT_UUID_BAP_SINK    "00002bc9-0000-1000-8000-00805f9b34fb"
+#define SPA_BT_UUID_BAP_SOURCE  "00002bcb-0000-1000-8000-00805f9b34fb"
 
 #define PROFILE_HSP_AG	"/Profile/HSPAG"
 #define PROFILE_HSP_HS	"/Profile/HSPHS"
@@ -158,9 +161,12 @@ extern "C" {
 #define HFP_AUDIO_CODEC_CVSD    0x01
 #define HFP_AUDIO_CODEC_MSBC    0x02
 
-#define A2DP_OBJECT_MANAGER_PATH "/MediaEndpoint"
-#define A2DP_SINK_ENDPOINT	A2DP_OBJECT_MANAGER_PATH "/A2DPSink"
-#define A2DP_SOURCE_ENDPOINT	A2DP_OBJECT_MANAGER_PATH "/A2DPSource"
+#define MEDIA_OBJECT_MANAGER_PATH "/MediaEndpoint"
+#define A2DP_SINK_ENDPOINT	MEDIA_OBJECT_MANAGER_PATH "/A2DPSink"
+#define A2DP_SOURCE_ENDPOINT	MEDIA_OBJECT_MANAGER_PATH "/A2DPSource"
+
+#define BAP_SINK_ENDPOINT	MEDIA_OBJECT_MANAGER_PATH "/BAPSink"
+#define BAP_SOURCE_ENDPOINT	MEDIA_OBJECT_MANAGER_PATH "/BAPSource"
 
 #define SPA_BT_UNKNOWN_DELAY			0
 
@@ -172,19 +178,30 @@ extern "C" {
 #define MSBC_ENCODED_SIZE       60  /* 2 bytes header + 57 mSBC payload + 1 byte padding */
 #define MSBC_PAYLOAD_SIZE       57
 
+enum spa_bt_media_direction {
+	SPA_BT_MEDIA_SOURCE,
+	SPA_BT_MEDIA_SINK,
+};
+
 enum spa_bt_profile {
 	SPA_BT_PROFILE_NULL =		0,
-	SPA_BT_PROFILE_A2DP_SINK =	(1 << 0),
-	SPA_BT_PROFILE_A2DP_SOURCE =	(1 << 1),
-	SPA_BT_PROFILE_HSP_HS =		(1 << 2),
-	SPA_BT_PROFILE_HSP_AG =		(1 << 3),
-	SPA_BT_PROFILE_HFP_HF =		(1 << 4),
-	SPA_BT_PROFILE_HFP_AG =		(1 << 5),
+	SPA_BT_PROFILE_BAP_SINK =	(1 << 0),
+	SPA_BT_PROFILE_BAP_SOURCE =	(1 << 1),
+	SPA_BT_PROFILE_A2DP_SINK =	(1 << 2),
+	SPA_BT_PROFILE_A2DP_SOURCE =	(1 << 3),
+	SPA_BT_PROFILE_HSP_HS =		(1 << 4),
+	SPA_BT_PROFILE_HSP_AG =		(1 << 5),
+	SPA_BT_PROFILE_HFP_HF =		(1 << 6),
+	SPA_BT_PROFILE_HFP_AG =		(1 << 7),
 
 	SPA_BT_PROFILE_A2DP_DUPLEX =	(SPA_BT_PROFILE_A2DP_SINK | SPA_BT_PROFILE_A2DP_SOURCE),
+	SPA_BT_PROFILE_BAP_DUPLEX =     (SPA_BT_PROFILE_BAP_SINK | SPA_BT_PROFILE_BAP_SOURCE),
 	SPA_BT_PROFILE_HEADSET_HEAD_UNIT = (SPA_BT_PROFILE_HSP_HS | SPA_BT_PROFILE_HFP_HF),
 	SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY = (SPA_BT_PROFILE_HSP_AG | SPA_BT_PROFILE_HFP_AG),
 	SPA_BT_PROFILE_HEADSET_AUDIO =  (SPA_BT_PROFILE_HEADSET_HEAD_UNIT | SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY),
+
+	SPA_BT_PROFILE_MEDIA_SINK =		(SPA_BT_PROFILE_A2DP_SINK | SPA_BT_PROFILE_BAP_SINK),
+	SPA_BT_PROFILE_MEDIA_SOURCE =	(SPA_BT_PROFILE_A2DP_SOURCE | SPA_BT_PROFILE_BAP_SOURCE),
 };
 
 static inline enum spa_bt_profile spa_bt_profile_from_uuid(const char *uuid)
@@ -203,6 +220,10 @@ static inline enum spa_bt_profile spa_bt_profile_from_uuid(const char *uuid)
 		return SPA_BT_PROFILE_HFP_HF;
 	else if (strcasecmp(uuid, SPA_BT_UUID_HFP_AG) == 0)
 		return SPA_BT_PROFILE_HFP_AG;
+	else if (strcasecmp(uuid, SPA_BT_UUID_BAP_SINK) == 0)
+		return SPA_BT_PROFILE_BAP_SINK;
+	else if (strcasecmp(uuid, SPA_BT_UUID_BAP_SOURCE) == 0)
+		return SPA_BT_PROFILE_BAP_SOURCE;
 	else
 		return 0;
 }
@@ -303,6 +324,12 @@ static inline const char *spa_bt_profile_name (enum spa_bt_profile profile) {
 	return "headset-audio-gateway";
       case SPA_BT_PROFILE_HEADSET_AUDIO:
 	return "headset-audio";
+      case SPA_BT_PROFILE_BAP_SOURCE:
+        return "bap-source";
+      case SPA_BT_PROFILE_BAP_SINK:
+        return "bap-sink";
+      case SPA_BT_PROFILE_BAP_DUPLEX:
+        return "bap-duplex";
       default:
         break;
       }
@@ -412,7 +439,7 @@ static inline enum spa_bt_form_factor spa_bt_form_factor_from_class(uint32_t blu
 	return SPA_BT_FORM_FACTOR_UNKNOWN;
 }
 
-struct spa_bt_a2dp_codec_switch;
+struct spa_bt_media_codec_switch;
 struct spa_bt_transport;
 
 struct spa_bt_device_events {
@@ -432,6 +459,8 @@ struct spa_bt_device_events {
 	void (*destroy) (void *data);
 };
 
+struct media_codec;
+
 struct spa_bt_device {
 	struct spa_list link;
 	struct spa_bt_monitor *monitor;
@@ -480,18 +509,18 @@ struct spa_bt_device {
 	const struct spa_dict *settings;
 
 	DBusPendingCall *battery_pending_call;
-};
 
-struct a2dp_codec;
+	const struct media_codec *preferred_codec;
+};
 
 struct spa_bt_device *spa_bt_device_find(struct spa_bt_monitor *monitor, const char *path);
 struct spa_bt_device *spa_bt_device_find_by_address(struct spa_bt_monitor *monitor, const char *remote_address, const char *local_address);
 int spa_bt_device_add_profile(struct spa_bt_device *device, enum spa_bt_profile 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, 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_media_codec(struct spa_bt_device *device, const struct media_codec * const *codecs);
+bool spa_bt_device_supports_media_codec(struct spa_bt_device *device, const struct media_codec *codec, bool sink);
+const struct media_codec **spa_bt_device_get_supported_media_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);
@@ -570,11 +599,13 @@ struct spa_bt_transport {
 	struct spa_list device_link;
 	enum spa_bt_profile profile;
 	enum spa_bt_transport_state state;
-	const struct a2dp_codec *a2dp_codec;
+	const struct media_codec *media_codec;
 	unsigned int codec;
 	void *configuration;
 	int configuration_len;
 	char *endpoint_path;
+	bool bap_initiator;
+	struct spa_list bap_transport_linked;
 
 	uint32_t n_channels;
 	uint32_t channels[64];
diff --git a/spa/plugins/bluez5/a2dp-codecs.c b/spa/plugins/bluez5/media-codecs.c
similarity index 89%
rename from spa/plugins/bluez5/a2dp-codecs.c
rename to spa/plugins/bluez5/media-codecs.c
index 42c7a193c1a759b6ecd5892928157a5d4b968e68..6f64e82e403ea6b6ddbc24188c3b0d3c8f9abf6d 100644
--- a/spa/plugins/bluez5/a2dp-codecs.c
+++ b/spa/plugins/bluez5/media-codecs.c
@@ -10,9 +10,9 @@
 
 #include <spa/utils/string.h>
 
-#include "a2dp-codecs.h"
+#include "media-codecs.h"
 
-int a2dp_codec_select_config(const struct a2dp_codec_config configs[], size_t n,
+int media_codec_select_config(const struct media_codec_config configs[], size_t n,
 			     uint32_t cap, int preferred_value)
 {
 	size_t i;
@@ -60,9 +60,9 @@ int a2dp_codec_select_config(const struct a2dp_codec_config configs[], size_t n,
 	return res;
 }
 
-bool a2dp_codec_check_caps(const struct a2dp_codec *codec, unsigned int codec_id,
+bool media_codec_check_caps(const struct media_codec *codec, unsigned int codec_id,
 			   const void *caps, size_t caps_size,
-			   const struct a2dp_codec_audio_info *info,
+			   const struct media_codec_audio_info *info,
 			   const struct spa_dict *global_settings)
 {
 	uint8_t config[A2DP_MAX_CAPS_SIZE];
@@ -98,7 +98,7 @@ impl_get_interface(struct spa_handle *handle, const char *type, void **interface
 
 	this = (struct impl *) handle;
 
-	if (spa_streq(type, SPA_TYPE_INTERFACE_Bluez5CodecA2DP))
+	if (spa_streq(type, SPA_TYPE_INTERFACE_Bluez5CodecMedia))
 		*interface = &this->bluez5_codec_a2dp;
 	else
 		return -ENOENT;
@@ -136,10 +136,10 @@ impl_init(const struct spa_handle_factory *factory,
 
 	this = (struct impl *) handle;
 
-	this->bluez5_codec_a2dp.codecs = codec_plugin_a2dp_codecs;
+	this->bluez5_codec_a2dp.codecs = codec_plugin_media_codecs;
 	this->bluez5_codec_a2dp.iface = SPA_INTERFACE_INIT(
-		SPA_TYPE_INTERFACE_Bluez5CodecA2DP,
-		SPA_VERSION_BLUEZ5_CODEC_A2DP,
+		SPA_TYPE_INTERFACE_Bluez5CodecMedia,
+		SPA_VERSION_BLUEZ5_CODEC_MEDIA,
 		NULL,
 		this);
 
@@ -147,7 +147,7 @@ impl_init(const struct spa_handle_factory *factory,
 }
 
 static const struct spa_interface_info impl_interfaces[] = {
-        {SPA_TYPE_INTERFACE_Bluez5CodecA2DP,},
+        {SPA_TYPE_INTERFACE_Bluez5CodecMedia,},
 };
 
 static int
diff --git a/spa/plugins/bluez5/a2dp-codecs.h b/spa/plugins/bluez5/media-codecs.h
similarity index 64%
rename from spa/plugins/bluez5/a2dp-codecs.h
rename to spa/plugins/bluez5/media-codecs.h
index 7fb5cd510611b6c48591b97d822d58649955633b..d3447db055071a42d07c98a651a991bc0d0090e3 100644
--- a/spa/plugins/bluez5/a2dp-codecs.h
+++ b/spa/plugins/bluez5/media-codecs.h
@@ -36,34 +36,35 @@
 #include <spa/support/log.h>
 
 #include "a2dp-codec-caps.h"
+#include "bap-codec-caps.h"
 
 /*
  * The codec plugin SPA interface is private.  The version should be incremented
  * when any of the structs or semantics change.
  */
 
-#define SPA_TYPE_INTERFACE_Bluez5CodecA2DP	SPA_TYPE_INFO_INTERFACE_BASE "Bluez5:Codec:A2DP:Private"
+#define SPA_TYPE_INTERFACE_Bluez5CodecMedia	SPA_TYPE_INFO_INTERFACE_BASE "Bluez5:Codec:Media:Private"
 
-#define SPA_VERSION_BLUEZ5_CODEC_A2DP		5
+#define SPA_VERSION_BLUEZ5_CODEC_MEDIA		7
 
 struct spa_bluez5_codec_a2dp {
 	struct spa_interface iface;
-	const struct a2dp_codec * const *codecs;	/**< NULL terminated array */
+	const struct media_codec * const *codecs;	/**< NULL terminated array */
 };
 
-#define A2DP_CODEC_FACTORY_NAME(basename)		(SPA_NAME_API_CODEC_BLUEZ5_A2DP "." basename)
+#define MEDIA_CODEC_FACTORY_NAME(basename)		(SPA_NAME_API_CODEC_BLUEZ5_MEDIA "." basename)
 
 #ifdef CODEC_PLUGIN
-#define A2DP_CODEC_EXPORT_DEF(basename,...)	\
-	const char *codec_plugin_factory_name = A2DP_CODEC_FACTORY_NAME(basename); \
-	static const struct a2dp_codec * const codec_plugin_a2dp_codec_list[] = { __VA_ARGS__, NULL };	\
-	const struct a2dp_codec * const * const codec_plugin_a2dp_codecs = codec_plugin_a2dp_codec_list;
+#define MEDIA_CODEC_EXPORT_DEF(basename,...)	\
+	const char *codec_plugin_factory_name = MEDIA_CODEC_FACTORY_NAME(basename); \
+	static const struct media_codec * const codec_plugin_media_codec_list[] = { __VA_ARGS__, NULL };	\
+	const struct media_codec * const * const codec_plugin_media_codecs = codec_plugin_media_codec_list;
 
-extern const struct a2dp_codec * const * const codec_plugin_a2dp_codecs;
+extern const struct media_codec * const * const codec_plugin_media_codecs;
 extern const char *codec_plugin_factory_name;
 #endif
 
-#define A2DP_CODEC_FLAG_SINK		(1 << 0)
+#define MEDIA_CODEC_FLAG_SINK		(1 << 0)
 
 #define A2DP_CODEC_DEFAULT_RATE		48000
 #define A2DP_CODEC_DEFAULT_CHANNELS	2
@@ -74,16 +75,18 @@ enum {
 	NEED_FLUSH_FRAGMENT = 2,
 };
 
-struct a2dp_codec_audio_info {
+struct media_codec_audio_info {
 	uint32_t rate;
 	uint32_t channels;
 };
 
-struct a2dp_codec {
+struct media_codec {
 	enum spa_bluetooth_audio_codec id;
 	uint8_t codec_id;
 	a2dp_vendor_codec_t vendor;
 
+	bool bap;
+
 	const char *name;
 	const char *description;
 	const char *endpoint_name;	/**< Endpoint name. If NULL, same as name */
@@ -91,37 +94,45 @@ struct a2dp_codec {
 
 	const size_t send_buf_size;
 
-	const struct a2dp_codec *duplex_codec;	/**< Codec for non-standard A2DP duplex channel */
+	const struct media_codec *duplex_codec;	/**< Codec for non-standard A2DP duplex channel */
+
+	struct spa_log *log;
 
-	int (*fill_caps) (const struct a2dp_codec *codec, uint32_t flags,
+	/** If fill_caps is NULL, no endpoint is registered (for sharing with another codec). */
+	int (*fill_caps) (const struct media_codec *codec, uint32_t flags,
 			uint8_t caps[A2DP_MAX_CAPS_SIZE]);
-	int (*select_config) (const struct a2dp_codec *codec, uint32_t flags,
+
+	int (*select_config) (const struct media_codec *codec, uint32_t flags,
 			const void *caps, size_t caps_size,
-			const struct a2dp_codec_audio_info *info,
+			const struct media_codec_audio_info *info,
 			const struct spa_dict *global_settings, uint8_t config[A2DP_MAX_CAPS_SIZE]);
-	int (*enum_config) (const struct a2dp_codec *codec, uint32_t flags,
+	int (*enum_config) (const struct media_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,
+	int (*validate_config) (const struct media_codec *codec, uint32_t flags,
 			const void *caps, size_t caps_size,
 			struct spa_audio_info *info);
+	int (*get_qos)(const struct media_codec *codec,
+			const void *config, size_t config_size,
+			const struct bap_endpoint_qos *endpoint_qos,
+			struct bap_codec_qos *qos);
 
 	/** qsort comparison sorting caps in order of preference for the codec.
 	 * Used in codec switching to select best remote endpoints.
 	 * The caps handed in correspond to this codec_id, but are
 	 * otherwise not checked beforehand.
 	 */
-	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,
+	int (*caps_preference_cmp) (const struct media_codec *codec, uint32_t flags, const void *caps1, size_t caps1_size,
+			const void *caps2, size_t caps2_size, const struct media_codec_audio_info *info,
 			const struct spa_dict *global_settings);
 
-	void *(*init_props) (const struct a2dp_codec *codec, uint32_t flags, const struct spa_dict *settings);
+	void *(*init_props) (const struct media_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);
 	int (*set_props) (void *props, const struct spa_pod *param);
 
-	void *(*init) (const struct a2dp_codec *codec, uint32_t flags, void *config, size_t config_size,
+	void *(*init) (const struct media_codec *codec, uint32_t flags, void *config, size_t config_size,
 			const struct spa_audio_info *info, void *props, size_t mtu);
 	void (*deinit) (void *data);
 
@@ -151,17 +162,17 @@ struct a2dp_codec {
 	void (*set_log) (struct spa_log *global_log);
 };
 
-struct a2dp_codec_config {
+struct media_codec_config {
 	uint32_t config;
 	int value;
 	unsigned int priority;
 };
 
-int a2dp_codec_select_config(const struct a2dp_codec_config configs[], size_t n,
+int media_codec_select_config(const struct media_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,
+bool media_codec_check_caps(const struct media_codec *codec, unsigned int codec_id,
+	const void *caps, size_t caps_size, const struct media_codec_audio_info *info,
 	const struct spa_dict *global_settings);
 
 #endif
diff --git a/spa/plugins/bluez5/a2dp-sink.c b/spa/plugins/bluez5/media-sink.c
similarity index 85%
rename from spa/plugins/bluez5/a2dp-sink.c
rename to spa/plugins/bluez5/media-sink.c
index 9d712d81551bd5a5ff7085eb9e1ed17a0e7547e4..a5d72fcfca7b0ff4ab67b538fccdf84b2f75d86a 100644
--- a/spa/plugins/bluez5/a2dp-sink.c
+++ b/spa/plugins/bluez5/media-sink.c
@@ -1,4 +1,4 @@
-/* Spa A2DP Sink
+/* Spa Media Sink
  *
  * Copyright © 2018 Wim Taymans
  *
@@ -53,26 +53,23 @@
 
 #include "defs.h"
 #include "rtp.h"
-#include "a2dp-codecs.h"
+#include "media-codecs.h"
 
-static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.sink.a2dp");
+static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.sink.media");
 #undef SPA_LOG_TOPIC_DEFAULT
 #define SPA_LOG_TOPIC_DEFAULT &log_topic
 
 #define DEFAULT_CLOCK_NAME	"clock.system.monotonic"
 
 struct props {
-	uint32_t min_latency;
-	uint32_t max_latency;
 	int64_t latency_offset;
 	char clock_name[64];
 };
 
-#define FILL_FRAMES 2
+#define FILL_FRAMES 4
+#define MIN_BUFFERS 2
 #define MAX_BUFFERS 32
-#define MIN_LATENCY	128
-#define MAX_LATENCY	8192
-#define BUFFER_SIZE	(MAX_LATENCY*8)
+#define BUFFER_SIZE	(8192*8)
 
 struct buffer {
 	uint32_t id;
@@ -121,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
@@ -136,6 +135,8 @@ struct impl {
 
 	unsigned int started:1;
 	unsigned int following:1;
+	unsigned int is_output:1;
+	unsigned int flush_pending:1;
 
 	unsigned int is_duplex:1;
 
@@ -151,8 +152,12 @@ struct impl {
 	uint64_t current_time;
 	uint64_t next_time;
 	uint64_t last_error;
+	uint64_t process_time;
+
+	uint64_t prev_flush_time;
+	uint64_t next_flush_time;
 
-	const struct a2dp_codec *codec;
+	const struct media_codec *codec;
 	bool codec_props_changed;
 	void *codec_props;
 	void *codec_data;
@@ -160,12 +165,11 @@ struct impl {
 
 	int need_flush;
 	bool fragment;
-	uint64_t fragment_timeout;
 	uint32_t block_size;
 	uint8_t buffer[BUFFER_SIZE];
 	uint32_t buffer_used;
 	uint32_t header_size;
-	uint32_t frame_count;
+	uint32_t block_count;
 	uint16_t seqnum;
 	uint32_t timestamp;
 	uint64_t sample_count;
@@ -174,12 +178,10 @@ struct impl {
 	uint32_t fd_buffer_size;
 };
 
-#define CHECK_PORT(this,d,p)    ((d) == SPA_DIRECTION_INPUT && (p) == 0)
+#define CHECK_PORT(this,d,p)	((d) == SPA_DIRECTION_INPUT && (p) == 0)
 
 static void reset_props(struct impl *this, struct props *props)
 {
-	props->min_latency = MIN_LATENCY;
-	props->max_latency = MAX_LATENCY;
 	props->latency_offset = 0;
 	strncpy(props->clock_name, DEFAULT_CLOCK_NAME, sizeof(props->clock_name));
 }
@@ -209,24 +211,8 @@ 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;
-		case 2:
 			param = spa_pod_builder_add_object(&b,
 				SPA_TYPE_OBJECT_PropInfo, id,
 				SPA_PROP_INFO_id,   SPA_POD_Id(SPA_PROP_latencyOffsetNsec),
@@ -235,7 +221,7 @@ static int impl_node_enum_params(void *object, int seq,
 			break;
 		default:
 			enum_codec = true;
-			index_offset = 3;
+			index_offset = 1;
 		}
 		break;
 	}
@@ -247,8 +233,6 @@ static int impl_node_enum_params(void *object, int seq,
 		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),
 				SPA_PROP_latencyOffsetNsec, SPA_POD_Long(p->latency_offset));
 			break;
 		default:
@@ -386,8 +370,6 @@ static int apply_props(struct impl *this, const struct spa_pod *param)
 	} 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),
 				SPA_PROP_latencyOffsetNsec, SPA_POD_OPT_Long(&new_props.latency_offset));
 	}
 
@@ -439,7 +421,7 @@ static int reset_buffer(struct impl *this)
 		this->codec_props_changed = false;
 	}
 	this->need_flush = 0;
-	this->frame_count = 0;
+	this->block_count = 0;
 	this->fragment = false;
 	this->buffer_used = this->codec->start_encode(this->codec_data,
 			this->buffer, sizeof(this->buffer),
@@ -464,20 +446,32 @@ static int get_transport_unused_size(struct impl *this)
 static int send_buffer(struct impl *this)
 {
 	int written, unsent;
+
 	unsent = get_transport_unused_size(this);
 	if (unsent >= 0) {
 		unsent = this->fd_buffer_size - unsent;
 		this->codec->abr_process(this->codec_data, unsent);
 	}
 
-	spa_log_trace(this->log, "%p: send %d %u %u %u %u",
-			this, this->frame_count, this->block_size, this->seqnum,
-			this->timestamp, this->buffer_used);
-
 	written = send(this->flush_source.fd, this->buffer,
 			this->buffer_used, MSG_DONTWAIT | MSG_NOSIGNAL);
 
-	spa_log_trace(this->log, "%p: send %d", this, written);
+	if (SPA_UNLIKELY(spa_log_level_topic_enabled(this->log, SPA_LOG_TOPIC_DEFAULT, SPA_LOG_LEVEL_TRACE))) {
+		struct timespec ts;
+		uint64_t now;
+		uint64_t dt;
+
+		spa_system_clock_gettime(this->data_system, CLOCK_MONOTONIC, &ts);
+		now = SPA_TIMESPEC_TO_NSEC(&ts);
+		dt = now - this->prev_flush_time;
+		this->prev_flush_time = now;
+
+		spa_log_trace(this->log,
+				"%p: send blocks:%d block:%u seq:%u ts:%u size:%u "
+				"wrote:%d dt:%"PRIu64,
+				this, this->block_count, this->block_size, this->seqnum,
+				this->timestamp, this->buffer_used, written, dt);
+	}
 
 	if (written < 0) {
 		spa_log_debug(this->log, "%p: %m", this);
@@ -497,7 +491,7 @@ static int encode_buffer(struct impl *this, const void *data, uint32_t size)
 
 	spa_log_trace(this->log, "%p: encode %d used %d, %d %d %d",
 			this, size, this->buffer_used, port->frame_size, this->block_size,
-			this->frame_count);
+			this->block_count);
 
 	if (this->need_flush)
 		return 0;
@@ -525,7 +519,7 @@ static int encode_buffer(struct impl *this, const void *data, uint32_t size)
 		return processed;
 
 	this->sample_count += processed / port->frame_size;
-	this->frame_count += processed / this->block_size;
+	this->block_count += processed / this->block_size;
 	this->buffer_used += out_encoded;
 
 	spa_log_trace(this->log, "%p: processed %d %zd used %d",
@@ -546,7 +540,7 @@ static int encode_fragment(struct impl *this)
 
 	spa_log_trace(this->log, "%p: encode fragment used %d, %d %d %d",
 			this, this->buffer_used, port->frame_size, this->block_size,
-			this->frame_count);
+			this->block_count);
 
 	if (this->need_flush)
 		return 0;
@@ -597,25 +591,41 @@ static int add_data(struct impl *this, const void *data, uint32_t size)
 	return total;
 }
 
-static void enable_flush(struct impl *this, bool enabled, uint64_t timeout)
+static void enable_flush_timer(struct impl *this, bool enabled)
 {
-	bool flush_enabled = enabled && (timeout == 0);
 	struct itimerspec ts;
 
-	if (SPA_FLAG_IS_SET(this->flush_source.mask, SPA_IO_OUT) != flush_enabled) {
-		SPA_FLAG_UPDATE(this->flush_source.mask, SPA_IO_OUT, flush_enabled);
-		spa_loop_update_source(this->data_loop, &this->flush_source);
-	}
-
 	if (!enabled)
-		timeout = 0;
+		this->next_flush_time = 0;
 
-	ts.it_value.tv_sec = timeout / SPA_NSEC_PER_SEC;
-	ts.it_value.tv_nsec = timeout % SPA_NSEC_PER_SEC;
+	ts.it_value.tv_sec = this->next_flush_time / SPA_NSEC_PER_SEC;
+	ts.it_value.tv_nsec = this->next_flush_time % SPA_NSEC_PER_SEC;
 	ts.it_interval.tv_sec = 0;
 	ts.it_interval.tv_nsec = 0;
 	spa_system_timerfd_settime(this->data_system,
-			this->flush_timerfd, 0, &ts, NULL);
+			this->flush_timerfd, SPA_FD_TIMER_ABSTIME, &ts, NULL);
+
+	this->flush_pending = enabled;
+}
+
+static uint32_t get_queued_frames(struct impl *this)
+{
+	struct port *port = &this->port;
+	uint32_t bytes = 0;
+	struct buffer *b;
+
+	spa_list_for_each(b, &port->ready, link) {
+		struct spa_data *d = b->buf->datas;
+
+		bytes += d[0].chunk->size;
+	}
+
+	if (bytes > port->ready_offset)
+		bytes -= port->ready_offset;
+	else
+		bytes = 0;
+
+	return bytes / port->frame_size;
 }
 
 static int flush_data(struct impl *this, uint64_t now_time)
@@ -623,6 +633,7 @@ static int flush_data(struct impl *this, uint64_t now_time)
 	int written;
 	uint32_t total_frames;
 	struct port *port = &this->port;
+	int unused_buffer;
 
 	if (!this->flush_source.loop) {
 		/* I/O in error state */
@@ -699,17 +710,30 @@ again:
 	}
 
 	if (written > 0 && this->buffer_used == this->header_size) {
-		enable_flush(this, false, 0);
+		enable_flush_timer(this, false);
+		return 0;
+	}
+
+	if (this->flush_pending) {
+		spa_log_trace(this->log, "%p: wait for flush timer", this);
 		return 0;
 	}
 
+	/*
+	 * Get socket queue size before writing to it.
+	 * This should be the same as buffer size to increase bitpool
+	 * Bitpool shouldn't be increased when data is left over in the buffer
+	 */
+	unused_buffer = get_transport_unused_size(this);
+
 	written = flush_buffer(this);
 
 	if (written == -EAGAIN) {
 		spa_log_trace(this->log, "%p: fail flush", this);
 		if (now_time - this->last_error > SPA_NSEC_PER_SEC / 2) {
-			spa_log_trace(this->log, "%p: reduce bitpool", this);
-			this->codec->reduce_bitpool(this->codec_data);
+			int res = this->codec->reduce_bitpool(this->codec_data);
+
+			spa_log_debug(this->log, "%p: reduce bitpool: %i", this, res);
 			this->last_error = now_time;
 		}
 
@@ -719,97 +743,99 @@ again:
 		 * glitch in any case.
 		 */
 		written = this->buffer_used;
-		reset_buffer(this);
 	}
 
 	if (written < 0) {
 		spa_log_trace(this->log, "%p: error flushing %s", this,
 				spa_strerror(written));
 		reset_buffer(this);
-		enable_flush(this, false, 0);
+		enable_flush_timer(this, false);
 		return written;
 	}
 	else if (written > 0) {
 		/*
-		 * We cannot write all data we have at once, since this can exceed
-		 * device buffers.  We'll want a limited number of "excess"
-		 * samples. This is an issue for the "low-latency" A2DP codecs.
-		 *
-		 * Flushing the rest of the data (if any) is delayed after a timeout,
-		 * selected on an average-rate basis:
-		 *
-		 * npackets = quantum / packet_samples
-		 * write_end_time = npackets * timeout
-		 * max_excess = quantum - sample_rate * write_end_time
-		 * packet_time = packet_samples / sample_rate
-		 * => timeout = (quantum - max_excess)/quantum * packet_time
+		 * We cannot write all data we have at once, since this can exceed device
+		 * buffers (esp. for the A2DP low-latency codecs) and socket buffers, so
+		 * flush needs to be delayed.
 		 */
-		uint64_t max_excess = 2*256;
-		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) ?
-			(packet_time * (quantum - max_excess) / quantum) : 0;
+		uint32_t packet_samples = this->block_count * this->block_size
+			/ port->frame_size;
+		uint64_t packet_time = (uint64_t)packet_samples * SPA_NSEC_PER_SEC
+			/ port->current_format.info.raw.rate;
+
+		if (SPA_LIKELY(this->position)) {
+			uint32_t frames = get_queued_frames(this);
+			uint64_t duration_ns;
+
+			/*
+			 * Flush at the time position of the next buffered sample.
+			 */
+			duration_ns = ((uint64_t)this->position->clock.duration * SPA_NSEC_PER_SEC
+					/ this->position->clock.rate.denom);
+			this->next_flush_time = this->process_time + duration_ns
+				- ((uint64_t)frames * SPA_NSEC_PER_SEC
+						/ port->current_format.info.raw.rate);
+
+			/*
+			 * We could delay the output by one packet to avoid waiting
+			 * for the next buffer and so make send intervals exactly regular.
+			 * However, this is not needed for A2DP or BAP. The controller
+			 * will do the scheduling for us, and there's also the socket buffer
+			 * in between.
+			 */
+#if 0
+			this->next_flush_time += SPA_MIN(packet_time,
+					duration_ns * (port->n_buffers - 1));
+#endif
+		} else {
+			if (this->next_flush_time == 0)
+				this->next_flush_time = this->process_time;
+			this->next_flush_time += packet_time;
+		}
 
 		if (this->need_flush == NEED_FLUSH_FRAGMENT) {
 			reset_buffer(this);
 			this->fragment = true;
-			this->fragment_timeout = (packet_samples > 0) ? timeout : this->fragment_timeout;
 			goto again;
 		}
-		if (this->fragment_timeout > 0) {
-			timeout = this->fragment_timeout;
-			this->fragment_timeout = 0;
-		}
 
-		reset_buffer(this);
 		if (now_time - this->last_error > SPA_NSEC_PER_SEC) {
-			if (get_transport_unused_size(this) == (int)this->fd_buffer_size) {
-				spa_log_trace(this->log, "%p: increase bitpool", this);
-				this->codec->increase_bitpool(this->codec_data);
+			if (unused_buffer == (int)this->fd_buffer_size) {
+				int res = this->codec->increase_bitpool(this->codec_data);
+
+				spa_log_debug(this->log, "%p: increase bitpool: %i", this, res);
 			}
 			this->last_error = now_time;
 		}
-		if (!spa_list_is_empty(&port->ready)) {
-			spa_log_trace(this->log, "%p: flush after %d ns", this, (int)timeout);
-			if (timeout == 0)
-				goto again;
-			else
-				enable_flush(this, true, timeout);
-		} else {
-			enable_flush(this, false, 0);
-		}
+
+		spa_log_trace(this->log, "%p: flush at:%"PRIu64" process:%"PRIu64, this,
+				this->next_flush_time, this->process_time);
+		reset_buffer(this);
+		enable_flush_timer(this, true);
 	}
 	else {
 		/* Don't want to flush yet, or failed to write anything */
 		spa_log_trace(this->log, "%p: skip flush", this);
-		enable_flush(this, false, 0);
+		enable_flush_timer(this, false);
 	}
 	return 0;
 }
 
-static void a2dp_on_flush(struct spa_source *source)
+static void media_on_flush_error(struct spa_source *source)
 {
 	struct impl *this = source->data;
 
-	spa_log_trace(this->log, "%p: flushing", this);
+	spa_log_trace(this->log, "%p: flush event", this);
 
-	if (!SPA_FLAG_IS_SET(source->rmask, SPA_IO_OUT)) {
+	if (source->rmask & (SPA_IO_ERR | SPA_IO_HUP)) {
 		spa_log_warn(this->log, "%p: error %d", this, source->rmask);
 		if (this->flush_source.loop)
 			spa_loop_remove_source(this->data_loop, &this->flush_source);
 		return;
 	}
-
-	if (this->transport == NULL) {
-		enable_flush(this, false, 0);
-		return;
-	}
-
-	flush_data(this, this->current_time);
 }
 
-static void a2dp_on_flush_timeout(struct spa_source *source)
+static void media_on_flush_timeout(struct spa_source *source)
 {
 	struct impl *this = source->data;
 	uint64_t exp;
@@ -820,14 +846,17 @@ static void a2dp_on_flush_timeout(struct spa_source *source)
 		spa_log_warn(this->log, "error reading timerfd: %s", strerror(errno));
 
 	if (this->transport == NULL) {
-		enable_flush(this, false, 0);
+		enable_flush_timer(this, false);
 		return;
 	}
 
-	flush_data(this, this->current_time);
+	while (exp-- > 0) {
+		this->flush_pending = false;
+		flush_data(this, this->current_time);
+	}
 }
 
-static void a2dp_on_timeout(struct spa_source *source)
+static void media_on_timeout(struct spa_source *source)
 {
 	struct impl *this = source->data;
 	struct port *port = &this->port;
@@ -889,6 +918,7 @@ static int do_start(struct impl *this)
 	struct port *port;
 	socklen_t len;
 	uint8_t *conf;
+	uint32_t flags;
 
 	if (this->started)
 		return 0;
@@ -897,7 +927,7 @@ static int do_start(struct impl *this)
 
 	this->following = is_following(this);
 
-        spa_log_debug(this->log, "%p: start following:%d", this, this->following);
+	spa_log_debug(this->log, "%p: start following:%d", this, this->following);
 
 	if ((res = spa_bt_transport_acquire(this->transport, false)) < 0)
 		return res;
@@ -910,8 +940,10 @@ static int do_start(struct impl *this)
 	spa_log_debug(this->log, "Transport configuration:");
 	spa_log_hexdump(this->log, SPA_LOG_LEVEL_DEBUG, 2, conf, (size_t)size);
 
+	flags = this->is_duplex ? MEDIA_CODEC_FLAG_SINK : 0;
+
 	this->codec_data = this->codec->init(this->codec,
-			this->is_duplex ? A2DP_CODEC_FLAG_SINK : 0,
+			flags,
 			this->transport->configuration,
 			this->transport->configuration_len,
 			&port->current_format,
@@ -920,8 +952,9 @@ static int do_start(struct impl *this)
 	if (this->codec_data == NULL)
 		return -EIO;
 
-        spa_log_info(this->log, "%p: using A2DP codec %s, delay:%"PRIi64" ms", this, this->codec->description,
-                     (int64_t)(spa_bt_transport_get_delay_nsec(this->transport) / SPA_NSEC_PER_MSEC));
+	spa_log_info(this->log, "%p: using %s codec %s, delay:%"PRIi64" ms", this,
+			this->codec->bap ? "BAP" : "A2DP", this->codec->description,
+			(int64_t)(spa_bt_transport_get_delay_nsec(this->transport) / SPA_NSEC_PER_MSEC));
 
 	this->seqnum = 0;
 
@@ -932,8 +965,7 @@ static int do_start(struct impl *this)
 		return -EIO;
 	}
 
-        spa_log_debug(this->log, "%p: block_size %d", this,
-			this->block_size);
+	spa_log_debug(this->log, "%p: block_size %d", this, this->block_size);
 
 	val = this->codec->send_buf_size > 0
 			/* The kernel doubles the SO_SNDBUF option value set by setsockopt(). */
@@ -963,25 +995,27 @@ static int do_start(struct impl *this)
 
 	this->source.data = this;
 	this->source.fd = this->timerfd;
-	this->source.func = a2dp_on_timeout;
+	this->source.func = media_on_timeout;
 	this->source.mask = SPA_IO_IN;
 	this->source.rmask = 0;
 	spa_loop_add_source(this->data_loop, &this->source);
 
 	this->flush_timer_source.data = this;
 	this->flush_timer_source.fd = this->flush_timerfd;
-	this->flush_timer_source.func = a2dp_on_flush_timeout;
+	this->flush_timer_source.func = media_on_flush_timeout;
 	this->flush_timer_source.mask = SPA_IO_IN;
 	this->flush_timer_source.rmask = 0;
 	spa_loop_add_source(this->data_loop, &this->flush_timer_source);
 
 	this->flush_source.data = this;
 	this->flush_source.fd = this->transport->fd;
-	this->flush_source.func = a2dp_on_flush;
-	this->flush_source.mask = 0;
+	this->flush_source.func = media_on_flush_error;
+	this->flush_source.mask = SPA_IO_ERR | SPA_IO_HUP;
 	this->flush_source.rmask = 0;
 	spa_loop_add_source(this->data_loop, &this->flush_source);
 
+	this->flush_pending = false;
+
 	set_timers(this);
 	this->started = true;
 
@@ -1027,7 +1061,7 @@ static int do_stop(struct impl *this)
 	if (!this->started)
 		return 0;
 
-        spa_log_trace(this->log, "%p: stop", this);
+	spa_log_trace(this->log, "%p: stop", this);
 
 	spa_loop_invoke(this->data_loop, do_remove_source, 0, NULL, 0, true, this);
 
@@ -1079,8 +1113,10 @@ static void emit_node_info(struct impl *this, bool full)
 {
 	struct spa_dict_item node_info_items[] = {
 		{ SPA_KEY_DEVICE_API, "bluez5" },
-		{ SPA_KEY_MEDIA_CLASS, "Audio/Sink" },
-		{ SPA_KEY_NODE_DRIVER, "true" },
+		{ SPA_KEY_MEDIA_CLASS, this->is_output ? "Audio/Sink" : "Stream/Input/Audio" },
+		{ "media.name", ((this->transport && this->transport->device->name) ?
+					this->transport->device->name : this->codec->bap ? "BAP" : "A2DP" ) },
+		{ SPA_KEY_NODE_DRIVER, this->is_output ? "true" : "false" },
 	};
 	uint64_t old = full ? this->info.change_mask : 0;
 	if (full)
@@ -1198,7 +1234,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->is_duplex ? MEDIA_CODEC_FLAG_SINK : 0,
 					this->transport->configuration,
 					this->transport->configuration_len,
 					id, result.index, &b, &param)) != 1)
@@ -1222,11 +1258,14 @@ 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(2, 2, MAX_BUFFERS),
+			SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(
+							MIN_BUFFERS,
+							MIN_BUFFERS,
+							MAX_BUFFERS),
 			SPA_PARAM_BUFFERS_blocks,  SPA_POD_Int(1),
-			SPA_PARAM_BUFFERS_size,    SPA_POD_CHOICE_RANGE_Int(
-							this->props.min_latency * port->frame_size,
-							this->props.min_latency * port->frame_size,
+			SPA_PARAM_BUFFERS_size,	   SPA_POD_CHOICE_RANGE_Int(
+							this->quantum_limit * port->frame_size,
+							16 * port->frame_size,
 							INT32_MAX),
 			SPA_PARAM_BUFFERS_stride,  SPA_POD_Int(port->frame_size));
 		break;
@@ -1315,6 +1354,11 @@ static int port_set_format(struct impl *this, struct port *port,
 		if (spa_format_audio_raw_parse(format, &info.info.raw) < 0)
 			return -EINVAL;
 
+		if (info.info.raw.rate == 0 ||
+		    info.info.raw.channels == 0 ||
+		    info.info.raw.channels > SPA_AUDIO_MAX_CHANNELS)
+			return -EINVAL;
+
 		port->frame_size = info.info.raw.channels;
 		switch (info.info.raw.format) {
 		case SPA_AUDIO_FORMAT_S16:
@@ -1486,18 +1530,21 @@ static int impl_node_process(void *object)
 		io->buffer_id = SPA_ID_INVALID;
 		io->status = SPA_STATUS_OK;
 	}
-	if (!spa_list_is_empty(&port->ready)) {
-		if (this->following) {
-			if (this->position) {
-				this->current_time = this->position->clock.nsec;
-			} else {
-				struct timespec now;
-				spa_system_clock_gettime(this->data_system, CLOCK_MONOTONIC, &now);
-				this->current_time = SPA_TIMESPEC_TO_NSEC(&now);
-			}
+
+	if (this->following) {
+		if (this->position) {
+			this->current_time = this->position->clock.nsec;
+		} else {
+			struct timespec now;
+			spa_system_clock_gettime(this->data_system, CLOCK_MONOTONIC, &now);
+			this->current_time = SPA_TIMESPEC_TO_NSEC(&now);
 		}
-		if (this->need_flush)
-			reset_buffer(this);
+	}
+
+	this->process_time = this->current_time;
+
+	if (!spa_list_is_empty(&port->ready)) {
+		spa_log_trace(this->log, "%p: flush on process", this);
 		flush_data(this, this->current_time);
 	}
 
@@ -1610,8 +1657,7 @@ static int impl_clear(struct spa_handle *handle)
 {
 	struct impl *this = (struct impl *) handle;
 
-	if (this->codec_data)
-		this->codec->deinit(this->codec_data);
+	do_stop(this);
 	if (this->codec_props && this->codec->clear_props)
 		this->codec->clear_props(this->codec_props);
 	if (this->transport)
@@ -1701,6 +1747,11 @@ impl_init(const struct spa_handle_factory *factory,
 
 	spa_list_init(&port->ready);
 
+	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, "api.bluez5.a2dp-duplex")) != NULL)
 		this->is_duplex = spa_atob(str);
 
@@ -1711,12 +1762,12 @@ impl_init(const struct spa_handle_factory *factory,
 		spa_log_error(this->log, "a transport is needed");
 		return -EINVAL;
 	}
-	if (this->transport->a2dp_codec == NULL) {
+	if (this->transport->media_codec == NULL) {
 		spa_log_error(this->log, "a transport codec is needed");
 		return -EINVAL;
 	}
 
-	this->codec = this->transport->a2dp_codec;
+	this->codec = this->transport->media_codec;
 
 	if (this->is_duplex) {
 		if (!this->codec->duplex_codec) {
@@ -1728,9 +1779,14 @@ impl_init(const struct spa_handle_factory *factory,
 
 	if (this->codec->init_props != NULL)
 		this->codec_props = this->codec->init_props(this->codec,
-					this->is_duplex ? A2DP_CODEC_FLAG_SINK : 0,
+					this->is_duplex ? MEDIA_CODEC_FLAG_SINK : 0,
 					this->transport->device->settings);
 
+	if (this->codec->bap)
+		this->is_output = this->transport->bap_initiator;
+	else
+		this->is_output = true;
+
 	reset_props(this, &this->props);
 
 	spa_bt_transport_add_listener(this->transport,
@@ -1770,12 +1826,22 @@ impl_enum_interface_info(const struct spa_handle_factory *factory,
 
 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 the a2dp" },
+	{ SPA_KEY_FACTORY_DESCRIPTION, "Play audio with the media" },
 	{ SPA_KEY_FACTORY_USAGE, SPA_KEY_API_BLUEZ5_TRANSPORT"=<transport>" },
 };
 
 static const struct spa_dict info = SPA_DICT_INIT_ARRAY(info_items);
 
+const struct spa_handle_factory spa_media_sink_factory = {
+	SPA_VERSION_HANDLE_FACTORY,
+	SPA_NAME_API_BLUEZ5_MEDIA_SINK,
+	&info,
+	impl_get_size,
+	impl_init,
+	impl_enum_interface_info,
+};
+
+/* Retained for backward compatibility: */
 const struct spa_handle_factory spa_a2dp_sink_factory = {
 	SPA_VERSION_HANDLE_FACTORY,
 	SPA_NAME_API_BLUEZ5_A2DP_SINK,
diff --git a/spa/plugins/bluez5/a2dp-source.c b/spa/plugins/bluez5/media-source.c
similarity index 94%
rename from spa/plugins/bluez5/a2dp-source.c
rename to spa/plugins/bluez5/media-source.c
index 0082b9d1e3ee25a4fa60160b49885b7a6667598d..58ff14a52955a73d16bc019a14d391f417cd4d5b 100644
--- a/spa/plugins/bluez5/a2dp-source.c
+++ b/spa/plugins/bluez5/media-source.c
@@ -1,4 +1,4 @@
-/* Spa A2DP Source
+/* Spa Media Source
  *
  * Copyright © 2018 Wim Taymans
  * Copyright © 2019 Collabora Ltd.
@@ -54,9 +54,9 @@
 
 #include "defs.h"
 #include "rtp.h"
-#include "a2dp-codecs.h"
+#include "media-codecs.h"
 
-static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.source.a2dp");
+static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.source.media");
 #undef SPA_LOG_TOPIC_DEFAULT
 #define SPA_LOG_TOPIC_DEFAULT &log_topic
 
@@ -156,7 +156,7 @@ struct impl {
 	uint64_t current_time;
 	uint64_t next_time;
 
-	const struct a2dp_codec *codec;
+	const struct media_codec *codec;
 	bool codec_props_changed;
 	void *codec_props;
 	void *codec_data;
@@ -276,6 +276,7 @@ static int do_reassign_follower(struct spa_loop *loop,
 	struct impl *this = user_data;
 	struct port *port = &this->port;
 
+	set_timers(this);
 	spa_bt_decode_buffer_recover(&port->buffer);
 	return 0;
 }
@@ -448,7 +449,7 @@ static int32_t decode_data(struct impl *this, uint8_t *src, uint32_t src_size,
 	return dst_size - avail;
 }
 
-static void a2dp_on_ready_read(struct spa_source *source)
+static void media_on_ready_read(struct spa_source *source)
 {
 	struct impl *this = source->data;
 	struct port *port = &this->port;
@@ -533,7 +534,7 @@ static int set_duplex_timeout(struct impl *this, uint64_t timeout)
 			this->duplex_timerfd, 0, &ts, NULL);
 }
 
-static void a2dp_on_duplex_timeout(struct spa_source *source)
+static void media_on_duplex_timeout(struct spa_source *source)
 {
 	struct impl *this = source->data;
 	uint64_t exp;
@@ -543,7 +544,7 @@ static void a2dp_on_duplex_timeout(struct spa_source *source)
 
 	set_duplex_timeout(this, this->duplex_timeout);
 
-	a2dp_on_ready_read(source);
+	media_on_ready_read(source);
 }
 
 static int setup_matching(struct impl *this)
@@ -567,13 +568,14 @@ static int setup_matching(struct impl *this)
 	return 0;
 }
 
-static void a2dp_on_timeout(struct spa_source *source)
+static int produce_buffer(struct impl *this);
+
+static void media_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)
@@ -608,8 +610,11 @@ static void a2dp_on_timeout(struct spa_source *source)
 		this->clock->next_nsec = this->next_time;
 	}
 
-	spa_log_trace(this->log, "%p: %d", this, io->status);
-	io->status = SPA_STATUS_HAVE_DATA;
+	if (port->io) {
+		int status = produce_buffer(this);
+		spa_log_trace(this->log, "%p: io:%d status:%d", this, port->io->status, status);
+	}
+
 	spa_node_call_ready(&this->callbacks, SPA_STATUS_HAVE_DATA);
 
 	set_timeout(this, this->next_time);
@@ -619,6 +624,7 @@ static int transport_start(struct impl *this)
 {
 	int res, val;
 	struct port *port = &this->port;
+	uint32_t flags;
 
 	if (this->transport_acquired)
 		return 0;
@@ -630,8 +636,10 @@ static int transport_start(struct impl *this)
 
 	this->transport_acquired = true;
 
+	flags = this->is_duplex ? 0 : MEDIA_CODEC_FLAG_SINK;
+
 	this->codec_data = this->codec->init(this->codec,
-			this->is_duplex ? 0 : A2DP_CODEC_FLAG_SINK,
+			flags,
 			this->transport->configuration,
 			this->transport->configuration_len,
 			&port->current_format,
@@ -640,7 +648,8 @@ static int transport_start(struct impl *this)
 	if (this->codec_data == NULL)
 		return -EIO;
 
-        spa_log_info(this->log, "%p: using A2DP codec %s", this, this->codec->description);
+	spa_log_info(this->log, "%p: using %s codec %s", this,
+	             this->codec->bap ? "BAP" : "A2DP", this->codec->description);
 
 	val = fcntl(this->transport->fd, F_GETFL);
 	if (fcntl(this->transport->fd, F_SETFL, val | O_NONBLOCK) < 0)
@@ -672,7 +681,7 @@ static int transport_start(struct impl *this)
 
 	if (!this->use_duplex_source) {
 		this->source.fd = this->transport->fd;
-		this->source.func = a2dp_on_ready_read;
+		this->source.func = media_on_ready_read;
 		this->source.mask = SPA_IO_IN;
 		this->source.rmask = 0;
 		spa_loop_add_source(this->data_loop, &this->source);
@@ -689,7 +698,7 @@ static int transport_start(struct impl *this)
 		 * XXX: forward stream.
 		 */
 		this->source.fd = this->duplex_timerfd;
-		this->source.func = a2dp_on_duplex_timeout;
+		this->source.func = media_on_duplex_timeout;
 		this->source.mask = SPA_IO_IN;
 		this->source.rmask = 0;
 		spa_loop_add_source(this->data_loop, &this->source);
@@ -700,7 +709,7 @@ static int transport_start(struct impl *this)
 
 	this->timer_source.data = this;
 	this->timer_source.fd = this->timerfd;
-	this->timer_source.func = a2dp_on_timeout;
+	this->timer_source.func = media_on_timeout;
 	this->timer_source.mask = SPA_IO_IN;
 	this->timer_source.rmask = 0;
 	spa_loop_add_source(this->data_loop, &this->timer_source);
@@ -729,7 +738,7 @@ static int do_start(struct impl *this)
 			this, this->transport->state, this->following);
 
 	if (this->transport->state >= SPA_BT_TRANSPORT_STATE_PENDING ||
-			this->is_duplex)
+			this->is_duplex || this->codec->bap)
 		res = transport_start(this);
 
 	this->started = true;
@@ -847,7 +856,7 @@ static void emit_node_info(struct impl *this, bool full)
 		{ SPA_KEY_MEDIA_CLASS, this->is_input ? "Audio/Source" : "Stream/Output/Audio" },
 		{ SPA_KEY_NODE_LATENCY, this->is_input ? "" : "512/48000" },
 		{ "media.name", ((this->transport && this->transport->device->name) ?
-					this->transport->device->name : "A2DP") },
+					this->transport->device->name : this->codec->bap ? "BAP" : "A2DP") },
 		{ SPA_KEY_NODE_DRIVER, this->is_input ? "true" : "false" },
 	};
 
@@ -968,7 +977,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->is_duplex ? 0 : MEDIA_CODEC_FLAG_SINK,
 					this->transport->configuration,
 					this->transport->configuration_len,
 					id, result.index, &b, &param)) != 1)
@@ -1092,6 +1101,11 @@ static int port_set_format(struct impl *this, struct port *port,
 		if (spa_format_audio_raw_parse(format, &info.info.raw) < 0)
 			return -EINVAL;
 
+		if (info.info.raw.rate == 0 ||
+		    info.info.raw.channels == 0 ||
+		    info.info.raw.channels > SPA_AUDIO_MAX_CHANNELS)
+			return -EINVAL;
+
 		port->frame_size = info.info.raw.channels;
 
 		switch (info.info.raw.format) {
@@ -1335,21 +1349,15 @@ static void process_buffering(struct impl *this)
 	}
 }
 
-static int impl_node_process(void *object)
+static int produce_buffer(struct impl *this)
 {
-	struct impl *this = object;
-	struct port *port;
-	struct spa_io_buffers *io;
 	struct buffer *buffer;
+	struct port *port = &this->port;
+	struct spa_io_buffers *io = port->io;
 
-	spa_return_val_if_fail(this != NULL, -EINVAL);
-
-	port = &this->port;
-	if ((io = port->io) == NULL)
+	if (io == NULL)
 		return -EIO;
 
-	spa_log_trace(this->log, "%p status:%d", this, io->status);
-
 	/* Return if we already have a buffer */
 	if (io->status == SPA_STATUS_HAVE_DATA)
 		return SPA_STATUS_HAVE_DATA;
@@ -1360,7 +1368,7 @@ static int impl_node_process(void *object)
 		io->buffer_id = SPA_ID_INVALID;
 	}
 
-	/* Handle buffering delay */
+	/* Handle buffering */
 	process_buffering(this);
 
 	/* Return if there are no buffers ready to be processed */
@@ -1380,6 +1388,37 @@ static int impl_node_process(void *object)
 	return SPA_STATUS_HAVE_DATA;
 }
 
+static int impl_node_process(void *object)
+{
+	struct impl *this = object;
+	struct port *port;
+	struct spa_io_buffers *io;
+
+	spa_return_val_if_fail(this != NULL, -EINVAL);
+
+	port = &this->port;
+	if ((io = port->io) == NULL)
+		return -EIO;
+
+	spa_log_trace(this->log, "%p status:%d", this, io->status);
+
+	/* Return if we already have a buffer */
+	if (io->status == SPA_STATUS_HAVE_DATA)
+		return SPA_STATUS_HAVE_DATA;
+
+	/* Recycle */
+	if (io->buffer_id < port->n_buffers) {
+		recycle_buffer(this, port, io->buffer_id);
+		io->buffer_id = SPA_ID_INVALID;
+	}
+
+	/* Follower produces buffers here, driver in timeout */
+	if (this->following)
+		return produce_buffer(this);
+	else
+		return SPA_STATUS_OK;
+}
+
 static const struct spa_node_methods impl_node = {
 	SPA_VERSION_NODE_METHODS,
 	.add_listener = impl_node_add_listener,
@@ -1445,8 +1484,8 @@ 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);
+
+	do_stop(this);
 	if (this->codec_props && this->codec->clear_props)
 		this->codec->clear_props(this->codec_props);
 	if (this->transport)
@@ -1555,7 +1594,7 @@ impl_init(const struct spa_handle_factory *factory,
 			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)
+		if ((str = spa_dict_lookup(info, "bluez5.media-source-role")) != NULL)
 			this->is_input = spa_streq(str, "input");
 		if ((str = spa_dict_lookup(info, "api.bluez5.a2dp-duplex")) != NULL)
 			this->is_duplex = spa_atob(str);
@@ -1565,11 +1604,11 @@ impl_init(const struct spa_handle_factory *factory,
 		spa_log_error(this->log, "a transport is needed");
 		return -EINVAL;
 	}
-	if (this->transport->a2dp_codec == NULL) {
+	if (this->transport->media_codec == NULL) {
 		spa_log_error(this->log, "a transport codec is needed");
 		return -EINVAL;
 	}
-	this->codec = this->transport->a2dp_codec;
+	this->codec = this->transport->media_codec;
 
 	if (this->is_duplex) {
 		if (!this->codec->duplex_codec) {
@@ -1581,9 +1620,12 @@ impl_init(const struct spa_handle_factory *factory,
 	}
 	this->use_duplex_source = this->is_duplex || (this->codec->duplex_codec != NULL);
 
+	if (this->codec->bap)
+		this->is_input = this->transport->bap_initiator;
+
 	if (this->codec->init_props != NULL)
 		this->codec_props = this->codec->init_props(this->codec,
-					this->is_duplex ? 0 : A2DP_CODEC_FLAG_SINK,
+					this->is_duplex ? 0 : MEDIA_CODEC_FLAG_SINK,
 					this->transport->device->settings);
 
 	spa_bt_transport_add_listener(this->transport,
@@ -1627,12 +1669,22 @@ impl_enum_interface_info(const struct spa_handle_factory *factory,
 
 static const struct spa_dict_item info_items[] = {
 	{ SPA_KEY_FACTORY_AUTHOR, "Collabora Ltd. <contact@collabora.com>" },
-	{ SPA_KEY_FACTORY_DESCRIPTION, "Capture bluetooth audio with a2dp" },
+	{ SPA_KEY_FACTORY_DESCRIPTION, "Capture bluetooth audio with media" },
 	{ SPA_KEY_FACTORY_USAGE, SPA_KEY_API_BLUEZ5_TRANSPORT"=<transport>" },
 };
 
 static const struct spa_dict info = SPA_DICT_INIT_ARRAY(info_items);
 
+const struct spa_handle_factory spa_media_source_factory = {
+	SPA_VERSION_HANDLE_FACTORY,
+	SPA_NAME_API_BLUEZ5_MEDIA_SOURCE,
+	&info,
+	impl_get_size,
+	impl_init,
+	impl_enum_interface_info,
+};
+
+/* Retained for backward compatibility */
 const struct spa_handle_factory spa_a2dp_source_factory = {
 	SPA_VERSION_HANDLE_FACTORY,
 	SPA_NAME_API_BLUEZ5_A2DP_SOURCE,
diff --git a/spa/plugins/bluez5/meson.build b/spa/plugins/bluez5/meson.build
index 2a17c78ee7cdd74b391d6f35d0622a604b755f8b..db010697fdd199f6701d3196ab0545edb9eb0155 100644
--- a/spa/plugins/bluez5/meson.build
+++ b/spa/plugins/bluez5/meson.build
@@ -10,6 +10,7 @@ cdata.set('HAVE_BLUEZ_5_BACKEND_NATIVE',
           get_option('bluez5-backend-hfp-native').allowed())
 cdata.set('HAVE_BLUEZ_5_BACKEND_HSP_NATIVE', get_option('bluez5-backend-hsp-native').allowed())
 cdata.set('HAVE_BLUEZ_5_BACKEND_HFP_NATIVE', get_option('bluez5-backend-hfp-native').allowed())
+cdata.set('HAVE_BLUEZ_5_BACKEND_NATIVE_MM', get_option('bluez5-backend-native-mm').allowed())
 cdata.set('HAVE_BLUEZ_5_BACKEND_OFONO', get_option('bluez5-backend-ofono').allowed())
 cdata.set('HAVE_BLUEZ_5_BACKEND_HSPHFPD', get_option('bluez5-backend-hsphfpd').allowed())
 cdata.set('HAVE_BLUEZ_5_HCI', dependency('bluez', version: '< 6', required: false).found())
@@ -17,9 +18,9 @@ cdata.set('HAVE_BLUEZ_5_HCI', dependency('bluez', version: '< 6', required: fals
 bluez5_sources = [
   'plugin.c',
   'codec-loader.c',
-  'a2dp-codecs.c',
-  'a2dp-sink.c',
-  'a2dp-source.c',
+  'media-codecs.c',
+  'media-sink.c',
+  'media-source.c',
   'sco-sink.c',
   'sco-source.c',
   'sco-io.c',
@@ -38,7 +39,11 @@ if get_option('bluez5-backend-hsp-native').allowed() or get_option('bluez5-backe
   if libusb_dep.found()
     bluez5_deps += libusb_dep
   endif
-  bluez5_sources += ['backend-native.c']
+  if mm_dep.found()
+    bluez5_deps += mm_dep
+    bluez5_sources += ['modemmanager.c']
+  endif
+  bluez5_sources += ['backend-native.c', 'upower.c']
 endif
 
 if get_option('bluez5-backend-ofono').allowed()
@@ -59,7 +64,7 @@ bluez5lib = shared_library('spa-bluez5',
 codec_args = [ '-DCODEC_PLUGIN' ]
 
 bluez_codec_sbc = shared_library('spa-codec-bluez5-sbc',
-  [ 'a2dp-codec-sbc.c', 'a2dp-codecs.c' ],
+  [ 'a2dp-codec-sbc.c', 'media-codecs.c' ],
   include_directories : [ configinc ],
   c_args : codec_args,
   dependencies : [ spa_dep, sbc_dep ],
@@ -67,7 +72,7 @@ bluez_codec_sbc = shared_library('spa-codec-bluez5-sbc',
   install_dir : spa_plugindir / 'bluez5')
 
 bluez_codec_faststream = shared_library('spa-codec-bluez5-faststream',
-  [ 'a2dp-codec-faststream.c', 'a2dp-codecs.c' ],
+  [ 'a2dp-codec-faststream.c', 'media-codecs.c' ],
   include_directories : [ configinc ],
   c_args : codec_args,
   dependencies : [ spa_dep, sbc_dep ],
@@ -76,7 +81,7 @@ bluez_codec_faststream = shared_library('spa-codec-bluez5-faststream',
 
 if fdk_aac_dep.found()
   bluez_codec_aac = shared_library('spa-codec-bluez5-aac',
-    [ 'a2dp-codec-aac.c', 'a2dp-codecs.c' ],
+    [ 'a2dp-codec-aac.c', 'media-codecs.c' ],
     include_directories : [ configinc ],
     c_args : codec_args,
     dependencies : [ spa_dep, fdk_aac_dep ],
@@ -86,7 +91,7 @@ endif
 
 if aptx_dep.found()
   bluez_codec_aptx = shared_library('spa-codec-bluez5-aptx',
-    [ 'a2dp-codec-aptx.c', 'a2dp-codecs.c' ],
+    [ 'a2dp-codec-aptx.c', 'media-codecs.c' ],
     include_directories : [ configinc ],
     c_args : codec_args,
     dependencies : [ spa_dep, aptx_dep, sbc_dep ],
@@ -102,7 +107,7 @@ if ldac_dep.found()
     ldac_dep += ldac_abr_dep
   endif
   bluez_codec_ldac = shared_library('spa-codec-bluez5-ldac',
-    [ 'a2dp-codec-ldac.c', 'a2dp-codecs.c' ],
+    [ 'a2dp-codec-ldac.c', 'media-codecs.c' ],
     include_directories : [ configinc ],
     c_args : ldac_args,
     dependencies : [ spa_dep, ldac_dep ],
@@ -112,7 +117,7 @@ endif
 
 if get_option('bluez5-codec-lc3plus').allowed() and lc3plus_dep.found()
   bluez_codec_lc3plus = shared_library('spa-codec-bluez5-lc3plus',
-    [ 'a2dp-codec-lc3plus.c', 'a2dp-codecs.c' ],
+    [ 'a2dp-codec-lc3plus.c', 'media-codecs.c' ],
     include_directories : [ configinc ],
     c_args : codec_args,
     dependencies : [ spa_dep, lc3plus_dep, mathlib ],
@@ -124,10 +129,20 @@ 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' ],
+    [ 'a2dp-codec-opus.c', 'media-codecs.c' ],
     include_directories : [ configinc ],
     c_args : opus_args,
     dependencies : [ spa_dep, opus_dep, mathlib ],
     install : true,
     install_dir : spa_plugindir / 'bluez5')
 endif
+
+if get_option('bluez5-codec-lc3').allowed() and lc3_dep.found()
+  bluez_codec_lc3 = shared_library('spa-codec-bluez5-lc3',
+    [ 'bap-codec-lc3.c', 'media-codecs.c' ],
+    include_directories : [ configinc ],
+    c_args : codec_args,
+    dependencies : [ spa_dep, lc3_dep, mathlib ],
+    install : true,
+    install_dir : spa_plugindir / 'bluez5')
+endif
diff --git a/spa/plugins/bluez5/modemmanager.c b/spa/plugins/bluez5/modemmanager.c
new file mode 100644
index 0000000000000000000000000000000000000000..d9df95ac87a69a1338c63328b0aec9d54836f8af
--- /dev/null
+++ b/spa/plugins/bluez5/modemmanager.c
@@ -0,0 +1,1249 @@
+/* Spa Bluez5 ModemManager proxy
+ *
+ * Copyright © 2022 Collabora
+ *
+ * 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/utils/string.h>
+
+#include <ModemManager.h>
+
+#include "modemmanager.h"
+
+#define DBUS_INTERFACE_OBJECTMANAGER "org.freedesktop.DBus.ObjectManager"
+
+struct modem {
+	char *path;
+	bool network_has_service;
+	unsigned int signal_strength;
+};
+
+struct impl {
+	struct spa_bt_monitor *monitor;
+
+	struct spa_log *log;
+	DBusConnection *conn;
+
+	char *allowed_modem_device;
+	bool filters_added;
+	DBusPendingCall *pending;
+	DBusPendingCall *voice_pending;
+
+	const struct mm_ops *ops;
+	void *user_data;
+
+	struct modem modem;
+	struct spa_list call_list;
+};
+
+struct dbus_cmd_data {
+	struct impl *this;
+	struct call *call;
+	void *user_data;
+};
+
+static bool mm_dbus_connection_send_with_reply(struct impl *this, DBusMessage *m, DBusPendingCall **pending_return,
+                                               DBusPendingCallNotifyFunction function, void *user_data)
+{
+	dbus_bool_t dbus_ret;
+
+	spa_assert(*pending_return == NULL);
+
+	dbus_ret = dbus_connection_send_with_reply(this->conn, m, pending_return, -1);
+	if (!dbus_ret || *pending_return == NULL) {
+		spa_log_debug(this->log, "dbus call failure");
+		return false;
+	}
+
+	dbus_ret = dbus_pending_call_set_notify(*pending_return, function, user_data, NULL);
+	if (!dbus_ret) {
+		spa_log_debug(this->log, "dbus set notify failure");
+		dbus_pending_call_cancel(*pending_return);
+		dbus_pending_call_unref(*pending_return);
+		*pending_return = NULL;
+		return false;
+	}
+
+	return true;
+}
+
+static int mm_state_to_clcc(struct impl *this, MMCallState state)
+{
+	switch (state) {
+	case MM_CALL_STATE_DIALING:
+		return CLCC_DIALING;
+	case MM_CALL_STATE_RINGING_OUT:
+		return CLCC_ALERTING;
+	case MM_CALL_STATE_RINGING_IN:
+		return CLCC_INCOMING;
+	case MM_CALL_STATE_ACTIVE:
+		return CLCC_ACTIVE;
+	case MM_CALL_STATE_HELD:
+		return CLCC_HELD;
+	case MM_CALL_STATE_WAITING:
+		return CLCC_WAITING;
+	case MM_CALL_STATE_TERMINATED:
+	case MM_CALL_STATE_UNKNOWN:
+	default:
+		return -1;
+	}
+}
+
+static void mm_call_state_changed(struct impl *this)
+{
+	struct call *call;
+	bool call_indicator = false;
+	enum call_setup call_setup_indicator = CIND_CALLSETUP_NONE;
+
+	spa_list_for_each(call, &this->call_list, link) {
+		call_indicator |= (call->state == CLCC_ACTIVE);
+
+		if (call->state == CLCC_INCOMING && call_setup_indicator < CIND_CALLSETUP_INCOMING)
+			call_setup_indicator = CIND_CALLSETUP_INCOMING;
+		else if (call->state == CLCC_DIALING && call_setup_indicator < CIND_CALLSETUP_DIALING)
+			call_setup_indicator = CIND_CALLSETUP_DIALING;
+		else if (call->state == CLCC_ALERTING && call_setup_indicator < CIND_CALLSETUP_ALERTING)
+			call_setup_indicator = CIND_CALLSETUP_ALERTING;
+	}
+
+	if (this->ops->set_call_active)
+		this->ops->set_call_active(call_indicator, this->user_data);
+
+	if (this->ops->set_call_setup)
+		this->ops->set_call_setup(call_setup_indicator, this->user_data);
+}
+
+static void mm_get_call_properties_reply(DBusPendingCall *pending, void *user_data)
+{
+	struct call *call = user_data;
+	struct impl *this = call->this;
+	DBusMessage *r;
+	DBusMessageIter arg_i, element_i;
+	MMCallDirection direction;
+	MMCallState state;
+
+	spa_assert(call->pending == pending);
+	dbus_pending_call_unref(pending);
+	call->pending = NULL;
+
+	r = dbus_pending_call_steal_reply(pending);
+	if (r == NULL)
+		return;
+
+	if (dbus_message_is_error(r, DBUS_ERROR_UNKNOWN_METHOD)) {
+		spa_log_warn(this->log, "ModemManager D-Bus Call not available");
+		goto finish;
+	}
+	if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) {
+		spa_log_error(this->log, "GetAll() failed: %s", dbus_message_get_error_name(r));
+		goto finish;
+	}
+
+	if (!dbus_message_iter_init(r, &arg_i) || !spa_streq(dbus_message_get_signature(r), "a{sv}")) {
+		spa_log_error(this->log, "Invalid arguments in GetAll() reply");
+		goto finish;
+	}
+
+	spa_log_debug(this->log, "Call path: %s", call->path);
+
+	dbus_message_iter_recurse(&arg_i, &element_i);
+	while (dbus_message_iter_get_arg_type(&element_i) != DBUS_TYPE_INVALID) {
+		DBusMessageIter i, value_i;
+		const char *key;
+
+		dbus_message_iter_recurse(&element_i, &i);
+
+		dbus_message_iter_get_basic(&i, &key);
+		dbus_message_iter_next(&i);
+		dbus_message_iter_recurse(&i, &value_i);
+
+		if (spa_streq(key, MM_CALL_PROPERTY_DIRECTION)) {
+			dbus_message_iter_get_basic(&value_i, &direction);
+			spa_log_debug(this->log, "Call direction: %u", direction);
+			call->direction = (direction == MM_CALL_DIRECTION_INCOMING) ? CALL_INCOMING : CALL_OUTGOING;
+		} else if (spa_streq(key, MM_CALL_PROPERTY_NUMBER)) {
+			char *number;
+
+			dbus_message_iter_get_basic(&value_i, &number);
+			spa_log_debug(this->log, "Call number: %s", number);
+			if (call->number)
+				free(call->number);
+			call->number = strdup(number);
+		} else if (spa_streq(key, MM_CALL_PROPERTY_STATE)) {
+			int clcc_state;
+
+			dbus_message_iter_get_basic(&value_i, &state);
+			spa_log_debug(this->log, "Call state: %u", state);
+			clcc_state = mm_state_to_clcc(this, state);
+			if (clcc_state < 0) {
+				spa_log_debug(this->log, "Unsupported modem state: %s, state=%d", call->path, call->state);
+			} else {
+				call->state = clcc_state;
+				mm_call_state_changed(this);
+			}
+		}
+
+		dbus_message_iter_next(&element_i);
+	}
+
+finish:
+	dbus_message_unref(r);
+}
+
+static DBusHandlerResult mm_parse_voice_properties(struct impl *this, DBusMessageIter *props_i)
+{
+	while (dbus_message_iter_get_arg_type(props_i) != DBUS_TYPE_INVALID) {
+		DBusMessageIter i, value_i, element_i;
+		const char *key;
+
+		dbus_message_iter_recurse(props_i, &i);
+
+		dbus_message_iter_get_basic(&i, &key);
+		dbus_message_iter_next(&i);
+		dbus_message_iter_recurse(&i, &value_i);
+
+		if (spa_streq(key, MM_MODEM_VOICE_PROPERTY_CALLS)) {
+			spa_log_debug(this->log, "Voice properties");
+			dbus_message_iter_recurse(&value_i, &element_i);
+
+			while (dbus_message_iter_get_arg_type(&element_i) == DBUS_TYPE_OBJECT_PATH) {
+				const char *call_object;
+
+				dbus_message_iter_get_basic(&element_i, &call_object);
+				spa_log_debug(this->log, "  Call: %s", call_object);
+
+				dbus_message_iter_next(&element_i);
+			}
+		}
+
+		dbus_message_iter_next(props_i);
+	}
+
+	return DBUS_HANDLER_RESULT_HANDLED;
+}
+
+static DBusHandlerResult mm_parse_modem3gpp_properties(struct impl *this, DBusMessageIter *props_i)
+{
+	while (dbus_message_iter_get_arg_type(props_i) != DBUS_TYPE_INVALID) {
+		DBusMessageIter i, value_i;
+		const char *key;
+
+		dbus_message_iter_recurse(props_i, &i);
+
+		dbus_message_iter_get_basic(&i, &key);
+		dbus_message_iter_next(&i);
+		dbus_message_iter_recurse(&i, &value_i);
+
+		if (spa_streq(key, MM_MODEM_MODEM3GPP_PROPERTY_OPERATORNAME)) {
+			char *operator_name;
+
+			dbus_message_iter_get_basic(&value_i, &operator_name);
+			spa_log_debug(this->log, "Network operator code: %s", operator_name);
+			if (this->ops->set_modem_operator_name)
+				this->ops->set_modem_operator_name(operator_name, this->user_data);
+		} else if (spa_streq(key, MM_MODEM_MODEM3GPP_PROPERTY_REGISTRATIONSTATE)) {
+			MMModem3gppRegistrationState state;
+			bool is_roaming;
+
+			dbus_message_iter_get_basic(&value_i, &state);
+			spa_log_debug(this->log, "Registration state: %d", state);
+
+			if (state == MM_MODEM_3GPP_REGISTRATION_STATE_ROAMING ||
+			      state == MM_MODEM_3GPP_REGISTRATION_STATE_ROAMING_CSFB_NOT_PREFERRED ||
+				  state == MM_MODEM_3GPP_REGISTRATION_STATE_ROAMING_SMS_ONLY)
+				is_roaming = true;
+			else
+				is_roaming = false;
+
+			if (this->ops->set_modem_roaming)
+				this->ops->set_modem_roaming(is_roaming, this->user_data);
+		}
+
+		dbus_message_iter_next(props_i);
+	}
+
+	return DBUS_HANDLER_RESULT_HANDLED;
+}
+
+static DBusHandlerResult mm_parse_modem_properties(struct impl *this, DBusMessageIter *props_i)
+{
+	while (dbus_message_iter_get_arg_type(props_i) != DBUS_TYPE_INVALID) {
+		DBusMessageIter i, value_i;
+		const char *key;
+
+		dbus_message_iter_recurse(props_i, &i);
+
+		dbus_message_iter_get_basic(&i, &key);
+		dbus_message_iter_next(&i);
+		dbus_message_iter_recurse(&i, &value_i);
+
+		if(spa_streq(key, MM_MODEM_PROPERTY_EQUIPMENTIDENTIFIER)) {
+			char *imei;
+
+			dbus_message_iter_get_basic(&value_i, &imei);
+			spa_log_debug(this->log, "Modem IMEI: %s", imei);
+		} else if(spa_streq(key, MM_MODEM_PROPERTY_MANUFACTURER)) {
+			char *manufacturer;
+
+			dbus_message_iter_get_basic(&value_i, &manufacturer);
+			spa_log_debug(this->log, "Modem manufacturer: %s", manufacturer);
+		} else if(spa_streq(key, MM_MODEM_PROPERTY_MODEL)) {
+			char *model;
+
+			dbus_message_iter_get_basic(&value_i, &model);
+			spa_log_debug(this->log, "Modem model: %s", model);
+		} else if (spa_streq(key, MM_MODEM_PROPERTY_OWNNUMBERS)) {
+			char *number;
+			DBusMessageIter array_i;
+
+			dbus_message_iter_recurse(&value_i, &array_i);
+			if (dbus_message_iter_get_arg_type(&array_i) == DBUS_TYPE_STRING) {
+				dbus_message_iter_get_basic(&array_i, &number);
+				spa_log_debug(this->log, "Modem own number: %s", number);
+				if (this->ops->set_modem_own_number)
+					this->ops->set_modem_own_number(number, this->user_data);
+			}
+		} else if(spa_streq(key, MM_MODEM_PROPERTY_REVISION)) {
+			char *revision;
+
+			dbus_message_iter_get_basic(&value_i, &revision);
+			spa_log_debug(this->log, "Modem revision: %s", revision);
+		} else if(spa_streq(key, MM_MODEM_PROPERTY_SIGNALQUALITY)) {
+			unsigned int percentage, signal_strength;
+			DBusMessageIter struct_i;
+
+			dbus_message_iter_recurse(&value_i, &struct_i);
+			if (dbus_message_iter_get_arg_type(&struct_i) == DBUS_TYPE_UINT32) {
+				dbus_message_iter_get_basic(&struct_i, &percentage);
+				signal_strength = (unsigned int) round(percentage / 20.0);
+				spa_log_debug(this->log, "Network signal strength: %d (%d)", percentage, signal_strength);
+				if(this->ops->set_modem_signal_strength)
+					this->ops->set_modem_signal_strength(signal_strength, this->user_data);
+			}
+		} else if(spa_streq(key, MM_MODEM_PROPERTY_STATE)) {
+			MMModemState state;
+			bool has_service;
+
+			dbus_message_iter_get_basic(&value_i, &state);
+			spa_log_debug(this->log, "Network state: %d", state);
+
+			has_service = (state >= MM_MODEM_STATE_REGISTERED);
+			if (this->ops->set_modem_service)
+				this->ops->set_modem_service(has_service, this->user_data);
+		}
+
+		dbus_message_iter_next(props_i);
+	}
+
+	return DBUS_HANDLER_RESULT_HANDLED;
+}
+
+static DBusHandlerResult mm_parse_interfaces(struct impl *this, DBusMessageIter *dict_i)
+{
+	DBusMessageIter element_i, props_i;
+	const char *path;
+
+	spa_assert(this);
+	spa_assert(dict_i);
+
+	dbus_message_iter_get_basic(dict_i, &path);
+	dbus_message_iter_next(dict_i);
+	dbus_message_iter_recurse(dict_i, &element_i);
+
+	while (dbus_message_iter_get_arg_type(&element_i) == DBUS_TYPE_DICT_ENTRY) {
+		DBusMessageIter iface_i;
+		const char *interface;
+
+		dbus_message_iter_recurse(&element_i, &iface_i);
+		dbus_message_iter_get_basic(&iface_i, &interface);
+		dbus_message_iter_next(&iface_i);
+		spa_assert(dbus_message_iter_get_arg_type(&iface_i) == DBUS_TYPE_ARRAY);
+
+		dbus_message_iter_recurse(&iface_i, &props_i);
+
+		if (spa_streq(interface, MM_DBUS_INTERFACE_MODEM)) {
+			spa_log_debug(this->log, "Found Modem interface %s, path %s", interface, path);
+			if (this->modem.path == NULL) {
+				if (this->allowed_modem_device) {
+					DBusMessageIter i;
+
+					dbus_message_iter_recurse(&iface_i, &i);
+					while (dbus_message_iter_get_arg_type(&i) != DBUS_TYPE_INVALID) {
+						DBusMessageIter key_i, value_i;
+						const char *key;
+
+						dbus_message_iter_recurse(&i, &key_i);
+
+						dbus_message_iter_get_basic(&key_i, &key);
+						dbus_message_iter_next(&key_i);
+						dbus_message_iter_recurse(&key_i, &value_i);
+
+						if (spa_streq(key, MM_MODEM_PROPERTY_DEVICE)) {
+							char *device;
+
+							dbus_message_iter_get_basic(&value_i, &device);
+							if (!spa_streq(this->allowed_modem_device, device)) {
+								spa_log_debug(this->log, "Modem not allowed: %s", device);
+								goto next;
+							}
+						}
+						dbus_message_iter_next(&i);
+					}
+				}
+				this->modem.path = strdup(path);
+			} else if (!spa_streq(this->modem.path, path)) {
+				spa_log_debug(this->log, "A modem is already registered");
+				goto next;
+			}
+			mm_parse_modem_properties(this, &props_i);
+		} else if (spa_streq(interface, MM_DBUS_INTERFACE_MODEM_MODEM3GPP)) {
+			if (spa_streq(this->modem.path, path)) {
+				spa_log_debug(this->log, "Found Modem3GPP interface %s, path %s", interface, path);
+				mm_parse_modem3gpp_properties(this, &props_i);
+			}
+		} else if (spa_streq(interface, MM_DBUS_INTERFACE_MODEM_VOICE)) {
+			if (spa_streq(this->modem.path, path)) {
+				spa_log_debug(this->log, "Found Voice interface %s, path %s", interface, path);
+				mm_parse_voice_properties(this, &props_i);
+			}
+		}
+
+next:
+		dbus_message_iter_next(&element_i);
+	}
+
+	return DBUS_HANDLER_RESULT_HANDLED;
+}
+
+static void mm_get_managed_objects_reply(DBusPendingCall *pending, void *user_data)
+{
+	struct impl *this = user_data;
+	DBusMessage *r;
+	DBusMessageIter i, array_i;
+
+	spa_assert(this->pending == pending);
+	dbus_pending_call_unref(pending);
+	this->pending = NULL;
+
+	r = dbus_pending_call_steal_reply(pending);
+	if (r == NULL)
+		return;
+
+	if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) {
+		spa_log_error(this->log, "Failed to get a list of endpoints from ModemManager: %s",
+				dbus_message_get_error_name(r));
+		goto finish;
+	}
+
+	if (!dbus_message_iter_init(r, &i) || !spa_streq(dbus_message_get_signature(r), "a{oa{sa{sv}}}")) {
+		spa_log_error(this->log, "Invalid arguments in GetManagedObjects() reply");
+		goto finish;
+	}
+
+	dbus_message_iter_recurse(&i, &array_i);
+	while (dbus_message_iter_get_arg_type(&array_i) != DBUS_TYPE_INVALID) {
+			DBusMessageIter dict_i;
+
+			dbus_message_iter_recurse(&array_i, &dict_i);
+			mm_parse_interfaces(this, &dict_i);
+			dbus_message_iter_next(&array_i);
+	}
+
+finish:
+	dbus_message_unref(r);
+}
+
+static void call_free(struct call *call) {
+	spa_list_remove(&call->link);
+
+	if (call->pending != NULL) {
+		dbus_pending_call_cancel(call->pending);
+		dbus_pending_call_unref(call->pending);
+	}
+
+	if (call->number)
+		free(call->number);
+	if (call->path)
+		free(call->path);
+	free(call);
+}
+
+static void mm_clean_voice(struct impl *this)
+{
+	struct call *call;
+
+	spa_list_consume(call, &this->call_list, link)
+		call_free(call);
+
+	if (this->voice_pending != NULL) {
+		dbus_pending_call_cancel(this->voice_pending);
+		dbus_pending_call_unref(this->voice_pending);
+	}
+
+	if (this->ops->set_call_setup)
+		this->ops->set_call_setup(CIND_CALLSETUP_NONE, this->user_data);
+	if (this->ops->set_call_active)
+		this->ops->set_call_active(false, this->user_data);
+}
+
+static void mm_clean_modem3gpp(struct impl *this)
+{
+	if (this->ops->set_modem_operator_name)
+		this->ops->set_modem_operator_name(NULL, this->user_data);
+	if (this->ops->set_modem_roaming)
+		this->ops->set_modem_roaming(false, this->user_data);
+}
+
+static void mm_clean_modem(struct impl *this)
+{
+	if (this->modem.path) {
+		free(this->modem.path);
+		this->modem.path = NULL;
+	}
+	if(this->ops->set_modem_signal_strength)
+		this->ops->set_modem_signal_strength(0, this->user_data);
+	if (this->ops->set_modem_service)
+		this->ops->set_modem_service(false, this->user_data);
+	this->modem.network_has_service = false;
+}
+
+static DBusHandlerResult mm_filter_cb(DBusConnection *bus, DBusMessage *m, void *user_data)
+{
+	struct impl *this = user_data;
+	DBusError err;
+
+	dbus_error_init(&err);
+
+	if (dbus_message_is_signal(m, "org.freedesktop.DBus", "NameOwnerChanged")) {
+		const char *name, *old_owner, *new_owner;
+
+		spa_log_debug(this->log, "Name owner changed %s", dbus_message_get_path(m));
+
+		if (!dbus_message_get_args(m, &err,
+					   DBUS_TYPE_STRING, &name,
+					   DBUS_TYPE_STRING, &old_owner,
+					   DBUS_TYPE_STRING, &new_owner,
+					   DBUS_TYPE_INVALID)) {
+			spa_log_error(this->log, "Failed to parse org.freedesktop.DBus.NameOwnerChanged: %s", err.message);
+			goto finish;
+		}
+
+		if (spa_streq(name, MM_DBUS_SERVICE)) {
+			if (old_owner && *old_owner) {
+				spa_log_debug(this->log, "ModemManager daemon disappeared (%s)", old_owner);
+				mm_clean_voice(this);
+				mm_clean_modem3gpp(this);
+				mm_clean_modem(this);
+			}
+
+			if (new_owner && *new_owner)
+				spa_log_debug(this->log, "ModemManager daemon appeared (%s)", new_owner);
+		}
+	} else if (dbus_message_is_signal(m, DBUS_INTERFACE_OBJECTMANAGER, DBUS_SIGNAL_INTERFACES_ADDED)) {
+		DBusMessageIter arg_i;
+
+		spa_log_warn(this->log, "sender: %s", dbus_message_get_sender(m));
+
+		if (!dbus_message_iter_init(m, &arg_i) || !spa_streq(dbus_message_get_signature(m), "oa{sa{sv}}")) {
+				spa_log_error(this->log, "Invalid signature found in InterfacesAdded");
+				goto finish;
+		}
+
+		mm_parse_interfaces(this, &arg_i);
+	} else if (dbus_message_is_signal(m, DBUS_INTERFACE_OBJECTMANAGER, DBUS_SIGNAL_INTERFACES_REMOVED)) {
+		const char *path;
+		DBusMessageIter arg_i, element_i;
+
+		if (!dbus_message_iter_init(m, &arg_i) || !spa_streq(dbus_message_get_signature(m), "oas")) {
+				spa_log_error(this->log, "Invalid signature found in InterfacesRemoved");
+				goto finish;
+		}
+
+		dbus_message_iter_get_basic(&arg_i, &path);
+		if (!spa_streq(this->modem.path, path))
+			goto finish;
+
+		dbus_message_iter_next(&arg_i);
+		dbus_message_iter_recurse(&arg_i, &element_i);
+
+		while (dbus_message_iter_get_arg_type(&element_i) == DBUS_TYPE_STRING) {
+				const char *iface;
+
+				dbus_message_iter_get_basic(&element_i, &iface);
+
+				spa_log_debug(this->log, "Interface removed %s", path);
+				if (spa_streq(iface, MM_DBUS_INTERFACE_MODEM)) {
+					spa_log_debug(this->log, "Modem interface %s removed, path %s", iface, path);
+					mm_clean_modem(this);
+				} else if (spa_streq(iface, MM_DBUS_INTERFACE_MODEM_MODEM3GPP)) {
+					spa_log_debug(this->log, "Modem3GPP interface %s removed, path %s", iface, path);
+					mm_clean_modem3gpp(this);
+				} else if (spa_streq(iface, MM_DBUS_INTERFACE_MODEM_VOICE)) {
+					spa_log_debug(this->log, "Voice interface %s removed, path %s", iface, path);
+					mm_clean_voice(this);
+				}
+
+				dbus_message_iter_next(&element_i);
+		}
+	} else if (dbus_message_is_signal(m, DBUS_INTERFACE_PROPERTIES, DBUS_SIGNAL_PROPERTIES_CHANGED)) {
+		const char *path;
+		DBusMessageIter iface_i, props_i;
+		const char *interface;
+
+		path = dbus_message_get_path(m);
+		if (!spa_streq(this->modem.path, path))
+			goto finish;
+
+		if (!dbus_message_iter_init(m, &iface_i) || !spa_streq(dbus_message_get_signature(m), "sa{sv}as")) {
+				spa_log_error(this->log, "Invalid signature found in PropertiesChanged");
+				goto finish;
+		}
+
+		dbus_message_iter_get_basic(&iface_i, &interface);
+		dbus_message_iter_next(&iface_i);
+		spa_assert(dbus_message_iter_get_arg_type(&iface_i) == DBUS_TYPE_ARRAY);
+
+		dbus_message_iter_recurse(&iface_i, &props_i);
+
+		if (spa_streq(interface, MM_DBUS_INTERFACE_MODEM)) {
+			spa_log_debug(this->log, "Properties changed on %s", path);
+			mm_parse_modem_properties(this, &props_i);
+		} else if (spa_streq(interface, MM_DBUS_INTERFACE_MODEM_MODEM3GPP)) {
+			spa_log_debug(this->log, "Properties changed on %s", path);
+			mm_parse_modem3gpp_properties(this, &props_i);
+		} else if (spa_streq(interface, MM_DBUS_INTERFACE_MODEM_VOICE)) {
+			spa_log_debug(this->log, "Properties changed on %s", path);
+			mm_parse_voice_properties(this, &props_i);
+		}
+	} else if (dbus_message_is_signal(m, MM_DBUS_INTERFACE_MODEM_VOICE, MM_MODEM_VOICE_SIGNAL_CALLADDED)) {
+		DBusMessageIter iface_i;
+		const char *path;
+		struct call *call_object;
+		const char *mm_call_interface = MM_DBUS_INTERFACE_CALL;
+
+		if (!spa_streq(this->modem.path, dbus_message_get_path(m)))
+			goto finish;
+
+		if (!dbus_message_iter_init(m, &iface_i) || !spa_streq(dbus_message_get_signature(m), "o")) {
+				spa_log_error(this->log, "Invalid signature found in %s", MM_MODEM_VOICE_SIGNAL_CALLADDED);
+				goto finish;
+		}
+
+		dbus_message_iter_get_basic(&iface_i, &path);
+		spa_log_debug(this->log, "New call: %s", path);
+
+		call_object = calloc(1, sizeof(struct call));
+		if (call_object == NULL)
+			return DBUS_HANDLER_RESULT_NEED_MEMORY;
+		call_object->this = this;
+		call_object->path = strdup(path);
+		spa_list_append(&this->call_list, &call_object->link);
+
+		m = dbus_message_new_method_call(MM_DBUS_SERVICE, path, DBUS_INTERFACE_PROPERTIES, "GetAll");
+		if (m == NULL)
+			goto finish;
+		dbus_message_append_args(m, DBUS_TYPE_STRING, &mm_call_interface, DBUS_TYPE_INVALID);
+		if (!mm_dbus_connection_send_with_reply(this, m, &call_object->pending, mm_get_call_properties_reply, call_object)) {
+			spa_log_error(this->log, "dbus call failure");
+			dbus_message_unref(m);
+			goto finish;
+		}
+	} else if (dbus_message_is_signal(m, MM_DBUS_INTERFACE_MODEM_VOICE, MM_MODEM_VOICE_SIGNAL_CALLDELETED)) {
+		const char *path;
+		DBusMessageIter iface_i;
+		struct call *call, *call_tmp;
+
+		if (!spa_streq(this->modem.path, dbus_message_get_path(m)))
+			goto finish;
+
+		if (!dbus_message_iter_init(m, &iface_i) || !spa_streq(dbus_message_get_signature(m), "o")) {
+				spa_log_error(this->log, "Invalid signature found in %s", MM_MODEM_VOICE_SIGNAL_CALLDELETED);
+				goto finish;
+		}
+
+		dbus_message_iter_get_basic(&iface_i, &path);
+		spa_log_debug(this->log, "Call ended: %s", path);
+
+		spa_list_for_each_safe(call, call_tmp, &this->call_list, link) {
+			if (spa_streq(call->path, path))
+				call_free(call);
+		}
+		mm_call_state_changed(this);
+	} else if (dbus_message_is_signal(m, MM_DBUS_INTERFACE_CALL, MM_CALL_SIGNAL_STATECHANGED)) {
+		const char *path;
+		DBusMessageIter iface_i;
+		MMCallState old, new;
+		MMCallStateReason reason;
+		struct call *call = NULL, *call_tmp;
+		int clcc_state;
+
+		if (!dbus_message_iter_init(m, &iface_i) || !spa_streq(dbus_message_get_signature(m), "iiu")) {
+				spa_log_error(this->log, "Invalid signature found in %s", MM_CALL_SIGNAL_STATECHANGED);
+				goto finish;
+		}
+
+		path = dbus_message_get_path(m);
+
+		dbus_message_iter_get_basic(&iface_i, &old);
+		dbus_message_iter_next(&iface_i);
+		dbus_message_iter_get_basic(&iface_i, &new);
+		dbus_message_iter_next(&iface_i);
+		dbus_message_iter_get_basic(&iface_i, &reason);
+
+		spa_log_debug(this->log, "Call state %s changed to %d (old = %d, reason = %u)", path, new, old, reason);
+
+		spa_list_for_each(call_tmp, &this->call_list, link) {
+			if (spa_streq(call_tmp->path, path)) {
+				call = call_tmp;
+				break;
+			}
+		}
+
+		if (call == NULL) {
+			spa_log_warn(this->log, "No call reference for %s", path);
+			goto finish;
+		}
+
+		clcc_state = mm_state_to_clcc(this, new);
+		if (clcc_state < 0) {
+			spa_log_debug(this->log, "Unsupported modem state: %s, state=%d", call->path, call->state);
+		} else {
+			call->state = clcc_state;
+			mm_call_state_changed(this);
+		}
+	}
+
+finish:
+	return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+}
+
+static int add_filters(struct impl *this)
+{
+	DBusError err;
+
+	if (this->filters_added)
+		return 0;
+
+	dbus_error_init(&err);
+
+	if (!dbus_connection_add_filter(this->conn, mm_filter_cb, this, NULL)) {
+		spa_log_error(this->log, "failed to add filter function");
+		goto fail;
+	}
+
+	dbus_bus_add_match(this->conn,
+			"type='signal',sender='org.freedesktop.DBus',"
+			"interface='org.freedesktop.DBus',member='NameOwnerChanged'," "arg0='" MM_DBUS_SERVICE "'", &err);
+	dbus_bus_add_match(this->conn,
+			"type='signal',sender='" MM_DBUS_SERVICE "',"
+			"interface='" DBUS_INTERFACE_OBJECTMANAGER "',member='" DBUS_SIGNAL_INTERFACES_ADDED "'", &err);
+	dbus_bus_add_match(this->conn,
+			"type='signal',sender='" MM_DBUS_SERVICE "',"
+			"interface='" DBUS_INTERFACE_OBJECTMANAGER "',member='" DBUS_SIGNAL_INTERFACES_REMOVED "'", &err);
+	dbus_bus_add_match(this->conn,
+			"type='signal',sender='" MM_DBUS_SERVICE "',"
+			"interface='" DBUS_INTERFACE_PROPERTIES "',member='" DBUS_SIGNAL_PROPERTIES_CHANGED "'", &err);
+	dbus_bus_add_match(this->conn,
+			"type='signal',sender='" MM_DBUS_SERVICE "',"
+			"interface='" MM_DBUS_INTERFACE_MODEM_VOICE "',member='" MM_MODEM_VOICE_SIGNAL_CALLADDED "'", &err);
+	dbus_bus_add_match(this->conn,
+			"type='signal',sender='" MM_DBUS_SERVICE "',"
+			"interface='" MM_DBUS_INTERFACE_MODEM_VOICE "',member='" MM_MODEM_VOICE_SIGNAL_CALLDELETED "'", &err);
+	dbus_bus_add_match(this->conn,
+			"type='signal',sender='" MM_DBUS_SERVICE "',"
+			"interface='" MM_DBUS_INTERFACE_CALL "',member='" MM_CALL_SIGNAL_STATECHANGED "'", &err);
+
+	this->filters_added = true;
+
+	return 0;
+
+fail:
+	dbus_error_free(&err);
+	return -EIO;
+}
+
+static bool is_dbus_service_available(struct impl *this, const char *service)
+{
+	DBusMessage *m, *r;
+	DBusError err;
+	bool success = false;
+
+	m = dbus_message_new_method_call("org.freedesktop.DBus", "/org/freedesktop/DBus",
+	                                  "org.freedesktop.DBus", "NameHasOwner");
+	if (m == NULL)
+		return false;
+	dbus_message_append_args(m, DBUS_TYPE_STRING, &service, DBUS_TYPE_INVALID);
+
+	dbus_error_init(&err);
+	r = dbus_connection_send_with_reply_and_block(this->conn, m, -1, &err);
+	dbus_message_unref(m);
+	m = NULL;
+
+	if (r == NULL) {
+		spa_log_info(this->log, "NameHasOwner failed for %s", service);
+		dbus_error_free(&err);
+		goto finish;
+	}
+
+	if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) {
+		spa_log_error(this->log, "NameHasOwner() returned error: %s", dbus_message_get_error_name(r));
+		goto finish;
+	}
+
+	if (!dbus_message_get_args(r, &err,
+				   DBUS_TYPE_BOOLEAN, &success,
+				   DBUS_TYPE_INVALID)) {
+		spa_log_error(this->log, "Failed to parse NameHasOwner() reply: %s", err.message);
+		dbus_error_free(&err);
+		goto finish;
+	}
+
+finish:
+	if (r)
+		dbus_message_unref(r);
+
+	return success;
+}
+
+bool mm_is_available(void *modemmanager)
+{
+	struct impl *this = modemmanager;
+
+	if (this == NULL)
+		return false;
+
+	return this->modem.path != NULL;
+}
+
+unsigned int mm_supported_features()
+{
+	return SPA_BT_HFP_AG_FEATURE_REJECT_CALL | SPA_BT_HFP_AG_FEATURE_ENHANCED_CALL_STATUS;
+}
+
+static void mm_get_call_simple_reply(DBusPendingCall *pending, void *data)
+{
+	struct dbus_cmd_data *dbus_cmd_data = data;
+	struct impl *this = dbus_cmd_data->this;
+	struct call *call = dbus_cmd_data->call;
+	void *user_data = dbus_cmd_data->user_data;
+	DBusMessage *r;
+
+	free(data);
+
+	spa_assert(call->pending == pending);
+	dbus_pending_call_unref(pending);
+	call->pending = NULL;
+
+	r = dbus_pending_call_steal_reply(pending);
+	if (r == NULL)
+		return;
+
+	if (dbus_message_is_error(r, DBUS_ERROR_UNKNOWN_METHOD)) {
+		spa_log_warn(this->log, "ModemManager D-Bus method not available");
+		goto finish;
+	}
+	if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) {
+		spa_log_error(this->log, "ModemManager method failed: %s", dbus_message_get_error_name(r));
+		goto finish;
+	}
+
+	this->ops->send_cmd_result(true, 0, user_data);
+	return;
+
+finish:
+	this->ops->send_cmd_result(false, CMEE_AG_FAILURE, user_data);
+}
+
+static void mm_get_call_create_reply(DBusPendingCall *pending, void *data)
+{
+	struct dbus_cmd_data *dbus_cmd_data = data;
+	struct impl *this = dbus_cmd_data->this;
+	void *user_data = dbus_cmd_data->user_data;
+	DBusMessage *r;
+
+	free(data);
+
+	spa_assert(this->voice_pending == pending);
+	dbus_pending_call_unref(pending);
+	this->voice_pending = NULL;
+
+	r = dbus_pending_call_steal_reply(pending);
+	if (r == NULL)
+		return;
+
+	if (dbus_message_is_error(r, DBUS_ERROR_UNKNOWN_METHOD)) {
+		spa_log_warn(this->log, "ModemManager D-Bus method not available");
+		goto finish;
+	}
+	if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) {
+		spa_log_error(this->log, "ModemManager method failed: %s", dbus_message_get_error_name(r));
+		goto finish;
+	}
+
+	this->ops->send_cmd_result(true, 0, user_data);
+	return;
+
+finish:
+	this->ops->send_cmd_result(false, CMEE_AG_FAILURE, user_data);
+}
+
+bool mm_answer_call(void *modemmanager, void *user_data, enum cmee_error *error)
+{
+	struct impl *this = modemmanager;
+	struct call *call_object, *call_tmp;
+	struct dbus_cmd_data *data;
+	DBusMessage *m;
+
+	call_object = NULL;
+	spa_list_for_each(call_tmp, &this->call_list, link) {
+		if (call_tmp->state == CLCC_INCOMING) {
+			call_object = call_tmp;
+			break;
+		}
+	}
+	if (!call_object) {
+		spa_log_debug(this->log, "No ringing in call");
+		if (error)
+			*error = CMEE_OPERATION_NOT_ALLOWED;
+		return false;
+	}
+
+	data = malloc(sizeof(struct dbus_cmd_data));
+	if (!data) {
+		if (error)
+			*error = CMEE_AG_FAILURE;
+		return false;
+	}
+	data->this = this;
+	data->call = call_object;
+	data->user_data = user_data;
+
+	m = dbus_message_new_method_call(MM_DBUS_SERVICE, call_object->path, MM_DBUS_INTERFACE_CALL, MM_CALL_METHOD_ACCEPT);
+	if (m == NULL) {
+		if (error)
+			*error = CMEE_AG_FAILURE;
+		return false;
+	}
+	if (!mm_dbus_connection_send_with_reply(this, m, &call_object->pending, mm_get_call_simple_reply, data)) {
+		spa_log_error(this->log, "dbus call failure");
+		dbus_message_unref(m);
+		if (error)
+			*error = CMEE_AG_FAILURE;
+		return false;
+	}
+
+	return true;
+}
+
+bool mm_hangup_call(void *modemmanager, void *user_data, enum cmee_error *error)
+{
+	struct impl *this = modemmanager;
+	struct call *call_object, *call_tmp;
+	struct dbus_cmd_data *data;
+	DBusMessage *m;
+
+	call_object = NULL;
+	spa_list_for_each(call_tmp, &this->call_list, link) {
+		if (call_tmp->state == CLCC_ACTIVE) {
+			call_object = call_tmp;
+			break;
+		}
+	}
+	if (!call_object) {
+		spa_list_for_each(call_tmp, &this->call_list, link) {
+			if (call_tmp->state == CLCC_DIALING ||
+				call_tmp->state == CLCC_ALERTING ||
+				call_tmp->state == CLCC_INCOMING) {
+				call_object = call_tmp;
+				break;
+			}
+		}
+	}
+	if (!call_object) {
+		spa_log_debug(this->log, "No call to reject or hang up");
+		if (error)
+			*error = CMEE_OPERATION_NOT_ALLOWED;
+		return false;
+	}
+
+	data = malloc(sizeof(struct dbus_cmd_data));
+	if (!data) {
+		if (error)
+			*error = CMEE_AG_FAILURE;
+		return false;
+	}
+	data->this = this;
+	data->call = call_object;
+	data->user_data = user_data;
+
+	m = dbus_message_new_method_call(MM_DBUS_SERVICE, call_object->path, MM_DBUS_INTERFACE_CALL, MM_CALL_METHOD_HANGUP);
+	if (m == NULL) {
+		if (error)
+			*error = CMEE_AG_FAILURE;
+		return false;
+	}
+	if (!mm_dbus_connection_send_with_reply(this, m, &call_object->pending, mm_get_call_simple_reply, data)) {
+		spa_log_error(this->log, "dbus call failure");
+		dbus_message_unref(m);
+		if (error)
+			*error = CMEE_AG_FAILURE;
+		return false;
+	}
+
+	return true;
+}
+
+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, 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);
+	dbus_message_iter_close_container(&dict_entry_it, &variant_it);
+	dbus_message_iter_close_container(dict, &dict_entry_it);
+}
+
+static inline bool is_valid_dial_string_char(char c)
+{
+	return ('0' <= c && c <= '9')
+		|| ('A' <= c && c <= 'C')
+		|| c == '*'
+		|| c == '#'
+		|| c == '+';
+}
+
+bool mm_do_call(void *modemmanager, const char* number, void *user_data, enum cmee_error *error)
+{
+	struct impl *this = modemmanager;
+	struct dbus_cmd_data *data;
+	DBusMessage *m;
+	DBusMessageIter iter, dict;
+
+	for (size_t i = 0; number[i]; i++) {
+		if (!is_valid_dial_string_char(number[i])) {
+			spa_log_warn(this->log, "Call creation canceled, invalid character found in dial string: %c", number[i]);
+			if (error)
+				*error = CMEE_INVALID_CHARACTERS_DIAL_STRING;
+			return false;
+		}
+	}
+
+	data = malloc(sizeof(struct dbus_cmd_data));
+	if (!data) {
+		if (error)
+			*error = CMEE_AG_FAILURE;
+		return false;
+	}
+	data->this = this;
+	data->user_data = user_data;
+
+	m = dbus_message_new_method_call(MM_DBUS_SERVICE, this->modem.path, MM_DBUS_INTERFACE_MODEM_VOICE, MM_MODEM_VOICE_METHOD_CREATECALL);
+	if (m == NULL) {
+		if (error)
+			*error = CMEE_AG_FAILURE;
+		return false;
+	}
+	dbus_message_iter_init_append(m, &iter);
+	dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{sv}", &dict);
+	append_basic_variant_dict_entry(&dict, "number", DBUS_TYPE_STRING, "s", &number);
+	dbus_message_iter_close_container(&iter, &dict);
+	if (!mm_dbus_connection_send_with_reply(this, m, &this->voice_pending, mm_get_call_create_reply, data)) {
+		spa_log_error(this->log, "dbus call failure");
+		dbus_message_unref(m);
+		if (error)
+			*error = CMEE_AG_FAILURE;
+		return false;
+	}
+
+	return true;
+}
+
+bool mm_send_dtmf(void *modemmanager, const char *dtmf, void *user_data, enum cmee_error *error)
+{
+	struct impl *this = modemmanager;
+	struct call *call_object, *call_tmp;
+	struct dbus_cmd_data *data;
+	DBusMessage *m;
+
+	call_object = NULL;
+	spa_list_for_each(call_tmp, &this->call_list, link) {
+		if (call_tmp->state == CLCC_ACTIVE) {
+			call_object = call_tmp;
+			break;
+		}
+	}
+	if (!call_object) {
+		spa_log_debug(this->log, "No active call");
+		if (error)
+			*error = CMEE_OPERATION_NOT_ALLOWED;
+		return false;
+	}
+
+	/* Allowed dtmf characters: 0-9, *, #, A-D */
+	if (!((dtmf[0] >= '0' && dtmf[0] <= '9')
+	    || (dtmf[0] == '*')
+	    || (dtmf[0] == '#')
+	    || (dtmf[0] >= 'A' && dtmf[0] <= 'D'))) {
+		spa_log_debug(this->log, "Invalid DTMF character: %s", dtmf);
+		if (error)
+			*error = CMEE_INVALID_CHARACTERS_TEXT_STRING;
+		return false;
+	}
+
+	data = malloc(sizeof(struct dbus_cmd_data));
+	if (!data) {
+		if (error)
+			*error = CMEE_AG_FAILURE;
+		return false;
+	}
+	data->this = this;
+	data->call = call_object;
+	data->user_data = user_data;
+
+	m = dbus_message_new_method_call(MM_DBUS_SERVICE, call_object->path, MM_DBUS_INTERFACE_CALL, MM_CALL_METHOD_SENDDTMF);
+	if (m == NULL) {
+		if (error)
+			*error = CMEE_AG_FAILURE;
+		return false;
+	}
+	dbus_message_append_args(m, DBUS_TYPE_STRING, &dtmf, DBUS_TYPE_INVALID);
+	if (!mm_dbus_connection_send_with_reply(this, m, &call_object->pending, mm_get_call_simple_reply, data)) {
+		spa_log_error(this->log, "dbus call failure");
+		dbus_message_unref(m);
+		if (error)
+			*error = CMEE_AG_FAILURE;
+		return false;
+	}
+
+	return true;
+}
+
+const char *mm_get_incoming_call_number(void *modemmanager)
+{
+	struct impl *this = modemmanager;
+	struct call *call_object, *call_tmp;
+
+	call_object = NULL;
+	spa_list_for_each(call_tmp, &this->call_list, link) {
+		if (call_tmp->state == CLCC_INCOMING) {
+			call_object = call_tmp;
+			break;
+		}
+	}
+	if (!call_object) {
+		spa_log_debug(this->log, "No ringing in call");
+		return NULL;
+	}
+
+	return call_object->number;
+}
+
+struct spa_list *mm_get_calls(void *modemmanager)
+{
+	struct impl *this = modemmanager;
+
+	return &this->call_list;
+}
+
+void *mm_register(struct spa_log *log, void *dbus_connection, const struct spa_dict *info,
+                  const struct mm_ops *ops, void *user_data)
+{
+	struct impl *this;
+	const char *modem_device_str = NULL;
+	bool modem_device_found = false;
+
+	spa_assert(log);
+	spa_assert(dbus_connection);
+
+	if (info) {
+		if ((modem_device_str = spa_dict_lookup(info, "bluez5.hfphsp-backend-native-modem")) != NULL) {
+			if (!spa_streq(modem_device_str, "none"))
+				modem_device_found = true;
+		}
+	}
+	if (!modem_device_found) {
+		spa_log_info(log, "No modem allowed, doesn't link to ModemManager");
+		return NULL;
+	}
+
+	this = calloc(1, sizeof(struct impl));
+	if (this == NULL)
+		return NULL;
+
+	this->log = log;
+	this->conn = dbus_connection;
+	this->ops = ops;
+	this->user_data = user_data;
+	if (modem_device_str && !spa_streq(modem_device_str, "any"))
+		this->allowed_modem_device = strdup(modem_device_str);
+	spa_list_init(&this->call_list);
+
+	if (add_filters(this) < 0) {
+		goto fail;
+	}
+
+	if (is_dbus_service_available(this, MM_DBUS_SERVICE)) {
+		DBusMessage *m;
+
+		m = dbus_message_new_method_call(MM_DBUS_SERVICE, "/org/freedesktop/ModemManager1",
+		                                 DBUS_INTERFACE_OBJECTMANAGER, "GetManagedObjects");
+		if (m == NULL)
+			goto fail;
+
+		if (!mm_dbus_connection_send_with_reply(this, m, &this->pending, mm_get_managed_objects_reply, this)) {
+			spa_log_error(this->log, "dbus call failure");
+			dbus_message_unref(m);
+			goto fail;
+		}
+	}
+
+    return this;
+
+fail:
+    free(this);
+    return NULL;
+}
+
+void mm_unregister(void *data)
+{
+	struct impl *this = data;
+
+	if (this->pending != NULL) {
+		dbus_pending_call_cancel(this->pending);
+		dbus_pending_call_unref(this->pending);
+	}
+
+	mm_clean_voice(this);
+	mm_clean_modem3gpp(this);
+	mm_clean_modem(this);
+
+	if (this->filters_added) {
+		dbus_connection_remove_filter(this->conn, mm_filter_cb, this);
+		this->filters_added = false;
+	}
+
+	if (this->allowed_modem_device)
+		free(this->allowed_modem_device);
+
+	free(this);
+}
diff --git a/spa/plugins/bluez5/modemmanager.h b/spa/plugins/bluez5/modemmanager.h
new file mode 100644
index 0000000000000000000000000000000000000000..a239b2ace2a0b14fbe80dba80f506e3ee8c5e8c8
--- /dev/null
+++ b/spa/plugins/bluez5/modemmanager.h
@@ -0,0 +1,161 @@
+/* Spa Bluez5 ModemManager proxy
+ *
+ * Copyright © 2022 Collabora
+ *
+ * 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_BLUEZ5_MODEMMANAGER_H_
+#define SPA_BLUEZ5_MODEMMANAGER_H_
+
+#include <spa/utils/list.h>
+
+#include "defs.h"
+
+enum cmee_error {
+	CMEE_AG_FAILURE = 0,
+	CMEE_NO_CONNECTION_TO_PHONE = 1,
+	CMEE_OPERATION_NOT_ALLOWED = 3,
+	CMEE_OPERATION_NOT_SUPPORTED = 4,
+	CMEE_INVALID_CHARACTERS_TEXT_STRING = 25,
+	CMEE_INVALID_CHARACTERS_DIAL_STRING = 27,
+	CMEE_NO_NETWORK_SERVICE = 30
+};
+
+enum call_setup {
+	CIND_CALLSETUP_NONE = 0,
+	CIND_CALLSETUP_INCOMING,
+	CIND_CALLSETUP_DIALING,
+	CIND_CALLSETUP_ALERTING
+};
+
+enum call_direction {
+	CALL_OUTGOING,
+	CALL_INCOMING
+};
+
+enum call_state {
+	CLCC_ACTIVE,
+	CLCC_HELD,
+	CLCC_DIALING,
+	CLCC_ALERTING,
+	CLCC_INCOMING,
+	CLCC_WAITING,
+	CLCC_RESPONSE_AND_HOLD
+};
+
+struct call {
+	struct spa_list link;
+	unsigned int index;
+	struct impl *this;
+	DBusPendingCall *pending;
+
+	char *path;
+	char *number;
+	bool call_indicator;
+	enum call_direction direction;
+	enum call_state state;
+	bool multiparty;
+};
+
+struct mm_ops {
+	void (*send_cmd_result)(bool success, enum cmee_error error, void *user_data);
+	void (*set_modem_service)(bool available, void *user_data);
+	void (*set_modem_signal_strength)(unsigned int strength, void *user_data);
+	void (*set_modem_operator_name)(const char *name, void *user_data);
+	void (*set_modem_own_number)(const char *number, void *user_data);
+	void (*set_modem_roaming)(bool is_roaming, void *user_data);
+	void (*set_call_active)(bool active, void *user_data);
+	void (*set_call_setup)(enum call_setup value, void *user_data);
+};
+
+#ifdef HAVE_BLUEZ_5_BACKEND_NATIVE_MM
+void *mm_register(struct spa_log *log, void *dbus_connection, const struct spa_dict *info,
+                  const struct mm_ops *ops, void *user_data);
+void mm_unregister(void *data);
+bool mm_is_available(void *modemmanager);
+unsigned int mm_supported_features();
+bool mm_answer_call(void *modemmanager, void *user_data, enum cmee_error *error);
+bool mm_hangup_call(void *modemmanager, void *user_data, enum cmee_error *error);
+bool mm_do_call(void *modemmanager, const char* number, void *user_data, enum cmee_error *error);
+bool mm_send_dtmf(void *modemmanager, const char *dtmf, void *user_data, enum cmee_error *error);
+const char *mm_get_incoming_call_number(void *modemmanager);
+struct spa_list *mm_get_calls(void *modemmanager);
+#else
+void *mm_register(struct spa_log *log, void *dbus_connection, const struct spa_dict *info,
+                  const struct mm_ops *ops, void *user_data)
+{
+	return NULL;
+}
+
+void mm_unregister(void *data)
+{
+}
+
+bool mm_is_available(void *modemmanager)
+{
+	return false;
+}
+
+unsigned int mm_supported_features(void)
+{
+	return 0;
+}
+
+bool mm_answer_call(void *modemmanager, void *user_data, enum cmee_error *error)
+{
+	if (error)
+		*error = CMEE_OPERATION_NOT_SUPPORTED;
+	return false;
+}
+
+bool mm_hangup_call(void *modemmanager, void *user_data, enum cmee_error *error)
+{
+	if (error)
+		*error = CMEE_OPERATION_NOT_SUPPORTED;
+	return false;
+}
+
+bool mm_do_call(void *modemmanager, const char* number, void *user_data, enum cmee_error *error)
+{
+	if (error)
+		*error = CMEE_OPERATION_NOT_SUPPORTED;
+	return false;
+}
+
+bool mm_send_dtmf(void *modemmanager, const char *dtmf, void *user_data, enum cmee_error *error)
+{
+	if (error)
+		*error = CMEE_OPERATION_NOT_SUPPORTED;
+	return false;
+}
+
+const char *mm_get_incoming_call_number(void *modemmanager)
+{
+	return NULL;
+}
+
+struct spa_list *mm_get_calls(void *modemmanager)
+{
+	return NULL;
+}
+#endif
+
+#endif
diff --git a/spa/plugins/bluez5/plugin.c b/spa/plugins/bluez5/plugin.c
index bb09a22a280617bb3be4e0b8a07c0b50170b9a12..4a06f81d344e3ae9398ec9fbb52cd8e905ee7b4c 100644
--- a/spa/plugins/bluez5/plugin.c
+++ b/spa/plugins/bluez5/plugin.c
@@ -29,10 +29,12 @@
 
 extern const struct spa_handle_factory spa_bluez5_dbus_factory;
 extern const struct spa_handle_factory spa_bluez5_device_factory;
-extern const struct spa_handle_factory spa_a2dp_sink_factory;
-extern const struct spa_handle_factory spa_a2dp_source_factory;
+extern const struct spa_handle_factory spa_media_sink_factory;
+extern const struct spa_handle_factory spa_media_source_factory;
 extern const struct spa_handle_factory spa_sco_sink_factory;
 extern const struct spa_handle_factory spa_sco_source_factory;
+extern const struct spa_handle_factory spa_a2dp_sink_factory;
+extern const struct spa_handle_factory spa_a2dp_source_factory;
 
 SPA_EXPORT
 int spa_handle_factory_enum(const struct spa_handle_factory **factory, uint32_t *index)
@@ -48,10 +50,10 @@ int spa_handle_factory_enum(const struct spa_handle_factory **factory, uint32_t
 		*factory = &spa_bluez5_device_factory;
 		break;
 	case 2:
-		*factory = &spa_a2dp_sink_factory;
+		*factory = &spa_media_sink_factory;
 		break;
 	case 3:
-		*factory = &spa_a2dp_source_factory;
+		*factory = &spa_media_source_factory;
 		break;
 	case 4:
 		*factory = &spa_sco_sink_factory;
@@ -59,6 +61,12 @@ int spa_handle_factory_enum(const struct spa_handle_factory **factory, uint32_t
 	case 5:
 		*factory = &spa_sco_source_factory;
 		break;
+	case 6:
+		*factory = &spa_a2dp_sink_factory;
+		break;
+	case 7:
+		*factory = &spa_a2dp_source_factory;
+		break;
 	default:
 		return 0;
 	}
diff --git a/spa/plugins/bluez5/quirks.c b/spa/plugins/bluez5/quirks.c
index 7612e9ae6d784aae14f4eef590593cc2cfb27fae..8a7f92643a5197a4d411082dbd916e31b9a7a030 100644
--- a/spa/plugins/bluez5/quirks.c
+++ b/spa/plugins/bluez5/quirks.c
@@ -56,7 +56,6 @@
 #include <spa/utils/json.h>
 #include <spa/utils/string.h>
 
-#include "a2dp-codecs.h"
 #include "defs.h"
 
 static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.quirks");
@@ -89,10 +88,9 @@ static enum spa_bt_feature parse_feature(const char *str)
 		{ "faststream", SPA_BT_FEATURE_FASTSTREAM },
 		{ "a2dp-duplex", SPA_BT_FEATURE_A2DP_DUPLEX },
 	};
-	size_t i;
-	for (i = 0; i < SPA_N_ELEMENTS(feature_keys); ++i) {
-		if (spa_streq(str, feature_keys[i].key))
-			return feature_keys[i].value;
+	SPA_FOR_EACH_ELEMENT_VAR(feature_keys, f) {
+		if (spa_streq(str, f->key))
+			return f->value;
 	}
 	return 0;
 }
diff --git a/spa/plugins/bluez5/sco-sink.c b/spa/plugins/bluez5/sco-sink.c
index d92bada2af852540f132759f84a24c27cd1302f0..f8db7eaf8fcf031610288dd0aab9d253dede3671 100644
--- a/spa/plugins/bluez5/sco-sink.c
+++ b/spa/plugins/bluez5/sco-sink.c
@@ -60,14 +60,10 @@ static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.sink.sco");
 #define DEFAULT_CLOCK_NAME	"clock.system.monotonic"
 
 struct props {
-	uint32_t min_latency;
-	uint32_t max_latency;
 	char clock_name[64];
 };
 
 #define MAX_BUFFERS 32
-#define MIN_LATENCY	512
-#define MAX_LATENCY	1024
 
 struct buffer {
 	uint32_t id;
@@ -130,6 +126,8 @@ struct impl {
 	struct spa_param_info params[N_NODE_PARAMS];
 	struct props props;
 
+	uint32_t quantum_limit;
+
 	/* Transport */
 	struct spa_bt_transport *transport;
 	struct spa_hook transport_listener;
@@ -140,38 +138,39 @@ struct impl {
 	/* Flags */
 	unsigned int started:1;
 	unsigned int following:1;
+	unsigned int flush_pending:1;
 
 	/* Sources */
 	struct spa_source source;
+	struct spa_source flush_timer_source;
 
 	/* Timer */
 	int timerfd;
-	struct timespec now;
+	int flush_timerfd;
 	struct spa_io_clock *clock;
 	struct spa_io_position *position;
 
+	uint64_t current_time;
+	uint64_t next_time;
+	uint64_t process_time;
+	uint64_t prev_flush_time;
+	uint64_t next_flush_time;
+
 	/* mSBC */
 	sbc_t msbc;
 	uint8_t *buffer;
 	uint8_t *buffer_head;
 	uint8_t *buffer_next;
 	int buffer_size;
-
-	/* Times */
-	uint64_t start_time;
-	uint64_t total_samples;
+	int msbc_seq;
 };
 
 #define CHECK_PORT(this,d,p)	((d) == SPA_DIRECTION_INPUT && (p) == 0)
 
-static const uint32_t default_min_latency = MIN_LATENCY;
-static const uint32_t default_max_latency = MAX_LATENCY;
 static const char sntable[4] = { 0x08, 0x38, 0xC8, 0xF8 };
 
 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));
 }
 
@@ -199,23 +198,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;
 		}
@@ -223,14 +206,10 @@ 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));
+				SPA_TYPE_OBJECT_Props, id);
 			break;
 		default:
 			return 0;
@@ -260,28 +239,17 @@ static int set_timeout(struct impl *this, uint64_t time)
 	ts.it_interval.tv_sec = 0;
 	ts.it_interval.tv_nsec = 0;
 	return spa_system_timerfd_settime(this->data_system,
-			this->timerfd, 0, &ts, NULL);
+			this->timerfd, SPA_FD_TIMER_ABSTIME, &ts, NULL);
 }
 
 static int set_timers(struct impl *this)
 {
-	return set_timeout(this, this->following ? 0 : 1);
-}
-
-static uint64_t get_next_timeout(struct impl *this, uint64_t now_time, uint64_t processed_samples)
-{
-	struct port *port = &this->port;
-	uint64_t playback_time = 0, elapsed_time = 0, next_time = 0;
-
-	this->total_samples += processed_samples;
+	struct timespec now;
 
-	playback_time = (this->total_samples * SPA_NSEC_PER_SEC) / port->current_format.info.raw.rate;
-	if (now_time > this->start_time)
-		elapsed_time = now_time - this->start_time;
-	if (elapsed_time < playback_time)
-		next_time = playback_time - elapsed_time;
+	spa_system_clock_gettime(this->data_system, CLOCK_MONOTONIC, &now);
+	this->next_time = SPA_TIMESPEC_TO_NSEC(&now);
 
-	return next_time;
+	return set_timeout(this, this->following ? 0 : this->next_time);
 }
 
 static int do_reassign_follower(struct spa_loop *loop,
@@ -344,9 +312,7 @@ static int apply_props(struct impl *this, const struct spa_pod *param)
 		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));
+				SPA_TYPE_OBJECT_Props, NULL);
 	}
 
 	changed = (memcmp(&new_props, &this->props, sizeof(struct props)) != 0);
@@ -378,22 +344,64 @@ static int impl_node_set_param(void *object, uint32_t id, uint32_t flags,
 	return 0;
 }
 
+static void enable_flush_timer(struct impl *this, bool enabled)
+{
+	struct itimerspec ts;
+
+	if (!enabled)
+		this->next_flush_time = 0;
+
+	ts.it_value.tv_sec = this->next_flush_time / SPA_NSEC_PER_SEC;
+	ts.it_value.tv_nsec = this->next_flush_time % SPA_NSEC_PER_SEC;
+	ts.it_interval.tv_sec = 0;
+	ts.it_interval.tv_nsec = 0;
+	spa_system_timerfd_settime(this->data_system,
+			this->flush_timerfd, SPA_FD_TIMER_ABSTIME, &ts, NULL);
+
+	this->flush_pending = enabled;
+}
+
+static uint32_t get_queued_frames(struct impl *this)
+{
+	struct port *port = &this->port;
+	uint32_t bytes = 0;
+	struct buffer *b;
+
+	spa_list_for_each(b, &port->ready, link) {
+		struct spa_data *d = b->buf->datas;
+
+		bytes += d[0].chunk->size;
+	}
+
+	if (bytes > port->ready_offset)
+		bytes -= port->ready_offset;
+	else
+		bytes = 0;
+
+	return bytes / port->frame_size;
+}
+
 static void flush_data(struct impl *this)
 {
 	struct port *port = &this->port;
-	struct spa_data *datas;
 	const uint32_t min_in_size =
 		(this->transport->codec == HFP_AUDIO_CODEC_MSBC) ?
 		MSBC_DECODED_SIZE : this->transport->write_mtu;
 	uint8_t * const packet =
 		(this->transport->codec == HFP_AUDIO_CODEC_MSBC) ?
 		this->buffer_head : port->write_buffer;
+	const uint32_t packet_samples = min_in_size / port->frame_size;
+	const uint64_t packet_time = (uint64_t)packet_samples * SPA_NSEC_PER_SEC
+		/ port->current_format.info.raw.rate;
+	int processed = 0;
+	int written;
 
 	if (this->transport == NULL || this->transport->sco_io == NULL)
 		return;
 
-again:
 	while (!spa_list_is_empty(&port->ready) && port->write_buffer_size < min_in_size) {
+		struct spa_data *datas;
+
 		/* get buffer */
 		if (!port->current_buffer) {
 			spa_return_if_fail(!spa_list_is_empty(&port->ready));
@@ -429,147 +437,212 @@ again:
 		}
 	}
 
-	/* send the data if the write buffer is full */
-	if (port->write_buffer_size >= min_in_size) {
-		uint64_t now_time;
-		static int sn = 0;
-		int processed = 0;
+	if (this->flush_pending) {
+		spa_log_trace(this->log, "%p: wait for flush timer", this);
+		return;
+	}
+
+	if (port->write_buffer_size < min_in_size) {
+		/* wait for more data */
+		spa_log_trace(this->log, "%p: skip flush", this);
+		enable_flush_timer(this, false);
+		return;
+	}
+
+	if (this->transport->codec == HFP_AUDIO_CODEC_MSBC) {
 		ssize_t out_encoded;
-		int written;
-		uint64_t next_timeout;
-
-		spa_system_clock_gettime(this->data_system, CLOCK_MONOTONIC, &this->now);
-		now_time = SPA_TIMESPEC_TO_NSEC(&this->now);
-		if (this->start_time == 0)
-			this->start_time = now_time;
-
-		if (this->transport->codec == HFP_AUDIO_CODEC_MSBC) {
-			/* Encode */
-			if (this->buffer_next + MSBC_ENCODED_SIZE > this->buffer + this->buffer_size) {
-				/* Buffer overrun; shouldn't usually happen. Drop data and reset. */
-				this->buffer_head = this->buffer_next = this->buffer;
-				spa_log_warn(this->log, "sco-sink: mSBC buffer overrun, dropping data");
-			}
-			this->buffer_next[0] = 0x01;
-			this->buffer_next[1] = sntable[sn];
-			this->buffer_next[59] = 0x00;
-			sn = (sn + 1) % 4;
-			processed = sbc_encode(&this->msbc, port->write_buffer, port->write_buffer_size,
-			                       this->buffer_next + 2, MSBC_ENCODED_SIZE - 3, &out_encoded);
-			if (processed < 0) {
-				spa_log_warn(this->log, "sbc_encode failed: %d", processed);
-				return;
-			}
-			this->buffer_next += out_encoded + 3;
-			port->write_buffer_size = 0;
-
-			/* Write */
-			written = spa_bt_sco_io_write(this->transport->sco_io, packet,
-					this->buffer_next - this->buffer_head);
-			if (written < 0) {
-				spa_log_warn(this->log, "failed to write data: %d (%s)",
-						written, spa_strerror(written));
-				goto stop;
-			}
-
-			this->buffer_head += written;
-
-			if (this->buffer_head == this->buffer_next)
-				this->buffer_head = this->buffer_next = this->buffer;
-			else if (this->buffer_next + MSBC_ENCODED_SIZE > this->buffer + this->buffer_size) {
-				/* Written bytes is not necessarily commensurate
-				 * with MSBC_ENCODED_SIZE. If this occurs, copy data.
-				 */
-				int size = this->buffer_next - this->buffer_head;
-				spa_memmove(this->buffer, this->buffer_head, size);
-				this->buffer_next = this->buffer + size;
-				this->buffer_head = this->buffer;
-			}
-		} else {
-			written = spa_bt_sco_io_write(this->transport->sco_io, packet,
-					port->write_buffer_size);
-			if (written < 0) {
-				spa_log_warn(this->log, "sco-sink: write failure: %d (%s)",
-						written, spa_strerror(written));
-				goto stop;
-			} else if (written == 0) {
-				/* EAGAIN or similar, just skip ahead */
-				written = SPA_MIN(port->write_buffer_size, (uint32_t)48);
-			}
-
-			processed = written;
-			port->write_buffer_size -= written;
-
-			if (port->write_buffer_size > 0 && written > 0) {
-				spa_memmove(port->write_buffer, port->write_buffer + written, port->write_buffer_size);
-			}
+
+		/* Encode */
+		if (this->buffer_next + MSBC_ENCODED_SIZE > this->buffer + this->buffer_size) {
+			/* Buffer overrun; shouldn't usually happen. Drop data and reset. */
+			this->buffer_head = this->buffer_next = this->buffer;
+			spa_log_warn(this->log, "sco-sink: mSBC buffer overrun, dropping data");
+		}
+		this->buffer_next[0] = 0x01;
+		this->buffer_next[1] = sntable[this->msbc_seq % 4];
+		this->buffer_next[59] = 0x00;
+		this->msbc_seq = (this->msbc_seq + 1) % 4;
+		processed = sbc_encode(&this->msbc, port->write_buffer, port->write_buffer_size,
+				this->buffer_next + 2, MSBC_ENCODED_SIZE - 3, &out_encoded);
+		if (processed < 0) {
+			spa_log_warn(this->log, "sbc_encode failed: %d", processed);
+			return;
+		}
+		this->buffer_next += out_encoded + 3;
+		port->write_buffer_size = 0;
+
+		/* Write */
+		written = spa_bt_sco_io_write(this->transport->sco_io, packet,
+				this->buffer_next - this->buffer_head);
+		if (written < 0) {
+			spa_log_warn(this->log, "failed to write data: %d (%s)",
+					written, spa_strerror(written));
+			goto stop;
 		}
 
-		spa_log_trace(this->log, "write socket data %d", written);
+		this->buffer_head += written;
+
+		if (this->buffer_head == this->buffer_next)
+			this->buffer_head = this->buffer_next = this->buffer;
+		else if (this->buffer_next + MSBC_ENCODED_SIZE > this->buffer + this->buffer_size) {
+			/* Written bytes is not necessarily commensurate
+			 * with MSBC_ENCODED_SIZE. If this occurs, copy data.
+			 */
+			int size = this->buffer_next - this->buffer_head;
+			spa_memmove(this->buffer, this->buffer_head, size);
+			this->buffer_next = this->buffer + size;
+			this->buffer_head = this->buffer;
+		}
+	} else {
+		written = spa_bt_sco_io_write(this->transport->sco_io, packet,
+				port->write_buffer_size);
+		if (written < 0) {
+			spa_log_warn(this->log, "sco-sink: write failure: %d (%s)",
+					written, spa_strerror(written));
+			goto stop;
+		} else if (written == 0) {
+			/* EAGAIN or similar, just skip ahead */
+			written = SPA_MIN(port->write_buffer_size, (uint32_t)48);
+		}
 
-		next_timeout = get_next_timeout(this, now_time, processed / port->frame_size);
+		processed = written;
+		port->write_buffer_size -= written;
 
-		if (!this->following && this->clock) {
-			this->clock->nsec = now_time;
-			this->clock->position = this->total_samples;
-			this->clock->delay = processed / port->frame_size;
-			this->clock->rate_diff = 1.0f;
-			this->clock->next_nsec = now_time + next_timeout;
+		if (port->write_buffer_size > 0 && written > 0) {
+			spa_memmove(port->write_buffer, port->write_buffer + written, port->write_buffer_size);
 		}
+	}
 
-		if (next_timeout == 0)
-			goto again;
+	if (SPA_UNLIKELY(spa_log_level_topic_enabled(this->log, SPA_LOG_TOPIC_DEFAULT, SPA_LOG_LEVEL_TRACE))) {
+		struct timespec ts;
+		uint64_t now;
+		uint64_t dt;
 
-		spa_log_trace(this->log, "timeout %"PRIu64" ns", next_timeout);
-		set_timeout(this, next_timeout);
-	} else {
-		/* As follower, driver will wake us up when there is data */
-		if (this->following)
-			return;
+		spa_system_clock_gettime(this->data_system, CLOCK_MONOTONIC, &ts);
+		now = SPA_TIMESPEC_TO_NSEC(&ts);
+		dt = now - this->prev_flush_time;
+		this->prev_flush_time = now;
+
+		spa_log_trace(this->log,
+				"%p: send wrote:%d dt:%"PRIu64,
+				this, written, dt);
+	}
+
+	spa_log_trace(this->log, "write socket data %d", written);
 
-		/* As driver, run timeout now to schedule data */
-		spa_log_trace(this->log, "timeout 1 ns (driver: schedule now)");
-		set_timeout(this, 1);
+	if (SPA_LIKELY(this->position)) {
+		uint32_t frames = get_queued_frames(this);
+		uint64_t duration_ns;
+
+		/*
+		 * Flush at the time position of the next buffered sample.
+		 */
+		duration_ns = ((uint64_t)this->position->clock.duration * SPA_NSEC_PER_SEC
+				/ this->position->clock.rate.denom);
+		this->next_flush_time = this->process_time + duration_ns
+			- ((uint64_t)frames * SPA_NSEC_PER_SEC
+					/ port->current_format.info.raw.rate);
+
+		/*
+		 * We could delay the output by one packet to avoid waiting
+		 * for the next buffer and so make send intervals more regular.
+		 * However, this appears not needed in practice, and it's better
+		 * to not add latency if not needed.
+		 */
+#if 0
+		this->next_flush_time += SPA_MIN(packet_time,
+				duration_ns * (port->n_buffers - 1));
+#endif
+	} else {
+		if (this->next_flush_time == 0)
+			this->next_flush_time = this->process_time;
+		this->next_flush_time += packet_time;
 	}
 
+	enable_flush_timer(this, true);
 	return;
 
 stop:
 	if (this->source.loop)
 		spa_loop_remove_source(this->data_loop, &this->source);
+	enable_flush_timer(this, false);
+}
+
+
+static void sco_on_flush_timeout(struct spa_source *source)
+{
+	struct impl *this = source->data;
+	uint64_t exp;
+
+	spa_log_trace(this->log, "%p: flush on timeout", this);
+
+	if (spa_system_timerfd_read(this->data_system, this->flush_timerfd, &exp) < 0)
+		spa_log_warn(this->log, "error reading timerfd: %s", strerror(errno));
+
+	if (this->transport == NULL) {
+		enable_flush_timer(this, false);
+		return;
+	}
+
+	while (exp-- > 0) {
+		this->flush_pending = false;
+		flush_data(this);
+	}
 }
 
 static void sco_on_timeout(struct spa_source *source)
 {
 	struct impl *this = source->data;
 	struct port *port = &this->port;
-	uint64_t exp;
+	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;
 
-	/* Read the timerfd */
 	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));
 
-	/* delay if no buffers available */
-	if (!this->following && spa_list_is_empty(&port->ready)) {
-		set_timeout(this, this->transport->write_mtu / port->frame_size * SPA_NSEC_PER_SEC / port->current_format.info.raw.rate);
-		port->io->status = SPA_STATUS_NEED_DATA;
-		spa_node_call_ready(&this->callbacks, SPA_STATUS_NEED_DATA);
-		return;
+	prev_time = this->current_time;
+	now_time = this->current_time = this->next_time;
+
+	spa_log_debug(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;
+	}
+
+	this->next_time = now_time + duration * SPA_NSEC_PER_SEC / rate;
+
+	if (SPA_LIKELY(this->clock)) {
+		this->clock->nsec = now_time;
+		this->clock->position += duration;
+		this->clock->duration = duration;
+		this->clock->rate_diff = 1.0f;
+		this->clock->next_nsec = this->next_time;
+		this->clock->delay = 0;
 	}
 
-	/* Flush data */
-	flush_data(this);
+	spa_log_trace(this->log, "%p: %d", this, io->status);
+	io->status = SPA_STATUS_NEED_DATA;
+	spa_node_call_ready(&this->callbacks, SPA_STATUS_NEED_DATA);
+
+	set_timeout(this, this->next_time);
 }
 
 /* greater common divider */
 static int gcd(int a, int b) {
     while(b) {
-        int c = b;
-        b = a % b;
-        a = c;
+	int c = b;
+	b = a % b;
+	a = c;
     }
     return a;
 }
@@ -616,6 +689,10 @@ static int do_start(struct impl *this)
 		this->buffer_size = lcm(24, lcm(60, lcm(this->transport->write_mtu, 2 * MSBC_ENCODED_SIZE)));
 		this->buffer = calloc(this->buffer_size, sizeof(uint8_t));
 		this->buffer_head = this->buffer_next = this->buffer;
+		if (this->buffer == NULL) {
+			res = -errno;
+			goto fail;
+		}
 	}
 
 	spa_return_val_if_fail(this->transport->write_mtu <= sizeof(this->port.write_buffer), -EINVAL);
@@ -632,7 +709,15 @@ static int do_start(struct impl *this)
 	this->source.rmask = 0;
 	spa_loop_add_source(this->data_loop, &this->source);
 
+	this->flush_timer_source.data = this;
+	this->flush_timer_source.fd = this->flush_timerfd;
+	this->flush_timer_source.func = sco_on_flush_timeout;
+	this->flush_timer_source.mask = SPA_IO_IN;
+	this->flush_timer_source.rmask = 0;
+	spa_loop_add_source(this->data_loop, &this->flush_timer_source);
+
 	/* start processing */
+	this->flush_pending = false;
 	set_timers(this);
 
 	/* Set the started flag */
@@ -675,13 +760,20 @@ static int do_remove_source(struct spa_loop *loop,
 			    void *user_data)
 {
 	struct impl *this = user_data;
+	struct itimerspec ts;
 
-	this->start_time = 0;
-	this->total_samples = 0;
 	set_timeout(this, 0);
 	if (this->source.loop)
 		spa_loop_remove_source(this->data_loop, &this->source);
 
+	if (this->flush_timer_source.loop)
+		spa_loop_remove_source(this->data_loop, &this->flush_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->flush_timerfd, 0, &ts, NULL);
+
 	/* Drop buffered data in the ready queue. Ideally there shouldn't be any. */
 	drop_port_output(this);
 
@@ -758,7 +850,7 @@ static void emit_node_info(struct impl *this, bool full)
 		{ SPA_KEY_DEVICE_API, "bluez5" },
 		{ SPA_KEY_MEDIA_CLASS, "Stream/Input/Audio" },
 		{ "media.name", ((this->transport && this->transport->device->name) ?
-		                 this->transport->device->name : "HSP/HFP") },
+				 this->transport->device->name : "HSP/HFP") },
 		{ SPA_KEY_MEDIA_ROLE, "Communication" },
 	};
 	bool is_ag = this->transport &&
@@ -917,9 +1009,9 @@ impl_node_port_enum_params(void *object, int seq,
 			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(1),
-			SPA_PARAM_BUFFERS_size,    SPA_POD_CHOICE_RANGE_Int(
-							this->props.max_latency * port->frame_size,
-							this->props.min_latency * port->frame_size,
+			SPA_PARAM_BUFFERS_size,	   SPA_POD_CHOICE_RANGE_Int(
+							this->quantum_limit * port->frame_size,
+							16 * port->frame_size,
 							INT32_MAX),
 			SPA_PARAM_BUFFERS_stride,  SPA_POD_Int(port->frame_size));
 		break;
@@ -1014,6 +1106,11 @@ static int port_set_format(struct impl *this, struct port *port,
 		if (spa_format_audio_raw_parse(format, &info.info.raw) < 0)
 			return -EINVAL;
 
+		if (info.info.raw.format != SPA_AUDIO_FORMAT_S16_LE ||
+		    info.info.raw.rate == 0 ||
+		    info.info.raw.channels != 1)
+			return -EINVAL;
+
 		port->frame_size = info.info.raw.channels * 2;
 		port->current_format = info;
 		port->have_format = true;
@@ -1150,6 +1247,11 @@ static int impl_node_process(void *object)
 	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;
+		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];
 
@@ -1167,8 +1269,22 @@ static int impl_node_process(void *object)
 		io->status = SPA_STATUS_OK;
 	}
 
-	if (!spa_list_is_empty(&port->ready))
+	if (this->following) {
+		if (this->position) {
+			this->current_time = this->position->clock.nsec;
+		} else {
+			struct timespec now;
+			spa_system_clock_gettime(this->data_system, CLOCK_MONOTONIC, &now);
+			this->current_time = SPA_TIMESPEC_TO_NSEC(&now);
+		}
+	}
+
+	this->process_time = this->current_time;
+
+	if (!spa_list_is_empty(&port->ready)) {
+		spa_log_trace(this->log, "%p: flush on process", this);
 		flush_data(this);
+	}
 
 	return SPA_STATUS_HAVE_DATA;
 }
@@ -1236,9 +1352,12 @@ 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;
+
+	do_stop(this);
 	if (this->transport)
 		spa_hook_remove(&this->transport_listener);
 	spa_system_close(this->data_system, this->timerfd);
+	spa_system_close(this->data_system, this->flush_timerfd);
 	return 0;
 }
 
@@ -1323,6 +1442,11 @@ impl_init(const struct spa_handle_factory *factory,
 
 	spa_list_init(&port->ready);
 
+	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);
 
@@ -1336,6 +1460,9 @@ impl_init(const struct spa_handle_factory *factory,
 	this->timerfd = spa_system_timerfd_create(this->data_system,
 			CLOCK_MONOTONIC, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK);
 
+	this->flush_timerfd = spa_system_timerfd_create(this->data_system,
+			CLOCK_MONOTONIC, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK);
+
 	return 0;
 }
 
diff --git a/spa/plugins/bluez5/sco-source.c b/spa/plugins/bluez5/sco-source.c
index 52a1d27cce5eac79b64dd67214a93b52b0424033..17855065f0f4634ecfebc6a58a49f672ec3038b8 100644
--- a/spa/plugins/bluez5/sco-source.c
+++ b/spa/plugins/bluez5/sco-source.c
@@ -248,6 +248,7 @@ static int do_reassign_follower(struct spa_loop *loop,
 	struct impl *this = user_data;
 	struct port *port = &this->port;
 
+	set_timers(this);
 	spa_bt_decode_buffer_recover(&port->buffer);
 	return 0;
 }
@@ -590,13 +591,14 @@ static int setup_matching(struct impl *this)
 	return 0;
 }
 
+static int produce_buffer(struct impl *this);
+
 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)
@@ -631,8 +633,11 @@ static void sco_on_timeout(struct spa_source *source)
 		this->clock->next_nsec = this->next_time;
 	}
 
-	spa_log_trace(this->log, "%p: %d", this, io->status);
-	io->status = SPA_STATUS_HAVE_DATA;
+	if (port->io) {
+		int status = produce_buffer(this);
+		spa_log_trace(this->log, "%p: io:%d status:%d", this, port->io->status, status);
+	}
+
 	spa_node_call_ready(&this->callbacks, SPA_STATUS_HAVE_DATA);
 
 	set_timeout(this, this->next_time);
@@ -1068,6 +1073,11 @@ static int port_set_format(struct impl *this, struct port *port,
 		if (spa_format_audio_raw_parse(format, &info.info.raw) < 0)
 			return -EINVAL;
 
+		if (info.info.raw.format != SPA_AUDIO_FORMAT_S16_LE ||
+		    info.info.raw.rate == 0 ||
+		    info.info.raw.channels != 1)
+			return -EINVAL;
+
 		port->frame_size = info.info.raw.channels * 2;
 		port->current_format = info;
 		port->have_format = true;
@@ -1285,17 +1295,13 @@ static void process_buffering(struct impl *this)
 	}
 }
 
-static int impl_node_process(void *object)
+static int produce_buffer(struct impl *this)
 {
-	struct impl *this = object;
-	struct port *port;
-	struct spa_io_buffers *io;
 	struct buffer *buffer;
+	struct port *port = &this->port;
+	struct spa_io_buffers *io = port->io;
 
-	spa_return_val_if_fail(this != NULL, -EINVAL);
-
-	port = &this->port;
-	if ((io = port->io) == NULL)
+	if (io == NULL)
 		return -EIO;
 
 	/* Return if we already have a buffer */
@@ -1308,7 +1314,7 @@ static int impl_node_process(void *object)
 		io->buffer_id = SPA_ID_INVALID;
 	}
 
-	/* Produce data */
+	/* Handle buffering */
 	process_buffering(this);
 
 	/* Return if there are no buffers ready to be processed */
@@ -1328,6 +1334,35 @@ static int impl_node_process(void *object)
 	return SPA_STATUS_HAVE_DATA;
 }
 
+static int impl_node_process(void *object)
+{
+	struct impl *this = object;
+	struct port *port;
+	struct spa_io_buffers *io;
+
+	spa_return_val_if_fail(this != NULL, -EINVAL);
+
+	port = &this->port;
+	if ((io = port->io) == NULL)
+		return -EIO;
+
+	/* Return if we already have a buffer */
+	if (io->status == SPA_STATUS_HAVE_DATA)
+		return SPA_STATUS_HAVE_DATA;
+
+	/* Recycle */
+	if (io->buffer_id < port->n_buffers) {
+		recycle_buffer(this, port, io->buffer_id);
+		io->buffer_id = SPA_ID_INVALID;
+	}
+
+	/* Follower produces buffers here, driver in timeout */
+	if (this->following)
+		return produce_buffer(this);
+	else
+		return SPA_STATUS_OK;
+}
+
 static const struct spa_node_methods impl_node = {
 	SPA_VERSION_NODE_METHODS,
 	.add_listener = impl_node_add_listener,
@@ -1391,6 +1426,8 @@ 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;
+
+	do_stop(this);
 	if (this->transport)
 		spa_hook_remove(&this->transport_listener);
 	spa_system_close(this->data_system, this->timerfd);
diff --git a/spa/plugins/bluez5/upower.c b/spa/plugins/bluez5/upower.c
new file mode 100644
index 0000000000000000000000000000000000000000..23a637a22fe47e134959d2b9f936ad6dbd071932
--- /dev/null
+++ b/spa/plugins/bluez5/upower.c
@@ -0,0 +1,311 @@
+/* Spa Bluez5 UPower proxy
+ *
+ * Copyright © 2022 Collabora
+ *
+ * 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/utils/string.h>
+
+#include "upower.h"
+
+#define UPOWER_SERVICE "org.freedesktop.UPower"
+#define UPOWER_DEVICE_INTERFACE UPOWER_SERVICE ".Device"
+#define UPOWER_DISPLAY_DEVICE_OBJECT "/org/freedesktop/UPower/devices/DisplayDevice"
+
+struct impl {
+	struct spa_bt_monitor *monitor;
+
+	struct spa_log *log;
+	DBusConnection *conn;
+
+	bool filters_added;
+
+	void *user_data;
+	void (*set_battery_level)(unsigned int level, void *user_data);
+};
+
+static DBusHandlerResult upower_parse_percentage(struct impl *this, DBusMessageIter *variant_i)
+{
+	double percentage;
+	unsigned int battery_level;
+
+	dbus_message_iter_get_basic(variant_i, &percentage);
+	spa_log_debug(this->log, "Battery level: %f %%", percentage);
+
+	battery_level = (unsigned int) round(percentage / 20.0);
+	this->set_battery_level(battery_level, this->user_data);
+
+	return DBUS_HANDLER_RESULT_HANDLED;
+}
+
+static void upower_get_percentage_properties_reply(DBusPendingCall *pending, void *user_data)
+{
+	struct impl *backend = user_data;
+	DBusMessage *r;
+	DBusMessageIter i, variant_i;
+
+	r = dbus_pending_call_steal_reply(pending);
+	if (r == NULL)
+		return;
+
+	if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) {
+		spa_log_error(backend->log, "Failed to get percentage from UPower: %s",
+				dbus_message_get_error_name(r));
+		goto finish;
+	}
+
+	if (!dbus_message_iter_init(r, &i) || !spa_streq(dbus_message_get_signature(r), "v")) {
+		spa_log_error(backend->log, "Invalid arguments in Get() reply");
+		goto finish;
+	}
+
+	dbus_message_iter_recurse(&i, &variant_i);
+	upower_parse_percentage(backend, &variant_i);
+
+finish:
+	dbus_message_unref(r);
+}
+
+static void upower_clean(struct impl *this)
+{
+	this->set_battery_level(0, this->user_data);
+}
+
+static DBusHandlerResult upower_filter_cb(DBusConnection *bus, DBusMessage *m, void *user_data)
+{
+	struct impl *this = user_data;
+	DBusError err;
+
+	dbus_error_init(&err);
+
+	if (dbus_message_is_signal(m, "org.freedesktop.DBus", "NameOwnerChanged")) {
+		const char *name, *old_owner, *new_owner;
+
+		spa_log_debug(this->log, "Name owner changed %s", dbus_message_get_path(m));
+
+		if (!dbus_message_get_args(m, &err,
+					   DBUS_TYPE_STRING, &name,
+					   DBUS_TYPE_STRING, &old_owner,
+					   DBUS_TYPE_STRING, &new_owner,
+					   DBUS_TYPE_INVALID)) {
+			spa_log_error(this->log, "Failed to parse org.freedesktop.DBus.NameOwnerChanged: %s", err.message);
+			goto finish;
+		}
+
+		if (spa_streq(name, UPOWER_SERVICE)) {
+			if (old_owner && *old_owner) {
+				spa_log_debug(this->log, "UPower daemon disappeared (%s)", old_owner);
+				upower_clean(this);
+			}
+
+			if (new_owner && *new_owner) {
+				DBusPendingCall *call;
+				static const char* upower_device_interface = UPOWER_DEVICE_INTERFACE;
+				static const char* percentage_property = "Percentage";
+
+				spa_log_debug(this->log, "UPower daemon appeared (%s)", new_owner);
+
+				m = dbus_message_new_method_call(UPOWER_SERVICE, UPOWER_DISPLAY_DEVICE_OBJECT, DBUS_INTERFACE_PROPERTIES, "Get");
+				if (m == NULL)
+					goto finish;
+				dbus_message_append_args(m, DBUS_TYPE_STRING, &upower_device_interface,
+						         DBUS_TYPE_STRING, &percentage_property, DBUS_TYPE_INVALID);
+				dbus_connection_send_with_reply(this->conn, m, &call, -1);
+				dbus_pending_call_set_notify(call, upower_get_percentage_properties_reply, this, NULL);
+				dbus_message_unref(m);
+			}
+		}
+	} else if (dbus_message_is_signal(m, DBUS_INTERFACE_PROPERTIES, DBUS_SIGNAL_PROPERTIES_CHANGED)) {
+		const char *path;
+		DBusMessageIter iface_i, props_i;
+		const char *interface;
+
+		if (!dbus_message_iter_init(m, &iface_i) || !spa_streq(dbus_message_get_signature(m), "sa{sv}as")) {
+				spa_log_error(this->log, "Invalid signature found in PropertiesChanged");
+				goto finish;
+		}
+
+		dbus_message_iter_get_basic(&iface_i, &interface);
+		dbus_message_iter_next(&iface_i);
+		spa_assert(dbus_message_iter_get_arg_type(&iface_i) == DBUS_TYPE_ARRAY);
+
+		dbus_message_iter_recurse(&iface_i, &props_i);
+
+		path = dbus_message_get_path(m);
+
+		if (spa_streq(interface, UPOWER_DEVICE_INTERFACE)) {
+			spa_log_debug(this->log, "Properties changed on %s", path);
+
+			while (dbus_message_iter_get_arg_type(&props_i) != DBUS_TYPE_INVALID) {
+				DBusMessageIter i, value_i;
+				const char *key;
+
+				dbus_message_iter_recurse(&props_i, &i);
+
+				dbus_message_iter_get_basic(&i, &key);
+				dbus_message_iter_next(&i);
+				dbus_message_iter_recurse(&i, &value_i);
+
+				if(spa_streq(key, "Percentage"))
+					upower_parse_percentage(this, &value_i);
+
+				dbus_message_iter_next(&props_i);
+			}
+		}
+	}
+
+finish:
+	return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+}
+
+static int add_filters(struct impl *this)
+{
+	DBusError err;
+
+	if (this->filters_added)
+		return 0;
+
+	dbus_error_init(&err);
+
+	if (!dbus_connection_add_filter(this->conn, upower_filter_cb, this, NULL)) {
+		spa_log_error(this->log, "failed to add filter function");
+		goto fail;
+	}
+
+	dbus_bus_add_match(this->conn,
+			"type='signal',sender='org.freedesktop.DBus',"
+			"interface='org.freedesktop.DBus',member='NameOwnerChanged'," "arg0='" UPOWER_SERVICE "'", &err);
+	dbus_bus_add_match(this->conn,
+			"type='signal',sender='" UPOWER_SERVICE "',"
+			"interface='" DBUS_INTERFACE_PROPERTIES "',member='" DBUS_SIGNAL_PROPERTIES_CHANGED "',"
+			"path='" UPOWER_DISPLAY_DEVICE_OBJECT "',arg0='" UPOWER_DEVICE_INTERFACE "'", &err);
+
+	this->filters_added = true;
+
+	return 0;
+
+fail:
+	dbus_error_free(&err);
+	return -EIO;
+}
+
+static bool is_dbus_service_available(struct impl *this, const char *service)
+{
+	DBusMessage *m, *r;
+	DBusError err;
+	bool success = false;
+
+	m = dbus_message_new_method_call("org.freedesktop.DBus", "/org/freedesktop/DBus",
+	                                  "org.freedesktop.DBus", "NameHasOwner");
+	if (m == NULL)
+		return false;
+	dbus_message_append_args(m, DBUS_TYPE_STRING, &service, DBUS_TYPE_INVALID);
+
+	dbus_error_init(&err);
+	r = dbus_connection_send_with_reply_and_block(this->conn, m, -1, &err);
+	dbus_message_unref(m);
+	m = NULL;
+
+	if (r == NULL) {
+		spa_log_info(this->log, "NameHasOwner failed for %s", service);
+		dbus_error_free(&err);
+		goto finish;
+	}
+
+	if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) {
+		spa_log_error(this->log, "NameHasOwner() returned error: %s", dbus_message_get_error_name(r));
+		goto finish;
+	}
+
+	if (!dbus_message_get_args(r, &err,
+				   DBUS_TYPE_BOOLEAN, &success,
+				   DBUS_TYPE_INVALID)) {
+		spa_log_error(this->log, "Failed to parse NameHasOwner() reply: %s", err.message);
+		dbus_error_free(&err);
+		goto finish;
+	}
+
+finish:
+	if (r)
+		dbus_message_unref(r);
+
+	return success;
+}
+
+void *upower_register(struct spa_log *log,
+                      void *dbus_connection,
+                      void (*set_battery_level)(unsigned int level, void *user_data),
+                      void *user_data)
+{
+	struct impl *this;
+
+	spa_assert(log);
+	spa_assert(dbus_connection);
+	spa_assert(set_battery_level);
+	spa_assert(user_data);
+
+	this = calloc(1, sizeof(struct impl));
+	if (this == NULL)
+		return NULL;
+
+	this->log = log;
+	this->conn = dbus_connection;
+	this->set_battery_level = set_battery_level;
+    this->user_data = user_data;
+
+	if (add_filters(this) < 0) {
+		goto fail4;
+	}
+
+	if (is_dbus_service_available(this, UPOWER_SERVICE)) {
+		DBusMessage *m;
+		DBusPendingCall *call;
+		static const char* upower_device_interface = UPOWER_DEVICE_INTERFACE;
+		static const char* percentage_property = "Percentage";
+
+		m = dbus_message_new_method_call(UPOWER_SERVICE, UPOWER_DISPLAY_DEVICE_OBJECT, DBUS_INTERFACE_PROPERTIES, "Get");
+		if (m == NULL)
+			goto fail4;
+		dbus_message_append_args(m, DBUS_TYPE_STRING, &upower_device_interface,
+		                         DBUS_TYPE_STRING, &percentage_property, DBUS_TYPE_INVALID);
+		dbus_connection_send_with_reply(this->conn, m, &call, -1);
+		dbus_pending_call_set_notify(call, upower_get_percentage_properties_reply, this, NULL);
+		dbus_message_unref(m);
+	}
+
+    return this;
+
+fail4:
+    free(this);
+    return NULL;
+}
+
+void upower_unregister(void *data)
+{
+	struct impl *this = data;
+
+	if (this->filters_added) {
+		dbus_connection_remove_filter(this->conn, upower_filter_cb, this);
+		this->filters_added = false;
+	}
+	free(this);
+}
diff --git a/spa/plugins/audioconvert/resample-peaks-c.c b/spa/plugins/bluez5/upower.h
similarity index 74%
rename from spa/plugins/audioconvert/resample-peaks-c.c
rename to spa/plugins/bluez5/upower.h
index 161e06fe00eadb9d7cc214d8dcf20a55584b41af..9ebd7510d0cfb046240443328e2e212674ec05a7 100644
--- a/spa/plugins/audioconvert/resample-peaks-c.c
+++ b/spa/plugins/bluez5/upower.h
@@ -1,6 +1,6 @@
-/* Spa
+/* Spa Bluez5 UPower proxy
  *
- * Copyright © 2018 Wim Taymans
+ * Copyright © 2022 Collabora
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
@@ -22,16 +22,15 @@
  * DEALINGS IN THE SOFTWARE.
  */
 
-#include <math.h>
+#ifndef SPA_BLUEZ5_UPOWER_H_
+#define SPA_BLUEZ5_UPOWER_H_
 
-#include "resample-peaks-impl.h"
+#include "defs.h"
 
-static inline float find_abs_max_c(const float *s, uint32_t n_samples, float m)
-{
-	uint32_t n;
-	for (n = 0; n < n_samples; n++)
-		m = fmaxf(fabsf(s[n]), m);
-	return m;
-}
+void *upower_register(struct spa_log *log,
+                      void *dbus_connection,
+                      void (*set_battery_level)(unsigned int level, void *user_data),
+                      void *user_data);
+void upower_unregister(void *data);
 
-MAKE_PEAKS(c);
+#endif
\ No newline at end of file
diff --git a/spa/plugins/libcamera/libcamera-client.c b/spa/plugins/libcamera/libcamera-client.c
index 24170e7b67dd6a70e903d4bffad04a0272348dfd..a31a41fd9a8836a9fef4c73eb4bfaa07168a9d80 100644
--- a/spa/plugins/libcamera/libcamera-client.c
+++ b/spa/plugins/libcamera/libcamera-client.c
@@ -77,8 +77,8 @@ static int emit_object_info(struct impl *this, uint32_t id)
 	items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_API, "libcamera");
 	items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_MEDIA_CLASS, "Video/Device");
 
-    info.props = &SPA_DICT_INIT(items, n_items);
-    spa_device_emit_object_info(&this->hooks, id, &info);
+	info.props = &SPA_DICT_INIT(items, n_items);
+	spa_device_emit_object_info(&this->hooks, id, &info);
 
 	return 1;
 }
@@ -112,18 +112,18 @@ impl_device_add_listener(void *object, struct spa_hook *listener,
 		const struct spa_device_events *events, void *data)
 {
 	struct impl *this = object;
-    struct spa_hook_list save;
+	struct spa_hook_list save;
 
 	spa_return_val_if_fail(this != NULL, -EINVAL);
 	spa_return_val_if_fail(events != NULL, -EINVAL);
 
-    spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
+	spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
 
 	emit_device_info(this, true);
 
 	emit_object_info(this, 0);
 
-    spa_hook_list_join(&this->hooks, &save);
+	spa_hook_list_join(&this->hooks, &save);
 
 	listener->removed = impl_hook_removed;
 	listener->priv = this;
diff --git a/spa/plugins/libcamera/libcamera-device.cpp b/spa/plugins/libcamera/libcamera-device.cpp
index f2f10c01785fb4b0267c2a4065392d218dae86a5..0edd1a33cb089480542709a4a1fb0ba258d50ba0 100644
--- a/spa/plugins/libcamera/libcamera-device.cpp
+++ b/spa/plugins/libcamera/libcamera-device.cpp
@@ -54,30 +54,29 @@
 
 using namespace libcamera;
 
-struct props {
-	char device[128];
-	char device_name[128];
-};
-
-static void reset_props(struct props *props)
-{
-	spa_zero(*props);
-}
+namespace {
 
 struct impl {
 	struct spa_handle handle;
-	struct spa_device device;
+	struct spa_device device = {};
 
 	struct spa_log *log;
 
-	struct props props;
+	std::string device_id;
 
 	struct spa_hook_list hooks;
 
-	CameraManager *manager;
+	std::shared_ptr<CameraManager> manager;
 	std::shared_ptr<Camera> camera;
+
+	impl(spa_log *log,
+	     std::shared_ptr<CameraManager> manager,
+	     std::shared_ptr<Camera> camera,
+	     std::string device_id);
 };
 
+}
+
 static std::string cameraModel(const Camera *camera)
 {
 	const ControlList &props = camera->properties();
@@ -120,11 +119,11 @@ static int emit_info(struct impl *impl, bool full)
 	info.change_mask = SPA_DEVICE_CHANGE_MASK_PROPS;
 
 #define ADD_ITEM(key, value) items[n_items++] = SPA_DICT_ITEM_INIT(key, value)
-	snprintf(path, sizeof(path), "libcamera:%s", impl->props.device);
+	snprintf(path, sizeof(path), "libcamera:%s", impl->device_id.c_str());
 	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, impl->props.device);
+	ADD_ITEM(SPA_KEY_API_LIBCAMERA_PATH, impl->device_id.c_str());
 
 	if (auto location = cameraLoc(impl->camera.get()))
 		ADD_ITEM(SPA_KEY_API_LIBCAMERA_LOCATION, location);
@@ -132,7 +131,7 @@ static int emit_info(struct impl *impl, bool full)
 	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);
-	snprintf(name, sizeof(name), "libcamera_device.%s", impl->props.device);
+	snprintf(name, sizeof(name), "libcamera_device.%s", impl->device_id.c_str());
 	ADD_ITEM(SPA_KEY_DEVICE_NAME, name);
 #undef ADD_ITEM
 
@@ -235,13 +234,30 @@ static int impl_get_interface(struct spa_handle *handle, const char *type, void
 
 static int impl_clear(struct spa_handle *handle)
 {
-	struct impl *impl = (struct impl *) handle;
-	if (impl->manager)
-		libcamera_manager_release(impl->manager);
-	impl->manager = NULL;
+	std::destroy_at(reinterpret_cast<impl *>(handle));
 	return 0;
 }
 
+impl::impl(spa_log *log,
+	   std::shared_ptr<CameraManager> manager,
+	   std::shared_ptr<Camera> camera,
+	   std::string device_id)
+	: handle({ SPA_VERSION_HANDLE, impl_get_interface, impl_clear }),
+	  log(log),
+	  device_id(std::move(device_id)),
+	  manager(std::move(manager)),
+	  camera(std::move(camera))
+{
+	libcamera_log_topic_init(log);
+
+	spa_hook_list_init(&hooks);
+
+	device.iface = SPA_INTERFACE_INIT(
+			SPA_TYPE_INTERFACE_Device,
+			SPA_VERSION_DEVICE,
+			&impl_device, this);
+}
+
 static size_t
 impl_get_size(const struct spa_handle_factory *factory,
 	      const struct spa_dict *params)
@@ -256,44 +272,32 @@ impl_init(const struct spa_handle_factory *factory,
 	  const struct spa_support *support,
 	  uint32_t n_support)
 {
-	struct impl *impl;
 	const char *str;
 	int res;
 
 	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;
-
-	impl->log = (struct spa_log*) spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
-	libcamera_log_topic_init(impl->log);
-
-	spa_hook_list_init(&impl->hooks);
-
-	impl->device.iface = SPA_INTERFACE_INIT(
-			SPA_TYPE_INTERFACE_Device,
-			SPA_VERSION_DEVICE,
-			&impl_device, impl);
+	auto log = static_cast<spa_log *>(spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log));
 
-	reset_props(&impl->props);
+	auto manager = libcamera_manager_acquire(res);
+	if (!manager) {
+		spa_log_error(log, "can't start camera manager: %s", spa_strerror(res));
+		return res;
+	}
 
+	std::string device_id;
 	if (info && (str = spa_dict_lookup(info, SPA_KEY_API_LIBCAMERA_PATH)))
-		strncpy(impl->props.device, str, sizeof(impl->props.device));
-
-	impl->manager = libcamera_manager_acquire();
-	if (impl->manager == NULL) {
-		res = -errno;
-		spa_log_error(impl->log, "can't start camera manager: %s", spa_strerror(res));
-                return res;
-	}
+		device_id = str;
 
-	impl->camera = impl->manager->get(impl->props.device);
-	if (impl->camera == NULL) {
-		spa_log_error(impl->log, "unknown camera id %s", impl->props.device);
-		libcamera_manager_release(impl->manager);
+	auto camera = manager->get(device_id);
+	if (!camera) {
+		spa_log_error(log, "unknown camera id %s", device_id.c_str());
 		return -ENOENT;
 	}
+
+	new (handle) impl(log, std::move(manager), std::move(camera), std::move(device_id));
+
 	return 0;
 }
 
diff --git a/spa/plugins/libcamera/libcamera-manager.cpp b/spa/plugins/libcamera/libcamera-manager.cpp
index b9243ad86982711eaee8f90067ed841757f0e63d..afba4032a8e6d9288e6e76c0292a9b9a1d9d2a18 100644
--- a/spa/plugins/libcamera/libcamera-manager.cpp
+++ b/spa/plugins/libcamera/libcamera-manager.cpp
@@ -29,7 +29,10 @@
 #include <sys/types.h>
 #include <sys/stat.h>
 #include <fcntl.h>
-
+#include <utility>
+#include <mutex>
+#include <optional>
+#include <queue>
 
 #include <libcamera/camera.h>
 #include <libcamera/camera_manager.h>
@@ -54,67 +57,72 @@ using namespace libcamera;
 
 #define MAX_DEVICES	64
 
-struct global {
-	int ref;
-	CameraManager *manager;
-};
-
-static struct global global;
+namespace {
 
 struct device {
 	uint32_t id;
 	std::shared_ptr<Camera> camera;
 };
 
-typedef struct impl {
+struct impl {
 	struct spa_handle handle;
-	struct spa_device device;
+	struct spa_device device = {};
 
 	struct spa_log *log;
-	struct spa_loop *main_loop;
+	struct spa_loop_utils *loop_utils;
 
 	struct spa_hook_list hooks;
 
-	uint64_t info_all;
-	struct spa_device_info info;
+	static constexpr uint64_t info_all = SPA_DEVICE_CHANGE_MASK_FLAGS | SPA_DEVICE_CHANGE_MASK_PROPS;
+	struct spa_device_info info = SPA_DEVICE_INFO_INIT();
 
-	CameraManager *manager;
+	std::shared_ptr<CameraManager> manager;
 	void addCamera(std::shared_ptr<libcamera::Camera> camera);
-        void removeCamera(std::shared_ptr<libcamera::Camera> camera);
+	void removeCamera(std::shared_ptr<libcamera::Camera> camera);
 
 	struct device devices[MAX_DEVICES];
-        uint32_t n_devices;
-} Impl;
+	uint32_t n_devices = 0;
 
-int libcamera_manager_release(CameraManager *manager)
-{
-	if (global.manager != manager)
-		return -EINVAL;
+	struct hotplug_event {
+		enum class type { add, remove } type;
+		std::shared_ptr<Camera> camera;
+	};
 
-	if (--global.ref == 0) {
-		global.manager->stop();
-		delete global.manager;
-		global.manager = NULL;
+	std::mutex hotplug_events_lock;
+	std::queue<hotplug_event> hotplug_events;
+	struct spa_source *hotplug_event_source;
+
+	impl(spa_log *log, spa_loop_utils *loop_utils, spa_source *hotplug_event_source);
+
+	~impl()
+	{
+		spa_loop_utils_destroy_source(loop_utils, hotplug_event_source);
 	}
-	return 0;
+};
+
 }
 
-CameraManager *libcamera_manager_acquire(void)
+static std::weak_ptr<CameraManager> global_manager;
+
+std::shared_ptr<CameraManager> libcamera_manager_acquire(int& res)
 {
-	int res;
+	if (auto manager = global_manager.lock())
+		return manager;
 
-	if (global.ref++ == 0) {
-		global.manager = new CameraManager();
-		if (global.manager == NULL)
-			return NULL;
+	auto manager = std::make_shared<CameraManager>();
+	if ((res = manager->start()) < 0)
+		return {};
 
-		if ((res = global.manager->start()) < 0) {
-			libcamera_manager_release(global.manager);
-			errno = -res;
-			return NULL;
-		}
-	}
-	return global.manager;
+	global_manager = manager;
+
+	return manager;
+}
+static uint32_t get_free_id(struct impl *impl)
+{
+	for (std::size_t i = 0; i < MAX_DEVICES; i++)
+		if (impl->devices[i].camera == nullptr)
+			return i;
+	return 0;
 }
 
 static struct device *add_device(struct impl *impl, std::shared_ptr<Camera> camera)
@@ -124,18 +132,19 @@ static struct device *add_device(struct impl *impl, std::shared_ptr<Camera> came
 
 	if (impl->n_devices >= MAX_DEVICES)
 		return NULL;
-	id = impl->n_devices++;
+	id = get_free_id(impl);;
 	device = &impl->devices[id];
-	device->id = id;
-	device->camera = camera;
+	device->id = get_free_id(impl);;
+	device->camera = std::move(camera);
+	impl->n_devices++;
 	return device;
 }
 
-static struct device *find_device(struct impl *impl, std::shared_ptr<Camera> camera)
+static struct device *find_device(struct impl *impl, const Camera *camera)
 {
 	uint32_t i;
 	for (i = 0; i < impl->n_devices; i++) {
-		if (impl->devices[i].camera == camera)
+		if (impl->devices[i].camera.get() == camera)
 			return &impl->devices[i];
 	}
 	return NULL;
@@ -143,12 +152,16 @@ static struct device *find_device(struct impl *impl, std::shared_ptr<Camera> cam
 
 static void remove_device(struct impl *impl, struct device *device)
 {
-	*device = impl->devices[--impl->n_devices];
+	uint32_t old = --impl->n_devices;
+	device->camera.reset();
+	*device = std::move(impl->devices[old]);
+	impl->devices[old].camera = nullptr;
 }
 
 static void clear_devices(struct impl *impl)
 {
-	impl->n_devices = 0;
+	while (impl->n_devices > 0)
+		impl->devices[--impl->n_devices].camera.reset();
 }
 
 static int emit_object_info(struct impl *impl, struct device *device)
@@ -176,66 +189,122 @@ static int emit_object_info(struct impl *impl, struct device *device)
 	ADD_ITEM(SPA_KEY_API_LIBCAMERA_PATH, path);
 #undef ADD_ITEM
 
-        dict = SPA_DICT_INIT(items, n_items);
-        info.props = &dict;
-        spa_device_emit_object_info(&impl->hooks, id, &info);
+	dict = SPA_DICT_INIT(items, n_items);
+	info.props = &dict;
+	spa_device_emit_object_info(&impl->hooks, id, &info);
 
 	return 1;
 }
 
-void Impl::addCamera(std::shared_ptr<Camera> camera)
+static void try_add_camera(struct impl *impl, std::shared_ptr<Camera> camera)
 {
-	struct impl *impl = this;
 	struct device *device;
 
-	spa_log_info(impl->log, "new camera");
-
-	if ((device = find_device(impl, camera)) != NULL)
+	if ((device = find_device(impl, camera.get())) != NULL)
 		return;
 
-	if ((device = add_device(impl, camera)) == NULL)
+	if ((device = add_device(impl, std::move(camera))) == NULL)
 		return;
 
+	spa_log_info(impl->log, "camera added: id:%d %s", device->id,
+			device->camera->id().c_str());
 	emit_object_info(impl, device);
 }
 
-void Impl::removeCamera(std::shared_ptr<Camera> camera)
+static void try_remove_camera(struct impl *impl, const Camera *camera)
 {
-	struct impl *impl = this;
 	struct device *device;
 
-	spa_log_info(impl->log, "camera removed");
 	if ((device = find_device(impl, camera)) == NULL)
 		return;
 
+	spa_log_info(impl->log, "camera removed: id:%d %s", device->id,
+			device->camera->id().c_str());
+	spa_device_emit_object_info(&impl->hooks, device->id, NULL);
 	remove_device(impl, device);
 }
 
-static int start_monitor(struct impl *impl)
+static void consume_hotplug_event(struct impl *impl, impl::hotplug_event& event)
 {
-	impl->manager->cameraAdded.connect(impl, &Impl::addCamera);
-        impl->manager->cameraRemoved.connect(impl, &Impl::removeCamera);
-	return 0;
+	auto& [ type, camera ] = event;
+
+	switch (type) {
+	case impl::hotplug_event::type::add:
+		spa_log_info(impl->log, "camera appeared: %s", camera->id().c_str());
+		try_add_camera(impl, std::move(camera));
+		break;
+	case impl::hotplug_event::type::remove:
+		spa_log_info(impl->log, "camera disappeared: %s", camera->id().c_str());
+		try_remove_camera(impl, camera.get());
+		break;
+	}
+}
+
+static void on_hotplug_event(void *data, std::uint64_t)
+{
+	auto impl = static_cast<struct impl *>(data);
+
+	for (;;) {
+		std::optional<impl::hotplug_event> event;
+
+		{
+			std::unique_lock guard(impl->hotplug_events_lock);
+
+			if (!impl->hotplug_events.empty()) {
+				event = std::move(impl->hotplug_events.front());
+				impl->hotplug_events.pop();
+			}
+		}
+
+		if (!event)
+			break;
+
+		consume_hotplug_event(impl, *event);
+	}
+}
+
+void impl::addCamera(std::shared_ptr<Camera> camera)
+{
+	{
+		std::unique_lock guard(hotplug_events_lock);
+		hotplug_events.push({ hotplug_event::type::add, std::move(camera) });
+	}
+
+	spa_loop_utils_signal_event(loop_utils, hotplug_event_source);
+}
+
+void impl::removeCamera(std::shared_ptr<Camera> camera)
+{
+	{
+		std::unique_lock guard(hotplug_events_lock);
+		hotplug_events.push({ hotplug_event::type::remove, std::move(camera) });
+	}
+
+	spa_loop_utils_signal_event(loop_utils, hotplug_event_source);
+}
+
+static void start_monitor(struct impl *impl)
+{
+	impl->manager->cameraAdded.connect(impl, &impl::addCamera);
+	impl->manager->cameraRemoved.connect(impl, &impl::removeCamera);
 }
 
 static int stop_monitor(struct impl *impl)
 {
-	if (impl->manager != NULL) {
-		impl->manager->cameraAdded.disconnect(impl, &Impl::addCamera);
-	        impl->manager->cameraRemoved.disconnect(impl, &Impl::removeCamera);
+	if (impl->manager) {
+		impl->manager->cameraAdded.disconnect(impl, &impl::addCamera);
+		impl->manager->cameraRemoved.disconnect(impl, &impl::removeCamera);
 	}
 	clear_devices (impl);
 	return 0;
 }
 
-static int enum_devices(struct impl *impl)
+static void collect_existing_devices(struct impl *impl)
 {
 	auto cameras = impl->manager->cameras();
 
-	for (const std::shared_ptr<Camera> &cam : cameras) {
-                impl->addCamera(cam);
-	}
-	return 0;
+	for (std::shared_ptr<Camera>& camera : cameras)
+		try_add_camera(impl, std::move(camera));
 }
 
 static const struct spa_dict_item device_info_items[] = {
@@ -262,9 +331,7 @@ static void impl_hook_removed(struct spa_hook *hook)
 	struct impl *impl = (struct impl*)hook->priv;
 	if (spa_hook_list_is_empty(&impl->hooks)) {
 		stop_monitor(impl);
-		if (impl->manager)
-			libcamera_manager_release(impl->manager);
-		impl->manager = NULL;
+		impl->manager.reset();
 	}
 }
 
@@ -274,26 +341,29 @@ impl_device_add_listener(void *object, struct spa_hook *listener,
 {
 	int res;
 	struct impl *impl = (struct impl*) object;
-        struct spa_hook_list save;
+	struct spa_hook_list save;
+	bool had_manager = !!impl->manager;
 
 	spa_return_val_if_fail(impl != NULL, -EINVAL);
 	spa_return_val_if_fail(events != NULL, -EINVAL);
 
-	impl->manager = libcamera_manager_acquire();
-	if (impl->manager == NULL)
-		return -errno;
+	if (!impl->manager && !(impl->manager = libcamera_manager_acquire(res)))
+		return res;
 
-        spa_hook_list_isolate(&impl->hooks, &save, listener, events, data);
+	spa_hook_list_isolate(&impl->hooks, &save, listener, events, data);
 
 	emit_device_info(impl, true);
 
-	if ((res = enum_devices(impl)) < 0)
-		return res;
-
-	if ((res = start_monitor(impl)) < 0)
-		return res;
+	if (had_manager) {
+		for (std::size_t i = 0; i < impl->n_devices; i++)
+			emit_object_info(impl, &impl->devices[i]);
+	}
+	else {
+		collect_existing_devices(impl);
+		start_monitor(impl);
+	}
 
-        spa_hook_list_join(&impl->hooks, &save);
+	spa_hook_list_join(&impl->hooks, &save);
 
 	listener->removed = impl_hook_removed;
 	listener->priv = impl;
@@ -325,14 +395,30 @@ static int impl_get_interface(struct spa_handle *handle, const char *type, void
 
 static int impl_clear(struct spa_handle *handle)
 {
-	struct impl *impl = (struct impl *) handle;
+	auto impl = reinterpret_cast<struct impl *>(handle);
+
 	stop_monitor(impl);
-	if (impl->manager)
-		libcamera_manager_release(impl->manager);
-	impl->manager = NULL;
+	std::destroy_at(impl);
+
 	return 0;
 }
 
+impl::impl(spa_log *log, spa_loop_utils *loop_utils, spa_source *hotplug_event_source)
+	: handle({ SPA_VERSION_HANDLE, impl_get_interface, impl_clear }),
+	  log(log),
+	  loop_utils(loop_utils),
+	  hotplug_event_source(hotplug_event_source)
+{
+	libcamera_log_topic_init(log);
+
+	spa_hook_list_init(&hooks);
+
+	device.iface = SPA_INTERFACE_INIT(
+			SPA_TYPE_INTERFACE_Device,
+			SPA_VERSION_DEVICE,
+			&impl_device, this);
+}
+
 static size_t
 impl_get_size(const struct spa_handle_factory *factory,
 	      const struct spa_dict *params)
@@ -347,35 +433,25 @@ 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;
-
-	impl->log = (struct spa_log*)spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
-	libcamera_log_topic_init(impl->log);
+	auto log = static_cast<spa_log *>(spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log));
 
-	impl->main_loop = (struct spa_loop*)spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Loop);
-	if (impl->main_loop == NULL) {
-		spa_log_error(impl->log, "a main-loop is needed");
+	auto loop_utils = static_cast<spa_loop_utils *>(spa_support_find(support, n_support, SPA_TYPE_INTERFACE_LoopUtils));
+	if (!loop_utils) {
+		spa_log_error(log, "a " SPA_TYPE_INTERFACE_LoopUtils " is needed");
 		return -EINVAL;
 	}
-	spa_hook_list_init(&impl->hooks);
 
-	impl->device.iface = SPA_INTERFACE_INIT(
-			SPA_TYPE_INTERFACE_Device,
-			SPA_VERSION_DEVICE,
-			&impl_device, impl);
+	auto hotplug_event_source = spa_loop_utils_add_event(loop_utils, on_hotplug_event, handle);
+	if (!hotplug_event_source) {
+		int res = -errno;
+		spa_log_error(log, "failed to create hotplug event: %m");
+		return res;
+	}
 
-	impl->info = SPA_DEVICE_INFO_INIT();
-	impl->info_all = SPA_DEVICE_CHANGE_MASK_FLAGS |
-			SPA_DEVICE_CHANGE_MASK_PROPS;
-	impl->info.flags = 0;
+	new (handle) impl(log, loop_utils, hotplug_event_source);
 
 	return 0;
 }
diff --git a/spa/plugins/libcamera/libcamera-manager.hpp b/spa/plugins/libcamera/libcamera-manager.hpp
index 53e488bc52f7386641bcc10780198cd576928cc0..4336a392ec2ae6b55cb8bc6d8739babc4e385a31 100644
--- a/spa/plugins/libcamera/libcamera-manager.hpp
+++ b/spa/plugins/libcamera/libcamera-manager.hpp
@@ -22,11 +22,8 @@
  * DEALINGS IN THE SOFTWARE.
  */
 
-#include <libcamera/camera_manager.h>
-
-#include <linux/media.h>
+#include <memory>
 
-using namespace libcamera;
+#include <libcamera/camera_manager.h>
 
-CameraManager *libcamera_manager_acquire(void);
-int libcamera_manager_release(CameraManager *manager);
+std::shared_ptr<libcamera::CameraManager> libcamera_manager_acquire(int& res);
diff --git a/spa/plugins/libcamera/libcamera-source.cpp b/spa/plugins/libcamera/libcamera-source.cpp
index 0d32b745b72c399c423169e8af553847d1cfcbd7..565325369cd4b499b896a635660d20c9506e58f4 100644
--- a/spa/plugins/libcamera/libcamera-source.cpp
+++ b/spa/plugins/libcamera/libcamera-source.cpp
@@ -31,6 +31,7 @@
 #include <sys/stat.h>
 #include <fcntl.h>
 #include <deque>
+#include <optional>
 
 #include <spa/support/plugin.h>
 #include <spa/support/log.h>
@@ -60,15 +61,9 @@
 #include "libcamera.h"
 #include "libcamera-manager.hpp"
 
-struct props {
-	char device[128];
-	char device_name[128];
-};
+using namespace libcamera;
 
-static void reset_props(struct props *props)
-{
-	spa_zero(*props);
-}
+namespace {
 
 #define MAX_BUFFERS	32
 #define MASK_BUFFERS	31
@@ -97,75 +92,110 @@ struct control {
 struct port {
 	struct impl *impl;
 
-	bool have_format;
-	struct spa_video_info current_format;
-	struct spa_fraction rate;
+	std::optional<spa_video_info> current_format;
+
+	struct spa_fraction rate = {};
 	StreamConfiguration streamConfig;
 
-	uint32_t memtype;
+	uint32_t memtype = 0;
 
 	struct control controls[MAX_CONTROLS];
-	uint32_t n_controls;
+	uint32_t n_controls = 0;
 
 	struct buffer buffers[MAX_BUFFERS];
-	uint32_t n_buffers;
+	uint32_t n_buffers = 0;
 	struct spa_list queue;
-	struct spa_ringbuffer ring;
+	struct spa_ringbuffer ring = SPA_RINGBUFFER_INIT();
 	uint32_t ring_ids[MAX_BUFFERS];
 
-	uint64_t info_all;
-	struct spa_port_info info;
-	struct spa_io_buffers *io;
-	struct spa_io_sequence *control;
-	struct spa_param_info params[8];
-
-	uint32_t fmt_index;
-	bool next_fmt;
+	static constexpr uint64_t info_all = SPA_PORT_CHANGE_MASK_FLAGS | SPA_PORT_CHANGE_MASK_PARAMS;
+	struct spa_port_info info = SPA_PORT_INFO_INIT();
+	struct spa_io_buffers *io = nullptr;
+	struct spa_io_sequence *control = nullptr;
+#define PORT_PropInfo	0
+#define PORT_EnumFormat	1
+#define PORT_Meta	2
+#define PORT_IO		3
+#define PORT_Format	4
+#define PORT_Buffers	5
+#define N_PORT_PARAMS	6
+	struct spa_param_info params[N_PORT_PARAMS];
+
+	uint32_t fmt_index = 0;
 	PixelFormat enum_fmt;
-	uint32_t size_index;
-	bool next_size;
+	uint32_t size_index = 0;
+
+	port(struct impl *impl)
+		: impl(impl)
+	{
+		spa_list_init(&queue);
+
+		params[PORT_PropInfo] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ);
+		params[PORT_EnumFormat] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ);
+		params[PORT_Meta] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ);
+		params[PORT_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
+		params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+		params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+
+		info.flags = SPA_PORT_FLAG_LIVE | SPA_PORT_FLAG_PHYSICAL | SPA_PORT_FLAG_TERMINAL;
+		info.params = params;
+		info.n_params = N_PORT_PARAMS;
+	}
 };
 
 struct impl {
 	struct spa_handle handle;
-	struct spa_node node;
+	struct spa_node node = {};
 
 	struct spa_log *log;
 	struct spa_loop *data_loop;
 	struct spa_system *system;
 
-	uint64_t info_all;
-	struct spa_node_info info;
-	struct spa_param_info params[8];
-	struct props props;
+	static constexpr uint64_t info_all =
+		SPA_NODE_CHANGE_MASK_FLAGS |
+		SPA_NODE_CHANGE_MASK_PROPS |
+		SPA_NODE_CHANGE_MASK_PARAMS;
+	struct spa_node_info info = SPA_NODE_INFO_INIT();
+#define NODE_PropInfo	0
+#define NODE_Props	1
+#define NODE_EnumFormat	2
+#define NODE_Format	3
+#define N_NODE_PARAMS	4
+	struct spa_param_info params[N_NODE_PARAMS];
+
+	std::string device_id;
+	std::string device_name;
 
 	struct spa_hook_list hooks;
-	struct spa_callbacks callbacks;
+	struct spa_callbacks callbacks = {};
 
-	struct port out_ports[1];
+	std::array<port, 1> out_ports;
 
-	struct spa_io_position *position;
-	struct spa_io_clock *clock;
+	struct spa_io_position *position = nullptr;
+	struct spa_io_clock *clock = nullptr;
 
-	CameraManager *manager;
+	std::shared_ptr<CameraManager> manager;
 	std::shared_ptr<Camera> camera;
 
-	FrameBufferAllocator *allocator;
+	FrameBufferAllocator *allocator = nullptr;
 	std::vector<std::unique_ptr<libcamera::Request>> requestPool;
 	std::deque<libcamera::Request *> pendingRequests;
 
 	void requestComplete(libcamera::Request *request);
 
-	unsigned int have_config;
 	std::unique_ptr<CameraConfiguration> config;
 
-	struct spa_source source;
+	struct spa_source source = {};
+
+	ControlList ctrls;
+	bool active = false;
+	bool acquired = false;
 
-	unsigned int active:1;
-	unsigned int acquired:1;
+	impl(spa_log *log, spa_loop *data_loop, spa_system *system,
+	     std::shared_ptr<CameraManager> manager, std::shared_ptr<Camera> camera, std::string device_id);
 };
 
-typedef struct impl Impl;
+}
 
 #define CHECK_PORT(impl,direction,port_id)  ((direction) == SPA_DIRECTION_OUTPUT && (port_id) == 0)
 
@@ -174,6 +204,55 @@ typedef struct impl Impl;
 
 #include "libcamera-utils.cpp"
 
+static int port_get_format(struct impl *impl, struct port *port,
+			   uint32_t index,
+			   const struct spa_pod *filter,
+			   struct spa_pod **param,
+			   struct spa_pod_builder *builder)
+{
+	struct spa_pod_frame f;
+
+	if (!port->current_format)
+		return -EIO;
+	if (index > 0)
+		return 0;
+
+	spa_pod_builder_push_object(builder, &f, SPA_TYPE_OBJECT_Format, SPA_PARAM_Format);
+	spa_pod_builder_add(builder,
+		SPA_FORMAT_mediaType,    SPA_POD_Id(port->current_format->media_type),
+		SPA_FORMAT_mediaSubtype, SPA_POD_Id(port->current_format->media_subtype),
+		0);
+
+	switch (port->current_format->media_subtype) {
+	case SPA_MEDIA_SUBTYPE_raw:
+		spa_pod_builder_add(builder,
+			SPA_FORMAT_VIDEO_format,    SPA_POD_Id(port->current_format->info.raw.format),
+			SPA_FORMAT_VIDEO_size,      SPA_POD_Rectangle(&port->current_format->info.raw.size),
+			SPA_FORMAT_VIDEO_framerate, SPA_POD_Fraction(&port->current_format->info.raw.framerate),
+			0);
+		break;
+	case SPA_MEDIA_SUBTYPE_mjpg:
+	case SPA_MEDIA_SUBTYPE_jpeg:
+		spa_pod_builder_add(builder,
+			SPA_FORMAT_VIDEO_size,      SPA_POD_Rectangle(&port->current_format->info.mjpg.size),
+			SPA_FORMAT_VIDEO_framerate, SPA_POD_Fraction(&port->current_format->info.mjpg.framerate),
+			0);
+		break;
+	case SPA_MEDIA_SUBTYPE_h264:
+		spa_pod_builder_add(builder,
+			SPA_FORMAT_VIDEO_size,      SPA_POD_Rectangle(&port->current_format->info.h264.size),
+			SPA_FORMAT_VIDEO_framerate, SPA_POD_Fraction(&port->current_format->info.h264.framerate),
+			0);
+		break;
+	default:
+		return -EIO;
+	}
+
+	*param = (struct spa_pod*)spa_pod_builder_pop(builder, &f);
+
+	return 1;
+}
+
 static int impl_node_enum_params(void *object, int seq,
 				 uint32_t id, uint32_t start, uint32_t num,
 				 const struct spa_pod *filter)
@@ -184,6 +263,7 @@ static int impl_node_enum_params(void *object, int seq,
 	uint8_t buffer[1024];
 	struct spa_result_node_params result;
 	uint32_t count = 0;
+	int res;
 
 	spa_return_val_if_fail(impl != NULL, -EINVAL);
 	spa_return_val_if_fail(num != 0, -EINVAL);
@@ -198,44 +278,49 @@ next:
 	switch (id) {
 	case SPA_PARAM_PropInfo:
 	{
-		struct props *p = &impl->props;
-
 		switch (result.index) {
 		case 0:
 			param = (struct spa_pod*)spa_pod_builder_add_object(&b,
 				SPA_TYPE_OBJECT_PropInfo, id,
 				SPA_PROP_INFO_id,   SPA_POD_Id(SPA_PROP_device),
 				SPA_PROP_INFO_description, SPA_POD_String("The libcamera device"),
-				SPA_PROP_INFO_type, SPA_POD_String(p->device));
+				SPA_PROP_INFO_type, SPA_POD_String(impl->device_id.c_str()));
 			break;
 		case 1:
 			param = (struct spa_pod*)spa_pod_builder_add_object(&b,
 				SPA_TYPE_OBJECT_PropInfo, id,
 				SPA_PROP_INFO_id,   SPA_POD_Id(SPA_PROP_deviceName),
 				SPA_PROP_INFO_description, SPA_POD_String("The libcamera device name"),
-				SPA_PROP_INFO_type, SPA_POD_String(p->device_name));
+				SPA_PROP_INFO_type, SPA_POD_String(impl->device_name.c_str()));
 			break;
 		default:
-			return 0;
+			return spa_libcamera_enum_controls(impl,
+					GET_OUT_PORT(impl, 0),
+					seq, result.index - 2, num, filter);
 		}
 		break;
 	}
 	case SPA_PARAM_Props:
 	{
-		struct props *p = &impl->props;
-
 		switch (result.index) {
 		case 0:
 			param = (struct spa_pod*)spa_pod_builder_add_object(&b,
 				SPA_TYPE_OBJECT_Props, id,
-				SPA_PROP_device,     SPA_POD_String(p->device),
-				SPA_PROP_deviceName, SPA_POD_String(p->device_name));
+				SPA_PROP_device,     SPA_POD_String(impl->device_id.c_str()),
+				SPA_PROP_deviceName, SPA_POD_String(impl->device_name.c_str()));
 			break;
 		default:
 			return 0;
 		}
 		break;
 	}
+	case SPA_PARAM_EnumFormat:
+		return spa_libcamera_enum_format(impl, GET_OUT_PORT(impl, 0),
+				seq, start, num, filter);
+	case SPA_PARAM_Format:
+		if ((res = port_get_format(impl, GET_OUT_PORT(impl, 0), result.index, filter, &param, &b)) <= 0)
+			return res;
+		break;
 	default:
 		return -ENOENT;
 	}
@@ -262,15 +347,28 @@ static int impl_node_set_param(void *object,
 	switch (id) {
 	case SPA_PARAM_Props:
 	{
-		struct props *p = &impl->props;
+		struct spa_pod_object *obj = (struct spa_pod_object *) param;
+		struct spa_pod_prop *prop;
 
 		if (param == NULL) {
-			reset_props(p);
+			impl->device_id.clear();
+			impl->device_name.clear();
 			return 0;
 		}
-		spa_pod_parse_object(param,
-			SPA_TYPE_OBJECT_Props, NULL,
-			SPA_PROP_device, SPA_POD_OPT_Stringn(p->device, sizeof(p->device)));
+		SPA_POD_OBJECT_FOREACH(obj, prop) {
+			char device[128];
+
+			switch (prop->key) {
+			case SPA_PROP_device:
+				strncpy(device, (char *)SPA_POD_CONTENTS(struct spa_pod_string, &prop->value),
+						sizeof(device)-1);
+				impl->device_id = device;
+				break;
+			default:
+				spa_libcamera_set_control(impl, prop);
+				break;
+			}
+		}
 		break;
 	}
 	default:
@@ -311,7 +409,7 @@ static int impl_node_send_command(void *object, const struct spa_command *comman
 	{
 		struct port *port = GET_OUT_PORT(impl, 0);
 
-		if (!port->have_format)
+		if (!port->current_format)
 			return -EIO;
 		if (port->n_buffers == 0)
 			return -EIO;
@@ -336,7 +434,6 @@ static const struct spa_dict_item info_items[] = {
 	{ SPA_KEY_DEVICE_API, "libcamera" },
 	{ SPA_KEY_MEDIA_CLASS, "Video/Source" },
 	{ SPA_KEY_MEDIA_ROLE, "Camera" },
-	{ SPA_KEY_NODE_PAUSE_ON_IDLE, "false" },
 	{ SPA_KEY_NODE_DRIVER, "true" },
 };
 
@@ -424,55 +521,6 @@ static int impl_node_remove_port(void *object,
 	return -ENOTSUP;
 }
 
-static int port_get_format(struct impl *impl, struct port *port,
-			   uint32_t index,
-			   const struct spa_pod *filter,
-			   struct spa_pod **param,
-			   struct spa_pod_builder *builder)
-{
-	struct spa_pod_frame f;
-
-	if (!port->have_format)
-		return -EIO;
-	if (index > 0)
-		return 0;
-
-	spa_pod_builder_push_object(builder, &f, SPA_TYPE_OBJECT_Format, SPA_PARAM_Format);
-	spa_pod_builder_add(builder,
-		SPA_FORMAT_mediaType,    SPA_POD_Id(port->current_format.media_type),
-		SPA_FORMAT_mediaSubtype, SPA_POD_Id(port->current_format.media_subtype),
-		0);
-
-	switch (port->current_format.media_subtype) {
-	case SPA_MEDIA_SUBTYPE_raw:
-		spa_pod_builder_add(builder,
-			SPA_FORMAT_VIDEO_format,    SPA_POD_Id(port->current_format.info.raw.format),
-			SPA_FORMAT_VIDEO_size,      SPA_POD_Rectangle(&port->current_format.info.raw.size),
-			SPA_FORMAT_VIDEO_framerate, SPA_POD_Fraction(&port->current_format.info.raw.framerate),
-			0);
-		break;
-	case SPA_MEDIA_SUBTYPE_mjpg:
-	case SPA_MEDIA_SUBTYPE_jpeg:
-		spa_pod_builder_add(builder,
-			SPA_FORMAT_VIDEO_size,      SPA_POD_Rectangle(&port->current_format.info.mjpg.size),
-			SPA_FORMAT_VIDEO_framerate, SPA_POD_Fraction(&port->current_format.info.mjpg.framerate),
-			0);
-		break;
-	case SPA_MEDIA_SUBTYPE_h264:
-		spa_pod_builder_add(builder,
-			SPA_FORMAT_VIDEO_size,      SPA_POD_Rectangle(&port->current_format.info.h264.size),
-			SPA_FORMAT_VIDEO_framerate, SPA_POD_Fraction(&port->current_format.info.h264.framerate),
-			0);
-		break;
-	default:
-		return -EIO;
-	}
-
-	*param = (struct spa_pod*)spa_pod_builder_pop(builder, &f);
-
-	return 1;
-}
-
 static int impl_node_port_enum_params(void *object, int seq,
 				      enum spa_direction direction,
 				      uint32_t port_id,
@@ -515,7 +563,7 @@ next:
 		break;
 	case SPA_PARAM_Buffers:
 	{
-		if (!port->have_format)
+		if (!port->current_format)
 			return -EIO;
 		if (result.index > 0)
 			return 0;
@@ -591,16 +639,17 @@ static int port_set_format(struct impl *impl, struct port *port,
 	int res;
 
 	if (format == NULL) {
-		if (!port->have_format)
+		if (!port->current_format)
 			return 0;
 
 		spa_libcamera_stream_off(impl);
 		spa_libcamera_clear_buffers(impl, port);
-		port->have_format = false;
+		port->current_format.reset();
 
 		spa_libcamera_close(impl);
 		goto done;
 	} else {
+		spa_zero(info);
 		if ((res = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0)
 			return res;
 
@@ -616,31 +665,31 @@ static int port_set_format(struct impl *impl, struct port *port,
 				return -EINVAL;
 			}
 
-			if (port->have_format && info.media_type == port->current_format.media_type &&
-			    info.media_subtype == port->current_format.media_subtype &&
-			    info.info.raw.format == port->current_format.info.raw.format &&
-			    info.info.raw.size.width == port->current_format.info.raw.size.width &&
-			    info.info.raw.size.height == port->current_format.info.raw.size.height)
+			if (port->current_format && info.media_type == port->current_format->media_type &&
+			    info.media_subtype == port->current_format->media_subtype &&
+			    info.info.raw.format == port->current_format->info.raw.format &&
+			    info.info.raw.size.width == port->current_format->info.raw.size.width &&
+			    info.info.raw.size.height == port->current_format->info.raw.size.height)
 				return 0;
 			break;
 		case SPA_MEDIA_SUBTYPE_mjpg:
 			if (spa_format_video_mjpg_parse(format, &info.info.mjpg) < 0)
 				return -EINVAL;
 
-			if (port->have_format && info.media_type == port->current_format.media_type &&
-			    info.media_subtype == port->current_format.media_subtype &&
-			    info.info.mjpg.size.width == port->current_format.info.mjpg.size.width &&
-			    info.info.mjpg.size.height == port->current_format.info.mjpg.size.height)
+			if (port->current_format && info.media_type == port->current_format->media_type &&
+			    info.media_subtype == port->current_format->media_subtype &&
+			    info.info.mjpg.size.width == port->current_format->info.mjpg.size.width &&
+			    info.info.mjpg.size.height == port->current_format->info.mjpg.size.height)
 				return 0;
 			break;
 		case SPA_MEDIA_SUBTYPE_h264:
 			if (spa_format_video_h264_parse(format, &info.info.h264) < 0)
 				return -EINVAL;
 
-			if (port->have_format && info.media_type == port->current_format.media_type &&
-			    info.media_subtype == port->current_format.media_subtype &&
-			    info.info.h264.size.width == port->current_format.info.h264.size.width &&
-			    info.info.h264.size.height == port->current_format.info.h264.size.height)
+			if (port->current_format && info.media_type == port->current_format->media_type &&
+			    info.media_subtype == port->current_format->media_subtype &&
+			    info.info.h264.size.width == port->current_format->info.h264.size.width &&
+			    info.info.h264.size.height == port->current_format->info.h264.size.height)
 				return 0;
 			break;
 		default:
@@ -648,9 +697,9 @@ static int port_set_format(struct impl *impl, struct port *port,
 		}
 	}
 
-	if (port->have_format && !(flags & SPA_NODE_PARAM_FLAG_TEST_ONLY)) {
+	if (port->current_format && !(flags & SPA_NODE_PARAM_FLAG_TEST_ONLY)) {
 		spa_libcamera_use_buffers(impl, port, NULL, 0);
-		port->have_format = false;
+		port->current_format.reset();
 	}
 
 	if (spa_libcamera_set_format(impl, port, &info, flags & SPA_NODE_PARAM_FLAG_TEST_ONLY) < 0)
@@ -658,19 +707,22 @@ static int port_set_format(struct impl *impl, struct port *port,
 
 	if (!(flags & SPA_NODE_PARAM_FLAG_TEST_ONLY)) {
 		port->current_format = info;
-		port->have_format = true;
 	}
 
     done:
+	impl->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS;
 	port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
-	if (port->have_format) {
-		port->params[4] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE);
-		port->params[5] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ);
+	if (port->current_format) {
+		impl->params[NODE_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE);
+		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[4] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
-		port->params[5] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+		impl->params[NODE_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+		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(impl, port, false);
+	emit_node_info(impl, false);
 
 	return 0;
 }
@@ -715,7 +767,7 @@ static int impl_node_port_use_buffers(void *object,
 
 	port = GET_PORT(impl, direction, port_id);
 
-	if (!port->have_format)
+	if (!port->current_format)
 		return -EIO;
 
 	if (port->n_buffers) {
@@ -781,15 +833,9 @@ static int impl_node_port_reuse_buffer(void *object,
 	return res;
 }
 
-static void set_control(struct impl *impl, struct port *port, uint32_t control_id, float value)
-{
-	spa_log_error(impl->log, "Failed to set control");
-}
-
 static int process_control(struct impl *impl, struct spa_pod_sequence *control)
 {
 	struct spa_pod_control *c;
-	struct port *port;
 
 	SPA_POD_SEQUENCE_FOREACH(control, c) {
 		switch (c->type) {
@@ -799,9 +845,7 @@ static int process_control(struct impl *impl, struct spa_pod_sequence *control)
 			struct spa_pod_object *obj = (struct spa_pod_object *) &c->value;
 
 			SPA_POD_OBJECT_FOREACH(obj, prop) {
-				port = GET_OUT_PORT(impl, 0);
-				set_control(impl, port, prop->key,
-						SPA_POD_VALUE(struct spa_pod_float, &prop->value));
+				spa_libcamera_set_control(impl, prop);
 			}
 			break;
 		}
@@ -896,13 +940,41 @@ static int impl_get_interface(struct spa_handle *handle, const char *type, void
 
 static int impl_clear(struct spa_handle *handle)
 {
-	struct impl *impl;
-
-	impl = (struct impl *) handle;
-	impl->~Impl();
+	std::destroy_at(reinterpret_cast<impl *>(handle));
 	return 0;
 }
 
+impl::impl(spa_log *log, spa_loop *data_loop, spa_system *system,
+	   std::shared_ptr<CameraManager> manager, std::shared_ptr<Camera> camera, std::string device_id)
+	: handle({ SPA_VERSION_HANDLE, impl_get_interface, impl_clear }),
+	  log(log),
+	  data_loop(data_loop),
+	  system(system),
+	  device_id(std::move(device_id)),
+	  out_ports{{this}},
+	  manager(std::move(manager)),
+	  camera(std::move(camera))
+{
+	libcamera_log_topic_init(log);
+
+	spa_hook_list_init(&hooks);
+
+	node.iface = SPA_INTERFACE_INIT(
+			SPA_TYPE_INTERFACE_Node,
+			SPA_VERSION_NODE,
+			&impl_node, this);
+
+	params[NODE_PropInfo] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ);
+	params[NODE_Props] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE);
+	params[NODE_EnumFormat] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ);
+	params[NODE_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+
+	info.max_output_ports = 1;
+	info.flags = SPA_NODE_FLAG_RT;
+	info.params = params;
+	info.n_params = N_NODE_PARAMS;
+}
+
 static size_t
 impl_get_size(const struct spa_handle_factory *factory,
 	      const struct spa_dict *params)
@@ -917,87 +989,45 @@ impl_init(const struct spa_handle_factory *factory,
 	  const struct spa_support *support,
 	  uint32_t n_support)
 {
-	struct impl *impl;
 	const char *str;
-	struct port *port;
 	int res;
 
 	spa_return_val_if_fail(factory != NULL, -EINVAL);
 	spa_return_val_if_fail(handle != NULL, -EINVAL);
 
-	impl = new(handle) Impl();
-
-	handle->get_interface = impl_get_interface;
-	handle->clear = impl_clear;
+	auto log = static_cast<spa_log *>(spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log));
+	auto data_loop = static_cast<spa_loop *>(spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataLoop));
+	auto system = static_cast<spa_system *>(spa_support_find(support, n_support, SPA_TYPE_INTERFACE_System));
 
-	impl->log = (struct spa_log*)spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
-	libcamera_log_topic_init(impl->log);
-
-	impl->data_loop = (struct spa_loop*)spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataLoop);
-	impl->system = (struct spa_system*)spa_support_find(support, n_support, SPA_TYPE_INTERFACE_System);
-
-	if (impl->data_loop == NULL) {
-		spa_log_error(impl->log, "a data_loop is needed");
+	if (!data_loop) {
+		spa_log_error(log, "a data_loop is needed");
 		return -EINVAL;
 	}
 
-	if (impl->system == NULL) {
-		spa_log_error(impl->log, "a system is needed");
+	if (!system) {
+		spa_log_error(log, "a system is needed");
 		return -EINVAL;
 	}
 
-	impl->node.iface = SPA_INTERFACE_INIT(
-			SPA_TYPE_INTERFACE_Node,
-			SPA_VERSION_NODE,
-			&impl_node, impl);
-	spa_hook_list_init(&impl->hooks);
-
-	impl->info_all = SPA_NODE_CHANGE_MASK_FLAGS |
-			SPA_NODE_CHANGE_MASK_PROPS |
-			SPA_NODE_CHANGE_MASK_PARAMS;
-	impl->info = SPA_NODE_INFO_INIT();
-	impl->info.max_output_ports = 1;
-	impl->info.flags = SPA_NODE_FLAG_RT;
-	impl->params[0] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ);
-	impl->params[1] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE);
-	impl->info.params = impl->params;
-	impl->info.n_params = 2;
-	reset_props(&impl->props);
-
-	port = GET_OUT_PORT(impl, 0);
-	port->impl = impl;
-	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_LIVE |
-			   SPA_PORT_FLAG_PHYSICAL |
-			   SPA_PORT_FLAG_TERMINAL;
-	port->params[0] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ);
-	port->params[1] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ);
-	port->params[2] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ);
-	port->params[3] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
-	port->params[4] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
-	port->params[5] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
-	port->info.params = port->params;
-	port->info.n_params = 6;
+	auto manager = libcamera_manager_acquire(res);
+	if (!manager) {
+		spa_log_error(log, "can't start camera manager: %s", spa_strerror(res));
+		return res;
+	}
 
+	std::string device_id;
 	if (info && (str = spa_dict_lookup(info, SPA_KEY_API_LIBCAMERA_PATH)))
-		strncpy(impl->props.device, str, sizeof(impl->props.device));
+		device_id = str;
 
-	impl->manager = libcamera_manager_acquire();
-	if (impl->manager == NULL) {
-		res = -errno;
-		spa_log_error(impl->log, "can't start camera manager: %s", spa_strerror(res));
-                return res;
-	}
-
-	impl->camera = impl->manager->get(impl->props.device);
-	if (impl->camera == NULL) {
-		spa_log_error(impl->log, "unknown camera id %s", impl->props.device);
-		libcamera_manager_release(impl->manager);
+	auto camera = manager->get(device_id);
+	if (!camera) {
+		spa_log_error(log, "unknown camera id %s", device_id.c_str());
 		return -ENOENT;
 	}
+
+	new (handle) impl(log, data_loop, system,
+			  std::move(manager), std::move(camera), std::move(device_id));
+
 	return 0;
 }
 
diff --git a/spa/plugins/libcamera/libcamera-utils.cpp b/spa/plugins/libcamera/libcamera-utils.cpp
index 99d0672541f1605ad05696e90c193beb4dee7ae4..446220bd10500cb38035cd9bddc1e3f120f39010 100644
--- a/spa/plugins/libcamera/libcamera-utils.cpp
+++ b/spa/plugins/libcamera/libcamera-utils.cpp
@@ -32,6 +32,7 @@
 #include <errno.h>
 #include <sys/mman.h>
 #include <poll.h>
+#include <limits.h>
 
 #include <linux/media.h>
 
@@ -40,7 +41,7 @@ int spa_libcamera_open(struct impl *impl)
 	if (impl->acquired)
 		return 0;
 
-	spa_log_info(impl->log, "open camera %s", impl->props.device);
+	spa_log_info(impl->log, "open camera %s", impl->device_id.c_str());
 	impl->camera->acquire();
 
 	impl->allocator = new FrameBufferAllocator(impl->camera);
@@ -54,10 +55,10 @@ int spa_libcamera_close(struct impl *impl)
 	struct port *port = &impl->out_ports[0];
 	if (!impl->acquired)
 		return 0;
-	if (impl->active || port->have_format)
+	if (impl->active || port->current_format)
 		return 0;
 
-	spa_log_info(impl->log, "close camera %s", impl->props.device);
+	spa_log_info(impl->log, "close camera %s", impl->device_id.c_str());
 	delete impl->allocator;
 	impl->allocator = nullptr;
 
@@ -69,13 +70,12 @@ int spa_libcamera_close(struct impl *impl)
 
 static void spa_libcamera_get_config(struct impl *impl)
 {
-	if (impl->have_config)
+	if (impl->config)
 		return;
 
 	StreamRoles roles;
 	roles.push_back(VideoRecording);
 	impl->config = impl->camera->generateConfiguration(roles);
-	impl->have_config = true;
 }
 
 static int spa_libcamera_buffer_recycle(struct impl *impl, struct port *port, uint32_t buffer_id)
@@ -91,10 +91,10 @@ static int spa_libcamera_buffer_recycle(struct impl *impl, struct port *port, ui
 	if (buffer_id >= impl->requestPool.size()) {
 		spa_log_warn(impl->log, "invalid buffer_id %u >= %zu",
 				buffer_id, impl->requestPool.size());
-                return -EINVAL;
-        }
+		return -EINVAL;
+	}
 	Request *request = impl->requestPool[buffer_id].get();
-        Stream *stream = port->streamConfig.stream();
+	Stream *stream = port->streamConfig.stream();
 	FrameBuffer *buffer = impl->allocator->buffers(stream)[buffer_id].get();
 	if ((res = request->addBuffer(stream, buffer)) < 0) {
 		spa_log_warn(impl->log, "can't add buffer %u for request: %s",
@@ -104,7 +104,9 @@ static int spa_libcamera_buffer_recycle(struct impl *impl, struct port *port, ui
 	if (!impl->active) {
 		impl->pendingRequests.push_back(request);
 		return 0;
-        } else {
+	} else {
+		request->controls().merge(impl->ctrls);
+		impl->ctrls.clear();
 		if ((res = impl->camera->queueRequest(request)) < 0) {
 			spa_log_warn(impl->log, "can't queue buffer %u: %s",
 				buffer_id, spa_strerror(res));
@@ -119,7 +121,7 @@ static int allocBuffers(struct impl *impl, struct port *port, unsigned int count
 	int res;
 
 	if ((res = impl->allocator->allocate(port->streamConfig.stream())) < 0)
-                return res;
+		return res;
 
 	for (unsigned int i = 0; i < count; i++) {
 		std::unique_ptr<Request> request = impl->camera->createRequest(i);
@@ -129,7 +131,7 @@ static int allocBuffers(struct impl *impl, struct port *port, unsigned int count
 		}
 		impl->requestPool.push_back(std::move(request));
 	}
-        return res;
+	return res;
 }
 
 static void freeBuffers(struct impl *impl, struct port *port)
@@ -239,6 +241,14 @@ static const struct format_info *find_format_info_by_media_type(uint32_t type,
 	return NULL;
 }
 
+static int score_size(Size &a, Size &b)
+{
+	int x, y;
+	x = (int)a.width - (int)b.width;
+	y = (int)a.height - (int)b.height;
+	return x * x + y * y;
+}
+
 static int
 spa_libcamera_enum_format(struct impl *impl, struct port *port, int seq,
 		     uint32_t start, uint32_t num, const struct spa_pod *filter)
@@ -250,7 +260,7 @@ spa_libcamera_enum_format(struct impl *impl, struct port *port, int seq,
 	struct spa_pod_frame f[2];
 	struct spa_result_node_params result;
 	struct spa_pod *fmt;
-	uint32_t count = 0;
+	uint32_t i, count = 0, num_sizes;
 	PixelFormat format;
 	Size frameSize;
 	SizeRange sizeRange = SizeRange();
@@ -285,8 +295,23 @@ next_fmt:
 		goto next_fmt;
 	}
 
-	if (port->size_index < formats.sizes(format).size()) {
-		frameSize = formats.sizes(format)[port->size_index];
+	num_sizes = formats.sizes(format).size();
+	if (num_sizes > 0 && port->size_index <= num_sizes) {
+		if (port->size_index == 0) {
+			Size wanted = Size(640, 480), test;
+			int score, best = INT_MAX;
+			for (i = 0; i < num_sizes; i++) {
+				test = formats.sizes(format)[i];
+				score = score_size(wanted, test);
+				if (score < best) {
+					best = score;
+					frameSize = test;
+				}
+			}
+		}
+		else {
+			frameSize = formats.sizes(format)[port->size_index - 1];
+		}
 	} else if (port->size_index < 1) {
 		sizeRange = formats.range(format);
 		if (sizeRange.hStep == 0 || sizeRange.vStep == 0) {
@@ -419,8 +444,6 @@ static int spa_libcamera_set_format(struct impl *impl, struct port *port,
 	if ((res = allocBuffers(impl, port, port->streamConfig.bufferCount)) < 0)
 		goto error;
 
-	port->have_format = true;
-
 	port->info.change_mask |= SPA_PORT_CHANGE_MASK_FLAGS | SPA_PORT_CHANGE_MASK_RATE;
 	port->info.flags = SPA_PORT_FLAG_CAN_ALLOC_BUFFERS |
 		SPA_PORT_FLAG_LIVE |
@@ -440,9 +463,158 @@ spa_libcamera_enum_controls(struct impl *impl, struct port *port, int seq,
 		       uint32_t start, uint32_t num,
 		       const struct spa_pod *filter)
 {
+	const ControlInfoMap &info = impl->camera->controls();
+	uint8_t buffer[1024];
+	struct spa_pod_builder b = { 0 };
+	struct spa_pod_frame f[2];
+	struct spa_result_node_params result;
+	struct spa_pod *ctrl;
+	uint32_t count = 0, skip;
+	int res;
+	const ControlId *ctrl_id;
+	ControlInfo ctrl_info;
+
+	result.id = SPA_PARAM_PropInfo;
+	result.next = start;
+
+	auto it = info.begin();
+	for (skip = result.next; skip; skip--)
+		it++;
+
+	if (false) {
+next:
+		it++;
+	}
+	result.index = result.next++;
+	if (it == info.end())
+		goto enum_end;
+
+	ctrl_id = it->first;
+	ctrl_info = it->second;
+
+	spa_pod_builder_init(&b, buffer, sizeof(buffer));
+	spa_pod_builder_push_object(&b, &f[0], SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo);
+	spa_pod_builder_add(&b,
+			SPA_PROP_INFO_id,   SPA_POD_Id(ctrl_id->id()),
+			SPA_PROP_INFO_description, SPA_POD_String(ctrl_id->name().c_str()),
+			0);
+
+	switch (ctrl_id->type()) {
+	case ControlTypeBool:
+		spa_pod_builder_add(&b,
+				SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(
+						(bool)ctrl_info.def().get<bool>()),
+				0);
+		break;
+	case ControlTypeFloat:
+		spa_pod_builder_add(&b,
+				SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float(
+						(float)ctrl_info.def().get<float>(),
+						(float)ctrl_info.min().get<float>(),
+						(float)ctrl_info.max().get<float>()),
+				0);
+		break;
+	case ControlTypeInteger32:
+		spa_pod_builder_add(&b,
+				SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Int(
+						(int32_t)ctrl_info.def().get<int32_t>(),
+						(int32_t)ctrl_info.min().get<int32_t>(),
+						(int32_t)ctrl_info.max().get<int32_t>()),
+				0);
+		break;
+	default:
+		goto next;
+	}
+
+	ctrl = (struct spa_pod*) spa_pod_builder_pop(&b, &f[0]);
+
+	if (spa_pod_filter(&b, &result.param, ctrl, filter) < 0)
+		goto next;
+
+	spa_node_emit_result(&impl->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+	if (++count != num)
+		goto next;
+
+enum_end:
+	res = 0;
+	return res;
+}
+
+struct val {
+	uint32_t type;
+	float f_val;
+	int32_t i_val;
+	bool b_val;
+	uint32_t id;
+};
+
+static int do_update_ctrls(struct spa_loop *loop,
+			    bool async,
+			    uint32_t seq,
+			    const void *data,
+			    size_t size,
+			    void *user_data)
+{
+	struct impl *impl = (struct impl *)user_data;
+	const struct val *d = (const struct val *)data;
+	switch (d->type) {
+	case ControlTypeBool:
+		impl->ctrls.set(d->id, d->b_val);
+		break;
+	case ControlTypeFloat:
+		impl->ctrls.set(d->id, d->f_val);
+		break;
+	case ControlTypeInteger32:
+		//impl->ctrls.set(d->id, (int32_t)d->i_val);
+		break;
+	default:
+		break;
+	}
 	return 0;
 }
 
+static int
+spa_libcamera_set_control(struct impl *impl, const struct spa_pod_prop *prop)
+{
+	const ControlInfoMap &info = impl->camera->controls();
+	const ControlId *ctrl_id;
+	int res;
+	struct val d;
+
+	auto v = info.idmap().find(prop->key);
+	if (v == info.idmap().end())
+		return -ENOENT;
+
+	ctrl_id = v->second;
+
+	d.type = ctrl_id->type();
+	d.id = ctrl_id->id();
+
+	switch (d.type) {
+	case ControlTypeBool:
+		if ((res = spa_pod_get_bool(&prop->value, &d.b_val)) < 0)
+			goto done;
+		break;
+	case ControlTypeFloat:
+		if ((res = spa_pod_get_float(&prop->value, &d.f_val)) < 0)
+			goto done;
+		break;
+	case ControlTypeInteger32:
+		if ((res = spa_pod_get_int(&prop->value, &d.i_val)) < 0)
+			goto done;
+		break;
+	default:
+		res = -EINVAL;
+		goto done;
+	}
+	spa_loop_invoke(impl->data_loop, do_update_ctrls, 0, &d, sizeof(d), true, impl);
+	res = 0;
+done:
+	return res;
+}
+
+
 static void libcamera_on_fd_events(struct spa_source *source)
 {
 	struct impl *impl = (struct impl*) source->data;
@@ -480,7 +652,12 @@ static void libcamera_on_fd_events(struct spa_source *source)
 	spa_list_append(&port->queue, &b->link);
 
 	io = port->io;
-	if (io != NULL && io->status != SPA_STATUS_HAVE_DATA) {
+	if (io == NULL) {
+		b = spa_list_first(&port->queue, struct buffer, link);
+		spa_list_remove(&b->link);
+		SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUTSTANDING);
+		spa_libcamera_buffer_recycle(impl, port, b->id);
+	} else if (io->status != SPA_STATUS_HAVE_DATA) {
 		if (io->buffer_id < port->n_buffers)
 			spa_libcamera_buffer_recycle(impl, port, io->buffer_id);
 
@@ -607,7 +784,7 @@ spa_libcamera_alloc_buffers(struct impl *impl, struct port *port,
 }
 
 
-void Impl::requestComplete(libcamera::Request *request)
+void impl::requestComplete(libcamera::Request *request)
 {
 	struct impl *impl = this;
 	struct port *port = &impl->out_ports[0];
@@ -617,20 +794,24 @@ void Impl::requestComplete(libcamera::Request *request)
 
 	spa_log_debug(impl->log, "request complete");
 
+	buffer_id = request->cookie();
+	b = &port->buffers[buffer_id];
+
 	if ((request->status() == Request::RequestCancelled)) {
-                spa_log_debug(impl->log, "Request was cancelled");
-                return;
-        }
+		spa_log_debug(impl->log, "Request was cancelled");
+		request->reuse();
+		SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUTSTANDING);
+		spa_libcamera_buffer_recycle(impl, port, b->id);
+		return;
+	}
 	FrameBuffer *buffer = request->findBuffer(stream);
 	if (buffer == nullptr) {
-                spa_log_warn(impl->log, "unknown buffer");
+		spa_log_warn(impl->log, "unknown buffer");
 		return;
 	}
 	const FrameMetadata &fmd = buffer->metadata();
 
-	buffer_id = request->cookie();
 
-	b = &port->buffers[buffer_id];
 
 	if (impl->clock) {
 		impl->clock->nsec = fmd.timestamp;
@@ -664,7 +845,7 @@ static int spa_libcamera_stream_on(struct impl *impl)
 	struct port *port = &impl->out_ports[0];
 	int res;
 
-	if (!port->have_format) {
+	if (!port->current_format) {
 		spa_log_error(impl->log, "Exting %s with -EIO", __FUNCTION__);
 		return -EIO;
 	}
@@ -674,15 +855,15 @@ static int spa_libcamera_stream_on(struct impl *impl)
 
 	impl->camera->requestCompleted.connect(impl, &impl::requestComplete);
 
-	spa_log_info(impl->log, "starting camera %s", impl->props.device);
+	spa_log_info(impl->log, "starting camera %s", impl->device_id.c_str());
 	if ((res = impl->camera->start()) < 0)
-		return res == -EACCES ? -EBUSY : res;
+		goto error;
 
 	for (Request *req : impl->pendingRequests) {
-                if ((res = impl->camera->queueRequest(req)) < 0)
-			return res == -EACCES ? -EBUSY : res;
-        }
-        impl->pendingRequests.clear();
+		if ((res = impl->camera->queueRequest(req)) < 0)
+			goto error_stop;
+	}
+	impl->pendingRequests.clear();
 
 	impl->source.func = libcamera_on_fd_events;
 	impl->source.data = impl;
@@ -691,13 +872,20 @@ static int spa_libcamera_stream_on(struct impl *impl)
 	impl->source.rmask = 0;
 	if (impl->source.fd < 0) {
 		spa_log_error(impl->log, "Failed to create eventfd: %s", spa_strerror(impl->source.fd));
-		return impl->source.fd;
+		res = impl->source.fd;
+		goto error_stop;
 	}
 	spa_loop_add_source(impl->data_loop, &impl->source);
 
 	impl->active = true;
 
 	return 0;
+
+error_stop:
+	impl->camera->stop();
+error:
+	impl->camera->requestCompleted.disconnect(impl, &impl::requestComplete);
+	return res == -EACCES ? -EBUSY : res;
 }
 
 static int do_remove_source(struct spa_loop *loop,
@@ -724,11 +912,14 @@ static int spa_libcamera_stream_off(struct impl *impl)
 		return 0;
 	}
 
-	spa_log_info(impl->log, "stopping camera %s", impl->props.device);
+	impl->active = false;
+	spa_log_info(impl->log, "stopping camera %s", impl->device_id.c_str());
 	impl->pendingRequests.clear();
 
-	if ((res = impl->camera->stop()) < 0)
-		return res == -EACCES ? -EBUSY : res;
+	if ((res = impl->camera->stop()) < 0) {
+		spa_log_warn(impl->log, "error stopping camera %s: %s",
+				impl->device_id.c_str(), spa_strerror(res));
+	}
 
 	impl->camera->requestCompleted.disconnect(impl, &impl::requestComplete);
 
@@ -739,7 +930,6 @@ static int spa_libcamera_stream_off(struct impl *impl)
 	}
 
 	spa_list_init(&port->queue);
-	impl->active = false;
 
 	return 0;
 }
diff --git a/spa/plugins/support/cpu-x86.c b/spa/plugins/support/cpu-x86.c
index ded9bfaa79d2097e5e73af3fb9217aeabfb02c5a..722f7c906df47afa445a3e760c2a8f42eef142ab 100644
--- a/spa/plugins/support/cpu-x86.c
+++ b/spa/plugins/support/cpu-x86.c
@@ -56,6 +56,7 @@ x86_init(struct impl *impl)
 		} else if (family == 0x06)
 			model += extended_model;
 	}
+	(void)model;
 
 	flags = 0;
 	if (ecx & bit_SSE3)
diff --git a/spa/plugins/support/cpu.c b/spa/plugins/support/cpu.c
index 67440af16f7d0868f7732bb950b22a25b3c6fa1e..3e6bb85dd7d76a5272b18c190a4de2691bac3efa 100644
--- a/spa/plugins/support/cpu.c
+++ b/spa/plugins/support/cpu.c
@@ -171,19 +171,18 @@ impl_cpu_get_vm_type(void *object)
 		/* https://wiki.freebsd.org/bhyve */
 		{ "BHYVE",		SPA_CPU_VM_BHYVE },
         };
-	uint32_t i, j;
 
-	for (i = 0; i < SPA_N_ELEMENTS(dmi_vendors); i++) {
+	SPA_FOR_EACH_ELEMENT_VAR(dmi_vendors, dv) {
 		char buffer[256], *s;
 
-		if ((s = read_file(dmi_vendors[i], buffer, sizeof(buffer))) == NULL)
+		if ((s = read_file(*dv, buffer, sizeof(buffer))) == NULL)
 			continue;
 
-		for (j = 0; j < SPA_N_ELEMENTS(dmi_vendor_table); j++) {
-			if (spa_strstartswith(s, dmi_vendor_table[j].vendor)) {
+		SPA_FOR_EACH_ELEMENT_VAR(dmi_vendor_table, t) {
+			if (spa_strstartswith(s, t->vendor)) {
 				spa_log_debug(impl->log, "Virtualization %s found in DMI (%s)",
-						s, dmi_vendors[i]);
-				impl->vm_type = dmi_vendor_table[j].id;
+						s, *dv);
+				impl->vm_type = t->id;
 				goto done;
                         }
 		}
diff --git a/spa/plugins/support/logger.c b/spa/plugins/support/logger.c
index 3b854c65eff17f1da02401bf2ae78d8afedf3c2a..615a49bee364d25752759cf1ab0590c0f23b43d3 100644
--- a/spa/plugins/support/logger.c
+++ b/spa/plugins/support/logger.c
@@ -357,7 +357,7 @@ impl_init(const struct spa_handle_factory *factory,
 		if ((str = spa_dict_lookup(info, SPA_KEY_LOG_LEVEL)) != NULL)
 			this->log.level = atoi(str);
 		if ((str = spa_dict_lookup(info, SPA_KEY_LOG_FILE)) != NULL) {
-			this->file = fopen(str, "w");
+			this->file = fopen(str, "we");
 			if (this->file == NULL)
 				fprintf(stderr, "Warning: failed to open file %s: (%m)", str);
 			else
diff --git a/spa/plugins/support/loop.c b/spa/plugins/support/loop.c
index 758ce59db192332bf7d7a475f3f5fd38e265b166..dd308cfda99a3445c0eeba1c9f358fbf1595a34e 100644
--- a/spa/plugins/support/loop.c
+++ b/spa/plugins/support/loop.c
@@ -186,8 +186,8 @@ static void flush_items(struct impl *impl)
 
 		if (block) {
 			if ((res = spa_system_eventfd_write(impl->system, impl->ack_fd, 1)) < 0)
-				spa_log_warn(impl->log, "%p: failed to write event fd: %s",
-						impl, spa_strerror(res));
+				spa_log_warn(impl->log, "%p: failed to write event fd:%d: %s",
+						impl, impl->ack_fd, spa_strerror(res));
 		}
 	}
 	impl->flushing = false;
@@ -283,8 +283,8 @@ loop_invoke(void *object,
 		spa_loop_control_hook_before(&impl->hooks_list);
 
 		if ((res = spa_system_eventfd_read(impl->system, impl->ack_fd, &count)) < 0)
-			spa_log_warn(impl->log, "%p: failed to read event fd: %s",
-					impl, spa_strerror(res));
+			spa_log_warn(impl->log, "%p: failed to read event fd:%d: %s",
+					impl, impl->ack_fd, spa_strerror(res));
 
 		spa_loop_control_hook_after(&impl->hooks_list);
 
@@ -526,12 +526,12 @@ static int loop_enable_idle(void *object, struct spa_source *source, bool enable
 
 	if (enabled && !s->enabled) {
 		if ((res = spa_system_eventfd_write(s->impl->system, source->fd, 1)) < 0)
-			spa_log_warn(s->impl->log, "%p: failed to write idle fd %d: %s",
+			spa_log_warn(s->impl->log, "%p: failed to write idle fd:%d: %s",
 					source, source->fd, spa_strerror(res));
 	} else if (!enabled && s->enabled) {
 		uint64_t count;
 		if ((res = spa_system_eventfd_read(s->impl->system, source->fd, &count)) < 0)
-			spa_log_warn(s->impl->log, "%p: failed to read idle fd %d: %s",
+			spa_log_warn(s->impl->log, "%p: failed to read idle fd:%d: %s",
 					source, source->fd, spa_strerror(res));
 	}
 	s->enabled = enabled;
@@ -586,7 +586,7 @@ static void source_event_func(struct spa_source *source)
 	int res;
 
 	if ((res = spa_system_eventfd_read(s->impl->system, source->fd, &count)) < 0)
-		spa_log_warn(s->impl->log, "%p: failed to read event fd %d: %s",
+		spa_log_warn(s->impl->log, "%p: failed to read event fd:%d: %s",
 				source, source->fd, spa_strerror(res));
 
 	s->func.event(source->data, count);
@@ -639,7 +639,7 @@ static int loop_signal_event(void *object, struct spa_source *source)
 	spa_assert(source->func == source_event_func);
 
 	if (SPA_UNLIKELY((res = spa_system_eventfd_write(s->impl->system, source->fd, 1)) < 0))
-		spa_log_warn(s->impl->log, "%p: failed to write event fd %d: %s",
+		spa_log_warn(s->impl->log, "%p: failed to write event fd:%d: %s",
 				source, source->fd, spa_strerror(res));
 	return res;
 }
@@ -652,7 +652,7 @@ static void source_timer_func(struct spa_source *source)
 
 	if (SPA_UNLIKELY((res = spa_system_timerfd_read(s->impl->system,
 				source->fd, &expirations)) < 0))
-		spa_log_warn(s->impl->log, "%p: failed to read timer fd %d: %s",
+		spa_log_warn(s->impl->log, "%p: failed to read timer fd:%d: %s",
 				source, source->fd, spa_strerror(res));
 
 	s->func.timer(source->data, expirations);
@@ -732,7 +732,7 @@ static void source_signal_func(struct spa_source *source)
 	int res, signal_number = 0;
 
 	if ((res = spa_system_signalfd_read(s->impl->system, source->fd, &signal_number)) < 0)
-		spa_log_warn(s->impl->log, "%p: failed to read signal fd %d: %s",
+		spa_log_warn(s->impl->log, "%p: failed to read signal fd:%d: %s",
 				source, source->fd, spa_strerror(res));
 
 	s->func.signal(source->data, signal_number);
@@ -865,11 +865,9 @@ static int impl_clear(struct spa_handle *handle)
 
 	impl = (struct impl *) handle;
 
-	if (impl->enter_count != 0)
-		spa_log_warn(impl->log, "%p: loop is entered %d times",
-				impl, impl->enter_count);
-
-	spa_assert(!impl->polling);
+	if (impl->enter_count != 0 || impl->polling)
+		spa_log_warn(impl->log, "%p: loop is entered %d times polling:%d",
+				impl, impl->enter_count, impl->polling);
 
 	spa_list_consume(source, &impl->source_list, link)
 		loop_destroy_source(impl, &source->source);
diff --git a/spa/plugins/support/null-audio-sink.c b/spa/plugins/support/null-audio-sink.c
index 4c000f3eb1d0eea5babd92ce70b1cf2699e0e3c1..e42c3c1c8ecf7891c746f4b7311f0e9344ee2098 100644
--- a/spa/plugins/support/null-audio-sink.c
+++ b/spa/plugins/support/null-audio-sink.c
@@ -610,6 +610,11 @@ port_set_format(struct impl *this,
 		if (spa_format_audio_raw_parse(format, &info.info.raw) < 0)
 			return -EINVAL;
 
+		if (info.info.raw.rate == 0 ||
+		    info.info.raw.channels == 0 ||
+		    info.info.raw.channels > SPA_AUDIO_MAX_CHANNELS)
+			return -EINVAL;
+
 		if (info.info.raw.format == SPA_AUDIO_FORMAT_F32) {
 			port->bpf = 4 * info.info.raw.channels;
 			port->blocks = 1;
diff --git a/spa/plugins/v4l2/v4l2-source.c b/spa/plugins/v4l2/v4l2-source.c
index 802ead7ee2dbfca254aaf93fe301f916e55e18c8..3d967f2552a1a654e291d862b8b83438aec624f1 100644
--- a/spa/plugins/v4l2/v4l2-source.c
+++ b/spa/plugins/v4l2/v4l2-source.c
@@ -145,7 +145,8 @@ struct impl {
 #define NODE_PropInfo	0
 #define NODE_Props	1
 #define NODE_EnumFormat	2
-#define N_NODE_PARAMS	3
+#define NODE_Format	3
+#define N_NODE_PARAMS	4
 	struct spa_param_info params[N_NODE_PARAMS];
 	struct props props;
 
@@ -167,6 +168,56 @@ struct impl {
 
 #include "v4l2-utils.c"
 
+static int port_get_format(struct port *port,
+			   uint32_t index,
+			   const struct spa_pod *filter,
+			   struct spa_pod **param,
+			   struct spa_pod_builder *builder)
+{
+	struct spa_pod_frame f;
+
+	if (!port->have_format)
+		return -EIO;
+	if (index > 0)
+		return 0;
+
+	spa_pod_builder_push_object(builder, &f, SPA_TYPE_OBJECT_Format, SPA_PARAM_Format);
+	spa_pod_builder_add(builder,
+		SPA_FORMAT_mediaType,    SPA_POD_Id(port->current_format.media_type),
+		SPA_FORMAT_mediaSubtype, SPA_POD_Id(port->current_format.media_subtype),
+		0);
+
+	switch (port->current_format.media_subtype) {
+	case SPA_MEDIA_SUBTYPE_raw:
+		spa_pod_builder_add(builder,
+			SPA_FORMAT_VIDEO_format,    SPA_POD_Id(port->current_format.info.raw.format),
+			SPA_FORMAT_VIDEO_size,      SPA_POD_Rectangle(&port->current_format.info.raw.size),
+			SPA_FORMAT_VIDEO_framerate, SPA_POD_Fraction(&port->current_format.info.raw.framerate),
+			0);
+		break;
+	case SPA_MEDIA_SUBTYPE_mjpg:
+	case SPA_MEDIA_SUBTYPE_jpeg:
+		spa_pod_builder_add(builder,
+			SPA_FORMAT_VIDEO_size,      SPA_POD_Rectangle(&port->current_format.info.mjpg.size),
+			SPA_FORMAT_VIDEO_framerate, SPA_POD_Fraction(&port->current_format.info.mjpg.framerate),
+			0);
+		break;
+	case SPA_MEDIA_SUBTYPE_h264:
+		spa_pod_builder_add(builder,
+			SPA_FORMAT_VIDEO_size,      SPA_POD_Rectangle(&port->current_format.info.h264.size),
+			SPA_FORMAT_VIDEO_framerate, SPA_POD_Fraction(&port->current_format.info.h264.framerate),
+			0);
+		break;
+	default:
+		return -EIO;
+	}
+
+	*param = spa_pod_builder_pop(builder, &f);
+
+	return 1;
+}
+
+
 static int impl_node_enum_params(void *object, int seq,
 				 uint32_t id, uint32_t start, uint32_t num,
 				 const struct spa_pod *filter)
@@ -177,6 +228,7 @@ static int impl_node_enum_params(void *object, int seq,
 	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);
@@ -216,9 +268,9 @@ static int impl_node_enum_params(void *object, int seq,
 				SPA_PROP_INFO_type, SPA_POD_Int(p->device_fd));
 			break;
 		default:
-			return 0;
+			return spa_v4l2_enum_controls(this, seq, result.index - 3, num, filter);
 		}
-		return spa_v4l2_enum_controls(this, seq, start, num, filter);
+		break;
 	}
 	case SPA_PARAM_Props:
 	{
@@ -239,6 +291,11 @@ static int impl_node_enum_params(void *object, int seq,
 	}
 	case SPA_PARAM_EnumFormat:
 		return spa_v4l2_enum_format(this, seq, start, num, filter);
+	case SPA_PARAM_Format:
+		if((res = port_get_format(GET_OUT_PORT(this, 0),
+						result.index, filter, &param, &b)) <= 0)
+			return res;
+		break;
 	default:
 		return -ENOENT;
 	}
@@ -266,14 +323,26 @@ static int impl_node_set_param(void *object,
 	case SPA_PARAM_Props:
 	{
 		struct props *p = &this->props;
+		struct spa_pod_object *obj = (struct spa_pod_object *) param;
+		struct spa_pod_prop *prop;
 
 		if (param == NULL) {
 			reset_props(p);
 			return 0;
 		}
-		spa_pod_parse_object(param,
-			SPA_TYPE_OBJECT_Props, NULL,
-			SPA_PROP_device, SPA_POD_OPT_Stringn(p->device, sizeof(p->device)));
+		SPA_POD_OBJECT_FOREACH(obj, prop) {
+			switch (prop->key) {
+			case SPA_PROP_device:
+				strncpy(p->device,
+						(char *)SPA_POD_CONTENTS(struct spa_pod_string, &prop->value),
+						sizeof(p->device)-1);
+				break;
+			default:
+				spa_v4l2_set_control(this, prop->key, prop);
+				break;
+			}
+		}
+
 		break;
 	}
 	default:
@@ -440,58 +509,6 @@ static int impl_node_remove_port(void *object,
 	return -ENOTSUP;
 }
 
-static int port_get_format(void *object,
-			   enum spa_direction direction, uint32_t port_id,
-			   uint32_t index,
-			   const struct spa_pod *filter,
-			   struct spa_pod **param,
-			   struct spa_pod_builder *builder)
-{
-	struct impl *this = object;
-	struct port *port = GET_PORT(this, direction, port_id);
-	struct spa_pod_frame f;
-
-	if (!port->have_format)
-		return -EIO;
-	if (index > 0)
-		return 0;
-
-	spa_pod_builder_push_object(builder, &f, SPA_TYPE_OBJECT_Format, SPA_PARAM_Format);
-	spa_pod_builder_add(builder,
-		SPA_FORMAT_mediaType,    SPA_POD_Id(port->current_format.media_type),
-		SPA_FORMAT_mediaSubtype, SPA_POD_Id(port->current_format.media_subtype),
-		0);
-
-	switch (port->current_format.media_subtype) {
-	case SPA_MEDIA_SUBTYPE_raw:
-		spa_pod_builder_add(builder,
-			SPA_FORMAT_VIDEO_format,    SPA_POD_Id(port->current_format.info.raw.format),
-			SPA_FORMAT_VIDEO_size,      SPA_POD_Rectangle(&port->current_format.info.raw.size),
-			SPA_FORMAT_VIDEO_framerate, SPA_POD_Fraction(&port->current_format.info.raw.framerate),
-			0);
-		break;
-	case SPA_MEDIA_SUBTYPE_mjpg:
-	case SPA_MEDIA_SUBTYPE_jpeg:
-		spa_pod_builder_add(builder,
-			SPA_FORMAT_VIDEO_size,      SPA_POD_Rectangle(&port->current_format.info.mjpg.size),
-			SPA_FORMAT_VIDEO_framerate, SPA_POD_Fraction(&port->current_format.info.mjpg.framerate),
-			0);
-		break;
-	case SPA_MEDIA_SUBTYPE_h264:
-		spa_pod_builder_add(builder,
-			SPA_FORMAT_VIDEO_size,      SPA_POD_Rectangle(&port->current_format.info.h264.size),
-			SPA_FORMAT_VIDEO_framerate, SPA_POD_Fraction(&port->current_format.info.h264.framerate),
-			0);
-		break;
-	default:
-		return -EIO;
-	}
-
-	*param = spa_pod_builder_pop(builder, &f);
-
-	return 1;
-}
-
 static int impl_node_port_enum_params(void *object, int seq,
 				      enum spa_direction direction,
 				      uint32_t port_id,
@@ -529,8 +546,7 @@ static int impl_node_port_enum_params(void *object, int seq,
 		return spa_v4l2_enum_format(this, seq, start, num, filter);
 
 	case SPA_PARAM_Format:
-		if((res = port_get_format(this, direction, port_id,
-						result.index, filter, &param, &b)) <= 0)
+		if((res = port_get_format(port, result.index, filter, &param, &b)) <= 0)
 			return res;
 		break;
 	case SPA_PARAM_Buffers:
@@ -541,7 +557,7 @@ static int 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(MAX_BUFFERS, 2, MAX_BUFFERS),
+			SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(4, 1, MAX_BUFFERS),
 			SPA_PARAM_BUFFERS_blocks,  SPA_POD_Int(1),
 			SPA_PARAM_BUFFERS_size,    SPA_POD_Int(port->fmt.fmt.pix.sizeimage),
 			SPA_PARAM_BUFFERS_stride,  SPA_POD_Int(port->fmt.fmt.pix.bytesperline));
@@ -668,15 +684,19 @@ static int port_set_format(struct impl *this, struct port *port,
 	}
 
     done:
+	this->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS;
 	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);
+		this->params[NODE_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, 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);
+		this->params[NODE_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, 0);
 	}
 	emit_port_info(this, port, false);
+	emit_node_info(this, false);
 
 	return 0;
 }
@@ -802,45 +822,9 @@ static int impl_node_port_reuse_buffer(void *object,
 	return res;
 }
 
-static uint32_t prop_to_control_id(uint32_t prop)
-{
-	switch (prop) {
-	case SPA_PROP_brightness:
-		return V4L2_CID_BRIGHTNESS;
-	case SPA_PROP_contrast:
-		return V4L2_CID_CONTRAST;
-	case SPA_PROP_saturation:
-		return V4L2_CID_SATURATION;
-	case SPA_PROP_hue:
-		return V4L2_CID_HUE;
-	case SPA_PROP_gamma:
-		return V4L2_CID_GAMMA;
-	case SPA_PROP_exposure:
-		return V4L2_CID_EXPOSURE;
-	case SPA_PROP_gain:
-		return V4L2_CID_GAIN;
-	case SPA_PROP_sharpness:
-		return V4L2_CID_SHARPNESS;
-	default:
-		return 0;
-	}
-}
-
-static void set_control(struct impl *this, struct port *port, uint32_t control_id, float value)
-{
-	struct v4l2_control c;
-
-	spa_zero(c);
-	c.id = control_id;
-	c.value = value;
-	if (ioctl(port->dev.fd, VIDIOC_S_CTRL, &c) < 0)
-		spa_log_error(this->log, "VIDIOC_S_CTRL %m");
-}
-
 static int process_control(struct impl *this, struct spa_pod_sequence *control)
 {
 	struct spa_pod_control *c;
-	struct port *port;
 
 	SPA_POD_SEQUENCE_FOREACH(control, c) {
 		switch (c->type) {
@@ -850,14 +834,7 @@ static int process_control(struct impl *this, struct spa_pod_sequence *control)
 			struct spa_pod_object *obj = (struct spa_pod_object *) &c->value;
 
 			SPA_POD_OBJECT_FOREACH(obj, prop) {
-				uint32_t control_id;
-
-				if ((control_id = prop_to_control_id(prop->key)) == 0)
-					continue;
-
-				port = GET_OUT_PORT(this, 0);
-				set_control(this, port, control_id,
-						SPA_POD_VALUE(struct spa_pod_float, &prop->value));
+				spa_v4l2_set_control(this, prop->key, prop);
 			}
 			break;
 		}
@@ -1005,8 +982,9 @@ impl_init(const struct spa_handle_factory *factory,
 	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_EnumFormat] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ);
 	this->params[NODE_Props] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE);
+	this->params[NODE_EnumFormat] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ);
+	this->params[NODE_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, 0);
 	this->info.params = this->params;
 	this->info.n_params = N_NODE_PARAMS;
 	reset_props(&this->props);
diff --git a/spa/plugins/v4l2/v4l2-udev.c b/spa/plugins/v4l2/v4l2-udev.c
index df1b1f8906b85a9a5a8b2ef976a7ff9e4016058a..e7f926585c131215a67d604f79163f94e010ea58 100644
--- a/spa/plugins/v4l2/v4l2-udev.c
+++ b/spa/plugins/v4l2/v4l2-udev.c
@@ -266,7 +266,7 @@ static int emit_object_info(struct impl *this, struct device *device)
 	if (str && *str) {
 		items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_BUS_PATH, str);
 	}
-	if ((str = udev_device_get_syspath(dev)) && *str) {
+	if ((str = udev_device_get_devpath(dev)) && *str) {
 		items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_SYSFS_PATH, str);
 	}
 	if ((str = udev_device_get_property_value(dev, "ID_ID")) && *str) {
diff --git a/spa/plugins/v4l2/v4l2-utils.c b/spa/plugins/v4l2/v4l2-utils.c
index 14e967f731b63fd9de6341da0bceb3c4be8ed0e1..8ffda138e216ea8ef29d31dc16a55e468eca3b36 100644
--- a/spa/plugins/v4l2/v4l2-utils.c
+++ b/spa/plugins/v4l2/v4l2-utils.c
@@ -30,7 +30,7 @@
 #include <sys/mman.h>
 #include <poll.h>
 
-static void v4l2_on_fd_events(struct spa_source *source);
+#include <spa/utils/result.h>
 
 static int xioctl(int fd, int request, void *arg)
 {
@@ -43,7 +43,6 @@ static int xioctl(int fd, int request, void *arg)
 	return err;
 }
 
-
 int spa_v4l2_open(struct spa_v4l2_device *dev, const char *path)
 {
 	struct stat st;
@@ -57,7 +56,7 @@ int spa_v4l2_open(struct spa_v4l2_device *dev, const char *path)
 		return -EIO;
 	}
 
-	spa_log_info(dev->log, "Playback device is '%s'", path);
+	spa_log_info(dev->log, "device is '%s'", path);
 
 	dev->fd = open(path, O_RDWR | O_NONBLOCK, 0);
 	if (dev->fd == -1) {
@@ -85,6 +84,7 @@ int spa_v4l2_open(struct spa_v4l2_device *dev, const char *path)
 		spa_log_error(dev->log, "'%s' QUERYCAP: %m", path);
 		goto error_close;
 	}
+	snprintf(dev->path, sizeof(dev->path), "%s", path);
 	return 0;
 
 error_close:
@@ -110,7 +110,7 @@ int spa_v4l2_close(struct spa_v4l2_device *dev)
 	if (dev->active || dev->have_format)
 		return 0;
 
-	spa_log_info(dev->log, "close");
+	spa_log_info(dev->log, "close '%s'", dev->path);
 
 	if (close(dev->fd))
 		spa_log_warn(dev->log, "close: %m");
@@ -351,11 +351,9 @@ static const struct format_info format_info[] = {
 
 static const struct format_info *fourcc_to_format_info(uint32_t fourcc)
 {
-	size_t i;
-
-	for (i = 0; i < SPA_N_ELEMENTS(format_info); i++) {
-		if (format_info[i].fourcc == fourcc)
-			return &format_info[i];
+	SPA_FOR_EACH_ELEMENT_VAR(format_info, i) {
+		if (i->fourcc == fourcc)
+			return i;
 	}
 	return NULL;
 }
@@ -363,11 +361,9 @@ static const struct format_info *fourcc_to_format_info(uint32_t fourcc)
 #if 0
 static const struct format_info *video_format_to_format_info(uint32_t format)
 {
-	int i;
-
-	for (i = 0; i < SPA_N_ELEMENTS(format_info); i++) {
-		if (format_info[i].format == format)
-			return &format_info[i];
+	SPA_FOR_EACH_ELEMENT_VAR(format_info, i) {
+		if (i->format == format)
+			return i;
 	}
 	return NULL;
 }
@@ -381,10 +377,11 @@ static const struct format_info *find_format_info_by_media_type(uint32_t type,
 	size_t i;
 
 	for (i = startidx; i < SPA_N_ELEMENTS(format_info); i++) {
-		if ((format_info[i].media_type == type) &&
-		    (format_info[i].media_subtype == subtype) &&
-		    (format == 0 || format_info[i].format == format))
-			return &format_info[i];
+		const struct format_info *fi = &format_info[i];
+		if ((fi->media_type == type) &&
+		    (fi->media_subtype == subtype) &&
+		    (format == 0 || fi->format == format))
+			return fi;
 	}
 	return NULL;
 }
@@ -895,8 +892,9 @@ static int spa_v4l2_set_format(struct impl *this, struct spa_video_info *format,
 	bool match;
 
 	spa_zero(fmt);
-	spa_zero(streamparm);
 	fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+
+	spa_zero(streamparm);
 	streamparm.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
 
 	switch (format->media_subtype) {
@@ -929,7 +927,6 @@ static int spa_v4l2_set_format(struct impl *this, struct spa_video_info *format,
 		return -EINVAL;
 	}
 
-
 	fmt.fmt.pix.pixelformat = info->fourcc;
 	fmt.fmt.pix.field = V4L2_FIELD_ANY;
 	fmt.fmt.pix.width = size->width;
@@ -976,7 +973,7 @@ static int spa_v4l2_set_format(struct impl *this, struct spa_video_info *format,
 		return match ? 0 : 1;
 
 	spa_log_info(this->log, "'%s' got %.4s %dx%d %d/%d",
-			this->props.device, (char *)&fmt.fmt.pix.pixelformat,
+			dev->path, (char *)&fmt.fmt.pix.pixelformat,
 			fmt.fmt.pix.width, fmt.fmt.pix.height,
 			streamparm.parm.capture.timeperframe.denominator,
 			streamparm.parm.capture.timeperframe.numerator);
@@ -1046,28 +1043,38 @@ static int query_ext_ctrl_ioctl(struct port *port, struct v4l2_query_ext_ctrl *q
 	return res;
 }
 
+static struct {
+	uint32_t v4l2_id;
+	uint32_t spa_id;
+} control_map[] = {
+	{ V4L2_CID_BRIGHTNESS, SPA_PROP_brightness },
+	{ V4L2_CID_CONTRAST, SPA_PROP_contrast },
+	{ V4L2_CID_SATURATION, SPA_PROP_saturation },
+	{ V4L2_CID_HUE, SPA_PROP_hue },
+	{ V4L2_CID_GAMMA, SPA_PROP_gamma },
+	{ V4L2_CID_EXPOSURE, SPA_PROP_exposure },
+	{ V4L2_CID_GAIN, SPA_PROP_gain },
+	{ V4L2_CID_SHARPNESS, SPA_PROP_sharpness },
+};
+
 static uint32_t control_to_prop_id(struct impl *impl, uint32_t control_id)
 {
-	switch (control_id) {
-	case V4L2_CID_BRIGHTNESS:
-		return SPA_PROP_brightness;
-	case V4L2_CID_CONTRAST:
-		return SPA_PROP_contrast;
-	case V4L2_CID_SATURATION:
-		return SPA_PROP_saturation;
-	case V4L2_CID_HUE:
-		return SPA_PROP_hue;
-	case V4L2_CID_GAMMA:
-		return SPA_PROP_gamma;
-	case V4L2_CID_EXPOSURE:
-		return SPA_PROP_exposure;
-	case V4L2_CID_GAIN:
-		return SPA_PROP_gain;
-	case V4L2_CID_SHARPNESS:
-		return SPA_PROP_sharpness;
-	default:
-		return SPA_PROP_START_CUSTOM + control_id;
+	SPA_FOR_EACH_ELEMENT_VAR(control_map, c) {
+		if (c->v4l2_id == control_id)
+			return c->spa_id;
+	}
+	return SPA_PROP_START_CUSTOM + control_id;
+}
+
+static uint32_t prop_id_to_control(struct impl *impl, uint32_t prop_id)
+{
+	SPA_FOR_EACH_ELEMENT_VAR(control_map, c) {
+		if (c->spa_id == prop_id)
+			return c->v4l2_id;
 	}
+	if (prop_id >= SPA_PROP_START_CUSTOM)
+		return prop_id - SPA_PROP_START_CUSTOM;
+	return SPA_ID_INVALID;
 }
 
 static int
@@ -1231,6 +1238,56 @@ spa_v4l2_enum_controls(struct impl *this, int seq,
 	return res;
 }
 
+static int
+spa_v4l2_set_control(struct impl *this, uint32_t id,
+		       const struct spa_pod_prop *prop)
+{
+	struct port *port = &this->out_ports[0];
+	struct spa_v4l2_device *dev = &port->dev;
+	struct v4l2_control control;
+	int res;
+
+	spa_zero(control);
+	control.id = prop_id_to_control(this, prop->key);
+	if (control.id == SPA_ID_INVALID)
+		return -ENOENT;
+
+	if ((res = spa_v4l2_open(dev, this->props.device)) < 0)
+		return res;
+
+	switch (SPA_POD_TYPE(&prop->value)) {
+	case SPA_TYPE_Bool:
+	{
+		bool val;
+		if ((res = spa_pod_get_bool(&prop->value, &val)) < 0)
+			goto done;
+		control.value = val;
+		break;
+	}
+	case SPA_TYPE_Int:
+	{
+		int32_t val;
+		if ((res = spa_pod_get_int(&prop->value, &val)) < 0)
+			goto done;
+		control.value = val;
+		break;
+	}
+	default:
+		res = -EINVAL;
+		goto done;
+	}
+	if (xioctl(dev->fd, VIDIOC_S_CTRL, &control) < 0) {
+		res = -errno;
+		goto done;
+	}
+
+	res = 0;
+
+done:
+	spa_v4l2_close(dev);
+	return res;
+}
+
 static int mmap_read(struct impl *this)
 {
 	struct port *port = &this->out_ports[0];
@@ -1310,7 +1367,13 @@ static void v4l2_on_fd_events(struct spa_source *source)
 		return;
 
 	io = port->io;
-	if (io != NULL && io->status != SPA_STATUS_HAVE_DATA) {
+	if (io == NULL) {
+		b = spa_list_first(&port->queue, struct buffer, link);
+		spa_list_remove(&b->link);
+		SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUTSTANDING);
+		spa_v4l2_buffer_recycle(this, b->id);
+	}
+	else if (io->status != SPA_STATUS_HAVE_DATA) {
 		if (io->buffer_id < port->n_buffers)
 			spa_v4l2_buffer_recycle(this, io->buffer_id);
 
@@ -1445,11 +1508,10 @@ mmap_init(struct impl *this,
 	}
 
 	spa_log_debug(this->log, "got %d buffers", reqbuf.count);
-	n_buffers = reqbuf.count;
 
-	if (n_buffers < 2) {
-		spa_log_error(this->log, "'%s' can't allocate enough buffers (%d)",
-				this->props.device, n_buffers);
+	if (reqbuf.count < n_buffers) {
+		spa_log_error(this->log, "'%s' can't allocate enough buffers (%d < %d)",
+				this->props.device, reqbuf.count, n_buffers);
 		return -ENOMEM;
 	}
 
@@ -1549,7 +1611,7 @@ fallback:
 		}
 		spa_v4l2_buffer_recycle(this, i);
 	}
-	spa_log_info(this->log, "have %u buffers using %s", n_buffers,
+	spa_log_info(this->log, "%s: have %u buffers using %s", dev->path, n_buffers,
 			use_expbuf ? "EXPBUF" : "MMAP");
 
 	port->n_buffers = n_buffers;
diff --git a/spa/plugins/v4l2/v4l2.h b/spa/plugins/v4l2/v4l2.h
index 892728127f5e6a60d89b23b16c8c6945964bc41b..e2293c7e28c8a3c3efba422081dca23540ffcc8f 100644
--- a/spa/plugins/v4l2/v4l2.h
+++ b/spa/plugins/v4l2/v4l2.h
@@ -41,6 +41,7 @@ struct spa_v4l2_device {
 	struct v4l2_capability cap;
 	unsigned int active:1;
 	unsigned int have_format:1;
+	char path[64];
 };
 
 int spa_v4l2_open(struct spa_v4l2_device *dev, const char *path);
diff --git a/spa/plugins/videoconvert/videoadapter.c b/spa/plugins/videoconvert/videoadapter.c
index 694e6af65abe48d9485b5be2bef7a2fffc537d1b..0a2a1422b2b5c42e02f7b1c120c530d9b344803b 100644
--- a/spa/plugins/videoconvert/videoadapter.c
+++ b/spa/plugins/videoconvert/videoadapter.c
@@ -562,24 +562,6 @@ static int reconfigure_mode(struct impl *this, bool passthrough,
 	return 0;
 }
 
-static int format_video_raw_parse_opt(const struct spa_pod *format, struct spa_video_info_raw *info)
-{
-	uint32_t media_type, media_subtype;
-	int res;
-	if ((res = spa_format_parse(format, &media_type, &media_subtype)) < 0)
-		return res;
-	if (media_type != SPA_MEDIA_TYPE_video ||
-	    media_subtype != SPA_MEDIA_SUBTYPE_raw)
-		return -ENOTSUP;
-
-	spa_zero(*info);
-	res = spa_pod_parse_object(format,
-			SPA_TYPE_OBJECT_Format, NULL,
-			SPA_FORMAT_VIDEO_format, SPA_POD_OPT_Id(&info->format),
-			SPA_FORMAT_VIDEO_size, SPA_POD_OPT_Int(&info->size));
-	return res;
-}
-
 static int impl_node_set_param(void *object, uint32_t id, uint32_t flags,
 			       const struct spa_pod *param)
 {
@@ -599,8 +581,9 @@ static int impl_node_set_param(void *object, uint32_t id, uint32_t flags,
 		if ((res = spa_format_parse(param, &info.media_type, &info.media_subtype)) < 0)
 			return res;
 		if (info.media_type != SPA_MEDIA_TYPE_video ||
-			info.media_subtype != SPA_MEDIA_SUBTYPE_raw)
-				return -EINVAL;
+		    info.media_subtype != SPA_MEDIA_SUBTYPE_raw)
+			return -EINVAL;
+
 		if (spa_format_video_raw_parse(param, &info.info.raw) < 0)
 			return -EINVAL;
 
@@ -627,7 +610,15 @@ static int impl_node_set_param(void *object, uint32_t id, uint32_t flags,
 
 		if (format) {
 			struct spa_video_info info;
-			if (format_video_raw_parse_opt(format, &info.info.raw) >= 0)
+
+			spa_zero(info);
+			if ((res = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0)
+				return res;
+			if (info.media_type != SPA_MEDIA_TYPE_video ||
+			    info.media_subtype != SPA_MEDIA_SUBTYPE_raw)
+				return -ENOTSUP;
+
+			if (spa_format_video_raw_parse(format, &info.info.raw) >= 0)
 				this->default_format = info;
 		}
 
@@ -823,12 +814,12 @@ static int impl_node_send_command(void *object, const struct spa_command *comman
 	case SPA_NODE_COMMAND_Suspend:
 		configure_format(this, 0, NULL);
 		SPA_FALLTHROUGH
-	case SPA_NODE_COMMAND_Flush:
-		this->io_buffers.status = SPA_STATUS_OK;
-		SPA_FALLTHROUGH
 	case SPA_NODE_COMMAND_Pause:
 		this->started = false;
 		break;
+	case SPA_NODE_COMMAND_Flush:
+		this->io_buffers.status = SPA_STATUS_OK;
+		break;
 	default:
 		break;
 	}
diff --git a/spa/plugins/videotestsrc/videotestsrc.c b/spa/plugins/videotestsrc/videotestsrc.c
index 97742975963b66e7d1a24e074d9e06cf17396b76..f2f42c0609689ccf9ae931e48d1b8b88aff31a27 100644
--- a/spa/plugins/videotestsrc/videotestsrc.c
+++ b/spa/plugins/videotestsrc/videotestsrc.c
@@ -643,6 +643,12 @@ static int port_set_format(struct impl *this, struct port *port,
 		else
 			return -EINVAL;
 
+		if (info.info.raw.size.width == 0 ||
+		    info.info.raw.size.height == 0 ||
+		    info.info.raw.framerate.num == 0 ||
+		    info.info.raw.framerate.denom == 0)
+			return -EINVAL;
+
 		port->current_format = info;
 		port->have_format = true;
 	}
diff --git a/spa/plugins/volume/volume.c b/spa/plugins/volume/volume.c
index fe6fcc4f4f9e09787baaacd4ab4d9df616cec8ed..08102a428587f8acd38c36ce96bcd2de358ce853 100644
--- a/spa/plugins/volume/volume.c
+++ b/spa/plugins/volume/volume.c
@@ -321,10 +321,9 @@ static int port_enum_formats(void *object,
 			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(3,
+			SPA_FORMAT_AUDIO_format,   SPA_POD_CHOICE_ENUM_Id(2,
 							SPA_AUDIO_FORMAT_S16,
-							SPA_AUDIO_FORMAT_S16,
-							SPA_AUDIO_FORMAT_S32),
+							SPA_AUDIO_FORMAT_S16),
 			SPA_FORMAT_AUDIO_rate,     SPA_POD_CHOICE_RANGE_Int(
 							DEFAULT_RATE, 1, INT32_MAX),
 			SPA_FORMAT_AUDIO_channels, SPA_POD_CHOICE_RANGE_Int(
@@ -472,6 +471,11 @@ static int port_set_format(void *object,
 		if (spa_format_audio_raw_parse(format, &info.info.raw) < 0)
 			return -EINVAL;
 
+		if (info.info.raw.format != SPA_AUDIO_FORMAT_S16 ||
+		    info.info.raw.channels == 0 ||
+		    info.info.raw.channels > SPA_AUDIO_MAX_CHANNELS)
+			return -EINVAL;
+
 		this->bpf = 2 * info.info.raw.channels;
 		this->current_format = info;
 		port->have_format = true;
diff --git a/spa/tests/benchmark-dict.c b/spa/tests/benchmark-dict.c
index 9d189b73f5e7a442fdf5a4cbc89113a650b413fc..c47845e7bf0e74b69fee62d884b721cbe2cb0cfb 100644
--- a/spa/tests/benchmark-dict.c
+++ b/spa/tests/benchmark-dict.c
@@ -39,7 +39,7 @@
 static struct spa_dict_item items[MAX_ITEMS];
 static char values[MAX_ITEMS][32];
 
-static void gen_values()
+static void gen_values(void)
 {
 	uint32_t i, j, idx;
 	static const char chars[] = "abcdefghijklmnopqrstuvwxyz.:*ABCDEFGHIJKLMNOPQRSTUVWXYZ";
diff --git a/spa/tests/benchmark-pod.c b/spa/tests/benchmark-pod.c
index 43ac20b3b77315c01f54a4718c0c0c1e39d4d64d..8af1a5f960f62ea789de9db4363d922f52d70919 100644
--- a/spa/tests/benchmark-pod.c
+++ b/spa/tests/benchmark-pod.c
@@ -37,7 +37,7 @@
 
 #define MAX_COUNT 10000000
 
-static void test_builder()
+static void test_builder(void)
 {
 	uint8_t buffer[1024];
 	struct spa_pod_builder b = { NULL, };
@@ -90,7 +90,7 @@ static void test_builder()
 			t2 - t1, count, count * (uint64_t)SPA_NSEC_PER_SEC / (t2 - t1));
 }
 
-static void test_builder2()
+static void test_builder2(void)
 {
 	uint8_t buffer[1024];
 	struct spa_pod_builder b = { NULL, };
@@ -131,7 +131,7 @@ static void test_builder2()
 			t2 - t1, count, count * (uint64_t)SPA_NSEC_PER_SEC / (t2 - t1));
 }
 
-static void test_parse()
+static void test_parse(void)
 {
 	uint8_t buffer[1024];
 	struct spa_pod_builder b = { NULL, };
@@ -216,7 +216,7 @@ static void test_parse()
 			t2 - t1, count, count * (uint64_t)SPA_NSEC_PER_SEC / (t2 - t1));
 }
 
-static void test_parser()
+static void test_parser(void)
 {
 	uint8_t buffer[1024];
 	struct spa_pod_builder b = { NULL, };
diff --git a/src/daemon/jack.conf.in b/src/daemon/jack.conf.in
index e3e2dc790ed60171610ca82528022e75d1457aeb..f1fa6bc6b7773242b65e65c9ee64d5fe6c499c5f 100644
--- a/src/daemon/jack.conf.in
+++ b/src/daemon/jack.conf.in
@@ -73,6 +73,7 @@ jack.properties = {
      #jack.merge-monitor = false
      #jack.short-name    = false
      #jack.filter-name   = false
+     #jack.filter-char   = " "
      #
      # allow:           Don't restrict self connect requests
      # fail-external:   Fail self connect requests to external ports only
@@ -83,6 +84,7 @@ jack.properties = {
      #jack.locked-process    = true
      #jack.default-as-system = false
      #jack.fix-midi-events   = true
+     #jack.global-buffer-size = false
 }
 
 # client specific properties
@@ -102,4 +104,23 @@ jack.rules = [
             }
         }
     }
+    { matches = [
+            { application.process.binary = "jack_bufsize" }
+        ]
+        actions = {
+            update-props = {
+                jack.global-buffer-size = true   # quantum set globally using metadata
+            }
+        }
+    }
+    { matches = [
+            { application.process.binary = "qsynth" }
+        ]
+        actions = {
+            update-props = {
+                node.pause-on-idle = false  # makes audio fade out when idle
+                node.passive = true         # makes the sink and qsynth suspend
+            }
+        }
+    }
 ]
diff --git a/src/daemon/pipewire-pulse.conf.in b/src/daemon/pipewire-pulse.conf.in
index 1ee3494a084b9f5f792330043c240c5e0f038ffb..2d381a14542007bd6ee6679cec974247e0914318 100644
--- a/src/daemon/pipewire-pulse.conf.in
+++ b/src/daemon/pipewire-pulse.conf.in
@@ -90,6 +90,7 @@ pulse.properties = {
     #pulse.default.frag     = 96000/48000   # 2 seconds
     #pulse.default.tlength  = 96000/48000   # 2 seconds
     #pulse.min.quantum      = 256/48000     # 5ms
+    #pulse.idle.timeout     = 5             # pause after 5s of underruns
     #pulse.default.format   = F32
     #pulse.default.position = [ FL FR ]
     # These overrides are only applied when running in a vm.
@@ -141,6 +142,7 @@ pulse.rules = [
             update-props = {
                 pulse.min.req          = 1024/48000     # 21ms
                 pulse.min.quantum      = 1024/48000     # 21ms
+                #pulse.idle.timeout    = 0
             }
         }
     }
diff --git a/src/daemon/pipewire.conf.in b/src/daemon/pipewire.conf.in
index 748c6173f036f2b4a1eaec4644ffd494b46b8bd5..4aa30a4f91034c34210db2641937239c4dd3cfba 100644
--- a/src/daemon/pipewire.conf.in
+++ b/src/daemon/pipewire.conf.in
@@ -159,14 +159,15 @@ context.modules = [
     { name = libpipewire-module-session-manager }
 
     # Use libcanberra to play X11 Bell
-    #{ name = libpipewire-module-x11-bell
-    #  args = {
-    #      #sink.name = "@DEFAULT_SINK@"
-    #      #sample.name = "bell-window-system"
-    #      #x11.display = null
-    #      #x11.xauthority = null
-    #  }
-    #}
+    { name = libpipewire-module-x11-bell
+        args = {
+            #sink.name = "@DEFAULT_SINK@"
+            #sample.name = "bell-window-system"
+            #x11.display = null
+            #x11.xauthority = null
+        }
+        flags = [ ifexists nofail ]
+    }
 ]
 
 context.objects = [
diff --git a/src/examples/audio-capture.c b/src/examples/audio-capture.c
new file mode 100644
index 0000000000000000000000000000000000000000..4c1afbbec780ce578258b0f0164b947b8f88f968
--- /dev/null
+++ b/src/examples/audio-capture.c
@@ -0,0 +1,209 @@
+/* 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.
+ */
+
+/*
+ [title]
+ Audio capture using \ref pw_stream "pw_stream".
+ [title]
+ */
+
+#include <stdio.h>
+#include <errno.h>
+#include <math.h>
+#include <signal.h>
+
+#include <spa/param/audio/format-utils.h>
+
+#include <pipewire/pipewire.h>
+
+struct data {
+	struct pw_main_loop *loop;
+	struct pw_stream *stream;
+
+	struct spa_audio_info format;
+	unsigned move:1;
+};
+
+/* our data processing function is in general:
+ *
+ *  struct pw_buffer *b;
+ *  b = pw_stream_dequeue_buffer(stream);
+ *
+ *  .. consume stuff in the buffer ...
+ *
+ *  pw_stream_queue_buffer(stream, b);
+ */
+static void on_process(void *userdata)
+{
+	struct data *data = userdata;
+	struct pw_buffer *b;
+	struct spa_buffer *buf;
+	float *samples, max;
+	uint32_t c, n, n_channels, n_samples, peak;
+
+	if ((b = pw_stream_dequeue_buffer(data->stream)) == NULL) {
+		pw_log_warn("out of buffers: %m");
+		return;
+	}
+
+	buf = b->buffer;
+	if ((samples = buf->datas[0].data) == NULL)
+		return;
+
+	n_channels = data->format.info.raw.channels;
+	n_samples = buf->datas[0].chunk->size / sizeof(float);
+
+	/* move cursor up */
+	if (data->move)
+		fprintf(stdout, "%c[%dA", 0x1b, n_channels + 1);
+	fprintf(stdout, "captured %d samples\n", n_samples / n_channels);
+	for (c = 0; c < data->format.info.raw.channels; c++) {
+		max = 0.0f;
+		for (n = c; n < n_samples; n += n_channels)
+			max = fmaxf(max, fabsf(samples[n]));
+
+		peak = SPA_CLAMP(max * 30, 0, 39);
+
+		fprintf(stdout, "channel %d: |%*s%*s| peak:%f\n",
+				c, peak+1, "*", 40 - peak, "", max);
+	}
+	data->move = true;
+	fflush(stdout);
+
+	pw_stream_queue_buffer(data->stream, b);
+}
+
+/* Be notified when the stream param changes. We're only looking at the
+ * format changes.
+ */
+static void
+on_stream_param_changed(void *_data, uint32_t id, const struct spa_pod *param)
+{
+	struct data *data = _data;
+
+	/* NULL means to clear the format */
+	if (param == NULL || id != SPA_PARAM_Format)
+		return;
+
+	if (spa_format_parse(param, &data->format.media_type, &data->format.media_subtype) < 0)
+		return;
+
+	/* only accept raw audio */
+	if (data->format.media_type != SPA_MEDIA_TYPE_audio ||
+	    data->format.media_subtype != SPA_MEDIA_SUBTYPE_raw)
+		return;
+
+	/* call a helper function to parse the format for us. */
+	spa_format_audio_raw_parse(param, &data->format.info.raw);
+
+	fprintf(stdout, "capturing rate:%d channels:%d\n",
+			data->format.info.raw.rate, data->format.info.raw.channels);
+
+}
+
+static const struct pw_stream_events stream_events = {
+	PW_VERSION_STREAM_EVENTS,
+	.param_changed = on_stream_param_changed,
+	.process = on_process,
+};
+
+static void do_quit(void *userdata, int signal_number)
+{
+	struct data *data = userdata;
+	pw_main_loop_quit(data->loop);
+}
+
+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);
+
+	/* make a main loop. If you already have another main loop, you can add
+	 * the fd of this pipewire mainloop to it. */
+	data.loop = pw_main_loop_new(NULL);
+
+	pw_loop_add_signal(pw_main_loop_get_loop(data.loop), SIGINT, do_quit, &data);
+	pw_loop_add_signal(pw_main_loop_get_loop(data.loop), SIGTERM, do_quit, &data);
+
+	/* Create a simple stream, the simple stream manages the core and remote
+	 * objects for you if you don't need to deal with them.
+	 *
+	 * If you plan to autoconnect your stream, you need to provide at least
+	 * media, category and role properties.
+	 *
+	 * Pass your events and a user_data pointer as the last arguments. This
+	 * will inform you about the stream state. The most important event
+	 * 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, "Capture",
+			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]);
+
+	/* uncomment if you want to capture from the sink monitor ports */
+	/* pw_properties_set(props, PW_KEY_STREAM_CAPTURE_SINK, "true"); */
+
+	data.stream = pw_stream_new_simple(
+			pw_main_loop_get_loop(data.loop),
+			"audio-capture",
+			props,
+			&stream_events,
+			&data);
+
+	/* Make one parameter with the supported formats. The SPA_PARAM_EnumFormat
+	 * id means that this is a format enumeration (of 1 value).
+	 * We leave the channels and rate empty to accept the native graph
+	 * rate and channels. */
+	params[0] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat,
+			&SPA_AUDIO_INFO_RAW_INIT(
+				.format = SPA_AUDIO_FORMAT_F32));
+
+	/* Now connect this stream. We ask that our process function is
+	 * called in a realtime thread. */
+	pw_stream_connect(data.stream,
+			  PW_DIRECTION_INPUT,
+			  PW_ID_ANY,
+			  PW_STREAM_FLAG_AUTOCONNECT |
+			  PW_STREAM_FLAG_MAP_BUFFERS |
+			  PW_STREAM_FLAG_RT_PROCESS,
+			  params, 1);
+
+	/* and wait while we let things run */
+	pw_main_loop_run(data.loop);
+
+	pw_stream_destroy(data.stream);
+	pw_main_loop_destroy(data.loop);
+	pw_deinit();
+
+	return 0;
+}
diff --git a/src/examples/export-sink.c b/src/examples/export-sink.c
index 9e0985475ec1591514135b7193be3c285c48c66f..ee8a57c45bb3ca8160b9b58726796f0c702a2f0e 100644
--- a/src/examples/export-sink.c
+++ b/src/examples/export-sink.c
@@ -303,11 +303,10 @@ static int port_set_format(void *object,
 	Uint32 sdl_format;
 	void *dest;
 
-	d->info.change_mask = SPA_PORT_CHANGE_MASK_PARAMS;
 	if (format == NULL) {
+		spa_zero(d->format);
 		SDL_DestroyTexture(d->texture);
-		d->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
-		d->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+		d->texture = NULL;
 	} else {
 		spa_debug_format(0, NULL, format);
 
@@ -316,6 +315,9 @@ static int port_set_format(void *object,
 		sdl_format = id_to_sdl_format(d->format.format);
 		if (sdl_format == SDL_PIXELFORMAT_UNKNOWN)
 			return -EINVAL;
+		if (d->format.size.width == 0 ||
+		    d->format.size.height == 0)
+			return -EINVAL;
 
 		d->texture = SDL_CreateTexture(d->renderer,
 						  sdl_format,
@@ -325,9 +327,16 @@ static int port_set_format(void *object,
 		SDL_LockTexture(d->texture, NULL, &dest, &d->stride);
 		SDL_UnlockTexture(d->texture);
 
+	}
+	d->info.change_mask = SPA_PORT_CHANGE_MASK_PARAMS;
+	if (format) {
 		d->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE);
 		d->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ);
+	} else {
+		d->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+		d->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
 	}
+
 	spa_node_emit_port_info(&d->hooks, direction, port_id, &d->info);
 	d->info.change_mask = 0;
 
diff --git a/src/examples/export-source.c b/src/examples/export-source.c
index f84d01d9262d6e004ed984955707acf74fc0390d..22dfa1426f0106f57122afdce4093cec278c65bc 100644
--- a/src/examples/export-source.c
+++ b/src/examples/export-source.c
@@ -275,26 +275,31 @@ static int port_set_format(void *object,
 {
 	struct data *d = object;
 
-	d->info.change_mask = SPA_PORT_CHANGE_MASK_PARAMS;
 	if (format == NULL) {
-		d->format.format = 0;
-		d->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
-		d->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
-		spa_node_emit_port_info(&d->hooks, SPA_DIRECTION_OUTPUT, 0, &d->info);
-		return 0;
-	}
+		spa_zero(d->format);
+	} else {
+		spa_debug_format(0, NULL, format);
 
-	spa_debug_format(0, NULL, format);
-
-	if (spa_format_audio_raw_parse(format, &d->format) < 0)
-		return -EINVAL;
+		if (spa_format_audio_raw_parse(format, &d->format) < 0)
+			return -EINVAL;
 
-	if (d->format.format != SPA_AUDIO_FORMAT_S16 &&
-	    d->format.format != SPA_AUDIO_FORMAT_F32)
-		return -EINVAL;
+		if (d->format.format != SPA_AUDIO_FORMAT_S16 &&
+		    d->format.format != SPA_AUDIO_FORMAT_F32)
+			return -EINVAL;
+		if (d->format.rate == 0 ||
+		    d->format.channels == 0 ||
+		    d->format.channels > SPA_AUDIO_MAX_CHANNELS)
+			return -EINVAL;
+	}
 
-	d->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE);
-	d->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ);
+	d->info.change_mask = SPA_PORT_CHANGE_MASK_PARAMS;
+	if (format) {
+		d->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE);
+		d->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ);
+	} else {
+		d->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+		d->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+	}
 	spa_node_emit_port_info(&d->hooks, SPA_DIRECTION_OUTPUT, 0, &d->info);
 
 	return 0;
diff --git a/src/examples/local-v4l2.c b/src/examples/local-v4l2.c
index 0e466ba8d6589e952d9d603d8e6142de563fa1a6..5f74767272432eb4144cc81ccdbf611b14973b34 100644
--- a/src/examples/local-v4l2.c
+++ b/src/examples/local-v4l2.c
@@ -207,28 +207,39 @@ static int port_set_format(void *object, enum spa_direction direction, uint32_t
 	Uint32 sdl_format;
 	void *dest;
 
-	if (format == NULL)
-		return 0;
-
-	spa_debug_format(0, NULL, format);
-
-	spa_format_video_raw_parse(format, &d->format);
-
-	sdl_format = id_to_sdl_format(d->format.format);
-	if (sdl_format == SDL_PIXELFORMAT_UNKNOWN)
-		return -EINVAL;
-
-	d->texture = SDL_CreateTexture(d->renderer,
-				       sdl_format,
-				       SDL_TEXTUREACCESS_STREAMING,
-				       d->format.size.width,
-				       d->format.size.height);
-	SDL_LockTexture(d->texture, NULL, &dest, &d->stride);
-	SDL_UnlockTexture(d->texture);
+	if (format == NULL) {
+		spa_zero(d->format);
+		SDL_DestroyTexture(d->texture);
+		d->texture = NULL;
+	} else {
+		spa_debug_format(0, NULL, format);
+
+		spa_format_video_raw_parse(format, &d->format);
+
+		sdl_format = id_to_sdl_format(d->format.format);
+		if (sdl_format == SDL_PIXELFORMAT_UNKNOWN)
+			return -EINVAL;
+		if (d->format.size.width == 0 ||
+		    d->format.size.height == 0)
+			return -EINVAL;
+
+		d->texture = SDL_CreateTexture(d->renderer,
+					       sdl_format,
+					       SDL_TEXTUREACCESS_STREAMING,
+					       d->format.size.width,
+					       d->format.size.height);
+		SDL_LockTexture(d->texture, NULL, &dest, &d->stride);
+		SDL_UnlockTexture(d->texture);
+	}
 
 	d->info.change_mask = SPA_PORT_CHANGE_MASK_PARAMS;
-	d->params[1] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE);
-	d->params[2] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ);
+	if (format) {
+		d->params[1] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE);
+		d->params[2] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ);
+	} else {
+		d->params[1] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+		d->params[2] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+	}
 	spa_node_emit_port_info(&d->hooks, SPA_DIRECTION_INPUT, 0, &d->info);
 
 	return 0;
diff --git a/src/examples/meson.build b/src/examples/meson.build
index 747cb1416233c48977cbd40a4f52f9eec14ef5a5..e2f260085b254530c2d5d40ff8a95198dfa6fcb6 100644
--- a/src/examples/meson.build
+++ b/src/examples/meson.build
@@ -3,6 +3,7 @@ examples = [
   'audio-src',
   'audio-dsp-src',
   'audio-dsp-filter',
+  'audio-capture',
   'video-play',
   'video-src',
   'video-dsp-play',
diff --git a/src/examples/sdl.h b/src/examples/sdl.h
index cb7b87a6f1d939fbed8362ab6aeb82c9b0516086..74ea74ab6700022d5b9c6e385c176622b2c7c18a 100644
--- a/src/examples/sdl.h
+++ b/src/examples/sdl.h
@@ -130,25 +130,22 @@ static struct {
 
 static inline uint32_t sdl_format_to_id(Uint32 format)
 {
-	size_t i;
-	for (i = 0; i < SPA_N_ELEMENTS(sdl_video_formats); i++) {
-		if (sdl_video_formats[i].format == format)
-			return sdl_video_formats[i].id;
+	SPA_FOR_EACH_ELEMENT_VAR(sdl_video_formats, f) {
+		if (f->format == format)
+			return f->id;
 	}
 	return SPA_VIDEO_FORMAT_UNKNOWN;
 }
 
 static inline Uint32 id_to_sdl_format(uint32_t id)
 {
-	size_t i;
-	for (i = 0; i < SPA_N_ELEMENTS(sdl_video_formats); i++) {
-		if (sdl_video_formats[i].id == id)
-			return sdl_video_formats[i].format;
+	SPA_FOR_EACH_ELEMENT_VAR(sdl_video_formats, f) {
+		if (f->id == id)
+			return f->format;
 	}
 	return SDL_PIXELFORMAT_UNKNOWN;
 }
 
-
 static inline struct spa_pod *sdl_build_formats(SDL_RendererInfo *info, struct spa_pod_builder *b)
 {
 	uint32_t i, c;
@@ -178,8 +175,8 @@ static inline struct spa_pod *sdl_build_formats(SDL_RendererInfo *info, struct s
 		spa_pod_builder_id(b, id);
 	}
 	/* then all the other ones SDL can convert from/to */
-	for (i = 0; i < SPA_N_ELEMENTS(sdl_video_formats); i++) {
-		uint32_t id = sdl_video_formats[i].id;
+	SPA_FOR_EACH_ELEMENT_VAR(sdl_video_formats, f) {
+		uint32_t id = f->id;
 		if (id != SPA_VIDEO_FORMAT_UNKNOWN)
 			spa_pod_builder_id(b, id);
 	}
diff --git a/src/examples/video-play-fixate.c b/src/examples/video-play-fixate.c
index 1badc3c39a8d60877b642a535419222122c3217a..021eb0df57f0f857cbdbe1f8845f24942c1e1c79 100644
--- a/src/examples/video-play-fixate.c
+++ b/src/examples/video-play-fixate.c
@@ -340,6 +340,10 @@ on_stream_param_changed(void *_data, uint32_t id, const struct spa_pod *param)
 		pw_stream_set_error(stream, -EINVAL, "unknown pixel format");
 		return;
 	}
+	if (data->size.width == 0 || data->size.height == 0) {
+		pw_stream_set_error(stream, -EINVAL, "invalid size");
+		return;
+	}
 
 	data->texture = SDL_CreateTexture(data->renderer,
 					  sdl_format,
diff --git a/src/examples/video-play-pull.c b/src/examples/video-play-pull.c
index 2b2c34660c1275ef98c1698b9aa4bf8b2be78897..8076779629b4e2c66a56b06f930b04298e570263 100644
--- a/src/examples/video-play-pull.c
+++ b/src/examples/video-play-pull.c
@@ -388,6 +388,10 @@ on_stream_param_changed(void *_data, uint32_t id, const struct spa_pod *param)
 		pw_stream_set_error(stream, -EINVAL, "unknown pixel format");
 		return;
 	}
+	if (data->size.width == 0 || data->size.height == 0) {
+		pw_stream_set_error(stream, -EINVAL, "invalid size");
+		return;
+	}
 
 	data->texture = SDL_CreateTexture(data->renderer,
 					  sdl_format,
diff --git a/src/examples/video-play-reneg.c b/src/examples/video-play-reneg.c
index aa071850afad34a68860867ba8161a9a891ddd67..f37b66285e6485a488cf93e912a5584f33f7e79c 100644
--- a/src/examples/video-play-reneg.c
+++ b/src/examples/video-play-reneg.c
@@ -233,6 +233,10 @@ on_stream_param_changed(void *_data, uint32_t id, const struct spa_pod *param)
 		pw_stream_set_error(stream, -EINVAL, "unknown pixel format");
 		return;
 	}
+	if (data->size.width == 0 || data->size.height == 0) {
+		pw_stream_set_error(stream, -EINVAL, "invalid size");
+		return;
+	}
 
 	data->texture = SDL_CreateTexture(data->renderer,
 					  sdl_format,
diff --git a/src/examples/video-play.c b/src/examples/video-play.c
index 68589c90218db9c5a1600893f60fb69f9df95194..61c3114f798d85f4c7da36b9c339c2fa8b6e96d5 100644
--- a/src/examples/video-play.c
+++ b/src/examples/video-play.c
@@ -336,6 +336,10 @@ on_stream_param_changed(void *_data, uint32_t id, const struct spa_pod *param)
 		pw_stream_set_error(stream, -EINVAL, "unknown pixel format");
 		return;
 	}
+	if (data->size.width == 0 || data->size.height == 0) {
+		pw_stream_set_error(stream, -EINVAL, "invalid size");
+		return;
+	}
 
 	data->texture = SDL_CreateTexture(data->renderer,
 					  sdl_format,
diff --git a/src/gst/gstpipewiredeviceprovider.c b/src/gst/gstpipewiredeviceprovider.c
index 0d2942d77bd7af544be5c9f19482fec06c9aa1cf..5dcce190e8c57c8c152363c1aeb12e315e026c2e 100644
--- a/src/gst/gstpipewiredeviceprovider.c
+++ b/src/gst/gstpipewiredeviceprovider.c
@@ -491,6 +491,8 @@ static void registry_event_global(void *data, uint32_t id, uint32_t permissions,
           gst_device_provider_hide_provider (provider, "pulsedeviceprovider");
 	else if (g_str_has_prefix(str, "v4l2:"))
           gst_device_provider_hide_provider (provider, "v4l2deviceprovider");
+	else if (g_str_has_prefix(str, "libcamera:"))
+          gst_device_provider_hide_provider (provider, "libcameraprovider");
       }
     }
 
diff --git a/src/modules/meson.build b/src/modules/meson.build
index 3e0e3a3fdccd890fcdfc83da5c7db5e33cf3efba..c267e681cd0437852b09b9c56c11bbb57a80501e 100644
--- a/src/modules/meson.build
+++ b/src/modules/meson.build
@@ -26,6 +26,8 @@ module_sources = [
   'module-rt.c',
   'module-raop-discover.c',
   'module-raop-sink.c',
+  'module-rtp-source.c',
+  'module-rtp-sink.c',
   'module-session-manager.c',
   'module-zeroconf-discover.c',
   'module-roc-source.c',
@@ -59,33 +61,36 @@ simd_cargs = []
 simd_dependencies = []
 
 if have_sse
-  pffft_sse = static_library('pffft_sse',
-    ['module-filter-chain/pffft.c' ],
+  filter_chain_sse = static_library('filter_chain_sse',
+    ['module-filter-chain/pffft.c',
+     'module-filter-chain/dsp-ops-sse.c' ],
     c_args : [sse_args, '-O3', '-DHAVE_SSE'],
     dependencies : [ spa_dep ],
     install : false
     )
   simd_cargs += ['-DHAVE_SSE']
-  simd_dependencies += pffft_sse
+  simd_dependencies += filter_chain_sse
 endif
 if have_neon
-  pffft_neon = static_library('pffft_neon',
+  filter_chain_neon = static_library('filter_chain_neon',
     ['module-filter-chain/pffft.c' ],
     c_args : [neon_args, '-O3', '-DHAVE_NEON'],
     dependencies : [ spa_dep ],
     install : false
     )
   simd_cargs += ['-DHAVE_NEON']
-  simd_dependencies += pffft_neon
+  simd_dependencies += filter_chain_neon
 endif
 
-pffft_c = static_library('pffft_c',
-  ['module-filter-chain/pffft.c' ],
+filter_chain_c = static_library('filter_chain_c',
+  ['module-filter-chain/pffft.c',
+   'module-filter-chain/dsp-ops.c',
+   'module-filter-chain/dsp-ops-c.c' ],
   c_args : [simd_cargs, '-O3', '-DPFFFT_SIMD_DISABLE'],
   dependencies : [ spa_dep ],
   install : false
 )
-simd_dependencies += pffft_c
+simd_dependencies += filter_chain_c
 
 filter_chain_sources = [
   'module-filter-chain.c',
@@ -258,6 +263,8 @@ pipewire_module_protocol_pulse_sources = [
   'module-protocol-pulse/modules/module-roc-sink.c',
   'module-protocol-pulse/modules/module-roc-sink-input.c',
   'module-protocol-pulse/modules/module-roc-source.c',
+  'module-protocol-pulse/modules/module-rtp-recv.c',
+  'module-protocol-pulse/modules/module-rtp-send.c',
   'module-protocol-pulse/modules/module-simple-protocol-tcp.c',
   'module-protocol-pulse/modules/module-switch-on-connect.c',
   'module-protocol-pulse/modules/module-tunnel-sink.c',
@@ -482,6 +489,24 @@ summary({'raop-sink (requires OpenSSL)': build_module_raop}, bool_yn: true, sect
 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')
 
+pipewire_module_rtp_source = shared_library('pipewire-module-rtp-source',
+  [ 'module-rtp-source.c' ],
+  include_directories : [configinc],
+  install : true,
+  install_dir : modules_install_dir,
+  install_rpath: modules_install_dir,
+  dependencies : [mathlib, dl_lib, rt_lib, pipewire_dep],
+)
+
+pipewire_module_rtp_sink = shared_library('pipewire-module-rtp-sink',
+  [ 'module-rtp-sink.c' ],
+  include_directories : [configinc],
+  install : true,
+  install_dir : modules_install_dir,
+  install_rpath: modules_install_dir,
+  dependencies : [mathlib, dl_lib, rt_lib, pipewire_dep],
+)
+
 build_module_roc = roc_lib.found()
 if build_module_roc
 pipewire_module_roc_sink = shared_library('pipewire-module-roc-sink',
diff --git a/src/modules/module-avb/acmp.c b/src/modules/module-avb/acmp.c
index 18ea1ba92c3948a09579f6e75a3a146c56be1d42..a7a409a27988531c50d0e4d0cbecfc1218b39aa5 100644
--- a/src/modules/module-avb/acmp.c
+++ b/src/modules/module-avb/acmp.c
@@ -345,11 +345,10 @@ static const struct msg_info msg_info[] = {
 
 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];
+	SPA_FOR_EACH_ELEMENT_VAR(msg_info, i) {
+		if ((name == NULL && type == i->type) ||
+		    (name != NULL && spa_streq(name, i->name)))
+			return i;
 	}
 	return NULL;
 }
diff --git a/src/modules/module-avb/aecp-aem.c b/src/modules/module-avb/aecp-aem.c
index 22a2ac2dd7ec4761626034bd563115eddf4ffe0c..d1913301b359c320f428d9f7001aee9813aef53b 100644
--- a/src/modules/module-avb/aecp-aem.c
+++ b/src/modules/module-avb/aecp-aem.c
@@ -250,11 +250,10 @@ static const struct cmd_info cmd_info[] = {
 
 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];
+	SPA_FOR_EACH_ELEMENT_VAR(cmd_info, i) {
+		if ((name == NULL && type == i->type) ||
+		    (name != NULL && spa_streq(name, i->name)))
+			return i;
 	}
 	return NULL;
 }
diff --git a/src/modules/module-avb/aecp.c b/src/modules/module-avb/aecp.c
index 3c25b0ea31ab81accea0520926869f1ffac01b38..d581f818bae2dca6fb5f96e0b05b49f7dca7d682 100644
--- a/src/modules/module-avb/aecp.c
+++ b/src/modules/module-avb/aecp.c
@@ -67,11 +67,10 @@ static const struct msg_info msg_info[] = {
 
 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];
+	SPA_FOR_EACH_ELEMENT_VAR(msg_info, i) {
+		if ((name == NULL && type == i->type) ||
+		    (name != NULL && spa_streq(name, i->name)))
+			return i;
 	}
 	return NULL;
 }
diff --git a/src/modules/module-avb/stream.c b/src/modules/module-avb/stream.c
index b86f0814c36452907bd1df197b74a4bdd799852f..0f5b148e8367e83d73b02dca138382d26fbcac2b 100644
--- a/src/modules/module-avb/stream.c
+++ b/src/modules/module-avb/stream.c
@@ -136,7 +136,7 @@ static int flush_write(struct stream *stream, uint64_t current_time)
 		p->timestamp = ptime;
 		p->dbc = dbc;
 
-		n = sendmsg(stream->source->fd, &stream->msg, 0);
+		n = sendmsg(stream->source->fd, &stream->msg, MSG_NOSIGNAL);
 		if (n < 0 || n != (ssize_t)stream->pdu_size) {
 			pw_log_error("sendmsg() failed %zd != %zd: %m",
 					n, stream->pdu_size);
diff --git a/src/modules/module-client-node/client-node.c b/src/modules/module-client-node/client-node.c
index 9357d03da9873fcc9e62ced8900cc7552d617bd6..563585553e202d74ae6a43cda1cd3424706bd2b1 100644
--- a/src/modules/module-client-node/client-node.c
+++ b/src/modules/module-client-node/client-node.c
@@ -744,7 +744,7 @@ do_port_use_buffers(struct impl *impl,
 	struct node *this = &impl->node;
 	struct port *p;
 	struct mix *mix;
-	uint32_t i, j, peer_id;
+	uint32_t i, j;
 	struct pw_client_node_buffer *mb;
 
 	p = GET_PORT(this, direction, port_id);
@@ -762,8 +762,6 @@ do_port_use_buffers(struct impl *impl,
 	if ((mix = find_mix(p, mix_id)) == NULL || !mix->valid)
 		return -EINVAL;
 
-	peer_id = mix->peer_id;
-
 	if (direction == SPA_DIRECTION_OUTPUT) {
 		mix_id = SPA_ID_INVALID;
 		if ((mix = find_mix(p, mix_id)) == NULL || !mix->valid)
@@ -881,10 +879,6 @@ do_port_use_buffers(struct impl *impl,
 	}
 	mix->n_buffers = n_buffers;
 
-	if (this->resource->version >= 4)
-		pw_client_node_resource_port_set_mix_info(this->resource,
-						 direction, port_id, mix_id,
-						 peer_id, NULL);
 	return pw_client_node_resource_port_use_buffers(this->resource,
 						 direction, port_id, mix_id, flags,
 						 n_buffers, mb);
@@ -1486,6 +1480,7 @@ static int impl_mix_port_set_io(void *object,
 	struct port *p = object;
 	struct pw_impl_port *port = p->port;
 	struct impl *impl = port->owner_data;
+	struct node *this = &impl->node;
 	struct pw_impl_port_mix *mix;
 
 	mix = pw_map_lookup(&port->mix_port_map, mix_id);
@@ -1497,6 +1492,11 @@ static int impl_mix_port_set_io(void *object,
 			mix->io = data;
 		else
 			mix->io = NULL;
+
+		if (mix->io != NULL && this->resource && this->resource->version >= 4)
+			pw_client_node_resource_port_set_mix_info(this->resource,
+						 direction, port->port_id,
+						 mix->port.port_id, mix->peer_id, NULL);
 	}
 
 	return do_port_set_io(impl,
@@ -1558,14 +1558,14 @@ static void node_port_added(void *data, struct pw_impl_port *port)
 	struct impl *impl = data;
 	struct port *p = pw_impl_port_get_user_data(port);
 
-	pw_impl_port_set_mix(port, &p->mix_node,
-			PW_IMPL_PORT_MIX_FLAG_MULTI |
-			PW_IMPL_PORT_MIX_FLAG_MIX_ONLY);
-
 	port->flags |= PW_IMPL_PORT_FLAG_NO_MIXER;
 
 	port->impl = SPA_CALLBACKS_INIT(&port_impl, p);
 	port->owner_data = impl;
+
+	pw_impl_port_set_mix(port, &p->mix_node,
+			PW_IMPL_PORT_MIX_FLAG_MULTI |
+			PW_IMPL_PORT_MIX_FLAG_MIX_ONLY);
 }
 
 static void node_port_removed(void *data, struct pw_impl_port *port)
diff --git a/src/modules/module-client-node/protocol-native.c b/src/modules/module-client-node/protocol-native.c
index 0b640b2519b187f9f9851967a9444d79a76fc43f..dd516928eebfa820bfab647c70b908d1daf100f8 100644
--- a/src/modules/module-client-node/protocol-native.c
+++ b/src/modules/module-client-node/protocol-native.c
@@ -556,6 +556,8 @@ static int client_node_demarshal_port_use_buffers(void *data, const struct pw_pr
 					      SPA_POD_Id(&m->type),
 					      SPA_POD_Int(&m->size), NULL) < 0)
 				return -EINVAL;
+
+			m->data = NULL;
 		}
 		if (spa_pod_parser_get(&prs,
 					SPA_POD_Int(&buf->n_datas), NULL) < 0)
@@ -576,7 +578,9 @@ static int client_node_demarshal_port_use_buffers(void *data, const struct pw_pr
 					      SPA_POD_Int(&d->maxsize), NULL) < 0)
 				return -EINVAL;
 
+			d->fd = -1;
 			d->data = SPA_UINT32_TO_PTR(data_id);
+			d->chunk = NULL;
 		}
 	}
 	pw_proxy_notify(proxy, struct pw_client_node_events, port_use_buffers, 0,
diff --git a/src/modules/module-client-node/v0/client-node.c b/src/modules/module-client-node/v0/client-node.c
index aeb5931c6552fd980fb2dab3b4554abf0684ddf1..747a4ad4a2f968268c8e84c90d28304fc21be20d 100644
--- a/src/modules/module-client-node/v0/client-node.c
+++ b/src/modules/module-client-node/v0/client-node.c
@@ -1333,13 +1333,12 @@ static void convert_properties(struct pw_properties *properties)
 		{ "pipewire.target.node", PW_KEY_NODE_TARGET, }
 	};
 
-	uint32_t i;
 	const char *str;
 
-	for(i = 0; i < SPA_N_ELEMENTS(props); i++) {
-		if ((str = pw_properties_get(properties, props[i].from)) != NULL) {
-			pw_properties_set(properties, props[i].to, str);
-			pw_properties_set(properties, props[i].from, NULL);
+	SPA_FOR_EACH_ELEMENT_VAR(props, p) {
+		if ((str = pw_properties_get(properties, p->from)) != NULL) {
+			pw_properties_set(properties, p->to, str);
+			pw_properties_set(properties, p->from, NULL);
 		}
 	}
 }
diff --git a/src/modules/module-client-node/v0/ext-client-node.h b/src/modules/module-client-node/v0/ext-client-node.h
index 564df77b65ec40ff10b3e7fa250c44986f512e65..21ba59718bd9aa4d592f21ef21bc0aa50d8e447a 100644
--- a/src/modules/module-client-node/v0/ext-client-node.h
+++ b/src/modules/module-client-node/v0/ext-client-node.h
@@ -140,9 +140,9 @@ struct pw_client_node0_message_port_reuse_buffer {
 
 #define PW_CLIENT_NODE0_MESSAGE_TYPE(message)	(((struct pw_client_node0_message*)(message))->body.type.value)
 
-#define PW_CLIENT_NODE0_MESSAGE_INIT(message) (struct pw_client_node0_message)			\
+#define PW_CLIENT_NODE0_MESSAGE_INIT(message) ((struct pw_client_node0_message)			\
 	{ { { sizeof(struct pw_client_node0_message_body), SPA_TYPE_Struct } },			\
-	  { SPA_POD_INIT_Int(message) } }
+	  { SPA_POD_INIT_Int(message) } })
 
 #define PW_CLIENT_NODE0_MESSAGE_INIT_FULL(type,size,message,...) (type)				\
 	{ { { size, SPA_TYPE_Struct } },							\
diff --git a/src/modules/module-echo-cancel.c b/src/modules/module-echo-cancel.c
index 42caf780bcd01fd4a4a2c190f6db9811fcbb2446..7e0d81a79fe9f2c9fa8136032006a5b735f38aa4 100644
--- a/src/modules/module-echo-cancel.c
+++ b/src/modules/module-echo-cancel.c
@@ -43,6 +43,7 @@
 #include <spa/param/audio/raw.h>
 #include <spa/param/profiler.h>
 #include <spa/pod/builder.h>
+#include <spa/pod/dynamic.h>
 #include <spa/support/plugin.h>
 #include <spa/utils/json.h>
 #include <spa/utils/names.h>
@@ -196,15 +197,18 @@ struct impl {
 
 	uint32_t max_buffer_size;
 	uint32_t buffer_delay;
+	uint32_t current_delay;
 
 	struct spa_handle *spa_handle;
 	struct spa_plugin_loader *loader;
+
+	bool monitor_mode;
 };
 
 static void process(struct impl *impl)
 {
 	struct pw_buffer *cout;
-	struct pw_buffer *pout;
+	struct pw_buffer *pout = NULL;
 	float rec_buf[impl->info.channels][impl->aec_blocksize / sizeof(float)];
 	float play_buf[impl->info.channels][impl->aec_blocksize / sizeof(float)];
 	float play_delayed_buf[impl->info.channels][impl->aec_blocksize / sizeof(float)];
@@ -218,7 +222,7 @@ static void process(struct impl *impl)
 	uint32_t rindex, pindex, oindex, pdindex, avail;
 	int32_t stride = 0;
 
-	if ((pout = pw_stream_dequeue_buffer(impl->playback)) == NULL) {
+	if (impl->playback != NULL && (pout = pw_stream_dequeue_buffer(impl->playback)) == NULL) {
 		pw_log_debug("out of playback buffers: %m");
 		goto done;
 	}
@@ -256,23 +260,50 @@ static void process(struct impl *impl)
 				impl->play_ringsize, pdindex % impl->play_ringsize,
 				(void *)play_delayed[i], size);
 
-		/* output to sink, just copy */
-		dd = &pout->buffer->datas[i];
-		memcpy(dd->data, play[i], size);
+		if (pout != NULL) {
+			/* output to sink, just copy */
+			dd = &pout->buffer->datas[i];
+			memcpy(dd->data, play[i], size);
 
-		dd->chunk->offset = 0;
-		dd->chunk->size = size;
-		dd->chunk->stride = stride;
+			dd->chunk->offset = 0;
+			dd->chunk->size = size;
+			dd->chunk->stride = stride;
+		}
 	}
 
 	spa_ringbuffer_read_update(&impl->rec_ring, rindex + size);
 	spa_ringbuffer_read_update(&impl->play_ring, pindex + size);
 	spa_ringbuffer_read_update(&impl->play_delayed_ring, pdindex + size);
 
-	pw_stream_queue_buffer(impl->playback, pout);
+	if (impl->playback != NULL)
+		pw_stream_queue_buffer(impl->playback, pout);
 
-	/* Now run the canceller */
-	spa_audio_aec_run(impl->aec, rec, play_delayed, out, size / sizeof(float));
+	if (SPA_UNLIKELY (impl->current_delay < impl->buffer_delay)) {
+		uint32_t delay_left = impl->buffer_delay - impl->current_delay;
+		uint32_t silence_size;
+
+		/* don't run the canceller until play_buffer has been filled,
+		 * copy silence to output in the meantime */
+		silence_size = SPA_MIN(size, delay_left * sizeof(float));
+		for (i = 0; i < impl->info.channels; i++)
+			memset(out[i], 0, silence_size);
+		impl->current_delay += silence_size / sizeof(float);
+		pw_log_debug("current_delay %d", impl->current_delay);
+
+		if (silence_size != size) {
+			const float *pd[impl->info.channels];
+			float *o[impl->info.channels];
+
+			for (i = 0; i < impl->info.channels; i++) {
+				pd[i] = play_delayed[i] + delay_left;
+				o[i] = out[i] + delay_left;
+			}
+			spa_audio_aec_run(impl->aec, rec, pd, o, size / sizeof(float) - delay_left);
+		}
+	} else {
+		/* run the canceller */
+		spa_audio_aec_run(impl->aec, rec, play_delayed, out, size / sizeof(float));
+	}
 
 	/* Next, copy over the output to the output ringbuffer */
 	avail = spa_ringbuffer_get_write_index(&impl->out_ring, &oindex);
@@ -479,14 +510,55 @@ static void input_param_latency_changed(struct impl *impl, const struct spa_pod
 	else
 		pw_stream_update_params(impl->capture, params, 1);
 }
+static struct spa_pod* get_props_param(struct impl* impl, struct spa_pod_builder* b)
+{
+	if (spa_audio_aec_get_params(impl->aec, NULL) > 0) {
+		struct spa_pod_frame f[2];
+		spa_pod_builder_push_object(
+		    b, &f[0], SPA_TYPE_OBJECT_Props, SPA_PARAM_Props);
+		spa_pod_builder_prop(b, SPA_PROP_params, 0);
+		spa_pod_builder_push_struct(b, &f[1]);
+
+		spa_audio_aec_get_params(impl->aec, b);
+
+		spa_pod_builder_pop(b, &f[1]);
+		return spa_pod_builder_pop(b, &f[0]);
+	}
+	return NULL;
+}
 
-static void input_param_changed(void *data, uint32_t id, const struct spa_pod *param)
+static void input_param_changed(void *data, uint32_t id, const struct spa_pod* param)
 {
-	struct impl *impl = data;
+	struct spa_pod_object* obj = (struct spa_pod_object*)param;
+	const struct spa_pod_prop* prop;
+	struct impl* impl = data;
 	switch (id) {
 	case SPA_PARAM_Latency:
 		input_param_latency_changed(impl, param);
 		break;
+	case SPA_PARAM_Props:
+		if (param != NULL) {
+			uint8_t buffer[1024];
+			struct spa_pod_dynamic_builder b;
+			const struct spa_pod* params[1];
+			SPA_POD_OBJECT_FOREACH(obj, prop)
+			{
+				if (prop->key == SPA_PROP_params) {
+					spa_audio_aec_set_params(impl->aec, &prop->value);
+				}
+			}
+
+			spa_pod_dynamic_builder_init(&b, buffer, sizeof(buffer), 4096);
+			params[0] = get_props_param(impl, &b.b);
+			if (params[0]) {
+				pw_stream_update_params(impl->capture, params, 1);
+				pw_stream_update_params(impl->playback, params, 1);
+			}
+			spa_pod_dynamic_builder_clean(&b);
+		} else {
+			pw_log_warn("param is null");
+		}
+		break;
 	}
 }
 
@@ -519,7 +591,11 @@ static void output_state_changed(void *data, enum pw_stream_state old,
 	switch (state) {
 	case PW_STREAM_STATE_PAUSED:
 		pw_stream_flush(impl->sink, false);
-		pw_stream_flush(impl->playback, false);
+		if (impl->playback != NULL)
+			pw_stream_flush(impl->playback, false);
+		if (old == PW_STREAM_STATE_STREAMING) {
+			impl->current_delay = 0;
+		}
 		break;
 	case PW_STREAM_STATE_UNCONNECTED:
 		pw_log_info("%p: output unconnected", impl);
@@ -548,17 +624,40 @@ static void output_param_latency_changed(struct impl *impl, const struct spa_pod
 
 	if (latency.direction == SPA_DIRECTION_INPUT)
 		pw_stream_update_params(impl->sink, params, 1);
-	else
+	else if (impl->playback != NULL)
 		pw_stream_update_params(impl->playback, params, 1);
 }
 
 static void output_param_changed(void *data, uint32_t id, const struct spa_pod *param)
 {
+	struct spa_pod_object *obj = (struct spa_pod_object *) param;
+	const struct spa_pod_prop *prop;
 	struct impl *impl = data;
 	switch (id) {
 	case SPA_PARAM_Latency:
 		output_param_latency_changed(impl, param);
 		break;
+	case SPA_PARAM_Props:
+		if (param != NULL) {
+			uint8_t buffer[1024];
+			struct spa_pod_dynamic_builder b;
+			const struct spa_pod* params[1];
+
+			SPA_POD_OBJECT_FOREACH(obj, prop)
+			{
+				if (prop->key == SPA_PROP_params) {
+					spa_audio_aec_set_params(impl->aec, &prop->value);
+				}
+			}
+			spa_pod_dynamic_builder_init(&b, buffer, sizeof(buffer), 4096);
+			params[0] = get_props_param(impl, &b.b);
+			if (params[0] != NULL) {
+				pw_stream_update_params(impl->capture, params, 1);
+				pw_stream_update_params(impl->playback, params, 1);
+			}
+			spa_pod_dynamic_builder_clean(&b);
+		}
+		break;
 	}
 }
 
@@ -635,8 +734,10 @@ static void sink_process(void *data)
 static void playback_destroy(void *d)
 {
 	struct impl *impl = d;
-	spa_hook_remove(&impl->playback_listener);
-	impl->playback = NULL;
+	if (impl->playback != NULL) {
+		spa_hook_remove(&impl->playback_listener);
+		impl->playback = NULL;
+	}
 }
 
 static const struct pw_stream_events playback_events = {
@@ -657,9 +758,9 @@ static int setup_streams(struct impl *impl)
 {
 	int res;
 	uint32_t n_params, i;
-	const struct spa_pod *params[1];
-	uint8_t buffer[1024];
-	struct spa_pod_builder b;
+	uint32_t offsets[512];
+	const struct spa_pod *params[512];
+	struct spa_pod_dynamic_builder b;
 	struct pw_properties *props;
 	const char *str;
 	uint32_t index;
@@ -681,6 +782,8 @@ static int setup_streams(struct impl *impl)
 		pw_properties_set(props, SPA_KEY_AUDIO_CHANNELS, str);
 	if ((str = pw_properties_get(impl->source_props, SPA_KEY_AUDIO_POSITION)) != NULL)
 		pw_properties_set(props, SPA_KEY_AUDIO_POSITION, str);
+	if ((str = pw_properties_get(impl->source_props, "resample.prefill")) != NULL)
+		pw_properties_set(props, "resample.prefill", str);
 
 	impl->capture = pw_stream_new(impl->core,
 			"Echo-Cancel Capture", props);
@@ -701,32 +804,38 @@ static int setup_streams(struct impl *impl)
 			&impl->source_listener,
 			&source_events, impl);
 
-	props = pw_properties_new(
+	if (impl->monitor_mode) {
+		impl->playback = NULL;
+	} else {
+		props = pw_properties_new(
 			PW_KEY_NODE_NAME, "echo-cancel-playback",
 			PW_KEY_NODE_VIRTUAL, "true",
 			PW_KEY_NODE_PASSIVE, "true",
 			NULL);
-	if ((str = pw_properties_get(impl->sink_props, PW_KEY_NODE_GROUP)) != NULL)
-		pw_properties_set(props, PW_KEY_NODE_GROUP, str);
-	if ((str = pw_properties_get(impl->sink_props, PW_KEY_NODE_LINK_GROUP)) != NULL)
-		pw_properties_set(props, PW_KEY_NODE_LINK_GROUP, str);
-	if ((str = pw_properties_get(impl->sink_props, PW_KEY_NODE_LATENCY)) != NULL)
-		pw_properties_set(props, PW_KEY_NODE_LATENCY, str);
-	else if (impl->aec->latency)
-		pw_properties_set(props, PW_KEY_NODE_LATENCY, impl->aec->latency);
-	if ((str = pw_properties_get(impl->sink_props, SPA_KEY_AUDIO_CHANNELS)) != NULL)
-		pw_properties_set(props, SPA_KEY_AUDIO_CHANNELS, str);
-	if ((str = pw_properties_get(impl->sink_props, SPA_KEY_AUDIO_POSITION)) != NULL)
-		pw_properties_set(props, SPA_KEY_AUDIO_POSITION, str);
-
-	impl->playback = pw_stream_new(impl->core,
-			"Echo-Cancel Playback", props);
-	if (impl->playback == NULL)
-		return -errno;
-
-	pw_stream_add_listener(impl->playback,
-			&impl->playback_listener,
-			&playback_events, impl);
+		if ((str = pw_properties_get(impl->sink_props, PW_KEY_NODE_GROUP)) != NULL)
+			pw_properties_set(props, PW_KEY_NODE_GROUP, str);
+		if ((str = pw_properties_get(impl->sink_props, PW_KEY_NODE_LINK_GROUP)) != NULL)
+			pw_properties_set(props, PW_KEY_NODE_LINK_GROUP, str);
+		if ((str = pw_properties_get(impl->sink_props, PW_KEY_NODE_LATENCY)) != NULL)
+			pw_properties_set(props, PW_KEY_NODE_LATENCY, str);
+		else if (impl->aec->latency)
+			pw_properties_set(props, PW_KEY_NODE_LATENCY, impl->aec->latency);
+		if ((str = pw_properties_get(impl->sink_props, SPA_KEY_AUDIO_CHANNELS)) != NULL)
+			pw_properties_set(props, SPA_KEY_AUDIO_CHANNELS, str);
+		if ((str = pw_properties_get(impl->sink_props, SPA_KEY_AUDIO_POSITION)) != NULL)
+			pw_properties_set(props, SPA_KEY_AUDIO_POSITION, str);
+		if ((str = pw_properties_get(impl->sink_props, "resample.prefill")) != NULL)
+			pw_properties_set(props, "resample.prefill", str);
+
+		impl->playback = pw_stream_new(impl->core,
+				"Echo-Cancel Playback", props);
+		if (impl->playback == NULL)
+			return -errno;
+
+		pw_stream_add_listener(impl->playback,
+				&impl->playback_listener,
+				&playback_events, impl);
+	}
 
 	impl->sink = pw_stream_new(impl->core,
 			"Echo-Cancel Sink", impl->sink_props);
@@ -739,9 +848,23 @@ static int setup_streams(struct impl *impl)
 			&sink_events, impl);
 
 	n_params = 0;
-	spa_pod_builder_init(&b, buffer, sizeof(buffer));
-	params[n_params++] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat,
-			&impl->info);
+	spa_pod_dynamic_builder_init(&b, NULL, 0, 4096);
+
+	offsets[n_params++] = b.b.state.offset;
+	spa_format_audio_raw_build(&b.b, SPA_PARAM_EnumFormat, &impl->info);
+
+	int nbr_of_external_props = spa_audio_aec_enum_props(impl->aec, 0, NULL);
+	if (nbr_of_external_props > 0) {
+		for (int i = 0; i < nbr_of_external_props; i++) {
+			offsets[n_params++] = b.b.state.offset;
+			spa_audio_aec_enum_props(impl->aec, i, &b.b);
+		}
+		get_props_param(impl, &b.b);
+	}
+
+	for (i = 0; i < n_params; i++) {
+		params[i] = spa_pod_builder_deref(&b.b, offsets[i]);
+	}
 
 	if ((res = pw_stream_connect(impl->capture,
 			PW_DIRECTION_INPUT,
@@ -749,33 +872,44 @@ static int setup_streams(struct impl *impl)
 			PW_STREAM_FLAG_AUTOCONNECT |
 			PW_STREAM_FLAG_MAP_BUFFERS |
 			PW_STREAM_FLAG_RT_PROCESS,
-			params, n_params)) < 0)
+			params, n_params)) < 0) {
+		spa_pod_dynamic_builder_clean(&b);
 		return res;
+	}
 
 	if ((res = pw_stream_connect(impl->source,
 			PW_DIRECTION_OUTPUT,
 			PW_ID_ANY,
 			PW_STREAM_FLAG_MAP_BUFFERS |
 			PW_STREAM_FLAG_RT_PROCESS,
-			params, n_params)) < 0)
+			params, n_params)) < 0) {
+		spa_pod_dynamic_builder_clean(&b);
 		return res;
+	}
 
 	if ((res = pw_stream_connect(impl->sink,
 			PW_DIRECTION_INPUT,
 			PW_ID_ANY,
-			PW_STREAM_FLAG_MAP_BUFFERS |
-			PW_STREAM_FLAG_RT_PROCESS,
-			params, n_params)) < 0)
+			impl->playback != NULL ?
+				PW_STREAM_FLAG_MAP_BUFFERS | PW_STREAM_FLAG_RT_PROCESS :
+				PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS | PW_STREAM_FLAG_RT_PROCESS,
+			params, n_params)) < 0) {
+		spa_pod_dynamic_builder_clean(&b);
 		return res;
+	}
 
-	if ((res = pw_stream_connect(impl->playback,
+	if (impl->playback != NULL && (res = pw_stream_connect(impl->playback,
 			PW_DIRECTION_OUTPUT,
 			PW_ID_ANY,
 			PW_STREAM_FLAG_AUTOCONNECT |
 			PW_STREAM_FLAG_MAP_BUFFERS |
 			PW_STREAM_FLAG_RT_PROCESS,
-			params, n_params)) < 0)
+			params, n_params)) < 0) {
+		spa_pod_dynamic_builder_clean(&b);
 		return res;
+	}
+
+	spa_pod_dynamic_builder_clean(&b);
 
 	impl->rec_ringsize = sizeof(float) * impl->max_buffer_size * impl->info.rate / 1000;
 	impl->play_ringsize = sizeof(float) * ((impl->max_buffer_size * impl->info.rate / 1000) + impl->buffer_delay);
@@ -963,6 +1097,10 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
 		goto error;
 	}
 
+	impl->monitor_mode = false;
+	if ((str = pw_properties_get(props, "monitor.mode")) != NULL)
+		impl->monitor_mode = pw_properties_parse_bool(str);
+
 	impl->module = module;
 	impl->context = context;
 
@@ -986,13 +1124,23 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
 		pw_properties_set(impl->source_props, PW_KEY_NODE_DESCRIPTION, "Echo-Cancel Source");
 	if (pw_properties_get(impl->source_props, PW_KEY_MEDIA_CLASS) == NULL)
 		pw_properties_set(impl->source_props, PW_KEY_MEDIA_CLASS, "Audio/Source");
+	if (pw_properties_get(impl->source_props, "resample.prefill") == NULL)
+		pw_properties_set(impl->source_props, "resample.prefill", "true");
 
 	if (pw_properties_get(impl->sink_props, PW_KEY_NODE_NAME) == NULL)
 		pw_properties_set(impl->sink_props, PW_KEY_NODE_NAME, "echo-cancel-sink");
 	if (pw_properties_get(impl->sink_props, PW_KEY_NODE_DESCRIPTION) == NULL)
 		pw_properties_set(impl->sink_props, PW_KEY_NODE_DESCRIPTION, "Echo-Cancel Sink");
 	if (pw_properties_get(impl->sink_props, PW_KEY_MEDIA_CLASS) == NULL)
-		pw_properties_set(impl->sink_props, PW_KEY_MEDIA_CLASS, "Audio/Sink");
+		pw_properties_set(impl->sink_props, PW_KEY_MEDIA_CLASS,
+				impl->monitor_mode ? "Stream/Input/Audio" : "Audio/Sink");
+	if (pw_properties_get(impl->sink_props, "resample.prefill") == NULL)
+		pw_properties_set(impl->sink_props, "resample.prefill", "true");
+	if (impl->monitor_mode) {
+		pw_properties_set(impl->sink_props, PW_KEY_NODE_PASSIVE, "true");
+		pw_properties_set(impl->sink_props, PW_KEY_STREAM_MONITOR, "true");
+		pw_properties_set(impl->sink_props, PW_KEY_STREAM_CAPTURE_SINK, "true");
+	}
 
 	if ((str = pw_properties_get(props, "aec.method")) != NULL)
 		pw_log_warn("aec.method is not supported anymore use library.name");
diff --git a/src/modules/module-filter-chain.c b/src/modules/module-filter-chain.c
index bacedf6ef75f199973fb0bd4725cb3746367c217..a8f89e394caa8af107baae178a9bb5a91bb00baf 100644
--- a/src/modules/module-filter-chain.c
+++ b/src/modules/module-filter-chain.c
@@ -519,13 +519,13 @@ struct link {
 
 struct graph_port {
 	const struct fc_descriptor *desc;
-	void *hndl;
+	void **hndl;
 	uint32_t port;
 };
 
 struct graph_hndl {
 	const struct fc_descriptor *desc;
-	void *hndl;
+	void **hndl;
 };
 
 struct graph {
@@ -577,6 +577,10 @@ struct impl {
 	struct graph graph;
 };
 
+static int graph_instantiate(struct graph *graph);
+static void graph_cleanup(struct graph *graph);
+
+
 static void capture_destroy(void *d)
 {
 	struct impl *impl = d;
@@ -585,20 +589,26 @@ static void capture_destroy(void *d)
 }
 
 static void capture_process(void *d)
+{
+	struct impl *impl = d;
+	pw_stream_trigger_process(impl->playback);
+}
+
+static void playback_process(void *d)
 {
 	struct impl *impl = d;
 	struct pw_buffer *in, *out;
 	struct graph *graph = &impl->graph;
-	uint32_t i, outsize = 0, n_hndl = graph->n_hndl;
+	uint32_t i, insize = 0, 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");
+		pw_log_debug("%p: out of capture buffers: %m", impl);
 
 	if ((out = pw_stream_dequeue_buffer(impl->playback)) == NULL)
-		pw_log_debug("out of playback buffers: %m");
+		pw_log_debug("%p: out of playback buffers: %m", impl);
 
 	if (in == NULL || out == NULL)
 		goto done;
@@ -614,12 +624,14 @@ static void capture_process(void *d)
 		port = i < graph->n_input ? &graph->input[i] : NULL;
 
 		if (port && port->desc)
-			port->desc->connect_port(port->hndl, port->port,
+			port->desc->connect_port(*port->hndl, port->port,
 				SPA_PTROFF(bd->data, offs, void));
 
-		outsize = i == 0 ? size : SPA_MIN(outsize, size);
+		insize = i == 0 ? size : SPA_MIN(insize, size);
 		stride = SPA_MAX(stride, bd->chunk->stride);
 	}
+	outsize = insize;
+
 	for (i = 0; i < out->buffer->n_datas; i++) {
 		bd = &out->buffer->datas[i];
 
@@ -628,7 +640,7 @@ static void capture_process(void *d)
 		port = i < graph->n_output ? &graph->output[i] : NULL;
 
 		if (port && port->desc)
-			port->desc->connect_port(port->hndl, port->port, bd->data);
+			port->desc->connect_port(*port->hndl, port->port, bd->data);
 		else
 			memset(bd->data, 0, outsize);
 
@@ -636,9 +648,13 @@ static void capture_process(void *d)
 		bd->chunk->size = outsize;
 		bd->chunk->stride = stride;
 	}
+
+	pw_log_trace_fp("%p: stride:%d in:%d out:%d requested:%"PRIu64" (%"PRIu64")", impl,
+			stride, insize, outsize, out->requested, out->requested * stride);
+
 	for (i = 0; i < n_hndl; i++) {
 		struct graph_hndl *hndl = &graph->hndl[i];
-		hndl->desc->run(hndl->hndl, outsize / sizeof(float));
+		hndl->desc->run(*hndl->hndl, outsize / sizeof(float));
 	}
 
 done:
@@ -646,8 +662,6 @@ done:
 		pw_stream_queue_buffer(impl->capture, in);
 	if (out != NULL)
 		pw_stream_queue_buffer(impl->playback, out);
-
-	pw_stream_trigger_process(impl->playback);
 }
 
 static float get_default(struct impl *impl, struct descriptor *desc, uint32_t p)
@@ -746,11 +760,12 @@ static struct spa_pod *get_prop_info(struct graph *graph, struct spa_pod_builder
 	struct fc_port *p = &d->ports[port->p];
 	float def, min, max;
 	char name[512];
+	uint32_t rate = impl->rate ? impl->rate : 48000;
 
 	if (p->hint & FC_HINT_SAMPLE_RATE) {
-		def = p->def * impl->rate;
-		min = p->min * impl->rate;
-		max = p->max * impl->rate;
+		def = p->def * rate;
+		min = p->min * rate;
+		max = p->max * rate;
 	} else {
 		def = p->def;
 		min = p->min;
@@ -770,10 +785,10 @@ static struct spa_pod *get_prop_info(struct graph *graph, struct spa_pod_builder
 	spa_pod_builder_prop(b, SPA_PROP_INFO_type, 0);
 	if (p->hint & FC_HINT_BOOLEAN) {
 		if (min == max) {
-			spa_pod_builder_bool(b, def <= 0.0 ? false : true);
+			spa_pod_builder_bool(b, def <= 0.0f ? false : true);
 		} else  {
 			spa_pod_builder_push_choice(b, &f[1], SPA_CHOICE_Enum, 0);
-			spa_pod_builder_bool(b, def <= 0.0 ? false : true);
+			spa_pod_builder_bool(b, def <= 0.0f ? false : true);
 			spa_pod_builder_bool(b, false);
 			spa_pod_builder_bool(b, true);
 			spa_pod_builder_pop(b, &f[1]);
@@ -829,7 +844,7 @@ static struct spa_pod *get_props_param(struct graph *graph, struct spa_pod_build
 
 		spa_pod_builder_string(b, name);
 		if (p->hint & FC_HINT_BOOLEAN) {
-			spa_pod_builder_bool(b, port->control_data <= 0.0 ? false : true);
+			spa_pod_builder_bool(b, port->control_data <= 0.0f ? false : true);
 		} else if (p->hint & FC_HINT_INTEGER) {
 			spa_pod_builder_int(b, port->control_data);
 		} else {
@@ -908,13 +923,14 @@ static void graph_reset(struct graph *graph)
 	for (i = 0; i < graph->n_hndl; i++) {
 		struct graph_hndl *hndl = &graph->hndl[i];
 		const struct fc_descriptor *d = hndl->desc;
+		if (hndl->hndl == NULL || *hndl->hndl == NULL)
+			continue;
 		if (d->deactivate)
-			d->deactivate(hndl->hndl);
+			d->deactivate(*hndl->hndl);
 		if (d->activate)
-			d->activate(hndl->hndl);
+			d->activate(*hndl->hndl);
 	}
 }
-
 static void param_props_changed(struct impl *impl, const struct spa_pod *param)
 {
 	struct spa_pod_object *obj = (struct spa_pod_object *) param;
@@ -986,11 +1002,25 @@ static void param_changed(void *data, uint32_t id, const struct spa_pod *param)
 {
 	struct impl *impl = data;
 	struct graph *graph = &impl->graph;
+	int res;
 
 	switch (id) {
 	case SPA_PARAM_Format:
-		if (param == NULL)
-			graph_reset(graph);
+		if (param == NULL) {
+			graph_cleanup(graph);
+		} else {
+			struct spa_audio_info_raw info;
+			spa_zero(info);
+			if ((res = spa_format_audio_raw_parse(param, &info)) < 0)
+				goto error;
+			if (info.rate == 0) {
+				res = -EINVAL;
+				goto error;
+			}
+			impl->rate = info.rate;
+			if ((res = graph_instantiate(graph)) < 0)
+				goto error;
+		}
 		break;
 	case SPA_PARAM_Props:
 		if (param != NULL)
@@ -1000,6 +1030,11 @@ static void param_changed(void *data, uint32_t id, const struct spa_pod *param)
 		param_latency_changed(impl, param);
 		break;
 	}
+	return;
+
+error:
+	pw_stream_set_error(impl->capture, res, "can't start graph: %s",
+			spa_strerror(res));
 }
 
 static const struct pw_stream_events in_stream_events = {
@@ -1020,6 +1055,7 @@ static void playback_destroy(void *d)
 static const struct pw_stream_events out_stream_events = {
 	PW_VERSION_STREAM_EVENTS,
 	.destroy = playback_destroy,
+	.process = playback_process,
 	.state_changed = state_changed,
 	.param_changed = param_changed
 };
@@ -1148,16 +1184,19 @@ static struct plugin *plugin_load(struct impl *impl, const char *type, const cha
 	else if (spa_streq(type, "ladspa")) {
 		pl = load_ladspa_plugin(support, n_support, path, NULL);
 	}
-#ifdef HAVE_LILV
 	else if (spa_streq(type, "lv2")) {
+#ifdef HAVE_LILV
 		pl = load_lv2_plugin(support, n_support, path, NULL);
-	}
+#else
+		pw_log_error("filter-chain is compiled without lv2 support");
+		pl = NULL;
+		errno = ENOTSUP;
 #endif
-	else {
+	} else {
+		pw_log_error("invalid plugin type '%s'", type);
 		pl = NULL;
 		errno = EINVAL;
 	}
-
 	if (pl == NULL)
 		goto exit;
 
@@ -1514,10 +1553,8 @@ static int load_node(struct graph *graph, struct spa_json *json)
 			break;
 	}
 
-	if (spa_streq(type, "builtin")) {
+	if (spa_streq(type, "builtin"))
 		snprintf(plugin, sizeof(plugin), "%s", "builtin");
-	} else if (!spa_streq(type, "ladspa") && !spa_streq(type, "lv2"))
-		return -ENOTSUP;
 
 	pw_log_info("loading type:%s plugin:%s label:%s", type, plugin, label);
 
@@ -1537,6 +1574,10 @@ static int load_node(struct graph *graph, struct spa_json *json)
 	node->control_port = calloc(desc->n_control, sizeof(struct port));
 	node->notify_port = calloc(desc->n_notify, sizeof(struct port));
 
+	pw_log_info("loaded n_input:%d n_output:%d n_control:%d n_notify:%d",
+			desc->n_input, desc->n_output,
+			desc->n_control, desc->n_notify);
+
 	for (i = 0; i < desc->n_input; i++) {
 		struct port *port = &node->input_port[i];
 		port->node = node;
@@ -1581,21 +1622,51 @@ static int load_node(struct graph *graph, struct spa_json *json)
 	return 0;
 }
 
-static void node_free(struct node *node)
+static void node_cleanup(struct node *node)
 {
-	uint32_t i, j;
 	const struct fc_descriptor *d = node->desc->desc;
+	uint32_t i;
 
-	spa_list_remove(&node->link);
 	for (i = 0; i < node->n_hndl; i++) {
-		for (j = 0; j < node->desc->n_output; j++)
-			free(node->output_port[j].audio_data[i]);
 		if (node->hndl[i] == NULL)
 			continue;
 		if (d->deactivate)
 			d->deactivate(node->hndl[i]);
 		d->cleanup(node->hndl[i]);
+		node->hndl[i] = NULL;
+	}
+}
+
+static int port_ensure_data(struct port *port, uint32_t i)
+{
+	float *data;
+	if ((data = port->audio_data[i]) == NULL) {
+		data = calloc(1, MAX_SAMPLES * sizeof(float));
+		if (data == NULL) {
+			pw_log_error("cannot create port data: %m");
+			return -errno;
+		}
 	}
+	port->audio_data[i] = data;
+	return 0;
+}
+
+static void port_free_data(struct port *port, uint32_t i)
+{
+	free(port->audio_data[i]);
+	port->audio_data[i] = NULL;
+}
+
+static void node_free(struct node *node)
+{
+	uint32_t i, j;
+
+	spa_list_remove(&node->link);
+	for (i = 0; i < node->n_hndl; i++) {
+		for (j = 0; j < node->desc->n_output; j++)
+			port_free_data(&node->output_port[j], i);
+	}
+	node_cleanup(node);
 	descriptor_unref(node->desc);
 	free(node->input_port);
 	free(node->output_port);
@@ -1604,61 +1675,92 @@ static void node_free(struct node *node)
 	free(node);
 }
 
-static struct node *find_next_node(struct graph *graph)
+static void graph_cleanup(struct graph *graph)
 {
 	struct node *node;
-	spa_list_for_each(node, &graph->node_list, link) {
-		if (node->n_deps == 0 && !node->visited) {
-			node->visited = true;
-			return node;
-		}
-	}
-	return NULL;
+	spa_list_for_each(node, &graph->node_list, link)
+		node_cleanup(node);
 }
 
-static int setup_input_port(struct graph *graph, struct port *port)
+static int graph_instantiate(struct graph *graph)
 {
-	struct descriptor *desc = port->node->desc;
-	const struct fc_descriptor *d = desc->desc;
+	struct impl *impl = graph->impl;
+	struct node *node;
+	struct port *port;
 	struct link *link;
-	uint32_t i, n_hndl = port->node->n_hndl;
+	struct descriptor *desc;
+	const struct fc_descriptor *d;
+	uint32_t i, j;
+	int res;
 
-	spa_list_for_each(link, &port->link_list, input_link) {
-		struct port *peer = link->output;
-		for (i = 0; i < n_hndl; i++) {
-			pw_log_info("connect input port %s[%d]:%s %p",
-					port->node->name, i, d->ports[port->p].name,
-					peer->audio_data[i]);
-			d->connect_port(port->node->hndl[i], port->p, peer->audio_data[i]);
+	spa_list_for_each(node, &graph->node_list, link) {
+		float *sd = silence_data, *dd = discard_data;
+
+		node_cleanup(node);
+
+		desc = node->desc;
+		d = desc->desc;
+		if (d->flags & FC_DESCRIPTOR_SUPPORTS_NULL_DATA)
+			sd = dd = NULL;
+
+		for (i = 0; i < node->n_hndl; i++) {
+			pw_log_info("instantiate %s %d rate:%lu", d->name, i, impl->rate);
+			if ((node->hndl[i] = d->instantiate(d, impl->rate, i, node->config)) == NULL) {
+				pw_log_error("cannot create plugin instance: %m");
+				res = -errno;
+				goto error;
+			}
+			for (j = 0; j < desc->n_input; j++) {
+				port = &node->input_port[j];
+				d->connect_port(node->hndl[i], port->p, sd);
+
+				spa_list_for_each(link, &port->link_list, input_link) {
+					struct port *peer = link->output;
+					if ((res = port_ensure_data(peer, i)) < 0)
+						goto error;
+					pw_log_info("connect input port %s[%d]:%s %p",
+							node->name, i, d->ports[port->p].name,
+							peer->audio_data[i]);
+					d->connect_port(node->hndl[i], port->p, peer->audio_data[i]);
+				}
+			}
+			for (j = 0; j < desc->n_output; j++) {
+				port = &node->output_port[j];
+				if ((res = port_ensure_data(port, i)) < 0)
+					goto error;
+				pw_log_info("connect output port %s[%d]:%s %p",
+						node->name, i, d->ports[port->p].name,
+						port->audio_data[i]);
+				d->connect_port(node->hndl[i], port->p, port->audio_data[i]);
+			}
+			for (j = 0; j < desc->n_control; j++) {
+				port = &node->control_port[j];
+				d->connect_port(node->hndl[i], port->p, &port->control_data);
+			}
+			for (j = 0; j < desc->n_notify; j++) {
+				port = &node->notify_port[j];
+				d->connect_port(node->hndl[i], port->p, &port->control_data);
+			}
+			if (d->activate)
+				d->activate(node->hndl[i]);
 		}
 	}
 	return 0;
+error:
+	graph_cleanup(graph);
+	return res;
 }
 
-static int setup_output_port(struct graph *graph, struct port *port)
+static struct node *find_next_node(struct graph *graph)
 {
-	struct descriptor *desc = port->node->desc;
-	const struct fc_descriptor *d = desc->desc;
-	struct link *link;
-	uint32_t i, n_hndl = port->node->n_hndl;
-
-	spa_list_for_each(link, &port->link_list, output_link) {
-		for (i = 0; i < n_hndl; i++) {
-			float *data;
-			if ((data = port->audio_data[i]) == NULL) {
-				data = calloc(1, MAX_SAMPLES * sizeof(float));
-				if (data == NULL)
-					return -errno;
-			}
-			port->audio_data[i] = data;
-			pw_log_info("connect output port %s[%d]:%s %p",
-					port->node->name, i, d->ports[port->p].name,
-					port->audio_data[i]);
-			d->connect_port(port->node->hndl[i], port->p, data);
+	struct node *node;
+	spa_list_for_each(node, &graph->node_list, link) {
+		if (node->n_deps == 0 && !node->visited) {
+			node->visited = true;
+			return node;
 		}
-		link->input->node->n_deps--;
 	}
-	return 0;
+	return NULL;
 }
 
 static int setup_graph(struct graph *graph, struct spa_json *inputs, struct spa_json *outputs)
@@ -1666,11 +1768,11 @@ static int setup_graph(struct graph *graph, struct spa_json *inputs, struct spa_
 	struct impl *impl = graph->impl;
 	struct node *node, *first, *last;
 	struct port *port;
+	struct link *link;
 	struct graph_port *gp;
 	struct graph_hndl *gh;
 	uint32_t i, j, n_nodes, n_input, n_output, n_control, n_hndl = 0;
 	int res;
-	unsigned long p;
 	struct descriptor *desc;
 	const struct fc_descriptor *d;
 	char v[256];
@@ -1741,52 +1843,11 @@ static int setup_graph(struct graph *graph, struct spa_json *inputs, struct spa_
 	n_control = 0;
 	n_nodes = 0;
 	spa_list_for_each(node, &graph->node_list, link) {
-		float *sd = silence_data, *dd = discard_data;
-
+		node->n_hndl = n_hndl;
 		desc = node->desc;
-		d = desc->desc;
-		if (d->flags & FC_DESCRIPTOR_SUPPORTS_NULL_DATA)
-			sd = dd = NULL;
-
-		for (i = 0; i < n_hndl; i++) {
-			pw_log_info("instantiate %s %d", d->name, i);
-			if ((node->hndl[i] = d->instantiate(d, &impl->rate, i, node->config)) == NULL) {
-				pw_log_error("cannot create plugin instance: %m");
-				res = -errno;
-				goto error;
-			}
-			node->n_hndl = i + 1;
-
-			for (j = 0; j < desc->n_input; j++) {
-				p = desc->input[j];
-				d->connect_port(node->hndl[i], p, sd);
-			}
-			for (j = 0; j < desc->n_output; j++) {
-				p = desc->output[j];
-				d->connect_port(node->hndl[i], p, dd);
-			}
-			for (j = 0; j < desc->n_control; j++) {
-				port = &node->control_port[j];
-				d->connect_port(node->hndl[i], port->p, &port->control_data);
-			}
-			for (j = 0; j < desc->n_notify; j++) {
-				port = &node->notify_port[j];
-				d->connect_port(node->hndl[i], port->p, &port->control_data);
-			}
-			if (d->activate)
-				d->activate(node->hndl[i]);
-		}
 		n_control += desc->n_control;
 		n_nodes++;
 	}
-	pw_log_info("suggested rate:%lu capture:%d playback:%d", impl->rate,
-			impl->capture_info.rate, impl->playback_info.rate);
-
-	if (impl->capture_info.rate == 0)
-		impl->capture_info.rate = impl->rate;
-	if (impl->playback_info.rate == 0)
-		impl->playback_info.rate = impl->rate;
-
 	graph->n_input = 0;
 	graph->input = calloc(n_input * n_hndl, sizeof(struct graph_port));
 	graph->n_output = 0;
@@ -1802,7 +1863,7 @@ static int setup_graph(struct graph *graph, struct spa_json *inputs, struct spa_
 				pw_log_info("input port %s[%d]:%s",
 						first->name, i, d->ports[desc->input[j]].name);
 				gp->desc = d;
-				gp->hndl = first->hndl[i];
+				gp->hndl = &first->hndl[i];
 				gp->port = desc->input[j];
 			}
 		} else {
@@ -1836,7 +1897,7 @@ static int setup_graph(struct graph *graph, struct spa_json *inputs, struct spa_
 							port->node->name, i, d->ports[port->p].name);
 					port->external = graph->n_input;
 					gp->desc = d;
-					gp->hndl = port->node->hndl[i];
+					gp->hndl = &port->node->hndl[i];
 					gp->port = port->p;
 				}
 				graph->n_input++;
@@ -1850,7 +1911,7 @@ static int setup_graph(struct graph *graph, struct spa_json *inputs, struct spa_
 				pw_log_info("output port %s[%d]:%s",
 						last->name, i, d->ports[desc->output[j]].name);
 				gp->desc = d;
-				gp->hndl = last->hndl[i];
+				gp->hndl = &last->hndl[i];
 				gp->port = desc->output[j];
 			}
 		} else {
@@ -1884,7 +1945,7 @@ static int setup_graph(struct graph *graph, struct spa_json *inputs, struct spa_
 							port->node->name, i, d->ports[port->p].name);
 					port->external = graph->n_output;
 					gp->desc = d;
-					gp->hndl = port->node->hndl[i];
+					gp->hndl = &port->node->hndl[i];
 					gp->port = port->p;
 				}
 				graph->n_output++;
@@ -1904,17 +1965,16 @@ static int setup_graph(struct graph *graph, struct spa_json *inputs, struct spa_
 		desc = node->desc;
 		d = desc->desc;
 
-		for (i = 0; i < desc->n_input; i++)
-			setup_input_port(graph, &node->input_port[i]);
-
 		for (i = 0; i < n_hndl; i++) {
 			gh = &graph->hndl[graph->n_hndl++];
-			gh->hndl = node->hndl[i];
+			gh->hndl = &node->hndl[i];
 			gh->desc = d;
-		}
 
-		for (i = 0; i < desc->n_output; i++)
-			setup_output_port(graph, &node->output_port[i]);
+		}
+		for (i = 0; i < desc->n_output; i++) {
+			spa_list_for_each(link, &node->output_port[i].link_list, output_link)
+				link->input->node->n_deps--;
+		}
 
 		/* collect all control ports on the graph */
 		for (i = 0; i < desc->n_control; i++) {
@@ -1922,17 +1982,8 @@ static int setup_graph(struct graph *graph, struct spa_json *inputs, struct spa_
 			graph->n_control++;
 		}
 	}
-	return 0;
-
+	res = 0;
 error:
-	spa_list_for_each(node, &graph->node_list, link) {
-		for (i = 0; i < node->n_hndl; i++) {
-			if (node->hndl[i] != NULL)
-				node->desc->desc->cleanup(node->hndl[i]);
-			node->hndl[i] = NULL;
-		}
-		node->n_hndl = 0;
-	}
 	return res;
 }
 
@@ -2063,12 +2114,20 @@ static const struct pw_proxy_events core_proxy_events = {
 
 static void impl_destroy(struct impl *impl)
 {
+	/* disconnect both streams before destroying any of them */
+	if (impl->capture)
+		pw_stream_disconnect(impl->capture);
+	if (impl->playback)
+		pw_stream_disconnect(impl->playback);
+
 	if (impl->capture)
 		pw_stream_destroy(impl->capture);
 	if (impl->playback)
 		pw_stream_destroy(impl->playback);
+
 	if (impl->core && impl->do_disconnect)
 		pw_core_disconnect(impl->core);
+
 	pw_properties_free(impl->capture_props);
 	pw_properties_free(impl->playback_props);
 	graph_free(&impl->graph);
@@ -2178,7 +2237,6 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
 	impl->module = module;
 	impl->context = context;
 
-	impl->rate = 48000;
 	impl->graph.impl = impl;
 	spa_list_init(&impl->plugin_list);
 
@@ -2188,6 +2246,8 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
 		pw_properties_setf(props, PW_KEY_NODE_LINK_GROUP, "filter-chain-%u-%u", pid, id);
 	if (pw_properties_get(props, PW_KEY_NODE_VIRTUAL) == NULL)
 		pw_properties_set(props, PW_KEY_NODE_VIRTUAL, "true");
+	if (pw_properties_get(props, "resample.prefill") == NULL)
+		pw_properties_set(props, "resample.prefill", "true");
 	if (pw_properties_get(props, PW_KEY_NODE_DESCRIPTION) == NULL)
 		pw_properties_setf(props, PW_KEY_NODE_DESCRIPTION, "filter-chain-%u-%u", pid, id);
 
@@ -2205,6 +2265,7 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
 	copy_props(impl, props, PW_KEY_NODE_LATENCY);
 	copy_props(impl, props, PW_KEY_NODE_VIRTUAL);
 	copy_props(impl, props, PW_KEY_MEDIA_NAME);
+	copy_props(impl, props, "resample.prefill");
 
 	parse_audio_info(impl->capture_props, &impl->capture_info);
 	parse_audio_info(impl->playback_props, &impl->playback_info);
diff --git a/src/modules/module-filter-chain/biquad.c b/src/modules/module-filter-chain/biquad.c
index 296cdd8e4733bfbd086402a9b3cf448a4c9eb3f9..58c8dd0b721eee8921d2210654863c7eda997723 100644
--- a/src/modules/module-filter-chain/biquad.c
+++ b/src/modules/module-filter-chain/biquad.c
@@ -11,24 +11,6 @@
 #include <math.h>
 #include "biquad.h"
 
-#ifndef max
-#define max(a, b)                                                              \
-	({                                                                     \
-		__typeof__(a) _a = (a);                                        \
-		__typeof__(b) _b = (b);                                        \
-		_a > _b ? _a : _b;                                             \
-	})
-#endif
-
-#ifndef min
-#define min(a, b)                                                              \
-	({                                                                     \
-		__typeof__(a) _a = (a);                                        \
-		__typeof__(b) _b = (b);                                        \
-		_a < _b ? _a : _b;                                             \
-	})
-#endif
-
 #ifndef M_PI
 #define M_PI 3.14159265358979323846
 #endif
@@ -47,7 +29,7 @@ static void set_coefficient(struct biquad *bq, double b0, double b1, double b2,
 static void biquad_lowpass(struct biquad *bq, double cutoff, double resonance)
 {
 	/* Limit cutoff to 0 to 1. */
-	cutoff = max(0.0, min(cutoff, 1.0));
+	cutoff = fmax(0.0, fmin(cutoff, 1.0));
 
 	if (cutoff == 1 || cutoff == 0) {
 		/* When cutoff is 1, the z-transform is 1.
@@ -59,7 +41,7 @@ static void biquad_lowpass(struct biquad *bq, double cutoff, double resonance)
 	}
 
 	/* Compute biquad coefficients for lowpass filter */
-	resonance = max(0.0, resonance); /* can't go negative */
+	resonance = fmax(0.0, resonance); /* can't go negative */
 	double g = pow(10.0, 0.05 * resonance);
 	double d = sqrt((4 - sqrt(16 - 16 / (g * g))) / 2);
 
@@ -81,7 +63,7 @@ static void biquad_lowpass(struct biquad *bq, double cutoff, double resonance)
 static void biquad_highpass(struct biquad *bq, double cutoff, double resonance)
 {
 	/* Limit cutoff to 0 to 1. */
-	cutoff = max(0.0, min(cutoff, 1.0));
+	cutoff = fmax(0.0, fmin(cutoff, 1.0));
 
 	if (cutoff == 1 || cutoff == 0) {
 		/* When cutoff is one, the z-transform is 0. */
@@ -95,7 +77,7 @@ static void biquad_highpass(struct biquad *bq, double cutoff, double resonance)
 	}
 
 	/* Compute biquad coefficients for highpass filter */
-	resonance = max(0.0, resonance); /* can't go negative */
+	resonance = fmax(0.0, resonance); /* can't go negative */
 	double g = pow(10.0, 0.05 * resonance);
 	double d = sqrt((4 - sqrt(16 - 16 / (g * g))) / 2);
 
@@ -117,10 +99,10 @@ static void biquad_highpass(struct biquad *bq, double cutoff, double resonance)
 static void biquad_bandpass(struct biquad *bq, double frequency, double Q)
 {
 	/* No negative frequencies allowed. */
-	frequency = max(0.0, frequency);
+	frequency = fmax(0.0, frequency);
 
 	/* Don't let Q go negative, which causes an unstable filter. */
-	Q = max(0.0, Q);
+	Q = fmax(0.0, Q);
 
 	if (frequency <= 0 || frequency >= 1) {
 		/* When the cutoff is zero, the z-transform approaches 0, if Q
@@ -158,7 +140,7 @@ static void biquad_bandpass(struct biquad *bq, double frequency, double Q)
 static void biquad_lowshelf(struct biquad *bq, double frequency, double db_gain)
 {
 	/* Clip frequencies to between 0 and 1, inclusive. */
-	frequency = max(0.0, min(frequency, 1.0));
+	frequency = fmax(0.0, fmin(frequency, 1.0));
 
 	double A = pow(10.0, db_gain / 40);
 
@@ -195,7 +177,7 @@ static void biquad_highshelf(struct biquad *bq, double frequency,
 			     double db_gain)
 {
 	/* Clip frequencies to between 0 and 1, inclusive. */
-	frequency = max(0.0, min(frequency, 1.0));
+	frequency = fmax(0.0, fmin(frequency, 1.0));
 
 	double A = pow(10.0, db_gain / 40);
 
@@ -232,10 +214,10 @@ static void biquad_peaking(struct biquad *bq, double frequency, double Q,
 			   double db_gain)
 {
 	/* Clip frequencies to between 0 and 1, inclusive. */
-	frequency = max(0.0, min(frequency, 1.0));
+	frequency = fmax(0.0, fmin(frequency, 1.0));
 
 	/* Don't let Q go negative, which causes an unstable filter. */
-	Q = max(0.0, Q);
+	Q = fmax(0.0, Q);
 
 	double A = pow(10.0, db_gain / 40);
 
@@ -270,10 +252,10 @@ static void biquad_peaking(struct biquad *bq, double frequency, double Q,
 static void biquad_notch(struct biquad *bq, double frequency, double Q)
 {
 	/* Clip frequencies to between 0 and 1, inclusive. */
-	frequency = max(0.0, min(frequency, 1.0));
+	frequency = fmax(0.0, fmin(frequency, 1.0));
 
 	/* Don't let Q go negative, which causes an unstable filter. */
-	Q = max(0.0, Q);
+	Q = fmax(0.0, Q);
 
 	if (frequency <= 0 || frequency >= 1) {
 		/* When frequency is 0 or 1, the z-transform is 1. */
@@ -306,10 +288,10 @@ static void biquad_notch(struct biquad *bq, double frequency, double Q)
 static void biquad_allpass(struct biquad *bq, double frequency, double Q)
 {
 	/* Clip frequencies to between 0 and 1, inclusive. */
-	frequency = max(0.0, min(frequency, 1.0));
+	frequency = fmax(0.0, fmin(frequency, 1.0));
 
 	/* Don't let Q go negative, which causes an unstable filter. */
-	Q = max(0.0, Q);
+	Q = fmax(0.0, Q);
 
 	if (frequency <= 0 || frequency >= 1) {
 		/* When frequency is 0 or 1, the z-transform is 1. */
diff --git a/src/modules/module-filter-chain/builtin_plugin.c b/src/modules/module-filter-chain/builtin_plugin.c
index 00982567e0d9c8547f01064e8ea963468364fa4b..6918ad91d08f14a23da2f06a26b7da4f8786111c 100644
--- a/src/modules/module-filter-chain/builtin_plugin.c
+++ b/src/modules/module-filter-chain/builtin_plugin.c
@@ -40,6 +40,9 @@
 #include "biquad.h"
 #include "pffft.h"
 #include "convolver.h"
+#include "dsp-ops.h"
+
+static struct dsp_ops dsp_ops;
 
 struct builtin {
 	unsigned long rate;
@@ -52,7 +55,7 @@ struct builtin {
 };
 
 static void *builtin_instantiate(const struct fc_descriptor * Descriptor,
-		unsigned long *SampleRate, int index, const char *config)
+		unsigned long SampleRate, int index, const char *config)
 {
 	struct builtin *impl;
 
@@ -60,7 +63,7 @@ static void *builtin_instantiate(const struct fc_descriptor * Descriptor,
 	if (impl == NULL)
 		return NULL;
 
-	impl->rate = *SampleRate;
+	impl->rate = SampleRate;
 
 	return impl;
 }
@@ -82,7 +85,7 @@ static void copy_run(void * Instance, unsigned long SampleCount)
 {
 	struct builtin *impl = Instance;
 	float *in = impl->port[1], *out = impl->port[0];
-	memcpy(out, in, SampleCount * sizeof(float));
+	dsp_ops_copy(&dsp_ops, out, in, SampleCount);
 }
 
 static struct fc_port copy_ports[] = {
@@ -112,10 +115,10 @@ static const struct fc_descriptor copy_desc = {
 static void mixer_run(void * Instance, unsigned long SampleCount)
 {
 	struct builtin *impl = Instance;
-	int i;
-	unsigned long j;
+	int i, n_src = 0;
 	float *out = impl->port[0];
-	bool first = true;
+	const void *src[8];
+	float gains[8];
 
 	if (out == NULL)
 		return;
@@ -127,24 +130,10 @@ static void mixer_run(void * Instance, unsigned long SampleCount)
 		if (in == NULL || gain == 0.0f)
 			continue;
 
-		if (first) {
-			if (gain == 1.0f)
-				memcpy(out, in, SampleCount * sizeof(float));
-			else
-				for (j = 0; j < SampleCount; j++)
-					out[j] = in[j] * gain;
-			first = false;
-		} else {
-			if (gain == 1.0f)
-				for (j = 0; j < SampleCount; j++)
-					out[j] += in[j];
-			else
-				for (j = 0; j < SampleCount; j++)
-					out[j] += in[j] * gain;
-		}
+		src[n_src] = in;
+		gains[n_src++] = gain;
 	}
-	if (first)
-		memset(out, 0, SampleCount * sizeof(float));
+	dsp_ops_mix_gain(&dsp_ops, out, src, gains, n_src, SampleCount);
 }
 
 static struct fc_port mixer_ports[] = {
@@ -576,7 +565,7 @@ static float *create_dirac(const char *filename, float gain, int delay, int offs
 }
 
 static void * convolver_instantiate(const struct fc_descriptor * Descriptor,
-		unsigned long *SampleRate, int index, const char *config)
+		unsigned long SampleRate, int index, const char *config)
 {
 	struct convolver_impl *impl;
 	float *samples;
@@ -588,7 +577,9 @@ static void * convolver_instantiate(const struct fc_descriptor * Descriptor,
 	int blocksize = 0, tailsize = 0;
 	int delay = 0;
 	float gain = 1.0f;
+	unsigned long rate;
 
+	errno = EINVAL;
 	if (config == NULL)
 		return NULL;
 
@@ -598,42 +589,60 @@ static void * convolver_instantiate(const struct fc_descriptor * Descriptor,
 
 	while (spa_json_get_string(&it[1], key, sizeof(key)) > 0) {
 		if (spa_streq(key, "blocksize")) {
-			if (spa_json_get_int(&it[1], &blocksize) <= 0)
+			if (spa_json_get_int(&it[1], &blocksize) <= 0) {
+				pw_log_error("convolver:blocksize requires a number");
 				return NULL;
+			}
 		}
 		else if (spa_streq(key, "tailsize")) {
-			if (spa_json_get_int(&it[1], &tailsize) <= 0)
+			if (spa_json_get_int(&it[1], &tailsize) <= 0) {
+				pw_log_error("convolver:tailsize requires a number");
 				return NULL;
+			}
 		}
 		else if (spa_streq(key, "gain")) {
-			if (spa_json_get_float(&it[1], &gain) <= 0)
+			if (spa_json_get_float(&it[1], &gain) <= 0) {
+				pw_log_error("convolver:gain requires a number");
 				return NULL;
+			}
 		}
 		else if (spa_streq(key, "delay")) {
-			if (spa_json_get_int(&it[1], &delay) <= 0)
+			if (spa_json_get_int(&it[1], &delay) <= 0) {
+				pw_log_error("convolver:delay requires a number");
 				return NULL;
+			}
 		}
 		else if (spa_streq(key, "filename")) {
-			if (spa_json_get_string(&it[1], filename, sizeof(filename)) <= 0)
+			if (spa_json_get_string(&it[1], filename, sizeof(filename)) <= 0) {
+				pw_log_error("convolver:filename requires a string");
 				return NULL;
+			}
 		}
 		else if (spa_streq(key, "offset")) {
-			if (spa_json_get_int(&it[1], &offset) <= 0)
+			if (spa_json_get_int(&it[1], &offset) <= 0) {
+				pw_log_error("convolver:offset requires a number");
 				return NULL;
+			}
 		}
 		else if (spa_streq(key, "length")) {
-			if (spa_json_get_int(&it[1], &length) <= 0)
+			if (spa_json_get_int(&it[1], &length) <= 0) {
+				pw_log_error("convolver:length requires a number");
 				return NULL;
+			}
 		}
 		else if (spa_streq(key, "channel")) {
-			if (spa_json_get_int(&it[1], &channel) <= 0)
+			if (spa_json_get_int(&it[1], &channel) <= 0) {
+				pw_log_error("convolver:channel requires a number");
 				return NULL;
+			}
 		}
 		else if (spa_json_next(&it[1], &val) < 0)
 			break;
 	}
-	if (!filename[0])
+	if (!filename[0]) {
+		pw_log_error("convolver:filename was not given");
 		return NULL;
+	}
 
 	if (delay < 0)
 		delay = 0;
@@ -647,24 +656,32 @@ static void * convolver_instantiate(const struct fc_descriptor * Descriptor,
 		samples = create_dirac(filename, gain, delay, offset,
 				length, &n_samples);
 	} else {
+		rate = SampleRate;
 		samples = read_samples(filename, gain, delay, offset,
-				length, channel, SampleRate, &n_samples);
+				length, channel, &rate, &n_samples);
+		if (rate != SampleRate) {
+			pw_log_warn("Convolver samplerate %lu doesn't match filter rate %lu. "
+					"Consider forcing a filter rate.", rate, SampleRate);
+		}
 	}
-	if (samples == NULL)
+	if (samples == NULL) {
+		errno = ENOENT;
 		return NULL;
+	}
 
 	if (blocksize <= 0)
 		blocksize = SPA_CLAMP(n_samples, 64, 256);
 	if (tailsize <= 0)
-		tailsize = SPA_CLAMP(4096, blocksize, 4096);
+		tailsize = SPA_CLAMP(4096, blocksize, 32768);
 
-	pw_log_info("using %d:%d blocksize ir:%s", blocksize, tailsize, filename);
+	pw_log_info("using n_samples:%u %d:%d blocksize ir:%s", n_samples,
+			blocksize, tailsize, filename);
 
 	impl = calloc(1, sizeof(*impl));
 	if (impl == NULL)
 		goto error;
 
-	impl->rate = *SampleRate;
+	impl->rate = SampleRate;
 
 	impl->conv = convolver_new(blocksize, tailsize, samples, n_samples);
 	if (impl->conv == NULL)
@@ -750,7 +767,7 @@ static void delay_cleanup(void * Instance)
 }
 
 static void *delay_instantiate(const struct fc_descriptor * Descriptor,
-		unsigned long *SampleRate, int index, const char *config)
+		unsigned long SampleRate, int index, const char *config)
 {
 	struct delay_impl *impl;
 	struct spa_json it[2];
@@ -769,8 +786,10 @@ static void *delay_instantiate(const struct fc_descriptor * Descriptor,
 
 	while (spa_json_get_string(&it[1], key, sizeof(key)) > 0) {
 		if (spa_streq(key, "max-delay")) {
-			if (spa_json_get_float(&it[1], &max_delay) <= 0)
+			if (spa_json_get_float(&it[1], &max_delay) <= 0) {
+				pw_log_error("delay:max-delay requires a number");
 				return NULL;
+			}
 		}
 		else if (spa_json_next(&it[1], &val) < 0)
 			break;
@@ -782,9 +801,9 @@ static void *delay_instantiate(const struct fc_descriptor * Descriptor,
 	if (impl == NULL)
 		return NULL;
 
-	impl->rate = *SampleRate;
+	impl->rate = SampleRate;
 	impl->buffer_samples = max_delay * impl->rate;
-	pw_log_info("%lu %d", impl->rate, impl->buffer_samples);
+	pw_log_info("max-delay:%f seconds rate:%lu samples:%d", max_delay, impl->rate, impl->buffer_samples);
 
 	impl->buffer = calloc(impl->buffer_samples, sizeof(float));
 	if (impl->buffer == NULL) {
@@ -911,7 +930,11 @@ struct fc_plugin *load_builtin_plugin(const struct spa_support *support, uint32_
 		const char *plugin, const char *config)
 {
 	struct spa_cpu *cpu_iface;
+	uint32_t cpu_flags;
 	cpu_iface = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_CPU);
-	pffft_select_cpu(cpu_iface ? spa_cpu_get_flags(cpu_iface) : 0);
+	cpu_flags = cpu_iface ? spa_cpu_get_flags(cpu_iface) : 0;
+	dsp_ops.cpu_flags = cpu_flags;
+	dsp_ops_init(&dsp_ops);
+	pffft_select_cpu(cpu_flags);
 	return &builtin_plugin;
 }
diff --git a/src/modules/module-filter-chain/convolver.c b/src/modules/module-filter-chain/convolver.c
index dcf54d03398bfd92ba3411660b3ff63169c25f36..fff4fbe179098ea755099514f42c302de31ace6c 100644
--- a/src/modules/module-filter-chain/convolver.c
+++ b/src/modules/module-filter-chain/convolver.c
@@ -251,7 +251,7 @@ static int convolver1_run(struct convolver1 *conv, const float *input, float *ou
 {
 	int i, processed = 0;
 
-	if (conv->segCount == 0) {
+	if (conv == NULL || conv->segCount == 0) {
 		fft_clear(output, len);
 		return len;
 	}
diff --git a/src/modules/module-filter-chain/dsp-ops-c.c b/src/modules/module-filter-chain/dsp-ops-c.c
new file mode 100644
index 0000000000000000000000000000000000000000..885c8cd38135995d91cb31eed940c6436263e689
--- /dev/null
+++ b/src/modules/module-filter-chain/dsp-ops-c.c
@@ -0,0 +1,100 @@
+/* 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 <string.h>
+#include <stdio.h>
+#include <math.h>
+
+#include <spa/utils/defs.h>
+
+#include "dsp-ops.h"
+
+static inline void dsp_clear_c(struct dsp_ops *ops, void * SPA_RESTRICT dst, uint32_t n_samples)
+{
+	memset(dst, 0, sizeof(float) * n_samples);
+}
+
+static inline void dsp_add_c(struct dsp_ops *ops, void * SPA_RESTRICT dst,
+			const void * SPA_RESTRICT src, uint32_t n_samples)
+{
+	uint32_t i;
+	const float *s = src;
+	float *d = dst;
+	for (i = 0; i < n_samples; i++)
+		d[i] += s[i];
+}
+
+static inline void dsp_gain_c(struct dsp_ops *ops, void * SPA_RESTRICT dst,
+			const void * SPA_RESTRICT src, float gain, uint32_t n_samples)
+{
+	uint32_t i;
+	const float *s = src;
+	float *d = dst;
+	for (i = 0; i < n_samples; i++)
+		d[i] = s[i] * gain;
+}
+
+static inline void dsp_gain_add_c(struct dsp_ops *ops, void * SPA_RESTRICT dst,
+			const void * SPA_RESTRICT src, float gain, uint32_t n_samples)
+{
+	uint32_t i;
+	const float *s = src;
+	float *d = dst;
+	for (i = 0; i < n_samples; i++)
+		d[i] += s[i] * gain;
+}
+
+
+void dsp_copy_c(struct dsp_ops *ops, void * SPA_RESTRICT dst,
+			const void * SPA_RESTRICT src, uint32_t n_samples)
+{
+	if (dst != src)
+		spa_memcpy(dst, src, sizeof(float) * n_samples);
+}
+
+void dsp_mix_gain_c(struct dsp_ops *ops,
+		void * SPA_RESTRICT dst,
+		const void * SPA_RESTRICT src[],
+		float gain[], uint32_t n_src, uint32_t n_samples)
+{
+	uint32_t i;
+	if (n_src == 0) {
+		dsp_clear_c(ops, dst, n_samples);
+	} else if (n_src == 1) {
+		if (dst != src[0])
+			dsp_copy_c(ops, dst, src[0], n_samples);
+	} else {
+		if (gain[0] == 1.0f)
+			dsp_copy_c(ops, dst, src[0], n_samples);
+		else
+			dsp_gain_c(ops, dst, src[0], gain[0], n_samples);
+
+		for (i = 1; i < n_src; i++) {
+			if (gain[i] == 1.0f)
+				dsp_add_c(ops, dst, src[i], n_samples);
+			else
+				dsp_gain_add_c(ops, dst, src[i], gain[i], n_samples);
+		}
+	}
+}
diff --git a/src/modules/module-filter-chain/dsp-ops-sse.c b/src/modules/module-filter-chain/dsp-ops-sse.c
new file mode 100644
index 0000000000000000000000000000000000000000..cabcae3405d7cf2c73cedcfbd0097e4c32df4b8a
--- /dev/null
+++ b/src/modules/module-filter-chain/dsp-ops-sse.c
@@ -0,0 +1,91 @@
+/* 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 <string.h>
+#include <stdio.h>
+#include <math.h>
+
+#include <spa/utils/defs.h>
+
+#include "dsp-ops.h"
+
+#include <xmmintrin.h>
+
+void dsp_mix_gain_sse(struct dsp_ops *ops,
+		void * SPA_RESTRICT dst,
+		const void * SPA_RESTRICT src[],
+		float gain[], uint32_t n_src, uint32_t n_samples)
+{
+	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], g;
+		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;
+
+		for (n = 0; n < unrolled; n += 16) {
+			g = _mm_set1_ps(gain[0]);
+			in[0] = _mm_mul_ps(g, _mm_load_ps(&s[0][n+ 0]));
+			in[1] = _mm_mul_ps(g, _mm_load_ps(&s[0][n+ 4]));
+			in[2] = _mm_mul_ps(g, _mm_load_ps(&s[0][n+ 8]));
+			in[3] = _mm_mul_ps(g, _mm_load_ps(&s[0][n+12]));
+
+			for (i = 1; i < n_src; i++) {
+				g = _mm_set1_ps(gain[i]);
+				in[0] = _mm_add_ps(in[0], _mm_mul_ps(g, _mm_load_ps(&s[i][n+ 0])));
+				in[1] = _mm_add_ps(in[1], _mm_mul_ps(g, _mm_load_ps(&s[i][n+ 4])));
+				in[2] = _mm_add_ps(in[2], _mm_mul_ps(g, _mm_load_ps(&s[i][n+ 8])));
+				in[3] = _mm_add_ps(in[3], _mm_mul_ps(g, _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++) {
+			g = _mm_set_ss(gain[0]);
+			in[0] = _mm_mul_ss(g, _mm_load_ss(&s[0][n]));
+			for (i = 1; i < n_src; i++) {
+				g = _mm_set_ss(gain[i]);
+				in[0] = _mm_add_ss(in[0], _mm_mul_ss(g, _mm_load_ss(&s[i][n])));
+			}
+			_mm_store_ss(&d[n], in[0]);
+		}
+	}
+}
diff --git a/src/modules/module-filter-chain/dsp-ops.c b/src/modules/module-filter-chain/dsp-ops.c
new file mode 100644
index 0000000000000000000000000000000000000000..b40278bf644d27919aed280914407df6ec6c4bff
--- /dev/null
+++ b/src/modules/module-filter-chain/dsp-ops.c
@@ -0,0 +1,92 @@
+/* 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 <string.h>
+#include <stdio.h>
+#include <math.h>
+
+#include <spa/support/cpu.h>
+#include <spa/utils/defs.h>
+#include <spa/param/audio/format-utils.h>
+
+#include "dsp-ops.h"
+
+struct dsp_info {
+	uint32_t cpu_flags;
+
+	void (*copy) (struct dsp_ops *ops,
+			void * SPA_RESTRICT dst,
+			const void * SPA_RESTRICT src, uint32_t n_samples);
+	void (*mix_gain) (struct dsp_ops *ops,
+			void * SPA_RESTRICT dst,
+			const void * SPA_RESTRICT src[],
+			float gain[], uint32_t n_src, uint32_t n_samples);
+};
+
+static struct dsp_info dsp_table[] =
+{
+#if defined (HAVE_SSE)
+	{ SPA_CPU_FLAG_SSE,
+		.copy = dsp_copy_c,
+		.mix_gain = dsp_mix_gain_sse,
+	},
+#endif
+	{ 0,
+		.copy = dsp_copy_c,
+		.mix_gain = dsp_mix_gain_c,
+	},
+};
+
+#define MATCH_CPU_FLAGS(a,b)	((a) == 0 || ((a) & (b)) == a)
+
+static const struct dsp_info *find_dsp_info(uint32_t cpu_flags)
+{
+	SPA_FOR_EACH_ELEMENT_VAR(dsp_table, t) {
+		if (MATCH_CPU_FLAGS(t->cpu_flags, cpu_flags))
+			return t;
+	}
+	return NULL;
+}
+
+static void impl_dsp_ops_free(struct dsp_ops *ops)
+{
+	spa_zero(*ops);
+}
+
+int dsp_ops_init(struct dsp_ops *ops)
+{
+	const struct dsp_info *info;
+
+	info = find_dsp_info(ops->cpu_flags);
+	if (info == NULL)
+		return -ENOTSUP;
+
+	ops->priv = info;
+	ops->cpu_flags = info->cpu_flags;
+	ops->copy = info->copy;
+	ops->mix_gain = info->mix_gain;
+	ops->free = impl_dsp_ops_free;
+
+	return 0;
+}
diff --git a/src/modules/module-filter-chain/dsp-ops.h b/src/modules/module-filter-chain/dsp-ops.h
new file mode 100644
index 0000000000000000000000000000000000000000..ffbca6ce7a4d549b1c9d40652a695d8a0fc946c5
--- /dev/null
+++ b/src/modules/module-filter-chain/dsp-ops.h
@@ -0,0 +1,62 @@
+/* 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 <spa/utils/defs.h>
+
+struct dsp_ops {
+	uint32_t cpu_flags;
+
+	void (*clear) (struct dsp_ops *ops, void * SPA_RESTRICT dst, uint32_t n_samples);
+	void (*copy) (struct dsp_ops *ops,
+			void * SPA_RESTRICT dst,
+			const void * SPA_RESTRICT src, uint32_t n_samples);
+	void (*mix_gain) (struct dsp_ops *ops,
+			void * SPA_RESTRICT dst,
+			const void * SPA_RESTRICT src[],
+			float gain[], uint32_t n_src, uint32_t n_samples);
+	void (*free) (struct dsp_ops *ops);
+
+	const void *priv;
+};
+
+int dsp_ops_init(struct dsp_ops *ops);
+
+#define dsp_ops_copy(ops,...)		(ops)->copy(ops, __VA_ARGS__)
+#define dsp_ops_mix_gain(ops,...)	(ops)->mix_gain(ops, __VA_ARGS__)
+#define dsp_ops_free(ops)		(ops)->free(ops)
+
+
+#define MAKE_COPY_FUNC(arch) \
+void dsp_copy_##arch(struct dsp_ops *ops, void * SPA_RESTRICT dst, \
+			const void * SPA_RESTRICT src, uint32_t n_samples)
+#define MAKE_MIX_GAIN_FUNC(arch) \
+void dsp_mix_gain_##arch(struct dsp_ops *ops, void * SPA_RESTRICT dst,	\
+	const void * SPA_RESTRICT src[], float gain[], uint32_t n_src, uint32_t n_samples)
+
+
+MAKE_COPY_FUNC(c);
+MAKE_MIX_GAIN_FUNC(c);
+#if defined (HAVE_SSE)
+MAKE_MIX_GAIN_FUNC(sse);
+#endif
diff --git a/src/modules/module-filter-chain/ladspa_plugin.c b/src/modules/module-filter-chain/ladspa_plugin.c
index 591002bea33da82d339b58661775a807b33a8649..172fc2523eab5cc1cfaa83314a7de6ab5fd3e42c 100644
--- a/src/modules/module-filter-chain/ladspa_plugin.c
+++ b/src/modules/module-filter-chain/ladspa_plugin.c
@@ -22,6 +22,8 @@
  * DEALINGS IN THE SOFTWARE.
  */
 
+#include "config.h"
+
 #include <dlfcn.h>
 #include <math.h>
 
@@ -47,10 +49,10 @@ struct descriptor {
 };
 
 static void *ladspa_instantiate(const struct fc_descriptor *desc,
-                        unsigned long *SampleRate, int index, const char *config)
+                        unsigned long SampleRate, int index, const char *config)
 {
 	struct descriptor *d = (struct descriptor *)desc;
-	return d->d->instantiate(d->d, *SampleRate);
+	return d->d->instantiate(d->d, SampleRate);
 }
 
 static const LADSPA_Descriptor *find_desc(LADSPA_Descriptor_Function desc_func, const char *name)
@@ -80,39 +82,39 @@ static float get_default(struct fc_port *port, LADSPA_PortRangeHintDescriptor hi
 		break;
 	case LADSPA_HINT_DEFAULT_LOW:
 		if (LADSPA_IS_HINT_LOGARITHMIC(hint))
-			def = (LADSPA_Data) exp(log(lower) * 0.75 + log(upper) * 0.25);
+			def = (LADSPA_Data) expf(logf(lower) * 0.75f + logf(upper) * 0.25f);
 		else
-			def = (LADSPA_Data) (lower * 0.75 + upper * 0.25);
+			def = (LADSPA_Data) (lower * 0.75f + upper * 0.25f);
 		break;
 	case LADSPA_HINT_DEFAULT_MIDDLE:
 		if (LADSPA_IS_HINT_LOGARITHMIC(hint))
-			def = (LADSPA_Data) exp(log(lower) * 0.5 + log(upper) * 0.5);
+			def = (LADSPA_Data) expf(logf(lower) * 0.5f + logf(upper) * 0.5f);
 		else
-			def = (LADSPA_Data) (lower * 0.5 + upper * 0.5);
+			def = (LADSPA_Data) (lower * 0.5f + upper * 0.5f);
 		break;
 	case LADSPA_HINT_DEFAULT_HIGH:
 		if (LADSPA_IS_HINT_LOGARITHMIC(hint))
-			def = (LADSPA_Data) exp(log(lower) * 0.25 + log(upper) * 0.75);
+			def = (LADSPA_Data) expf(logf(lower) * 0.25f + logf(upper) * 0.75f);
 		else
-			def = (LADSPA_Data) (lower * 0.25 + upper * 0.75);
+			def = (LADSPA_Data) (lower * 0.25f + upper * 0.75f);
 		break;
 	case LADSPA_HINT_DEFAULT_0:
-		def = 0;
+		def = 0.0f;
 		break;
 	case LADSPA_HINT_DEFAULT_1:
-		def = 1;
+		def = 1.0f;
 		break;
 	case LADSPA_HINT_DEFAULT_100:
-		def = 100;
+		def = 100.0f;
 		break;
 	case LADSPA_HINT_DEFAULT_440:
-		def = 440;
+		def = 440.0f;
 		break;
 	default:
 		if (upper == lower)
 			def = upper;
 		else
-			def = SPA_CLAMP(0.5 * upper, lower, upper);
+			def = SPA_CLAMPF(0.5f * upper, lower, upper);
 		break;
 	}
 	if (LADSPA_IS_HINT_INTEGER(hint))
@@ -238,7 +240,7 @@ struct fc_plugin *load_ladspa_plugin(const struct spa_support *support, uint32_t
 
 		search_dirs = getenv("LADSPA_PATH");
 		if (!search_dirs)
-			search_dirs = "/usr/lib64/ladspa";
+			search_dirs = "/usr/lib64/ladspa:/usr/lib/ladspa:" LIBDIR;
 
 		/*
 		 * set the errno for the case when `ladspa_handle_load_by_path()`
diff --git a/src/modules/module-filter-chain/lv2_plugin.c b/src/modules/module-filter-chain/lv2_plugin.c
index fc4f274bc180ad1b6115364f808e1a6c08a32976..e607e2f1e265291b05e5942cc3c350866ddefc29 100644
--- a/src/modules/module-filter-chain/lv2_plugin.c
+++ b/src/modules/module-filter-chain/lv2_plugin.c
@@ -298,7 +298,7 @@ work_schedule(LV2_Worker_Schedule_Handle handle, uint32_t size, const void *data
 }
 
 static void *lv2_instantiate(const struct fc_descriptor *desc,
-                        unsigned long *SampleRate, int index, const char *config)
+                        unsigned long SampleRate, int index, const char *config)
 {
 	struct descriptor *d = (struct descriptor*)desc;
 	struct plugin *p = d->p;
@@ -308,7 +308,7 @@ static void *lv2_instantiate(const struct fc_descriptor *desc,
 	static const int32_t min_block_length = 1;
 	static const int32_t max_block_length = 8192;
 	static const int32_t seq_size = 32768;
-	float fsample_rate = *SampleRate;
+	float fsample_rate = SampleRate;
 
 	i = calloc(1, sizeof(*i));
 	if (i == NULL)
@@ -350,7 +350,7 @@ static void *lv2_instantiate(const struct fc_descriptor *desc,
         i->options_feature.data = i->options;
         i->features[n_features++] = &i->options_feature;
 
-	i->instance = lilv_plugin_instantiate(p->p, *SampleRate, i->features);
+	i->instance = lilv_plugin_instantiate(p->p, SampleRate, i->features);
 	if (i->instance == NULL) {
 		free(i);
 		return NULL;
diff --git a/src/modules/module-filter-chain/pffft.c b/src/modules/module-filter-chain/pffft.c
index d89a709495969248a9051b6058511378589359e3..2f65e3fcd1f0925434ca23f9f18a320bf943153d 100644
--- a/src/modules/module-filter-chain/pffft.c
+++ b/src/modules/module-filter-chain/pffft.c
@@ -25,7 +25,7 @@
    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.  
+   specific prior written permission.
 
    - Redistributions of source code must retain the above copyright
    notices, this list of conditions, and the disclaimer below.
@@ -52,7 +52,7 @@
 */
 
 /*
-  ChangeLog: 
+  ChangeLog:
   - 2011/10/02, version 1: This is the very first release of this file.
 */
 
@@ -243,7 +243,7 @@ typedef union v4sf_union {
 #define assertv4(v,f0,f1,f2,f3) assert(v.f[0] == (f0) && v.f[1] == (f1) && v.f[2] == (f2) && v.f[3] == (f3))
 
 /* detect bugs with the vector support macros */
-static void validate_pffft_simd()
+static void validate_pffft_simd(void)
 {
 	float f[16] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 };
 	v4sf_union a0, a1, a2, a3, t, u;
@@ -301,7 +301,7 @@ static void validate_pffft_simd()
 	assertv4(a3, 3, 7, 11, 15);
 }
 #else
-static void validate_pffft_simd()
+static void validate_pffft_simd(void)
 {
 }				// allow test_pffft.c to call this function even when simd is not available..
 #endif				//!PFFFT_SIMD_DISABLE
@@ -1421,7 +1421,7 @@ static PFFFT_Setup *new_setup_simd(int N, pffft_transform_t transform)
 {
 	PFFFT_Setup *s = (PFFFT_Setup *) malloc(sizeof(PFFFT_Setup));
 	int k, m;
-	/* unfortunately, the fft size must be a multiple of 16 for complex FFTs 
+	/* unfortunately, the fft size must be a multiple of 16 for complex FFTs
 	   and 32 for real FFTs -- a lot of stuff would need to be rewritten to
 	   handle other cases (or maybe just switch to a scalar fft, I don't know..) */
 	if (transform == PFFFT_REAL) {
@@ -1615,7 +1615,7 @@ static void pffft_cplx_finalize(int Ncvec, const v4sf * in, v4sf * out, const v4
 		   [0   0   0   0   1   1   1   1] * [i0]
 		   [0   1   0  -1   1   0  -1   0]   [i1]
 		   [0   0   0   0   1  -1   1  -1]   [i2]
-		   [0  -1   0   1   1   0  -1   0]   [i3]    
+		   [0  -1   0   1   1   0  -1   0]   [i3]
 		 */
 
 		r0 = VADD(sr0, sr1);
@@ -1719,7 +1719,7 @@ static ALWAYS_INLINE(void) pffft_real_finalize_4x4(const v4sf * in0,
 	   [0   0   0   0   1   1   1   1] * [i0]
 	   [0  -1   0   1  -1   0   1   0]   [i1]
 	   [0  -1   0   1   1   0  -1   0]   [i2]
-	   [0   0   0   0  -1   1  -1   1]   [i3]    
+	   [0   0   0   0  -1   1  -1   1]   [i3]
 	 */
 
 	//cerr << "matrix initial, before e , REAL:\n 1: " << r0 << "\n 1: " << r1 << "\n 1: " << r2 << "\n 1: " << r3 << "\n";
@@ -1832,7 +1832,7 @@ static ALWAYS_INLINE(void) pffft_real_preprocess_4x4(const v4sf * in,
 	   [0   0   0   0   1  -1   1  -1] * [i0]
 	   [0  -1   1   0   1   0   0   1]   [i1]
 	   [0   0   0   0   1   1  -1  -1]   [i2]
-	   [0   1  -1   0   1   0   0   1]   [i3]    
+	   [0   1  -1   0   1   0   0   1]   [i3]
 	 */
 
 	v4sf sr0 = VADD(r0, r3), dr0 = VSUB(r0, r3);
diff --git a/src/modules/module-filter-chain/pffft.h b/src/modules/module-filter-chain/pffft.h
index 75e0d21cd6199eaa333b3b479d6529ea6f36935f..ca58676fb2a7a093aa0d3169d1f16f728e2ea6d7 100644
--- a/src/modules/module-filter-chain/pffft.h
+++ b/src/modules/module-filter-chain/pffft.h
@@ -165,13 +165,13 @@ extern "C" {
   /*
     the float buffers must have the correct alignment (16-byte boundary
     on intel and powerpc). This function may be used to obtain such
-    correctly aligned buffers. 
+    correctly aligned buffers.
   */
   void *pffft_aligned_malloc(size_t nb_bytes);
   void pffft_aligned_free(void *);
 
   /* return 4 or 1 depending on whether support for SSE/Altivec instructions was enabled when building pffft.c */
-  int pffft_simd_size();
+  int pffft_simd_size(void);
 
   void pffft_select_cpu(int flags);
 
diff --git a/src/modules/module-filter-chain/plugin.h b/src/modules/module-filter-chain/plugin.h
index e4f2eb5f1d6c1df78d53c68c17ea93dd0a2a3232..ce46b62ecf87bdaf2f442ad499728f2b37fb0b09 100644
--- a/src/modules/module-filter-chain/plugin.h
+++ b/src/modules/module-filter-chain/plugin.h
@@ -72,7 +72,7 @@ struct fc_descriptor {
 	struct fc_port *ports;
 
 	void *(*instantiate) (const struct fc_descriptor *desc,
-			unsigned long *SampleRate, int index, const char *config);
+			unsigned long SampleRate, int index, const char *config);
 
 	void (*cleanup) (void *instance);
 
diff --git a/src/modules/module-loopback.c b/src/modules/module-loopback.c
index 7cd07ded5bcbb991c4e21a3e0c197038824748c2..505ff1c1381a0352ed375feb968c4dbad69e8b4e 100644
--- a/src/modules/module-loopback.c
+++ b/src/modules/module-loopback.c
@@ -35,6 +35,7 @@
 #include <spa/utils/result.h>
 #include <spa/utils/string.h>
 #include <spa/utils/json.h>
+#include <spa/utils/ringbuffer.h>
 #include <spa/param/profiler.h>
 #include <spa/debug/pod.h>
 
@@ -54,6 +55,7 @@
  * ## Module Options
  *
  * - `node.description`: a human readable name for the loopback streams
+ * - `target.delay.sec`: delay in seconds as float (Since 0.3.60)
  * - `capture.props = {}`: properties to be passed to the input stream
  * - `playback.props = {}`: properties to be passed to the output stream
  *
@@ -91,6 +93,7 @@
  * {   name = libpipewire-module-loopback
  *     args = {
  *         node.description = "CM106 Stereo Pair 2"
+ *         #target.delay.sec = 1.5
  *         capture.props = {
  *             node.name = "CM106_stereo_pair_2"
  *             media.class = "Audio/Sink"
@@ -128,6 +131,7 @@ static const struct spa_dict_item module_props[] = {
 				"[ audio.rate=<sample rate> ] "
 				"[ audio.channels=<number of channels> ] "
 				"[ audio.position=<channel map> ] "
+				"[ target.delay.sec=<delay as seconds in float> ] "
 				"[ capture.props=<properties> ] "
 				"[ playback.props=<properties> ] " },
 	{ PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
@@ -160,13 +164,21 @@ struct impl {
 	struct pw_stream *capture;
 	struct spa_hook capture_listener;
 	struct spa_audio_info_raw capture_info;
+	struct spa_latency_info capture_latency;
 
 	struct pw_properties *playback_props;
 	struct pw_stream *playback;
 	struct spa_hook playback_listener;
 	struct spa_audio_info_raw playback_info;
+	struct spa_latency_info playback_latency;
 
 	unsigned int do_disconnect:1;
+	unsigned int recalc_delay:1;
+
+	float target_delay;
+	struct spa_ringbuffer buffer;
+	uint8_t *buffer_data;
+	uint32_t buffer_size;
 };
 
 static void capture_destroy(void *d)
@@ -176,12 +188,45 @@ static void capture_destroy(void *d)
 	impl->capture = NULL;
 }
 
+static void recalculate_delay(struct impl *impl)
+{
+	uint32_t target = impl->capture_info.rate * impl->target_delay, cdelay, pdelay;
+	uint32_t delay, w;
+	struct pw_time pwt;
+
+	pw_stream_get_time_n(impl->playback, &pwt, sizeof(pwt));
+	pdelay = pwt.delay;
+	pw_stream_get_time_n(impl->capture, &pwt, sizeof(pwt));
+	cdelay = pwt.delay;
+
+	delay = target - SPA_MIN(target, pdelay + cdelay);
+	delay = SPA_MIN(delay, impl->buffer_size / 4);
+
+	spa_ringbuffer_get_write_index(&impl->buffer, &w);
+	spa_ringbuffer_read_update(&impl->buffer, w - (delay * 4));
+
+	pw_log_info("target:%d c:%d + p:%d + delay:%d = (%d)",
+			target, cdelay, pdelay, delay,
+			cdelay + pdelay + delay);
+}
+
 static void capture_process(void *d)
+{
+	struct impl *impl = d;
+	pw_stream_trigger_process(impl->playback);
+}
+
+static void playback_process(void *d)
 {
 	struct impl *impl = d;
 	struct pw_buffer *in, *out;
 	uint32_t i;
 
+	if (impl->recalc_delay) {
+		recalculate_delay(impl);
+		impl->recalc_delay = false;
+	}
+
 	if ((in = pw_stream_dequeue_buffer(impl->capture)) == NULL)
 		pw_log_debug("out of capture buffers: %m");
 
@@ -193,6 +238,7 @@ static void capture_process(void *d)
 		int32_t stride = 0;
 		struct spa_data *d;
 		const void *src[in->buffer->n_datas];
+		uint32_t r, w, buffer_size;
 
 		for (i = 0; i < in->buffer->n_datas; i++) {
 			uint32_t offs, size;
@@ -205,13 +251,33 @@ static void capture_process(void *d)
 			outsize = SPA_MIN(outsize, size);
 			stride = SPA_MAX(stride, d->chunk->stride);
 		}
+		if (impl->buffer_size > 0) {
+			buffer_size = impl->buffer_size;
+			spa_ringbuffer_get_write_index(&impl->buffer, &w);
+			for (i = 0; i < in->buffer->n_datas; i++) {
+				void *buffer_data = &impl->buffer_data[i * buffer_size];
+				spa_ringbuffer_write_data(&impl->buffer,
+						buffer_data, buffer_size,
+						w % buffer_size, src[i], outsize);
+				src[i] = buffer_data;
+			}
+			w += outsize;
+			spa_ringbuffer_write_update(&impl->buffer, w);
+			spa_ringbuffer_get_read_index(&impl->buffer, &r);
+		} else {
+			r = 0;
+			buffer_size = outsize;
+		}
 		for (i = 0; i < out->buffer->n_datas; i++) {
 			d = &out->buffer->datas[i];
 
 			outsize = SPA_MIN(outsize, d->maxsize);
 
 			if (i < in->buffer->n_datas)
-				memcpy(d->data, src[i], outsize);
+				spa_ringbuffer_read_data(&impl->buffer,
+						src[i], buffer_size,
+						r % buffer_size,
+						d->data, outsize);
 			else
 				memset(d->data, 0, outsize);
 
@@ -219,18 +285,20 @@ static void capture_process(void *d)
 			d->chunk->size = outsize;
 			d->chunk->stride = stride;
 		}
+		if (impl->buffer_size > 0) {
+			r += outsize;
+			spa_ringbuffer_read_update(&impl->buffer, r);
+		}
 	}
 
 	if (in != NULL)
 		pw_stream_queue_buffer(impl->capture, in);
 	if (out != NULL)
 		pw_stream_queue_buffer(impl->playback, out);
-
-	pw_stream_trigger_process(impl->playback);
 }
 
 static void param_latency_changed(struct impl *impl, const struct spa_pod *param,
-		struct pw_stream *other)
+		struct spa_latency_info *info, struct pw_stream *other)
 {
 	struct spa_latency_info latency;
 	uint8_t buffer[1024];
@@ -240,9 +308,13 @@ static void param_latency_changed(struct impl *impl, const struct spa_pod *param
 	if (spa_latency_parse(param, &latency) < 0)
 		return;
 
+	*info = latency;
+
 	spa_pod_builder_init(&b, buffer, sizeof(buffer));
 	params[0] = spa_latency_build(&b, SPA_PARAM_Latency, &latency);
 	pw_stream_update_params(other, params, 1);
+
+	impl->recalc_delay = true;
 }
 
 static void stream_state_changed(void *data, enum pw_stream_state old,
@@ -253,6 +325,7 @@ static void stream_state_changed(void *data, enum pw_stream_state old,
 	case PW_STREAM_STATE_PAUSED:
 		pw_stream_flush(impl->playback, false);
 		pw_stream_flush(impl->capture, false);
+		impl->recalc_delay = true;
 		break;
 	case PW_STREAM_STATE_UNCONNECTED:
 		pw_log_info("module %p: unconnected", impl);
@@ -266,13 +339,53 @@ static void stream_state_changed(void *data, enum pw_stream_state old,
 	}
 }
 
+static void recalculate_buffer(struct impl *impl)
+{
+	if (impl->target_delay > 0.0f) {
+		uint32_t delay = impl->capture_info.rate * impl->target_delay;
+		void *data;
+
+		impl->buffer_size = (delay + (1u<<15)) * 4;
+		data = realloc(impl->buffer_data, impl->buffer_size * impl->capture_info.channels);
+		if (data == NULL) {
+			pw_log_warn("can't allocate delay buffer, delay disabled: %m");
+			impl->buffer_size = 0;
+			free(impl->buffer_data);
+		}
+		impl->buffer_data = data;
+		spa_ringbuffer_init(&impl->buffer);
+	} else {
+		impl->buffer_size = 0;
+		free(impl->buffer_data);
+		impl->buffer_data = NULL;
+	}
+	pw_log_info("configured delay:%f buffer:%d", impl->target_delay, impl->buffer_size);
+	impl->recalc_delay = true;
+}
+
 static void capture_param_changed(void *data, uint32_t id, const struct spa_pod *param)
 {
 	struct impl *impl = data;
 
 	switch (id) {
+	case SPA_PARAM_Format:
+	{
+		struct spa_audio_info_raw info;
+		if (param == NULL)
+			return;
+		if (spa_format_audio_raw_parse(param, &info) < 0)
+			return;
+		if (info.rate == 0 ||
+		    info.channels == 0 ||
+		    info.channels > SPA_AUDIO_MAX_CHANNELS)
+			return;
+
+		impl->capture_info = info;
+		recalculate_buffer(impl);
+		break;
+	}
 	case SPA_PARAM_Latency:
-		param_latency_changed(impl, param, impl->playback);
+		param_latency_changed(impl, param, &impl->capture_latency, impl->playback);
 		break;
 	}
 }
@@ -298,13 +411,14 @@ static void playback_param_changed(void *data, uint32_t id, const struct spa_pod
 
 	switch (id) {
 	case SPA_PARAM_Latency:
-		param_latency_changed(impl, param, impl->capture);
+		param_latency_changed(impl, param, &impl->playback_latency, impl->capture);
 		break;
 	}
 }
 static const struct pw_stream_events out_stream_events = {
 	PW_VERSION_STREAM_EVENTS,
 	.destroy = playback_destroy,
+	.process = playback_process,
 	.state_changed = stream_state_changed,
 	.param_changed = playback_param_changed,
 };
@@ -399,12 +513,20 @@ static const struct pw_proxy_events core_proxy_events = {
 
 static void impl_destroy(struct impl *impl)
 {
+	/* disconnect both streams before destroying any of them */
+	if (impl->capture)
+		pw_stream_disconnect(impl->capture);
+	if (impl->playback)
+		pw_stream_disconnect(impl->playback);
+
 	if (impl->capture)
 		pw_stream_destroy(impl->capture);
 	if (impl->playback)
 		pw_stream_destroy(impl->playback);
+
 	if (impl->core && impl->do_disconnect)
 		pw_core_disconnect(impl->core);
+
 	pw_properties_free(impl->capture_props);
 	pw_properties_free(impl->playback_props);
 	free(impl);
@@ -518,12 +640,23 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
 		pw_properties_setf(props, PW_KEY_NODE_LINK_GROUP, "loopback-%u-%u", pid, id);
 	if (pw_properties_get(props, PW_KEY_NODE_VIRTUAL) == NULL)
 		pw_properties_set(props, PW_KEY_NODE_VIRTUAL, "true");
+	if (pw_properties_get(props, "resample.prefill") == NULL)
+		pw_properties_set(props, "resample.prefill", "true");
 
 	if ((str = pw_properties_get(props, "capture.props")) != NULL)
 		pw_properties_update_string(impl->capture_props, str, strlen(str));
 	if ((str = pw_properties_get(props, "playback.props")) != NULL)
 		pw_properties_update_string(impl->playback_props, str, strlen(str));
 
+	if ((str = pw_properties_get(props, "target.delay.sec")) != NULL)
+		spa_atof(str, &impl->target_delay);
+	if (impl->target_delay > 0.0f &&
+	    pw_properties_get(props, PW_KEY_NODE_LATENCY) == NULL)
+		/* a source and sink (USB) usually have a 1.5 quantum delay, so we use
+		 * a 2 times smaller quantum to compensate */
+		pw_properties_setf(props, PW_KEY_NODE_LATENCY, "%u/%u",
+				(unsigned)(impl->target_delay * 48000 / 3), 48000);
+
 	copy_props(impl, props, PW_KEY_AUDIO_RATE);
 	copy_props(impl, props, PW_KEY_AUDIO_CHANNELS);
 	copy_props(impl, props, SPA_KEY_AUDIO_POSITION);
@@ -533,6 +666,7 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
 	copy_props(impl, props, PW_KEY_NODE_LATENCY);
 	copy_props(impl, props, PW_KEY_NODE_VIRTUAL);
 	copy_props(impl, props, PW_KEY_MEDIA_NAME);
+	copy_props(impl, props, "resample.prefill");
 
 	if ((str = pw_properties_get(props, PW_KEY_NODE_NAME)) == NULL) {
 		pw_properties_setf(props, PW_KEY_NODE_NAME,
diff --git a/src/modules/module-pipe-tunnel.c b/src/modules/module-pipe-tunnel.c
index ff4a2596867ee45143a7ce217b715d8502379c0f..cc33e2389fa54d3e87c3327478818a6911ce2481 100644
--- a/src/modules/module-pipe-tunnel.c
+++ b/src/modules/module-pipe-tunnel.c
@@ -55,7 +55,7 @@
 /** \page page_module_pipe_tunnel PipeWire Module: Unix Pipe Tunnel
  *
  * The pipe-tunnel module provides a source or sink that tunnels all audio to
- * a unix pipe.
+ * or from a unix pipe respectively.
  *
  * ## Module Options
  *
@@ -315,7 +315,7 @@ static const struct pw_stream_events capture_stream_events = {
 	.process = capture_stream_process
 };
 
-static int create_stream(struct impl *impl)
+static int  create_stream(struct impl *impl)
 {
 	int res;
 	uint32_t n_params;
@@ -664,6 +664,7 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
 	copy_props(impl, props, PW_KEY_NODE_LATENCY);
 	copy_props(impl, props, PW_KEY_NODE_VIRTUAL);
 	copy_props(impl, props, PW_KEY_MEDIA_CLASS);
+	copy_props(impl, props, PW_KEY_TARGET_OBJECT);
 
 	parse_audio_info(impl->stream_props, &impl->info);
 
diff --git a/src/modules/module-protocol-native.c b/src/modules/module-protocol-native.c
index 5ec880e80afdfdf84634d4405c74ca16f04cd980..7604da9015271ad03d8b02c3c76cc1721844609a 100644
--- a/src/modules/module-protocol-native.c
+++ b/src/modules/module-protocol-native.c
@@ -228,6 +228,9 @@ static void debug_msg(const char *prefix, const struct pw_protocol_native_messag
 		hex = true;
 	if (hex)
 		spa_debug_mem(0, msg->data, msg->size);
+
+	pw_logt_debug(mod_topic_connection, "%s ****", prefix);
+
 }
 
 static void pre_demarshal(struct pw_protocol_native_connection *conn,
diff --git a/src/modules/module-protocol-native/connection.c b/src/modules/module-protocol-native/connection.c
index 6383024bbdfa4a5ac2b74b9e0a7f9dc754bc1abe..1ba256ca3cf85759170468678532267cf8909549 100644
--- a/src/modules/module-protocol-native/connection.c
+++ b/src/modules/module-protocol-native/connection.c
@@ -221,7 +221,10 @@ static int refill_buffer(struct pw_protocol_native_connection *conn, struct buff
 	struct cmsghdr *cmsg = NULL;
 	struct msghdr msg = { 0 };
 	struct iovec iov[1];
-	char cmsgbuf[CMSG_SPACE(MAX_FDS_MSG * sizeof(int))];
+	union {
+		char cmsgbuf[CMSG_SPACE(MAX_FDS_MSG * sizeof(int))];
+		struct cmsghdr align;
+	} cmsgbuf;
 	int n_fds = 0;
 	size_t avail;
 
@@ -231,7 +234,7 @@ static int refill_buffer(struct pw_protocol_native_connection *conn, struct buff
 	iov[0].iov_len = avail;
 	msg.msg_iov = iov;
 	msg.msg_iovlen = 1;
-	msg.msg_control = cmsgbuf;
+	msg.msg_control = &cmsgbuf;
 	msg.msg_controllen = sizeof(cmsgbuf);
 	msg.msg_flags = MSG_CMSG_CLOEXEC | MSG_DONTWAIT;
 
@@ -725,9 +728,12 @@ pw_protocol_native_connection_end(struct pw_protocol_native_connection *conn,
 		buf->n_fds = buf->msg.n_fds;
 
 	if (mod_topic_connection->level >= SPA_LOG_LEVEL_DEBUG) {
-		pw_log_debug(">>>>>>>>> out: id:%d op:%d size:%d seq:%d",
+		pw_logt_debug(mod_topic_connection,
+			">>>>>>>>> out: id:%d op:%d size:%d seq:%d",
 				buf->msg.id, buf->msg.opcode, size, buf->msg.seq);
 	        spa_debug_pod(0, NULL, SPA_PTROFF(p, impl->hdr_size, struct spa_pod));
+		pw_logt_debug(mod_topic_connection,
+			">>>>>>>>> out: done");
 	}
 
 	buf->seq = (buf->seq + 1) & SPA_ASYNC_SEQ_MASK;
@@ -755,7 +761,10 @@ int pw_protocol_native_connection_flush(struct pw_protocol_native_connection *co
 	struct msghdr msg = { 0 };
 	struct iovec iov[1];
 	struct cmsghdr *cmsg;
-	char cmsgbuf[CMSG_SPACE(MAX_FDS_MSG * sizeof(int))];
+	union {
+		char cmsgbuf[CMSG_SPACE(MAX_FDS_MSG * sizeof(int))];
+		struct cmsghdr align;
+	} cmsgbuf;
 	int res = 0, *fds;
 	uint32_t fds_len, to_close, n_fds, outfds, i;
 	struct buffer *buf;
@@ -786,7 +795,7 @@ int pw_protocol_native_connection_flush(struct pw_protocol_native_connection *co
 		msg.msg_iovlen = 1;
 
 		if (outfds > 0) {
-			msg.msg_control = cmsgbuf;
+			msg.msg_control = &cmsgbuf;
 			msg.msg_controllen = CMSG_SPACE(fds_len);
 			cmsg = CMSG_FIRSTHDR(&msg);
 			cmsg->cmsg_level = SOL_SOCKET;
diff --git a/src/modules/module-protocol-native/defs.h b/src/modules/module-protocol-native/defs.h
index 60adad027ef69a311f15e007cd1bb49e97ffdf0c..dc3c625942f17b045af5475eeb850e550d8e5cd2 100644
--- a/src/modules/module-protocol-native/defs.h
+++ b/src/modules/module-protocol-native/defs.h
@@ -31,13 +31,19 @@ int pw_protocol_native_connect_portal_screencast(struct pw_protocol_client *clie
 					    void (*done_callback) (void *data, int res),
 					    void *data);
 
-static inline void *get_first_pod_from_data(void *data, size_t maxsize, off_t offset)
+static inline void *get_first_pod_from_data(void *data, uint32_t maxsize, uint64_t offset)
 {
 	void *pod;
-	if (offset + sizeof(struct spa_pod) > maxsize)
+	if (maxsize <= offset)
 		return NULL;
+
+	/* spa_pod_parser_advance() rounds up, so round down here to compensate */
+	maxsize = SPA_ROUND_DOWN_N(maxsize - offset, 8);
+	if (maxsize < sizeof(struct spa_pod))
+		return NULL;
+
 	pod = SPA_PTROFF(data, offset, void);
-	if (offset + SPA_POD_SIZE(pod) > maxsize)
+	if (SPA_POD_BODY_SIZE(pod) > maxsize - sizeof(struct spa_pod))
 		return NULL;
 	return pod;
 }
diff --git a/src/modules/module-protocol-native/protocol-footer.c b/src/modules/module-protocol-native/protocol-footer.c
index bc4caa4a80b5014f61f3f131f1d3218f2248fe76..9b6fe62e60740934acf784043ed00c8c59709de1 100644
--- a/src/modules/module-protocol-native/protocol-footer.c
+++ b/src/modules/module-protocol-native/protocol-footer.c
@@ -45,7 +45,7 @@ struct footer_builder {
 	unsigned int started:1;
 };
 
-#define FOOTER_BUILDER_INIT(builder) (struct footer_builder) { builder }
+#define FOOTER_BUILDER_INIT(builder) ((struct footer_builder) { (builder) })
 
 static void start_footer_entry(struct footer_builder *fb, uint32_t opcode)
 {
diff --git a/src/modules/module-protocol-native/protocol-native.c b/src/modules/module-protocol-native/protocol-native.c
index b2a1dc56bb15bcf21bb5b18a046320f3a85ef998..1c2d83e4ac400965e7d322da8aaff1f420906818 100644
--- a/src/modules/module-protocol-native/protocol-native.c
+++ b/src/modules/module-protocol-native/protocol-native.c
@@ -219,13 +219,13 @@ do {											\
 	    spa_pod_parser_get(prs,							\
 			       SPA_POD_Int(&(n_params)), NULL) < 0)			\
 		return -EINVAL;								\
-	params = NULL;									\
-	if (n_params > 0) {								\
+	(params) = NULL;									\
+	if ((n_params) > 0) {								\
 		uint32_t i;								\
-		if (n_params > MAX_PARAM_INFO)						\
+		if ((n_params) > MAX_PARAM_INFO)						\
 			return -ENOSPC;							\
-		params = alloca(n_params * sizeof(struct spa_param_info));		\
-		for (i = 0; i < n_params; i++) {					\
+		(params) = alloca((n_params) * sizeof(struct spa_param_info));		\
+		for (i = 0; i < (n_params); i++) {					\
 			if (spa_pod_parser_get(prs,					\
 				       SPA_POD_Id(&(params)[i].id),			\
 				       SPA_POD_Int(&(params)[i].flags), NULL) < 0)	\
@@ -240,18 +240,18 @@ do {											\
 do {												\
 	if (spa_pod_parser_push_struct(prs, f) < 0 ||						\
 	    spa_pod_parser_get(prs,								\
-		    SPA_POD_Int(&n_permissions), NULL) < 0)					\
+		    SPA_POD_Int(&(n_permissions)), NULL) < 0)					\
 		return -EINVAL;									\
-	permissions = NULL;									\
-	if (n_permissions > 0) {								\
+	(permissions) = NULL;									\
+	if ((n_permissions) > 0) {								\
 		uint32_t i;									\
-		if (n_permissions > MAX_PERMISSIONS)						\
+		if ((n_permissions) > MAX_PERMISSIONS)						\
 			return -ENOSPC;								\
-		permissions = alloca(n_permissions * sizeof(struct pw_permission));		\
-		for (i = 0; i < n_permissions; i++) {						\
+		(permissions) = alloca((n_permissions) * sizeof(struct pw_permission));		\
+		for (i = 0; i < (n_permissions); i++) {						\
 			if (spa_pod_parser_get(prs,						\
-					SPA_POD_Int(&permissions[i].id),			\
-					SPA_POD_Int(&permissions[i].permissions), NULL) < 0)	\
+					SPA_POD_Int(&(permissions)[i].id),			\
+					SPA_POD_Int(&(permissions)[i].permissions), NULL) < 0)	\
 				return -EINVAL;							\
 		}										\
 	}											\
diff --git a/src/modules/module-protocol-native/v0/protocol-native.c b/src/modules/module-protocol-native/v0/protocol-native.c
index bc05e9162dbebbee49a6ea49400e950c11e7d66c..65e055c16f1ae62cee4ccfe5c14cd27b9363c59e 100644
--- a/src/modules/module-protocol-native/v0/protocol-native.c
+++ b/src/modules/module-protocol-native/v0/protocol-native.c
@@ -415,7 +415,7 @@ struct spa_pod_prop_body0 {
              (iter) <= SPA_PTROFF((body), (_size)-(body)->value.size, __typeof__(*iter));       \
              (iter) = SPA_PTROFF((iter), (body)->value.size, __typeof__(*iter)))
 
-#define SPA0_POD_PROP_N_VALUES(b,size)     ((size - sizeof(struct spa_pod_prop_body0)) / (b)->value.size)
+#define SPA0_POD_PROP_N_VALUES(b,size)     (((size) - sizeof(struct spa_pod_prop_body0)) / (b)->value.size)
 
 static int remap_from_v2(uint32_t type, void *body, uint32_t size, struct pw_impl_client *client,
 		struct spa_pod_builder *builder)
diff --git a/src/modules/module-protocol-pulse/client.c b/src/modules/module-protocol-pulse/client.c
index fe7d7885831557d91de73289c19af44f942c3ef6..1e6d202b72478fa55decad0df7664bf10200b029 100644
--- a/src/modules/module-protocol-pulse/client.c
+++ b/src/modules/module-protocol-pulse/client.c
@@ -266,17 +266,12 @@ static int client_try_flush_messages(struct client *client)
 				int res = -errno;
 				if (res == -EINTR)
 					continue;
-				if (res != -EAGAIN && res != -EWOULDBLOCK)
-					pw_log_warn("client %p: send channel:%u %zu, error %d: %m",
-						    client, m->channel, size, res);
 				return res;
 			}
-
 			client->out_index += sent;
 			break;
 		}
 	}
-
 	return 0;
 }
 
@@ -296,7 +291,6 @@ int client_flush_messages(struct client *client)
 		if (res != -EAGAIN && res != -EWOULDBLOCK)
 			return res;
 	}
-
 	return 0;
 }
 
diff --git a/src/modules/module-protocol-pulse/extension.c b/src/modules/module-protocol-pulse/extension.c
index 00a26a74343224667afc548024b56fb66e428a19..0ffa4c53fb0e1b60d84f323878392f08cd026497 100644
--- a/src/modules/module-protocol-pulse/extension.c
+++ b/src/modules/module-protocol-pulse/extension.c
@@ -37,12 +37,9 @@ static const struct extension extensions[] = {
 
 const struct extension *extension_find(uint32_t index, const char *name)
 {
-	const struct extension *ext;
-
-	SPA_FOR_EACH_ELEMENT(extensions, ext) {
+	SPA_FOR_EACH_ELEMENT_VAR(extensions, ext) {
 		if (index == ext->index || spa_streq(name, ext->name))
 			return ext;
 	}
-
 	return NULL;
 }
diff --git a/src/modules/module-protocol-pulse/format.c b/src/modules/module-protocol-pulse/format.c
index fa5a4d4d228b9cf7e0f25049cc7a09e09ceffee7..a3e7a22fd18f480a7c5a32e3496d73e999e4b1cd 100644
--- a/src/modules/module-protocol-pulse/format.c
+++ b/src/modules/module-protocol-pulse/format.c
@@ -159,32 +159,28 @@ uint32_t format_name2id(const char *name)
 
 uint32_t format_paname2id(const char *name, size_t size)
 {
-	size_t i;
-	for (i = 0; i < SPA_N_ELEMENTS(audio_formats); i++) {
-		if (audio_formats[i].name != NULL &&
-		    strncmp(name, audio_formats[i].name, size) == 0)
-			return audio_formats[i].id;
+	SPA_FOR_EACH_ELEMENT_VAR(audio_formats, f) {
+		if (f->name != NULL &&
+		    strncmp(name, f->name, size) == 0)
+			return f->id;
 	}
 	return SPA_AUDIO_FORMAT_UNKNOWN;
 }
 
 enum sample_format format_id2pa(uint32_t id)
 {
-	size_t i;
-	for (i = 0; i < SPA_N_ELEMENTS(audio_formats); i++) {
-		if (id == audio_formats[i].id)
-			return audio_formats[i].pa;
+	SPA_FOR_EACH_ELEMENT_VAR(audio_formats, f) {
+		if (id == f->id)
+			return f->pa;
 	}
 	return SAMPLE_INVALID;
 }
 
 const char *format_id2paname(uint32_t id)
 {
-	size_t i;
-	for (i = 0; i < SPA_N_ELEMENTS(audio_formats); i++) {
-		if (id == audio_formats[i].id &&
-		    audio_formats[i].name != NULL)
-			return audio_formats[i].name;
+	SPA_FOR_EACH_ELEMENT_VAR(audio_formats, f) {
+		if (id == f->id && f->name != NULL)
+			return f->name;
 	}
 	return "invalid";
 }
@@ -266,21 +262,18 @@ enum channel_position channel_id2pa(uint32_t id, uint32_t *aux)
 
 const char *channel_id2paname(uint32_t id, uint32_t *aux)
 {
-	size_t i;
-	for (i = 0; i < SPA_N_ELEMENTS(audio_channels); i++) {
-		if (id == audio_channels[i].channel &&
-		    audio_channels[i].name != NULL)
-			return audio_channels[i].name;
+	SPA_FOR_EACH_ELEMENT_VAR(audio_channels, i) {
+		if (id == i->channel && i->name != NULL)
+			return i->name;
 	}
 	return audio_channels[CHANNEL_POSITION_AUX0 + ((*aux)++ & 31)].name;
 }
 
 uint32_t channel_paname2id(const char *name, size_t size)
 {
-	size_t i;
-	for (i = 0; i < SPA_N_ELEMENTS(audio_channels); i++) {
-		if (strncmp(name, audio_channels[i].name, size) == 0)
-			return audio_channels[i].channel;
+	SPA_FOR_EACH_ELEMENT_VAR(audio_channels, i) {
+		if (strncmp(name, i->name, size) == 0)
+			return i->channel;
 	}
 	return SPA_AUDIO_CHANNEL_UNKNOWN;
 }
@@ -424,25 +417,6 @@ static enum encoding format_encoding_from_id(uint32_t id)
 	return ENCODING_ANY;
 }
 
-static inline int
-audio_raw_parse_opt(const struct spa_pod *format, struct spa_audio_info_raw *info)
-{
-	struct spa_pod *position = NULL;
-	int res;
-	info->flags = 0;
-	res = spa_pod_parse_object(format,
-			SPA_TYPE_OBJECT_Format, NULL,
-			SPA_FORMAT_AUDIO_format,        SPA_POD_OPT_Id(&info->format),
-			SPA_FORMAT_AUDIO_rate,          SPA_POD_OPT_Int(&info->rate),
-			SPA_FORMAT_AUDIO_channels,      SPA_POD_OPT_Int(&info->channels),
-			SPA_FORMAT_AUDIO_position,      SPA_POD_OPT_Pod(&position));
-	if (position == NULL ||
-	    !spa_pod_copy_array(position, SPA_TYPE_Id, info->position, SPA_AUDIO_MAX_CHANNELS))
-		SPA_FLAG_SET(info->flags, SPA_AUDIO_FLAG_UNPOSITIONED);
-
-	return res;
-}
-
 int format_parse_param(const struct spa_pod *param, bool collect,
 		struct sample_spec *ss, struct channel_map *map,
 		const struct sample_spec *def_ss, const struct channel_map *def_map)
@@ -458,14 +432,17 @@ int format_parse_param(const struct spa_pod *param, bool collect,
 
 	switch (info.media_subtype) {
 	case SPA_MEDIA_SUBTYPE_raw:
+		if (spa_format_audio_raw_parse(param, &info.info.raw) < 0)
+	                return -ENOTSUP;
 		if (def_ss != NULL) {
 			if (ss != NULL)
 				*ss = *def_ss;
-			if (audio_raw_parse_opt(param, &info.info.raw) < 0)
-		                return -ENOTSUP;
 		} else {
-			if (spa_format_audio_raw_parse(param, &info.info.raw) < 0)
-		                return -ENOTSUP;
+			if (info.info.raw.format == 0 ||
+			    info.info.raw.rate == 0 ||
+			    info.info.raw.channels == 0 ||
+			    info.info.raw.channels > SPA_AUDIO_MAX_CHANNELS)
+				return -ENOTSUP;
 		}
 		break;
 	case SPA_MEDIA_SUBTYPE_iec958:
diff --git a/src/modules/module-protocol-pulse/internal.h b/src/modules/module-protocol-pulse/internal.h
index ca6b749264df14bd325bd7b4603ff262fa8773e0..1d5731c999bcb1419197a89167f7c3d1a8c0a3cc 100644
--- a/src/modules/module-protocol-pulse/internal.h
+++ b/src/modules/module-protocol-pulse/internal.h
@@ -36,6 +36,7 @@
 #include <pipewire/private.h>
 
 #include "format.h"
+#include "server.h"
 
 struct pw_loop;
 struct pw_context;
@@ -52,6 +53,7 @@ struct defs {
 	struct sample_spec sample_spec;
 	struct channel_map channel_map;
 	uint32_t quantum_limit;
+	uint32_t idle_timeout;
 };
 
 struct stats {
@@ -72,6 +74,7 @@ struct impl {
 
 	struct ratelimit rate_limit;
 
+	struct spa_hook_list hooks;
 	struct spa_list servers;
 
 	struct pw_work_queue *work_queue;
@@ -85,6 +88,19 @@ struct impl {
 	struct stats stat;
 };
 
+struct impl_events {
+#define VERSION_IMPL_EVENTS	0
+	uint32_t version;
+
+	void (*server_started) (void *data, struct server *server);
+
+	void (*server_stopped) (void *data, struct server *server);
+};
+
+void impl_add_listener(struct impl *impl,
+		struct spa_hook *listener,
+		const struct impl_events *events, void *data);
+
 extern bool debug_messages;
 
 void broadcast_subscribe_event(struct impl *impl, uint32_t mask, uint32_t event, uint32_t id);
diff --git a/src/modules/module-protocol-pulse/manager.c b/src/modules/module-protocol-pulse/manager.c
index 4841b67c09bf6a00d43a3258742479d713717833..7aa8d70bd03fc648edd3f250d57fce0c5d840988 100644
--- a/src/modules/module-protocol-pulse/manager.c
+++ b/src/modules/module-protocol-pulse/manager.c
@@ -33,15 +33,13 @@
 #include "log.h"
 #include "module-protocol-pulse/server.h"
 
-#define MAX_PARAMS 32
-
-#define manager_emit_sync(m) spa_hook_list_call(&m->hooks, struct pw_manager_events, sync, 0)
-#define manager_emit_added(m,o) spa_hook_list_call(&m->hooks, struct pw_manager_events, added, 0, o)
-#define manager_emit_updated(m,o) spa_hook_list_call(&m->hooks, struct pw_manager_events, updated, 0, o)
-#define manager_emit_removed(m,o) spa_hook_list_call(&m->hooks, struct pw_manager_events, removed, 0, o)
-#define manager_emit_metadata(m,o,s,k,t,v) spa_hook_list_call(&m->hooks, struct pw_manager_events, metadata,0,o,s,k,t,v)
-#define manager_emit_disconnect(m) spa_hook_list_call(&m->hooks, struct pw_manager_events, disconnect, 0)
-#define manager_emit_object_data_timeout(m,o,k) spa_hook_list_call(&m->hooks, struct pw_manager_events, object_data_timeout,0,o,k)
+#define manager_emit_sync(m) spa_hook_list_call(&(m)->hooks, struct pw_manager_events, sync, 0)
+#define manager_emit_added(m,o) spa_hook_list_call(&(m)->hooks, struct pw_manager_events, added, 0, o)
+#define manager_emit_updated(m,o) spa_hook_list_call(&(m)->hooks, struct pw_manager_events, updated, 0, o)
+#define manager_emit_removed(m,o) spa_hook_list_call(&(m)->hooks, struct pw_manager_events, removed, 0, o)
+#define manager_emit_metadata(m,o,s,k,t,v) spa_hook_list_call(&(m)->hooks, struct pw_manager_events, metadata,0,o,s,k,t,v)
+#define manager_emit_disconnect(m) spa_hook_list_call(&(m)->hooks, struct pw_manager_events, disconnect, 0)
+#define manager_emit_object_data_timeout(m,o,k) spa_hook_list_call(&(m)->hooks, struct pw_manager_events, object_data_timeout,0,o,k)
 
 struct object;
 
@@ -85,8 +83,6 @@ struct object {
 	struct spa_hook proxy_listener;
 	struct spa_hook object_listener;
 
-	int param_seq[MAX_PARAMS];
-
 	struct spa_list data_list;
 };
 
@@ -113,7 +109,7 @@ static uint32_t clear_params(struct spa_list *param_list, uint32_t id)
 }
 
 static struct pw_manager_param *add_param(struct spa_list *params,
-		int seq, int *param_seq, uint32_t id, const struct spa_pod *param)
+		int seq, uint32_t id, const struct spa_pod *param)
 {
 	struct pw_manager_param *p;
 
@@ -125,24 +121,12 @@ static struct pw_manager_param *add_param(struct spa_list *params,
 		id = SPA_POD_OBJECT_ID(param);
 	}
 
-	if (id >= MAX_PARAMS) {
-		pw_log_error("too big param id %d", id);
-		errno = EINVAL;
-		return NULL;
-	}
-
-	if (seq != param_seq[id]) {
-		pw_log_debug("ignoring param %d, seq:%d != current_seq:%d",
-				id, seq, param_seq[id]);
-		errno = EBUSY;
-		return NULL;
-	}
-
 	p = malloc(sizeof(*p) + (param != NULL ? SPA_POD_SIZE(param) : 0));
 	if (p == NULL)
 		return NULL;
 
 	p->id = id;
+	p->seq = seq;
 	if (param != NULL) {
 		p->param = SPA_PTROFF(p, sizeof(*p), struct spa_pod);
 		memcpy(p->param, param, SPA_POD_SIZE(param));
@@ -167,7 +151,6 @@ static bool has_param(struct spa_list *param_list, struct pw_manager_param *p)
 	return false;
 }
 
-
 static struct object *find_object_by_id(struct manager *m, uint32_t id)
 {
 	struct object *o;
@@ -180,7 +163,19 @@ static struct object *find_object_by_id(struct manager *m, uint32_t id)
 
 static void object_update_params(struct object *o)
 {
-	struct pw_manager_param *p;
+	struct pw_manager_param *p, *t;
+	uint32_t i;
+
+	for (i = 0; i < o->this.n_params; i++) {
+		spa_list_for_each_safe(p, t, &o->pending_list, link) {
+			if (p->id == o->this.params[i].id &&
+			    p->seq != o->this.params[i].seq &&
+			    p->param != NULL) {
+				spa_list_remove(&p->link);
+				free(p);
+			}
+		}
+	}
 
 	spa_list_consume(p, &o->pending_list, link) {
 		spa_list_remove(&p->link);
@@ -236,6 +231,8 @@ static void client_event_info(void *data, const struct pw_client_info *info)
 	pw_log_debug("object %p: id:%d change-mask:%08"PRIx64, o, o->this.id, info->change_mask);
 
 	info = o->this.info = pw_client_info_merge(o->this.info, info, o->this.changed == 0);
+	if (info == NULL)
+		return;
 
 	if (info->change_mask & PW_CLIENT_CHANGE_MASK_PROPS)
 		changed++;
@@ -275,6 +272,8 @@ static void module_event_info(void *data, const struct pw_module_info *info)
 	pw_log_debug("object %p: id:%d change-mask:%08"PRIx64, o, o->this.id, info->change_mask);
 
 	info = o->this.info = pw_module_info_merge(o->this.info, info, o->this.changed == 0);
+	if (info == NULL)
+		return;
 
 	if (info->change_mask & PW_MODULE_CHANGE_MASK_PROPS)
 		changed++;
@@ -314,6 +313,11 @@ static void device_event_info(void *data, const struct pw_device_info *info)
 	pw_log_debug("object %p: id:%d change-mask:%08"PRIx64, o, o->this.id, info->change_mask);
 
 	info = o->this.info = pw_device_info_merge(o->this.info, info, o->this.changed == 0);
+	if (info == NULL)
+		return;
+
+	o->this.n_params = info->n_params;
+	o->this.params = info->params;
 
 	if (info->change_mask & PW_DEVICE_CHANGE_MASK_PROPS)
 		changed++;
@@ -327,11 +331,6 @@ static void device_event_info(void *data, const struct pw_device_info *info)
 				continue;
 			info->params[i].user = 0;
 
-			if (id >= MAX_PARAMS) {
-				pw_log_error("too big param id %d", id);
-				continue;
-			}
-
 			switch (id) {
 			case SPA_PARAM_EnumProfile:
 			case SPA_PARAM_Profile:
@@ -341,14 +340,14 @@ static void device_event_info(void *data, const struct pw_device_info *info)
 			case SPA_PARAM_Route:
 				break;
 			}
-			add_param(&o->pending_list, o->param_seq[id], o->param_seq, id, NULL);
+			add_param(&o->pending_list, info->params[i].seq, id, NULL);
 			if (!(info->params[i].flags & SPA_PARAM_INFO_READ))
 				continue;
 
 			res = pw_device_enum_params((struct pw_device*)o->this.proxy,
-					++o->param_seq[id], id, 0, -1, NULL);
+					++info->params[i].seq, id, 0, -1, NULL);
 			if (SPA_RESULT_IS_ASYNC(res))
-				o->param_seq[id] = res;
+				info->params[i].seq = res;
 		}
 	}
 	if (changed) {
@@ -385,7 +384,7 @@ static void device_event_param(void *data, int seq,
 	struct manager *m = o->manager;
 	struct pw_manager_param *p;
 
-	p = add_param(&o->pending_list, seq, o->param_seq, id, param);
+	p = add_param(&o->pending_list, seq, id, param);
 	if (p == NULL)
 		return;
 
@@ -434,6 +433,11 @@ static void node_event_info(void *data, const struct pw_node_info *info)
 	pw_log_debug("object %p: id:%d change-mask:%08"PRIx64, o, o->this.id, info->change_mask);
 
 	info = o->this.info = pw_node_info_merge(o->this.info, info, o->this.changed == 0);
+	if (info == NULL)
+		return;
+
+	o->this.n_params = info->n_params;
+	o->this.params = info->params;
 
 	if (info->change_mask & PW_NODE_CHANGE_MASK_STATE)
 		changed++;
@@ -450,20 +454,15 @@ static void node_event_info(void *data, const struct pw_node_info *info)
 				continue;
 			info->params[i].user = 0;
 
-			if (id >= MAX_PARAMS) {
-				pw_log_error("too big param id %d", id);
-				continue;
-			}
-
 			changed++;
-			add_param(&o->pending_list, o->param_seq[id], o->param_seq, id, NULL);
+			add_param(&o->pending_list, info->params[i].seq, id, NULL);
 			if (!(info->params[i].flags & SPA_PARAM_INFO_READ))
 				continue;
 
 			res = pw_node_enum_params((struct pw_node*)o->this.proxy,
-					++o->param_seq[id], id, 0, -1, NULL);
+					++info->params[i].seq, id, 0, -1, NULL);
 			if (SPA_RESULT_IS_ASYNC(res))
-				o->param_seq[id] = res;
+				info->params[i].seq = res;
 		}
 	}
 	if (changed) {
@@ -477,7 +476,7 @@ static void node_event_param(void *data, int seq,
 		const struct spa_pod *param)
 {
 	struct object *o = data;
-	add_param(&o->pending_list, seq, o->param_seq, id, param);
+	add_param(&o->pending_list, seq, id, param);
 }
 
 static const struct pw_node_events node_events = {
@@ -553,11 +552,10 @@ static const struct object_info *objects[] =
 
 static const struct object_info *find_info(const char *type, uint32_t version)
 {
-	size_t i;
-	for (i = 0; i < SPA_N_ELEMENTS(objects); i++) {
-		if (spa_streq(objects[i]->type, type) &&
-		    objects[i]->version <= version)
-			return objects[i];
+	SPA_FOR_EACH_ELEMENT_VAR(objects, i) {
+		if (spa_streq((*i)->type, type) &&
+		    (*i)->version <= version)
+			return *i;
 	}
 	return NULL;
 }
diff --git a/src/modules/module-protocol-pulse/manager.h b/src/modules/module-protocol-pulse/manager.h
index 05ce2e9c5c45b5c15c84c04429867e90b281ce3f..56aea25bb1e5f9d414fa7a5a5f4fd42e40a48c41 100644
--- a/src/modules/module-protocol-pulse/manager.h
+++ b/src/modules/module-protocol-pulse/manager.h
@@ -72,6 +72,7 @@ struct pw_manager {
 
 struct pw_manager_param {
 	uint32_t id;
+	int32_t seq;
 	struct spa_list link;           /**< link in manager_object param_list */
 	struct spa_pod *param;
 };
@@ -92,6 +93,9 @@ struct pw_manager_object {
 
 	int changed;
 	void *info;
+	struct spa_param_info *params;
+	uint32_t n_params;
+
 	struct spa_list param_list;
 	unsigned int creating:1;
 	unsigned int removing:1;
diff --git a/src/modules/module-protocol-pulse/modules/module-loopback.c b/src/modules/module-protocol-pulse/modules/module-loopback.c
index df6e47a4bc76b4506742514fe960d676a41d3c11..94d7fc70a5cc06dca1052970eba26b0455f1bb23 100644
--- a/src/modules/module-protocol-pulse/modules/module-loopback.c
+++ b/src/modules/module-protocol-pulse/modules/module-loopback.c
@@ -47,6 +47,7 @@ struct module_loopback_data {
 	struct pw_properties *playback_props;
 
 	struct spa_audio_info_raw info;
+	uint32_t latency_msec;
 };
 
 static void module_destroy(void *data)
@@ -68,6 +69,7 @@ static int module_loopback_load(struct client *client, struct module *module)
 	FILE *f;
 	char *args;
 	size_t size, i;
+	char val[256];
 
 	pw_properties_setf(data->capture_props, PW_KEY_NODE_GROUP, "loopback-%u", module->index);
 	pw_properties_setf(data->playback_props, PW_KEY_NODE_GROUP, "loopback-%u", module->index);
@@ -88,6 +90,10 @@ static int module_loopback_load(struct client *client, struct module *module)
 			fprintf(f, " ]");
 		}
 	}
+	if (data->latency_msec != 0)
+		fprintf(f, " target.delay.sec = %s",
+				spa_json_format_float(val, sizeof(val),
+					data->latency_msec / 1000.0f));
 	fprintf(f, " capture.props = {");
 	pw_properties_serialize_dict(f, &data->capture_props->dict, 0);
 	fprintf(f, " } playback.props = {");
@@ -204,15 +210,8 @@ static int module_loopback_prepare(struct module * const module)
 		pw_properties_set(props, "remix", NULL);
 	}
 
-	if ((str = pw_properties_get(props, "latency_msec")) != NULL) {
-		/* Half the latency on each of the playback and capture streams */
-		pw_properties_setf(capture_props, PW_KEY_NODE_LATENCY, "%s/2000", str);
-		pw_properties_setf(playback_props, PW_KEY_NODE_LATENCY, "%s/2000", str);
-		pw_properties_set(props, "latency_msec", NULL);
-	} else {
-		pw_properties_set(capture_props, PW_KEY_NODE_LATENCY, "100/1000");
-		pw_properties_set(playback_props, PW_KEY_NODE_LATENCY, "100/1000");
-	}
+	if ((str = pw_properties_get(props, "latency_msec")) != NULL)
+		d->latency_msec = atoi(str);
 
 	if ((str = pw_properties_get(props, "sink_input_properties")) != NULL) {
 		module_args_add_props(playback_props, str);
diff --git a/src/modules/module-protocol-pulse/modules/module-rtp-recv.c b/src/modules/module-protocol-pulse/modules/module-rtp-recv.c
new file mode 100644
index 0000000000000000000000000000000000000000..29a0d70da38420b3f506530f7c6a948ef3056b97
--- /dev/null
+++ b/src/modules/module-protocol-pulse/modules/module-rtp-recv.c
@@ -0,0 +1,165 @@
+/* PipeWire
+ *
+ * Copyright © 2022 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * 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/hook.h>
+#include <pipewire/pipewire.h>
+#include <pipewire/private.h>
+
+#include "../defs.h"
+#include "../module.h"
+
+#define NAME "rtp-recv"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+struct module_rtp_recv_data {
+	struct module *module;
+
+	struct spa_hook mod_listener;
+	struct pw_impl_module *mod;
+
+	struct pw_properties *stream_props;
+	struct pw_properties *global_props;
+};
+
+static void module_destroy(void *data)
+{
+	struct module_rtp_recv_data *d = data;
+	spa_hook_remove(&d->mod_listener);
+	d->mod = NULL;
+	module_schedule_unload(d->module);
+}
+
+static const struct pw_impl_module_events module_events = {
+	PW_VERSION_IMPL_MODULE_EVENTS,
+	.destroy = module_destroy
+};
+
+static int module_rtp_recv_load(struct client *client, struct module *module)
+{
+	struct module_rtp_recv_data *data = module->user_data;
+	FILE *f;
+	char *args;
+	size_t size;
+
+	pw_properties_setf(data->stream_props, "pulse.module.id",
+			"%u", module->index);
+
+	if ((f = open_memstream(&args, &size)) == NULL)
+		return -errno;
+
+	fprintf(f, "{");
+	pw_properties_serialize_dict(f, &data->global_props->dict, 0);
+	fprintf(f, " stream.props = {");
+	pw_properties_serialize_dict(f, &data->stream_props->dict, 0);
+	fprintf(f, " } }");
+	fclose(f);
+
+	data->mod = pw_context_load_module(module->impl->context,
+			"libpipewire-module-rtp-source",
+			args, NULL);
+
+	free(args);
+
+	if (data->mod == NULL)
+		return -errno;
+
+	pw_impl_module_add_listener(data->mod,
+			&data->mod_listener,
+			&module_events, data);
+
+	return 0;
+}
+
+static int module_rtp_recv_unload(struct module *module)
+{
+	struct module_rtp_recv_data *d = module->user_data;
+
+	if (d->mod) {
+		spa_hook_remove(&d->mod_listener);
+		pw_impl_module_destroy(d->mod);
+		d->mod = NULL;
+	}
+
+	pw_properties_free(d->global_props);
+	pw_properties_free(d->stream_props);
+
+	return 0;
+}
+
+static const struct spa_dict_item module_rtp_recv_info[] = {
+	{ PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+	{ PW_KEY_MODULE_DESCRIPTION, "Receive data from a network via RTP/SAP/SDP" },
+	{ PW_KEY_MODULE_USAGE,	"sink=<name of the sink> "
+				"sap_address=<multicast address to listen on> "
+				"latency_msec=<latency in ms> " },
+	{ PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+static int module_rtp_recv_prepare(struct module * const module)
+{
+	struct module_rtp_recv_data * const d = module->user_data;
+	struct pw_properties * const props = module->props;
+	struct pw_properties *stream_props = NULL, *global_props = NULL;
+	const char *str;
+	int res;
+
+	PW_LOG_TOPIC_INIT(mod_topic);
+
+	stream_props = pw_properties_new(NULL, NULL);
+	global_props = pw_properties_new(NULL, NULL);
+	if (!stream_props || !global_props) {
+		res = -errno;
+		goto out;
+	}
+	if ((str = pw_properties_get(props, "sink")) != NULL)
+		pw_properties_set(stream_props, PW_KEY_NODE_TARGET, str);
+
+	if ((str = pw_properties_get(props, "sap_address")) != NULL)
+		pw_properties_set(global_props, "sap.ip", str);
+
+	if ((str = pw_properties_get(props, "latency_msec")) != NULL)
+		pw_properties_set(global_props, "sess.latency.msec", str);
+
+	d->module = module;
+	d->stream_props = stream_props;
+	d->global_props = global_props;
+
+	return 0;
+out:
+	pw_properties_free(stream_props);
+	pw_properties_free(global_props);
+
+	return res;
+}
+
+DEFINE_MODULE_INFO(module_rtp_recv) = {
+	.name = "module-rtp-recv",
+	.prepare = module_rtp_recv_prepare,
+	.load = module_rtp_recv_load,
+	.unload = module_rtp_recv_unload,
+	.properties = &SPA_DICT_INIT_ARRAY(module_rtp_recv_info),
+	.data_size = sizeof(struct module_rtp_recv_data),
+};
diff --git a/src/modules/module-protocol-pulse/modules/module-rtp-send.c b/src/modules/module-protocol-pulse/modules/module-rtp-send.c
new file mode 100644
index 0000000000000000000000000000000000000000..1f13e65075dcbcd50cc6e01beaa2a70cd4546128
--- /dev/null
+++ b/src/modules/module-protocol-pulse/modules/module-rtp-send.c
@@ -0,0 +1,226 @@
+/* PipeWire
+ *
+ * Copyright © 2022 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * 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/hook.h>
+#include <pipewire/pipewire.h>
+#include <pipewire/private.h>
+
+#include "../defs.h"
+#include "../module.h"
+
+#define NAME "rtp-send"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+struct module_rtp_send_data {
+	struct module *module;
+
+	struct spa_hook mod_listener;
+	struct pw_impl_module *mod;
+
+	struct pw_properties *stream_props;
+	struct pw_properties *global_props;
+	struct spa_audio_info_raw info;
+};
+
+static void module_destroy(void *data)
+{
+	struct module_rtp_send_data *d = data;
+	spa_hook_remove(&d->mod_listener);
+	d->mod = NULL;
+	module_schedule_unload(d->module);
+}
+
+static const struct pw_impl_module_events module_events = {
+	PW_VERSION_IMPL_MODULE_EVENTS,
+	.destroy = module_destroy
+};
+
+static int module_rtp_send_load(struct client *client, struct module *module)
+{
+	struct module_rtp_send_data *data = module->user_data;
+	FILE *f;
+	char *args;
+	size_t size;
+	uint32_t i;
+
+	pw_properties_setf(data->stream_props, "pulse.module.id",
+			"%u", module->index);
+
+	if ((f = open_memstream(&args, &size)) == NULL)
+		return -errno;
+
+	fprintf(f, "{");
+	pw_properties_serialize_dict(f, &data->global_props->dict, 0);
+	if (data->info.format != 0)
+		fprintf(f, " \"audio.format\": \"%s\"", format_id2name(data->info.format));
+	if (data->info.rate != 0)
+		fprintf(f, " \"audio.rate\": %u,", data->info.rate);
+	if (data->info.channels != 0) {
+		fprintf(f, " \"audio.channels\": %u,", data->info.channels);
+		if (!(data->info.flags & SPA_AUDIO_FLAG_UNPOSITIONED)) {
+			fprintf(f, " \"audio.position\": [ ");
+			for (i = 0; i < data->info.channels; i++)
+				fprintf(f, "%s\"%s\"", i == 0 ? "" : ",",
+					channel_id2name(data->info.position[i]));
+			fprintf(f, " ],");
+		}
+	}
+	fprintf(f, " stream.props = {");
+	pw_properties_serialize_dict(f, &data->stream_props->dict, 0);
+	fprintf(f, " } }");
+	fclose(f);
+
+	data->mod = pw_context_load_module(module->impl->context,
+			"libpipewire-module-rtp-sink",
+			args, NULL);
+
+	free(args);
+
+	if (data->mod == NULL)
+		return -errno;
+
+	pw_impl_module_add_listener(data->mod,
+			&data->mod_listener,
+			&module_events, data);
+
+	return 0;
+}
+
+static int module_rtp_send_unload(struct module *module)
+{
+	struct module_rtp_send_data *d = module->user_data;
+
+	if (d->mod) {
+		spa_hook_remove(&d->mod_listener);
+		pw_impl_module_destroy(d->mod);
+		d->mod = NULL;
+	}
+
+	pw_properties_free(d->global_props);
+	pw_properties_free(d->stream_props);
+
+	return 0;
+}
+
+static const struct spa_dict_item module_rtp_send_info[] = {
+	{ PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+	{ PW_KEY_MODULE_DESCRIPTION, "Read data from source and send it to the network via RTP/SAP/SDP" },
+	{ PW_KEY_MODULE_USAGE,	"source=<name of the source> "
+				"format=<sample format> "
+				"channels=<number of channels> "
+				"rate=<sample rate> "
+				"destination_ip=<destination IP address> "
+				"source_ip=<source IP address> "
+				"port=<port number> "
+				"mtu=<maximum transfer unit> "
+				"loop=<loopback to local host?> "
+				"ttl=<ttl value> "
+				"inhibit_auto_suspend=<always|never|only_with_non_monitor_sources> "
+				"stream_name=<name of the stream> "
+				"enable_opus=<enable OPUS codec>" },
+	{ PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+static int module_rtp_send_prepare(struct module * const module)
+{
+	struct module_rtp_send_data * const d = module->user_data;
+	struct pw_properties * const props = module->props;
+	struct pw_properties *stream_props = NULL, *global_props = NULL;
+	struct spa_audio_info_raw info = { 0 };
+	const char *str;
+	int res;
+
+	PW_LOG_TOPIC_INIT(mod_topic);
+
+	stream_props = pw_properties_new(NULL, NULL);
+	global_props = pw_properties_new(NULL, NULL);
+	if (!stream_props || !global_props) {
+		res = -errno;
+		goto out;
+	}
+
+	if ((str = pw_properties_get(props, "source")) != NULL) {
+		pw_properties_set(stream_props, PW_KEY_NODE_TARGET, str);
+		if (spa_strendswith(str, ".monitor")) {
+			pw_properties_setf(stream_props, PW_KEY_NODE_TARGET,
+					"%.*s", (int)strlen(str)-8, str);
+			pw_properties_set(stream_props, PW_KEY_STREAM_CAPTURE_SINK,
+					"true");
+		} else {
+			pw_properties_set(stream_props, PW_KEY_NODE_TARGET, str);
+		}
+	}
+	if (module_args_to_audioinfo(module->impl, props, &info) < 0) {
+		res = -EINVAL;
+		goto out;
+	}
+	info.format = 0;
+	if ((str = pw_properties_get(props, "format")) != NULL) {
+		if ((info.format = format_paname2id(str, strlen(str))) ==
+				SPA_AUDIO_FORMAT_UNKNOWN) {
+			pw_log_error("unknown format %s", str);
+			res = -EINVAL;
+			goto out;
+		}
+	}
+
+	if ((str = pw_properties_get(props, "destination_ip")) != NULL)
+		pw_properties_set(global_props, "destination.ip", str);
+	if ((str = pw_properties_get(props, "source_ip")) != NULL)
+		pw_properties_set(global_props, "source.ip", str);
+	if ((str = pw_properties_get(props, "port")) != NULL)
+		pw_properties_set(global_props, "destination.port", str);
+	if ((str = pw_properties_get(props, "mtu")) != NULL)
+		pw_properties_set(global_props, "net.mtu", str);
+	if ((str = pw_properties_get(props, "loop")) != NULL)
+		pw_properties_set(global_props, "net.loop",
+				module_args_parse_bool(str) ? "true" : "false");
+	if ((str = pw_properties_get(props, "ttl")) != NULL)
+		pw_properties_set(global_props, "net.ttl", str);
+	if ((str = pw_properties_get(props, "stream_name")) != NULL)
+		pw_properties_set(global_props, "sess.name", str);
+
+	d->module = module;
+	d->stream_props = stream_props;
+	d->global_props = global_props;
+	d->info = info;
+
+	return 0;
+out:
+	pw_properties_free(stream_props);
+	pw_properties_free(global_props);
+
+	return res;
+}
+
+DEFINE_MODULE_INFO(module_rtp_send) = {
+	.name = "module-rtp-send",
+	.prepare = module_rtp_send_prepare,
+	.load = module_rtp_send_load,
+	.unload = module_rtp_send_unload,
+	.properties = &SPA_DICT_INIT_ARRAY(module_rtp_send_info),
+	.data_size = sizeof(struct module_rtp_send_data),
+};
diff --git a/src/modules/module-protocol-pulse/modules/module-zeroconf-discover.c b/src/modules/module-protocol-pulse/modules/module-zeroconf-discover.c
index 4affa7fae681cf81c4e927a22eb72f245d463b28..79f14e853c2433b2286ad9742e3b7c91e5ec7c81 100644
--- a/src/modules/module-protocol-pulse/modules/module-zeroconf-discover.c
+++ b/src/modules/module-protocol-pulse/modules/module-zeroconf-discover.c
@@ -40,6 +40,8 @@ struct module_zeroconf_discover_data {
 
 	struct spa_hook mod_listener;
 	struct pw_impl_module *mod;
+
+	uint32_t latency_msec;
 };
 
 static void module_destroy(void *data)
@@ -58,10 +60,25 @@ static const struct pw_impl_module_events module_events = {
 static int module_zeroconf_discover_load(struct client *client, struct module *module)
 {
 	struct module_zeroconf_discover_data *data = module->user_data;
+	FILE *f;
+	char *args;
+	size_t size;
+
+	if ((f = open_memstream(&args, &size)) == NULL)
+		return -errno;
+
+	fprintf(f, "{");
+	if (data->latency_msec > 0)
+		fprintf(f, " pulse.latency = %u ", data->latency_msec);
+	fprintf(f, "}");
+	fclose(f);
 
 	data->mod = pw_context_load_module(module->impl->context,
 			"libpipewire-module-zeroconf-discover",
-			NULL, NULL);
+			args, NULL);
+
+	free(args);
+
 	if (data->mod == NULL)
 		return -errno;
 
@@ -88,7 +105,8 @@ static int module_zeroconf_discover_unload(struct module *module)
 static const struct spa_dict_item module_zeroconf_discover_info[] = {
 	{ PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.con>" },
 	{ PW_KEY_MODULE_DESCRIPTION, "mDNS/DNS-SD Service Discovery" },
-	{ PW_KEY_MODULE_USAGE, "" },
+	{ PW_KEY_MODULE_USAGE,
+		"latency_msec=<fixed latency in ms> " },
 	{ PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
 };
 
@@ -96,9 +114,12 @@ static int module_zeroconf_discover_prepare(struct module * const module)
 {
 	PW_LOG_TOPIC_INIT(mod_topic);
 
+	struct pw_properties * const props = module->props;
 	struct module_zeroconf_discover_data * const data = module->user_data;
 	data->module = module;
 
+	pw_properties_fetch_uint32(props, "latency_msec", &data->latency_msec);
+
 	return 0;
 }
 
diff --git a/src/modules/module-protocol-pulse/modules/module-zeroconf-publish.c b/src/modules/module-protocol-pulse/modules/module-zeroconf-publish.c
index e30c91bee3b79d012b5ebb5d275b5d8f34a1883a..8da1b6ae83edf518bc5264fdccae07e554c6995b 100644
--- a/src/modules/module-protocol-pulse/modules/module-zeroconf-publish.c
+++ b/src/modules/module-protocol-pulse/modules/module-zeroconf-publish.c
@@ -24,6 +24,7 @@
  */
 
 #include <sys/utsname.h>
+#include <arpa/inet.h>
 
 #include <pipewire/pipewire.h>
 
@@ -32,6 +33,7 @@
 #include "../manager.h"
 #include "../module.h"
 #include "../pulse-server.h"
+#include "../server.h"
 #include "../../module-zeroconf-discover/avahi-poll.h"
 
 #include <avahi-client/client.h>
@@ -83,6 +85,7 @@ struct service {
 	struct pw_properties *props;
 
 	char service_name[AVAHI_LABEL_MAX];
+	unsigned published:1;
 };
 
 struct module_zeroconf_publish_data {
@@ -93,8 +96,7 @@ struct module_zeroconf_publish_data {
 
 	struct spa_hook core_listener;
 	struct spa_hook manager_listener;
-
-	unsigned int port;
+	struct spa_hook impl_listener;
 
 	AvahiPoll *avahi_poll;
 	AvahiClient *client;
@@ -151,6 +153,7 @@ static void unpublish_service(struct service *s)
 {
 	spa_list_remove(&s->link);
 	spa_list_append(&s->userdata->pending, &s->link);
+	s->published = false;
 }
 
 static void unpublish_all_services(struct module_zeroconf_publish_data *d)
@@ -315,6 +318,11 @@ static void service_entry_group_callback(AvahiEntryGroup *g, AvahiEntryGroupStat
 	struct service *s = userdata;
 
 	spa_assert(s);
+	if (!s->published) {
+		pw_log_info("cancel unpublished service: %s", s->service_name);
+		clear_entry_group(s);
+		return;
+	}
 
 	switch (state) {
 	case AVAHI_ENTRY_GROUP_ESTABLISHED:
@@ -380,8 +388,7 @@ static AvahiStringList *get_service_txt(const struct service *s)
 	txt = avahi_string_list_add_pair(txt, "channel_map", channel_map_snprint(cm, sizeof(cm), &s->cm));
 	txt = avahi_string_list_add_pair(txt, "subtype", subtype_text[s->subtype]);
 
-	const struct mapping *m;
-	SPA_FOR_EACH_ELEMENT(mappings, m) {
+	SPA_FOR_EACH_ELEMENT_VAR(mappings, m) {
 		const char *value = pw_properties_get(s->props, m->pw_key);
 		if (value != NULL)
 			txt = avahi_string_list_add_pair(txt, m->txt_key, value);
@@ -390,17 +397,47 @@ static AvahiStringList *get_service_txt(const struct service *s)
 	return txt;
 }
 
+static int find_port(struct service *s, int *proto, uint16_t *port)
+{
+	struct module_zeroconf_publish_data *d = s->userdata;
+	struct impl *impl = d->module->impl;
+	struct server *server;
+
+	spa_list_for_each(server, &impl->servers, link) {
+		if (server->addr.ss_family == AF_INET) {
+			*proto = AVAHI_PROTO_INET;
+			*port = ntohs(((struct sockaddr_in*) &server->addr)->sin_port);
+			return 0;
+		} else if (server->addr.ss_family == AF_INET6) {
+			*proto = AVAHI_PROTO_INET6;
+			*port = ntohs(((struct sockaddr_in6*) &server->addr)->sin6_port);
+			return 0;
+		}
+	}
+	return -ENODEV;
+}
+
 static void publish_service(struct service *s)
 {
-	if (!s->userdata->client || avahi_client_get_state(s->userdata->client) != AVAHI_CLIENT_S_RUNNING)
+	struct module_zeroconf_publish_data *d = s->userdata;
+	int proto;
+	uint16_t port;
+
+	if (find_port(s, &proto, &port) < 0)
+		return;
+
+	pw_log_debug("found proto:%d port:%d", proto, port);
+
+	if (!d->client || avahi_client_get_state(d->client) != AVAHI_CLIENT_S_RUNNING)
 		return;
 
+	s->published = true;
 	if (!s->entry_group) {
-		s->entry_group = avahi_entry_group_new(s->userdata->client, service_entry_group_callback, s);
+		s->entry_group = avahi_entry_group_new(d->client, service_entry_group_callback, s);
 		if (s->entry_group == NULL) {
 			pw_log_error("avahi_entry_group_new(): %s",
-					avahi_strerror(avahi_client_errno(s->userdata->client)));
-			return;
+					avahi_strerror(avahi_client_errno(d->client)));
+			goto error;
 		}
 	} else {
 		avahi_entry_group_reset(s->entry_group);
@@ -411,22 +448,22 @@ static void publish_service(struct service *s)
 
 	if (avahi_entry_group_add_service_strlst(
 				s->entry_group,
-				AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC,
+				AVAHI_IF_UNSPEC, proto,
 				0,
 				s->service_name,
 				s->service_type,
 				NULL,
 				NULL,
-				s->userdata->port,
+				port,
 				s->txt) < 0) {
 		pw_log_error("avahi_entry_group_add_service_strlst(): %s",
-			avahi_strerror(avahi_client_errno(s->userdata->client)));
-		return;
+			avahi_strerror(avahi_client_errno(d->client)));
+		goto error;
 	}
 
 	if (avahi_entry_group_add_service_subtype(
 				s->entry_group,
-				AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC,
+				AVAHI_IF_UNSPEC, proto,
 				0,
 				s->service_name,
 				s->service_type,
@@ -435,35 +472,40 @@ static void publish_service(struct service *s)
 				(s->subtype == SUBTYPE_HARDWARE ? SERVICE_SUBTYPE_SOURCE_HARDWARE : (s->subtype == SUBTYPE_VIRTUAL ? SERVICE_SUBTYPE_SOURCE_VIRTUAL : SERVICE_SUBTYPE_SOURCE_MONITOR))) < 0) {
 
 		pw_log_error("avahi_entry_group_add_service_subtype(): %s",
-			avahi_strerror(avahi_client_errno(s->userdata->client)));
-		return;
+			avahi_strerror(avahi_client_errno(d->client)));
+		goto error;
 	}
 
 	if (!s->is_sink && s->subtype != SUBTYPE_MONITOR) {
 		if (avahi_entry_group_add_service_subtype(
 					s->entry_group,
-					AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC,
+					AVAHI_IF_UNSPEC, proto,
 					0,
 					s->service_name,
 					SERVICE_TYPE_SOURCE,
 					NULL,
 					SERVICE_SUBTYPE_SOURCE_NON_MONITOR) < 0) {
 			pw_log_error("avahi_entry_group_add_service_subtype(): %s",
-					avahi_strerror(avahi_client_errno(s->userdata->client)));
-			return;
+					avahi_strerror(avahi_client_errno(d->client)));
+			goto error;
 		}
 	}
 
 	if (avahi_entry_group_commit(s->entry_group) < 0) {
 		pw_log_error("avahi_entry_group_commit(): %s",
-				avahi_strerror(avahi_client_errno(s->userdata->client)));
-		return;
+				avahi_strerror(avahi_client_errno(d->client)));
+		goto error;
 	}
 
 	spa_list_remove(&s->link);
-	spa_list_append(&s->userdata->published, &s->link);
+	spa_list_append(&d->published, &s->link);
 
 	pw_log_info("created service: %s", s->service_name);
+	return;
+
+error:
+	s->published = false;
+	return;
 }
 
 static void publish_pending(struct module_zeroconf_publish_data *data)
@@ -572,6 +614,28 @@ static const struct pw_manager_events manager_events = {
 	.removed = manager_removed,
 };
 
+
+static void impl_server_started(void *data, struct server *server)
+{
+	struct module_zeroconf_publish_data *d = data;
+	pw_log_info("a new server is started, try publish");
+	publish_pending(d);
+}
+
+static void impl_server_stopped(void *data, struct server *server)
+{
+	struct module_zeroconf_publish_data *d = data;
+	pw_log_info("a server stopped, try republish");
+	unpublish_all_services(d);
+	publish_pending(d);
+}
+
+static const struct impl_events impl_events = {
+	VERSION_IMPL_EVENTS,
+	.server_started = impl_server_started,
+	.server_stopped = impl_server_stopped,
+};
+
 static int module_zeroconf_publish_load(struct client *client, struct module *module)
 {
 	struct module_zeroconf_publish_data *data = module->user_data;
@@ -608,6 +672,8 @@ static int module_zeroconf_publish_load(struct client *client, struct module *mo
 	pw_manager_add_listener(data->manager, &data->manager_listener,
 			&manager_events, data);
 
+	impl_add_listener(module->impl, &data->impl_listener, &impl_events, data);
+
 	return 0;
 }
 
@@ -616,6 +682,8 @@ static int module_zeroconf_publish_unload(struct module *module)
 	struct module_zeroconf_publish_data *d = module->user_data;
 	struct service *s;
 
+	spa_hook_remove(&d->impl_listener);
+
 	unpublish_all_services(d);
 
 	spa_list_consume(s, &d->pending, link)
@@ -643,7 +711,6 @@ static int module_zeroconf_publish_unload(struct module *module)
 static const struct spa_dict_item module_zeroconf_publish_info[] = {
 	{ PW_KEY_MODULE_AUTHOR, "Sanchayan Maity <sanchayan@asymptotic.io" },
 	{ PW_KEY_MODULE_DESCRIPTION, "mDNS/DNS-SD Service Publish" },
-	{ PW_KEY_MODULE_USAGE, "port=<TCP port number>" },
 	{ PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
 };
 
@@ -653,7 +720,6 @@ static int module_zeroconf_publish_prepare(struct module * const module)
 
 	struct module_zeroconf_publish_data * const data = module->user_data;
 	data->module = module;
-	data->port = pw_properties_get_uint32(module->props, "port", PW_PROTOCOL_PULSE_DEFAULT_PORT);
 	spa_list_init(&data->pending);
 	spa_list_init(&data->published);
 
diff --git a/src/modules/module-protocol-pulse/pulse-server.c b/src/modules/module-protocol-pulse/pulse-server.c
index b403aba2140b893be7f3bf7ed9ff86032aa8a943..84bf3b56c5299aed0fc905073f35a9258f6fdfc7 100644
--- a/src/modules/module-protocol-pulse/pulse-server.c
+++ b/src/modules/module-protocol-pulse/pulse-server.c
@@ -85,11 +85,12 @@
 #define DEFAULT_MIN_QUANTUM	"256/48000"
 #define DEFAULT_FORMAT		"F32"
 #define DEFAULT_POSITION	"[ FL FR ]"
+#define DEFAULT_IDLE_TIMEOUT	"5"
 
 #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 MAX_BLOCK	(64*1024)
 
 #define TEMPORARY_MOVE_TIMEOUT	(SPA_NSEC_PER_SEC)
 
@@ -455,7 +456,7 @@ static void clamp_latency(struct stream *s, struct spa_fraction *lat)
 static uint64_t fix_playback_buffer_attr(struct stream *s, struct buffer_attr *attr,
 		uint32_t rate, struct spa_fraction *lat)
 {
-	uint32_t frame_size, max_prebuf, minreq, latency, max_latency;
+	uint32_t frame_size, max_prebuf, minreq, latency, max_latency, maxlength;
 	struct defs *defs = &s->impl->defs;
 
 	if ((frame_size = s->frame_size) == 0)
@@ -463,24 +464,26 @@ static uint64_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",
+	maxlength = SPA_ROUND_DOWN(MAXLENGTH, frame_size);
+
+	pw_log_info("[%s] maxlength:%u tlength:%u minreq:%u prebuf:%u max:%u",
 			s->client->name, attr->maxlength, attr->tlength,
-			attr->minreq, attr->prebuf);
+			attr->minreq, attr->prebuf, maxlength);
 
 	minreq = frac_to_bytes_round_up(s->min_req, &s->ss);
 	max_latency = defs->quantum_limit * frame_size;
 
-	if (attr->maxlength == (uint32_t) -1 || attr->maxlength > MAXLENGTH)
-		attr->maxlength = MAXLENGTH;
-	attr->maxlength = SPA_ROUND_UP(attr->maxlength, frame_size);
+	if (attr->maxlength == (uint32_t) -1 || attr->maxlength > maxlength)
+		attr->maxlength = maxlength;
+	else
+		attr->maxlength = SPA_ROUND_DOWN(attr->maxlength, frame_size);
 
 	minreq = SPA_MIN(minreq, attr->maxlength);
 
 	if (attr->tlength == (uint32_t) -1)
 		attr->tlength = frac_to_bytes_round_up(s->default_tlength, &s->ss);
-	attr->tlength = SPA_MIN(attr->tlength, attr->maxlength);
+	attr->tlength = SPA_CLAMP(attr->tlength, minreq, attr->maxlength);
 	attr->tlength = SPA_ROUND_UP(attr->tlength, frame_size);
-	attr->tlength = SPA_MAX(attr->tlength, minreq);
 
 	if (attr->minreq == (uint32_t) -1) {
 		uint32_t process = frac_to_bytes_round_up(s->default_req, &s->ss);
@@ -655,39 +658,46 @@ static int reply_create_playback_stream(struct stream *stream, struct pw_manager
 static uint64_t fix_record_buffer_attr(struct stream *s, struct buffer_attr *attr,
 		uint32_t rate, struct spa_fraction *lat)
 {
-	uint32_t frame_size, minfrag, latency;
+	uint32_t frame_size, minfrag, latency, maxlength;
 
 	if ((frame_size = s->frame_size) == 0)
 		frame_size = sample_spec_frame_size(&s->ss);
 	if (frame_size == 0)
 		frame_size = 4;
 
+	maxlength = SPA_ROUND_DOWN(MAXLENGTH, frame_size);
+
 	pw_log_info("[%s] maxlength:%u fragsize:%u framesize:%u",
 			s->client->name, attr->maxlength, attr->fragsize,
 			frame_size);
 
-	if (attr->maxlength == (uint32_t) -1 || attr->maxlength > MAXLENGTH)
-		attr->maxlength = MAXLENGTH;
-	attr->maxlength -= attr->maxlength % frame_size;
+	if (attr->maxlength == (uint32_t) -1 || attr->maxlength > maxlength)
+		attr->maxlength = maxlength;
+	else
+		attr->maxlength = SPA_ROUND_DOWN(attr->maxlength, frame_size);
 	attr->maxlength = SPA_MAX(attr->maxlength, frame_size);
 
 	minfrag = frac_to_bytes_round_up(s->min_frag, &s->ss);
 
 	if (attr->fragsize == (uint32_t) -1 || attr->fragsize == 0)
 		attr->fragsize = frac_to_bytes_round_up(s->default_frag, &s->ss);
-	attr->fragsize = SPA_MIN(attr->fragsize, attr->maxlength);
+	attr->fragsize = SPA_CLAMP(attr->fragsize, minfrag, attr->maxlength);
 	attr->fragsize = SPA_ROUND_UP(attr->fragsize, frame_size);
-	attr->fragsize = SPA_MAX(attr->fragsize, minfrag);
 
 	attr->tlength = attr->minreq = attr->prebuf = 0;
 
-	/* make sure can queue at least to fragsize without overruns */
-	if (attr->maxlength < attr->fragsize * 4)
+	/* make sure we can queue at least to fragsize without overruns */
+	if (attr->maxlength < attr->fragsize * 4) {
 		attr->maxlength = attr->fragsize * 4;
+		if (attr->maxlength > maxlength) {
+			attr->maxlength = maxlength;
+			attr->fragsize = SPA_ROUND_DOWN(maxlength / 4, frame_size);
+		}
+	}
 
-	latency = attr->fragsize / frame_size;
+	latency = attr->fragsize;
 
-	lat->num = latency;
+	lat->num = latency / frame_size;
 	lat->denom = rate;
 	clamp_latency(s, lat);
 
@@ -1232,7 +1242,7 @@ static void stream_param_changed(void *data, uint32_t id, const struct spa_pod *
 				SPA_PROP_mute, 1, &val, 0);
 		}
 		if (stream->corked)
-			pw_stream_set_active(stream->stream, false);
+			stream_set_paused(stream, true, "cork after create");
 
 		/* if peer exists, reply immediately, otherwise reply when the link is created */
 		peer = find_linked(stream->client->manager, stream->id, stream->direction);
@@ -1267,6 +1277,7 @@ struct process_data {
 	uint32_t minreq;
 	uint32_t quantum;
 	unsigned int underrun:1;
+	unsigned int idle:1;
 };
 
 static int
@@ -1306,6 +1317,17 @@ do_process_done(struct spa_loop *loop,
 			else
 				stream_send_started(stream);
 		}
+		if (pd->idle) {
+			if (!stream->is_idle) {
+				stream->idle_time = stream->timestamp;
+			} else if (!stream->is_paused &&
+			    stream->idle_timeout_sec > 0 &&
+			    stream->timestamp - stream->idle_time >
+					(stream->idle_timeout_sec * SPA_NSEC_PER_SEC)) {
+				stream_set_paused(stream, true, "long underrun");
+			}
+		}
+		stream->is_idle = pd->idle;
 		stream->playing_for += pd->playing_for;
 		if (stream->underrun_for != (uint64_t)-1)
 			stream->underrun_for += pd->underrun_for;
@@ -1341,7 +1363,8 @@ do_process_done(struct spa_loop *loop,
 			pw_log_trace("avail:%d index:%u", avail, index);
 
 			while ((uint32_t)avail >= stream->attr.fragsize) {
-				towrite = SPA_MIN(avail, MAX_FRAGSIZE);
+				towrite = SPA_MIN(avail, MAX_BLOCK);
+				towrite = SPA_MIN(towrite, stream->attr.fragsize);
 				towrite = SPA_ROUND_DOWN(towrite, stream->frame_size);
 
 				msg = message_alloc(impl, stream->channel, towrite);
@@ -1418,16 +1441,19 @@ static void stream_process(void *data)
 			}
 			if ((stream->attr.prebuf == 0 || do_flush) && !stream->corked) {
 				if (avail > 0) {
+					avail = SPA_MIN((uint32_t)avail, size);
 					spa_ringbuffer_read_data(&stream->ring,
 						stream->buffer, MAXLENGTH,
 						index % MAXLENGTH,
 						p, avail);
-					index += avail;
 				}
-				pd.playing_for = size;
+				index += size;
 				pd.read_inc = size;
 				spa_ringbuffer_read_update(&stream->ring, index);
+
+				pd.playing_for = size;
 			}
+			pd.idle = true;
 			pw_log_debug("%p: [%s] underrun read:%u avail:%d max:%u",
 					stream, client->name, index, avail, minreq);
 		} else {
@@ -1512,7 +1538,7 @@ static void stream_drained(void *data)
 		reply_simple_ack(stream->client, stream->drain_tag);
 		stream->drain_tag = 0;
 
-		pw_stream_set_active(stream->stream, true);
+		stream_set_paused(stream, false, "complete drain");
 	}
 }
 
@@ -2690,7 +2716,7 @@ static int do_cork_stream(struct client *client, uint32_t command, uint32_t tag,
 		return -ENOENT;
 
 	stream->corked = cork;
-	pw_stream_set_active(stream->stream, !cork);
+	stream_set_paused(stream, cork, "cork request");
 	if (cork) {
 		stream->is_underrun = true;
 	} else {
@@ -3450,7 +3476,7 @@ static int do_drain_stream(struct client *client, uint32_t command, uint32_t tag
 
 	stream->drain_tag = tag;
 	stream->draining = true;
-	pw_stream_set_active(stream->stream, true);
+	stream_set_paused(stream, false, "drain start");
 
 	return 0;
 }
@@ -5449,6 +5475,8 @@ static void impl_clear(struct impl *impl)
 	pw_map_for_each(&impl->samples, impl_free_sample, impl);
 	pw_map_clear(&impl->samples);
 
+	spa_hook_list_clean(&impl->hooks);
+
 #ifdef HAVE_DBUS
 	if (impl->dbus_name) {
 		dbus_release_name(impl->dbus_name);
@@ -5488,8 +5516,10 @@ static int parse_frac(struct pw_properties *props, const char *key, const char *
 	if (props == NULL ||
 	    (str = pw_properties_get(props, key)) == NULL)
 		str = def;
-	if (sscanf(str, "%u/%u", &res->num, &res->denom) != 2 || res->denom == 0)
-		return -EINVAL;
+	if (sscanf(str, "%u/%u", &res->num, &res->denom) != 2 || res->denom == 0) {
+		pw_log_warn(": invalid fraction %s, default to %s", str, def);
+		sscanf(def, "%u/%u", &res->num, &res->denom);
+	}
 	pw_log_info(": defaults: %s = %u/%u", key, res->num, res->denom);
 	return 0;
 }
@@ -5529,7 +5559,21 @@ static int parse_format(struct pw_properties *props, const char *key, const char
 		pw_log_warn(": unknown format %s, default to %s", str, def);
 		res->format = format_name2id(def);
 	}
-	pw_log_info(": defaults: %s = %s", key, str);
+	pw_log_info(": defaults: %s = %s", key, format_id2name(res->format));
+	return 0;
+}
+static int parse_uint32(struct pw_properties *props, const char *key, const char *def,
+		uint32_t *res)
+{
+	const char *str;
+	if (props == NULL ||
+	    (str = pw_properties_get(props, key)) == NULL)
+		str = def;
+	if (!spa_atou32(str, res, 0)) {
+		pw_log_warn(": invalid uint32_t %s, default to %s", str, def);
+		spa_atou32(def, res, 0);
+	}
+	pw_log_info(": defaults: %s = %u", key, *res);
 	return 0;
 }
 
@@ -5543,6 +5587,7 @@ static void load_defaults(struct defs *def, struct pw_properties *props)
 	parse_frac(props, "pulse.min.quantum", DEFAULT_MIN_QUANTUM, &def->min_quantum);
 	parse_format(props, "pulse.default.format", DEFAULT_FORMAT, &def->sample_spec);
 	parse_position(props, "pulse.default.position", DEFAULT_POSITION, &def->channel_map);
+	parse_uint32(props, "pulse.idle.timeout", DEFAULT_IDLE_TIMEOUT, &def->idle_timeout);
 	def->sample_spec.channels = def->channel_map.channels;
 	def->quantum_limit = 8192;
 }
@@ -5587,6 +5632,7 @@ struct pw_protocol_pulse *pw_protocol_pulse_new(struct pw_context *context,
 
 	impl->work_queue = pw_context_get_work_queue(context);
 
+	spa_hook_list_init(&impl->hooks);
 	spa_list_init(&impl->servers);
 	impl->rate_limit.interval = 2 * SPA_NSEC_PER_SEC;
 	impl->rate_limit.burst = 1;
@@ -5638,6 +5684,13 @@ error_exit:
 	return NULL;
 }
 
+void impl_add_listener(struct impl *impl,
+		struct spa_hook *listener,
+		const struct impl_events *events, void *data)
+{
+	spa_hook_list_append(&impl->hooks, listener, events, data);
+}
+
 void *pw_protocol_pulse_get_user_data(struct pw_protocol_pulse *pulse)
 {
 	return SPA_PTROFF(pulse, sizeof(struct impl), void);
diff --git a/src/modules/module-protocol-pulse/quirks.c b/src/modules/module-protocol-pulse/quirks.c
index e00eafe91739ba6922894bdaeede3de37f1c8bfc..eb2243839afee3966696a62c1965d43182993227 100644
--- a/src/modules/module-protocol-pulse/quirks.c
+++ b/src/modules/module-protocol-pulse/quirks.c
@@ -38,10 +38,9 @@ static uint64_t parse_quirks(const char *str)
 		{ "force-s16-info", QUIRK_FORCE_S16_FORMAT },
 		{ "remove-capture-dont-move", QUIRK_REMOVE_CAPTURE_DONT_MOVE },
 	};
-	size_t i;
-	for (i = 0; i < SPA_N_ELEMENTS(quirk_keys); ++i) {
-		if (spa_streq(str, quirk_keys[i].key))
-			return quirk_keys[i].value;
+	SPA_FOR_EACH_ELEMENT_VAR(quirk_keys, i) {
+		if (spa_streq(str, i->key))
+			return i->value;
 	}
 	return 0;
 }
diff --git a/src/modules/module-protocol-pulse/server.c b/src/modules/module-protocol-pulse/server.c
index af1913b9518033c35924f3229fcee8e018362743..503746f8f83dbbff1e1e1c5b9cd70720e92ba0f9 100644
--- a/src/modules/module-protocol-pulse/server.c
+++ b/src/modules/module-protocol-pulse/server.c
@@ -191,6 +191,9 @@ static int handle_memblock(struct client *client, struct message *msg)
 
 	stream_send_request(stream);
 
+	if (stream->is_paused && !stream->corked)
+		stream_set_paused(stream, false, "new data");
+
 finish:
 	message_free(msg, false, false);
 	return res;
@@ -616,7 +619,6 @@ static int start_unix_server(struct server *server, const struct sockaddr_storag
 			pw_log_warn("server %p: unlink('%s') failed: %m",
 				    server, addr_un->sun_path);
 	}
-
 	if (bind(fd, (const struct sockaddr *) addr_un, SUN_LEN(addr_un)) < 0) {
 		res = -errno;
 		pw_log_warn("server %p: bind() to '%s' failed: %m",
@@ -624,6 +626,10 @@ static int start_unix_server(struct server *server, const struct sockaddr_storag
 		goto error_close;
 	}
 
+	if (chmod(addr_un->sun_path, 0777) < 0)
+		pw_log_warn("server %p: chmod('%s') failed: %m",
+				    server, addr_un->sun_path);
+
 	if (listen(fd, server->listen_backlog) < 0) {
 		res = -errno;
 		pw_log_warn("server %p: listen() on '%s' failed: %m",
@@ -896,7 +902,7 @@ static struct server *server_new(struct impl *impl)
 
 static int server_start(struct server *server, const struct sockaddr_storage *addr)
 {
-	const struct impl * const impl = server->impl;
+	struct impl * const impl = server->impl;
 	int res = 0, fd;
 
 	switch (addr->ss_family) {
@@ -921,6 +927,8 @@ static int server_start(struct server *server, const struct sockaddr_storage *ad
 		res = -errno;
 		pw_log_error("server %p: can't create server source: %m", impl);
 	}
+	if (res >= 0)
+		spa_hook_list_call(&impl->hooks, struct impl_events, server_started, 0, server);
 
 	return res;
 }
@@ -1066,6 +1074,8 @@ void server_free(struct server *server)
 		client_unref(c);
 	}
 
+	spa_hook_list_call(&impl->hooks, struct impl_events, server_stopped, 0, server);
+
 	if (server->source)
 		pw_loop_destroy_source(impl->loop, server->source);
 
diff --git a/src/modules/module-protocol-pulse/stream.c b/src/modules/module-protocol-pulse/stream.c
index 730580da3a6b6709750e612d0161a21ee711376f..fb430908608ba59d2f8f869427ea1c617a6d452f 100644
--- a/src/modules/module-protocol-pulse/stream.c
+++ b/src/modules/module-protocol-pulse/stream.c
@@ -64,6 +64,7 @@ struct stream *stream_new(struct client *client, enum stream_type type, uint32_t
 {
 	int res;
 	struct defs *defs = &client->impl->defs;
+	const char *str;
 
 	struct stream *stream = calloc(1, sizeof(*stream));
 	if (stream == NULL)
@@ -90,6 +91,9 @@ struct stream *stream_new(struct client *client, enum stream_type type, uint32_t
 	parse_frac(client->props, "pulse.default.req", &defs->default_req, &stream->default_req);
 	parse_frac(client->props, "pulse.default.frag", &defs->default_frag, &stream->default_frag);
 	parse_frac(client->props, "pulse.default.tlength", &defs->default_tlength, &stream->default_tlength);
+	stream->idle_timeout_sec = defs->idle_timeout;
+	if ((str = pw_properties_get(client->props, "pulse.idle.timeout")) != NULL)
+		spa_atou32(str, &stream->idle_timeout_sec, 0);
 
 	switch (type) {
 	case STREAM_TYPE_RECORD:
@@ -207,6 +211,20 @@ uint32_t stream_pop_missing(struct stream *stream)
 	return missing;
 }
 
+void stream_set_paused(struct stream *stream, bool paused, const char *reason)
+{
+	if (stream->is_paused == paused)
+		return;
+
+	if (reason && stream->client)
+		pw_log_info("%p: [%s] %s because of %s",
+				stream, stream->client->name,
+				paused ? "paused" : "resumed", reason);
+
+	stream->is_paused = paused;
+	pw_stream_set_active(stream->stream, !paused);
+}
+
 int stream_send_underflow(struct stream *stream, int64_t offset)
 {
 	struct client *client = stream->client;
diff --git a/src/modules/module-protocol-pulse/stream.h b/src/modules/module-protocol-pulse/stream.h
index 7ee6de36b48521f041a663ce11d85f5982af8194..424b2a1e8b4ea2ffdfbbf5578b7183a174ddcf1d 100644
--- a/src/modules/module-protocol-pulse/stream.h
+++ b/src/modules/module-protocol-pulse/stream.h
@@ -82,6 +82,7 @@ struct stream {
 	uint64_t playing_for;
 	uint64_t ticks_base;
 	uint64_t timestamp;
+	uint64_t idle_time;
 	int64_t delay;
 
 	uint32_t last_quantum;
@@ -93,6 +94,7 @@ struct stream {
 	struct spa_fraction default_frag;
 	struct spa_fraction default_tlength;
 	struct spa_fraction min_quantum;
+	uint32_t idle_timeout_sec;
 
 	struct sample_spec ss;
 	struct channel_map map;
@@ -115,6 +117,8 @@ struct stream {
 	unsigned int in_prebuf:1;
 	unsigned int killed:1;
 	unsigned int pending:1;
+	unsigned int is_idle:1;
+	unsigned int is_paused:1;
 };
 
 struct stream *stream_new(struct client *client, enum stream_type type, uint32_t create_tag,
@@ -124,6 +128,8 @@ void stream_free(struct stream *stream);
 void stream_flush(struct stream *stream);
 uint32_t stream_pop_missing(struct stream *stream);
 
+void stream_set_paused(struct stream *stream, bool paused, const char *reason);
+
 int stream_send_underflow(struct stream *stream, int64_t offset);
 int stream_send_overflow(struct stream *stream);
 int stream_send_killed(struct stream *stream);
diff --git a/src/modules/module-protocol-pulse/utils.c b/src/modules/module-protocol-pulse/utils.c
index 1c9bc1c5f30cb4d4188e6248976bbbbd31478d51..617ca05244ffd3e0fe542d1295887412eaaf14e9 100644
--- a/src/modules/module-protocol-pulse/utils.c
+++ b/src/modules/module-protocol-pulse/utils.c
@@ -195,7 +195,7 @@ int create_pid_file(void) {
 
 	strcat(pid_file, "/pid");
 
-	if ((f = fopen(pid_file, "w")) == NULL) {
+	if ((f = fopen(pid_file, "we")) == NULL) {
 		res = -errno;
 		pw_log_error("failed to open pid file: %m");
 		return res;
diff --git a/src/modules/module-pulse-tunnel.c b/src/modules/module-pulse-tunnel.c
index f0231b9f3117e830551a64686ce8fec77337e95a..2c9a97df781fd21130ead6e3754dff392fbd2309 100644
--- a/src/modules/module-pulse-tunnel.c
+++ b/src/modules/module-pulse-tunnel.c
@@ -72,7 +72,7 @@
  *                  (Default `sink`)
  * - `pulse.server.address`: the address of the PulseAudio server to tunnel to.
  * - `pulse.latency`: the latency to end-to-end latency in milliseconds to
- *                    maintain (Default 200ms).
+ *                    maintain (Default 200).
  * - `stream.props`: Extra properties for the local stream.
  *
  * ## General options
@@ -101,6 +101,7 @@
  *         tunnel.mode = sink
  *         # Set the remote address to tunnel to
  *         pulse.server.address = "tcp:192.168.1.126"
+ *         #pulse.latency = 200
  *         #audio.rate=<sample rate>
  *         #audio.channels=<number of channels>
  *         #audio.position=<channel map>
@@ -183,6 +184,8 @@ struct impl {
 	pa_context *pa_context;
 	pa_stream *pa_stream;
 
+	struct ratelimit rate_limit;
+
 	uint32_t target_latency;
 	uint32_t current_latency;
 	uint32_t target_buffer;
@@ -524,9 +527,10 @@ static void stream_write_request_cb(pa_stream *s, size_t length, void *userdata)
 {
 	struct impl *impl = userdata;
 	int32_t avail;
-	uint32_t index, len, offset, l0, l1;
+	uint32_t index;
+	size_t size;
 	pa_usec_t latency;
-	int negative;
+	int negative, res;
 
 	if (impl->resync) {
 		impl->resync = false;
@@ -542,43 +546,54 @@ static void stream_write_request_cb(pa_stream *s, size_t length, void *userdata)
 	impl->current_latency += avail / impl->frame_size;
 
 	while (avail < (int32_t)length) {
+		uint32_t maxsize = SPA_ROUND_DOWN(sizeof(impl->empty), impl->frame_size);
 		/* send silence for the data we don't have */
-		len = SPA_MIN(length - avail, sizeof(impl->empty));
-		pa_stream_write(impl->pa_stream,
-				impl->empty, len,
-				NULL, 0, PA_SEEK_RELATIVE);
-		length -= len;
+		size = SPA_MIN(length - avail, maxsize);
+		if ((res = pa_stream_write(impl->pa_stream,
+				impl->empty, size,
+				NULL, 0, PA_SEEK_RELATIVE)) != 0)
+			pw_log_warn("error writing stream: %s", pa_strerror(res));
+		length -= size;
 	}
-	if (length > 0 && avail >= (int32_t)length) {
-		/* always send as much as is requested */
-		len = length;
-		offset = index & RINGBUFFER_MASK;
-		l0 = SPA_MIN(len, RINGBUFFER_SIZE - offset);
-		l1 = len - l0;
-
-		pa_stream_write(impl->pa_stream,
-				SPA_PTROFF(impl->buffer, offset, void), l0,
-				NULL, 0, PA_SEEK_RELATIVE);
-
-		if (SPA_UNLIKELY(l1 > 0)) {
-			pa_stream_write(impl->pa_stream,
-					impl->buffer, l1,
-					NULL, 0, PA_SEEK_RELATIVE);
-		}
-		index += len;
+	while (length > 0 && avail >= (int32_t)length) {
+		void *data;
+
+		size = length;
+		pa_stream_begin_write(impl->pa_stream, &data, &size);
+
+		spa_ringbuffer_read_data(&impl->ring,
+				impl->buffer, RINGBUFFER_SIZE,
+				index & RINGBUFFER_MASK,
+				data, size);
+
+		if ((res = pa_stream_write(impl->pa_stream,
+			data, size, NULL, 0, PA_SEEK_RELATIVE)) != 0)
+			pw_log_warn("error writing stream: %zd %s", size,
+					pa_strerror(res));
+
+		index += size;
+		length -= size;
+		avail -= size;
 		spa_ringbuffer_read_update(&impl->ring, index);
 	}
 }
 static void stream_underflow_cb(pa_stream *s, void *userdata)
 {
 	struct impl *impl = userdata;
-	pw_log_warn("underflow");
+	struct timespec ts;
+
+	clock_gettime(CLOCK_MONOTONIC, &ts);
+	if (ratelimit_test(&impl->rate_limit, SPA_TIMESPEC_TO_NSEC(&ts), SPA_LOG_LEVEL_WARN))
+		pw_log_warn("underflow");
 	impl->resync = true;
 }
 static void stream_overflow_cb(pa_stream *s, void *userdata)
 {
 	struct impl *impl = userdata;
-	pw_log_warn("overflow");
+	struct timespec ts;
+	clock_gettime(CLOCK_MONOTONIC, &ts);
+	if (ratelimit_test(&impl->rate_limit, SPA_TIMESPEC_TO_NSEC(&ts), SPA_LOG_LEVEL_WARN))
+		pw_log_warn("overflow");
 	impl->resync = true;
 }
 
@@ -606,13 +621,14 @@ static pa_proplist* tunnel_new_proplist(struct impl *impl)
 static int create_pulse_stream(struct impl *impl)
 {
 	pa_sample_spec ss;
+	pa_channel_map map;
 	const char *server_address, *remote_node_target;
 	pa_proplist *props = NULL;
 	pa_mainloop_api *api;
 	char stream_name[1024];
 	pa_buffer_attr bufferattr;
 	int res = -EIO;
-	uint32_t latency_bytes;
+	uint32_t latency_bytes, i, aux = 0;
 
 	if ((impl->pa_mainloop = pa_threaded_mainloop_new()) == NULL)
 		goto error;
@@ -659,10 +675,14 @@ static int create_pulse_stream(struct impl *impl)
 	ss.channels = impl->info.channels;
 	ss.rate = impl->info.rate;
 
+	map.channels = impl->info.channels;
+	for (i = 0; i < map.channels; i++)
+		map.map[i] = (pa_channel_position_t)channel_id2pa(impl->info.position[i], &aux);
+
 	snprintf(stream_name, sizeof(stream_name), _("Tunnel for %s@%s"),
 			pw_get_user_name(), pw_get_host_name());
 
-	if (!(impl->pa_stream = pa_stream_new(impl->pa_context, stream_name, &ss, NULL))) {
+	if (!(impl->pa_stream = pa_stream_new(impl->pa_context, stream_name, &ss, &map))) {
 		res = pa_context_errno(impl->pa_context);
 		goto error_unlock;
 	}
@@ -701,6 +721,8 @@ static int create_pulse_stream(struct impl *impl)
 				PA_STREAM_AUTO_TIMING_UPDATE);
 	} else {
 		bufferattr.tlength = latency_bytes / 2;
+		bufferattr.minreq = bufferattr.tlength / 4;
+		bufferattr.prebuf = bufferattr.tlength;
 
 		res = pa_stream_connect_playback(impl->pa_stream,
 				remote_node_target, &bufferattr,
@@ -951,6 +973,8 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
 	spa_ringbuffer_init(&impl->ring);
 	impl->buffer = calloc(1, RINGBUFFER_SIZE);
 	spa_dll_init(&impl->dll);
+	impl->rate_limit.interval = 2 * SPA_NSEC_PER_SEC;
+	impl->rate_limit.burst = 1;
 
 	if ((str = pw_properties_get(props, "tunnel.mode")) != NULL) {
 		if (spa_streq(str, "source")) {
@@ -989,6 +1013,7 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
 	copy_props(impl, props, PW_KEY_NODE_GROUP);
 	copy_props(impl, props, PW_KEY_NODE_LATENCY);
 	copy_props(impl, props, PW_KEY_NODE_VIRTUAL);
+	copy_props(impl, props, PW_KEY_NODE_NETWORK);
 	copy_props(impl, props, PW_KEY_MEDIA_CLASS);
 
 	parse_audio_info(impl->stream_props, &impl->info);
diff --git a/src/modules/module-raop-discover.c b/src/modules/module-raop-discover.c
index dd26bad950630b0c952dce8ffeaf01f7663d0c49..483e7d301bee0942e2371ae82658034fb2ce1b20 100644
--- a/src/modules/module-raop-discover.c
+++ b/src/modules/module-raop-discover.c
@@ -113,7 +113,7 @@ struct tunnel_info {
 	const char *domain;
 };
 
-#define TUNNEL_INFO(...) (struct tunnel_info){ __VA_ARGS__ }
+#define TUNNEL_INFO(...) ((struct tunnel_info){ __VA_ARGS__ })
 
 struct tunnel {
 	struct spa_list link;
diff --git a/src/modules/module-raop-sink.c b/src/modules/module-raop-sink.c
index fe6288341bb3d4304d4e1b3d6e43c46a65ee2213..4578411d1f7430fe5698d166ded21eb09006a101 100644
--- a/src/modules/module-raop-sink.c
+++ b/src/modules/module-raop-sink.c
@@ -108,12 +108,12 @@
  *         raop.hostname = "my-raop-device"
  *         raop.port = 8190
  *         #raop.transport = "udp"
- *         raop.encryption = "RSA"
+ *         raop.encryption.type = "RSA"
  *         #raop.audio.codec = "PCM"
  *         #raop.password = "****"
  *         #audio.format = "S16"
  *         #audio.rate = 44100
- *         #audio.channels = 22
+ *         #audio.channels = 2
  *         #audio.position = [ FL FR ]
  *         stream.props = {
  *             # extra sink properties
@@ -157,7 +157,7 @@ PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
 #define DEFAULT_CHANNELS 2
 #define DEFAULT_POSITION "[ FL FR ]"
 
-#define DEFAULT_LATENCY (DEFAULT_RATE*2)
+#define DEFAULT_LATENCY 22050
 
 #define MODULE_USAGE	"[ raop.hostname=<name of host> ] "					\
 			"[ raop.port=<remote port> ] "						\
@@ -421,6 +421,7 @@ static int flush_to_udp_packet(struct impl *impl)
 
 	switch (impl->codec) {
 	case CODEC_PCM:
+	case CODEC_ALAC:
 		len = write_codec_pcm(dst, impl->buffer, n_frames);
 		break;
 	default:
@@ -428,7 +429,7 @@ static int flush_to_udp_packet(struct impl *impl)
 		memset(dst, 0, len);
 		break;
 	}
-	if (impl->encryption != CRYPTO_NONE)
+	if (impl->encryption == CRYPTO_RSA)
 		aes_encrypt(impl, dst, len);
 
 	impl->rtptime += n_frames;
@@ -463,6 +464,7 @@ static int flush_to_tcp_packet(struct impl *impl)
 
 	switch (impl->codec) {
 	case CODEC_PCM:
+	case CODEC_ALAC:
 		len = write_codec_pcm(dst, impl->buffer, n_frames);
 		break;
 	default:
@@ -470,7 +472,7 @@ static int flush_to_tcp_packet(struct impl *impl)
 		memset(dst, 0, len);
 		break;
 	}
-	if (impl->encryption != CRYPTO_NONE)
+	if (impl->encryption == CRYPTO_RSA)
 		aes_encrypt(impl, dst, len);
 
 	pkt[0] |= htonl((uint32_t) len + 12);
@@ -577,10 +579,10 @@ static int create_udp_socket(struct impl *impl, uint16_t *port)
 
 		if (ip_version == 4) {
 			sa4.sin_port = htons(*port);
-			ret = bind(fd, &sa4, sizeof(sa4));
+			ret = bind(fd, (struct sockaddr*)&sa4, sizeof(sa4));
 		} else {
 			sa6.sin6_port = htons(*port);
-			ret = bind(fd, &sa6, sizeof(sa6));
+			ret = bind(fd, (struct sockaddr*)&sa6, sizeof(sa6));
 		}
 		if (ret == 0)
 			break;
@@ -722,9 +724,10 @@ on_control_source_io(void *data, int fd, uint32_t mask)
 	}
 }
 
-static void rtsp_flush_reply(void *data, int status, const struct spa_dict *headers)
+static int rtsp_flush_reply(void *data, int status, const struct spa_dict *headers)
 {
 	pw_log_info("reply %d", status);
+	return 0;
 }
 
 static int rtsp_do_flush(struct impl *impl)
@@ -749,7 +752,7 @@ static int rtsp_do_flush(struct impl *impl)
 	return res;
 }
 
-static void rtsp_record_reply(void *data, int status, const struct spa_dict *headers)
+static int rtsp_record_reply(void *data, int status, const struct spa_dict *headers)
 {
 	struct impl *impl = data;
 	const char *str;
@@ -758,6 +761,7 @@ static void rtsp_record_reply(void *data, int status, const struct spa_dict *hea
 	uint8_t buffer[1024];
 	struct spa_pod_builder b;
 	struct spa_latency_info latency;
+	char progress[128];
 
 	pw_log_info("reply %d", status);
 
@@ -782,6 +786,10 @@ static void rtsp_record_reply(void *data, int status, const struct spa_dict *hea
 	impl->sync = 0;
 	impl->sync_period = impl->info.rate / (impl->block_size / impl->frame_size);
 	impl->recording = true;
+
+	snprintf(progress, sizeof(progress), "progress: %s/%s/%s\r\n", "0", "0", "0");
+	return pw_rtsp_client_send(impl->rtsp, "SET_PARAMETER", NULL,
+			"text/parameters", progress, NULL, NULL);
 }
 
 static int rtsp_do_record(struct impl *impl)
@@ -835,7 +843,7 @@ error:
 	pw_loop_update_io(impl->loop, impl->server_source, 0);
 }
 
-static void rtsp_setup_reply(void *data, int status, const struct spa_dict *headers)
+static int rtsp_setup_reply(void *data, int status, const struct spa_dict *headers)
 {
 	struct impl *impl = data;
 	const char *str, *state = NULL, *s;
@@ -847,13 +855,13 @@ static void rtsp_setup_reply(void *data, int status, const struct spa_dict *head
 
 	if ((str = spa_dict_lookup(headers, "Session")) == NULL) {
 		pw_log_error("missing Session header");
-		return;
+		return 0;
 	}
 	pw_properties_set(impl->headers, "Session", str);
 
 	if ((str = spa_dict_lookup(headers, "Transport")) == NULL) {
 		pw_log_error("missing Transport header");
-		return;
+		return 0;
 	}
 
 	impl->server_port = control_port = timing_port = 0;
@@ -870,18 +878,21 @@ static void rtsp_setup_reply(void *data, int status, const struct spa_dict *head
 	}
 	if (impl->server_port == 0) {
 		pw_log_error("missing server port in Transport");
-		return;
+		return 0;
 	}
 
-	pw_getrandom(&impl->seq, sizeof(impl->seq), 0);
-	pw_getrandom(&impl->rtptime, sizeof(impl->rtptime), 0);
+	if (pw_getrandom(&impl->seq, sizeof(impl->seq), 0) < 0 ||
+	    pw_getrandom(&impl->rtptime, sizeof(impl->rtptime), 0) <  0) {
+		pw_log_error("error generating random seq and rtptime: %m");
+		return 0;
+	}
 
 	pw_log_info("server port:%u", impl->server_port);
 
 	switch (impl->protocol) {
 	case PROTO_TCP:
-		if ((impl->server_fd = connect_socket(impl, SOCK_STREAM, -1, impl->server_port)) <= 0)
-			return;
+		if ((impl->server_fd = connect_socket(impl, SOCK_STREAM, -1, impl->server_port)) < 0)
+			return impl->server_fd;
 
 		impl->server_source = pw_loop_add_io(impl->loop, impl->server_fd,
 				SPA_IO_OUT, false, on_server_source_io, impl);
@@ -890,16 +901,16 @@ static void rtsp_setup_reply(void *data, int status, const struct spa_dict *head
 	case PROTO_UDP:
 		if (control_port == 0 || timing_port == 0) {
 			pw_log_error("missing UDP ports in Transport");
-			return;
+			return 0;
 		}
 		pw_log_info("control:%u timing:%u", control_port, timing_port);
 
-		if ((impl->server_fd = connect_socket(impl, SOCK_DGRAM, -1, impl->server_port)) <= 0)
-			return;
-		if ((impl->control_fd = connect_socket(impl, SOCK_DGRAM, impl->control_fd, control_port)) <= 0)
-			return;
-		if ((impl->timing_fd = connect_socket(impl, SOCK_DGRAM, impl->timing_fd, timing_port)) <= 0)
-			return;
+		if ((impl->server_fd = connect_socket(impl, SOCK_DGRAM, -1, impl->server_port)) < 0)
+			return impl->server_fd;
+		if ((impl->control_fd = connect_socket(impl, SOCK_DGRAM, impl->control_fd, control_port)) < 0)
+			return impl->control_fd;
+		if ((impl->timing_fd = connect_socket(impl, SOCK_DGRAM, impl->timing_fd, timing_port)) < 0)
+			return impl->timing_fd;
 
 		ntp = ntp_now(CLOCK_MONOTONIC);
 		send_udp_timing_packet(impl, ntp, ntp, NULL, 0);
@@ -912,8 +923,9 @@ static void rtsp_setup_reply(void *data, int status, const struct spa_dict *head
 			rtsp_do_record(impl);
 		break;
 	default:
-		return;
+		return 0;
 	}
+	return 0;
 }
 
 static int rtsp_do_setup(struct impl *impl)
@@ -964,7 +976,7 @@ error:
 	return -EIO;
 }
 
-static void rtsp_announce_reply(void *data, int status, const struct spa_dict *headers)
+static int rtsp_announce_reply(void *data, int status, const struct spa_dict *headers)
 {
 	struct impl *impl = data;
 
@@ -972,7 +984,7 @@ static void rtsp_announce_reply(void *data, int status, const struct spa_dict *h
 
 	pw_properties_set(impl->headers, "Apple-Challenge", NULL);
 
-	rtsp_do_setup(impl);
+	return rtsp_do_setup(impl);
 }
 
 static void base64_encode(const uint8_t *data, size_t len, char *enc, char pad)
@@ -1059,7 +1071,8 @@ static int rtsp_do_announce(struct impl *impl)
 	int res, frames, i, ip_version;
 	char *sdp;
 	char local_ip[256];
-
+	int min_latency;
+	min_latency = DEFAULT_LATENCY;
 	host = pw_properties_get(impl->props, "raop.hostname");
 
 	if (impl->protocol == PROTO_TCP)
@@ -1086,10 +1099,26 @@ static int rtsp_do_announce(struct impl *impl)
 				ip_version, host, frames);
 		break;
 
+	case CRYPTO_AUTH_SETUP:
+		asprintf(&sdp, "v=0\r\n"
+				"o=iTunes %s 0 IN IP%d %s\r\n"
+				"s=iTunes\r\n"
+				"c=IN IP%d %s\r\n"
+				"t=0 0\r\n"
+				"m=audio 0 RTP/AVP 96\r\n"
+				"a=rtpmap:96 AppleLossless\r\n"
+				"a=fmtp:96 %d 0 16 40 10 14 2 255 0 0 44100\r\n"
+				"a=min-latency:%d",
+				impl->session_id, ip_version, local_ip,
+				ip_version, host, frames, min_latency);
+		break;
+
 	case CRYPTO_RSA:
-		pw_getrandom(impl->key, sizeof(impl->key), 0);
+		if (pw_getrandom(impl->key, sizeof(impl->key), 0) < 0 ||
+		    pw_getrandom(impl->iv, sizeof(impl->iv), 0) < 0)
+			return -errno;
+
 		AES_set_encrypt_key(impl->key, 128, &impl->aes);
-		pw_getrandom(impl->iv, sizeof(impl->iv), 0);
 
 		i = rsa_encrypt(impl->key, 16, rsakey);
 	        base64_encode(rsakey, i, key, '=');
@@ -1118,15 +1147,13 @@ 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)
+static int 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);
+	return rtsp_do_announce(impl);
 }
 
 static int rtsp_do_auth_setup(struct impl *impl)
@@ -1159,16 +1186,22 @@ static const char *find_attr(char **tokens, const char *key)
 	return NULL;
 }
 
-static void rtsp_auth_reply(void *data, int status, const struct spa_dict *headers)
+static int rtsp_auth_reply(void *data, int status, const struct spa_dict *headers)
 {
 	struct impl *impl = data;
+	int res = 0;
+
 	pw_log_info("auth %d", status);
 
 	switch (status) {
 	case 200:
-		rtsp_do_announce(impl);
+		if (impl->encryption == CRYPTO_AUTH_SETUP)
+			res = rtsp_do_auth_setup(impl);
+		else
+			res = rtsp_do_announce(impl);
 		break;
 	}
+	return res;
 }
 
 SPA_PRINTF_FUNC(2,3)
@@ -1217,7 +1250,7 @@ static int rtsp_do_auth(struct impl *impl, const struct spa_dict *headers)
 		spa_scnprintf(auth, sizeof(auth), "Basic %s", enc);
 	}
 	else if (spa_streq(tokens[0], "Digest")) {
-		const char *realm, *nonce;
+		const char *realm, *nonce, *url;
 		char h1[MD5_HASH_LENGTH+1];
 		char h2[MD5_HASH_LENGTH+1];
 		char resp[MD5_HASH_LENGTH+1];
@@ -1227,13 +1260,15 @@ static int rtsp_do_auth(struct impl *impl, const struct spa_dict *headers)
 		if (realm == NULL || nonce == NULL)
 			goto error;
 
+		url = pw_rtsp_client_get_url(impl->rtsp);
+
 		MD5_hash(h1, "%s:%s:%s", DEFAULT_USER_NAME, realm, impl->password);
-		MD5_hash(h2, "OPTIONS:*");
+		MD5_hash(h2, "OPTIONS:%s", url);
 		MD5_hash(resp, "%s:%s:%s", h1, nonce, h2);
 
 		spa_scnprintf(auth, sizeof(auth),
-				"Digest username=\"%s\", realm=\"%s\", nonce=\"%s\", uri=\"*\", response=\"%s\"",
-				DEFAULT_USER_NAME, realm, nonce, resp);
+				"username=\"%s\", realm=\"%s\", nonce=\"%s\", uri=\"%s\", response=\"%s\"",
+				DEFAULT_USER_NAME, realm, nonce, url, resp);
 	}
 	else
 		goto error;
@@ -1251,22 +1286,25 @@ error:
 	return -EINVAL;
 }
 
-static void rtsp_options_reply(void *data, int status, const struct spa_dict *headers)
+static int rtsp_options_reply(void *data, int status, const struct spa_dict *headers)
 {
 	struct impl *impl = data;
+	int res = 0;
+
 	pw_log_info("options %d", status);
 
 	switch (status) {
 	case 401:
-		rtsp_do_auth(impl, headers);
+		res = rtsp_do_auth(impl, headers);
 		break;
 	case 200:
 		if (impl->encryption == CRYPTO_AUTH_SETUP)
-			rtsp_do_auth_setup(impl);
+			res = rtsp_do_auth_setup(impl);
 		else
-			rtsp_do_announce(impl);
+			res = rtsp_do_announce(impl);
 		break;
 	}
+	return res;
 }
 
 static void rtsp_connected(void *data)
@@ -1280,11 +1318,15 @@ static void rtsp_connected(void *data)
 
 	impl->connected = true;
 
-	pw_getrandom(sci, sizeof(sci), 0);
+	if (pw_getrandom(sci, sizeof(sci), 0) < 0 ||
+	    pw_getrandom(rac, sizeof(rac), 0) < 0) {
+		pw_log_error("error generating random data: %m");
+		return;
+	}
+
 	pw_properties_setf(impl->headers, "Client-Instance",
 			"%08x%08x", sci[0], sci[1]);
 
-	pw_getrandom(rac, sizeof(rac), 0);
 	base64_encode(rac, sizeof(rac), sac, '\0');
 	pw_properties_set(impl->headers, "Apple-Challenge", sac);
 
@@ -1297,29 +1339,29 @@ static void rtsp_connected(void *data)
 static void connection_cleanup(struct impl *impl)
 {
 	impl->ready = false;
-	if (impl->server_fd != -1) {
+	if (impl->server_source != NULL) {
+		pw_loop_destroy_source(impl->loop, impl->server_source);
+		impl->server_source = NULL;
+	}
+	if (impl->server_fd >= 0) {
 		close(impl->server_fd);
 		impl->server_fd = -1;
 	}
-	if (impl->control_fd != -1) {
+	if (impl->control_source != NULL) {
+		pw_loop_destroy_source(impl->loop, impl->control_source);
+		impl->control_source = NULL;
+	}
+	if (impl->control_fd >= 0) {
 		close(impl->control_fd);
 		impl->control_fd = -1;
 	}
-	if (impl->timing_fd != -1) {
-		close(impl->timing_fd);
-		impl->timing_fd = -1;
-	}
-	if (impl->server_source != NULL) {
-		pw_loop_destroy_source(impl->loop, impl->server_source);
-		impl->server_source = NULL;
-	}
 	if (impl->timing_source != NULL) {
 		pw_loop_destroy_source(impl->loop, impl->timing_source);
 		impl->timing_source = NULL;
 	}
-	if (impl->control_source != NULL) {
-		pw_loop_destroy_source(impl->loop, impl->control_source);
-		impl->control_source = NULL;
+	if (impl->timing_fd >= 0) {
+		close(impl->timing_fd);
+		impl->timing_fd = -1;
 	}
 }
 
@@ -1390,13 +1432,15 @@ static int rtsp_do_connect(struct impl *impl)
 	if (hostname == NULL || port == NULL)
 		return -EINVAL;
 
-	pw_getrandom(&session_id, sizeof(session_id), 0);
+	if (pw_getrandom(&session_id, sizeof(session_id), 0) < 0)
+		return -errno;
+
 	spa_scnprintf(impl->session_id, sizeof(impl->session_id), "%u", session_id);
 
 	return pw_rtsp_client_connect(impl->rtsp, hostname, atoi(port), impl->session_id);
 }
 
-static void rtsp_teardown_reply(void *data, int status, const struct spa_dict *headers)
+static int rtsp_teardown_reply(void *data, int status, const struct spa_dict *headers)
 {
 	struct impl *impl = data;
 	const char *str;
@@ -1409,6 +1453,7 @@ static void rtsp_teardown_reply(void *data, int status, const struct spa_dict *h
 		if (spa_streq(str, "close"))
 			pw_rtsp_client_disconnect(impl->rtsp);
 	}
+	return 0;
 }
 
 static int rtsp_do_teardown(struct impl *impl)
@@ -1756,6 +1801,8 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
 		str = "PCM";
 	if (spa_streq(str, "PCM"))
 		impl->codec = CODEC_PCM;
+	else if (spa_streq(str, "ALAC"))
+		impl->codec = CODEC_ALAC;
 	else {
 		pw_log_error( "can't handle codec type %s", str);
 		res = -EINVAL;
diff --git a/src/modules/module-raop/rtsp-client.c b/src/modules/module-raop/rtsp-client.c
index c731a0469e923e35143eedcfe4ffdbb31155ccdf..4ac9a31e497d5797eb340af41e8525f3ac5d5501 100644
--- a/src/modules/module-raop/rtsp-client.c
+++ b/src/modules/module-raop/rtsp-client.c
@@ -45,7 +45,7 @@ struct message {
 	size_t len;
 	size_t offset;
 	uint32_t cseq;
-	void (*reply) (void *user_data, int status, const struct spa_dict *headers);
+	int (*reply) (void *user_data, int status, const struct spa_dict *headers);
 	void *user_data;
 };
 
@@ -133,6 +133,11 @@ void *pw_rtsp_client_get_user_data(struct pw_rtsp_client *client)
 	return client->user_data;
 }
 
+const char *pw_rtsp_client_get_url(struct pw_rtsp_client *client)
+{
+	return client->url;
+}
+
 void pw_rtsp_client_add_listener(struct pw_rtsp_client *client,
 		struct spa_hook *listener,
 		const struct pw_rtsp_client_events *events, void *data)
@@ -282,16 +287,23 @@ static int process_status(struct pw_rtsp_client *client, char *buf)
 static void dispatch_handler(struct pw_rtsp_client *client)
 {
 	uint32_t cseq;
+	int res;
+	struct message *msg;
+
 	if (pw_properties_fetch_uint32(client->headers, "CSeq", &cseq) < 0)
 		return;
 
 	pw_log_info("received reply to request with cseq:%" PRIu32, cseq);
 
-	struct message *msg = find_pending(client, cseq);
+	msg = find_pending(client, cseq);
 	if (msg) {
-		msg->reply(msg->user_data, client->status, &client->headers->dict);
+		res = msg->reply(msg->user_data, client->status, &client->headers->dict);
 		spa_list_remove(&msg->link);
 		free(msg);
+
+		if (res < 0)
+			pw_log_warn("client %p: handle reply cseq:%u error: %s",
+					client, cseq, spa_strerror(res));
 	}
 	else {
 		pw_rtsp_client_emit_message(client, client->status, &client->headers->dict);
@@ -314,8 +326,8 @@ static int process_header(struct pw_rtsp_client *client, char *buf)
 			return -EPROTO;
 
 		*value++ = '\0';
-		while (*value == ' ')
-			value++;
+
+		value = pw_strip(value, " ");
 
 		pw_properties_set(client->headers, key, value);
 	}
@@ -554,7 +566,7 @@ int pw_rtsp_client_disconnect(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 void *content, size_t content_length,
-		void (*reply) (void *user_data, int status, const struct spa_dict *headers),
+		int (*reply) (void *user_data, int status, const struct spa_dict *headers),
 		void *user_data)
 {
 	FILE *f;
@@ -608,7 +620,7 @@ int pw_rtsp_client_url_send(struct pw_rtsp_client *client, const char *url,
 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),
+		int (*reply) (void *user_data, int status, const struct spa_dict *headers),
 		void *user_data)
 {
 	const size_t content_length = content ? strlen(content) : 0;
diff --git a/src/modules/module-raop/rtsp-client.h b/src/modules/module-raop/rtsp-client.h
index 75b1ce6a15795d3166d3678758739a465187efbe..014468beaa10992f4a4aa113f95245ae60ff9149 100644
--- a/src/modules/module-raop/rtsp-client.h
+++ b/src/modules/module-raop/rtsp-client.h
@@ -57,6 +57,7 @@ struct pw_rtsp_client * pw_rtsp_client_new(struct pw_loop *main_loop,
 void pw_rtsp_client_destroy(struct pw_rtsp_client *client);
 
 void *pw_rtsp_client_get_user_data(struct pw_rtsp_client *client);
+const char *pw_rtsp_client_get_url(struct pw_rtsp_client *client);
 
 void pw_rtsp_client_add_listener(struct pw_rtsp_client *client,
 		struct spa_hook *listener,
@@ -74,13 +75,13 @@ int pw_rtsp_client_get_local_ip(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 void *content, size_t content_length,
-		void (*reply) (void *user_data, int status, const struct spa_dict *headers),
+		int (*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,
-		void (*reply) (void *user_data, int status, const struct spa_dict *headers),
+		int (*reply) (void *user_data, int status, const struct spa_dict *headers),
 		void *user_data);
 
 
diff --git a/src/modules/module-rtp-sink.c b/src/modules/module-rtp-sink.c
new file mode 100644
index 0000000000000000000000000000000000000000..014446d5c057951ef913cac94e84dd1a32e27715
--- /dev/null
+++ b/src/modules/module-rtp-sink.c
@@ -0,0 +1,945 @@
+/* PipeWire
+ *
+ * Copyright © 2022 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * 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 <limits.h>
+#include <unistd.h>
+#include <sys/stat.h>
+#include <sys/socket.h>
+#include <sys/ioctl.h>
+#include <arpa/inet.h>
+#include <netinet/ip.h>
+#include <netinet/in.h>
+#include <net/if.h>
+#include <ctype.h>
+
+#include <spa/param/audio/format-utils.h>
+#include <spa/utils/hook.h>
+#include <spa/utils/ringbuffer.h>
+#include <spa/utils/json.h>
+#include <spa/debug/pod.h>
+
+#include <pipewire/pipewire.h>
+#include <pipewire/private.h>
+
+#include <module-rtp/sap.h>
+#include <module-rtp/rtp.h>
+
+
+/** \page page_module_rtp_sink PipeWire Module: RTP sink
+ *
+ * The `rtp-sink` module creates a PipeWire sink that sends audio
+ * RTP packets.
+ *
+ * ## Module Options
+ *
+ * Options specific to the behavior of this module
+ *
+ * - `sap.ip = <str>`: IP address of the SAP messages, default "224.0.0.56"
+ * - `sap.port = <int>`: port of the SAP messages, default 9875
+ * - `source.ip =<str>`: source IP address, default "0.0.0.0"
+ * - `destination.ip =<str>`: destination IP address, default "224.0.0.56"
+ * - `destination.port =<int>`: destination port, default random beteen 46000 and 47024
+ * - `local.ifname = <str>`: interface name to use
+ * - `net.mtu = <int>`: MTU to use, default 1280
+ * - `net.ttl = <int>`: TTL to use, default 1
+ * - `net.loop = <bool>`: loopback multicast, default false
+ * - `sess.name = <str>`: a session name
+ * - `stream.props = {}`: properties to be passed to the stream
+ *
+ * ## General options
+ *
+ * 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
+ * - \ref PW_KEY_NODE_NAME
+ * - \ref PW_KEY_NODE_DESCRIPTION
+ * - \ref PW_KEY_MEDIA_NAME
+ * - \ref PW_KEY_NODE_GROUP
+ * - \ref PW_KEY_NODE_LATENCY
+ * - \ref PW_KEY_NODE_VIRTUAL
+ * - \ref PW_KEY_MEDIA_CLASS
+ *
+ * ## Example configuration
+ *\code{.unparsed}
+ * context.modules = [
+ * {   name = libpipewire-module-rtp-sink
+ *     args = {
+ *         #sap.ip = "224.0.0.56"
+ *         #sap.port = 9875
+ *         #source.ip = "0.0.0.0"
+ *         #destination.ip = "224.0.0.56"
+ *         #destination.port = 46000
+ *         #local.ifname = "eth0"
+ *         #net.mtu = 1280
+ *         #net.ttl = 1
+ *         #net.loop = false
+ *         #sess.name = "PipeWire RTP stream"
+ *         #audio.format = "S16BE"
+ *         #audio.rate = 48000
+ *         #audio.channels = 2
+ *         #audio.position = [ FL FR ]
+ *         stream.props = {
+ *             node.name = "rtp-sink"
+ *         }
+ *     }
+ *}
+ *]
+ *\endcode
+ *
+ * \since 0.3.60
+ */
+
+#define NAME "rtp-sink"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+#define SAP_INTERVAL_SEC	5
+#define SAP_MIME_TYPE		"application/sdp"
+
+#define BUFFER_SIZE		(1u<<20)
+#define BUFFER_MASK		(BUFFER_SIZE-1)
+
+#define DEFAULT_SAP_IP		"224.0.0.56"
+#define DEFAULT_SAP_PORT	9875
+
+#define DEFAULT_FORMAT		"S16BE"
+#define DEFAULT_RATE		48000
+#define DEFAULT_CHANNELS	2
+#define DEFAULT_POSITION	"[ FL FR ]"
+
+#define DEFAULT_PORT		46000
+#define DEFAULT_SOURCE_IP	"0.0.0.0"
+#define DEFAULT_DESTINATION_IP	"224.0.0.56"
+#define DEFAULT_TTL		1
+#define DEFAULT_MTU		1280
+#define DEFAULT_LOOP		false
+
+#define DEFAULT_MIN_PTIME	2
+#define DEFAULT_MAX_PTIME	20
+
+#define USAGE	"sap.ip=<SAP IP address to send announce, default:"DEFAULT_SAP_IP"> "		\
+		"sap.port=<SAP port to send on, default:"SPA_STRINGIFY(DEFAULT_SAP_PORT)"> "	\
+		"source.ip=<source IP address, default:"DEFAULT_SOURCE_IP"> "			\
+		"destination.ip=<destination IP address, default:"DEFAULT_DESTINATION_IP"> "	\
+		"local.ifname=<local interface name to use> "					\
+		"net.mtu=<desired MTU, default:"SPA_STRINGIFY(DEFAULT_MTU)"> "			\
+		"net.ttl=<desired TTL, default:"SPA_STRINGIFY(DEFAULT_TTL)"> "			\
+		"net.loop=<desired loopback, default:"SPA_STRINGIFY(DEFAULT_LOOP)"> "		\
+		"sess.name=<a name for the session> "						\
+		"audio.format=<format, default:"DEFAULT_FORMAT"> "				\
+		"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= { key=value ... }"
+
+static const struct spa_dict_item module_info[] = {
+	{ PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+	{ PW_KEY_MODULE_DESCRIPTION, "RTP Sink" },
+	{ PW_KEY_MODULE_USAGE, USAGE },
+	{ PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+static const struct format_info {
+	uint32_t format;
+	uint32_t size;
+	const char *mime;
+} format_info[] = {
+	{ SPA_AUDIO_FORMAT_U8, 1, "L8" },
+	{ SPA_AUDIO_FORMAT_ALAW, 1, "PCMA" },
+	{ SPA_AUDIO_FORMAT_ULAW, 1, "PCMU" },
+	{ SPA_AUDIO_FORMAT_S16_BE, 2, "L16" },
+	{ SPA_AUDIO_FORMAT_S24_BE, 3, "L24" },
+};
+
+static const struct format_info *find_format_info(uint32_t format)
+{
+	SPA_FOR_EACH_ELEMENT_VAR(format_info, f)
+		if (f->format == format)
+			return f;
+	return NULL;
+}
+
+struct impl {
+	struct pw_impl_module *module;
+	struct spa_hook module_listener;
+	struct pw_properties *props;
+	struct pw_context *module_context;
+
+	struct pw_loop *loop;
+
+	struct pw_core *core;
+	struct spa_hook core_listener;
+	struct spa_hook core_proxy_listener;
+
+	struct spa_source *timer;
+
+	struct pw_properties *stream_props;
+	struct pw_stream *stream;
+	struct spa_hook stream_listener;
+
+	unsigned int do_disconnect:1;
+
+	char *ifname;
+	char *session_name;
+	int sess_latency_msec;
+	int mtu;
+	bool ttl;
+	bool mcast_loop;
+	uint32_t min_ptime;
+	uint32_t max_ptime;
+
+	struct sockaddr_storage src_addr;
+	socklen_t src_len;
+
+	uint16_t port;
+	struct sockaddr_storage dst_addr;
+	socklen_t dst_len;
+
+	uint16_t sap_port;
+	struct sockaddr_storage sap_addr;
+	socklen_t sap_len;
+
+	uint16_t msg_id_hash;
+	uint32_t ntp;
+
+	struct spa_audio_info_raw info;
+	const struct format_info *format_info;
+	uint32_t frame_size;
+	int payload;
+	uint16_t seq;
+	uint32_t timestamp;
+	uint32_t ssrc;
+
+	struct spa_ringbuffer ring;
+	uint8_t buffer[BUFFER_SIZE];
+
+	int rtp_fd;
+	int sap_fd;
+};
+
+
+static void stream_destroy(void *d)
+{
+	struct impl *impl = d;
+	spa_hook_remove(&impl->stream_listener);
+	impl->stream = NULL;
+}
+
+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 flush_packets(struct impl *impl)
+{
+	int32_t avail;
+	uint32_t index;
+	struct iovec iov[3];
+	struct msghdr msg;
+	ssize_t n;
+	struct rtp_header header;
+	int32_t tosend;
+
+	avail = spa_ringbuffer_get_read_index(&impl->ring, &index);
+
+	tosend = SPA_ROUND_DOWN(impl->mtu, impl->frame_size);
+
+	if (avail < tosend)
+		return;
+
+	spa_zero(header);
+	header.v = 2;
+	header.pt = impl->payload;
+	header.ssrc = htonl(impl->ssrc);
+
+	iov[0].iov_base = &header;
+	iov[0].iov_len = sizeof(header);
+
+	msg.msg_name = NULL;
+	msg.msg_namelen = 0;
+	msg.msg_iov = iov;
+	msg.msg_iovlen = 3;
+	msg.msg_control = NULL;
+	msg.msg_controllen = 0;
+	msg.msg_flags = 0;
+
+	while (avail >= tosend) {
+		header.sequence_number = htons(impl->seq);
+		header.timestamp = htonl(impl->timestamp);
+
+		set_iovec(&impl->ring,
+			impl->buffer, BUFFER_SIZE,
+			index & BUFFER_MASK,
+			&iov[1], tosend);
+
+		n = sendmsg(impl->rtp_fd, &msg, MSG_NOSIGNAL);
+		if (n < 0)
+			pw_log_warn("sendmsg() failed: %m");
+
+		impl->seq++;
+		impl->timestamp += tosend / impl->frame_size;
+
+		index += tosend;
+		avail -= tosend;
+	}
+	spa_ringbuffer_read_update(&impl->ring, index);
+}
+
+static void stream_process(void *data)
+{
+	struct impl *impl = data;
+	struct pw_buffer *buf;
+	struct spa_data *d;
+	uint32_t index;
+        int32_t filled, wanted;
+
+	if ((buf = pw_stream_dequeue_buffer(impl->stream)) == NULL) {
+		pw_log_debug("Out of stream buffers: %m");
+		return;
+	}
+	d = buf->buffer->datas;
+
+	wanted = d[0].chunk->size;
+
+	filled = spa_ringbuffer_get_write_index(&impl->ring, &index);
+
+	if (filled + wanted > (int32_t)BUFFER_SIZE) {
+		pw_log_warn("overrun %u + %u > %u", filled, wanted, BUFFER_SIZE);
+	} else {
+		spa_ringbuffer_write_data(&impl->ring,
+				impl->buffer,
+				BUFFER_SIZE,
+                                index & BUFFER_MASK,
+                                d[0].data, wanted);
+
+                index += wanted;
+                spa_ringbuffer_write_update(&impl->ring, index);
+        }
+	pw_stream_queue_buffer(impl->stream, buf);
+
+	flush_packets(impl);
+}
+
+static void on_stream_state_changed(void *d, enum pw_stream_state old,
+		enum pw_stream_state state, const char *error)
+{
+	struct impl *impl = d;
+
+	switch (state) {
+	case PW_STREAM_STATE_UNCONNECTED:
+		pw_log_info("stream disconnected, unloading");
+		pw_impl_module_schedule_destroy(impl->module);
+		break;
+	case PW_STREAM_STATE_ERROR:
+		pw_log_error("stream error: %s", error);
+		break;
+	default:
+		break;
+	}
+}
+
+static const struct pw_stream_events in_stream_events = {
+	PW_VERSION_STREAM_EVENTS,
+	.destroy = stream_destroy,
+	.state_changed = on_stream_state_changed,
+	.process = stream_process
+};
+
+static int parse_address(const char *address, uint16_t port,
+		struct sockaddr_storage *addr, socklen_t *len)
+{
+	struct sockaddr_in *sa4 = (struct sockaddr_in*)addr;
+	struct sockaddr_in6 *sa6 = (struct sockaddr_in6*)addr;
+
+	if (inet_pton(AF_INET, address, &sa4->sin_addr) > 0) {
+		sa4->sin_family = AF_INET;
+		sa4->sin_port = htons(port);
+		*len = sizeof(*sa4);
+	} else if (inet_pton(AF_INET6, address, &sa6->sin6_addr) > 0) {
+		sa6->sin6_family = AF_INET6;
+		sa6->sin6_port = htons(port);
+		*len = sizeof(*sa6);
+	} else
+		return -EINVAL;
+
+	return 0;
+}
+
+static bool is_multicast(struct sockaddr *sa, socklen_t salen)
+{
+	if (sa->sa_family == AF_INET) {
+		static const uint32_t ipv4_mcast_mask = 0xe0000000;
+		struct sockaddr_in *sa4 = (struct sockaddr_in*)sa;
+		return (ntohl(sa4->sin_addr.s_addr) & ipv4_mcast_mask) == ipv4_mcast_mask;
+	} else if (sa->sa_family == AF_INET6) {
+		struct sockaddr_in6 *sa6 = (struct sockaddr_in6*)sa;
+		return sa6->sin6_addr.s6_addr[0] == 0xff;
+	}
+	return false;
+}
+
+static int make_socket(struct sockaddr_storage *src, socklen_t src_len,
+		struct sockaddr_storage *dst, socklen_t dst_len,
+		bool loop, int ttl)
+{
+	int af, fd, val, res;
+
+	af = src->ss_family;
+	if ((fd = socket(af, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0)) < 0) {
+		pw_log_error("socket failed: %m");
+		return -errno;
+	}
+	if (bind(fd, (struct sockaddr*)src, src_len) < 0) {
+		res = -errno;
+		pw_log_error("bind() failed: %m");
+		goto error;
+	}
+	if (connect(fd, (struct sockaddr*)dst, dst_len) < 0) {
+		res = -errno;
+		pw_log_error("connect() failed: %m");
+		goto error;
+	}
+	if (is_multicast((struct sockaddr*)dst, dst_len)) {
+		if (setsockopt(fd, IPPROTO_IP, IP_MULTICAST_LOOP, &val, sizeof(val)) < 0)
+			pw_log_warn("setsockopt(IP_MULTICAST_LOOP) failed: %m");
+
+		val = ttl;
+		if (setsockopt(fd, IPPROTO_IP, IP_MULTICAST_TTL, &val, sizeof(val)) < 0)
+			pw_log_warn("setsockopt(IP_MULTICAST_TTL) failed: %m");
+	}
+#ifdef SO_PRIORITY
+	val = 6;
+	if (setsockopt(fd, SOL_SOCKET, SO_PRIORITY, &val, sizeof(val)) < 0)
+		pw_log_warn("setsockopt(SO_PRIORITY) failed: %m");
+#endif
+	val = IPTOS_LOWDELAY;
+	if (setsockopt(fd, IPPROTO_IP, IP_TOS, &val, sizeof(val)) < 0)
+		pw_log_warn("setsockopt(IP_TOS) failed: %m");
+
+
+	return fd;
+error:
+	close(fd);
+	return res;
+}
+
+static int setup_stream(struct impl *impl)
+{
+	const struct spa_pod *params[1];
+	struct spa_pod_builder b;
+	uint32_t n_params;
+	uint8_t buffer[1024];
+	struct pw_properties *props;
+	int res, fd;
+
+	props = pw_properties_copy(impl->stream_props);
+	if (props == NULL)
+		return -errno;
+
+	if (pw_properties_get(props, PW_KEY_NODE_LATENCY) == NULL) {
+		pw_properties_setf(props, PW_KEY_NODE_LATENCY,
+				"%d/%d", impl->mtu / impl->frame_size,
+				impl->info.rate);
+	}
+	pw_properties_setf(props, PW_KEY_NODE_RATE, "1/%d", impl->info.rate);
+
+	impl->stream = pw_stream_new(impl->core,
+			"rtp-sink capture", props);
+	if (impl->stream == NULL)
+		return -errno;
+
+	pw_stream_add_listener(impl->stream,
+			&impl->stream_listener,
+			&in_stream_events, impl);
+
+	n_params = 0;
+	spa_pod_builder_init(&b, buffer, sizeof(buffer));
+	params[n_params++] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat,
+			&impl->info);
+
+	if ((res = pw_stream_connect(impl->stream,
+			PW_DIRECTION_INPUT,
+			PW_ID_ANY,
+			PW_STREAM_FLAG_MAP_BUFFERS |
+			PW_STREAM_FLAG_AUTOCONNECT |
+			PW_STREAM_FLAG_RT_PROCESS,
+			params, n_params)) < 0)
+		return res;
+
+
+	if ((fd = make_socket(&impl->src_addr, impl->src_len,
+					&impl->dst_addr, impl->dst_len,
+					impl->mcast_loop, impl->ttl)) < 0)
+		return fd;
+
+	impl->rtp_fd = fd;
+
+	return 0;
+}
+
+static int get_ip(const struct sockaddr_storage *sa, char *ip, size_t len)
+{
+	if (sa->ss_family == AF_INET) {
+		struct sockaddr_in *in = (struct sockaddr_in*)sa;
+		inet_ntop(sa->ss_family, &in->sin_addr, ip, len);
+	} else if (sa->ss_family == AF_INET6) {
+		struct sockaddr_in6 *in = (struct sockaddr_in6*)sa;
+		inet_ntop(sa->ss_family, &in->sin6_addr, ip, len);
+	} else
+		return -EIO;
+	return 0;
+}
+static void send_sap(struct impl *impl, bool bye)
+{
+	char buffer[2048], src_addr[64], dst_addr[64], dst_ttl[8];
+	const char *user_name, *af;
+	struct sockaddr *sa = (struct sockaddr*)&impl->src_addr;
+	struct sap_header header;
+	struct iovec iov[4];
+	struct msghdr msg;
+
+	spa_zero(header);
+	header.v = 1;
+	header.t = bye;
+	header.msg_id_hash = impl->msg_id_hash;
+
+	iov[0].iov_base = &header;
+	iov[0].iov_len = sizeof(header);
+
+	if (sa->sa_family == AF_INET) {
+		iov[1].iov_base = &((struct sockaddr_in*) sa)->sin_addr;
+		iov[1].iov_len = 4U;
+		af = "IP4";
+	} else {
+		iov[1].iov_base = &((struct sockaddr_in6*) sa)->sin6_addr;
+		iov[1].iov_len = 16U;
+		header.a = 1;
+		af = "IP6";
+	}
+	iov[2].iov_base = SAP_MIME_TYPE;
+	iov[2].iov_len = sizeof(SAP_MIME_TYPE);
+
+	get_ip(&impl->src_addr, src_addr, sizeof(src_addr));
+	get_ip(&impl->dst_addr, dst_addr, sizeof(dst_addr));
+
+	if ((user_name = pw_get_user_name()) == NULL)
+		user_name = "-";
+
+	spa_zero(dst_ttl);
+	if (is_multicast((struct sockaddr*)&impl->dst_addr, impl->dst_len))
+		snprintf(dst_ttl, sizeof(dst_ttl), "/%d", impl->ttl);
+
+	snprintf(buffer, sizeof(buffer),
+			"v=0\n"
+			"o=%s %u 0 IN %s %s\n"
+			"s=%s\n"
+			"c=IN %s %s%s\n"
+			"t=%u 0\n"
+			"a=recvonly\n"
+			"m=audio %u RTP/AVP %i\n"
+			"a=rtpmap:%i %s/%u/%u\n"
+			"a=type:broadcast\n",
+			user_name, impl->ntp, af, src_addr,
+			impl->session_name,
+			af, dst_addr, dst_ttl,
+			impl->ntp,
+			impl->port, impl->payload,
+			impl->payload, impl->format_info->mime,
+			impl->info.rate, impl->info.channels);
+
+	iov[3].iov_base = buffer;
+	iov[3].iov_len = strlen(buffer);
+
+	msg.msg_name = NULL;
+	msg.msg_namelen = 0;
+	msg.msg_iov = iov;
+	msg.msg_iovlen = 4;
+	msg.msg_control = NULL;
+	msg.msg_controllen = 0;
+	msg.msg_flags = 0;
+
+	sendmsg(impl->sap_fd, &msg, MSG_NOSIGNAL);
+}
+
+static void on_timer_event(void *data, uint64_t expirations)
+{
+	struct impl *impl = data;
+	send_sap(impl, 0);
+}
+
+static int start_sap_announce(struct impl *impl)
+{
+	int fd, res;
+	struct timespec value, interval;
+
+	if ((fd = make_socket(&impl->src_addr, impl->src_len,
+					&impl->sap_addr, impl->sap_len,
+					impl->mcast_loop, impl->ttl)) < 0)
+		return fd;
+
+	impl->sap_fd = fd;
+
+	pw_log_info("starting SAP timer");
+	impl->timer = pw_loop_add_timer(impl->loop, on_timer_event, impl);
+	if (impl->timer == NULL) {
+		res = -errno;
+		pw_log_error("can't create timer source: %m");
+		goto error;
+	}
+	value.tv_sec = 0;
+	value.tv_nsec = 1;
+	interval.tv_sec = SAP_INTERVAL_SEC;
+	interval.tv_nsec = 0;
+	pw_loop_update_timer(impl->loop, impl->timer, &value, &interval, false);
+
+	return 0;
+error:
+	close(fd);
+	return res;
+
+}
+
+static void core_destroy(void *d)
+{
+	struct impl *impl = d;
+	spa_hook_remove(&impl->core_listener);
+	impl->core = NULL;
+	pw_impl_module_schedule_destroy(impl->module);
+}
+
+static const struct pw_proxy_events core_proxy_events = {
+	.destroy = core_destroy,
+};
+
+static void impl_destroy(struct impl *impl)
+{
+	send_sap(impl, 1);
+
+	if (impl->stream)
+		pw_stream_destroy(impl->stream);
+
+	if (impl->core && impl->do_disconnect)
+		pw_core_disconnect(impl->core);
+
+	if (impl->timer)
+		pw_loop_destroy_source(impl->loop, impl->timer);
+
+	if (impl->rtp_fd != -1)
+		close(impl->rtp_fd);
+	if (impl->sap_fd != -1)
+		close(impl->sap_fd);
+
+	pw_properties_free(impl->stream_props);
+	pw_properties_free(impl->props);
+
+	free(impl->ifname);
+	free(impl->session_name);
+	free(impl);
+}
+
+static void module_destroy(void *d)
+{
+	struct impl *impl = d;
+	spa_hook_remove(&impl->module_listener);
+	impl_destroy(impl);
+}
+
+static const struct pw_impl_module_events module_events = {
+	PW_VERSION_IMPL_MODULE_EVENTS,
+	.destroy = module_destroy,
+};
+
+static void on_core_error(void *d, uint32_t id, int seq, int res, const char *message)
+{
+	struct impl *impl = d;
+
+	pw_log_error("error id:%u seq:%d res:%d (%s): %s",
+			id, seq, res, spa_strerror(res), message);
+
+	if (id == PW_ID_CORE && res == -EPIPE)
+		pw_impl_module_schedule_destroy(impl->module);
+}
+
+static const struct pw_core_events core_events = {
+	PW_VERSION_CORE_EVENTS,
+	.error = on_core_error,
+};
+
+static inline uint32_t 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 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 void parse_position(struct spa_audio_info_raw *info, 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);
+
+	info->channels = 0;
+	while (spa_json_get_string(&it[1], v, sizeof(v)) > 0 &&
+	    info->channels < SPA_AUDIO_MAX_CHANNELS) {
+		info->position[info->channels++] = channel_from_name(v);
+	}
+}
+
+static void parse_audio_info(const struct pw_properties *props, struct spa_audio_info_raw *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 void copy_props(struct impl *impl, struct pw_properties *props, const char *key)
+{
+	const char *str;
+	if ((str = pw_properties_get(props, key)) != NULL) {
+		if (pw_properties_get(impl->stream_props, key) == NULL)
+			pw_properties_set(impl->stream_props, key, str);
+	}
+}
+
+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 impl *impl;
+	struct pw_properties *props = NULL, *stream_props = NULL;
+	uint32_t id = pw_global_get_id(pw_impl_module_get_global(module));
+	uint32_t pid = getpid(), port;
+	char addr[64];
+	const char *str;
+	int res = 0;
+
+	PW_LOG_TOPIC_INIT(mod_topic);
+
+	impl = calloc(1, sizeof(struct impl));
+	if (impl == NULL)
+		return -errno;
+
+	impl->rtp_fd = -1;
+	impl->sap_fd = -1;
+
+	if (args == NULL)
+		args = "";
+
+	props = pw_properties_new_string(args);
+	if (props == NULL) {
+		res = -errno;
+		pw_log_error( "can't create properties: %m");
+		goto out;
+	}
+	impl->props = props;
+
+	stream_props = pw_properties_new(NULL, NULL);
+	if (stream_props == NULL) {
+		res = -errno;
+		pw_log_error( "can't create properties: %m");
+		goto out;
+	}
+	impl->stream_props = stream_props;
+
+	impl->module = module;
+	impl->module_context = context;
+	impl->loop = pw_context_get_main_loop(context);
+
+	if (pw_properties_get(props, PW_KEY_NODE_VIRTUAL) == NULL)
+		pw_properties_set(props, PW_KEY_NODE_VIRTUAL, "true");
+	if (pw_properties_get(stream_props, PW_KEY_NODE_NETWORK) == NULL)
+		pw_properties_set(stream_props, PW_KEY_NODE_NETWORK, "true");
+
+	if (pw_properties_get(props, PW_KEY_NODE_NAME) == NULL)
+		pw_properties_setf(props, PW_KEY_NODE_NAME, "rtp-sink-%u-%u", pid, id);
+	if (pw_properties_get(props, PW_KEY_NODE_DESCRIPTION) == NULL)
+		pw_properties_set(props, PW_KEY_NODE_DESCRIPTION,
+				pw_properties_get(props, PW_KEY_NODE_NAME));
+	if (pw_properties_get(props, PW_KEY_MEDIA_NAME) == NULL)
+		pw_properties_set(props, PW_KEY_MEDIA_NAME, "RTP Sender Stream");
+
+	if ((str = pw_properties_get(props, "stream.props")) != NULL)
+		pw_properties_update_string(stream_props, str, strlen(str));
+
+	copy_props(impl, props, PW_KEY_AUDIO_FORMAT);
+	copy_props(impl, props, PW_KEY_AUDIO_RATE);
+	copy_props(impl, props, PW_KEY_AUDIO_CHANNELS);
+	copy_props(impl, props, SPA_KEY_AUDIO_POSITION);
+	copy_props(impl, props, PW_KEY_NODE_NAME);
+	copy_props(impl, props, PW_KEY_NODE_DESCRIPTION);
+	copy_props(impl, props, PW_KEY_NODE_GROUP);
+	copy_props(impl, props, PW_KEY_NODE_LATENCY);
+	copy_props(impl, props, PW_KEY_NODE_VIRTUAL);
+	copy_props(impl, props, PW_KEY_MEDIA_NAME);
+	copy_props(impl, props, PW_KEY_MEDIA_CLASS);
+
+	parse_audio_info(impl->stream_props, &impl->info);
+
+	impl->format_info = find_format_info(impl->info.format);
+	if (impl->format_info == NULL) {
+		pw_log_error("unsupported audio format:%d channels:%d",
+				impl->info.format, impl->info.channels);
+		res = -EINVAL;
+		goto out;
+	}
+	impl->frame_size = impl->format_info->size * impl->info.channels;
+	impl->msg_id_hash = rand();
+	impl->ntp = (uint32_t) time(NULL) + 2208988800U;
+
+	impl->payload = 127;
+	impl->seq = rand();
+	impl->timestamp = rand();
+	impl->ssrc = rand();
+
+	str = pw_properties_get(props, "local.ifname");
+	impl->ifname = str ? strdup(str) : NULL;
+
+	if ((str = pw_properties_get(props, "sap.ip")) == NULL)
+		str = DEFAULT_SAP_IP;
+	port = pw_properties_get_uint32(props, "sap.port", DEFAULT_SAP_PORT);
+	if ((res = parse_address(str, port, &impl->sap_addr, &impl->sap_len)) < 0) {
+		pw_log_error("invalid sap.ip %s: %s", str, spa_strerror(res));
+		goto out;
+	}
+
+	if ((str = pw_properties_get(props, "source.ip")) == NULL)
+		str = DEFAULT_SOURCE_IP;
+	if ((res = parse_address(str, 0, &impl->src_addr, &impl->src_len)) < 0) {
+		pw_log_error("invalid source.ip %s: %s", str, spa_strerror(res));
+		goto out;
+	}
+
+	impl->port = DEFAULT_PORT + ((uint32_t) (rand() % 512) << 1);
+	impl->port = pw_properties_get_uint32(props, "destination.port", impl->port);
+	if ((str = pw_properties_get(props, "destination.ip")) == NULL)
+		str = DEFAULT_DESTINATION_IP;
+	if ((res = parse_address(str, impl->port, &impl->dst_addr, &impl->dst_len)) < 0) {
+		pw_log_error("invalid destination.ip %s: %s", str, spa_strerror(res));
+		goto out;
+	}
+
+	impl->mtu = pw_properties_get_uint32(props, "net.mtu", DEFAULT_MTU);
+	impl->ttl = pw_properties_get_uint32(props, "net.ttl", DEFAULT_TTL);
+	impl->mcast_loop = pw_properties_get_bool(props, "net.loop", DEFAULT_LOOP);
+
+	impl->min_ptime = pw_properties_get_uint32(props, "sess.min-ptime", DEFAULT_MIN_PTIME);
+	impl->max_ptime = pw_properties_get_uint32(props, "sess.max-ptime", DEFAULT_MAX_PTIME);
+
+	if ((str = pw_properties_get(props, "sess.name")) == NULL)
+		pw_properties_setf(props, "sess.name", "PipeWire RTP Stream on %s",
+				pw_get_host_name());
+	str = pw_properties_get(props, "sess.name");
+	impl->session_name = str ? strdup(str) : NULL;
+
+	pw_properties_set(stream_props, "rtp.session", impl->session_name);
+	get_ip(&impl->src_addr, addr, sizeof(addr));
+	pw_properties_set(stream_props, "rtp.source.ip", addr);
+	get_ip(&impl->dst_addr, addr, sizeof(addr));
+	pw_properties_set(stream_props, "rtp.destination.ip", addr);
+	pw_properties_setf(stream_props, "rtp.destination.port", "%u", impl->port);
+	pw_properties_setf(stream_props, "rtp.mtu", "%u", impl->mtu);
+	pw_properties_setf(stream_props, "rtp.ttl", "%u", impl->ttl);
+
+	impl->core = pw_context_get_object(impl->module_context, PW_TYPE_INTERFACE_Core);
+	if (impl->core == NULL) {
+		str = pw_properties_get(props, PW_KEY_REMOTE_NAME);
+		impl->core = pw_context_connect(impl->module_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 out;
+	}
+
+	pw_proxy_add_listener((struct pw_proxy*)impl->core,
+			&impl->core_proxy_listener,
+			&core_proxy_events, impl);
+	pw_core_add_listener(impl->core,
+			&impl->core_listener,
+			&core_events, impl);
+
+	if ((res = setup_stream(impl)) < 0)
+		goto out;
+
+	if ((res = start_sap_announce(impl)) < 0)
+		goto out;
+
+	pw_impl_module_add_listener(module, &impl->module_listener, &module_events, impl);
+
+	pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_info));
+
+	pw_log_info("Successfully loaded module-rtp-sink");
+
+	return 0;
+out:
+	impl_destroy(impl);
+	return res;
+}
diff --git a/src/modules/module-rtp-source.c b/src/modules/module-rtp-source.c
new file mode 100644
index 0000000000000000000000000000000000000000..503bfc1961b064cafab2fbf78b511fd9ac051a90
--- /dev/null
+++ b/src/modules/module-rtp-source.c
@@ -0,0 +1,1104 @@
+/* PipeWire
+ *
+ * Copyright © 2022 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * 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 <limits.h>
+#include <unistd.h>
+#include <sys/stat.h>
+#include <sys/socket.h>
+#include <sys/ioctl.h>
+#include <arpa/inet.h>
+#include <netinet/in.h>
+#include <net/if.h>
+#include <ctype.h>
+
+#include <spa/param/audio/format-utils.h>
+#include <spa/utils/hook.h>
+#include <spa/utils/ringbuffer.h>
+#include <spa/utils/dll.h>
+#include <spa/debug/mem.h>
+
+#include <pipewire/pipewire.h>
+#include <pipewire/private.h>
+
+#include <module-rtp/sap.h>
+#include <module-rtp/rtp.h>
+
+#ifdef __FreeBSD__
+#define ifr_ifindex ifr_index
+#endif
+
+/** \page page_module_rtp_source PipeWire Module: RTP source
+ *
+ * The `rtp-source` module creates a PipeWire source that receives audio
+ * RTP packets.
+ *
+ * ## Module Options
+ *
+ * Options specific to the behavior of this module
+ *
+ * - `sap.ip = <str>`: IP address of the SAP messages, default "224.0.0.56"
+ * - `sap.port = <str>`: port of the SAP messages, default 9875
+ * - `local.ifname = <str>`: interface name to use
+ * - `sess.latency.msec = <str>`: target network latency in milliseconds, default 100
+ * - `stream.props = {}`: properties to be passed to the stream
+ *
+ * ## General options
+ *
+ * Options with well-known behavior:
+ *
+ * - \ref PW_KEY_NODE_NAME
+ * - \ref PW_KEY_NODE_DESCRIPTION
+ * - \ref PW_KEY_MEDIA_NAME
+ * - \ref PW_KEY_MEDIA_CLASS
+ *
+ * ## Example configuration
+ *\code{.unparsed}
+ * context.modules = [
+ *  {   name = libpipewire-module-rtp-source
+ *      args = {
+ *          #sap.ip = 224.0.0.56
+ *          #sap.port = 9875
+ *          #local.ifname = eth0
+ *          sess.latency.msec = 100
+ *          stream.props = {
+ *             node.name = "rtp-source"
+ *             #media.class = "Audio/Source"
+ *          }
+ *      }
+ *  }
+ *]
+ *\endcode
+ *
+ * \since 0.3.60
+ */
+
+#define NAME "rtp-source"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+#define SAP_MIME_TYPE		"application/sdp"
+
+#define ERROR_MSEC		2
+#define MAX_SESSIONS		16
+#define CLEANUP_INTERVAL_SEC	20
+
+#define DEFAULT_SAP_IP		"224.0.0.56"
+#define DEFAULT_SAP_PORT	9875
+#define DEFAULT_SESS_LATENCY	100
+
+#define BUFFER_SIZE		(1u<<22)
+#define BUFFER_MASK		(BUFFER_SIZE-1)
+
+#define USAGE	"sap.ip=<SAP IP address to listen on, default "DEFAULT_SAP_IP"> "				\
+		"sap.port=<SAP port to listen on, default "SPA_STRINGIFY(DEFAULT_SAP_PORT)"> "			\
+		"local.ifname=<local interface name to use> "							\
+		"sess.latency.msec=<target network latency, default "SPA_STRINGIFY(DEFAULT_SESS_LATENCY)"> "	\
+		"stream.props= { key=value ... }"
+
+static const struct spa_dict_item module_info[] = {
+	{ PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+	{ PW_KEY_MODULE_DESCRIPTION, "RTP Source" },
+	{ PW_KEY_MODULE_USAGE,	USAGE },
+	{ PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+struct impl {
+	struct pw_impl_module *module;
+	struct spa_hook module_listener;
+	struct pw_properties *props;
+	struct pw_context *module_context;
+
+	struct pw_loop *loop;
+	struct pw_loop *data_loop;
+
+	struct pw_core *core;
+	struct spa_hook core_listener;
+	struct spa_hook core_proxy_listener;
+
+	struct spa_source *timer;
+	struct spa_source *sap_source;
+
+	struct pw_properties *stream_props;
+
+	unsigned int do_disconnect:1;
+
+	char *ifname;
+	char *sap_ip;
+	int sap_port;
+	int sess_latency_msec;
+
+	struct spa_list sessions;
+	uint32_t n_sessions;
+};
+
+static const struct format_info {
+	uint32_t format;
+	uint32_t size;
+	const char *mime;
+} format_info[] = {
+	{ SPA_AUDIO_FORMAT_U8, 1, "L8" },
+	{ SPA_AUDIO_FORMAT_ALAW, 1, "PCMA" },
+	{ SPA_AUDIO_FORMAT_ULAW, 1, "PCMU" },
+	{ SPA_AUDIO_FORMAT_S16_BE, 2, "L16" },
+	{ SPA_AUDIO_FORMAT_S24_BE, 3, "L24" },
+};
+
+static const struct format_info *find_format_info(const char *mime)
+{
+	SPA_FOR_EACH_ELEMENT_VAR(format_info, f)
+		if (spa_streq(f->mime, mime))
+			return f;
+	return NULL;
+}
+
+struct sdp_info {
+	uint16_t hash;
+
+	char origin[128];
+	char session[256];
+
+	struct sockaddr_storage sa;
+	socklen_t salen;
+
+	uint16_t port;
+	uint8_t payload;
+
+	const struct format_info *format_info;
+	struct spa_audio_info_raw info;
+	uint32_t stride;
+};
+
+struct session {
+	struct impl *impl;
+	struct spa_list link;
+
+	uint64_t timestamp;
+
+	struct sdp_info info;
+
+	struct spa_source *source;
+
+	struct pw_stream *stream;
+	struct spa_hook stream_listener;
+
+	uint32_t expected_ssrc;
+	uint16_t expected_seq;
+	unsigned have_ssrc:1;
+	unsigned have_seq:1;
+	unsigned have_sync:1;
+
+	struct spa_ringbuffer ring;
+	uint8_t buffer[BUFFER_SIZE];
+
+	struct spa_dll dll;
+	uint32_t target_buffer;
+	float max_error;
+	unsigned buffering:1;
+};
+
+static void stream_destroy(void *d)
+{
+	struct session *sess = d;
+	spa_hook_remove(&sess->stream_listener);
+	sess->stream = NULL;
+}
+
+static void stream_process(void *data)
+{
+	struct session *sess = data;
+	struct pw_buffer *buf;
+	struct spa_data *d;
+	uint32_t index;
+        int32_t avail, wanted;
+
+	if ((buf = pw_stream_dequeue_buffer(sess->stream)) == NULL) {
+		pw_log_debug("Out of stream buffers: %m");
+		return;
+	}
+	d = buf->buffer->datas;
+
+	wanted = buf->requested ?
+		SPA_MIN(buf->requested * sess->info.stride, d[0].maxsize)
+		: d[0].maxsize;
+
+	avail = spa_ringbuffer_get_read_index(&sess->ring, &index);
+
+        if (avail < wanted || sess->buffering) {
+                memset(d[0].data, 0, wanted);
+		if (!sess->buffering && sess->have_sync) {
+			pw_log_info("underrun %u/%u < %u, buffering...",
+					avail, sess->target_buffer, wanted);
+			sess->buffering = true;
+		}
+        } else {
+		float error, corr;
+		if (avail > (int32_t)BUFFER_SIZE) {
+			index += avail - sess->target_buffer;
+			avail = sess->target_buffer;
+			pw_log_warn("overrun %u > %u", avail, BUFFER_SIZE);
+		} else {
+			error = (float)sess->target_buffer - (float)avail;
+			error = SPA_CLAMP(error, -sess->max_error, sess->max_error);
+
+			corr = spa_dll_update(&sess->dll, error);
+
+			pw_log_debug("avail:%u target:%u error:%f corr:%f", avail,
+					sess->target_buffer, error, corr);
+
+			pw_stream_set_control(sess->stream,
+					SPA_PROP_rate, 1, &corr, NULL);
+		}
+		spa_ringbuffer_read_data(&sess->ring,
+				sess->buffer,
+				BUFFER_SIZE,
+                                index & BUFFER_MASK,
+                                d[0].data, wanted);
+
+                index += wanted;
+                spa_ringbuffer_read_update(&sess->ring, index);
+        }
+        d[0].chunk->size = wanted;
+        d[0].chunk->stride = sess->info.stride;
+        d[0].chunk->offset = 0;
+        buf->size = wanted / sess->info.stride;
+
+	pw_stream_queue_buffer(sess->stream, buf);
+}
+
+static void on_stream_state_changed(void *d, enum pw_stream_state old,
+		enum pw_stream_state state, const char *error)
+{
+	struct session *sess = d;
+	struct impl *impl = sess->impl;
+
+	switch (state) {
+	case PW_STREAM_STATE_UNCONNECTED:
+		pw_log_info("stream disconnected, unloading");
+		pw_impl_module_schedule_destroy(impl->module);
+		break;
+	case PW_STREAM_STATE_ERROR:
+		pw_log_error("stream error: %s", error);
+		break;
+	default:
+		break;
+	}
+}
+
+static const struct pw_stream_events out_stream_events = {
+	PW_VERSION_STREAM_EVENTS,
+	.destroy = stream_destroy,
+	.state_changed = on_stream_state_changed,
+	.process = stream_process
+};
+
+static void
+on_rtp_io(void *data, int fd, uint32_t mask)
+{
+	struct session *sess = data;
+	struct rtp_header *hdr;
+	ssize_t len, hlen;
+	uint8_t buffer[2048], *payload;
+
+	if (mask & SPA_IO_IN) {
+		uint32_t index, expected_index, timestamp;
+		uint16_t seq;
+		int32_t filled;
+
+		pw_log_trace("got rtp");
+		if ((len = recv(fd, buffer, sizeof(buffer), 0)) < 0)
+			goto receive_error;
+
+		if (len < 12)
+			goto short_packet;
+
+		hdr = (struct rtp_header*)buffer;
+		if (hdr->v != 2)
+			goto invalid_version;
+
+		hlen = 12 + hdr->cc * 4;
+		if (hlen > len)
+			goto invalid_len;
+
+		if (sess->have_ssrc && sess->expected_ssrc != hdr->ssrc)
+			goto unexpected_ssrc;
+		sess->expected_ssrc = hdr->ssrc;
+		sess->have_ssrc = true;
+
+		seq = ntohs(hdr->sequence_number);
+		if (sess->have_seq && sess->expected_seq != seq) {
+			pw_log_warn("unexpected seq (%d != %d)", seq, sess->expected_seq);
+		}
+		sess->expected_seq = seq + 1;
+		sess->have_seq = true;
+
+		len = SPA_ROUND_DOWN(len - hlen, sess->info.stride);
+		payload = &buffer[hlen];
+
+		filled = spa_ringbuffer_get_write_index(&sess->ring, &index);
+
+		timestamp = ntohl(hdr->timestamp);
+		expected_index = timestamp * sess->info.stride;
+
+		if (!sess->have_sync) {
+			sess->ring.readindex = sess->ring.writeindex =
+				index = expected_index;
+			filled = 0;
+			sess->have_sync = true;
+			sess->buffering = true;
+			pw_log_info("sync to timestamp %u", index);
+
+			spa_dll_init(&sess->dll);
+			spa_dll_set_bw(&sess->dll, SPA_DLL_BW_MIN, 128, sess->info.info.rate);
+
+		} else if (expected_index != index) {
+			pw_log_debug("unexpected timestamp (%u != %u)",
+					index / sess->info.stride,
+					expected_index / sess->info.stride);
+			index = expected_index;
+			filled = 0;
+		}
+
+		if (filled + len > BUFFER_SIZE) {
+			pw_log_warn("capture overrun %u %zd", filled, len);
+			sess->have_sync = false;
+		} else {
+			spa_ringbuffer_write_data(&sess->ring,
+					sess->buffer,
+					BUFFER_SIZE,
+					index & BUFFER_MASK,
+					payload, len);
+			index += len;
+			filled += len;
+			spa_ringbuffer_write_update(&sess->ring, index);
+
+			if (sess->buffering && (uint32_t)filled > sess->target_buffer) {
+				sess->buffering = false;
+				pw_log_info("buffering done %u > %u",
+					filled, sess->target_buffer);
+			}
+		}
+	}
+	return;
+
+receive_error:
+	pw_log_warn("recv error: %m");
+	return;
+short_packet:
+	pw_log_warn("short packet received");
+	return;
+invalid_version:
+	pw_log_warn("invalid RTP version");
+	return;
+invalid_len:
+	pw_log_warn("invalid RTP length");
+	return;
+unexpected_ssrc:
+	pw_log_warn("unexpected SSRC (expected %u != %u)",
+		sess->expected_ssrc, hdr->ssrc);
+	return;
+}
+
+static int make_socket(const struct sockaddr* sa, socklen_t salen, char *ifname)
+{
+	int af, fd, val, res;
+	struct ifreq req;
+
+	af = sa->sa_family;
+	if ((fd = socket(af, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0)) < 0) {
+		pw_log_error("socket failed: %m");
+		return -errno;
+	}
+#ifdef SO_TIMESTAMP
+	val = 1;
+	if (setsockopt(fd, SOL_SOCKET, SO_TIMESTAMP, &val, sizeof(val)) < 0) {
+		res = -errno;
+		pw_log_error("setsockopt failed: %m");
+		goto error;
+	}
+#endif
+	val = 1;
+	if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val)) < 0) {
+		res = -errno;
+		pw_log_error("setsockopt failed: %m");
+		goto error;
+	}
+
+	spa_zero(req);
+	if (ifname) {
+		snprintf(req.ifr_name, sizeof(req.ifr_name), "%s", ifname);
+		res = ioctl(fd, SIOCGIFINDEX, &req);
+	        if (res < 0)
+	                pw_log_warn("SIOCGIFINDEX %s failed: %m", ifname);
+	}
+	res = 0;
+	if (af == AF_INET) {
+		static const uint32_t ipv4_mcast_mask = 0xe0000000;
+		struct sockaddr_in *sa4 = (struct sockaddr_in*)sa;
+		if ((ntohl(sa4->sin_addr.s_addr) & ipv4_mcast_mask) == ipv4_mcast_mask) {
+			struct ip_mreqn mr4;
+			memset(&mr4, 0, sizeof(mr4));
+			mr4.imr_multiaddr = sa4->sin_addr;
+			mr4.imr_ifindex = req.ifr_ifindex;
+			res = setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mr4, sizeof(mr4));
+		} else {
+			sa4->sin_addr.s_addr = INADDR_ANY;
+		}
+	} else if (af == AF_INET6) {
+		struct sockaddr_in6 *sa6 = (struct sockaddr_in6*)sa;
+		if (sa6->sin6_addr.s6_addr[0] == 0xff) {
+			struct ipv6_mreq mr6;
+			memset(&mr6, 0, sizeof(mr6));
+			mr6.ipv6mr_multiaddr = sa6->sin6_addr;
+			mr6.ipv6mr_interface = req.ifr_ifindex;
+			res = setsockopt(fd, IPPROTO_IPV6, IPV6_JOIN_GROUP, &mr6, sizeof(mr6));
+		} else {
+		        sa6->sin6_addr = in6addr_any;
+		}
+	} else {
+		res = -EINVAL;
+		goto error;
+	}
+
+	if (res < 0) {
+		res = -errno;
+		pw_log_error("join mcast failed: %m");
+		goto error;
+	}
+
+	if (bind(fd, sa, salen) < 0) {
+		res = -errno;
+		pw_log_error("bind() failed: %m");
+		goto error;
+	}
+	return fd;
+error:
+	return res;
+}
+
+static uint32_t msec_to_bytes(struct sdp_info *info, uint32_t msec)
+{
+	return msec * info->stride * info->info.rate / 1000;
+}
+
+static void session_touch(struct session *sess)
+{
+	struct timespec ts;
+	clock_gettime(CLOCK_MONOTONIC, &ts);
+	sess->timestamp = SPA_TIMESPEC_TO_NSEC(&ts);
+}
+
+static void session_free(struct session *sess)
+{
+	pw_log_info("free session %s %s", sess->info.origin, sess->info.session);
+	if (sess->impl) {
+		sess->impl->n_sessions--;
+		spa_list_remove(&sess->link);
+	}
+	if (sess->stream)
+		pw_stream_destroy(sess->stream);
+	if (sess->source)
+		pw_loop_destroy_source(sess->impl->data_loop, sess->source);
+	free(sess);
+}
+
+static int session_new(struct impl *impl, struct sdp_info *info)
+{
+	struct session *session;
+	const struct spa_pod *params[1];
+	struct spa_pod_builder b;
+	uint32_t n_params;
+	uint8_t buffer[1024];
+	struct pw_properties *props;
+	int res, fd;
+
+	if (impl->n_sessions >= MAX_SESSIONS) {
+		pw_log_warn("too many sessions (%u >= %u)", impl->n_sessions, MAX_SESSIONS);
+		return -EMFILE;
+	}
+
+	session = calloc(1, sizeof(struct session));
+	if (session == NULL)
+		return -errno;
+
+	session->info = *info;
+
+	session->target_buffer = msec_to_bytes(info, impl->sess_latency_msec);
+	session->max_error = msec_to_bytes(info, ERROR_MSEC);
+
+	spa_dll_init(&session->dll);
+	spa_dll_set_bw(&session->dll, SPA_DLL_BW_MIN, 128, session->info.info.rate);
+
+	props = pw_properties_copy(impl->stream_props);
+	if (props == NULL) {
+		res = -errno;
+		goto error;
+	}
+
+	pw_properties_setf(props, PW_KEY_NODE_RATE, "1/%d", info->info.rate);
+	pw_properties_setf(props, PW_KEY_NODE_LATENCY, "%d/%d",
+			session->target_buffer / (2 * info->stride), info->info.rate);
+	pw_properties_set(props, "rtp.origin", info->origin);
+	pw_properties_setf(props, "rtp.payload", "%u", info->payload);
+	pw_properties_setf(props, "rtp.fmt", "%s/%u/%u", info->format_info->mime,
+			info->info.rate, info->info.channels);
+	if (info->session[0]) {
+		pw_properties_set(props, "rtp.session", info->session);
+		pw_properties_setf(props, PW_KEY_MEDIA_NAME, "RTP Stream (%s)",
+				info->session);
+	} else {
+		pw_properties_set(props, PW_KEY_MEDIA_NAME, "RTP Stream");
+	}
+
+	pw_log_info("new session %s %s", info->origin, info->session);
+
+	session->stream = pw_stream_new(impl->core,
+			"rtp-source playback", props);
+	if (session->stream == NULL) {
+		res = -errno;
+		pw_log_error("can't create stream: %m");
+		goto error;
+	}
+
+	pw_stream_add_listener(session->stream,
+			&session->stream_listener,
+			&out_stream_events, session);
+
+	n_params = 0;
+	spa_pod_builder_init(&b, buffer, sizeof(buffer));
+	params[n_params++] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat,
+			&info->info);
+
+	if ((res = pw_stream_connect(session->stream,
+			PW_DIRECTION_OUTPUT,
+			PW_ID_ANY,
+			PW_STREAM_FLAG_MAP_BUFFERS |
+			PW_STREAM_FLAG_AUTOCONNECT |
+			PW_STREAM_FLAG_RT_PROCESS,
+			params, n_params)) < 0) {
+		pw_log_error("can't connect stream: %s", spa_strerror(res));
+		goto error;
+	}
+
+	if ((fd = make_socket((const struct sockaddr *)&info->sa,
+					info->salen, impl->ifname)) < 0) {
+		res = fd;
+		goto error;
+	}
+
+	session->source = pw_loop_add_io(impl->data_loop, fd,
+				SPA_IO_IN, true, on_rtp_io, session);
+	if (session->source == NULL) {
+		res = -errno;
+		pw_log_error("can't create io source: %m");
+		goto error;
+	}
+
+	pw_log_info("starting RTP listener");
+	session_touch(session);
+
+	session->impl = impl;
+	spa_list_append(&impl->sessions, &session->link);
+	impl->n_sessions++;
+
+	return 0;
+error:
+	session_free(session);
+	return res;
+}
+
+static struct session *session_find(struct impl *impl, struct sdp_info *info)
+{
+	struct session *sess;
+	spa_list_for_each(sess, &impl->sessions, link) {
+		if (info->hash == sess->info.hash &&
+		    spa_streq(info->origin, sess->info.origin))
+			return sess;
+	}
+	return NULL;
+}
+
+static int parse_sdp_c(struct impl *impl, char *c, struct sdp_info *info)
+{
+	int res;
+
+	c[strcspn(c, "/")] = 0;
+	if (spa_strstartswith(c, "c=IN IP4 ")) {
+		struct sockaddr_in *sa = (struct sockaddr_in*) &info->sa;
+
+		c += strlen("c=IN IP4 ");
+		if (inet_pton(AF_INET, c, &sa->sin_addr) <= 0) {
+			res = -errno;
+			pw_log_warn("inet_pton(%s) failed: %m", c);
+			goto error;
+		}
+		sa->sin_family = AF_INET;
+		info->salen = sizeof(struct sockaddr_in);
+	}
+	else if (spa_strstartswith(c, "c=IN IP6 ")) {
+		struct sockaddr_in6 *sa = (struct sockaddr_in6*) &info->sa;
+
+		c += strlen("c=IN IP6 ");
+		if (inet_pton(AF_INET6, c, &sa->sin6_addr) <= 0) {
+			res = -errno;
+			pw_log_warn("inet_pton(%s) failed: %m", c);
+			goto error;
+		}
+
+		sa->sin6_family = AF_INET6;
+		info->salen = sizeof(struct sockaddr_in6);
+	} else
+		return -EINVAL;
+
+
+	res= 0;
+error:
+	return res;
+}
+
+static int parse_sdp_m(struct impl *impl, char *c, struct sdp_info *info)
+{
+	int port, payload;
+
+	if (!spa_strstartswith(c, "m=audio "))
+		return -EINVAL;
+
+	c += strlen("m=audio ");
+	if (sscanf(c, "%i RTP/AVP %i", &port, &payload) != 2)
+		return -EINVAL;
+
+	if (port <= 0 || port > 0xFFFF)
+		return -EINVAL;
+
+	if (payload < 0 || payload > 127)
+		return -EINVAL;
+
+	info->port = (uint16_t) port;
+	info->payload = (uint8_t) payload;
+
+	return 0;
+}
+
+static int parse_sdp_a(struct impl *impl, char *c, struct sdp_info *info)
+{
+	int payload, len, rate, channels;
+
+	if (!spa_strstartswith(c, "a=rtpmap:"))
+		return 0;
+
+	c += strlen("a=rtpmap:");
+
+	if (sscanf(c, "%i %n", &payload, &len) != 1)
+		return -EINVAL;
+
+	if (payload < 0 || payload > 127)
+		return -EINVAL;
+
+	if (payload != info->payload)
+		return 0;
+
+	c += len;
+	c[strcspn(c, "/")] = 0;
+
+	info->format_info = find_format_info(c);
+	if (info->format_info == NULL)
+		return -EINVAL;
+
+	info->info.format = info->format_info->format;
+	info->stride = info->format_info->size;
+
+	c += strlen(c) + 1;
+	if (sscanf(c, "%u/%u", &rate, &channels) == 2) {
+		info->info.rate = rate;
+		info->info.channels = channels;
+		if (channels == 2) {
+			info->info.position[0] = SPA_AUDIO_CHANNEL_FL;
+			info->info.position[1] = SPA_AUDIO_CHANNEL_FR;
+		}
+	} else if (sscanf(c, "%u", &rate) == 1) {
+		info->info.rate = rate;
+		info->info.channels = 1;
+	} else
+		return -EINVAL;
+
+	info->stride *= info->info.channels;
+
+	return 0;
+}
+
+static int parse_sdp(struct impl *impl, char *sdp, struct sdp_info *info)
+{
+	char *s = sdp;
+	int count = 0, res = 0;
+	size_t l;
+
+	while (*s) {
+		if ((l = strcspn(s, "\r\n")) < 2)
+			goto too_short;
+
+		s[l] = 0;
+		pw_log_debug("%d: %s", count, s);
+
+		if (count++ == 0 && strcmp(s, "v=0") != 0)
+			goto invalid_version;
+
+		if (spa_strstartswith(s, "o="))
+			snprintf(info->origin, sizeof(info->origin), "%s", &s[2]);
+		else if (spa_strstartswith(s, "s="))
+			snprintf(info->session, sizeof(info->session), "%s", &s[2]);
+		else if (spa_strstartswith(s, "c="))
+			res = parse_sdp_c(impl, s, info);
+		else if (spa_strstartswith(s, "m="))
+			res = parse_sdp_m(impl, s, info);
+		else if (spa_strstartswith(s, "a="))
+			res = parse_sdp_a(impl, s, info);
+
+		if (res < 0)
+			goto error;
+		s += l + 1;
+		while (isspace(*s))
+			s++;
+        }
+	if (((struct sockaddr*) &info->sa)->sa_family == AF_INET)
+		((struct sockaddr_in*) &info->sa)->sin_port = htons(info->port);
+	else
+		((struct sockaddr_in6*) &info->sa)->sin6_port = htons(info->port);
+
+	return 0;
+too_short:
+	pw_log_warn("SDP: line starting with `%.6s...' too short", s);
+	return -EINVAL;
+invalid_version:
+	pw_log_warn("SDP: invalid first version line `%*s'", (int)l, s);
+	return -EINVAL;
+error:
+	pw_log_warn("SDP: error: %s", spa_strerror(res));
+	return res;
+}
+
+static int parse_sap(struct impl *impl, void *data, size_t len)
+{
+	struct sap_header *header;
+	char *mime, *sdp;
+	struct sdp_info info;
+	struct session *sess;
+	int res;
+	size_t offs;
+	bool bye;
+
+	if (len < 8)
+		return -EINVAL;
+
+	header = (struct sap_header*) data;
+	if (header->v != 1)
+		return -EINVAL;
+
+	if (header->e)
+		return -ENOTSUP;
+	if (header->c)
+		return -ENOTSUP;
+
+	offs = header->a ? 12 : 8;
+	offs += header->auth_len * 4;
+	if (len <= offs)
+		return -EINVAL;
+
+	mime = SPA_PTROFF(data, offs, char);
+	if (spa_strstartswith(mime, "v=0")) {
+		sdp = mime;
+		mime = SAP_MIME_TYPE;
+	} else if (spa_streq(mime, SAP_MIME_TYPE))
+		sdp = SPA_PTROFF(mime, strlen(mime)+1, char);
+	else
+		return -EINVAL;
+
+	pw_log_debug("got sap: %s %s", mime, sdp);
+
+	spa_zero(info);
+	if ((res = parse_sdp(impl, sdp, &info)) < 0)
+		return res;
+
+	bye = header->t;
+
+	sess = session_find(impl, &info);
+	if (sess == NULL) {
+		if (!bye)
+			session_new(impl, &info);
+	} else {
+		if (bye)
+			session_free(sess);
+		else
+			session_touch(sess);
+	}
+	return res;
+}
+
+static void
+on_sap_io(void *data, int fd, uint32_t mask)
+{
+	struct impl *impl = data;
+
+	if (mask & SPA_IO_IN) {
+		uint8_t buffer[2048];
+		ssize_t len;
+
+		if ((len = recv(fd, buffer, sizeof(buffer), 0)) < 0) {
+			pw_log_warn("recv error: %m");
+			return;
+		}
+		if ((size_t)len >= sizeof(buffer))
+			return;
+
+		buffer[len] = 0;
+		parse_sap(impl, buffer, len);
+	}
+}
+
+static int start_sap_listener(struct impl *impl)
+{
+	struct sockaddr_in sa4;
+	struct sockaddr_in6 sa6;
+	struct sockaddr *sa;
+	socklen_t salen;
+	int fd, res;
+
+	if (inet_pton(AF_INET, impl->sap_ip, &sa4.sin_addr) > 0) {
+		sa4.sin_family = AF_INET;
+		sa4.sin_port = htons(impl->sap_port);
+		sa = (struct sockaddr*) &sa4;
+		salen = sizeof(sa4);
+	} else if (inet_pton(AF_INET6, impl->sap_ip, &sa6.sin6_addr) > 0) {
+		sa6.sin6_family = AF_INET6;
+		sa6.sin6_port = htons(impl->sap_port);
+		sa = (struct sockaddr*) &sa6;
+		salen = sizeof(sa6);
+	} else
+		return -EINVAL;
+
+	if ((fd = make_socket(sa, salen, impl->ifname)) < 0)
+		return fd;
+
+	pw_log_info("starting SAP listener");
+	impl->sap_source = pw_loop_add_io(impl->loop, fd,
+				SPA_IO_IN, true, on_sap_io, impl);
+	if (impl->sap_source == NULL) {
+		res = -errno;
+		goto error;
+	}
+	return 0;
+error:
+	close(fd);
+	return res;
+
+}
+
+static void on_timer_event(void *data, uint64_t expirations)
+{
+	struct impl *impl = data;
+	struct timespec now;
+	struct session *sess, *tmp;
+	uint64_t timestamp, interval;
+
+	clock_gettime(CLOCK_MONOTONIC, &now);
+	timestamp = SPA_TIMESPEC_TO_NSEC(&now);
+	interval = CLEANUP_INTERVAL_SEC * SPA_NSEC_PER_SEC;
+
+	spa_list_for_each_safe(sess, tmp, &impl->sessions, link) {
+		if (sess->timestamp + interval < timestamp)
+			session_free(sess);
+	}
+}
+
+static void core_destroy(void *d)
+{
+	struct impl *impl = d;
+	spa_hook_remove(&impl->core_listener);
+	impl->core = NULL;
+	pw_impl_module_schedule_destroy(impl->module);
+}
+
+static const struct pw_proxy_events core_proxy_events = {
+	.destroy = core_destroy,
+};
+
+static void impl_destroy(struct impl *impl)
+{
+	struct session *sess;
+	spa_list_consume(sess, &impl->sessions, link)
+		session_free(sess);
+
+	if (impl->core && impl->do_disconnect)
+		pw_core_disconnect(impl->core);
+
+	if (impl->sap_source)
+		pw_loop_destroy_source(impl->loop, impl->sap_source);
+	if (impl->timer)
+		pw_loop_destroy_source(impl->loop, impl->timer);
+
+	pw_properties_free(impl->stream_props);
+	pw_properties_free(impl->props);
+
+	free(impl->ifname);
+	free(impl->sap_ip);
+	free(impl);
+}
+
+static void module_destroy(void *d)
+{
+	struct impl *impl = d;
+	spa_hook_remove(&impl->module_listener);
+	impl_destroy(impl);
+}
+
+static const struct pw_impl_module_events module_events = {
+	PW_VERSION_IMPL_MODULE_EVENTS,
+	.destroy = module_destroy,
+};
+
+static void on_core_error(void *d, uint32_t id, int seq, int res, const char *message)
+{
+	struct impl *impl = d;
+
+	pw_log_error("error id:%u seq:%d res:%d (%s): %s",
+			id, seq, res, spa_strerror(res), message);
+
+	if (id == PW_ID_CORE && res == -EPIPE)
+		pw_impl_module_schedule_destroy(impl->module);
+}
+
+static const struct pw_core_events core_events = {
+	PW_VERSION_CORE_EVENTS,
+	.error = on_core_error,
+};
+
+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 impl *impl;
+	struct pw_properties *props = NULL, *stream_props = NULL;
+	const char *str;
+	struct timespec value, interval;
+	int res = 0;
+
+	PW_LOG_TOPIC_INIT(mod_topic);
+
+	impl = calloc(1, sizeof(struct impl));
+	if (impl == NULL)
+		return -errno;
+
+	if (args == NULL)
+		args = "";
+
+	props = pw_properties_new_string(args);
+	if (props == NULL) {
+		res = -errno;
+		pw_log_error( "can't create properties: %m");
+		goto out;
+	}
+	spa_list_init(&impl->sessions);
+	impl->props = props;
+
+	stream_props = pw_properties_new(NULL, NULL);
+	if (stream_props == NULL) {
+		res = -errno;
+		pw_log_error( "can't create properties: %m");
+		goto out;
+	}
+	impl->stream_props = stream_props;
+
+	impl->module = module;
+	impl->module_context = context;
+	impl->loop = pw_context_get_main_loop(context);
+	impl->data_loop = pw_data_loop_get_loop(pw_context_get_data_loop(context));
+
+	if (pw_properties_get(stream_props, PW_KEY_NODE_VIRTUAL) == NULL)
+		pw_properties_set(stream_props, PW_KEY_NODE_VIRTUAL, "true");
+	if (pw_properties_get(stream_props, PW_KEY_NODE_NETWORK) == NULL)
+		pw_properties_set(stream_props, PW_KEY_NODE_NETWORK, "true");
+
+	if ((str = pw_properties_get(props, "stream.props")) != NULL)
+		pw_properties_update_string(stream_props, str, strlen(str));
+
+	str = pw_properties_get(props, "local.ifname");
+	impl->ifname = str ? strdup(str) : NULL;
+
+	str = pw_properties_get(props, "sap.ip");
+	impl->sap_ip = strdup(str ? str : DEFAULT_SAP_IP);
+	impl->sap_port = pw_properties_get_uint32(props,
+			"sap.port", DEFAULT_SAP_PORT);
+	impl->sess_latency_msec = pw_properties_get_uint32(props,
+			"sess.latency.msec", DEFAULT_SESS_LATENCY);
+
+	impl->core = pw_context_get_object(impl->module_context, PW_TYPE_INTERFACE_Core);
+	if (impl->core == NULL) {
+		str = pw_properties_get(props, PW_KEY_REMOTE_NAME);
+		impl->core = pw_context_connect(impl->module_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 out;
+	}
+
+	pw_proxy_add_listener((struct pw_proxy*)impl->core,
+			&impl->core_proxy_listener,
+			&core_proxy_events, impl);
+	pw_core_add_listener(impl->core,
+			&impl->core_listener,
+			&core_events, impl);
+
+	impl->timer = pw_loop_add_timer(impl->loop, on_timer_event, impl);
+	if (impl->timer == NULL) {
+		res = -errno;
+		pw_log_error("can't create timer source: %m");
+		goto out;
+	}
+	value.tv_sec = 0;
+	value.tv_nsec = 1;
+	interval.tv_sec = CLEANUP_INTERVAL_SEC;
+	interval.tv_nsec = 0;
+	pw_loop_update_timer(impl->loop, impl->timer, &value, &interval, false);
+
+	if ((res = start_sap_listener(impl)) < 0)
+		goto out;
+
+	pw_impl_module_add_listener(module, &impl->module_listener, &module_events, impl);
+
+	pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_info));
+
+	pw_log_info("Successfully loaded module-rtp-source");
+
+	return 0;
+out:
+	impl_destroy(impl);
+	return res;
+}
diff --git a/src/modules/module-rtp/rtp.h b/src/modules/module-rtp/rtp.h
new file mode 100644
index 0000000000000000000000000000000000000000..92ff3640f4debec89c7b8f373087baff029c860c
--- /dev/null
+++ b/src/modules/module-rtp/rtp.h
@@ -0,0 +1,78 @@
+/* PipeWire
+ *
+ * Copyright © 2022 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * 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_RTP_H
+#define PIPEWIRE_RTP_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct rtp_header {
+#if __BYTE_ORDER == __LITTLE_ENDIAN
+	unsigned cc:4;
+	unsigned x:1;
+	unsigned p:1;
+	unsigned v:2;
+
+	unsigned pt:7;
+	unsigned m:1;
+#elif __BYTE_ORDER == __BIG_ENDIAN
+	unsigned v:2;
+	unsigned p:1;
+	unsigned x:1;
+	unsigned cc:4;
+
+	unsigned m:1;
+	unsigned pt:7;
+#else
+#error "Unknown byte order"
+#endif
+	uint16_t sequence_number;
+	uint32_t timestamp;
+	uint32_t ssrc;
+	uint32_t csrc[0];
+} __attribute__ ((packed));
+
+struct rtp_payload {
+#if __BYTE_ORDER == __LITTLE_ENDIAN
+	unsigned frame_count:4;
+	unsigned rfa0:1;
+	unsigned is_last_fragment:1;
+	unsigned is_first_fragment:1;
+	unsigned is_fragmented:1;
+#elif __BYTE_ORDER == __BIG_ENDIAN
+	unsigned is_fragmented:1;
+	unsigned is_first_fragment:1;
+	unsigned is_last_fragment:1;
+	unsigned rfa0:1;
+	unsigned frame_count:4;
+#endif
+} __attribute__ ((packed));
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PIPEWIRE_RTP_H */
diff --git a/src/modules/module-rtp/sap.h b/src/modules/module-rtp/sap.h
new file mode 100644
index 0000000000000000000000000000000000000000..b9a0a7a700f46d93b8df68a390e98e506d20db68
--- /dev/null
+++ b/src/modules/module-rtp/sap.h
@@ -0,0 +1,58 @@
+/* PipeWire
+ *
+ * Copyright © 2022 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * 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_SAP_H
+#define PIPEWIRE_SAP_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct sap_header {
+#if __BYTE_ORDER == __LITTLE_ENDIAN
+	unsigned c:1;
+	unsigned e:1;
+	unsigned t:1;
+	unsigned r:1;
+	unsigned a:1;
+	unsigned v:3;
+#elif __BYTE_ORDER == __BIG_ENDIAN
+	unsigned v:3;
+	unsigned a:1;
+	unsigned r:1;
+	unsigned t:1;
+	unsigned e:1;
+	unsigned c:1;
+#else
+#error "Unknown byte order"
+#endif
+	uint8_t auth_len;
+	uint16_t msg_id_hash;
+} __attribute__ ((packed));
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PIPEWIRE_SAP_H */
diff --git a/src/modules/module-session-manager/client-session/endpoint-link.h b/src/modules/module-session-manager/client-session/endpoint-link.h
index 936eed13074e31bcc45f04bd2464b95074d2dddb..c75f975bab93263630ee650bda2a2fccde8106ce 100644
--- a/src/modules/module-session-manager/client-session/endpoint-link.h
+++ b/src/modules/module-session-manager/client-session/endpoint-link.h
@@ -25,6 +25,7 @@
 
 #ifndef MODULE_SESSION_MANAGER_ENDPOINT_LINK_H
 #define MODULE_SESSION_MANAGER_ENDPOINT_LINK_H
+#include <stdint.h>
 
 #ifdef __cplusplus
 extern "C" {
diff --git a/src/modules/module-session-manager/client-session/session.h b/src/modules/module-session-manager/client-session/session.h
index ec6549e82f59aef7c4eda03b9e27c7e3aaa76833..a94b18fc27a3bb956b10711b2a95d2b7be71e0fc 100644
--- a/src/modules/module-session-manager/client-session/session.h
+++ b/src/modules/module-session-manager/client-session/session.h
@@ -25,6 +25,7 @@
 
 #ifndef MODULE_SESSION_MANAGER_SESSION_H
 #define MODULE_SESSION_MANAGER_SESSION_H
+#include <stdint.h>
 
 #ifdef __cplusplus
 extern "C" {
diff --git a/src/modules/module-x11-bell.c b/src/modules/module-x11-bell.c
index b7bc2a9708259677a2c72ec7b2356539ea913dba..cc0c2c8d50005e3c1ec89f7bd5eec6060975a0a4 100644
--- a/src/modules/module-x11-bell.c
+++ b/src/modules/module-x11-bell.c
@@ -109,7 +109,7 @@ static int play_sample(struct impl *impl)
 	if (sample == NULL)
 		sample = "bell-window-system";
 
-	pw_log_debug("play sample %s", sample);
+	pw_log_info("play sample %s", sample);
 
 	if ((res = ca_context_create(&ca)) < 0) {
 		pw_log_error("canberra context create error: %s", ca_strerror(res));
diff --git a/src/modules/module-zeroconf-discover.c b/src/modules/module-zeroconf-discover.c
index 317f87ee428e72590d657890b63e5189debb3e55..cc4c042353fdba2a4965c56f9a230d8f3747d4cb 100644
--- a/src/modules/module-zeroconf-discover.c
+++ b/src/modules/module-zeroconf-discover.c
@@ -54,7 +54,10 @@
  * audio to/from remote PulseAudio servers. It also works with
  * module-protocol-pulse.
  *
- * This module has no options.
+ * ## Module Options
+ *
+ * - `pulse.latency`: the latency to end-to-end latency in milliseconds to
+ *                    maintain (Default 200ms).
  *
  * ## Example configuration
  *
@@ -72,7 +75,7 @@
 PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
 #define PW_LOG_TOPIC_DEFAULT mod_topic
 
-#define MODULE_USAGE	" "
+#define MODULE_USAGE	"pulse.latency=<latency in msec> "
 
 static const struct spa_dict_item module_props[] = {
 	{ PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
@@ -109,7 +112,7 @@ struct tunnel_info {
 	const char *domain;
 };
 
-#define TUNNEL_INFO(...) (struct tunnel_info){ __VA_ARGS__ }
+#define TUNNEL_INFO(...) ((struct tunnel_info){ __VA_ARGS__ })
 
 struct tunnel {
 	struct spa_list link;
@@ -348,6 +351,9 @@ static void resolver_cb(AvahiServiceResolver *r, AvahiIfIndex interface, AvahiPr
 				_("%s on %s"), desc, fqdn);
 	}
 
+	if ((str = pw_properties_get(impl->properties, "pulse.latency")) != NULL)
+		pw_properties_set(props, "pulse.latency", str);
+
 	if ((f = open_memstream(&args, &size)) == NULL) {
 		pw_log_error("Can't open memstream: %m");
 		goto done;
diff --git a/src/pipewire/array.h b/src/pipewire/array.h
index 4e2dd72ebbe95589e42ac1cfe97cccf7c495131e..44f31a897786dab9d20c13e2e88779b81567a9ca 100644
--- a/src/pipewire/array.h
+++ b/src/pipewire/array.h
@@ -52,7 +52,7 @@ struct pw_array {
 	size_t extend;		/**< number of bytes to extend with */
 };
 
-#define PW_ARRAY_INIT(extend) (struct pw_array) { NULL, 0, 0, extend }
+#define PW_ARRAY_INIT(extend) ((struct pw_array) { NULL, 0, 0, (extend) })
 
 #define pw_array_get_len_s(a,s)			((a)->size / (s))
 #define pw_array_get_unchecked_s(a,idx,s,t)	SPA_PTROFF((a)->data,(idx)*(s),t)
@@ -67,17 +67,17 @@ struct pw_array {
 
 #define pw_array_first(a)	((a)->data)
 #define pw_array_end(a)		SPA_PTROFF((a)->data, (a)->size, void)
-#define pw_array_check(a,p)	(SPA_PTROFF(p,sizeof(*p),void) <= pw_array_end(a))
+#define pw_array_check(a,p)	(SPA_PTROFF(p,sizeof(*(p)),void) <= pw_array_end(a))
 
 #define pw_array_for_each(pos, array)					\
-	for (pos = (__typeof__(pos)) pw_array_first(array);		\
+	for ((pos) = (__typeof__(pos)) pw_array_first(array);		\
 	     pw_array_check(array, pos);				\
 	     (pos)++)
 
 #define pw_array_consume(pos, array)					\
-	for (pos = (__typeof__(pos)) pw_array_first(array);		\
+	for ((pos) = (__typeof__(pos)) pw_array_first(array);		\
 	     pw_array_check(array, pos);				\
-	     pos = (__typeof__(pos)) pw_array_first(array))
+	     (pos) = (__typeof__(pos)) pw_array_first(array))
 
 #define pw_array_remove(a,p)						\
 ({									\
diff --git a/src/pipewire/buffers.c b/src/pipewire/buffers.c
index d27b9ee245378315dc5142b36febb432d35f37f4..3499805ee36328163712d22db966480ed9830808 100644
--- a/src/pipewire/buffers.c
+++ b/src/pipewire/buffers.c
@@ -255,6 +255,12 @@ int pw_buffers_negotiate(struct pw_context *context, uint32_t flags,
 	struct port input = { innode, SPA_DIRECTION_INPUT, in_port_id };
 	int res;
 
+	if (flags & PW_BUFFERS_FLAG_IN_PRIORITY) {
+		struct port tmp = output;
+		output = input;
+		input = tmp;
+	}
+
 	res = param_filter(result, &input, &output, SPA_PARAM_Buffers, &b);
 	if (res < 0) {
 		pw_context_debug_port_params(context, input.node, input.direction,
diff --git a/src/pipewire/buffers.h b/src/pipewire/buffers.h
index df72bf3f8e715c6a969734640ea3e6e55df0c9a8..abef3925f8e2b6527095608279d43f5ef38f7fb7 100644
--- a/src/pipewire/buffers.h
+++ b/src/pipewire/buffers.h
@@ -48,6 +48,7 @@ extern "C" {
 #define PW_BUFFERS_FLAG_SHARED		(1<<1)	/**< buffers can be shared */
 #define PW_BUFFERS_FLAG_DYNAMIC		(1<<2)	/**< buffers have dynamic data */
 #define PW_BUFFERS_FLAG_SHARED_MEM	(1<<3)	/**< buffers need shared memory */
+#define PW_BUFFERS_FLAG_IN_PRIORITY	(1<<4)	/**< input parameters have priority */
 
 struct pw_buffers {
 	struct pw_memblock *mem;	/**< allocated buffer memory */
diff --git a/src/pipewire/context.c b/src/pipewire/context.c
index f86179121c640b2572bed839007e21c801b8963d..f5dc54cac00356e5f23cf23b6ca3ad3b21fe70a5 100644
--- a/src/pipewire/context.c
+++ b/src/pipewire/context.c
@@ -968,13 +968,15 @@ static int fraction_compare(const struct spa_fraction *a, const struct spa_fract
 	return fa < fb ? -1 : (fa > fb ? 1 : 0);
 }
 
-static bool rates_contains(uint32_t *rates, uint32_t n_rates, uint32_t rate)
+static uint32_t find_best_rate(uint32_t *rates, uint32_t n_rates, uint32_t rate, uint32_t best)
 {
 	uint32_t i;
-	for (i = 0; i < n_rates; i++)
-		if (rates[i] == rate)
-			return true;
-	return false;
+	for (i = 0; i < n_rates; i++) {
+		if (SPA_ABS((int32_t)rate - (int32_t)rates[i]) <
+		    SPA_ABS((int32_t)rate - (int32_t)best))
+			best = rates[i];
+	}
+	return best;
 }
 
 /* here we evaluate the complete state of the graph.
@@ -1166,6 +1168,10 @@ again:
 			if (s->active)
 				running = !n->passive;
 
+			pw_log_debug("%p: follower %p running:%d passive:%d rate:%u/%u latency %u/%u '%s'",
+				context, s, running, s->passive, rate.num, rate.denom,
+				latency.num, latency.denom, s->name);
+
 			s->moved = false;
 		}
 
@@ -1187,13 +1193,13 @@ again:
 			 * Start with the default rate. If the desired rate is
 			 * allowed, switch to it */
 			target_rate = def_rate;
-			if (rate.denom != 0 && rate.num == 1) {
-				if (rates_contains(rates, n_rates, rate.denom))
-					target_rate = rate.denom;
-			}
+			if (rate.denom != 0 && rate.num == 1)
+				target_rate = find_best_rate(rates, n_rates,
+						rate.denom, target_rate);
 		}
 
 		if (target_rate != current_rate) {
+			bool do_suspend = false;
 			/* we doing a rate switch */
 			pw_log_info("(%s-%u) state:%s new rate:%u->%u",
 					n->name, n->info.id,
@@ -1203,18 +1209,21 @@ again:
 
 			if (force_rate) {
 				if (settings->clock_rate_update_mode == CLOCK_RATE_UPDATE_MODE_HARD)
-					suspend_driver(context, n);
+					do_suspend = true;
 			} else {
-				if (n->info.state >= PW_NODE_STATE_IDLE)
-					suspend_driver(context, n);
+				if (n->info.state >= PW_NODE_STATE_SUSPENDED)
+					do_suspend = true;
 			}
+			if (do_suspend)
+				suspend_driver(context, n);
 			/* we're setting the pending rate. This will become the new
 			 * current rate in the next iteration of the graph. */
 			n->current_rate = SPA_FRACTION(1, target_rate);
 			n->current_pending = true;
 			current_rate = target_rate;
 			/* we might be suspended now and the links need to be prepared again */
-			goto again;
+			if (do_suspend)
+				goto again;
 		}
 
 		if (rate_quantum != 0 && current_rate != rate_quantum) {
@@ -1263,7 +1272,7 @@ again:
 			n->current_pending = false;
 		}
 
-		pw_log_debug("%p: driving %p running:%d passive:%d quantum:%u '%s'",
+		pw_log_debug("%p: driver %p running:%d passive:%d quantum:%u '%s'",
 				context, n, running, n->passive, quantum, n->name);
 
 		/* first change the node states of the followers to the new target */
diff --git a/src/pipewire/filter.c b/src/pipewire/filter.c
index 0eb368def05462567da909b84fef8c2bcfdd600c..4af39fbbec2acf029643c7936a6e1d3934cf82c0 100644
--- a/src/pipewire/filter.c
+++ b/src/pipewire/filter.c
@@ -419,16 +419,16 @@ static int enum_params(struct filter *d, struct spa_list *param_list, int seq,
 	spa_list_for_each(p, param_list, link) {
 		struct spa_pod *param;
 
-		result.index = result.next++;
-		if (result.index < start)
-			continue;
-
 		param = p->param;
 		if (param == NULL || p->id != id)
 			continue;
 
 		found = true;
 
+		result.index = result.next++;
+		if (result.index < start)
+			continue;
+
 		spa_pod_dynamic_builder_init(&b, buffer, sizeof(buffer), 4096);
 		if (spa_pod_filter(&b.b, &result.param, param, filter) == 0) {
 			spa_node_emit_result(&d->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
@@ -1179,7 +1179,7 @@ struct match {
 	struct pw_filter *filter;
 	int count;
 };
-#define MATCH_INIT(f) (struct match){ .filter = f }
+#define MATCH_INIT(f) ((struct match){ .filter = (f) })
 
 static int execute_match(void *data, const char *location, const char *action,
 		const char *val, size_t len)
diff --git a/src/pipewire/impl-device.c b/src/pipewire/impl-device.c
index 188da6f9ded067f83e33c254dbb6bcf4b0eea4c8..855b046de738a737f46150d9b13d21cf4bb3437b 100644
--- a/src/pipewire/impl-device.c
+++ b/src/pipewire/impl-device.c
@@ -235,7 +235,7 @@ static void remove_busy_resource(struct resource_data *d)
 
 	if (d->end != -1) {
 		if (d->pi && d->data.cache) {
-			pw_param_update(&impl->param_list, &impl->pending_list);
+			pw_param_update(&impl->param_list, &impl->pending_list, 0, NULL);
 			d->pi->user = 1;
 			d->pi = NULL;
 		}
@@ -281,8 +281,8 @@ static void result_device_params(void *data, int seq, int res, uint32_t type, co
 		if (d->cache) {
 			pw_log_debug("%p: add param %d", impl, r->id);
 			if (d->count++ == 0)
-				pw_param_add(&impl->pending_list, r->id, NULL);
-			pw_param_add(&impl->pending_list, r->id, r->param);
+				pw_param_add(&impl->pending_list, seq, r->id, NULL);
+			pw_param_add(&impl->pending_list, seq, r->id, r->param);
 		}
 		break;
 	}
@@ -333,10 +333,10 @@ int pw_impl_device_for_each_param(struct pw_impl_device *device,
 		result.next = 0;
 
 		spa_list_for_each(p, &impl->param_list, link) {
-			result.index = result.next++;
 			if (p->id != param_id)
 				continue;
 
+			result.index = result.next++;
 			if (result.index < index)
 				continue;
 
@@ -364,7 +364,7 @@ int pw_impl_device_for_each_param(struct pw_impl_device *device,
 		spa_hook_remove(&listener);
 
 		if (!SPA_RESULT_IS_ASYNC(res) && user_data.cache) {
-			pw_param_update(&impl->param_list, &impl->pending_list);
+			pw_param_update(&impl->param_list, &impl->pending_list, 0, NULL);
 			pi->user = 1;
 		}
 	}
diff --git a/src/pipewire/impl-link.c b/src/pipewire/impl-link.c
index b0293772927d2cc1affb4835a4338d3b6956f3ed..5eeff5b01dcfc799da18dd8cd7d55a8aa6791dbf 100644
--- a/src/pipewire/impl-link.c
+++ b/src/pipewire/impl-link.c
@@ -517,6 +517,9 @@ static int do_allocation(struct pw_impl_link *this)
 		if (output->node->remote || input->node->remote)
 			alloc_flags |= PW_BUFFERS_FLAG_SHARED_MEM;
 
+		if (output->node->driver)
+			alloc_flags |= PW_BUFFERS_FLAG_IN_PRIORITY;
+
 		/* if output port can alloc buffers, alloc skeleton buffers */
 		if (SPA_FLAG_IS_SET(out_flags, SPA_PORT_FLAG_CAN_ALLOC_BUFFERS)) {
 			SPA_FLAG_SET(alloc_flags, PW_BUFFERS_FLAG_NO_MEM);
@@ -619,7 +622,8 @@ int pw_impl_link_activate(struct pw_impl_link *this)
 	pw_log_debug("%p: activate activated:%d state:%s", this, impl->activated,
 			pw_link_state_as_string(this->info.state));
 
-	if (impl->activated || !this->prepared || !impl->inode->added || !impl->onode->active)
+	if (impl->activated || !this->prepared || !impl->inode->active ||
+			!impl->inode->added || !impl->onode->active)
 		return 0;
 
 	if (!impl->io_set) {
diff --git a/src/pipewire/impl-module.c b/src/pipewire/impl-module.c
index 0d4df47c09ab6bfc51e705bcf591bfe5e20f8f0a..282ba081b7993f1c6dc82477484ef5711ab48efa 100644
--- a/src/pipewire/impl-module.c
+++ b/src/pipewire/impl-module.c
@@ -178,6 +178,8 @@ pw_context_load_module(struct pw_context *context,
 		NULL
 	};
 
+	pw_log_info("%p: name:%s args:%s", context, name, args);
+
 	module_dir = getenv("PIPEWIRE_MODULE_DIR");
 	if (module_dir == NULL) {
 		module_dir = MODULEDIR;
@@ -276,10 +278,10 @@ pw_context_load_module(struct pw_context *context,
 
 error_not_found:
 	res = -ENOENT;
-	pw_log_error("No module \"%s\" was found", name);
+	pw_log_info("No module \"%s\" was found", name);
 	goto error_cleanup;
 error_open_failed:
-	res = -ENOENT;
+	res = -EIO;
 	pw_log_error("Failed to open module: \"%s\" %s", filename, dlerror());
 	goto error_free_filename;
 error_no_pw_module:
diff --git a/src/pipewire/impl-node.c b/src/pipewire/impl-node.c
index 2f3ec571588ab0b649afc87c3b891405f587a744..5480d273e1c4274fd84dd43a7ddb9bae2d013770 100644
--- a/src/pipewire/impl-node.c
+++ b/src/pipewire/impl-node.c
@@ -158,19 +158,6 @@ static void remove_node(struct pw_impl_node *this)
 	this->rt.driver_target.node = NULL;
 }
 
-static int
-do_node_remove(struct spa_loop *loop,
-	       bool async, uint32_t seq, const void *data, size_t size, void *user_data)
-{
-	struct pw_impl_node *this = user_data;
-	if (this->source.loop != NULL) {
-		spa_loop_remove_source(loop, &this->source);
-		remove_node(this);
-	}
-	this->added = false;
-	return 0;
-}
-
 static void node_deactivate(struct pw_impl_node *this)
 {
 	struct pw_impl_port *port;
@@ -185,15 +172,14 @@ static void node_deactivate(struct pw_impl_node *this)
 		spa_list_for_each(link, &port->links, output_link)
 			pw_impl_link_deactivate(link);
 	}
-	pw_loop_invoke(this->data_loop, do_node_remove, 1, NULL, 0, true, this);
 }
 
-static int pause_node(struct pw_impl_node *this)
+static int idle_node(struct pw_impl_node *this)
 {
 	struct impl *impl = SPA_CONTAINER_OF(this, struct impl, this);
 	int res = 0;
 
-	pw_log_debug("%p: pause node state:%s pending:%s pause-on-idle:%d", this,
+	pw_log_debug("%p: idle node state:%s pending:%s pause-on-idle:%d", this,
 			pw_node_state_as_string(this->info.state),
 			pw_node_state_as_string(impl->pending_state),
 			impl->pause_on_idle);
@@ -201,6 +187,9 @@ static int pause_node(struct pw_impl_node *this)
 	if (impl->pending_state <= PW_NODE_STATE_IDLE)
 		return 0;
 
+	if (!impl->pause_on_idle)
+		return 0;
+
 	node_deactivate(this);
 
 	res = spa_node_send_command(this->node,
@@ -247,7 +236,8 @@ static int start_node(struct pw_impl_node *this)
 	if (impl->pending_state >= PW_NODE_STATE_RUNNING)
 		return 0;
 
-	pw_log_debug("%p: start node", this);
+	pw_log_debug("%p: start node driving:%d driver:%d added:%d", this,
+			this->driving, this->driver, this->added);
 
 	if (!(this->driving && this->driver)) {
 		impl->pending_play = true;
@@ -350,6 +340,19 @@ do_node_add(struct spa_loop *loop,
 	return 0;
 }
 
+static int
+do_node_remove(struct spa_loop *loop,
+	       bool async, uint32_t seq, const void *data, size_t size, void *user_data)
+{
+	struct pw_impl_node *this = user_data;
+	if (this->source.loop != NULL) {
+		spa_loop_remove_source(loop, &this->source);
+		remove_node(this);
+	}
+	this->added = false;
+	return 0;
+}
+
 static void node_update_state(struct pw_impl_node *node, enum pw_node_state state, int res, char *error)
 {
 	struct impl *impl = SPA_CONTAINER_OF(node, struct impl, this);
@@ -357,20 +360,32 @@ static void node_update_state(struct pw_impl_node *node, enum pw_node_state stat
 
 	switch (state) {
 	case PW_NODE_STATE_RUNNING:
+		pw_log_debug("%p: start node driving:%d driver:%d added:%d", node,
+				node->driving, node->driver, node->added);
+
+		if (res >= 0) {
+			pw_loop_invoke(node->data_loop, do_node_add, 1, NULL, 0, true, node);
+		}
 		if (node->driving && node->driver) {
 			res = spa_node_send_command(node->node,
 				&SPA_NODE_COMMAND_INIT(SPA_NODE_COMMAND_Start));
 			if (res < 0) {
 				state = PW_NODE_STATE_ERROR;
 				error = spa_aprintf("Start error: %s", spa_strerror(res));
+				pw_loop_invoke(node->data_loop, do_node_remove, 1, NULL, 0, true, node);
 			}
 		}
 		if (res >= 0) {
-			pw_loop_invoke(node->data_loop, do_node_add, 1, NULL, 0, true, node);
 			/* now activate the inputs */
 			node_activate_inputs(node);
 		}
 		break;
+	case PW_NODE_STATE_IDLE:
+	case PW_NODE_STATE_SUSPENDED:
+	case PW_NODE_STATE_ERROR:
+		if (state != PW_NODE_STATE_IDLE || impl->pause_on_idle)
+			pw_loop_invoke(node->data_loop, do_node_remove, 1, NULL, 0, true, node);
+		break;
 	default:
 		break;
 	}
@@ -438,6 +453,9 @@ static int suspend_node(struct pw_impl_node *this)
 		p->state = PW_IMPL_PORT_STATE_CONFIGURE;
 	}
 
+	pw_log_debug("%p: suspend node driving:%d driver:%d added:%d", this,
+			this->driving, this->driver, this->added);
+
 	res = spa_node_send_command(this->node,
 				    &SPA_NODE_COMMAND_INIT(SPA_NODE_COMMAND_Suspend));
 	if (res == -ENOTSUP)
@@ -1614,8 +1632,13 @@ static int node_ready(void *data, int status)
 	struct pw_node_target *t;
 	struct pw_impl_port *p;
 
-	pw_log_trace_fp("%p: ready driver:%d exported:%d %p status:%d", node,
-			node->driver, node->exported, driver, status);
+	pw_log_trace_fp("%p: ready driver:%d exported:%d %p status:%d added:%d", node,
+			node->driver, node->exported, driver, status, node->added);
+
+	if (!node->added) {
+		pw_log_warn("%p: ready non-active node", node);
+		return -EIO;
+	}
 
 	if (SPA_UNLIKELY(node == driver)) {
 		struct pw_node_activation *a = node->rt.activation;
@@ -1944,8 +1967,8 @@ static void result_node_params(void *data, int seq, int res, uint32_t type, cons
 			d->callback(d->data, seq, r->id, r->index, r->next, r->param);
 			if (d->cache) {
 				if (d->count++ == 0)
-					pw_param_add(&impl->pending_list, r->id, NULL);
-				pw_param_add(&impl->pending_list, r->id, r->param);
+					pw_param_add(&impl->pending_list, seq, r->id, NULL);
+				pw_param_add(&impl->pending_list, seq, r->id, r->param);
 			}
 		}
 		break;
@@ -1997,10 +2020,10 @@ int pw_impl_node_for_each_param(struct pw_impl_node *node,
 		result.next = 0;
 
 		spa_list_for_each(p, &impl->param_list, link) {
-			result.index = result.next++;
 			if (p->id != param_id)
 				continue;
 
+			result.index = result.next++;
 			if (result.index < index)
 				continue;
 
@@ -2029,7 +2052,7 @@ int pw_impl_node_for_each_param(struct pw_impl_node *node,
 		spa_hook_remove(&listener);
 
 		if (user_data.cache) {
-			pw_param_update(&impl->param_list, &impl->pending_list);
+			pw_param_update(&impl->param_list, &impl->pending_list, 0, NULL);
 			pi->user = 1;
 		}
 	}
@@ -2185,8 +2208,7 @@ int pw_impl_node_set_state(struct pw_impl_node *node, enum pw_node_state state)
 		break;
 
 	case PW_NODE_STATE_IDLE:
-		if (impl->pause_on_idle)
-			res = pause_node(node);
+		res = idle_node(node);
 		break;
 
 	case PW_NODE_STATE_RUNNING:
@@ -2215,8 +2237,7 @@ int pw_impl_node_set_state(struct pw_impl_node *node, enum pw_node_state state)
 			    state < PW_NODE_STATE_RUNNING &&
 			    impl->pending_play) {
 				impl->pending_play = false;
-				spa_node_send_command(node->node,
-					&SPA_NODE_COMMAND_INIT(SPA_NODE_COMMAND_Pause));
+				idle_node(node);
 			}
 			pw_work_queue_cancel(impl->work, node, impl->pending_id);
 			node->info.state = impl->pending_state;
diff --git a/src/pipewire/impl-port.c b/src/pipewire/impl-port.c
index 8b0cb38b02f0df565faae6005a5b5d530b5ed0d0..ad7e191f1307705795ec8df87edacea61a3ac2f4 100644
--- a/src/pipewire/impl-port.c
+++ b/src/pipewire/impl-port.c
@@ -1199,8 +1199,8 @@ static void result_port_params(void *data, int seq, int res, uint32_t type, cons
 			d->callback(d->data, seq, r->id, r->index, r->next, r->param);
 			if (d->cache) {
 				if (d->count++ == 0)
-					pw_param_add(&impl->pending_list, r->id, NULL);
-				pw_param_add(&impl->pending_list, r->id, r->param);
+					pw_param_add(&impl->pending_list, seq, r->id, NULL);
+				pw_param_add(&impl->pending_list, seq, r->id, r->param);
 			}
 		}
 		break;
@@ -1253,10 +1253,10 @@ int pw_impl_port_for_each_param(struct pw_impl_port *port,
 		result.next = 0;
 
 		spa_list_for_each(p, &impl->param_list, link) {
-			result.index = result.next++;
 			if (p->id != param_id)
 				continue;
 
+			result.index = result.next++;
 			if (result.index < index)
 				continue;
 
@@ -1286,7 +1286,7 @@ int pw_impl_port_for_each_param(struct pw_impl_port *port,
 		spa_hook_remove(&listener);
 
 		if (user_data.cache) {
-			pw_param_update(&impl->param_list, &impl->pending_list);
+			pw_param_update(&impl->param_list, &impl->pending_list, 0, NULL);
 			pi->user = 1;
 		}
 	}
diff --git a/src/pipewire/introspect.c b/src/pipewire/introspect.c
index 2855009c01e23e07fee3aa0c52dba55a5ccff803..e52ef2851cf072a5ffd540a77006b3daeecae23c 100644
--- a/src/pipewire/introspect.c
+++ b/src/pipewire/introspect.c
@@ -210,7 +210,7 @@ struct pw_node_info *pw_node_info_merge(struct pw_node_info *info,
 		info->props = pw_spa_dict_copy(update->props);
 	}
 	if (update->change_mask & PW_NODE_CHANGE_MASK_PARAMS) {
-		uint32_t i, user, n_params = update->n_params;
+		uint32_t i, n_params = update->n_params;
 		void *np;
 
 		np = pw_reallocarray(info->params, n_params, sizeof(struct spa_param_info));
@@ -222,15 +222,18 @@ struct pw_node_info *pw_node_info_merge(struct pw_node_info *info,
 		info->params = np;
 
 		for (i = 0; i < SPA_MIN(info->n_params, n_params); i++) {
-			user = reset ? 0 : info->params[i].user;
-			if (info->params[i].flags != update->params[i].flags)
-				user++;
-			info->params[i] = update->params[i];
-			info->params[i].user = user;
+			info->params[i].id = update->params[i].id;
+			if (reset)
+				info->params[i].user = 0;
+			if (info->params[i].flags != update->params[i].flags) {
+				info->params[i].flags = update->params[i].flags;
+				info->params[i].user++;
+			}
 		}
 		info->n_params = n_params;
 		for (; i < info->n_params; i++) {
-			info->params[i] = update->params[i];
+			info->params[i].id = update->params[i].id;
+			info->params[i].flags = update->params[i].flags;
 			info->params[i].user = 1;
 		}
 	}
@@ -280,7 +283,7 @@ struct pw_port_info *pw_port_info_merge(struct pw_port_info *info,
 		info->props = pw_spa_dict_copy(update->props);
 	}
 	if (update->change_mask & PW_PORT_CHANGE_MASK_PARAMS) {
-		uint32_t i, user, n_params = update->n_params;
+		uint32_t i, n_params = update->n_params;
 		void *np;
 
 		np = pw_reallocarray(info->params, n_params, sizeof(struct spa_param_info));
@@ -292,15 +295,18 @@ struct pw_port_info *pw_port_info_merge(struct pw_port_info *info,
 		info->params = np;
 
 		for (i = 0; i < SPA_MIN(info->n_params, n_params); i++) {
-			user = reset ? 0 : info->params[i].user;
-			if (info->params[i].flags != update->params[i].flags)
-				user++;
-			info->params[i] = update->params[i];
-			info->params[i].user = user;
+			info->params[i].id = update->params[i].id;
+			if (reset)
+				info->params[i].user = 0;
+			if (info->params[i].flags != update->params[i].flags) {
+				info->params[i].flags = update->params[i].flags;
+				info->params[i].user++;
+			}
 		}
 		info->n_params = n_params;
 		for (; i < info->n_params; i++) {
-			info->params[i] = update->params[i];
+			info->params[i].id = update->params[i].id;
+			info->params[i].flags = update->params[i].flags;
 			info->params[i].user = 1;
 		}
 	}
@@ -440,7 +446,7 @@ struct pw_device_info *pw_device_info_merge(struct pw_device_info *info,
 		info->props = pw_spa_dict_copy(update->props);
 	}
 	if (update->change_mask & PW_DEVICE_CHANGE_MASK_PARAMS) {
-		uint32_t i, user, n_params = update->n_params;
+		uint32_t i, n_params = update->n_params;
 		void *np;
 
 		np = pw_reallocarray(info->params, n_params, sizeof(struct spa_param_info));
@@ -452,15 +458,18 @@ struct pw_device_info *pw_device_info_merge(struct pw_device_info *info,
 		info->params = np;
 
 		for (i = 0; i < SPA_MIN(info->n_params, n_params); i++) {
-			user = reset ? 0 : info->params[i].user;
-			if (info->params[i].flags != update->params[i].flags)
-				user++;
-			info->params[i] = update->params[i];
-			info->params[i].user = user;
+			info->params[i].id = update->params[i].id;
+			if (reset)
+				info->params[i].user = 0;
+			if (info->params[i].flags != update->params[i].flags) {
+				info->params[i].flags = update->params[i].flags;
+				info->params[i].user++;
+			}
 		}
 		info->n_params = n_params;
 		for (; i < info->n_params; i++) {
-			info->params[i] = update->params[i];
+			info->params[i].id = update->params[i].id;
+			info->params[i].flags = update->params[i].flags;
 			info->params[i].user = 1;
 		}
 	}
diff --git a/src/pipewire/log.h b/src/pipewire/log.h
index 26ffc20f9a0d6e5d3690abb6192079bf40c565f3..f91dc1136e96a4a3f5065708ded5a57891a507db 100644
--- a/src/pipewire/log.h
+++ b/src/pipewire/log.h
@@ -116,7 +116,7 @@ _pw_log_topic_new(struct spa_log_topic *topic);
  */
 #define PW_LOG_TOPIC_STATIC(var, topic) \
   static struct spa_log_topic var##__LINE__ = SPA_LOG_TOPIC(0, topic); \
-  static struct spa_log_topic *var = &(var##__LINE__)
+  static struct spa_log_topic *(var) = &(var##__LINE__)
 
 /**
  * Declare a static log topic named \a var.
@@ -131,7 +131,7 @@ _pw_log_topic_new(struct spa_log_topic *topic);
  */
 #define PW_LOG_TOPIC(var, topic) \
   struct spa_log_topic var##__LINE__ = SPA_LOG_TOPIC(0, topic); \
-  struct spa_log_topic *var = &(var##__LINE__)
+  struct spa_log_topic *(var) = &(var##__LINE__)
 
 #define PW_LOG_TOPIC_INIT(var) \
    spa_log_topic_init(pw_log_get(), var);
diff --git a/src/pipewire/map.h b/src/pipewire/map.h
index f2b54fc7cfb9b037f7090854d8d2d023e81c0216..9a46082e235ad315a415cebda1343e3d7b5a6396 100644
--- a/src/pipewire/map.h
+++ b/src/pipewire/map.h
@@ -85,7 +85,7 @@ struct pw_map {
 };
 
 /** \param extend the amount of bytes to grow the map with when needed */
-#define PW_MAP_INIT(extend) (struct pw_map) { PW_ARRAY_INIT(extend), SPA_ID_INVALID }
+#define PW_MAP_INIT(extend) ((struct pw_map) { PW_ARRAY_INIT(extend), SPA_ID_INVALID })
 
 /**
  * Get the number of currently allocated elements in the map.
diff --git a/src/pipewire/permission.h b/src/pipewire/permission.h
index e0274ddcbb2c463678e128d3bce324afca5387cf..1bcebc3aa0a3d115c7681aa8a9b03640208dbb11 100644
--- a/src/pipewire/permission.h
+++ b/src/pipewire/permission.h
@@ -66,7 +66,7 @@ struct pw_permission {
 	uint32_t permissions;	/**< bitmask of above permissions */
 };
 
-#define PW_PERMISSION_INIT(id,p) (struct pw_permission){ (id), (p) }
+#define PW_PERMISSION_INIT(id,p) ((struct pw_permission){ (id), (p) })
 
 #define PW_PERMISSION_FORMAT "%c%c%c%c"
 #define PW_PERMISSION_ARGS(permission)		\
diff --git a/src/pipewire/private.h b/src/pipewire/private.h
index 50080079afc5f87be846bbb1fd5735408a31455a..659f4ab8f65e41bc56f8368baa9ccfd344ab3bcb 100644
--- a/src/pipewire/private.h
+++ b/src/pipewire/private.h
@@ -49,7 +49,7 @@ struct ucred {
 #define spa_debug(...) pw_log_trace(__VA_ARGS__)
 #endif
 
-#define MAX_RATES				16u
+#define MAX_RATES				32u
 #define CLOCK_MIN_QUANTUM			4u
 #define CLOCK_MAX_QUANTUM			65536u
 
@@ -104,6 +104,7 @@ static inline bool ratelimit_test(struct ratelimit *r, uint64_t now, enum spa_lo
 
 struct pw_param {
 	uint32_t id;
+	int32_t seq;
 	struct spa_list link;
 	struct spa_pod *param;
 };
@@ -123,7 +124,7 @@ static inline uint32_t pw_param_clear(struct spa_list *param_list, uint32_t id)
 	return count;
 }
 
-static inline struct pw_param *pw_param_add(struct spa_list *params,
+static inline struct pw_param *pw_param_add(struct spa_list *params, int32_t seq,
 		uint32_t id, const struct spa_pod *param)
 {
 	struct pw_param *p;
@@ -140,6 +141,7 @@ static inline struct pw_param *pw_param_add(struct spa_list *params,
 		return NULL;
 
 	p->id = id;
+	p->seq = seq;
 	if (param != NULL) {
 		p->param = SPA_PTROFF(p, sizeof(*p), struct spa_pod);
 		memcpy(p->param, param, SPA_POD_SIZE(param));
@@ -151,10 +153,22 @@ static inline struct pw_param *pw_param_add(struct spa_list *params,
 	return p;
 }
 
-static inline void pw_param_update(struct spa_list *param_list, struct spa_list *pending_list)
+static inline void pw_param_update(struct spa_list *param_list, struct spa_list *pending_list,
+			uint32_t n_params, struct spa_param_info *params)
 {
-	struct pw_param *p;
+	struct pw_param *p, *t;
+	uint32_t i;
 
+	for (i = 0; i < n_params; i++) {
+		spa_list_for_each_safe(p, t, pending_list, link) {
+			if (p->id == params[i].id &&
+			    p->seq != params[i].seq &&
+			    p->param != NULL) {
+				spa_list_remove(&p->link);
+				free(p);
+			}
+		}
+	}
 	spa_list_consume(p, pending_list, link) {
 		spa_list_remove(&p->link);
 		if (p->param == NULL) {
@@ -177,7 +191,7 @@ static inline struct spa_param_info *pw_param_info_find(struct spa_param_info in
 	return NULL;
 }
 
-#define pw_protocol_emit_destroy(p) spa_hook_list_call(&p->listener_list, struct pw_protocol_events, destroy, 0)
+#define pw_protocol_emit_destroy(p) spa_hook_list_call(&(p)->listener_list, struct pw_protocol_events, destroy, 0)
 
 struct pw_protocol {
 	struct spa_list link;                   /**< link in context protocol_list */
@@ -805,7 +819,7 @@ struct pw_impl_port_implementation {
 #define pw_impl_port_emit_param_changed(p,i)		pw_impl_port_emit(p, param_changed, 1, i)
 #define pw_impl_port_emit_latency_changed(p)		pw_impl_port_emit(p, latency_changed, 2)
 
-#define PW_IMPL_PORT_IS_CONTROL(port)	SPA_FLAG_MASK(port->flags, \
+#define PW_IMPL_PORT_IS_CONTROL(port)	SPA_FLAG_MASK((port)->flags, \
 						PW_IMPL_PORT_FLAG_BUFFERS|PW_IMPL_PORT_FLAG_CONTROL,\
 						PW_IMPL_PORT_FLAG_CONTROL)
 struct pw_impl_port {
diff --git a/src/pipewire/stream.c b/src/pipewire/stream.c
index ff0e0f84068769766ad7b7ea48d80519798432c4..808eb47473c460fd659949cf8fda5dc6a5b26415 100644
--- a/src/pipewire/stream.c
+++ b/src/pipewire/stream.c
@@ -400,6 +400,28 @@ static struct buffer *get_buffer(struct pw_stream *stream, uint32_t id)
 	return NULL;
 }
 
+static inline uint32_t update_requested(struct stream *impl)
+{
+	uint32_t index, id, res = 0;
+	struct buffer *buffer;
+	struct spa_io_rate_match *r = impl->rate_match;
+
+	if (spa_ringbuffer_get_read_index(&impl->dequeued.ring, &index) < 1)
+		return 0;
+
+	id = impl->dequeued.ids[index & MASK_BUFFERS];
+	buffer = &impl->buffers[id];
+	if (r) {
+		buffer->this.requested = r->size;
+		res = r->size > 0 ? 1 : 0;
+	} else {
+		buffer->this.requested = impl->quantum;
+		res = 1;
+	}
+	pw_log_trace_fp("%p: update buffer:%u size:%"PRIu64, impl, id, buffer->this.requested);
+	return res;
+}
+
 static int
 do_call_process(struct spa_loop *loop,
                  bool async, uint32_t seq, const void *data, size_t size, void *user_data)
@@ -411,9 +433,11 @@ do_call_process(struct spa_loop *loop,
 	return 0;
 }
 
-static void call_process(struct stream *impl)
+static inline void call_process(struct stream *impl)
 {
 	pw_log_trace_fp("%p: call process rt:%u", impl, impl->process_rt);
+	if (impl->direction == SPA_DIRECTION_OUTPUT && update_requested(impl) <= 0)
+		return;
 	if (impl->process_rt)
 		spa_callbacks_call(&impl->rt_callbacks, struct pw_stream_events, process, 0);
 	else
@@ -520,16 +544,16 @@ static int enum_params(void *object, bool is_port, int seq, uint32_t id, uint32_
 	spa_list_for_each(p, &d->param_list, link) {
 		struct spa_pod *param;
 
-		result.index = result.next++;
-		if (result.index < start)
-			continue;
-
 		param = p->param;
 		if (param == NULL || p->id != id)
 			continue;
 
 		found = true;
 
+		result.index = result.next++;
+		if (result.index < start)
+			continue;
+
 		spa_pod_dynamic_builder_init(&b, buffer, sizeof(buffer), 4096);
 		if (spa_pod_filter(&b.b, &result.param, param, filter) == 0) {
 			spa_node_emit_result(&d->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
@@ -563,26 +587,26 @@ static int impl_set_param(void *object, uint32_t id, uint32_t flags, const struc
 	return 0;
 }
 
-static inline uint32_t update_requested(struct stream *impl)
+static inline void copy_position(struct stream *impl, int64_t queued)
 {
-	uint32_t index, id, res = 0;
-	struct buffer *buffer;
-	struct spa_io_rate_match *r = impl->rate_match;
-
-	if (spa_ringbuffer_get_read_index(&impl->dequeued.ring, &index) < 1)
-		return 0;
+	struct spa_io_position *p = impl->rt.position;
 
-	id = impl->dequeued.ids[index & MASK_BUFFERS];
-	buffer = &impl->buffers[id];
-	if (r) {
-		buffer->this.requested = r->size;
-		res = r->size > 0 ? 1 : 0;
-	} else {
-		buffer->this.requested = impl->quantum;
-		res = 1;
+	SEQ_WRITE(impl->seq);
+	if (SPA_LIKELY(p != NULL)) {
+		impl->time.now = p->clock.nsec;
+		impl->time.rate = p->clock.rate;
+		if (SPA_UNLIKELY(impl->clock_id != p->clock.id)) {
+			impl->base_pos = p->clock.position - impl->time.ticks;
+			impl->clock_id = p->clock.id;
+		}
+		impl->time.ticks = p->clock.position - impl->base_pos;
+		impl->time.delay = 0;
+		impl->time.queued = queued;
+		impl->quantum = p->clock.duration;
 	}
-	pw_log_trace_fp("%p: update buffer:%u size:%"PRIu64, impl, id, buffer->this.requested);
-	return res;
+	if (SPA_LIKELY(impl->rate_match != NULL))
+		impl->rate_queued = impl->rate_match->delay;
+	SEQ_WRITE(impl->seq);
 }
 
 static int impl_send_command(void *object, const struct spa_command *command)
@@ -613,8 +637,8 @@ static int impl_send_command(void *object, const struct spa_command *command)
 			if (impl->direction == SPA_DIRECTION_INPUT)
 				impl->io->status = SPA_STATUS_NEED_DATA;
 			else if (!impl->process_rt && !impl->driving) {
-				if (update_requested(impl) > 0)
-					call_process(impl);
+				copy_position(impl, impl->queued.incount);
+				call_process(impl);
 			}
 
 			stream_set_state(stream, PW_STREAM_STATE_STREAMING, NULL);
@@ -963,28 +987,6 @@ static int impl_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffe
 	return 0;
 }
 
-static inline void copy_position(struct stream *impl, int64_t queued)
-{
-	struct spa_io_position *p = impl->rt.position;
-
-	SEQ_WRITE(impl->seq);
-	if (SPA_LIKELY(p != NULL)) {
-		impl->time.now = p->clock.nsec;
-		impl->time.rate = p->clock.rate;
-		if (SPA_UNLIKELY(impl->clock_id != p->clock.id)) {
-			impl->base_pos = p->clock.position - impl->time.ticks;
-			impl->clock_id = p->clock.id;
-		}
-		impl->time.ticks = p->clock.position - impl->base_pos;
-		impl->time.delay = 0;
-		impl->time.queued = queued;
-		impl->quantum = p->clock.duration;
-	}
-	if (SPA_LIKELY(impl->rate_match != NULL))
-		impl->rate_queued = impl->rate_match->delay;
-	SEQ_WRITE(impl->seq);
-}
-
 static int impl_node_process_input(void *object)
 {
 	struct stream *impl = object;
@@ -1081,8 +1083,7 @@ again:
 		/* we're not draining, not a driver check if we need to get
 		 * more buffers */
 		if (ask_more) {
-			if (update_requested(impl) > 0)
-				call_process(impl);
+			call_process(impl);
 			/* 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 &&
@@ -1403,7 +1404,7 @@ struct match {
 	struct pw_stream *stream;
 	int count;
 };
-#define MATCH_INIT(s) (struct match){ .stream = s }
+#define MATCH_INIT(s) ((struct match){ .stream = (s) })
 
 static int execute_match(void *data, const char *location, const char *action,
 		const char *val, size_t len)
@@ -2331,7 +2332,7 @@ int pw_stream_flush(struct pw_stream *stream, bool drain)
 	struct stream *impl = SPA_CONTAINER_OF(stream, struct stream, this);
 	pw_loop_invoke(impl->context->data_loop,
 			drain ? do_drain : do_flush, 1, NULL, 0, true, impl);
-	if (!drain)
+	if (!drain && impl->node != NULL)
 		spa_node_send_command(impl->node->node,
 				&SPA_NODE_COMMAND_INIT(SPA_NODE_COMMAND_Flush));
 	return 0;
@@ -2352,7 +2353,7 @@ do_trigger_process(struct spa_loop *loop,
 	int res;
 	if (impl->direction == SPA_DIRECTION_OUTPUT) {
 		if (impl->process_rt)
-			spa_callbacks_call(&impl->rt_callbacks, struct pw_stream_events, process, 0);
+			call_process(impl);
 		res = impl->node_methods.process(impl);
 	} else {
 		res = SPA_STATUS_NEED_DATA;
@@ -2386,11 +2387,9 @@ int pw_stream_trigger_process(struct pw_stream *stream)
 	if (!impl->driving && !impl->trigger) {
 		res = trigger_request_process(impl);
 	} else {
-		if (impl->direction == SPA_DIRECTION_OUTPUT &&
-		    !impl->process_rt) {
-			pw_loop_invoke(impl->context->main_loop,
-				do_call_process, 1, NULL, 0, false, impl);
-		}
+		if (!impl->process_rt)
+			call_process(impl);
+
 		res = pw_loop_invoke(impl->context->data_loop,
 			do_trigger_process, 1, NULL, 0, false, impl);
 	}
diff --git a/src/pipewire/thread-loop.c b/src/pipewire/thread-loop.c
index 5588b63461f29acc1b60bfeaa0b878b112989cc1..2fdd50e224d309d464d7ea3d3a5bcf2852ee3fe0 100644
--- a/src/pipewire/thread-loop.c
+++ b/src/pipewire/thread-loop.c
@@ -91,7 +91,7 @@ static void do_stop(void *data, uint64_t count)
 
 #define CHECK(expression,label)						\
 do {									\
-	if ((errno = expression) != 0) {				\
+	if ((errno = (expression)) != 0) {				\
 		res = -errno;						\
 		pw_log_error(#expression ": %s", strerror(errno));	\
 		goto label;						\
diff --git a/src/pipewire/thread.c b/src/pipewire/thread.c
index 72be387bd82f96826346d45df51615d1ef183bad..02b8b9ed1729a7592531b209f534222f0b7fbfa9 100644
--- a/src/pipewire/thread.c
+++ b/src/pipewire/thread.c
@@ -37,7 +37,7 @@
 
 #define CHECK(expression,label)						\
 do {									\
-	if ((errno = expression) != 0) {				\
+	if ((errno = (expression)) != 0) {				\
 		res = -errno;						\
 		pw_log_error(#expression ": %s", strerror(errno));	\
 		goto label;						\
@@ -116,6 +116,17 @@ static int impl_get_rt_range(void *object, const struct spa_dict *props,
 		*max = sched_get_priority_max(SCHED_OTHER);
 	return 0;
 }
+static int impl_acquire_rt(void *object, struct spa_thread *thread, int priority)
+{
+	pw_log_warn("acquire_rt thread:%p prio:%d not implemented", thread, priority);
+	return -ENOTSUP;
+}
+
+static int impl_drop_rt(void *object, struct spa_thread *thread)
+{
+	pw_log_warn("drop_rt thread:%p not implemented", thread);
+	return -ENOTSUP;
+}
 
 static struct {
 	struct spa_thread_utils utils;
@@ -128,7 +139,9 @@ static struct {
 	{ SPA_VERSION_THREAD_UTILS_METHODS,
 		.create = impl_create,
 		.join = impl_join,
-		.get_rt_range = impl_get_rt_range
+		.get_rt_range = impl_get_rt_range,
+		.acquire_rt = impl_acquire_rt,
+		.drop_rt = impl_drop_rt,
 	}
 };
 
diff --git a/src/tools/pw-cat.c b/src/tools/pw-cat.c
index ea502d6473069ec02241c154c557ec1fc02d5186..ab43bd12e25e7c586f55defd6b5f11285c93c6ff 100644
--- a/src/tools/pw-cat.c
+++ b/src/tools/pw-cat.c
@@ -174,20 +174,18 @@ static const struct format_info {
 
 static const 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];
+	SPA_FOR_EACH_ELEMENT_VAR(format_info, i)
+		if (spa_streq(str, i->name))
+			return i;
 	return NULL;
 }
 
 static const struct format_info *format_info_by_sf_format(int format)
 {
-	uint32_t i;
 	int sub_type = (format & SF_FORMAT_SUBMASK);
-	for (i = 0; i < SPA_N_ELEMENTS(format_info); i++)
-		if (format_info[i].sf_format == sub_type)
-			return &format_info[i];
+	SPA_FOR_EACH_ELEMENT_VAR(format_info, i)
+		if (i->sf_format == sub_type)
+			return i;
 	return NULL;
 }
 
@@ -431,10 +429,10 @@ static int parse_channelmap(const char *channel_map, struct channelmap *map)
 	int i, nch;
 	char **ch;
 
-	for (i = 0; i < (int) SPA_N_ELEMENTS(maps); i++) {
-		if (spa_streq(maps[i].name, channel_map)) {
-			map->n_channels = maps[i].channels;
-			spa_memcpy(map->channels, &maps[i].values,
+	SPA_FOR_EACH_ELEMENT_VAR(maps, m) {
+		if (spa_streq(m->name, channel_map)) {
+			map->n_channels = m->channels;
+			spa_memcpy(map->channels, &m->values,
 					map->n_channels * sizeof(unsigned int));
 			return 0;
 		}
@@ -1589,17 +1587,16 @@ int main(int argc, char *argv[])
 	case TYPE_DSD:
 	{
 		struct spa_audio_info_dsd info;
-		size_t i;
 
 		spa_zero(info);
 		info.channels = data.dsf.info.channels;
 		info.rate = data.dsf.info.rate / 8;
 
-		for (i = 0; i < SPA_N_ELEMENTS(dsd_layouts); i++) {
-			if (dsd_layouts[i].type != data.dsf.info.channel_type)
+		SPA_FOR_EACH_ELEMENT_VAR(dsd_layouts, i) {
+			if (i->type != data.dsf.info.channel_type)
 				continue;
-			info.channels = dsd_layouts[i].info.n_channels;
-			memcpy(info.position, dsd_layouts[i].info.position,
+			info.channels = i->info.n_channels;
+			memcpy(info.position, i->info.position,
 					info.channels * sizeof(uint32_t));
 		}
 		params[0] = spa_format_audio_dsd_build(&b, SPA_PARAM_EnumFormat, &info);
diff --git a/src/tools/pw-cli.c b/src/tools/pw-cli.c
index 4d6655b062b37249d9951aed04db8a27973db6d8..bb5ab99cb521fa6ad2fc5af668ffc39424f4184a 100644
--- a/src/tools/pw-cli.c
+++ b/src/tools/pw-cli.c
@@ -262,14 +262,11 @@ static bool do_quit(struct data *data, const char *cmd, char *args, char **error
 
 static bool do_help(struct data *data, const char *cmd, char *args, char **error)
 {
-	size_t i;
-
 	printf("Available commands:\n");
-	for (i = 0; i < SPA_N_ELEMENTS(command_list); i++) {
+	SPA_FOR_EACH_ELEMENT_VAR(command_list, c) {
 		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);
+		snprintf(cmd, sizeof(cmd), "%s | %s", c->name, c->alias);
+		printf("\t%-20.20s\t%s\n", cmd, c->description);
 	}
 	return true;
 }
@@ -1312,11 +1309,10 @@ static const struct class *classes[] =
 
 static const struct class *find_class(const char *type, uint32_t version)
 {
-	size_t i;
-	for (i = 0; i < SPA_N_ELEMENTS(classes); i++) {
-		if (spa_streq(classes[i]->type, type) &&
-		    classes[i]->version <= version)
-			return classes[i];
+	SPA_FOR_EACH_ELEMENT_VAR(classes, c) {
+		if (spa_streq((*c)->type, type) &&
+		    (*c)->version <= version)
+			return *c;
 	}
 	return NULL;
 }
@@ -2110,7 +2106,6 @@ static bool parse(struct data *data, char *buf, char **error)
 {
 	char *a[2];
 	int n;
-	size_t i;
 	char *p, *cmd, *args;
 
 	if ((p = strchr(buf, '#')))
@@ -2128,10 +2123,10 @@ static bool parse(struct data *data, char *buf, char **error)
 	cmd = a[0];
 	args = n > 1 ? a[1] : "";
 
-	for (i = 0; i < SPA_N_ELEMENTS(command_list); i++) {
-		if (spa_streq(command_list[i].name, cmd) ||
-		    spa_streq(command_list[i].alias, cmd)) {
-			return command_list[i].func(data, cmd, args, error);
+	SPA_FOR_EACH_ELEMENT_VAR(command_list, c) {
+		if (spa_streq(c->name, cmd) ||
+		    spa_streq(c->alias, cmd)) {
+			return c->func(data, cmd, args, error);
 		}
 	}
         *error = spa_aprintf("Command \"%s\" does not exist. Type 'help' for usage.", cmd);
@@ -2231,13 +2226,13 @@ readline_command_completion(const char *text, int start, int end)
 	return matches;
 }
 
-static void readline_init()
+static void readline_init(void)
 {
 	rl_attempted_completion_function = readline_command_completion;
 	rl_callback_handler_install(">> ", input_process_line);
 }
 
-static void readline_cleanup()
+static void readline_cleanup(void)
 {
 	rl_callback_handler_remove();
 }
@@ -2374,8 +2369,8 @@ int main(int argc, char *argv[])
 			fprintf(stderr, "Error: \"%s\"\n", error);
 			free(error);
 		}
+		data.current->prompt_pending = pw_core_sync(data.current->core, 0, 0);
 		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;
diff --git a/src/tools/pw-dot.c b/src/tools/pw-dot.c
index 68abc3b95434356bbc3d2947e6cdcb7238785ed8..680bd248843e976c196871bc328a15dfa8919870 100644
--- a/src/tools/pw-dot.c
+++ b/src/tools/pw-dot.c
@@ -94,7 +94,7 @@ struct global {
 	struct spa_hook object_listener;
 };
 
-static char *dot_str_new()
+static char *dot_str_new(void)
 {
         return strdup("");
 }
@@ -514,7 +514,7 @@ static int draw_graph(struct data *d, const char *path)
 		fputs(d->dot_str, stdout);
 	} else {
 		/* open the file */
-		fp = fopen(path, "w");
+		fp = fopen(path, "we");
 		if (fp == NULL) {
 			printf("open error: could not open %s for writing\n", path);
 			return -1;
diff --git a/src/tools/pw-dump.c b/src/tools/pw-dump.c
index 8b8cc76f1e3d9d9652f6242bfd033f846410a38d..b25e7863332392000f0dbed6fafa0fdb0a95983d 100644
--- a/src/tools/pw-dump.c
+++ b/src/tools/pw-dump.c
@@ -88,6 +88,7 @@ struct data {
 
 struct param {
 	uint32_t id;
+	int32_t seq;
 	struct spa_list link;
 	struct spa_pod *param;
 };
@@ -116,6 +117,8 @@ struct object {
 
 	const struct class *class;
 	void *info;
+	struct spa_param_info *params;
+	uint32_t n_params;
 
 	int changed;
 	struct spa_list param_list;
@@ -148,7 +151,8 @@ static uint32_t clear_params(struct spa_list *param_list, uint32_t id)
 	return count;
 }
 
-static struct param *add_param(struct spa_list *params, uint32_t id, const struct spa_pod *param)
+static struct param *add_param(struct spa_list *params, int seq,
+		uint32_t id, const struct spa_pod *param)
 {
 	struct param *p;
 
@@ -165,6 +169,7 @@ static struct param *add_param(struct spa_list *params, uint32_t id, const struc
 		return NULL;
 
 	p->id = id;
+	p->seq = seq;
 	if (param != NULL) {
 		p->param = SPA_PTROFF(p, sizeof(*p), struct spa_pod);
 		memcpy(p->param, param, SPA_POD_SIZE(param));
@@ -187,17 +192,30 @@ static struct object *find_object(struct data *d, uint32_t id)
 	return NULL;
 }
 
-static void object_update_params(struct object *o)
+static void object_update_params(struct spa_list *param_list, struct spa_list *pending_list,
+		uint32_t n_params, struct spa_param_info *params)
 {
-	struct param *p;
+	struct param *p, *t;
+	uint32_t i;
+
+	for (i = 0; i < n_params; i++) {
+		spa_list_for_each_safe(p, t, pending_list, link) {
+			if (p->id == params[i].id &&
+			    p->seq != params[i].seq &&
+			    p->param != NULL) {
+				spa_list_remove(&p->link);
+				free(p);
+			}
+		}
+	}
 
-	spa_list_consume(p, &o->pending_list, link) {
+	spa_list_consume(p, pending_list, link) {
 		spa_list_remove(&p->link);
 		if (p->param == NULL) {
-			clear_params(&o->param_list, p->id);
+			clear_params(param_list, p->id);
 			free(p);
 		} else {
-			spa_list_append(&o->param_list, &p->link);
+			spa_list_append(param_list, &p->link);
 		}
 	}
 }
@@ -587,9 +605,11 @@ static void client_event_info(void *data, const struct pw_client_info *info)
 	struct object *o = data;
 	int changed = 0;
 
-        pw_log_debug("object %p: id:%d change-mask:%08"PRIx64, o, o->id, info->change_mask);
+	pw_log_debug("object %p: id:%d change-mask:%08"PRIx64, o, o->id, info->change_mask);
 
-        info = o->info = pw_client_info_update(o->info, info);
+	info = o->info = pw_client_info_update(o->info, info);
+	if (info == NULL)
+		return;
 
 	if (info->change_mask & PW_CLIENT_CHANGE_MASK_PROPS)
 		changed++;
@@ -644,12 +664,14 @@ static void module_dump(struct object *o)
 
 static void module_event_info(void *data, const struct pw_module_info *info)
 {
-        struct object *o = data;
+	struct object *o = data;
 	int changed = 0;
 
-        pw_log_debug("object %p: id:%d change-mask:%08"PRIx64, o, o->id, info->change_mask);
+	pw_log_debug("object %p: id:%d change-mask:%08"PRIx64, o, o->id, info->change_mask);
 
-        info = o->info = pw_module_info_update(o->info, info);
+	info = o->info = pw_module_info_update(o->info, info);
+	if (info == NULL)
+		return;
 
 	if (info->change_mask & PW_MODULE_CHANGE_MASK_PROPS)
 		changed++;
@@ -704,12 +726,14 @@ static void factory_dump(struct object *o)
 
 static void factory_event_info(void *data, const struct pw_factory_info *info)
 {
-        struct object *o = data;
+	struct object *o = data;
 	int changed = 0;
 
-        pw_log_debug("object %p: id:%d change-mask:%08"PRIx64, o, o->id, info->change_mask);
+	pw_log_debug("object %p: id:%d change-mask:%08"PRIx64, o, o->id, info->change_mask);
 
-        info = o->info = pw_factory_info_update(o->info, info);
+	info = o->info = pw_factory_info_update(o->info, info);
+	if (info == NULL)
+		return;
 
 	if (info->change_mask & PW_FACTORY_CHANGE_MASK_PROPS)
 		changed++;
@@ -765,10 +789,16 @@ static void device_event_info(void *data, const struct pw_device_info *info)
 {
 	struct object *o = data;
 	uint32_t i, changed = 0;
+	int res;
 
 	pw_log_debug("object %p: id:%d change-mask:%08"PRIx64, o, o->id, info->change_mask);
 
 	info = o->info = pw_device_info_update(o->info, info);
+	if (info == NULL)
+		return;
+
+	o->params = info->params;
+	o->n_params = info->n_params;
 
 	if (info->change_mask & PW_DEVICE_CHANGE_MASK_PROPS)
 		changed++;
@@ -782,12 +812,14 @@ static void device_event_info(void *data, const struct pw_device_info *info)
 			info->params[i].user = 0;
 
 			changed++;
-			clear_params(&o->pending_list, id);
+			add_param(&o->pending_list, 0, id, NULL);
 			if (!(info->params[i].flags & SPA_PARAM_INFO_READ))
 				continue;
 
-			pw_device_enum_params((struct pw_device*)o->proxy,
-					0, id, 0, -1, NULL);
+			res = pw_device_enum_params((struct pw_device*)o->proxy,
+					++info->params[i].seq, id, 0, -1, NULL);
+			if (SPA_RESULT_IS_ASYNC(res))
+				info->params[i].seq = res;
 		}
 	}
 	if (changed) {
@@ -801,7 +833,7 @@ static void device_event_param(void *data, int seq,
 		const struct spa_pod *param)
 {
 	struct object *o = data;
-	add_param(&o->pending_list, id, param);
+	add_param(&o->pending_list, seq, id, param);
 }
 
 static const struct pw_device_events device_events = {
@@ -859,10 +891,16 @@ static void node_event_info(void *data, const struct pw_node_info *info)
 {
 	struct object *o = data;
 	uint32_t i, changed = 0;
+	int res;
 
 	pw_log_debug("object %p: id:%d change-mask:%08"PRIx64, o, o->id, info->change_mask);
 
 	info = o->info = pw_node_info_update(o->info, info);
+	if (info == NULL)
+		return;
+
+	o->params = info->params;
+	o->n_params = info->n_params;
 
 	if (info->change_mask & PW_NODE_CHANGE_MASK_STATE)
 		changed++;
@@ -879,12 +917,14 @@ static void node_event_info(void *data, const struct pw_node_info *info)
 			info->params[i].user = 0;
 
 			changed++;
-			add_param(&o->pending_list, id, NULL);
+			add_param(&o->pending_list, 0, id, NULL);
 			if (!(info->params[i].flags & SPA_PARAM_INFO_READ))
 				continue;
 
-			pw_node_enum_params((struct pw_node*)o->proxy,
-					0, id, 0, -1, NULL);
+			res = pw_node_enum_params((struct pw_node*)o->proxy,
+					++info->params[i].seq, id, 0, -1, NULL);
+			if (SPA_RESULT_IS_ASYNC(res))
+				info->params[i].seq = res;
 		}
 	}
 	if (changed) {
@@ -898,7 +938,7 @@ static void node_event_param(void *data, int seq,
 		const struct spa_pod *param)
 {
 	struct object *o = data;
-	add_param(&o->pending_list, id, param);
+	add_param(&o->pending_list, seq, id, param);
 }
 
 static const struct pw_node_events node_events = {
@@ -948,10 +988,16 @@ static void port_event_info(void *data, const struct pw_port_info *info)
 {
 	struct object *o = data;
 	uint32_t i, changed = 0;
+	int res;
 
 	pw_log_debug("object %p: id:%d change-mask:%08"PRIx64, o, o->id, info->change_mask);
 
 	info = o->info = pw_port_info_update(o->info, info);
+	if (info == NULL)
+		return;
+
+	o->params = info->params;
+	o->n_params = info->n_params;
 
 	if (info->change_mask & PW_PORT_CHANGE_MASK_PROPS)
 		changed++;
@@ -965,12 +1011,14 @@ static void port_event_info(void *data, const struct pw_port_info *info)
 			info->params[i].user = 0;
 
 			changed++;
-			add_param(&o->pending_list, id, NULL);
+			add_param(&o->pending_list, 0, id, NULL);
 			if (!(info->params[i].flags & SPA_PARAM_INFO_READ))
 				continue;
 
-			pw_port_enum_params((struct pw_port*)o->proxy,
-					0, id, 0, -1, NULL);
+			res = pw_port_enum_params((struct pw_port*)o->proxy,
+					++info->params[i].seq, id, 0, -1, NULL);
+			if (SPA_RESULT_IS_ASYNC(res))
+				info->params[i].seq = res;
 		}
 	}
 	if (changed) {
@@ -984,7 +1032,7 @@ static void port_event_param(void *data, int seq,
 		const struct spa_pod *param)
 {
 	struct object *o = data;
-	add_param(&o->pending_list, id, param);
+	add_param(&o->pending_list, seq, id, param);
 }
 
 static const struct pw_port_events port_events = {
@@ -1044,6 +1092,8 @@ static void link_event_info(void *data, const struct pw_link_info *info)
 	pw_log_debug("object %p: id:%d change-mask:%08"PRIx64, o, o->id, info->change_mask);
 
 	info = o->info = pw_link_info_update(o->info, info);
+	if (info == NULL)
+		return;
 
 	if (info->change_mask & PW_LINK_CHANGE_MASK_STATE)
 		changed++;
@@ -1241,11 +1291,10 @@ static const struct class *classes[] =
 
 static const struct class *find_class(const char *type, uint32_t version)
 {
-	size_t i;
-	for (i = 0; i < SPA_N_ELEMENTS(classes); i++) {
-		if (spa_streq(classes[i]->type, type) &&
-		    classes[i]->version <= version)
-			return classes[i];
+	SPA_FOR_EACH_ELEMENT_VAR(classes, c) {
+		if (spa_streq((*c)->type, type) &&
+		    (*c)->version <= version)
+			return *c;
 	}
 	return NULL;
 }
@@ -1455,7 +1504,8 @@ static void on_core_done(void *data, uint32_t id, int seq)
 		pw_log_debug("sync end %u/%u", d->sync_seq, seq);
 
 		spa_list_for_each(o, &d->object_list, link)
-			object_update_params(o);
+			object_update_params(&o->param_list, &o->pending_list,
+					o->n_params, o->params);
 
 		dump_objects(d);
 		if (!d->monitor)
diff --git a/src/tools/pw-link.c b/src/tools/pw-link.c
index 9e008aa60147ac201cfa223f6cd58f45afba2568..8696eb296c76380d74dd34255f3bb16576dfe1da 100644
--- a/src/tools/pw-link.c
+++ b/src/tools/pw-link.c
@@ -400,8 +400,16 @@ static int do_link_ports(struct data *data)
 			struct object *port_out = find_node_port(data, out_node, PW_DIRECTION_OUTPUT, port_id);
 			struct object *port_in = find_node_port(data, in_node, PW_DIRECTION_INPUT, port_id);
 
-			if (!port_out || !port_in)
-				break;
+			if (!port_out && !port_in) {
+				fprintf(stderr, "Input & output port do not exist\n");
+				goto no_port;
+			} else if (!port_in) {
+				fprintf(stderr, "Input port does not exist\n");
+				goto no_port;
+			} else if (!port_out) {
+				fprintf(stderr, "Output port does not exist\n");
+				goto no_port;
+			}
 
 			pw_properties_setf(data->props, PW_KEY_LINK_OUTPUT_PORT, "%u", port_out->id);
 			pw_properties_setf(data->props, PW_KEY_LINK_INPUT_PORT, "%u", port_in->id);
@@ -422,6 +430,9 @@ static int do_link_ports(struct data *data)
 	pw_properties_setf(data->props, PW_KEY_LINK_INPUT_PORT, "%u", in_port);
 
 	return create_link(data);
+
+no_port:
+	return -ENOENT;
 }
 
 static int do_unlink_ports(struct data *data)
diff --git a/src/tools/pw-loopback.c b/src/tools/pw-loopback.c
index 4f4fb963c344bf0f1da4d85b84ebddf66032124f..7ec93a1bb02478838cff62e704d92dd2ee2226be 100644
--- a/src/tools/pw-loopback.c
+++ b/src/tools/pw-loopback.c
@@ -57,6 +57,7 @@ struct data {
 
 	uint32_t channels;
 	uint32_t latency;
+	float delay;
 
 	struct pw_properties *capture_props;
 	struct pw_properties *playback_props;
@@ -93,6 +94,7 @@ static void show_help(struct data *data, const char *name, bool error)
 		"  -c, --channels                        Number of channels (default %d)\n"
 		"  -m, --channel-map                     Channel map (default '%s')\n"
 		"  -l, --latency                         Desired latency in ms\n"
+		"  -d, --delay                           Desired delay in float s\n"
 		"  -C  --capture                         Capture source to connect to\n"
 		"      --capture-props                   Capture stream properties\n"
 		"  -P  --playback                        Playback sink to connect to\n"
@@ -109,7 +111,7 @@ int main(int argc, char *argv[])
 	struct data data = { 0 };
 	struct pw_loop *l;
 	const char *opt_remote = NULL;
-	char cname[256];
+	char cname[256], value[256];
 	char *args;
 	size_t size;
 	FILE *f;
@@ -121,6 +123,7 @@ int main(int argc, char *argv[])
 		{ "name",		required_argument,	NULL, 'n' },
 		{ "channels",		required_argument,	NULL, 'c' },
 		{ "latency",		required_argument,	NULL, 'l' },
+		{ "delay",		required_argument,	NULL, 'd' },
 		{ "capture",		required_argument,	NULL, 'C' },
 		{ "playback",		required_argument,	NULL, 'P' },
 		{ "capture-props",	required_argument,	NULL, 'i' },
@@ -146,7 +149,7 @@ int main(int argc, char *argv[])
 		goto exit;
 	}
 
-	while ((c = getopt_long(argc, argv, "hVr:n:g:c:m:l:C:P:i:o:", long_options, NULL)) != -1) {
+	while ((c = getopt_long(argc, argv, "hVr:n:g:c:m:l:d:C:P:i:o:", long_options, NULL)) != -1) {
 		switch (c) {
 		case 'h':
 			show_help(&data, argv[0], false);
@@ -177,6 +180,9 @@ int main(int argc, char *argv[])
 		case 'l':
 			data.latency = atoi(optarg) * DEFAULT_RATE / SPA_MSEC_PER_SEC;
 			break;
+		case 'd':
+			data.delay = atof(optarg);
+			break;
 		case 'C':
 			pw_properties_set(data.capture_props, PW_KEY_NODE_TARGET, optarg);
 			break;
@@ -223,6 +229,9 @@ int main(int argc, char *argv[])
 		fprintf(f, " remote.name = \"%s\"", opt_remote);
 	if (data.latency != 0)
 		fprintf(f, " node.latency = %u/%u", data.latency, DEFAULT_RATE);
+	if (data.delay != 0.0f)
+		fprintf(f, " target.delay.sec = %s",
+				spa_json_format_float(value, sizeof(value), data.delay));
 	if (data.channels != 0)
 		fprintf(f, " audio.channels = %u", data.channels);
 	if (data.opt_channel_map != NULL)
diff --git a/src/tools/pw-profiler.c b/src/tools/pw-profiler.c
index f2000a9860c8342d1cc83f379881125acbe019db..00dc4104a0c2464a3f718341eeed29a34584fa1f 100644
--- a/src/tools/pw-profiler.c
+++ b/src/tools/pw-profiler.c
@@ -262,7 +262,7 @@ static void dump_scripts(struct data *d)
 
 	printf("\ndumping scripts for %d followers\n", d->n_followers);
 
-	out = fopen("Timing1.plot", "w");
+	out = fopen("Timing1.plot", "we");
 	if (out == NULL) {
 		pw_log_error("Can't open Timing1.plot: %m");
 	} else {
@@ -282,7 +282,7 @@ static void dump_scripts(struct data *d)
 		fclose(out);
 	}
 
-	out = fopen("Timing2.plot", "w");
+	out = fopen("Timing2.plot", "we");
 	if (out == NULL) {
 		pw_log_error("Can't open Timing2.plot: %m");
 	} else {
@@ -298,7 +298,7 @@ static void dump_scripts(struct data *d)
 		fclose(out);
 	}
 
-	out = fopen("Timing3.plot", "w");
+	out = fopen("Timing3.plot", "we");
 	if (out == NULL) {
 		pw_log_error("Can't open Timing3.plot: %m");
 	} else {
@@ -328,7 +328,7 @@ static void dump_scripts(struct data *d)
 		fclose(out);
 	}
 
-	out = fopen("Timing4.plot", "w");
+	out = fopen("Timing4.plot", "we");
 	if (out == NULL) {
 		pw_log_error("Can't open Timing4.plot: %m");
 	} else {
@@ -355,7 +355,7 @@ static void dump_scripts(struct data *d)
 		fclose(out);
 	}
 
-	out = fopen("Timing5.plot", "w");
+	out = fopen("Timing5.plot", "we");
 	if (out == NULL) {
 		pw_log_error("Can't open Timing5.plot: %m");
 	} else {
@@ -381,7 +381,7 @@ static void dump_scripts(struct data *d)
 			"unset output\n");
 		fclose(out);
 	}
-	out = fopen("Timings.html", "w");
+	out = fopen("Timings.html", "we");
 	if (out == NULL) {
 		pw_log_error("Can't open Timings.html: %m");
 	} else {
@@ -409,7 +409,7 @@ static void dump_scripts(struct data *d)
 		fclose(out);
 	}
 
-	out = fopen("generate_timings.sh", "w");
+	out = fopen("generate_timings.sh", "we");
 	if (out == NULL) {
 		pw_log_error("Can't open generate_timings.sh: %m");
 	} else {
@@ -624,7 +624,7 @@ int main(int argc, char *argv[])
 
 	data.filename = opt_output;
 
-	data.output = fopen(data.filename, "w");
+	data.output = fopen(data.filename, "we");
 	if (data.output == NULL) {
 		fprintf(stderr, "Can't open file %s: %m\n", data.filename);
 		return -1;
diff --git a/src/tools/pw-top.c b/src/tools/pw-top.c
index 459dacb11fcf5e774d25b64485b8cfad7713cbaa..61a05c5958f0ace96a628efc696c9ff3e240171e 100644
--- a/src/tools/pw-top.c
+++ b/src/tools/pw-top.c
@@ -62,8 +62,10 @@ struct measurement {
 
 struct node {
 	struct spa_list link;
+	struct data *data;
 	uint32_t id;
 	char name[MAX_NAME+1];
+	enum pw_node_state state;
 	struct measurement measurement;
 	struct driver info;
 	struct node *driver;
@@ -73,6 +75,7 @@ struct node {
 	char format[MAX_FORMAT+1];
 	struct pw_proxy *proxy;
 	struct spa_hook proxy_listener;
+	unsigned int inactive:1;
 	struct spa_hook object_listener;
 };
 
@@ -95,6 +98,7 @@ struct data {
 	int n_nodes;
 	struct spa_list node_list;
 	uint32_t generation;
+	unsigned pending_refresh:1;
 
 	WINDOW *win;
 };
@@ -159,12 +163,29 @@ static const struct pw_proxy_events proxy_events = {
 	.destroy = on_node_destroy,
 };
 
+static void do_refresh(struct data *d);
+
+static void node_info(void *data, const struct pw_node_info *info)
+{
+	struct node *n = data;
+
+	if (n->state != info->state) {
+		n->state = info->state;
+		do_refresh(n->data);
+	}
+}
+
 static void node_param(void *data, int seq,
 			uint32_t id, uint32_t index, uint32_t next,
 			const struct spa_pod *param)
 {
 	struct node *n = data;
 
+	if (param == NULL) {
+		spa_zero(n->format);
+		goto done;
+	}
+
 	switch (id) {
 	case SPA_PARAM_Format:
 	{
@@ -177,17 +198,18 @@ static void node_param(void *data, int seq,
 			switch(media_subtype) {
 			case SPA_MEDIA_SUBTYPE_raw:
 			{
-				struct spa_audio_info_raw info;
+				struct spa_audio_info_raw info = { 0 };
 				if (spa_format_audio_raw_parse(param, &info) >= 0) {
 					snprintf(n->format, sizeof(n->format), "%6.6s %d %d",
-						spa_debug_type_find_short_name(spa_type_audio_format, info.format),
+						spa_debug_type_find_short_name(
+							spa_type_audio_format, info.format),
 						info.channels, info.rate);
 				}
 				break;
 			}
 			case SPA_MEDIA_SUBTYPE_dsd:
 			{
-				struct spa_audio_info_dsd info;
+				struct spa_audio_info_dsd info = { 0 };
 				if (spa_format_audio_dsd_parse(param, &info) >= 0) {
 					snprintf(n->format, sizeof(n->format), "DSD%d %d ",
 						8 * info.rate / 44100, info.channels);
@@ -195,13 +217,25 @@ static void node_param(void *data, int seq,
 				}
 				break;
 			}
+			case SPA_MEDIA_SUBTYPE_iec958:
+			{
+				struct spa_audio_info_iec958 info = { 0 };
+				if (spa_format_audio_iec958_parse(param, &info) >= 0) {
+					snprintf(n->format, sizeof(n->format), "IEC958 %s %d",
+						spa_debug_type_find_short_name(
+							spa_type_audio_iec958_codec, info.codec),
+						info.rate);
+
+				}
+				break;
+			}
 			}
 			break;
 		case SPA_MEDIA_TYPE_video:
 			switch(media_subtype) {
 			case SPA_MEDIA_SUBTYPE_raw:
 			{
-				struct spa_video_info_raw info;
+				struct spa_video_info_raw info = { 0 };
 				if (spa_format_video_raw_parse(param, &info) >= 0) {
 					snprintf(n->format, sizeof(n->format), "%6.6s %dx%d",
 						spa_debug_type_find_short_name(spa_type_video_format, info.format),
@@ -209,6 +243,24 @@ static void node_param(void *data, int seq,
 				}
 				break;
 			}
+			case SPA_MEDIA_SUBTYPE_mjpg:
+			{
+				struct spa_video_info_mjpg info = { 0 };
+				if (spa_format_video_mjpg_parse(param, &info) >= 0) {
+					snprintf(n->format, sizeof(n->format), "MJPG %dx%d",
+						info.size.width, info.size.height);
+				}
+				break;
+			}
+			case SPA_MEDIA_SUBTYPE_h264:
+			{
+				struct spa_video_info_h264 info = { 0 };
+				if (spa_format_video_h264_parse(param, &info) >= 0) {
+					snprintf(n->format, sizeof(n->format), "H264 %dx%d",
+						info.size.width, info.size.height);
+				}
+				break;
+			}
 			}
 			break;
 		case SPA_MEDIA_TYPE_application:
@@ -224,10 +276,13 @@ static void node_param(void *data, int seq,
 	default:
 		break;
 	}
+done:
+	do_refresh(n->data);
 }
 
 static const struct pw_node_events node_events = {
 	PW_VERSION_NODE,
+	.info = node_info,
 	.param = node_param,
 };
 
@@ -242,6 +297,7 @@ static struct node *add_node(struct data *d, uint32_t id, const char *name)
 		strncpy(n->name, name, MAX_NAME);
 	else
 		snprintf(n->name, sizeof(n->name), "%u", id);
+	n->data = d;
 	n->id = id;
 	n->driver = n;
 	n->proxy = pw_registry_bind(d->registry, id, PW_TYPE_INTERFACE_Node, PW_VERSION_NODE, 0);
@@ -258,6 +314,7 @@ static struct node *add_node(struct data *d, uint32_t id, const char *name)
 	}
 	spa_list_append(&d->node_list, &n->link);
 	d->n_nodes++;
+	d->pending_refresh = true;
 
 	return n;
 }
@@ -268,6 +325,7 @@ static void remove_node(struct data *d, struct node *n)
 		pw_proxy_destroy(n->proxy);
 	spa_list_remove(&n->link);
 	d->n_nodes--;
+	d->pending_refresh = true;
 	free(n);
 }
 
@@ -332,7 +390,10 @@ static int process_follower_block(struct data *d, const struct spa_pod *pod, str
 		return -ENOENT;
 
 	n->measurement = m;
-	n->driver = point->driver;
+	if (n->driver != point->driver) {
+		n->driver = point->driver;
+		d->pending_refresh = true;
+	}
 	n->generation = d->generation;
 	if (m.status != 3) {
 		n->errors++;
@@ -342,9 +403,9 @@ static int process_follower_block(struct data *d, const struct spa_pod *pod, str
 	return 0;
 }
 
-static const char *print_time(char *buf, size_t len, uint64_t val)
+static const char *print_time(char *buf, bool active, size_t len, uint64_t val)
 {
-	if (val == (uint64_t)-1)
+	if (val == (uint64_t)-1 || !active)
 		snprintf(buf, len, "   --- ");
 	else if (val == (uint64_t)-2)
 		snprintf(buf, len, "   +++ ");
@@ -357,9 +418,9 @@ static const char *print_time(char *buf, size_t len, uint64_t val)
 	return buf;
 }
 
-static const char *print_perc(char *buf, size_t len, uint64_t val, float quantum)
+static const char *print_perc(char *buf, bool active, size_t len, uint64_t val, float quantum)
 {
-	if (val == (uint64_t)-1) {
+	if (val == (uint64_t)-1 || !active) {
 		snprintf(buf, len, " --- ");
 	} else if (val == (uint64_t)-2) {
 		snprintf(buf, len, " +++ ");
@@ -370,6 +431,23 @@ static const char *print_perc(char *buf, size_t len, uint64_t val, float quantum
 	return buf;
 }
 
+static const char *state_as_string(enum pw_node_state state)
+{
+	switch (state) {
+	case PW_NODE_STATE_ERROR:
+		return "E";
+	case PW_NODE_STATE_CREATING:
+		return "C";
+	case PW_NODE_STATE_SUSPENDED:
+		return "S";
+	case PW_NODE_STATE_IDLE:
+		return "I";
+	case PW_NODE_STATE_RUNNING:
+		return "R";
+	}
+	return "!";
+}
+
 static void print_node(struct data *d, struct driver *i, struct node *n, int y)
 {
 	char buf1[64];
@@ -379,8 +457,13 @@ static void print_node(struct data *d, struct driver *i, struct node *n, int y)
 	uint64_t waiting, busy;
 	float quantum;
 	struct spa_fraction frac;
+	bool active;
+
+	active = n->state == PW_NODE_STATE_RUNNING || n->state == PW_NODE_STATE_IDLE;
 
-	if (n->driver == n)
+	if (!active)
+		frac = SPA_FRACTION(0, 0);
+	else if (n->driver == n)
 		frac = SPA_FRACTION((uint32_t)(i->clock.duration * i->clock.rate.num), i->clock.rate.denom);
 	else
 		frac = SPA_FRACTION(n->measurement.latency.num, n->measurement.latency.denom);
@@ -405,19 +488,28 @@ static void print_node(struct data *d, struct driver *i, struct node *n, int y)
 		busy = -1;
 
 	mvwprintw(d->win, y, 0, "%s %4.1u %6.1u %6.1u %s %s %s %s  %3.1u %16.16s %s%s",
-			n->measurement.status != 3 ? "!" : " ",
+			state_as_string(n->state),
 			n->id,
 			frac.num, frac.denom,
-			print_time(buf1, 64, waiting),
-			print_time(buf2, 64, busy),
-			print_perc(buf3, 64, waiting, quantum),
-			print_perc(buf4, 64, busy, quantum),
+			print_time(buf1, active, 64, waiting),
+			print_time(buf2, active, 64, busy),
+			print_perc(buf3, active, 64, waiting, quantum),
+			print_perc(buf4, active, 64, busy, quantum),
 			i->xrun_count + n->errors,
-			n->measurement.status != 3 ? "" : n->format,
+			active ? n->format : "",
 			n->driver == n ? "" : " + ",
 			n->name);
 }
 
+static void clear_node(struct node *n)
+{
+	n->driver = n;
+	spa_zero(n->measurement);
+	spa_zero(n->info);
+	n->errors = 0;
+	n->last_error_status = 0;
+}
+
 static void do_refresh(struct data *d)
 {
 	struct node *n, *t, *f;
@@ -438,14 +530,8 @@ static void do_refresh(struct data *d)
 			break;
 
 		spa_list_for_each(f, &d->node_list, link) {
-			if (d->generation > f->generation + 2) {
-				f->driver = f;
-				spa_zero(f->measurement);
-				spa_zero(f->info);
-				spa_zero(f->format);
-				f->errors = 0;
-				f->last_error_status = 0;
-			}
+			if (d->generation > f->generation + 22)
+				clear_node(f);
 
 			if (f->driver != n || f == n)
 				continue;
@@ -462,6 +548,7 @@ static void do_refresh(struct data *d)
 	wclrtobot(d->win);
 
 	wrefresh(d->win);
+	d->pending_refresh = false;
 }
 
 static void do_timeout(void *data, uint64_t expirations)
@@ -507,6 +594,8 @@ static void profiler_profile(void *data, const struct spa_pod *pod)
 		if (res < 0)
 			continue;
 	}
+	if (d->pending_refresh)
+		do_refresh(d);
 }
 
 static const struct pw_profiler_events profiler_events = {
@@ -545,7 +634,8 @@ static void registry_event_global(void *data, uint32_t id,
 		d->profiler = proxy;
 		pw_proxy_add_object_listener(proxy, &d->profiler_listener, &profiler_events, d);
 	}
-
+	if (d->pending_refresh)
+		do_refresh(d);
 	return;
 
 error_proxy:
@@ -559,6 +649,8 @@ static void registry_event_global_remove(void *data, uint32_t id)
 	struct node *n;
 	if ((n = find_node(d, id)) != NULL)
 		remove_node(d, n);
+	if (d->pending_refresh)
+		do_refresh(d);
 }
 
 static const struct pw_registry_events registry_events = {
@@ -586,8 +678,9 @@ static void on_core_done(void *_data, uint32_t id, int seq)
 		if (d->profiler == NULL) {
 			pw_log_error("no Profiler Interface found, please load one in the server");
 			pw_main_loop_quit(d->loop);
-		} else
+		} else {
 			do_refresh(d);
+		}
 	}
 }
 
@@ -612,7 +705,7 @@ static void show_help(const char *name, bool error)
 		name);
 }
 
-static void terminal_start()
+static void terminal_start(void)
 {
 	initscr();
 	cbreak();
@@ -620,7 +713,7 @@ static void terminal_start()
 	refresh();
 }
 
-static void terminal_stop()
+static void terminal_stop(void)
 {
 	endwin();
 }
diff --git a/test/pwtest.c b/test/pwtest.c
index 18be5c6ad5a38679eb2965fbce6d045443b1395e..968805b5e6ac4682b860f95143808f0f33f1bdcd 100644
--- a/test/pwtest.c
+++ b/test/pwtest.c
@@ -358,16 +358,13 @@ pwtest_spa_plugin_new(void)
 void
 pwtest_spa_plugin_destroy(struct pwtest_spa_plugin *plugin)
 {
-	void **dll;
-	struct spa_handle **hnd;
-
-	SPA_FOR_EACH_ELEMENT(plugin->handles, hnd) {
+	SPA_FOR_EACH_ELEMENT_VAR(plugin->handles, hnd) {
 		if (*hnd) {
 			spa_handle_clear(*hnd);
 			free(*hnd);
 		}
 	}
-	SPA_FOR_EACH_ELEMENT(plugin->dlls, dll) {
+	SPA_FOR_EACH_ELEMENT_VAR(plugin->dlls, dll) {
 		if (*dll)
 			dlclose(*dll);
 	}
@@ -835,7 +832,7 @@ static int init_pipes(int read_fds[_FD_LAST], int write_fds[_FD_LAST])
 #ifdef __linux__
 	{
 		FILE *f;
-		f = fopen("/proc/sys/fs/pipe-max-size", "r");
+		f = fopen("/proc/sys/fs/pipe-max-size", "re");
 		if (f) {
 			if (fscanf(f, "%d", &r) == 1)
 				pipe_max_size = SPA_MIN(r, pipe_max_size);
@@ -1244,7 +1241,7 @@ static char* make_xdg_runtime_dir(void)
 	/* Marker file to avoid removing a random directory during cleanup */
 	r = spa_scnprintf(path, sizeof(path), "%s/pwtest.dir", dir);
 	spa_assert_se((size_t)r == strlen(dir) + 11);
-	fp = fopen(path, "w");
+	fp = fopen(path, "we");
 	spa_assert_se(fp);
 	fprintf(fp, "pwtest\n");
 	fclose(fp);
diff --git a/test/test-config.c b/test/test-config.c
index d1397bafbc66d9c5a1a8c676a2f854fed2adfa31..9ebbc26d17225470446ffcfc32074f81e24bfce6 100644
--- a/test/test-config.c
+++ b/test/test-config.c
@@ -35,7 +35,7 @@ PWTEST(config_load_abspath)
 	char *basename;
 
 	pwtest_mkstemp(path);
-	fp = fopen(path, "w");
+	fp = fopen(path, "we");
 	fputs("data = x", fp);
 	fclose(fp);
 
diff --git a/test/test-functional.c b/test/test-functional.c
index 620ce37322862053dff7f5661a94dc6a6f9ad2f6..ae837b41ccb400cf4209740ebca5ac56b15fce44 100644
--- a/test/test-functional.c
+++ b/test/test-functional.c
@@ -29,10 +29,8 @@
 
 PWTEST(openal_info_test)
 {
-	int status;
-
 #ifdef OPENAL_INFO_PATH
-	status = pwtest_spawn(OPENAL_INFO_PATH, (char *[]){ "openal-info", NULL });
+	int status = pwtest_spawn(OPENAL_INFO_PATH, (char *[]){ "openal-info", NULL });
 	pwtest_int_eq(WEXITSTATUS(status), 0);
 	return PWTEST_PASS;
 #else
@@ -42,10 +40,8 @@ PWTEST(openal_info_test)
 
 PWTEST(pactl_test)
 {
-	int status;
-
 #ifdef PACTL_PATH
-	status = pwtest_spawn(PACTL_PATH, (char *[]){ "pactl", "info", NULL });
+	int status = pwtest_spawn(PACTL_PATH, (char *[]){ "pactl", "info", NULL });
 	pwtest_int_eq(WEXITSTATUS(status), 0);
 	return PWTEST_PASS;
 #else
diff --git a/test/test-logger.c b/test/test-logger.c
index a1b0ff01dd466c0e35be379262c982016a14927d..5aca7d1998448472ebc274bfa473517069601136 100644
--- a/test/test-logger.c
+++ b/test/test-logger.c
@@ -63,7 +63,7 @@ PWTEST(logger_truncate_long_lines)
 	/* Print a line expected to be truncated */
 	spa_log_error(iface, "MARK: %1100s", "foo");
 
-	fp = fopen(fname, "r");
+	fp = fopen(fname, "re");
 	while (fgets(buffer, sizeof(buffer), fp) != NULL) {
 		if (strstr(buffer, "MARK:")) {
 			const char *suffix = ".. (truncated)\n";
@@ -110,7 +110,7 @@ PWTEST(logger_no_ansi)
 	 * tty so expect none despite colors being enabled */
 	spa_log_error(iface, "MARK\n");
 
-	fp = fopen(fname, "r");
+	fp = fopen(fname, "re");
 	while (fgets(buffer, sizeof(buffer), fp) != NULL) {
 		if (strstr(buffer, "MARK")) {
 			mark_line_found = true;
@@ -157,7 +157,7 @@ test_log_levels(enum spa_log_level level)
 	if (level < SPA_LOG_LEVEL_TRACE)
 		pw_log(level + 1, "ABOVE");
 
-	fp = fopen(fname, "r");
+	fp = fopen(fname, "re");
 	while (fgets(buffer, sizeof(buffer), fp) != NULL) {
 		if (strstr(buffer, "CURRENT"))
 			current_level_found = true;
@@ -427,7 +427,7 @@ PWTEST(logger_topics)
 
 	spa_logt_info(iface, &topic, "MARK\n");
 
-	fp = fopen(fname, "r");
+	fp = fopen(fname, "re");
 	while (fgets(buffer, sizeof(buffer), fp) != NULL) {
 		if (strstr(buffer, "MARK")) {
 			mark_line_found = true;
@@ -602,7 +602,7 @@ PWTEST(logger_journal_chain)
 	/* Now check that the line is in the chained file logger too */
 	spa_memzero(buffer, sizeof(buffer));
 	mark_line_found = false;
-	fp = fopen(fname, "r");
+	fp = fopen(fname, "re");
 	while (fgets(buffer, sizeof(buffer), fp) != NULL) {
 		if (strstr(buffer, token)) {
 			mark_line_found = true;
diff --git a/test/test-properties.c b/test/test-properties.c
index 0a4d38754ba07204238ccfcdd96b6a58afc8d4cd..89e66bb370a7fe70e151948fabba6dc07f82b5bd 100644
--- a/test/test-properties.c
+++ b/test/test-properties.c
@@ -484,7 +484,7 @@ PWTEST(properties_serialize_dict_stack_overflow)
 	dict = SPA_DICT_INIT(items, 2);
 
 	pwtest_mkstemp(tmpfile);
-	fp = fopen(tmpfile, "w");
+	fp = fopen(tmpfile, "we");
 	pwtest_ptr_notnull(fp);
 	r = pw_properties_serialize_dict(fp, &dict, 0);
 	pwtest_int_eq(r, 1);
diff --git a/test/test-spa-pod.c b/test/test-spa-pod.c
index 989d3dc5dd9dab3606a9aef7632a6b2a2bfbf705..24c3303cb07b78ed623e5b3a23192b2daf5df5c1 100644
--- a/test/test-spa-pod.c
+++ b/test/test-spa-pod.c
@@ -1639,6 +1639,54 @@ PWTEST(pod_overflow)
 	return PWTEST_PASS;
 }
 
+static int handle_overflow(void *data, uint32_t size)
+{
+	uint32_t *d = data;
+	(*d)++;
+	return -ENOSPC;
+}
+
+static struct spa_pod_builder_callbacks overflow_cb = {
+	SPA_VERSION_POD_BUILDER_CALLBACKS,
+	.overflow = handle_overflow
+};
+
+PWTEST(pod_overflow2)
+{
+	uint8_t buffer[1024];
+	struct spa_pod_builder b = { 0 };
+	struct spa_pod_builder_state state;
+	struct spa_pod_frame f[2];
+	uint32_t idx, overflow_count = 0;
+	struct spa_pod *pod;
+
+	spa_pod_builder_init(&b, buffer, sizeof(buffer));
+	spa_pod_builder_set_callbacks(&b, &overflow_cb, &overflow_count);
+
+	spa_pod_builder_push_object(&b, &f[0], SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo);
+	spa_pod_builder_add(&b,
+			SPA_PROP_INFO_id,    SPA_POD_Id(32567359),
+			SPA_PROP_INFO_type,  SPA_POD_CHOICE_ENUM_Int(1, 0),
+			SPA_PROP_INFO_description,  SPA_POD_String("DV Timings"),
+			0);
+
+	spa_pod_builder_get_state(&b, &state),
+
+	spa_pod_builder_prop(&b, SPA_PROP_INFO_labels, 0);
+	spa_pod_builder_push_struct(&b, &f[1]);
+
+	for (idx = 0; idx < 512; idx++) {
+		spa_pod_builder_int(&b, idx);
+		spa_pod_builder_string(&b, "foo");
+	}
+	spa_assert_se(b.state.offset > sizeof(buffer));
+	pod = spa_pod_builder_pop(&b, &f[1]);
+	spa_assert_se(pod == NULL);
+	spa_assert_se(overflow_count == 1);
+
+	return PWTEST_PASS;
+}
+
 PWTEST_SUITE(spa_pod)
 {
 	pwtest_add(pod_abi_sizes, PWTEST_NOARG);
@@ -1652,6 +1700,7 @@ PWTEST_SUITE(spa_pod)
 	pwtest_add(pod_parser2, PWTEST_NOARG);
 	pwtest_add(pod_static, PWTEST_NOARG);
 	pwtest_add(pod_overflow, PWTEST_NOARG);
+	pwtest_add(pod_overflow2, PWTEST_NOARG);
 
 	return PWTEST_PASS;
 }
diff --git a/test/test-spa-utils.c b/test/test-spa-utils.c
index 2f198f90a7c2b898c1df8ad95ff5a4c2f0c37cec..4184f435b793c8039aef981b7e643b8e5c376a0a 100644
--- a/test/test-spa-utils.c
+++ b/test/test-spa-utils.c
@@ -184,9 +184,8 @@ PWTEST(utils_macros)
 
 #define check_traversal(array_) \
 	{ \
-		__typeof__(array_[0]) *it; \
 		int count = 0; \
-		SPA_FOR_EACH_ELEMENT(array_, it) \
+		SPA_FOR_EACH_ELEMENT_VAR(array_, it) \
 			*it = count++; \
 		for (size_t i = 0; i < SPA_N_ELEMENTS(array_); i++) \
 			pwtest_int_eq(array_[i], i); \
diff --git a/test/test-utils.c b/test/test-utils.c
index 3ceb2d25afacd806246ebbbf40006adfb28f84fd..9b7c520e244679b43945c8297722f68dd922f011 100644
--- a/test/test-utils.c
+++ b/test/test-utils.c
@@ -172,9 +172,7 @@ static void test__pw_split_walk(void)
 		},
 	};
 
-	const struct test_case *tc;
-
-	SPA_FOR_EACH_ELEMENT(test_cases, tc) {
+	SPA_FOR_EACH_ELEMENT_VAR(test_cases, tc) {
 		const char *str = tc->input, *s;
 		const char *state = NULL;
 		size_t j = 0, len;