From 1931bc096d8a2ca4d53fef9dee2921f86c0648d3 Mon Sep 17 00:00:00 2001
From: Apertis CI <devel@lists.apertis.org>
Date: Mon, 10 Mar 2025 12:32:46 +0000
Subject: [PATCH 1/5] Import Upstream version 1.4.0

---
 .gitlab-ci.yml                                |   33 +-
 Makefile.in                                   |    2 +-
 NEWS                                          |  422 ++-
 doc/dox/config/pipewire-client.conf.5.md      |   19 +-
 doc/dox/config/pipewire-jack.conf.5.md        |   10 +-
 doc/dox/config/pipewire-props.7.md            |   63 +-
 doc/dox/config/xref.md                        |    2 +-
 doc/dox/index.dox                             |    3 +-
 doc/dox/internals/midi.dox                    |   20 +-
 doc/dox/modules.dox                           |    4 +
 doc/dox/programs/pipewire.1.md                |    4 +
 doc/dox/programs/pw-loopback.1.md             |    4 +-
 doc/dox/programs/spa-acp-tool.1.md            |   16 +-
 meson.build                                   |   85 +-
 meson_options.txt                             |   12 +
 pipewire-alsa/alsa-plugins/ctl_pipewire.c     |   28 +-
 pipewire-alsa/alsa-plugins/pcm_pipewire.c     |  196 +-
 pipewire-jack/examples/ump-source.c           |  114 +
 pipewire-jack/examples/video-dsp-play.c       |    2 +
 pipewire-jack/jack/types.h                    |   23 +
 pipewire-jack/src/control.c                   |    4 +-
 pipewire-jack/src/meson.build                 |   17 +-
 pipewire-jack/src/pipewire-jack-extensions.h  |    7 +
 pipewire-jack/src/pipewire-jack.c             |  423 ++-
 pipewire-v4l2/src/pipewire-v4l2.c             |    2 +-
 po/LINGUAS                                    |    1 +
 po/POTFILES.skip                              |    1 +
 po/de.po                                      |  144 +-
 po/fi.po                                      |  409 +--
 po/id.po                                      |  425 +--
 po/nl.po                                      |    2 +-
 po/pl.po                                      |  211 +-
 po/sl.po                                      |  766 +++++
 po/sv.po                                      |  212 +-
 po/zh_CN.po                                   |  410 +--
 spa/examples/local-libcamera.c                |   52 +-
 spa/examples/local-v4l2.c                     |   53 +-
 spa/examples/local-videotestsrc.c             |  535 ++++
 spa/examples/meson.build                      |    2 +
 spa/include/spa/buffer/alloc.h                |   16 +-
 spa/include/spa/buffer/buffer.h               |   12 +-
 spa/include/spa/buffer/meta.h                 |   37 +-
 spa/include/spa/control/control.h             |    9 +-
 spa/include/spa/control/type-info.h           |    3 +-
 spa/include/spa/control/ump-utils.h           |  230 ++
 spa/include/spa/debug/buffer.h                |   12 +-
 spa/include/spa/debug/context.h               |   11 +-
 spa/include/spa/debug/dict.h                  |   12 +-
 spa/include/spa/debug/file.h                  |   10 +-
 spa/include/spa/debug/format.h                |   17 +-
 spa/include/spa/debug/log.h                   |   10 +-
 spa/include/spa/debug/mem.h                   |   12 +-
 spa/include/spa/debug/node.h                  |   12 +-
 spa/include/spa/debug/pod.h                   |   20 +-
 spa/include/spa/debug/types.h                 |   28 +-
 spa/include/spa/filter-graph/filter-graph.h   |  150 +
 spa/include/spa/graph/graph.h                 |   55 +-
 spa/include/spa/interfaces/audio/aec.h        |   91 +-
 spa/include/spa/monitor/device.h              |   52 +-
 spa/include/spa/monitor/utils.h               |   12 +-
 spa/include/spa/node/io.h                     |   49 +-
 spa/include/spa/node/node.h                   |  160 +-
 spa/include/spa/node/utils.h                  |   14 +-
 spa/include/spa/param/audio/aac-utils.h       |   12 +-
 spa/include/spa/param/audio/alac-utils.h      |   13 +-
 spa/include/spa/param/audio/amr-utils.h       |   13 +-
 spa/include/spa/param/audio/ape-utils.h       |   12 +-
 spa/include/spa/param/audio/dsd-utils.h       |   12 +-
 spa/include/spa/param/audio/dsp-utils.h       |   12 +-
 spa/include/spa/param/audio/flac-utils.h      |   12 +-
 spa/include/spa/param/audio/format-utils.h    |   12 +-
 spa/include/spa/param/audio/iec958-types.h    |   16 +
 spa/include/spa/param/audio/iec958-utils.h    |   12 +-
 spa/include/spa/param/audio/layout.h          |    4 +-
 spa/include/spa/param/audio/mp3-utils.h       |   12 +-
 spa/include/spa/param/audio/ra-utils.h        |   12 +-
 spa/include/spa/param/audio/raw-json.h        |  105 +
 spa/include/spa/param/audio/raw-types.h       |   28 +
 spa/include/spa/param/audio/raw-utils.h       |   12 +-
 spa/include/spa/param/audio/raw.h             |    4 +-
 spa/include/spa/param/audio/vorbis-utils.h    |   12 +-
 spa/include/spa/param/audio/wma-utils.h       |   12 +-
 spa/include/spa/param/bluetooth/audio.h       |    3 +
 spa/include/spa/param/bluetooth/type-info.h   |    2 +
 spa/include/spa/param/format-types.h          |    6 +
 spa/include/spa/param/format-utils.h          |   10 +-
 spa/include/spa/param/format.h                |    2 +
 spa/include/spa/param/latency-utils.h         |   57 +-
 spa/include/spa/param/latency.h               |   12 +-
 spa/include/spa/param/param-types.h           |    5 +
 spa/include/spa/param/profiler-types.h        |    1 +
 spa/include/spa/param/profiler.h              |   17 +-
 spa/include/spa/param/props-types.h           |   10 +-
 spa/include/spa/param/route-types.h           |    4 +-
 spa/include/spa/param/tag-utils.h             |   22 +-
 spa/include/spa/param/video/dsp-utils.h       |   12 +-
 spa/include/spa/param/video/format-utils.h    |   12 +-
 spa/include/spa/param/video/h264-utils.h      |   12 +-
 spa/include/spa/param/video/mjpg-utils.h      |   12 +-
 spa/include/spa/param/video/raw-types.h       |   20 +-
 spa/include/spa/param/video/raw-utils.h       |   12 +-
 spa/include/spa/pod/builder.h                 |  103 +-
 spa/include/spa/pod/compare.h                 |   12 +-
 spa/include/spa/pod/dynamic.h                 |   12 +-
 spa/include/spa/pod/filter.h                  |   20 +-
 spa/include/spa/pod/iter.h                    |  114 +-
 spa/include/spa/pod/parser.h                  |   68 +-
 spa/include/spa/support/cpu.h                 |   53 +-
 spa/include/spa/support/dbus.h                |   49 +-
 spa/include/spa/support/i18n.h                |   28 +-
 spa/include/spa/support/log.h                 |   45 +-
 spa/include/spa/support/loop.h                |  298 +-
 spa/include/spa/support/plugin-loader.h       |   28 +-
 spa/include/spa/support/plugin.h              |   54 +-
 spa/include/spa/support/system.h              |  144 +-
 spa/include/spa/support/thread.h              |   48 +-
 spa/include/spa/utils/defs.h                  |   29 +-
 spa/include/spa/utils/dict.h                  |   27 +-
 spa/include/spa/utils/dll.h                   |   16 +-
 spa/include/spa/utils/endian.h                |   26 +
 spa/include/spa/utils/hook.h                  |   99 +-
 spa/include/spa/utils/json-core.h             |  635 ++++
 spa/include/spa/utils/json-pod.h              |   41 +-
 spa/include/spa/utils/json.h                  |  693 +----
 spa/include/spa/utils/keys.h                  |   17 +
 spa/include/spa/utils/list.h                  |   20 +-
 spa/include/spa/utils/names.h                 |    2 +
 spa/include/spa/utils/ratelimit.h             |   12 +-
 spa/include/spa/utils/result.h                |   26 +-
 spa/include/spa/utils/ringbuffer.h            |   24 +-
 spa/include/spa/utils/string.h                |   44 +-
 spa/include/spa/utils/type-info.h             |    4 -
 spa/include/spa/utils/type.h                  |   50 +
 spa/lib/lib.c                                 |  161 +
 spa/lib/meson.build                           |    6 +
 spa/meson.build                               |   17 +-
 spa/plugins/aec/aec-webrtc.cpp                |   60 +-
 spa/plugins/alsa/90-pipewire-alsa.rules       |    2 +-
 spa/plugins/alsa/acp-tool.c                   |   48 +-
 spa/plugins/alsa/acp/acp.c                    |  132 +-
 spa/plugins/alsa/acp/acp.h                    |    9 +
 spa/plugins/alsa/acp/alsa-mixer.c             |    2 +
 spa/plugins/alsa/acp/alsa-mixer.h             |    2 +
 spa/plugins/alsa/acp/alsa-ucm.c               |  391 ++-
 spa/plugins/alsa/acp/alsa-ucm.h               |   14 +
 spa/plugins/alsa/acp/alsa-util.c              |   53 +-
 spa/plugins/alsa/acp/alsa-util.h              |    1 +
 spa/plugins/alsa/acp/card.h                   |    1 +
 spa/plugins/alsa/acp/compat.h                 |    8 +
 spa/plugins/alsa/alsa-acp-device.c            |   37 +-
 spa/plugins/alsa/alsa-pcm-sink.c              |    4 +-
 spa/plugins/alsa/alsa-pcm-source.c            |    4 +-
 spa/plugins/alsa/alsa-pcm.c                   |  168 +-
 spa/plugins/alsa/alsa-pcm.h                   |   66 +-
 spa/plugins/alsa/alsa-seq-bridge.c            |   79 +-
 spa/plugins/alsa/alsa-seq.c                   |   99 +-
 spa/plugins/alsa/alsa-seq.h                   |    1 +
 .../alsa/mixer/profile-sets/hdmi-ac3.conf     |  110 +
 ...ixer => USB Device 0x46d_0x9a4--USB Mixer} |    0
 spa/plugins/audioconvert/audioadapter.c       |  529 ++--
 spa/plugins/audioconvert/audioconvert.c       | 1011 +++++--
 spa/plugins/audioconvert/benchmark-fmt-ops.c  |   22 +
 spa/plugins/audioconvert/biquad.c             |  369 ++-
 spa/plugins/audioconvert/biquad.h             |   27 +-
 spa/plugins/audioconvert/channelmix-ops-c.c   |   93 +-
 spa/plugins/audioconvert/channelmix-ops-sse.c |  211 +-
 spa/plugins/audioconvert/channelmix-ops.c     |   17 +-
 spa/plugins/audioconvert/channelmix-ops.h     |   13 +-
 spa/plugins/audioconvert/crossover.c          |   49 +-
 spa/plugins/audioconvert/crossover.h          |    3 -
 spa/plugins/audioconvert/delay.h              |   52 -
 spa/plugins/audioconvert/fmt-ops-avx2.c       |  265 +-
 spa/plugins/audioconvert/fmt-ops-rvv.c        |  259 ++
 spa/plugins/audioconvert/fmt-ops-sse2.c       |  291 +-
 spa/plugins/audioconvert/fmt-ops.c            |   26 +
 spa/plugins/audioconvert/fmt-ops.h            |   52 +-
 spa/plugins/audioconvert/hilbert.h            |    7 +
 spa/plugins/audioconvert/meson.build          |   42 +
 spa/plugins/audioconvert/resample-native.c    |   50 +-
 spa/plugins/audioconvert/resample-peaks.c     |    6 +
 spa/plugins/audioconvert/resample.h           |    5 +
 .../audioconvert/spa-resample-dump-coeffs.c   |  200 ++
 spa/plugins/audioconvert/spa-resample.c       |   30 +-
 spa/plugins/audioconvert/test-fmt-ops.c       |  137 +-
 .../audioconvert/test-resample-delay.c        |  456 +++
 spa/plugins/audiomixer/mix-ops.h              |    2 +-
 spa/plugins/avb/avb-pcm.c                     |    5 +-
 spa/plugins/avb/avb-pcm.h                     |   46 +-
 spa/plugins/bluez5/README-Telephony.md        |  345 +++
 spa/plugins/bluez5/a2dp-codec-aac.c           |   41 +-
 spa/plugins/bluez5/a2dp-codec-aptx.c          |   15 +-
 spa/plugins/bluez5/a2dp-codec-caps.h          |    2 +
 spa/plugins/bluez5/a2dp-codec-faststream.c    |   17 +-
 spa/plugins/bluez5/a2dp-codec-lc3plus.c       |    6 +-
 spa/plugins/bluez5/a2dp-codec-ldac.c          |   23 +-
 spa/plugins/bluez5/a2dp-codec-opus-g.c        |   24 +-
 spa/plugins/bluez5/a2dp-codec-opus.c          |   26 +-
 spa/plugins/bluez5/a2dp-codec-sbc.c           |   18 +-
 spa/plugins/bluez5/asha-codec-g722.c          |  176 ++
 spa/plugins/bluez5/backend-native.c           | 1166 +++++++-
 spa/plugins/bluez5/bap-codec-caps.h           |   27 +
 spa/plugins/bluez5/bap-codec-lc3.c            |  172 +-
 spa/plugins/bluez5/bluez5-dbus.c              |  342 ++-
 spa/plugins/bluez5/bluez5-device.c            |  446 ++-
 spa/plugins/bluez5/codec-loader.c             |    4 +-
 spa/plugins/bluez5/decode-buffer.h            |  242 +-
 spa/plugins/bluez5/defs.h                     |   60 +-
 spa/plugins/bluez5/g722/g722_enc_dec.h        |  148 +
 spa/plugins/bluez5/g722/g722_encode.c         |  387 +++
 spa/plugins/bluez5/media-codecs.h             |   16 +-
 spa/plugins/bluez5/media-sink.c               |   37 +-
 spa/plugins/bluez5/media-source.c             |  171 +-
 spa/plugins/bluez5/meson.build                |   12 +-
 spa/plugins/bluez5/midi-node.c                |   46 +-
 spa/plugins/bluez5/quirks.c                   |   66 +-
 spa/plugins/bluez5/sco-source.c               |   16 +-
 spa/plugins/bluez5/telephony.c                | 1870 ++++++++++++
 spa/plugins/bluez5/telephony.h                |  132 +
 spa/plugins/control/mixer.c                   |   82 +-
 spa/plugins/filter-graph/audio-dsp-avx.c      |  326 ++
 spa/plugins/filter-graph/audio-dsp-c.c        |  345 +++
 spa/plugins/filter-graph/audio-dsp-impl.h     |   94 +
 spa/plugins/filter-graph/audio-dsp-sse.c      |  744 +++++
 .../plugins/filter-graph/audio-dsp.c          |   63 +-
 spa/plugins/filter-graph/audio-dsp.h          |  168 ++
 spa/plugins/filter-graph/audio-plugin.h       |   95 +
 .../plugins/filter-graph}/biquad.h            |   29 +-
 spa/plugins/filter-graph/builtin_plugin.c     | 2647 +++++++++++++++++
 .../plugins/filter-graph}/convolver.c         |  246 +-
 .../plugins/filter-graph}/convolver.h         |    4 +-
 spa/plugins/filter-graph/ebur128_plugin.c     |  631 ++++
 spa/plugins/filter-graph/filter-graph.c       | 2170 ++++++++++++++
 .../plugins/filter-graph}/ladspa.h            |    0
 spa/plugins/filter-graph/ladspa_plugin.c      |  385 +++
 .../plugins/filter-graph}/lv2_plugin.c        |  268 +-
 spa/plugins/filter-graph/meson.build          |  117 +
 .../plugins/filter-graph}/pffft.c             |    0
 .../plugins/filter-graph}/pffft.h             |    0
 .../plugins/filter-graph}/sofa_plugin.c       |  216 +-
 spa/plugins/libcamera/libcamera-device.cpp    |   65 +-
 spa/plugins/libcamera/libcamera-source.cpp    |    8 +-
 spa/plugins/libcamera/libcamera-utils.cpp     |   75 +-
 spa/plugins/meson.build                       |    1 +
 spa/plugins/support/cpu-riscv.c               |   29 +
 spa/plugins/support/cpu.c                     |    6 +
 spa/plugins/support/logger.c                  |   39 +-
 spa/plugins/support/loop.c                    |  312 +-
 spa/plugins/support/meson.build               |    1 +
 spa/plugins/support/null-audio-sink.c         |   41 +-
 spa/plugins/v4l2/meson.build                  |    2 +-
 spa/plugins/v4l2/v4l2-source.c                |   29 +-
 spa/plugins/v4l2/v4l2-utils.c                 |   70 +-
 spa/plugins/videoconvert/meson.build          |   19 +-
 spa/plugins/videoconvert/plugin.c             |   12 +
 spa/plugins/videoconvert/videoadapter.c       |  406 ++-
 spa/plugins/videoconvert/videoconvert-dummy.c |  720 +++++
 .../videoconvert/videoconvert-ffmpeg.c        | 2082 +++++++++++++
 spa/plugins/videotestsrc/videotestsrc.c       |   46 +-
 spa/plugins/vulkan/vulkan-utils.c             |    7 +-
 spa/tools/spa-json-dump.c                     |    7 +-
 .../client-rt.conf.avail/20-upmix.conf.in     |    8 -
 src/daemon/client-rt.conf.avail/meson.build   |   12 -
 src/daemon/client-rt.conf.in                  |  136 -
 src/daemon/client.conf.in                     |   72 +-
 src/daemon/filter-chain/35-ebur128.conf       |   63 +
 src/daemon/filter-chain/36-dcblock.conf       |   59 +
 src/daemon/filter-chain/meson.build           |    1 +
 .../filter-chain/sink-upmix-5.1-filter.conf   |  151 +
 src/daemon/jack.conf.in                       |    1 +
 src/daemon/meson.build                        |    2 -
 src/daemon/minimal.conf.in                    |    4 +-
 src/daemon/pipewire-aes67.conf.in             |    4 +
 src/daemon/pipewire-pulse.conf.in             |   15 +-
 src/daemon/pipewire-vulkan.conf.in            |    4 +-
 src/daemon/pipewire.conf.in                   |   75 +-
 src/daemon/systemd/system/meson.build         |    8 +-
 .../systemd/system/pipewire-pulse.service.in  |   24 +
 .../systemd/system/pipewire-pulse.socket      |   12 +
 src/examples/audio-capture.c                  |    1 -
 src/examples/audio-src-ring.c                 |  226 ++
 src/examples/audio-src-ring2.c                |  269 ++
 src/examples/gmain.c                          |  101 +
 src/examples/internal.c                       |   18 +-
 src/examples/local-v4l2.c                     |   20 +-
 src/examples/meson.build                      |    4 +
 src/examples/midi-src.c                       |   14 +-
 src/examples/video-src.c                      |    6 +-
 src/gst/gstpipewireclock.c                    |    2 +-
 src/gst/gstpipewirecore.c                     |    3 +-
 src/gst/gstpipewiredeviceprovider.c           |  206 +-
 src/gst/gstpipewiredeviceprovider.h           |   11 +-
 src/gst/gstpipewireformat.c                   |   20 +
 src/gst/gstpipewirepool.c                     |   62 +-
 src/gst/gstpipewirepool.h                     |    9 +
 src/gst/gstpipewiresink.c                     |  345 ++-
 src/gst/gstpipewiresink.h                     |   21 +
 src/gst/gstpipewiresrc.c                      |   70 +-
 src/gst/gstpipewiresrc.h                      |    2 +-
 src/gst/gstpipewirestream.c                   |    1 +
 src/gst/gstpipewirestream.h                   |    8 +
 src/gst/meson.build                           |    2 +-
 src/modules/meson.build                       |  131 +-
 src/modules/module-access.c                   |   56 +-
 src/modules/module-adapter.c                  |   22 +
 src/modules/module-avb/adp.c                  |   14 +-
 src/modules/module-avb/maap.c                 |   18 +-
 src/modules/module-client-device.c            |   53 +
 src/modules/module-client-node.c              |   68 +
 src/modules/module-combine-stream.c           |  135 +-
 src/modules/module-echo-cancel.c              |   63 +-
 src/modules/module-example-filter.c           |   56 +-
 src/modules/module-example-sink.c             |   64 +-
 src/modules/module-example-source.c           |   64 +-
 src/modules/module-ffado-driver.c             |  113 +-
 src/modules/module-filter-chain.c             | 2276 ++------------
 src/modules/module-filter-chain/biquad.c      |  364 ---
 .../module-filter-chain/builtin_plugin.c      | 1771 -----------
 src/modules/module-filter-chain/dsp-ops-avx.c |   65 -
 src/modules/module-filter-chain/dsp-ops-c.c   |  196 --
 src/modules/module-filter-chain/dsp-ops-sse.c |  122 -
 src/modules/module-filter-chain/dsp-ops.h     |  136 -
 .../module-filter-chain/ladspa_plugin.c       |  256 --
 src/modules/module-filter-chain/plugin.h      |   86 -
 src/modules/module-jack-tunnel.c              |   93 +-
 src/modules/module-jackdbus-detect.c          |   16 +
 src/modules/module-link-factory.c             |  114 +-
 src/modules/module-loopback.c                 |  216 +-
 src/modules/module-metadata.c                 |  101 +-
 src/modules/module-netjack2-driver.c          |   85 +-
 src/modules/module-netjack2-manager.c         |  122 +-
 src/modules/module-netjack2/peer.c            |   86 +-
 src/modules/module-parametric-equalizer.c     |  247 +-
 src/modules/module-pipe-tunnel.c              |   84 +-
 src/modules/module-profiler.c                 |   80 +-
 src/modules/module-protocol-native.c          |   89 +-
 .../module-protocol-native/local-socket.c     |   31 +-
 .../module-protocol-native/security-context.c |    2 +
 src/modules/module-protocol-pulse.c           |   20 +-
 src/modules/module-protocol-pulse/client.c    |   37 +-
 src/modules/module-protocol-pulse/client.h    |    2 +-
 src/modules/module-protocol-pulse/cmd.c       |   42 +-
 src/modules/module-protocol-pulse/defs.h      |   77 +-
 src/modules/module-protocol-pulse/format.c    |   82 +-
 src/modules/module-protocol-pulse/format.h    |    3 +-
 src/modules/module-protocol-pulse/internal.h  |    2 +-
 src/modules/module-protocol-pulse/module.c    |    4 +-
 .../modules/module-stream-restore.c           |   26 +-
 .../module-protocol-pulse/pulse-server.c      |  137 +-
 src/modules/module-protocol-pulse/quirks.c    |    2 +
 src/modules/module-protocol-pulse/quirks.h    |    2 +
 src/modules/module-protocol-pulse/server.c    |   32 +-
 src/modules/module-protocol-pulse/stream.c    |   10 +
 src/modules/module-protocol-pulse/stream.h    |    1 +
 src/modules/module-protocol-pulse/utils.c     |   45 +-
 src/modules/module-protocol-pulse/utils.h     |    1 +
 src/modules/module-protocol-simple.c          |   71 +-
 src/modules/module-pulse-tunnel.c             |   97 +-
 src/modules/module-raop-sink.c                |   91 +-
 src/modules/module-raop/rtsp-client.c         |    4 +-
 src/modules/module-roc-source.c               |   57 +-
 src/modules/module-roc/common.h               |   45 +-
 src/modules/module-rt.c                       |   16 +-
 src/modules/module-rtp-sap.c                  |  410 ++-
 src/modules/module-rtp-sink.c                 |   59 +-
 src/modules/module-rtp-source.c               |   70 +-
 src/modules/module-rtp/audio.c                |  278 +-
 src/modules/module-rtp/midi.c                 |   44 +-
 src/modules/module-rtp/opus.c                 |   22 +-
 src/modules/module-rtp/stream.c               |  145 +-
 src/modules/module-rtp/stream.h               |    8 +-
 src/modules/module-snapcast-discover.c        |  110 +-
 ...-factory.c => module-spa-device-factory.c} |    6 +-
 .../module-device.c => module-spa-device.c}   |    6 +-
 ...de-factory.c => module-spa-node-factory.c} |    6 +-
 .../{spa/module-node.c => module-spa-node.c}  |    6 +-
 src/modules/module-vban-recv.c                |  433 ++-
 src/modules/module-vban-send.c                |   13 +-
 src/modules/module-vban/audio.c               |   30 +-
 src/modules/module-vban/midi.c                |   51 +-
 src/modules/module-vban/stream.c              |  104 +-
 src/modules/module-vban/vban.h                |  116 +-
 src/modules/network-utils.h                   |   30 +-
 src/modules/spa/meson.build                   |   31 -
 src/modules/spa/spa-node.c                    |   81 -
 src/pipewire/array.h                          |   19 +-
 src/pipewire/client.h                         |   58 +-
 src/pipewire/conf.c                           |  145 +-
 src/pipewire/conf.h                           |    4 +
 src/pipewire/context.c                        |  103 +-
 src/pipewire/core.c                           |   25 +
 src/pipewire/core.h                           |  150 +-
 src/pipewire/device.h                         |   55 +-
 src/pipewire/extensions/client-node.h         |   71 +-
 src/pipewire/extensions/metadata.h            |   50 +-
 src/pipewire/extensions/profiler.h            |   25 +-
 src/pipewire/extensions/security-context.h    |   37 +-
 src/pipewire/factory.h                        |   25 +-
 src/pipewire/filter.c                         |   73 +-
 src/pipewire/filter.h                         |   36 +-
 src/pipewire/impl-client.c                    |    4 +
 src/pipewire/impl-device.c                    |    1 +
 src/pipewire/impl-factory.c                   |    2 +
 src/pipewire/impl-link.c                      |  109 +-
 src/pipewire/impl-module.c                    |    1 +
 src/pipewire/impl-node.c                      |  149 +-
 src/pipewire/impl-port.c                      |   19 +-
 src/pipewire/keys.h                           |    6 +-
 src/pipewire/link.h                           |   26 +-
 src/pipewire/loop.c                           |    1 +
 src/pipewire/loop.h                           |  122 +-
 src/pipewire/map.h                            |   20 +-
 src/pipewire/mem.c                            |    1 +
 src/pipewire/mem.h                            |    8 +-
 src/pipewire/module.h                         |   25 +-
 src/pipewire/node.h                           |   65 +-
 src/pipewire/pipewire.c                       |    5 +-
 src/pipewire/port.h                           |   45 +-
 src/pipewire/private.h                        |   47 +-
 src/pipewire/properties.c                     |   32 +-
 src/pipewire/properties.h                     |   26 +-
 src/pipewire/settings.c                       |    9 +-
 src/pipewire/stream.c                         |  171 +-
 src/pipewire/stream.h                         |   97 +-
 src/pipewire/thread-loop.h                    |    5 +
 src/pipewire/thread.c                         |   12 +-
 src/pipewire/thread.h                         |   30 +-
 src/pipewire/utils.c                          |   13 +-
 src/tests/test-security-context.c             |    2 +-
 src/tools/dfffile.c                           |  136 +-
 src/tools/dsffile.c                           |  138 +-
 src/tools/midifile.c                          |  532 +++-
 src/tools/midifile.h                          |    3 +
 src/tools/pw-cat.c                            |  159 +-
 src/tools/pw-cli.c                            |   51 +-
 src/tools/pw-config.c                         |    3 +-
 src/tools/pw-container.c                      |   10 +-
 src/tools/pw-dot.c                            |   10 +-
 src/tools/pw-dump.c                           |   12 +-
 src/tools/pw-loopback.c                       |    4 +-
 src/tools/pw-mididump.c                       |   10 +-
 src/tools/pw-mon.c                            |    4 +-
 src/tools/pw-profiler.c                       |  216 +-
 src/tools/pw-top.c                            |   11 +-
 subprojects/webrtc-audio-processing.wrap      |    4 +-
 subprojects/wireplumber.wrap                  |    2 +-
 test/meson.build                              |    1 +
 test/test-example.c                           |    4 +
 test/test-spa-control.c                       |  173 ++
 test/test-spa-json.c                          |   33 +
 449 files changed, 36389 insertions(+), 13169 deletions(-)
 create mode 100644 pipewire-jack/examples/ump-source.c
 create mode 100644 po/sl.po
 create mode 100644 spa/examples/local-videotestsrc.c
 create mode 100644 spa/include/spa/control/ump-utils.h
 create mode 100644 spa/include/spa/filter-graph/filter-graph.h
 create mode 100644 spa/include/spa/param/audio/raw-json.h
 create mode 100644 spa/include/spa/utils/endian.h
 create mode 100644 spa/include/spa/utils/json-core.h
 create mode 100644 spa/lib/lib.c
 create mode 100644 spa/lib/meson.build
 create mode 100644 spa/plugins/alsa/mixer/profile-sets/hdmi-ac3.conf
 rename spa/plugins/alsa/mixer/samples/{USB Device 0x46d:0x9a4--USB Mixer => USB Device 0x46d_0x9a4--USB Mixer} (100%)
 delete mode 100644 spa/plugins/audioconvert/delay.h
 create mode 100644 spa/plugins/audioconvert/fmt-ops-rvv.c
 create mode 100644 spa/plugins/audioconvert/spa-resample-dump-coeffs.c
 create mode 100644 spa/plugins/audioconvert/test-resample-delay.c
 create mode 100644 spa/plugins/bluez5/README-Telephony.md
 create mode 100644 spa/plugins/bluez5/asha-codec-g722.c
 create mode 100644 spa/plugins/bluez5/g722/g722_enc_dec.h
 create mode 100644 spa/plugins/bluez5/g722/g722_encode.c
 create mode 100644 spa/plugins/bluez5/telephony.c
 create mode 100644 spa/plugins/bluez5/telephony.h
 create mode 100644 spa/plugins/filter-graph/audio-dsp-avx.c
 create mode 100644 spa/plugins/filter-graph/audio-dsp-c.c
 create mode 100644 spa/plugins/filter-graph/audio-dsp-impl.h
 create mode 100644 spa/plugins/filter-graph/audio-dsp-sse.c
 rename src/modules/module-filter-chain/dsp-ops.c => spa/plugins/filter-graph/audio-dsp.c (55%)
 create mode 100644 spa/plugins/filter-graph/audio-dsp.h
 create mode 100644 spa/plugins/filter-graph/audio-plugin.h
 rename {src/modules/module-filter-chain => spa/plugins/filter-graph}/biquad.h (96%)
 create mode 100644 spa/plugins/filter-graph/builtin_plugin.c
 rename {src/modules/module-filter-chain => spa/plugins/filter-graph}/convolver.c (51%)
 rename {src/modules/module-filter-chain => spa/plugins/filter-graph}/convolver.h (72%)
 create mode 100644 spa/plugins/filter-graph/ebur128_plugin.c
 create mode 100644 spa/plugins/filter-graph/filter-graph.c
 rename {src/modules/module-filter-chain => spa/plugins/filter-graph}/ladspa.h (100%)
 create mode 100644 spa/plugins/filter-graph/ladspa_plugin.c
 rename {src/modules/module-filter-chain => spa/plugins/filter-graph}/lv2_plugin.c (70%)
 create mode 100644 spa/plugins/filter-graph/meson.build
 rename {src/modules/module-filter-chain => spa/plugins/filter-graph}/pffft.c (100%)
 rename {src/modules/module-filter-chain => spa/plugins/filter-graph}/pffft.h (100%)
 rename {src/modules/module-filter-chain => spa/plugins/filter-graph}/sofa_plugin.c (62%)
 create mode 100644 spa/plugins/support/cpu-riscv.c
 create mode 100644 spa/plugins/videoconvert/videoconvert-dummy.c
 create mode 100644 spa/plugins/videoconvert/videoconvert-ffmpeg.c
 delete mode 100644 src/daemon/client-rt.conf.avail/20-upmix.conf.in
 delete mode 100644 src/daemon/client-rt.conf.avail/meson.build
 delete mode 100644 src/daemon/client-rt.conf.in
 create mode 100644 src/daemon/filter-chain/35-ebur128.conf
 create mode 100644 src/daemon/filter-chain/36-dcblock.conf
 create mode 100644 src/daemon/filter-chain/sink-upmix-5.1-filter.conf
 create mode 100644 src/daemon/systemd/system/pipewire-pulse.service.in
 create mode 100644 src/daemon/systemd/system/pipewire-pulse.socket
 create mode 100644 src/examples/audio-src-ring.c
 create mode 100644 src/examples/audio-src-ring2.c
 create mode 100644 src/examples/gmain.c
 delete mode 100644 src/modules/module-filter-chain/biquad.c
 delete mode 100644 src/modules/module-filter-chain/builtin_plugin.c
 delete mode 100644 src/modules/module-filter-chain/dsp-ops-avx.c
 delete mode 100644 src/modules/module-filter-chain/dsp-ops-c.c
 delete mode 100644 src/modules/module-filter-chain/dsp-ops-sse.c
 delete mode 100644 src/modules/module-filter-chain/dsp-ops.h
 delete mode 100644 src/modules/module-filter-chain/ladspa_plugin.c
 delete mode 100644 src/modules/module-filter-chain/plugin.h
 rename src/modules/{spa/module-device-factory.c => module-spa-device-factory.c} (97%)
 rename src/modules/{spa/module-device.c => module-spa-device.c} (95%)
 rename src/modules/{spa/module-node-factory.c => module-spa-node-factory.c} (98%)
 rename src/modules/{spa/module-node.c => module-spa-node.c} (95%)
 delete mode 100644 src/modules/spa/meson.build
 create mode 100644 test/test-spa-control.c

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index b671db36..3953445e 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,3 +1,13 @@
+# Create merge request pipelines for open merge requests, branch pipelines
+# otherwise. This allows MRs for new users to run CI, and prevents duplicate
+# pipelines for branches with open MRs.
+workflow:
+  rules:
+    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
+    - if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS
+      when: never
+    - if: $CI_COMMIT_BRANCH
+
 stages:
   - container
   - container_coverity
@@ -25,7 +35,7 @@ include:
 .fedora:
   variables:
     # Update this tag when you want to trigger a rebuild
-    FDO_DISTRIBUTION_TAG: '2024-05-30.0'
+    FDO_DISTRIBUTION_TAG: '2024-12-10.0'
     FDO_DISTRIBUTION_VERSION: '40'
     FDO_DISTRIBUTION_PACKAGES: >-
       alsa-lib-devel
@@ -46,6 +56,7 @@ include:
       jack-audio-connection-kit-devel
       libasan
       libcanberra-devel
+      libebur128-devel
       libffado-devel
       libldac-devel
       libmysofa-devel
@@ -307,17 +318,24 @@ build_on_fedora_html_docs:
         -Dsndfile=enabled
         -Dsession-managers=[]
   before_script:
-    - git fetch origin 1.0 master
+    - git fetch origin 1.0 1.2 master
     - git branch -f 1.0 origin/1.0
-    - git branch -f master origin/master
     - git clone -b 1.0 . branch-1.0
+    - git branch -f 1.2 origin/1.2
+    - git clone -b 1.2 . branch-1.2
+    - git branch -f master origin/master
     - git clone -b master . branch-master
     - !reference [.build, before_script]
   script:
     - cd branch-1.0
     - meson setup builddir $MESON_OPTIONS
     - meson compile -C builddir doc/pipewire-docs
-    - cd ../branch-master
+    - cd ..
+    - cd branch-1.2
+    - meson setup builddir $MESON_OPTIONS
+    - meson compile -C builddir doc/pipewire-docs
+    - cd ..
+    - cd branch-master
     - meson setup builddir $MESON_OPTIONS
     - meson compile -C builddir doc/pipewire-docs
   artifacts:
@@ -558,12 +576,15 @@ pages:
   dependencies:
     - build_on_fedora_html_docs
   script:
-    - mkdir public public/devel
-    - cp -R branch-1.0/builddir/doc/html/* public/
+    - mkdir public public/1.0 public/1.2 public/devel
+    - cp -R branch-1.0/builddir/doc/html/* public/1.0/
+    - cp -R branch-1.2/builddir/doc/html/* public/1.2/
     - cp -R branch-master/builddir/doc/html/* public/devel/
+    - (cd public && ln -s 1.2/* .)
   artifacts:
     paths:
       - public
   rules:
     - if: $CI_COMMIT_BRANCH == 'master'
     - if: $CI_COMMIT_BRANCH == '1.0'
+    - if: $CI_COMMIT_BRANCH == '1.2'
diff --git a/Makefile.in b/Makefile.in
index 67b199bb..10461931 100644
--- a/Makefile.in
+++ b/Makefile.in
@@ -38,7 +38,7 @@ gdb:
 	$(MAKE) run DBG=gdb
 
 valgrind:
-	$(MAKE) run DBG="DISABLE_RTKIT=1 PIPEWIRE_DLCLOSE=false valgrind --trace-children=yes"
+	$(MAKE) run DBG="DISABLE_RTKIT=1 PIPEWIRE_DLCLOSE=false valgrind --trace-children=yes --leak-check=full"
 
 test: all
 	ninja -C $(BUILD_ROOT) test
diff --git a/NEWS b/NEWS
index ea6fab48..66ce22fc 100644
--- a/NEWS
+++ b/NEWS
@@ -1,3 +1,343 @@
+# PipeWire 1.4.0 (2025-03-06)
+
+This is the 1.4 release that is API and ABI compatible with previous
+1.2.x and 1.0.x releases.
+
+This release contains some of the bigger changes that happened since
+the 1.2 release last year, including:
+
+  * client-rt.conf was removed, all clients now use client.conf and
+    are given RT priority in the data threads.
+  * UMP (aka MIDI2) support was added and is now the default format
+    to carry MIDI1 and MIDI2 around in PipeWire. There are helper
+    functions to convert between legacy MIDI and UMP.
+  * The resampler can now precompute (at compile time) some common
+    conversion filters. Delay reporting in the resampler was fixed and
+    improved.
+  * Bluetooth support for BAP broadcast links and support for hearing aids
+    using ASHA was added. A new G722 codec was also added.
+    Delay reporting and configuration in Bluetooth was improved.
+  * The ALSA plugin now supports DSD playback when explicitly allowed
+    with the alsa.formats property.
+  * A PipeWire JACK control API was added.
+  * A system service was added for pipewire-pulse.
+  * Many documentation and translation updates.
+  * Many of the SPA macros are converted to inline functions.  All SPA
+    inline functions are now also compiled into a libspa.so library to
+    make it easier to access them from bindings.
+  * The module-filter-chain graph code was moved to a separate
+    filter-graph SPA plugin so that it becomes usable in more places.
+    EBUR128, param_eq and dcblock plugins were added to filter-graph.
+    The filter graph can now also use fftw for doing convolutions.
+    The audioconvert plugin was optimized and support was added to
+    audioconvert to insert extra filter-graphs in the processing pipeline.
+  * New helper functions were added to parse JSON format descriptions.
+  * The profiler now also includes the clock of the followers.
+  * RISCV CPU support and assembler optimisations were added.
+  * The clock used for logging timestamps can be configured now.
+  * The JSON parser was split into core functions and helper.
+  * Support for UCM split PCMs was added. Instead of alsa-lib splitting
+    up PCMs, PipeWire can mark the PCMs with the correct metadata so that
+    the session manager can use native PipeWire features to do this.
+  * Support for webrtc2 was added to echo-cancel.
+  * IEC958 codecs are now detected from the HDMI ELD data.
+  * Conversion between floating point and 32 bits now preserve 25 bits of
+    precision instead of 24 bits.
+  * A new Telephony D-BUS API compatible with ofono was added.
+  * The invoke queues are now more efficient and can be called from multiple
+    threads concurrently.
+  * Clock information in v4l2 was improved.
+  * An ffmpeg based videoconvert plugin was added that can be used with the
+    videoadapter.
+  * The GStreamer elements have improved buffer pool handling and rate
+    matching.
+  * The combine-stream module can now also mix streams.
+  * link-factory now checks that the port and node belong together.
+  * The netjack-manager module has support for autoconnecting streams.
+  * The native-protocol has support for abstract sockets.
+  * The pulse server has support for blocking playback and capture in
+    pulse.rules.
+  * The corked state of stream is now reported correctly in pulse-server.
+  * Fix backwards jumps in pulse-server.
+  * Latency configuration support was added in loopback and raop-sink.
+  * The ROC module has more configuration options.
+  * The SAP module now only send updated SDP when something changed.
+  * RTP source now has a standby mode where it idles when there is no
+    data received.
+  * Support for PTP clocking was added the RTP streams.
+  * The VBAN receiver can now dynamically create streams when they are
+    detected.
+  * Error reporting when making links was improved.
+  * Support for returning (canceling) a dequeued buffer in pw-stream.
+  * Support for emiting events in pw-stream was added.
+  * pw-cat now support stdin and stdout.
+
+
+## Highlights (since the previous 1.3.83 release)
+  - Small fixes and improvements.
+
+## PipeWire
+  - Fix some missing includes in metadata.h
+  - Pass the current error in errno when a stream is in error (#4574)
+
+
+## modules
+  - Evaluate node rules before loading adapter follower to ensure
+    properties are set correctly. (#4562)
+
+## SPA
+  - Avoid a use after free when building PODs. (#4445)
+  - Take headroom into account when calculating resync.
+
+## Bluetooth
+  - Fix +CLCC parsing.
+
+## GStreamer
+  - Notify about default device changes in deviceprovider.
+  - Copy frames between pools and avoid splitting video buffers.
+
+## JACK
+  - Add an option to disable the MIDI2 port flags. (#4584)
+
+
+Older versions:
+
+# PipeWire 1.3.83 (2025-02-20)
+
+This is the third and hopefully last 1.4 release candidate that
+is (almost) API and (entirely) ABI compatible with previous 1.2.x
+and 1.0.x releases.
+
+We note that in the 1.3.x series, the API is slighty not backwards
+compatible because some methods previously used to accept void* as
+a parameter while they now require the correct type. We think this
+is however a good kind of API breakage and expect projects to patch
+their code to get things compiled with newer version (which will also
+compile for older versions). Note also that this is not an ABI break.
+
+
+## Highlights
+  - Handle JACK transport updates in a better way.
+  - Fix a SAP regression when starting.
+  - Fix regression in rate scaling.
+  - Improve bluetooth source rate handling.
+  - More small bugfixes and improvements.
+
+
+## PipeWire
+  - Handle JACK transport updates in a better way. (#4543)
+
+## Modules
+  - Check that the link factory port and nodes match. Deprecate the
+    port.id when making links.
+  - Improve profiler output by scaling the quantum with the node
+    rate so that we don't end up with confusing information. (#4555)
+  - Fix sending of the SAP SDP. Handle some SDP parsing errors.
+  - Add some more options to the ROC source module. (#4516)
+
+## SPA
+  - Fix firewire quirks in udev rules. (#4528)
+  - Fix a bug in the rate scaling in some cases that would make things
+    run with the wrong samplerate.
+  - Improve introspection of control types.
+
+## Bluetooth
+  - Use the G722 codec from Android instead of FFmpeg for ASHA.
+  - Use the A2DP source rate as the graph rate. (#4555)
+  - Specify the bluetooth source latency property in the rate of the
+    stream to avoid conversions and rounding errors.
+
+# PipeWire 1.3.82 (2025-02-06)
+
+This is the second 1.4 release candidate that is API and ABI
+compatible with previous 1.2.x and 1.0.x releases.
+
+## Highlights
+  - Various pw-stream improvements: timing information fixes,
+    avoid locking buffers in some cases and an improved drain
+    event.
+  - A new Telephony D-BUS API compatible with ofono.
+  - Documentation fixes and updates.
+  - More small fixes and improvements.
+
+
+## PipeWire
+  - Improve timing information when rate is unknown.
+  - Avoid locked buffers in pw_stream in some cases.
+  - Improve pw_stream drain event emission.
+  - Improve manager socket handling. Applications can avoid hardcoding
+    the sockets so that they will respect the config settings.
+
+## modules
+  - Fix header size calculation when using ipv6. (#4524)
+
+## SPA
+  - Optimize byteswapped s16 conversions.
+  - Improve event handling for internal events.
+  - Optimize negiotiation when in convert mode, prefer the format
+    of the follower in adapter.
+  - Fix EnumPortConfig for videoadapter without converter.
+  - Fix libcamera property buffer size.
+
+## Pulse-server
+  - Add systemwide systemd files.
+
+## JACK
+  - Add a UMP example.
+  - Use the new JackPortMIDI2 flag to mark UMP ports to JACK.
+
+## Bluetooth
+  - Support BAP hardware volume.
+  - Add a Telephony DBUS API.
+
+## GStreamer
+  - Disable buffer pools for audio by default.
+
+## Docs
+  - Improve the module documentation.
+
+# PipeWire 1.3.81 (2025-01-23)
+
+This is the first 1.4 release candidate that is API and ABI
+compatible with previous 1.2.x and 1.0.x releases.
+
+In addition to all the changes backported to 1.2.x, this release
+also contains some new features:
+
+## Highlights
+  - UMP support was added with MIDI 1.0 and MIDI 2.0 support in the ALSA
+    sequencer plugin. By default PipeWire will now use MIDI 2.0 in UMP
+    messages to transport MIDI in the graph, with conversions to/from legacy
+    MIDI where required. This requires UMP support in the kernel.
+  - client-rt.conf is no longer supported. Custom changes made to this
+    config should be moved to client.conf. Clients that try to load the
+    client-rt.conf will emit a warning and be directed to client.conf
+    automatically for backwards compatibility.
+  - The module-filter-chain code was moved to a new filter-graph plugin. This
+    made it possible to add filter-graph support directly in audioconvert. It
+    is now possible to run up to 8 run-time swappable filter-graphs inside
+    streams and nodes. This should make it easier to add effects to streams
+    and device nodes.
+  - Bluetooth support for BAP broadcast links and support for hearing aids
+    using ASHA was added.
+  - Many more bugfixes and improvements.
+
+## PipeWire
+  - Nodes are now only scheduled when ready to signal the driver.
+  - Add slovenian translation. (#4156)
+  - Link errors are handled better.
+  - The videoadapter is now enabled by default but no videoconverter
+    is loaded yet by default.
+  - Streams now have support for ProcessLatency.
+  - Streams now have a method to emit events.
+  - The RequestProcess event and command can now pass around extra
+    properties.
+  - Local timestamps are now used for logging.
+  - client-rt.conf is no longer supported. Custom changes made to this
+    config should be moved to client.conf. Clients that try to load the
+    client-rt.conf will emit a warning and be directed to cliert.conf
+    automatically to preserve backwards compatibility.
+  - pw_stream now has an API to return unused buffers.
+
+## modules
+  - module-combine-stream can now mix streams.
+  - Links in error are now destroyed by link-factory.
+  - The netjack2 driver can now also create streams that autoconnect when
+    specified. (#4125)
+  - Many updates and bugfixes to the RTP modules.
+  - The netjack2 driver can now bind to a custom IP and port pair. (#4144)
+  - The loopback module and module-raop have support for ProcessLatency, which
+    can be used to query and update the latency.
+  - The profiler module can now reduce the sampling rate.
+  - The filter-chain was optimized some more.
+  - The filter-chain gained some more plugins: param_eq, ebur128, dcblock.
+  - Support for fftw based convolver was added.
+  - Some module arguments can now be overridden.
+  - The VBAN receiver now creates new streams per stream name. (#4400)
+  - The RTP SAP module is now smarter with generating new SAP messages.
+  - The RTP source can now be paused when no data is received. (#4456)
+
+## tools
+  - pw-cat can now stream most formats from stdin/stdout.
+  - pw-profiler has a JSON dump option to dump the raw profiler data.
+  - pw-cli now supports unload-module. (#4276)
+
+## SPA
+  - The resampler can precompute some common coeficients now at compile
+    time.
+  - UMP support was added with MIDI 1.0 and MIDI 2.0 support in the ALSA
+    sequencer plugin. By default PipeWire will now use MIDI 2.0 in UMP
+    messages to transport MIDI in the graph, with conversions to/from legacy
+    MIDI where required.
+  - Control types can now be negotiated.
+  - Support for writing ALSA bind controls was added.
+  - The ALSA sequencer now has better names for the ports.
+  - The F32 to S32 conversion now uses 25 bits for an extra bit of
+    precision.
+  - libcamera controls can now be set in all cases.
+  - The videoadapter has been improved and a dummy and ffmpeg based
+    videoconverter plugin was added.
+  - Negotiation was improved in audioadapter. First a passthrough format
+    is tried.
+  - Some JSON helper functions were added and some duplicate code removed
+    or simplified.
+  - Add support for RISC V CPU detection and add many optimizations in
+    the audio converters.
+  - Add an option to disable ALSA mixer path select. (#4311)
+  - Fix a potential bug with the cleanup of the loop queues.
+  - ALSA nodes now dynamically adjust the DLL bandwidth based on average
+    measured variance.
+  - The loop invoke queue was made more efficient and make it possible to
+    invoke from multiple threads.
+  - The filter-chain code was moved to a new filter-graph plugin.
+  - Most function macros are now static inlined and can also be built into a
+    libspa.so file. This should improve language bindings.
+  - V4l2 clock information was improved.
+  - Supported IEC958 codecs are now autodetected via ELD info.
+  - Audioconvert was optimized some more.
+  - Audioconvert can now include filter-graphs in its processing.
+  - webrtc-audio-processing-2 is now supported in AEC.
+  - The resampler now reports the delay and subsample delay. Also the
+    delay is reported in the samplerate of the input.
+  - The ALSA sequencer now handle kernels without UMP support. (#4507)
+
+## Pulse-server
+  - Add quirk to block clients from making record and playback streams.
+  - The corked state is now set on stream to always report this state
+    correctly to other clients.
+  - Readiness notification was added to the pulse server with the
+    PIPEWIRE_PULSE_NOTIFICATION_FD environment variable. (#4347)
+  - The pulse.cmd config now supports conditions.
+  - A bug in clearing the ringbuffer was fixed. (#4464)
+
+## GStreamer
+  - Support for the default devices was added to the deviceprovider. (#4268)
+  - The graph clock is now used as the source for the GStreamer clock.
+  - The sink now does some rate control.
+
+## ALSA
+  - The ALSA plugin now supports DSD when explicitly enabled.
+
+## JACK
+  - JACK now supports 2 new extension formats for OSC and UMP.
+  - JACK clients can receive UMP MIDI1 or MIDI2 messages when using
+    the new UMP port format extension.
+  - JACK now reports the PipeWire version in the minor/micro/proto.
+  - Implement more jackserver functions.
+
+## Bluetooth
+  - Support BAP broadcast links.
+  - Support for ASHA was added.
+  - Delay reporting in A2DP sources was improved.
+
+## Examples
+  - 2 new examples of pw-stream using spa_ringbuffer were added.
+
+## Docs
+  - Many updates to the man pages.
+  - More documentation about thread safety of functions in stream
+    and filters. (#4521)
+
 # PipeWire 1.2.7 (2024-11-26)
 
 This is a bugfix release that is API and ABI compatible with the previous
@@ -48,9 +388,6 @@ This is a bugfix release that is API and ABI compatible with the previous
 ## Tools
   - Fix pw-dot link labels.
 
-Older versions:
-
-
 # PipeWire 1.2.6 (2024-10-23)
 
 This is a bugfix release that is API and ABI compatible with the previous
@@ -93,6 +430,40 @@ This is a bugfix release that is API and ABI compatible with the previous
 ## Docs
   - Backport docs from master.
 
+# PipeWire 1.0.9 (2024-10-22)
+
+This is a bugfix release that is API and ABI compatible with previous
+1.0.x releases.
+
+## Highlights
+  - Fix an fd leak and confusion in the protocol that would cause leaks and
+    wrong memory to be used.
+  - Fix bug where the mixer would not be synced correctly after selecting
+    a port, leaving the audio muted. (#4084)
+  - Backport v4l2 systemd-logind support to avoid races when starting.
+    (#3539 and #3960).
+  - Other small fixed and improvements.
+
+
+## PipeWire
+  - Fix a bug where renegotiation would sometimes fail to deactivate a link.
+  - Fix an fd leaks and confusion in the protocol.
+
+## modules
+  - Fix a use-after-free in the rt module when stopping a thread.
+
+## SPA
+  - Fix bug where the mixer would not be synced correctly after selecting
+    a port, leaving the audio muted. (#4084)
+  - Fix a compilation issue with empty initializers. (#4317)
+  - Backport v4l2 systemd-logind support to avoid races when starting.
+    (#3539 and #3960).
+  - Fix a potential crash when cleaning ALSA nodes.
+
+## JACK
+  - align buffers to the max cpu alignment in order to allow more
+    optimizations.
+
 # PipeWire 1.2.5 (2024-09-27)
 
 This is an important bugfix release that is API and ABI compatible with the
@@ -138,7 +509,6 @@ previous 1.2.x and 1.0.x releases.
 ## Doc
   - Some small doc updates. (#4272)
 
-
 # PipeWire 1.2.4 (2024-09-19)
 
 This is a bugfix release that is API and ABI compatible with the
@@ -167,6 +537,50 @@ previous 1.2.x and 1.0.x releases.
   - Emit buffer_size callback in jack_activate() to improve
     compatibility with GStreamer. (#4260)
 
+# PipeWire 1.0.8 (2024-09-19)
+
+This is a small bugfix release that is API and ABI compatible with previous
+1.0.x releases.
+
+## Highlights
+  - Backport support for explicit sync.
+  - FFADO backport fixes.
+  - Fix some races in JACK.
+  - More small fixes and improvements.
+
+
+## PipeWire
+  - Add support for mandatory metadata and explicit sync metadata.
+  - Fix RequestProcess again.
+  - Include config.h to use malloc_trim() when cleaning nodes.
+  - Avoid crash when destroying a global. (#4250)
+
+## Modules
+  - FFADO fixes: improve timing reporting, avoid some xruns, improve
+    samplerate and period size handling, implement freewheeling.
+  - Decrease memory usage of the profiler.
+
+## Tools
+  - Fix pw-dump metadata changes fix. (#4053)
+  - Support large params in pw-cli. (#4166)
+
+## SPA
+  - Improve libcamera devices reporting to properly filter out duplicates in
+    all cases.
+  - Improve property reporting in v4l2.
+  - Fix lost buffer in v4l2.
+
+## Bluetooth
+  - Improve compatibility with some devices.
+
+## JACK
+  - Fix some races when shutting down.
+  - Fix rt-priority on the main thread when using custom thread create
+    function. (#4099)
+
+## ALSA
+  - Handle format renegotiation. (#3856)
+
 # PipeWire 1.2.3 (2024-08-22)
 
 This is a bugfix release that is API and ABI compatible with the
diff --git a/doc/dox/config/pipewire-client.conf.5.md b/doc/dox/config/pipewire-client.conf.5.md
index 11a849d3..66bf3d2e 100644
--- a/doc/dox/config/pipewire-client.conf.5.md
+++ b/doc/dox/config/pipewire-client.conf.5.md
@@ -18,18 +18,6 @@ The PipeWire client configuration file.
 
 *$XDG_CONFIG_HOME/pipewire/client.conf.d/*
 
-*$XDG_CONFIG_HOME/pipewire/client-rt.conf*
-
-*$(PIPEWIRE_CONFIG_DIR)/client-rt.conf*
-
-*$(PIPEWIRE_CONFDATADIR)/client-rt.conf*
-
-*$(PIPEWIRE_CONFDATADIR)/client-rt.conf.d/*
-
-*$(PIPEWIRE_CONFIG_DIR)/client-rt.conf.d/*
-
-*$XDG_CONFIG_HOME/pipewire/client-rt.conf.d/*
-
 # DESCRIPTION
 
 Configuration for PipeWire native clients, and for PipeWire's ALSA
@@ -38,9 +26,6 @@ plugin.
 A PipeWire native client program selects the default config to load,
 and if nothing is specified, it usually loads `client.conf`.
 
-The ALSA plugin uses the `client-rt.conf` file, as do some PipeWire
-native clients such as \ref page_man_pw-cat_1 "pw-cat(1)".
-
 The configuration file format and lookup logic is the same as for \ref page_man_pipewire_conf_5 "pipewire.conf(5)".
 
 Drop-in configuration files `client.conf.d/*.conf` can be used, and are recommended.
@@ -148,7 +133,7 @@ An `alsa.properties` section can be added to configure client applications
 that connect via the PipeWire ALSA plugin.
 
 ```css
-# ~/.config/pipewire/client-rt.conf.d/custom.conf
+# ~/.config/pipewire/client.conf.d/custom.conf
 
 alsa.properties = {
     #alsa.deny = false
@@ -193,7 +178,7 @@ set any of the above ALSA properties or any of the `stream.properties`.
 ### Example
 
 ```css
-# ~/.config/pipewire/client-rt.conf.d/custom.conf
+# ~/.config/pipewire/client.conf.d/custom.conf
 
 alsa.rules = [
     {   matches = [ { application.process.binary = "resolve" } ]
diff --git a/doc/dox/config/pipewire-jack.conf.5.md b/doc/dox/config/pipewire-jack.conf.5.md
index a2e2f6e3..b8a4a5cf 100644
--- a/doc/dox/config/pipewire-jack.conf.5.md
+++ b/doc/dox/config/pipewire-jack.conf.5.md
@@ -70,7 +70,7 @@ jack.properties = {
     #jack.max-client-ports   = 768
     #jack.fill-aliases       = false
     #jack.writable-input     = false
-
+    #jack.flag-midi2         = false
 }
 ```
 
@@ -198,6 +198,14 @@ from the buffer.
 Set this to true to avoid buffer corruption if you are only dealing with non-buggy clients.
 \endparblock
 
+@PAR@ jack.conf  jack.flag-midi2
+\parblock
+Use the new JACK MIDI2 port flag on MIDI2 (UMP) ports. This is disabled by default because most
+JACK apps don't know about this flag yet and refuse to show the port.
+
+Set this to true for applications that know how to handle MIDI2 ports.
+\endparblock
+
 # MATCH RULES  @IDX@ jack.conf
 
 `jack.rules` provides an `update-props` action that takes an object with properties that are updated
diff --git a/doc/dox/config/pipewire-props.7.md b/doc/dox/config/pipewire-props.7.md
index 9822cce9..d7e6af82 100644
--- a/doc/dox/config/pipewire-props.7.md
+++ b/doc/dox/config/pipewire-props.7.md
@@ -664,8 +664,19 @@ This option does nothing if `api.alsa.use-acp` is set to `false`.
 @PAR@ device-prop  api.alsa.soft-mixer = false  # boolean
 Setting this option to `true` will disable the hardware mixer for volume
 control and mute. All volume handling will then use software volume and mute,
-leaving the hardware mixer untouched. The hardware mixer will still be used
-to mute unused audio paths in the device.
+leaving the hardware mixer untouched. This can be interesting to work around
+bugs in the mixer detection or decibel reporting. The hardware mixer will still
+be used to mute unused audio paths in the device. Use `api.alsa.disable-mixer-path`
+to also disable mixer path selection.
+
+@PAR@ device-prop  api.alsa.disable-mixer-path = false  # boolean
+Setting this option to `true` will disable the hardware mixer path selection.
+The hardware mixer path is the configuration of the mixer depending on the
+jacks that are inserted in the card. If this is disabled, you will have to
+manually enable and disable mixer controls but it can be used to work around
+bugs in the mixer. The hardware mixer will still be used for
+volume and mute. Use `api.alsa.soft-mixer` to also disable hardware volume
+and mute.
 
 @PAR@ device-prop  api.alsa.ignore-dB = false  # boolean
 Setting this option to `true` will ignore the decibel setting configured by
@@ -702,6 +713,11 @@ Normally, the maximum amount of channels will be used but with this setting
 this can be reduced, which can make it possible to use other samplerates on
 some devices.
 
+@PAR@ device-prop  api.alsa.split-enable    # boolean
+\parblock
+\copydoc SPA_KEY_API_ALSA_SPLIT_ENABLE
+\endparblock
+
 ## Node properties
 
 @PAR@ node-prop  audio.channels    # integer
@@ -785,6 +801,24 @@ UNDOCUMENTED
 Enable only specific IEC958 codecs. This can be used to disable some codecs the hardware supports.
 Available values: PCM, AC3, DTS, MPEG, MPEG2-AAC, EAC3, TRUEHD, DTSHD
 
+@PAR@ device-prop  api.alsa.split.parent    # boolean
+\parblock
+\copydoc SPA_KEY_API_ALSA_SPLIT_PARENT
+\endparblock
+
+@PAR@ node-prop  api.alsa.split.position  # JSON
+\parblock
+\copybrief SPA_KEY_API_ALSA_SPLIT_POSITION
+Informative property.
+\endparblock
+
+@PAR@ node-prop  api.alsa.split.hw-position  # JSON
+\parblock
+\copybrief SPA_KEY_API_ALSA_SPLIT_HW_POSITION
+Informative property.
+\endparblock
+
+
 # BLUETOOTH PROPERTIES  @IDX@ props
 
 ## Monitor properties
@@ -890,6 +924,7 @@ bluez5.bcast_source.config = [
   {
     "broadcast_code": "Børne House",
     "encryption: false,
+    "sync_factor": 2,
     "bis": [
       { # BIS configuration
         "qos_preset": "16_2_1", # QOS preset name from table Table 6.4 from BAP_v1.0.1.
@@ -904,6 +939,30 @@ bluez5.bcast_source.config = [
 ```
 \endparblock
 
+@PAR@ monitor-prop  bluez5.bap-server-capabilities.rates		# Array of integers
+Supported sampling frequencies for the LC3 codec (default: all).
+Possible values:
+`8000`, `16000`, `24000`, `32000`, `44100`, `48000`
+
+@PAR@ monitor-prop  bluez5.bap-server-capabilities.durations	# Array of doubles
+Supported frame durations for the LC3 codec (default: all).
+Possible values:
+`7.5`, `10`
+
+@PAR@ monitor-prop  bluez5.bap-server-capabilities.channels	# Array of integers
+Supported audio channel counts for the LC3 codec (default: [1, 2]).
+Possible values:
+`1`, `2`, `3`, `4`, `5`, `6`, `7`, `8`
+
+@PAR@ monitor-prop  bluez5.bap-server-capabilities.framelen_min		# integer
+Minimum number of octets supported per codec frame for the LC3 codec (default: 20).
+
+@PAR@ monitor-prop  bluez5.bap-server-capabilities.framelen_max		# integer
+Maximum number of octets supported per codec frame for the LC3 codec (default: 400).
+
+@PAR@ monitor-prop  bluez5.bap-server-capabilities.max_frames		# integer
+Maximum number of codec frames supported per SDU for the LC3 codec (default: 2).
+
 ## Device properties
 
 @PAR@ device-prop  bluez5.auto-connect   # boolean
diff --git a/doc/dox/config/xref.md b/doc/dox/config/xref.md
index e5652fd8..507d09bf 100644
--- a/doc/dox/config/xref.md
+++ b/doc/dox/config/xref.md
@@ -8,7 +8,7 @@
 
 @SECREF@ pipewire-pulse.conf
 
-\ref page_man_pipewire-client_conf_5 "client.conf, client-rt.conf"
+\ref page_man_pipewire-client_conf_5 "client.conf"
 
 @SECREF@ client.conf
 
diff --git a/doc/dox/index.dox b/doc/dox/index.dox
index 58a2b48b..6688ca16 100644
--- a/doc/dox/index.dox
+++ b/doc/dox/index.dox
@@ -40,7 +40,8 @@ See \ref page_api.
 # Resources
 
 - [PipeWire and AGL](https://wiki.automotivelinux.org/_media/pipewire_agl_20181206.pdf)
-- [LAC 2020 Paper](https://lac2020.sciencesconf.org/307881/document)
+- [LAC 2020 Paper](https://lac2020.sciencesconf.org//data/proceedings.pdf) and
+   [Video](https://tube.aquilenet.fr/w/uy8PJyMnBrpBFNEZ9D48Uu)
 - [PipeWire Under The Hood](https://venam.nixers.net/blog/unix/2021/06/23/pipewire-under-the-hood.html)
 - [PipeWire: The Linux audio/video bus (LWN)](https://lwn.net/Articles/847412)
 - [PipeWire Wikipedia](https://en.wikipedia.org/wiki/PipeWire)
diff --git a/doc/dox/internals/midi.dox b/doc/dox/internals/midi.dox
index 77c2b27a..12283004 100644
--- a/doc/dox/internals/midi.dox
+++ b/doc/dox/internals/midi.dox
@@ -58,6 +58,10 @@ Since the MIDI events are embedded in the generic control stream,
 they can be interleaved with other control message types, such as
 property updates or OSC messages.
 
+As of 1.4, SPA_CONTROL_UMP (Universal Midi Packet) is the prefered format
+for the MIDI 1.0 and 2.0 messages in the \ref spa_pod_sequence. Conversion
+to SPA_CONTROL_Midi is performed for legacy applications.
+
 ## The PipeWire Daemon
 
 Nothing special is implemented for MIDI. Negotiation of formats
@@ -78,9 +82,9 @@ in order to route MIDI streams to them from applications that want this.
 
 # Implementation
 
-## PipeWire Media Session
+## Session manager (Wireplumber)
 
-PipeWire media session uses the \ref SPA_NAME_API_ALSA_SEQ_BRIDGE plugin for
+The session manager uses the \ref SPA_NAME_API_ALSA_SEQ_BRIDGE plugin for
 the MIDI features. This creates a single SPA Node with ports per
 MIDI client/stream.
 
@@ -93,8 +97,16 @@ until the sequencer device node is accessible.
 JACK assumes all `"application/control"` ports are MIDI ports.
 
 The control messages are converted to the JACK event format by
-filtering out the \ref SPA_CONTROL_Midi types. On output ports, the JACK
-event stream is converted to control messages in a similar way.
+filtering out the \ref SPA_CONTROL_Midi, \ref SPA_CONTROL_OSC and
+\ref SPA_CONTROL_UMP types. On output ports, the JACK event stream is
+converted to control messages in a similar way.
+
+Normally, all MIDI and UMP messages are converted to MIDI1 jack events unless
+the JACK port was created with an explcit "32 bits raw UMP" format, in which
+case the raw UMP is passed to the JACK application directly. For output ports,
+the JACK events are assumed to be MIDI1 and converted to UMP unless the port
+has the "32 bit raw UMP" format, in which case the UMP messages are simply
+passed on.
 
 There is a 1 to 1 mapping between the JACK events and control
 messages so there is no information loss or need for complicated
diff --git a/doc/dox/modules.dox b/doc/dox/modules.dox
index cef6100b..4e935819 100644
--- a/doc/dox/modules.dox
+++ b/doc/dox/modules.dox
@@ -86,6 +86,10 @@ List of known modules:
 - \subpage page_module_rtp_source
 - \subpage page_module_rtp_session
 - \subpage page_module_rt
+- \subpage page_module_spa_node
+- \subpage page_module_spa_node_factory
+- \subpage page_module_spa_device
+- \subpage page_module_spa_device_factory
 - \subpage page_module_session_manager
 - \subpage page_module_snapcast_discover
 - \subpage page_module_vban_recv
diff --git a/doc/dox/programs/pipewire.1.md b/doc/dox/programs/pipewire.1.md
index aaf089e4..43f03e3f 100644
--- a/doc/dox/programs/pipewire.1.md
+++ b/doc/dox/programs/pipewire.1.md
@@ -154,6 +154,10 @@ systemd.
 @PAR@ pipewire-env PIPEWIRE_LOG_LINE
 Enables the logging of line numbers. Default true.
 
+@PAR@ pipewire-env PIPEWIRE_LOG_TIMESTAMP
+Logging timestamp type: "local", "monotonic", "realtime", "none".
+Default "local".
+
 @PAR@ pipewire-env PIPEWIRE_LOG
 Specifies a log file to use instead of the default logger.
 
diff --git a/doc/dox/programs/pw-loopback.1.md b/doc/dox/programs/pw-loopback.1.md
index 98293430..dea34339 100644
--- a/doc/dox/programs/pw-loopback.1.md
+++ b/doc/dox/programs/pw-loopback.1.md
@@ -45,10 +45,10 @@ Target device to capture from
 \par -P | \--playback=TARGET
 Target device to play to
 
-\par \--capture-props=PROPS
+\par -i | \--capture-props=PROPS
 Wanted properties of capture node (in JSON)
 
-\par \--playback-props=PROPS
+\par -o | \--playback-props=PROPS
 Wanted properties of capture node (in JSON)
 
 # AUTHORS
diff --git a/doc/dox/programs/spa-acp-tool.1.md b/doc/dox/programs/spa-acp-tool.1.md
index b77f144d..402a606f 100644
--- a/doc/dox/programs/spa-acp-tool.1.md
+++ b/doc/dox/programs/spa-acp-tool.1.md
@@ -4,7 +4,7 @@ The PipeWire ALSA profile debugging utility
 
 # SYNOPSIS
 
-**spa-acp-tool** \[*COMMAND*\]
+**spa-acp-tool** \[*OPTIONS*\] \[*COMMAND*\]
 
 # DESCRIPTION
 
@@ -14,6 +14,20 @@ running PipeWire.
 May be used to debug problems where PipeWire has incorrectly
 functioning ALSA card profiles.
 
+# OPTIONS
+
+\par -h | \--help
+Show help
+
+\par -v | \--verbose
+Increase verbosity by one level
+
+\par -c NUMBER | \--card NUMBER
+Select which card to probe
+
+\par -p | \--properties
+Additional properties to pass to ACP, e.g. `key=value ...`.
+
 # COMMANDS
 
 \par help | h
diff --git a/meson.build b/meson.build
index be8ce5d1..3dfdc184 100644
--- a/meson.build
+++ b/meson.build
@@ -1,5 +1,5 @@
 project('pipewire', ['c' ],
-  version : '1.2.7',
+  version : '1.4.0',
   license : [ 'MIT', 'LGPL-2.1-or-later', 'GPL-2.0-only' ],
   meson_version : '>= 0.61.1',
   default_options : [ 'warning_level=3',
@@ -33,6 +33,9 @@ jack_version_minor = libversion_minor
 # libjack[server] version has 0 for major (for compatibility with other implementations),
 # 3 for minor, and "1000*major + 100*minor + micro" as micro version (the minor libpipewire soversion number)
 libjackversion = '@0@.@1@.@2@'.format(soversion, jack_version_major, jack_version_minor)
+# jack[server] version has 3 for major
+# and pipewire's "1000*major + 100*minor + micro" as minor version
+jackversion = '@0@.@1@.@2@'.format(jack_version_major, jack_version_minor, 0)
 
 pipewire_name = 'pipewire-@0@'.format(apiversion)
 spa_name = 'spa-@0@'.format(spaversion)
@@ -50,6 +53,7 @@ pipewire_confdatadir = pipewire_datadir / 'pipewire'
 modules_install_dir = pipewire_libdir / pipewire_name
 
 cc = meson.get_compiler('c')
+cc_native = meson.get_compiler('c', native: true)
 
 if cc.has_header('features.h') and cc.get_define('__GLIBC__', prefix: '#include <features.h>') != ''
   # glibc ld.so interprets ${LIB} in a library loading path with an
@@ -81,7 +85,8 @@ common_flags = [
   '-Wsign-compare',
   '-Wpointer-arith',
   '-Wpointer-sign',
-  '-Wformat',
+  '-Werror=format',
+  '-Wno-error=format-overflow', # avoid some "‘%s’ directive argument is null"
   '-Wformat-security',
   '-Wimplicit-fallthrough',
   '-Wmissing-braces',
@@ -95,6 +100,7 @@ common_flags = [
   '-Wunused-result',
   '-Werror=return-type',
   '-Werror=float-conversion',
+  '-Werror=constant-conversion',
 ]
 
 cc_flags = common_flags + [
@@ -111,6 +117,8 @@ cc_flags = common_flags + [
 ]
 add_project_arguments(cc.get_supported_arguments(cc_flags), language: 'c')
 
+cc_flags_native = cc_native.get_supported_arguments(cc_flags)
+
 have_cpp = add_languages('cpp', native: false, required : false)
 
 if have_cpp
@@ -170,6 +178,20 @@ elif cc.has_argument('-mfpu=neon')
   endif
 endif
 
+have_rvv = false
+if host_machine.cpu_family() == 'riscv64'
+  if cc.compiles('''
+      int main() {
+          __asm__ __volatile__ (
+            ".option arch, +v\nvsetivli zero, 0, e8, m1, ta, ma"
+          );
+      }
+    ''',
+    name : 'riscv64 V Support')
+      have_rvv = true
+  endif
+endif
+
 libatomic = cc.find_library('atomic', required : false)
 
 test_8_byte_atomic = '''
@@ -230,6 +252,7 @@ if host_machine.endian() == 'big'
 endif
 
 check_headers = [
+  ['sys/auxv.h', 'HAVE_SYS_AUXV_H'],
   ['sys/mount.h', 'HAVE_SYS_MOUNT_H'],
   ['sys/param.h', 'HAVE_SYS_PARAM_H'],
   ['sys/random.h', 'HAVE_SYS_RANDOM_H'],
@@ -279,6 +302,7 @@ configure_file(input : 'Makefile.in',
 
 # Find dependencies
 mathlib = cc.find_library('m', required : false)
+mathlib_native = cc_native.find_library('m', required : false)
 rt_lib = cc.find_library('rt', required : false) # clock_gettime
 dl_lib = cc.find_library('dl', required : false)
 pthread_lib = dependency('threads')
@@ -288,6 +312,9 @@ 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)
+fftw_dep = dependency('fftw3f', required : false)
+summary({'fftw3f (filter-chain convolver)': fftw_dep.found()}, bool_yn: true, section: 'Misc dependencies')
+cdata.set('HAVE_FFTW', fftw_dep.found())
 
 if get_option('readline').disabled()
   readline_dep = dependency('', required: false)
@@ -306,7 +333,8 @@ ffmpeg = get_option('ffmpeg')
 if pw_cat_ffmpeg.allowed() or ffmpeg.allowed()
   avcodec_dep = dependency('libavcodec', required: pw_cat_ffmpeg.enabled() or ffmpeg.enabled())
   avformat_dep = dependency('libavformat', required: pw_cat_ffmpeg.enabled())
-  avutil_dep = dependency('libavutil', required: pw_cat_ffmpeg.enabled())
+  avutil_dep = dependency('libavutil', required: pw_cat_ffmpeg.enabled() or ffmpeg.enabled())
+  swscale_dep = dependency('libswscale', required: pw_cat_ffmpeg.enabled() or ffmpeg.enabled())
 else
   avcodec_dep = dependency('', required: false)
 endif
@@ -322,8 +350,6 @@ ncurses_dep = dependency('ncursesw', required : false)
 sndfile_dep = dependency('sndfile', version : '>= 1.0.20', required : get_option('sndfile'))
 summary({'sndfile': sndfile_dep.found()}, bool_yn: true, section: 'pw-cat/pw-play/pw-dump/filter-chain')
 cdata.set('HAVE_SNDFILE', sndfile_dep.found())
-libmysofa_dep = dependency('libmysofa', required : get_option('libmysofa'))
-summary({'libmysofa': libmysofa_dep.found()}, bool_yn: true, section: 'filter-chain')
 pulseaudio_dep = dependency('libpulse', required : get_option('libpulse'))
 summary({'libpulse': pulseaudio_dep.found()}, bool_yn: true, section: 'Streaming between daemons')
 avahi_dep = dependency('avahi-client', required : get_option('avahi'))
@@ -404,20 +430,37 @@ cdata.set('HAVE_GSTREAMER_DMA_DRM', gst_dma_drm_found)
 
 if get_option('echo-cancel-webrtc').disabled()
   webrtc_dep = dependency('', required: false)
-  summary({'WebRTC Echo Canceling >= 1.2': webrtc_dep.found()}, bool_yn: true, section: 'Misc dependencies')
+  summary({'WebRTC Echo Canceling': webrtc_dep.found()}, bool_yn: false, section: 'Misc dependencies')
 else
-  webrtc_dep = dependency('webrtc-audio-processing-1',
-    version : ['>= 1.2' ],
+  webrtc_dep = dependency('webrtc-audio-processing-2',
+    version : ['>= 2.0' ],
     required : false)
-  cdata.set('HAVE_WEBRTC1', webrtc_dep.found())
+  cdata.set('HAVE_WEBRTC2', webrtc_dep.found())
   if webrtc_dep.found()
-    summary({'WebRTC Echo Canceling >= 1.2': webrtc_dep.found()}, bool_yn: true, section: 'Misc dependencies')
+    summary({'WebRTC Echo Canceling >= 2.0': webrtc_dep.found()}, bool_yn: true, section: 'Misc dependencies')
   else
-    webrtc_dep = dependency('webrtc-audio-processing',
-      version : ['>= 0.2', '< 1.0'],
-      required : get_option('echo-cancel-webrtc'))
-    cdata.set('HAVE_WEBRTC', webrtc_dep.found())
-    summary({'WebRTC Echo Canceling < 1.0': webrtc_dep.found()}, bool_yn: true, section: 'Misc dependencies')
+    webrtc_dep = dependency('webrtc-audio-processing-1',
+      version : ['>= 1.2' ],
+      required : false)
+    cdata.set('HAVE_WEBRTC1', webrtc_dep.found())
+    if webrtc_dep.found()
+      summary({'WebRTC Echo Canceling >= 1.2': webrtc_dep.found()}, bool_yn: true, section: 'Misc dependencies')
+    else
+      webrtc_dep = dependency('webrtc-audio-processing',
+        version : ['>= 0.2', '< 1.0'],
+        required : false)
+      cdata.set('HAVE_WEBRTC', webrtc_dep.found())
+      if webrtc_dep.found()
+        summary({'WebRTC Echo Canceling < 1.0': webrtc_dep.found()}, bool_yn: true, section: 'Misc dependencies')
+      else
+        # If deps are not found on the system but it's enabled, try to fallback to the subproject
+        webrtc_dep = dependency('webrtc-audio-processing-2',
+          version : ['>= 2.0' ],
+          required : get_option('echo-cancel-webrtc'))
+        cdata.set('HAVE_WEBRTC2', webrtc_dep.found())
+        summary({'WebRTC Echo Canceling > 2.0': webrtc_dep.found()}, bool_yn: true, section: 'Misc dependencies')
+      endif
+    endif
   endif
 endif
 
@@ -438,7 +481,7 @@ endif
 summary({'intl support': libintl_dep.found()}, bool_yn: true)
 
 need_alsa = get_option('pipewire-alsa').enabled() or 'media-session' in get_option('session-managers')
-alsa_dep = dependency('alsa', version : '>=1.1.7', required: need_alsa)
+alsa_dep = dependency('alsa', version : '>=1.2.10', required: need_alsa)
 summary({'pipewire-alsa': alsa_dep.found()}, bool_yn: true)
 
 if host_machine.system() == 'freebsd' or host_machine.system() == 'midnightbsd'
@@ -453,9 +496,6 @@ else
 endif
 summary({'OpenSSL (for raop-sink)': openssl_lib.found()}, bool_yn: true)
 
-lilv_lib = dependency('lilv-0', required: get_option('lv2'))
-summary({'lilv (for lv2 plugins)': lilv_lib.found()}, bool_yn: true)
-
 libffado_dep = dependency('libffado', required: get_option('libffado'))
 summary({'ffado': libffado_dep.found()}, bool_yn: true)
 glib2_snap_dep = dependency('glib-2.0', required : get_option('snap'))
@@ -565,13 +605,12 @@ devenv.set('PIPEWIRE_MODULE_DIR', pipewire_dep.get_variable('moduledir'))
 devenv.set('SPA_PLUGIN_DIR', spa_dep.get_variable('plugindir'))
 devenv.set('SPA_DATA_DIR', spa_dep.get_variable('datadir'))
 
-devenv.set('GST_PLUGIN_PATH', builddir / 'src'/ 'gst')
-
-devenv.set('ALSA_PLUGIN_DIR', builddir / 'pipewire-alsa' / 'alsa-plugins')
 devenv.set('ACP_PATHS_DIR', srcdir / 'spa' / 'plugins' / 'alsa' / 'mixer' / 'paths')
 devenv.set('ACP_PROFILES_DIR', srcdir / 'spa' / 'plugins' / 'alsa' / 'mixer' / 'profile-sets')
 
-devenv.set('LD_LIBRARY_PATH', builddir / 'pipewire-jack' / 'src')
+devenv.prepend('GST_PLUGIN_PATH', builddir / 'src'/ 'gst')
+devenv.prepend('ALSA_PLUGIN_DIR', builddir / 'pipewire-alsa' / 'alsa-plugins')
+devenv.prepend('LD_LIBRARY_PATH', builddir / 'pipewire-jack' / 'src')
 
 devenv.set('PW_UNINSTALLED', '1')
 
diff --git a/meson_options.txt b/meson_options.txt
index 94761f14..dc1b339f 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -93,6 +93,10 @@ option('audioconvert',
        description: 'Enable audioconvert spa plugin integration',
        type: 'feature',
        value: 'enabled')
+option('resampler-precomp-tuples',
+	description: 'Array of "inrate,outrate[,quality]" tuples to precompute resampler coefficients for',
+	type: 'array',
+	value: [ '32000,44100', '32000,48000', '48000,44100', '44100,48000' ])
 option('bluez5',
        description: 'Enable bluez5 spa plugin integration',
        type: 'feature',
@@ -141,6 +145,10 @@ option('bluez5-codec-lc3',
         description: 'Enable LC3 open source codec implementation',
         type: 'feature',
         value: 'auto')
+option('bluez5-codec-g722',
+        description: 'Enable G722 open source codec implementation',
+        type: 'feature',
+        value: 'auto')
 option('control',
        description: 'Enable control spa plugin integration',
        type: 'feature',
@@ -367,3 +375,7 @@ option('doc-sysconfdir-value',
        description : 'Sysconf data directory to show in documentation instead of the actual value.',
        type : 'string',
        value : '')
+option('ebur128',
+       description: 'Enable code that depends on ebur128',
+       type: 'feature',
+       value: 'auto')
diff --git a/pipewire-alsa/alsa-plugins/ctl_pipewire.c b/pipewire-alsa/alsa-plugins/ctl_pipewire.c
index 2a54a0dd..7fcfd572 100644
--- a/pipewire-alsa/alsa-plugins/ctl_pipewire.c
+++ b/pipewire-alsa/alsa-plugins/ctl_pipewire.c
@@ -1019,29 +1019,6 @@ static const struct global_info node_info = {
 };
 
 /** metadata */
-static int json_object_find(const char *obj, const char *key, char *value, size_t len)
-{
-	struct spa_json it[2];
-	const char *v;
-	char k[128];
-
-	spa_json_init(&it[0], obj, strlen(obj));
-	if (spa_json_enter_object(&it[0], &it[1]) <= 0)
-		return -EINVAL;
-
-	while (spa_json_get_string(&it[1], k, sizeof(k)) > 0) {
-		if (spa_streq(k, key)) {
-			if (spa_json_get_string(&it[1], value, len) <= 0)
-				continue;
-			return 0;
-		} else {
-			if (spa_json_next(&it[1], &v) <= 0)
-				break;
-		}
-	}
-	return -ENOENT;
-}
-
 static int metadata_property(void *data,
                         uint32_t subject,
                         const char *key,
@@ -1054,14 +1031,14 @@ static int metadata_property(void *data,
 	if (subject == PW_ID_CORE) {
 		if (key == NULL || spa_streq(key, "default.audio.sink")) {
 			if (value == NULL ||
-			    json_object_find(value, "name",
+			    spa_json_str_object_find(value, strlen(value), "name",
 					ctl->default_sink, sizeof(ctl->default_sink)) < 0)
 				ctl->default_sink[0] = '\0';
 			pw_log_debug("found default sink: %s", ctl->default_sink);
 		}
 		if (key == NULL || spa_streq(key, "default.audio.source")) {
 			if (value == NULL ||
-			    json_object_find(value, "name",
+			    spa_json_str_object_find(value, strlen(value), "name",
 					ctl->default_source, sizeof(ctl->default_source)) < 0)
 				ctl->default_source[0] = '\0';
 			pw_log_debug("found default source: %s", ctl->default_source);
@@ -1367,7 +1344,6 @@ SND_CTL_PLUGIN_DEFINE_FUNC(pipewire)
 	ctl->context = pw_context_new(loop,
 					pw_properties_new(
 						PW_KEY_CLIENT_API, "alsa",
-						PW_KEY_CONFIG_NAME, "client-rt.conf",
 						NULL),
 					0);
 	if (ctl->context == NULL) {
diff --git a/pipewire-alsa/alsa-plugins/pcm_pipewire.c b/pipewire-alsa/alsa-plugins/pcm_pipewire.c
index 4ba25ee6..bd6836e5 100644
--- a/pipewire-alsa/alsa-plugins/pcm_pipewire.c
+++ b/pipewire-alsa/alsa-plugins/pcm_pipewire.c
@@ -5,9 +5,6 @@
 #define __USE_GNU
 
 #include <limits.h>
-#if !defined(__FreeBSD__) && !defined(__MidnightBSD__)
-#include <byteswap.h>
-#endif
 #include <sys/shm.h>
 #include <sys/types.h>
 #include <sys/socket.h>
@@ -20,6 +17,7 @@
 #include <spa/debug/types.h>
 #include <spa/param/props.h>
 #include <spa/utils/atomic.h>
+#include <spa/utils/endian.h>
 #include <spa/utils/result.h>
 #include <spa/utils/string.h>
 #include <spa/utils/json.h>
@@ -83,7 +81,8 @@ typedef struct {
 	int64_t now;
 	uintptr_t seq;
 
-	struct spa_audio_info_raw format;
+	struct spa_audio_info requested;
+	struct spa_audio_info format;
 } snd_pcm_pipewire_t;
 
 static int snd_pcm_pipewire_stop(snd_pcm_ioplug_t *io);
@@ -346,6 +345,25 @@ static void on_stream_param_changed(void *data, uint32_t id, const struct spa_po
 	if (param == NULL || id != SPA_PARAM_Format)
 		return;
 
+	if (spa_format_audio_parse(param, &pw->format) < 0) {
+		pw->error = -EINVAL;
+	} else {
+		switch (pw->format.media_subtype) {
+		case SPA_MEDIA_SUBTYPE_raw:
+			break;
+		case SPA_MEDIA_SUBTYPE_dsd:
+			if (pw->format.info.dsd.interleave != pw->requested.info.dsd.interleave ||
+			    pw->format.info.dsd.bitorder != pw->requested.info.dsd.bitorder) {
+				pw->error = -EINVAL;
+			}
+			break;
+		}
+	}
+	if (pw->error < 0) {
+		pw_thread_loop_signal(pw->main_loop, false);
+		return;
+	}
+
 	io->period_size = pw->min_avail;
 
 	buffers = SPA_CLAMP(io->buffer_size / io->period_size, MIN_BUFFERS, MAX_BUFFERS);
@@ -373,7 +391,7 @@ static void on_stream_state_changed(void *data, enum pw_stream_state old, enum p
 
 	if (state == PW_STREAM_STATE_ERROR) {
 		pw_log_warn("%s", error);
-		pw->error = -EIO;
+		pw->error = -errno;
 		update_active(&pw->io);
 	}
 }
@@ -527,7 +545,7 @@ static int snd_pcm_pipewire_prepare(snd_pcm_ioplug_t *io)
 	pw_properties_setf(pw->props, PW_KEY_NODE_LATENCY, "%lu/%u", pw->min_avail, io->rate);
 	pw_properties_setf(pw->props, PW_KEY_NODE_RATE, "1/%u", io->rate);
 
-	params[0] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat, &pw->format);
+	params[0] = spa_format_audio_build(&b, SPA_PARAM_EnumFormat, &pw->format);
 
 	if (pw->stream != NULL) {
 		pw_stream_set_active(pw->stream, false);
@@ -624,29 +642,29 @@ static int snd_pcm_pipewire_pause(snd_pcm_ioplug_t * io, int enable)
 #define _FORMAT_BE(p, fmt)  p ? SPA_AUDIO_FORMAT_UNKNOWN : SPA_AUDIO_FORMAT_ ## fmt ## _OE
 #endif
 
-static int set_default_channels(struct spa_audio_info_raw *info)
+static int set_default_channels(uint32_t channels, uint32_t position[SPA_AUDIO_MAX_CHANNELS])
 {
-	switch (info->channels) {
+	switch (channels) {
 	case 8:
-		info->position[6] = SPA_AUDIO_CHANNEL_SL;
-		info->position[7] = SPA_AUDIO_CHANNEL_SR;
+		position[6] = SPA_AUDIO_CHANNEL_SL;
+		position[7] = SPA_AUDIO_CHANNEL_SR;
 		SPA_FALLTHROUGH
 	case 6:
-		info->position[5] = SPA_AUDIO_CHANNEL_LFE;
+		position[5] = SPA_AUDIO_CHANNEL_LFE;
 		SPA_FALLTHROUGH
 	case 5:
-		info->position[4] = SPA_AUDIO_CHANNEL_FC;
+		position[4] = SPA_AUDIO_CHANNEL_FC;
 		SPA_FALLTHROUGH
 	case 4:
-		info->position[2] = SPA_AUDIO_CHANNEL_RL;
-		info->position[3] = SPA_AUDIO_CHANNEL_RR;
+		position[2] = SPA_AUDIO_CHANNEL_RL;
+		position[3] = SPA_AUDIO_CHANNEL_RR;
 		SPA_FALLTHROUGH
 	case 2:
-		info->position[0] = SPA_AUDIO_CHANNEL_FL;
-		info->position[1] = SPA_AUDIO_CHANNEL_FR;
+		position[0] = SPA_AUDIO_CHANNEL_FL;
+		position[1] = SPA_AUDIO_CHANNEL_FR;
 		return 1;
 	case 1:
-		info->position[0] = SPA_AUDIO_CHANNEL_MONO;
+		position[0] = SPA_AUDIO_CHANNEL_MONO;
 		return 1;
 	default:
 		return 0;
@@ -658,6 +676,7 @@ static int snd_pcm_pipewire_hw_params(snd_pcm_ioplug_t * io,
 {
 	snd_pcm_pipewire_t *pw = io->private_data;
 	bool planar;
+	const char *fmt_str = NULL;
 
 	snd_pcm_hw_params_dump(params, pw->output);
 	fflush(pw->log_file);
@@ -678,48 +697,98 @@ static int snd_pcm_pipewire_hw_params(snd_pcm_ioplug_t * io,
 		return -EINVAL;
 	}
 
+	pw->requested.media_type = SPA_MEDIA_TYPE_audio;
 	switch(io->format) {
 	case SND_PCM_FORMAT_U8:
-		pw->format.format = planar ? SPA_AUDIO_FORMAT_U8P : SPA_AUDIO_FORMAT_U8;
+		pw->requested.media_subtype = SPA_MEDIA_SUBTYPE_raw;
+		pw->requested.info.raw.format = planar ? SPA_AUDIO_FORMAT_U8P : SPA_AUDIO_FORMAT_U8;
 		break;
 	case SND_PCM_FORMAT_S16_LE:
-		pw->format.format = _FORMAT_LE(planar, S16);
+		pw->requested.media_subtype = SPA_MEDIA_SUBTYPE_raw;
+		pw->requested.info.raw.format = _FORMAT_LE(planar, S16);
 		break;
 	case SND_PCM_FORMAT_S16_BE:
-		pw->format.format = _FORMAT_BE(planar, S16);
+		pw->requested.media_subtype = SPA_MEDIA_SUBTYPE_raw;
+		pw->requested.info.raw.format = _FORMAT_BE(planar, S16);
 		break;
 	case SND_PCM_FORMAT_S24_LE:
-		pw->format.format = _FORMAT_LE(planar, S24_32);
+		pw->requested.media_subtype = SPA_MEDIA_SUBTYPE_raw;
+		pw->requested.info.raw.format = _FORMAT_LE(planar, S24_32);
 		break;
 	case SND_PCM_FORMAT_S24_BE:
-		pw->format.format = _FORMAT_BE(planar, S24_32);
+		pw->requested.media_subtype = SPA_MEDIA_SUBTYPE_raw;
+		pw->requested.info.raw.format = _FORMAT_BE(planar, S24_32);
 		break;
 	case SND_PCM_FORMAT_S32_LE:
-		pw->format.format = _FORMAT_LE(planar, S32);
+		pw->requested.media_subtype = SPA_MEDIA_SUBTYPE_raw;
+		pw->requested.info.raw.format = _FORMAT_LE(planar, S32);
 		break;
 	case SND_PCM_FORMAT_S32_BE:
-		pw->format.format = _FORMAT_BE(planar, S32);
+		pw->requested.media_subtype = SPA_MEDIA_SUBTYPE_raw;
+		pw->requested.info.raw.format = _FORMAT_BE(planar, S32);
 		break;
 	case SND_PCM_FORMAT_S24_3LE:
-		pw->format.format = _FORMAT_LE(planar, S24);
+		pw->requested.media_subtype = SPA_MEDIA_SUBTYPE_raw;
+		pw->requested.info.raw.format = _FORMAT_LE(planar, S24);
 		break;
 	case SND_PCM_FORMAT_S24_3BE:
-		pw->format.format = _FORMAT_BE(planar, S24);
+		pw->requested.media_subtype = SPA_MEDIA_SUBTYPE_raw;
+		pw->requested.info.raw.format = _FORMAT_BE(planar, S24);
 		break;
 	case SND_PCM_FORMAT_FLOAT_LE:
-		pw->format.format = _FORMAT_LE(planar, F32);
+		pw->requested.media_subtype = SPA_MEDIA_SUBTYPE_raw;
+		pw->requested.info.raw.format = _FORMAT_LE(planar, F32);
 		break;
 	case SND_PCM_FORMAT_FLOAT_BE:
-		pw->format.format = _FORMAT_BE(planar, F32);
+		pw->requested.media_subtype = SPA_MEDIA_SUBTYPE_raw;
+		pw->requested.info.raw.format = _FORMAT_BE(planar, F32);
+		break;
+	case SND_PCM_FORMAT_DSD_U32_BE:
+		pw->requested.media_subtype = SPA_MEDIA_SUBTYPE_dsd;
+		pw->requested.info.dsd.interleave = 4;
+		break;
+	case SND_PCM_FORMAT_DSD_U32_LE:
+		pw->requested.media_subtype = SPA_MEDIA_SUBTYPE_dsd;
+		pw->requested.info.dsd.interleave = -4;
+		break;
+	case SND_PCM_FORMAT_DSD_U16_BE:
+		pw->requested.media_subtype = SPA_MEDIA_SUBTYPE_dsd;
+		pw->requested.info.dsd.interleave = 2;
+		break;
+	case SND_PCM_FORMAT_DSD_U16_LE:
+		pw->requested.media_subtype = SPA_MEDIA_SUBTYPE_dsd;
+		pw->requested.info.dsd.interleave = -2;
+		break;
+	case SND_PCM_FORMAT_DSD_U8:
+		pw->requested.media_subtype = SPA_MEDIA_SUBTYPE_dsd;
+		pw->requested.info.dsd.interleave = 1;
 		break;
 	default:
 		SNDERR("PipeWire: invalid format: %d\n", io->format);
 		return -EINVAL;
 	}
-	pw->format.channels = io->channels;
-	pw->format.rate = io->rate;
-
-	set_default_channels(&pw->format);
+	switch (pw->requested.media_subtype) {
+	case SPA_MEDIA_SUBTYPE_raw:
+		pw->requested.info.raw.channels = io->channels;
+		pw->requested.info.raw.rate = io->rate;
+		set_default_channels(io->channels, pw->requested.info.raw.position);
+		fmt_str = spa_type_audio_format_to_short_name(pw->requested.info.raw.format);
+		pw->format = pw->requested;
+		break;
+	case SPA_MEDIA_SUBTYPE_dsd:
+		pw->requested.info.dsd.bitorder = SPA_PARAM_BITORDER_msb;
+		pw->requested.info.dsd.channels = io->channels;
+		pw->requested.info.dsd.rate = io->rate * SPA_ABS(pw->requested.info.dsd.interleave);
+		set_default_channels(io->channels, pw->requested.info.dsd.position);
+		pw->format = pw->requested;
+		/* we need to let the server decide these values */
+		pw->format.info.dsd.bitorder = 0;
+		pw->format.info.dsd.interleave = 0;
+		fmt_str = "DSD";
+		break;
+	default:
+		return -EIO;
+	}
 
 	pw->sample_bits = snd_pcm_format_physical_width(io->format);
 	if (planar) {
@@ -730,8 +799,7 @@ static int snd_pcm_pipewire_hw_params(snd_pcm_ioplug_t * io,
 		pw->stride = (io->channels * pw->sample_bits) / 8;
 	}
 	pw->hw_params_changed = true;
-	pw_log_info("%p: format:%s channels:%d rate:%d stride:%d blocks:%d", pw,
-			spa_debug_type_find_name(spa_type_audio_format, pw->format.format),
+	pw_log_info("%p: format:%s channels:%d rate:%d stride:%d blocks:%d", pw, fmt_str,
 			io->channels, io->rate, pw->stride, pw->blocks);
 
 	return 0;
@@ -833,14 +901,26 @@ static int snd_pcm_pipewire_set_chmap(snd_pcm_ioplug_t * io,
 {
 	snd_pcm_pipewire_t *pw = io->private_data;
 	unsigned int i;
+	uint32_t *position;
 
-	pw->format.channels = map->channels;
+	switch (pw->requested.media_subtype) {
+	case SPA_MEDIA_SUBTYPE_raw:
+		pw->requested.info.raw.channels = map->channels;
+		position = pw->requested.info.raw.position;
+		break;
+	case SPA_MEDIA_SUBTYPE_dsd:
+		pw->requested.info.dsd.channels = map->channels;
+		position = pw->requested.info.dsd.position;
+		break;
+	default:
+		return -EINVAL;
+	}
 	for (i = 0; i < map->channels; i++) {
-		pw->format.position[i] = chmap_to_channel(map->pos[i]);
+		position[i] = chmap_to_channel(map->pos[i]);
 		pw_log_debug("map %d: %s / %s", i,
 				snd_pcm_chmap_name(map->pos[i]),
 				spa_debug_type_find_short_name(spa_type_audio_channel,
-					pw->format.position[i]));
+					position[i]));
 	}
 	return 1;
 }
@@ -849,13 +929,26 @@ static snd_pcm_chmap_t * snd_pcm_pipewire_get_chmap(snd_pcm_ioplug_t * io)
 {
 	snd_pcm_pipewire_t *pw = io->private_data;
 	snd_pcm_chmap_t *map;
-	uint32_t i;
+	uint32_t i, channels, *position;
+
+	switch (pw->requested.media_subtype) {
+	case SPA_MEDIA_SUBTYPE_raw:
+		channels = pw->requested.info.raw.channels;
+		position = pw->requested.info.raw.position;
+		break;
+	case SPA_MEDIA_SUBTYPE_dsd:
+		channels = pw->requested.info.dsd.channels;
+		position = pw->requested.info.dsd.position;
+		break;
+	default:
+		return NULL;
+	}
 
 	map = calloc(1, sizeof(snd_pcm_chmap_t) +
-				 pw->format.channels * sizeof(unsigned int));
-	map->channels = pw->format.channels;
-	for (i = 0; i < pw->format.channels; i++)
-		map->pos[i] = channel_to_chmap(pw->format.position[i]);
+				 channels * sizeof(unsigned int));
+	map->channels = channels;
+	for (i = 0; i < channels; i++)
+		map->pos[i] = channel_to_chmap(position[i]);
 
 	return map;
 }
@@ -991,7 +1084,16 @@ struct param_info infos[] = {
 			SND_PCM_FORMAT_S24_3BE,
 			SND_PCM_FORMAT_S16_BE,
 #endif
-			SND_PCM_FORMAT_U8 }, 7, collect_format },
+			SND_PCM_FORMAT_U8,
+			/* we don't add DSD formats here, use alsa.formats to
+			 * force this. Because we can't convert to/from DSD, enabling this
+			 * might fail when the system has no native DSD
+			 * SND_PCM_FORMAT_DSD_U32_BE,
+			 * SND_PCM_FORMAT_DSD_U32_LE,
+			 * SND_PCM_FORMAT_DSD_U16_BE,
+			 * SND_PCM_FORMAT_DSD_U16_LE,
+			 * SND_PCM_FORMAT_DSD_U8 */
+		}, 7, collect_format },
 	{ "alsa.rate", SND_PCM_IOPLUG_HW_RATE, TYPE_MIN_MAX,
 		{ 1, MAX_RATE }, 2, collect_int },
 	{ "alsa.channels", SND_PCM_IOPLUG_HW_CHANNELS, TYPE_MIN_MAX,
@@ -1020,8 +1122,7 @@ static int parse_value(const char *str, struct param_info *info)
 	const char *val;
 	int len;
 
-	spa_json_init(&it[0], str, strlen(str));
-	if ((len = spa_json_next(&it[0], &val)) <= 0)
+	if ((len = spa_json_begin(&it[0], str, strlen(str), &val)) <= 0)
 		return -EINVAL;
 
 	if (spa_json_is_array(val, len)) {
@@ -1039,9 +1140,7 @@ static int parse_value(const char *str, struct param_info *info)
 		info->type = TYPE_MIN_MAX;
 		info->n_vals = 2;
 		spa_json_enter(&it[0], &it[1]);
-                while (spa_json_get_string(&it[1], key, sizeof(key)) > 0) {
-                        if ((len = spa_json_next(&it[1], &val)) <= 0)
-                                break;
+                while ((len = spa_json_object_next(&it[1], key, sizeof(key), &val)) > 0) {
 			if (info->collect(val, len, &v) < 0)
 				continue;
 			if (spa_streq(key, "min"))
@@ -1189,7 +1288,6 @@ static int snd_pcm_pipewire_open(snd_pcm_t **pcmp,
 	pw->system = loop->system;
 	if ((pw->context = pw_context_new(loop,
 					pw_properties_new(
-						PW_KEY_CONFIG_NAME, "client-rt.conf",
 						PW_KEY_CLIENT_API, "alsa",
 						NULL),
 					0)) == NULL) {
diff --git a/pipewire-jack/examples/ump-source.c b/pipewire-jack/examples/ump-source.c
new file mode 100644
index 00000000..274d7248
--- /dev/null
+++ b/pipewire-jack/examples/ump-source.c
@@ -0,0 +1,114 @@
+/* PipeWire */
+/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */
+/* SPDX-License-Identifier: MIT */
+
+#include <stdio.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <sys/mman.h>
+
+#include <jack/jack.h>
+#include <jack/midiport.h>
+
+#include <pipewire-jack-extensions.h>
+
+#define MAX_BUFFERS	64
+
+struct data {
+	const char *path;
+
+	jack_client_t *client;
+	const char *client_name;
+	jack_port_t *out_port;
+
+	int cycle;
+	uint64_t position;
+	uint64_t next_sample;
+	uint64_t period;
+};
+
+static int
+process (jack_nframes_t nframes, void *arg)
+{
+        struct data *d = (struct data*)arg;
+	void *buf;
+	uint32_t event[2];
+
+	buf = jack_port_get_buffer (d->out_port, nframes);
+	jack_midi_clear_buffer(buf);
+
+	while (d->position >= d->next_sample && d->position + nframes > d->next_sample) {
+		uint64_t pos = d->position - d->next_sample;
+
+		if (d->cycle == 0) {
+			/* MIDI 2.0 note on, channel 0, middle C, max velocity, no attribute */
+			event[0] = 0x40903c00;
+			event[1] = 0xffff0000;
+		} else {
+			/* MIDI 2.0 note off, channel 0, middle C, max velocity, no attribute */
+			event[0] = 0x40803c00;
+			event[1] = 0xffff0000;
+		}
+
+		d->cycle ^= 1;
+
+		jack_midi_event_write(buf, pos, (const jack_midi_data_t *) event, sizeof(event));
+
+		d->next_sample += d->period;
+	}
+	d->position += nframes;
+	return 0;
+}
+
+int main(int argc, char *argv[])
+{
+	struct data data = { 0, };
+	jack_options_t options = JackNullOption;
+        jack_status_t status;
+
+	data.client = jack_client_open ("ump-source", options, &status);
+        if (data.client == NULL) {
+                fprintf (stderr, "jack_client_open() failed, "
+                         "status = 0x%2.0x\n", status);
+                if (status & JackServerFailed) {
+                        fprintf (stderr, "Unable to connect to JACK server\n");
+                }
+                exit (1);
+        }
+        if (status & JackServerStarted) {
+                fprintf (stderr, "JACK server started\n");
+        }
+        if (status & JackNameNotUnique) {
+                data.client_name = jack_get_client_name(data.client);
+                fprintf (stderr, "unique name `%s' assigned\n", data.client_name);
+        }
+
+	/* send 2 events per second */
+	data.period = jack_get_sample_rate(data.client) / 2;
+
+        jack_set_process_callback (data.client, process, &data);
+
+	/* the UMP port type allows both sending and receiving of UMP
+	 * messages, which can contain MIDI 1.0 and MIDI 2.0 messages. */
+	data.out_port = jack_port_register (data.client, "output",
+                                          JACK_DEFAULT_MIDI_TYPE,
+                                          JackPortIsOutput | JackPortIsMIDI2, 0);
+
+       if (data.out_port == NULL) {
+                fprintf(stderr, "no more JACK ports available\n");
+                exit (1);
+        }
+
+        if (jack_activate (data.client)) {
+                fprintf (stderr, "cannot activate client");
+                exit (1);
+        }
+
+	while (1) {
+		sleep (1);
+	}
+
+        jack_client_close (data.client);
+
+	return 0;
+}
diff --git a/pipewire-jack/examples/video-dsp-play.c b/pipewire-jack/examples/video-dsp-play.c
index 52891115..5bca72a4 100644
--- a/pipewire-jack/examples/video-dsp-play.c
+++ b/pipewire-jack/examples/video-dsp-play.c
@@ -2,7 +2,9 @@
 /* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */
 /* SPDX-License-Identifier: MIT */
 
+#include <math.h>
 #include <stdio.h>
+#include <string.h>
 #include <unistd.h>
 #include <sys/mman.h>
 
diff --git a/pipewire-jack/jack/types.h b/pipewire-jack/jack/types.h
index 00606438..206d7270 100644
--- a/pipewire-jack/jack/types.h
+++ b/pipewire-jack/jack/types.h
@@ -515,6 +515,29 @@ enum JackPortFlags {
      */
     JackPortIsTerminal = 0x10,
 
+    /**
+     * if JackPortIsCV is set, then the port buffer represents audio-rate
+     * control data rather than audio.
+     *
+     * Clients SHOULD prevent connections between Audio and CV ports.
+     *
+     * To make the ports more meaningful, clients can add meta-data to them.
+     * It is recommended to set these 2 in particular:
+     *  - http://lv2plug.in/ns/lv2core#minimum
+     *  - http://lv2plug.in/ns/lv2core#maximum
+     */
+    JackPortIsCV = 0x20,
+
+    /**
+     * if JackPortIsMIDI2 is set, then the port expects to receive MIDI2 data.
+     *
+     * JACK will automatically convert MIDI1 data into MIDI2 for this port.
+     *
+     * for ports without this flag JACK will convert MIDI2 into MIDI1
+     * as much possible, some events might be skipped.
+     */
+    JackPortIsMIDI2 = 0x20,
+
 };
 
 /**
diff --git a/pipewire-jack/src/control.c b/pipewire-jack/src/control.c
index f86b0b55..8299e8a0 100644
--- a/pipewire-jack/src/control.c
+++ b/pipewire-jack/src/control.c
@@ -124,7 +124,7 @@ bool jackctl_server_stop(jackctl_server_t * server)
 {
 	// stub
 	pw_log_warn("%p: not implemented", server);
-	return false;
+	return true;
 }
 
 SPA_EXPORT
@@ -132,7 +132,7 @@ bool jackctl_server_close(jackctl_server_t * server)
 {
 	// stub
 	pw_log_warn("%p: not implemented", server);
-	return false;
+	return true;
 }
 
 SPA_EXPORT
diff --git a/pipewire-jack/src/meson.build b/pipewire-jack/src/meson.build
index def7746a..0630d96a 100644
--- a/pipewire-jack/src/meson.build
+++ b/pipewire-jack/src/meson.build
@@ -88,9 +88,16 @@ if get_option('jack-devel') == true
   libraries : [pipewire_jack],
   name : 'jack',
   description : 'PipeWire JACK API',
-  version : '1.9.17',
+  version : jackversion,
   extra_cflags : '-D_REENTRANT',
   unescaped_variables: ['server_libs=-L${libdir} -ljackserver', 'jack_implementation=pipewire'])
+
+  pkgconfig.generate(filebase : 'jackserver',
+  libraries : [pipewire_jackserver],
+  name : 'jackserver',
+  description : 'PipeWire JACK Control API',
+  version : jackversion,
+  unescaped_variables: ['jack_implementation=pipewire'])
 endif
 
 if sdl_dep.found()
@@ -103,3 +110,11 @@ if sdl_dep.found()
     link_with: pipewire_jack,
   )
 endif
+executable('ump-source',
+  '../examples/ump-source.c',
+  include_directories : [jack_inc],
+  install : installed_tests_enabled,
+  install_dir : installed_tests_execdir / 'examples' / 'jack',
+  dependencies : [mathlib],
+  link_with: pipewire_jack,
+)
diff --git a/pipewire-jack/src/pipewire-jack-extensions.h b/pipewire-jack/src/pipewire-jack-extensions.h
index 0caec40c..dc3b9f94 100644
--- a/pipewire-jack/src/pipewire-jack-extensions.h
+++ b/pipewire-jack/src/pipewire-jack-extensions.h
@@ -25,6 +25,13 @@ int jack_get_video_image_size(jack_client_t *client, jack_image_size_t *size);
 
 int jack_set_sample_rate (jack_client_t *client, jack_nframes_t nframes);
 
+/* raw OSC message */
+#define JACK_DEFAULT_OSC_TYPE "8 bit raw OSC"
+
+/* MIDI 2.0 UMP type. This contains raw UMP data, which can have MIDI 1.0 or
+ * MIDI 2.0 packets. The data is an array of 32 bit ints. */
+#define JACK_DEFAULT_UMP_TYPE "32 bit raw UMP"
+
 #ifdef __cplusplus
 }
 #endif
diff --git a/pipewire-jack/src/pipewire-jack.c b/pipewire-jack/src/pipewire-jack.c
index 55c49425..839a9fc2 100644
--- a/pipewire-jack/src/pipewire-jack.c
+++ b/pipewire-jack/src/pipewire-jack.c
@@ -1,5 +1,6 @@
 /* PipeWire */
 /* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */
+/* SPDX-FileCopyrightText: Copyright © 2024 Nedko Arnaudov */
 /* SPDX-License-Identifier: MIT */
 
 #include "config.h"
@@ -29,6 +30,7 @@
 #include <spa/utils/result.h>
 #include <spa/utils/string.h>
 #include <spa/utils/ringbuffer.h>
+#include <spa/control/ump-utils.h>
 
 #include <pipewire/pipewire.h>
 #include <pipewire/private.h>
@@ -39,8 +41,6 @@
 #include "pipewire/extensions/metadata.h"
 #include "pipewire-jack-extensions.h"
 
-#define JACK_DEFAULT_VIDEO_TYPE	"32 bit float RGBA video"
-
 /* use 512KB stack per thread - the default is way too high to be feasible
  * with mlockall() on many systems */
 #define THREAD_STACK 524288
@@ -65,9 +65,16 @@ PW_LOG_TOPIC_STATIC(jack_log_topic, "jack");
 #define PW_LOG_TOPIC_DEFAULT jack_log_topic
 
 #define TYPE_ID_AUDIO	0
-#define TYPE_ID_MIDI	1
-#define TYPE_ID_VIDEO	2
-#define TYPE_ID_OTHER	3
+#define TYPE_ID_VIDEO	1
+#define TYPE_ID_MIDI	2
+#define TYPE_ID_OSC	3
+#define TYPE_ID_UMP	4
+#define TYPE_ID_OTHER	5
+
+#define TYPE_ID_IS_EVENT(t)	((t) >= TYPE_ID_MIDI && (t) <= TYPE_ID_UMP)
+#define TYPE_ID_CAN_OSC(t)	((t) == TYPE_ID_MIDI || (t) == TYPE_ID_OSC)
+#define TYPE_ID_IS_HIDDEN(t)	((t) >= TYPE_ID_OTHER)
+#define TYPE_ID_IS_COMPATIBLE(a,b)(((a) == (b)) || (TYPE_ID_IS_EVENT(a) && TYPE_ID_IS_EVENT(b)))
 
 #define SELF_CONNECT_ALLOW	0
 #define SELF_CONNECT_FAIL_EXT	-1
@@ -134,11 +141,16 @@ struct object {
 #define INTERFACE_Port		1
 #define INTERFACE_Node		2
 #define INTERFACE_Link		3
+#define INTERFACE_Client	4
 	uint32_t type;
 	uint32_t id;
 	uint32_t serial;
 
 	union {
+		struct {
+			char name[1024];
+			int32_t pid;
+		} pwclient;
 		struct {
 			char name[JACK_CLIENT_NAME_SIZE+1];
 			char node_name[512];
@@ -460,6 +472,7 @@ struct client {
 	unsigned int fill_aliases:1;
 	unsigned int writable_input:1;
 	unsigned int async:1;
+	unsigned int flag_midi2:1;
 
 	uint32_t max_frames;
 	uint32_t max_align;
@@ -647,7 +660,7 @@ static struct mix *find_port_peer(struct port *port, uint32_t peer_id)
 {
 	struct mix *mix;
 	spa_list_for_each(mix, &port->mix, port_link) {
-		pw_log_info("%p %d %d", port, mix->peer_id, peer_id);
+		pw_log_trace("%p %d %d", port, mix->peer_id, peer_id);
 		if (mix->peer_id == peer_id)
 			return mix;
 	}
@@ -884,6 +897,11 @@ static struct object *find_type(struct client *c, uint32_t id, uint32_t type, bo
 	return NULL;
 }
 
+static struct object *find_client(struct client *c, uint32_t client_id)
+{
+	return find_type(c, client_id, INTERFACE_Client, false);
+}
+
 static struct object *find_link(struct client *c, uint32_t src, uint32_t dst)
 {
 	struct object *l;
@@ -943,11 +961,11 @@ void jack_get_version(int *major_ptr, int *minor_ptr, int *micro_ptr, int *proto
 	if (major_ptr)
 		*major_ptr = 3;
 	if (minor_ptr)
-		*minor_ptr = 0;
+		*minor_ptr = PW_MAJOR;
 	if (micro_ptr)
-		*micro_ptr = 0;
+		*micro_ptr = PW_MINOR;
 	if (proto_ptr)
-		*proto_ptr = 0;
+		*proto_ptr = PW_MICRO;
 }
 
 #define do_callback_expr(c,expr,callback,do_emit,...)		\
@@ -994,7 +1012,10 @@ const char *
 jack_get_version_string(void)
 {
 	static char name[1024];
-	snprintf(name, sizeof(name), "3.0.0.0 (using PipeWire %s)", pw_get_library_version());
+	int major, minor, micro, proto;
+	jack_get_version(&major, &minor, &micro, &proto);
+	snprintf(name, sizeof(name), "%d.%d.%d.%d (using PipeWire %s)",
+			major, minor, micro, proto, pw_get_library_version());
 	return name;
 }
 
@@ -1078,7 +1099,7 @@ static void on_notify_event(void *data, uint64_t count)
 			do_recompute_capture = do_recompute_playback = true;
 			break;
 		case NOTIFY_TYPE_BUFFER_FRAMES:
-			pw_log_debug("%p: buffer frames %d", c, notify->arg1);
+			pw_log_debug("%p: buffer frames %d -> %d", c, c->buffer_frames, notify->arg1);
 			if (c->buffer_frames != (uint32_t)notify->arg1) {
 				do_callback_expr(c, c->buffer_frames = notify->arg1,
 						bufsize_callback, c->active,
@@ -1087,7 +1108,7 @@ static void on_notify_event(void *data, uint64_t count)
 			}
 			break;
 		case NOTIFY_TYPE_SAMPLE_RATE:
-			pw_log_debug("%p: sample rate %d", c, notify->arg1);
+			pw_log_debug("%p: sample rate %d -> %d", c, c->sample_rate, notify->arg1);
 			if (c->sample_rate != (uint32_t)notify->arg1) {
 				do_callback_expr(c, c->sample_rate = notify->arg1,
 						srate_callback, c->active,
@@ -1392,12 +1413,25 @@ static inline bool is_osc(jack_midi_event_t *ev)
 	return ev->size >= 1 && (ev->buffer[0] == '#' || ev->buffer[0] == '/');
 }
 
-static size_t convert_from_midi(void *midi, void *buffer, size_t size)
+static size_t convert_from_event(void *midi, void *buffer, size_t size, uint32_t type)
 {
 	struct spa_pod_builder b = { 0, };
 	uint32_t i, count;
 	struct spa_pod_frame f;
+	uint32_t event_type;
 
+	switch (type) {
+	case TYPE_ID_MIDI:
+	case TYPE_ID_OSC:
+		/* we handle MIDI as OSC, check below */
+		event_type = SPA_CONTROL_OSC;
+		break;
+	case TYPE_ID_UMP:
+		event_type = SPA_CONTROL_UMP;
+		break;
+	default:
+		return 0;
+	}
 	count = jack_midi_get_event_count(midi);
 
 	spa_pod_builder_init(&b, buffer, size);
@@ -1406,14 +1440,43 @@ static size_t convert_from_midi(void *midi, void *buffer, size_t size)
 	for (i = 0; i < count; i++) {
 		jack_midi_event_t ev;
 		jack_midi_event_get(&ev, midi, i);
-		spa_pod_builder_control(&b, ev.time,
-				is_osc(&ev) ? SPA_CONTROL_OSC : SPA_CONTROL_Midi);
-		spa_pod_builder_bytes(&b, ev.buffer, ev.size);
+
+		if (type != TYPE_ID_MIDI || is_osc(&ev)) {
+			/* no midi port or it's OSC */
+			spa_pod_builder_control(&b, ev.time, event_type);
+			spa_pod_builder_bytes(&b, ev.buffer, ev.size);
+		} else {
+			/* midi port and it's not OSC, convert to UMP */
+			uint8_t *data = ev.buffer;
+			size_t size = ev.size;
+			uint64_t state = 0;
+
+			while (size > 0) {
+				uint32_t ump[4];
+				int ump_size = spa_ump_from_midi(&data, &size,
+						ump, sizeof(ump), 0, &state);
+				if (ump_size <= 0)
+					break;
+				spa_pod_builder_control(&b, ev.time, SPA_CONTROL_UMP);
+				spa_pod_builder_bytes(&b, ump, ump_size);
+			}
+		}
 	}
 	spa_pod_builder_pop(&b, &f);
 	return b.state.offset;
 }
 
+static inline int event_compare(uint8_t s1, uint8_t s2)
+{
+	/* 11 (controller) > 12 (program change) >
+	 * 8 (note off) > 9 (note on) > 10 (aftertouch) >
+	 * 13 (channel pressure) > 14 (pitch bend) */
+	static int priotab[] = { 5,4,3,7,6,2,1,0 };
+	if ((s1 & 0xf) != (s2 & 0xf))
+		return 0;
+	return priotab[(s2>>4) & 7] - priotab[(s1>>4) & 7];
+}
+
 static inline int event_sort(struct spa_pod_control *a, struct spa_pod_control *b)
 {
 	if (a->offset < b->offset)
@@ -1425,21 +1488,20 @@ static inline int event_sort(struct spa_pod_control *a, struct spa_pod_control *
 	switch(a->type) {
 	case SPA_CONTROL_Midi:
 	{
-		/* 11 (controller) > 12 (program change) >
-		 * 8 (note off) > 9 (note on) > 10 (aftertouch) >
-		 * 13 (channel pressure) > 14 (pitch bend) */
-		static int priotab[] = { 5,4,3,7,6,2,1,0 };
-		uint8_t *da, *db;
-
-		if (SPA_POD_BODY_SIZE(&a->value) < 1 ||
-		    SPA_POD_BODY_SIZE(&b->value) < 1)
+		uint8_t *sa = SPA_POD_BODY(&a->value), *sb = SPA_POD_BODY(&b->value);
+		if (SPA_POD_BODY_SIZE(&a->value) < 1 || SPA_POD_BODY_SIZE(&b->value) < 1)
 			return 0;
-
-		da = SPA_POD_BODY(&a->value);
-		db = SPA_POD_BODY(&b->value);
-		if ((da[0] & 0xf) != (db[0] & 0xf))
+		return event_compare(sa[0], sb[0]);
+	}
+	case SPA_CONTROL_UMP:
+	{
+		uint32_t *sa = SPA_POD_BODY(&a->value), *sb = SPA_POD_BODY(&b->value);
+		if (SPA_POD_BODY_SIZE(&a->value) < 4 || SPA_POD_BODY_SIZE(&b->value) < 4)
 			return 0;
-		return priotab[(db[0]>>4) & 7] - priotab[(da[0]>>4) & 7];
+		if ((sa[0] >> 28) != 2 || (sa[0] >> 28) != 4 ||
+		    (sb[0] >> 28) != 2 || (sb[0] >> 28) != 4)
+			return 0;
+		return event_compare(sa[0] >> 16, sb[0] >> 16);
 	}
 	default:
 		return 0;
@@ -1498,11 +1560,12 @@ static inline int midi_event_write(void *port_buffer,
 	return 0;
 }
 
-static void convert_to_midi(struct spa_pod_sequence **seq, uint32_t n_seq, void *midi, bool fix)
+static void convert_to_event(struct spa_pod_sequence **seq, uint32_t n_seq, void *midi, bool fix, uint32_t type)
 {
 	struct spa_pod_control *c[n_seq];
+	uint64_t state = 0;
 	uint32_t i;
-	int res;
+	int res = 0;
 
 	for (i = 0; i < n_seq; i++)
 		c[i] = spa_pod_control_first(&seq[i]->body);
@@ -1526,16 +1589,51 @@ static void convert_to_midi(struct spa_pod_sequence **seq, uint32_t n_seq, void
 
 		switch(next->type) {
 		case SPA_CONTROL_OSC:
+			if (!TYPE_ID_CAN_OSC(type))
+				break;
+			SPA_FALLTHROUGH;
 		case SPA_CONTROL_Midi:
 		{
 			uint8_t *data = SPA_POD_BODY(&next->value);
 			size_t size = SPA_POD_BODY_SIZE(&next->value);
 
-			if ((res = midi_event_write(midi, next->offset, data, size, fix)) < 0)
+			if (type == TYPE_ID_UMP) {
+				while (size > 0) {
+					uint32_t ump[4];
+					int ump_size = spa_ump_from_midi(&data, &size, ump, sizeof(ump), 0, &state);
+					if (ump_size <= 0)
+						break;
+					if ((res = midi_event_write(midi, next->offset,
+								(uint8_t*)ump, ump_size, false)) < 0)
+						break;
+				}
+			} else {
+				res = midi_event_write(midi, next->offset, data, size, fix);
+			}
+			if (res < 0)
 				pw_log_warn("midi %p: can't write event: %s", midi,
 						spa_strerror(res));
 			break;
 		}
+		case SPA_CONTROL_UMP:
+		{
+			void *data = SPA_POD_BODY(&next->value);
+			size_t size = SPA_POD_BODY_SIZE(&next->value);
+			uint8_t ev[32];
+
+			if (type == TYPE_ID_MIDI) {
+				int ev_size = spa_ump_to_midi(data, size, ev, sizeof(ev));
+				if (ev_size <= 0)
+					break;
+				size = ev_size;
+				data = ev;
+			} else if (type != TYPE_ID_UMP)
+				break;
+
+			if ((res = midi_event_write(midi, next->offset, data, size, fix)) < 0)
+				pw_log_warn("midi %p: can't write event: %s", midi,
+						spa_strerror(res));
+		}
 		}
 		c[next_index] = spa_pod_control_next(c[next_index]);
 	}
@@ -1602,19 +1700,22 @@ static inline void process_empty(struct port *p, uint32_t frames)
 	struct client *c = p->client;
 	void *ptr, *src = p->emptyptr;
 	struct port *tied = p->tied;
+	uint32_t type = p->object->port.type_id;
 
 	if (SPA_UNLIKELY(tied != NULL)) {
 		if ((src = tied->get_buffer(tied, frames)) == NULL)
 			src = p->emptyptr;
 	}
 
-	switch (p->object->port.type_id) {
+	switch (type) {
 	case TYPE_ID_AUDIO:
 		ptr = get_buffer_output(p, frames, sizeof(float), NULL);
 		if (SPA_LIKELY(ptr != NULL))
 			memcpy(ptr, src, frames * sizeof(float));
 		break;
 	case TYPE_ID_MIDI:
+	case TYPE_ID_OSC:
+	case TYPE_ID_UMP:
 	{
 		struct buffer *b;
 		ptr = get_buffer_output(p, c->max_frames, 1, &b);
@@ -1622,8 +1723,8 @@ static inline void process_empty(struct port *p, uint32_t frames)
 			/* first build the complete pod in scratch memory, then copy it
 			 * to the target buffer. This makes it possible for multiple threads
 			 * to do this concurrently */
-			b->datas[0].chunk->size = convert_from_midi(src,
-					midi_scratch, MIDI_SCRATCH_FRAMES * sizeof(float));
+			b->datas[0].chunk->size = convert_from_event(src, midi_scratch,
+					MIDI_SCRATCH_FRAMES * sizeof(float), type);
 			memcpy(ptr, midi_scratch, b->datas[0].chunk->size);
 		}
 		break;
@@ -2132,7 +2233,7 @@ static int client_node_set_param(void *data,
 			const struct spa_pod *param)
 {
 	struct client *c = (struct client *) data;
-	pw_proxy_error((struct pw_proxy*)c->node, -ENOTSUP, "set_param: not supported");
+	pw_proxy_error((struct pw_proxy*)c->node, -ENOTSUP, "not supported");
 	return -ENOTSUP;
 }
 
@@ -2408,6 +2509,8 @@ static int param_enum_format(struct client *c, struct port *p,
 			SPA_FORMAT_mediaSubtype,   SPA_POD_Id(SPA_MEDIA_SUBTYPE_dsp),
 	                SPA_FORMAT_AUDIO_format,   SPA_POD_Id(SPA_AUDIO_FORMAT_DSP_F32));
 		break;
+	case TYPE_ID_UMP:
+	case TYPE_ID_OSC:
 	case TYPE_ID_MIDI:
 		*param = spa_pod_builder_add_object(b,
 			SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat,
@@ -2439,6 +2542,8 @@ static int param_format(struct client *c, struct port *p,
 		                SPA_FORMAT_AUDIO_format,   SPA_POD_Id(SPA_AUDIO_FORMAT_DSP_F32));
 		break;
 	case TYPE_ID_MIDI:
+	case TYPE_ID_OSC:
+	case TYPE_ID_UMP:
 		*param = spa_pod_builder_add_object(b,
 				SPA_TYPE_OBJECT_Format, SPA_PARAM_Format,
 				SPA_FORMAT_mediaType,      SPA_POD_Id(SPA_MEDIA_TYPE_application),
@@ -2463,6 +2568,8 @@ static int param_buffers(struct client *c, struct port *p,
 	switch (p->object->port.type_id) {
 	case TYPE_ID_AUDIO:
 	case TYPE_ID_MIDI:
+	case TYPE_ID_OSC:
+	case TYPE_ID_UMP:
 		*param = spa_pod_builder_add_object(b,
 			SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers,
 			SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(1, 1, MAX_BUFFERS),
@@ -2584,7 +2691,7 @@ static int port_set_format(struct client *c, struct port *p,
 		p->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE);
 	}
 
-	pw_log_info("port %s: update", p->object->port.name);
+	pw_log_debug("port %s: update", p->object->port.name);
 
 	p->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
 
@@ -2624,7 +2731,7 @@ static void port_update_latency(struct port *p)
 	param_latency(c, p, &params[5], &b);
 	param_latency_other(c, p, &params[6], &b);
 
-	pw_log_info("port %s: update", p->object->port.name);
+	pw_log_debug("port %s: update", p->object->port.name);
 
 	p->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
 	p->params[IDX_Latency].flags ^= SPA_PARAM_INFO_SERIAL;
@@ -2796,7 +2903,7 @@ static inline void *init_buffer(struct port *p, uint32_t nframes)
 	if (p->zeroed)
 		return data;
 
-	if (p->object->port.type_id == TYPE_ID_MIDI) {
+	if (TYPE_ID_IS_EVENT(p->object->port.type_id)) {
 		struct midi_buffer *mb = data;
 		midi_init_buffer(data, c->max_frames, nframes);
 		pw_log_debug("port %p: init midi buffer size:%d frames:%d", p,
@@ -2837,7 +2944,8 @@ static int client_node_port_use_buffers(void *data,
 
 	if (n_buffers > MAX_BUFFERS) {
 		pw_log_error("%p: too many buffers %u > %u", c, n_buffers, MAX_BUFFERS);
-		return -ENOSPC;
+		res = -ENOSPC;
+		goto done;
 	}
 
 	fl = PW_MEMMAP_FLAG_READ;
@@ -2951,9 +3059,11 @@ static int client_node_port_use_buffers(void *data,
 	mix->n_buffers = n_buffers;
 	res = 0;
 
-      done:
+done:
 	if (res < 0)
-		pw_proxy_errorf((struct pw_proxy*)c->node, res, "port_use_buffers: %s", spa_strerror(res));
+		pw_proxy_errorf((struct pw_proxy*)c->node, res,
+				"port_use_buffers(%u:%u:%u): %s", direction, port_id,
+				mix_id, spa_strerror(res));
 	return res;
 }
 
@@ -3018,7 +3128,9 @@ exit_free:
 	pw_memmap_free(old);
 exit:
 	if (res < 0)
-		pw_proxy_errorf((struct pw_proxy*)c->node, res, "port_set_io: %s", spa_strerror(res));
+		pw_proxy_errorf((struct pw_proxy*)c->node, res,
+				"port_set_io(%u:%u:%u %u): %s", direction, port_id,
+				mix_id, id, spa_strerror(res));
 	return res;
 }
 
@@ -3151,7 +3263,8 @@ static int client_node_set_activation(void *data,
 
       exit:
 	if (res < 0)
-		pw_proxy_errorf((struct pw_proxy*)c->node, res, "set_activation: %s", spa_strerror(res));
+		pw_proxy_errorf((struct pw_proxy*)c->node, res,
+				"set_activation(%u): %s", node_id, spa_strerror(res));
 	return res;
 }
 
@@ -3168,7 +3281,7 @@ static int client_node_port_set_mix_info(void *data,
 	int res = 0;
 
 	if (p == NULL || !p->valid) {
-		res = -EINVAL;
+		res = peer_id == SPA_ID_INVALID ? 0 : -EINVAL;
 		goto exit;
 	}
 
@@ -3192,7 +3305,9 @@ static int client_node_port_set_mix_info(void *data,
 	}
 exit:
 	if (res < 0)
-		pw_proxy_errorf((struct pw_proxy*)c->node, res, "set_mix_info: %s", spa_strerror(res));
+		pw_proxy_errorf((struct pw_proxy*)c->node, res,
+				"set_mix_info(%u:%u:%u %u): %s", direction, port_id,
+				mix_id, peer_id, spa_strerror(res));
 	return res;
 }
 
@@ -3234,7 +3349,7 @@ static struct spa_thread *impl_create(void *object,
 	if (globals.creator != NULL) {
 		uint32_t i, n_items = props ? props->n_items : 0;
 
-		items = alloca((n_items) + 1 * sizeof(*items));
+		items = alloca((n_items + 1) * sizeof(*items));
 
 		for (i = 0; i < n_items; i++)
 			items[i] = props->items[i];
@@ -3281,10 +3396,14 @@ static jack_port_type_id_t string_to_type(const char *port_type)
 {
 	if (spa_streq(JACK_DEFAULT_AUDIO_TYPE, port_type))
 		return TYPE_ID_AUDIO;
-	else if (spa_streq(JACK_DEFAULT_MIDI_TYPE, port_type))
-		return TYPE_ID_MIDI;
 	else if (spa_streq(JACK_DEFAULT_VIDEO_TYPE, port_type))
 		return TYPE_ID_VIDEO;
+	else if (spa_streq(JACK_DEFAULT_MIDI_TYPE, port_type))
+		return TYPE_ID_MIDI;
+	else if (spa_streq(JACK_DEFAULT_OSC_TYPE, port_type))
+		return TYPE_ID_OSC;
+	else if (spa_streq(JACK_DEFAULT_UMP_TYPE, port_type))
+		return TYPE_ID_UMP;
 	else if (spa_streq("other", port_type))
 		return TYPE_ID_OTHER;
 	else
@@ -3296,22 +3415,46 @@ static const char* type_to_string(jack_port_type_id_t type_id)
 	switch(type_id) {
 	case TYPE_ID_AUDIO:
 		return JACK_DEFAULT_AUDIO_TYPE;
-	case TYPE_ID_MIDI:
-		return JACK_DEFAULT_MIDI_TYPE;
 	case TYPE_ID_VIDEO:
 		return JACK_DEFAULT_VIDEO_TYPE;
+	case TYPE_ID_MIDI:
+	case TYPE_ID_OSC:
+	case TYPE_ID_UMP:
+		/* all returned as MIDI */
+		return JACK_DEFAULT_MIDI_TYPE;
 	case TYPE_ID_OTHER:
 		return "other";
 	default:
 		return NULL;
 	}
 }
+
+static const char* type_to_format_dsp(jack_port_type_id_t type_id)
+{
+	switch(type_id) {
+	case TYPE_ID_AUDIO:
+		return JACK_DEFAULT_AUDIO_TYPE;
+	case TYPE_ID_VIDEO:
+		return JACK_DEFAULT_VIDEO_TYPE;
+	case TYPE_ID_OSC:
+		return JACK_DEFAULT_OSC_TYPE;
+	case TYPE_ID_MIDI:
+		return JACK_DEFAULT_MIDI_TYPE;
+	case TYPE_ID_UMP:
+		return JACK_DEFAULT_UMP_TYPE;
+	default:
+		return NULL;
+	}
+}
+
 static bool type_is_dsp(jack_port_type_id_t type_id)
 {
 	switch(type_id) {
 	case TYPE_ID_AUDIO:
 	case TYPE_ID_MIDI:
 	case TYPE_ID_VIDEO:
+	case TYPE_ID_OSC:
+	case TYPE_ID_UMP:
 		return true;
 	default:
 		return false;
@@ -3328,29 +3471,6 @@ static jack_uuid_t client_make_uuid(uint32_t id, bool monitor)
 	return uuid;
 }
 
-static int json_object_find(const char *obj, const char *key, char *value, size_t len)
-{
-	struct spa_json it[2];
-	const char *v;
-	char k[128];
-
-	spa_json_init(&it[0], obj, strlen(obj));
-	if (spa_json_enter_object(&it[0], &it[1]) <= 0)
-		return -EINVAL;
-
-	while (spa_json_get_string(&it[1], k, sizeof(k)) > 0) {
-		if (spa_streq(k, key)) {
-			if (spa_json_get_string(&it[1], value, len) <= 0)
-				continue;
-			return 0;
-		} else {
-			if (spa_json_next(&it[1], &v) <= 0)
-				break;
-		}
-	}
-	return -ENOENT;
-}
-
 static int metadata_property(void *data, uint32_t id,
 		const char *key, const char *type, const char *value)
 {
@@ -3363,7 +3483,7 @@ static int metadata_property(void *data, uint32_t id,
 	if (id == PW_ID_CORE) {
 		if (key == NULL || spa_streq(key, "default.audio.sink")) {
 			if (value != NULL) {
-				if (json_object_find(value, "name",
+				if (spa_json_str_object_find(value, strlen(value), "name",
 						c->metadata->default_audio_sink,
 						sizeof(c->metadata->default_audio_sink)) < 0)
 					value = NULL;
@@ -3373,7 +3493,7 @@ static int metadata_property(void *data, uint32_t id,
 		}
 		if (key == NULL || spa_streq(key, "default.audio.source")) {
 			if (value != NULL) {
-				if (json_object_find(value, "name",
+				if (spa_json_str_object_find(value, strlen(value), "name",
 						c->metadata->default_audio_source,
 						sizeof(c->metadata->default_audio_source)) < 0)
 					value = NULL;
@@ -3564,6 +3684,7 @@ static void registry_event_global(void *data, uint32_t id,
 	const char *str;
 	bool do_emit = true, do_sync = false;
 	uint32_t serial;
+	const char *app;
 
 	if (props == NULL)
 		return;
@@ -3574,8 +3695,30 @@ static void registry_event_global(void *data, uint32_t id,
 
 	pw_log_debug("new %s id:%u serial:%u", type, id, serial);
 
-	if (spa_streq(type, PW_TYPE_INTERFACE_Node)) {
-		const char *app, *node_name;
+	if (spa_streq(type, PW_TYPE_INTERFACE_Client)) {
+		app = spa_dict_lookup(props, PW_KEY_APP_NAME);
+
+		if ((str = spa_dict_lookup(props, PW_KEY_SEC_PID)) != NULL) {
+			pw_log_debug("%p: pid of \"%s\" is \"%s\"", c, app, str);
+		} else {
+			pw_log_debug("%p: pid of \"%s\" is unknown", c, app);
+		}
+
+		o = alloc_object(c, INTERFACE_Client);
+		if (o == NULL)
+			goto exit;
+
+		o->pwclient.pid = (int32_t)atoi(str);
+		snprintf(o->pwclient.name, sizeof(o->pwclient.name), "%s", app);
+
+		pw_log_debug("%p: add pw client %d (%s) pid %llu", c, id, app, (unsigned long long)o->pwclient.pid);
+
+		pthread_mutex_lock(&c->context.lock);
+		spa_list_append(&c->context.objects, &o->link);
+		pthread_mutex_unlock(&c->context.lock);
+	}
+	else if (spa_streq(type, PW_TYPE_INTERFACE_Node)) {
+		const char *node_name;
 		char tmp[JACK_CLIENT_NAME_SIZE+1];
 
 		o = alloc_object(c, INTERFACE_Node);
@@ -3676,6 +3819,9 @@ static void registry_event_global(void *data, uint32_t id,
 		if ((str = spa_dict_lookup(props, PW_KEY_PORT_NAME)) == NULL)
 			goto exit;
 
+		if (type_id == TYPE_ID_UMP && c->flag_midi2)
+			flags |= JackPortIsMIDI2;
+
 		spa_dict_for_each(item, props) {
 			if (spa_streq(item->key, PW_KEY_PORT_DIRECTION)) {
 				if (spa_streq(item->value, "in"))
@@ -3701,7 +3847,7 @@ static void registry_event_global(void *data, uint32_t id,
 		}
 		if (is_monitor && !c->show_monitor)
 			goto exit;
-		if (type_id == TYPE_ID_MIDI && !c->show_midi)
+		if (TYPE_ID_IS_EVENT(type_id) && !c->show_midi)
 			goto exit;
 
 		o = NULL;
@@ -3762,6 +3908,8 @@ static void registry_event_global(void *data, uint32_t id,
 						(int)(sizeof(tmp)-11), tmp, serial);
 			else
 				snprintf(o->port.name, sizeof(o->port.name), "%s", tmp);
+
+			o->port.type_id = type_id;
 		}
 
 		if (c->fill_aliases) {
@@ -3781,7 +3929,6 @@ static void registry_event_global(void *data, uint32_t id,
 		}
 
 		o->port.flags = flags;
-		o->port.type_id = type_id;
 		o->port.node_id = node_id;
 		o->port.is_monitor = is_monitor;
 
@@ -3933,6 +4080,9 @@ static void registry_event_global_remove(void *data, uint32_t id)
 	o->removing = true;
 
 	switch (o->type) {
+	case INTERFACE_Client:
+		free_object(c, o);
+		break;
 	case INTERFACE_Node:
 		if (c->metadata) {
 			if (spa_streq(o->node.node_name, c->metadata->default_audio_sink))
@@ -4003,10 +4153,12 @@ static int execute_match(void *data, const char *location, const char *action,
 	return 1;
 }
 
+static struct client * g_first_client;
+
 SPA_EXPORT
 jack_client_t * jack_client_open (const char *client_name,
                                   jack_options_t options,
-                                  jack_status_t *status, ...)
+                                  jack_status_t *status_ptr, ...)
 {
 	struct client *client;
 	const struct spa_support *support;
@@ -4015,7 +4167,7 @@ jack_client_t * jack_client_open (const char *client_name,
 	struct spa_cpu *cpu_iface;
 	const struct pw_properties *props;
 	va_list ap;
-
+        jack_status_t status;
         if (getenv("PIPEWIRE_NOJACK") != NULL ||
             getenv("PIPEWIRE_INTERNAL") != NULL ||
 	    spa_strstartswith(pw_get_library_version(), "0.2"))
@@ -4029,7 +4181,7 @@ jack_client_t * jack_client_open (const char *client_name,
 
 	pw_log_info("%p: open '%s' options:%d", client, client_name, options);
 
-	va_start(ap, status);
+	va_start(ap, status_ptr);
 	varargs_parse(client, options, ap);
 	va_end(ap);
 
@@ -4249,6 +4401,7 @@ jack_client_t * jack_client_open (const char *client_name,
 	client->fill_aliases = pw_properties_get_bool(client->props, "jack.fill-aliases", false);
 	client->writable_input = pw_properties_get_bool(client->props, "jack.writable-input", true);
 	client->async = pw_properties_get_bool(client->props, PW_KEY_NODE_ASYNC, false);
+	client->flag_midi2 = pw_properties_get_bool(client->props, "jack.flag-midi2", false);
 
 	client->self_connect_mode = SELF_CONNECT_ALLOW;
 	if ((str = pw_properties_get(client->props, "jack.self-connect-mode")) != NULL) {
@@ -4271,8 +4424,9 @@ jack_client_t * jack_client_open (const char *client_name,
 
 	client->rt_max = pw_properties_get_int32(client->props, "rt.prio", DEFAULT_RT_MAX);
 
-	if (status)
-		*status = 0;
+	status = 0;
+	if (status_ptr)
+		*status_ptr = status;
 
 	client->pending_sync = pw_proxy_sync((struct pw_proxy*)client->core, client->pending_sync);
 
@@ -4287,40 +4441,48 @@ jack_client_t * jack_client_open (const char *client_name,
 	}
 
 	if (!spa_streq(client->name, client_name)) {
-		if (status)
-			*status |= JackNameNotUnique;
+		status |= JackNameNotUnique;
+		if (status_ptr)
+			*status_ptr = status;
 		if (options & JackUseExactName)
 			goto exit_unlock;
 	}
 	pw_thread_loop_unlock(client->context.loop);
 
+	if (g_first_client == NULL)
+		g_first_client = client;
+
 	pw_thread_loop_start(client->context.notify);
 
 	pw_log_info("%p: opened", client);
 	return (jack_client_t *)client;
 
 no_props:
-	if (status)
-		*status = JackFailure | JackInitFailure;
+	status = JackFailure | JackInitFailure;
+	if (status_ptr)
+		*status_ptr = status;
 	goto exit;
 init_failed:
-	if (status)
-		*status = JackFailure | JackInitFailure;
+	status = JackFailure | JackInitFailure;
+	if (status_ptr)
+		*status_ptr = status;
 	goto exit_unlock;
 server_failed:
-	if (status)
-		*status = JackFailure | JackServerFailed;
+	status = JackFailure | JackServerFailed;
+	if (status_ptr)
+		*status_ptr = status;
 	goto exit_unlock;
 exit_unlock:
 	pw_thread_loop_unlock(client->context.loop);
 exit:
-	pw_log_info("%p: error %d", client, *status);
+	pw_log_info("%p: error %d", client, status);
 	jack_client_close((jack_client_t *) client);
 	return NULL;
 disabled:
 	pw_log_warn("JACK is disabled");
-	if (status)
-		*status = JackFailure | JackInitFailure;
+	status = JackFailure | JackInitFailure;
+	if (status_ptr)
+		*status_ptr = status;
 	return NULL;
 }
 
@@ -4350,6 +4512,9 @@ int jack_client_close (jack_client_t *client)
 
 	pw_log_info("%p: close", client);
 
+	if (g_first_client == c)
+		g_first_client = NULL;
+
 	c->destroyed = true;
 
 	res = jack_deactivate(client);
@@ -4703,8 +4868,25 @@ int jack_deactivate (jack_client_t *client)
 SPA_EXPORT
 int jack_get_client_pid (const char *name)
 {
-	pw_log_error("not implemented on library side");
-	return 0;
+	struct object *on, *oc;
+
+	if (g_first_client == NULL) return 0;
+
+	on = find_node(g_first_client, name);
+	if (on == NULL) {
+		pw_log_warn("unknown (jack-client) node \"%s\"", name);
+		return 0;
+	}
+
+	oc = find_client(g_first_client, on->node.client_id);
+	if (oc == NULL) {
+		pw_log_warn("unknown (pw) client %d", (int)on->node.client_id);
+		return 0;
+	}
+
+	pw_log_info("pid %d (%s)", (int)oc->pwclient.pid, oc->pwclient.name);
+
+	return (int)oc->pwclient.pid;
 }
 
 SPA_EXPORT
@@ -5162,7 +5344,7 @@ jack_nframes_t jack_get_sample_rate (jack_client_t *client)
 		}
 	}
 	c->sample_rate = res;
-	pw_log_debug("sample_rate: %u", res);
+	pw_log_trace_fp("sample_rate: %u", res);
 	return res;
 }
 
@@ -5260,6 +5442,9 @@ jack_port_t * jack_port_register (jack_client_t *client,
 		pw_log_warn("unknown port type %s", port_type);
 		return NULL;
 	}
+	if (type_id == TYPE_ID_MIDI && (flags & JackPortIsMIDI2))
+		type_id = TYPE_ID_UMP;
+
 	len = snprintf(name, sizeof(name), "%s:%s", c->name, port_name);
 	if (len < 0 || (size_t)len >= sizeof(name)) {
 		pw_log_warn("%p: name \"%s:%s\" too long", c,
@@ -5293,6 +5478,8 @@ jack_port_t * jack_port_register (jack_client_t *client,
 			p->get_buffer = get_buffer_input_float;
 			break;
 		case TYPE_ID_MIDI:
+		case TYPE_ID_OSC:
+		case TYPE_ID_UMP:
 			p->get_buffer = get_buffer_input_midi;
 			break;
 		default:
@@ -5306,6 +5493,8 @@ jack_port_t * jack_port_register (jack_client_t *client,
 			p->get_buffer = get_buffer_output_float;
 			break;
 		case TYPE_ID_MIDI:
+		case TYPE_ID_OSC:
+		case TYPE_ID_UMP:
 			p->get_buffer = get_buffer_output_midi;
 			break;
 		default:
@@ -5318,7 +5507,7 @@ jack_port_t * jack_port_register (jack_client_t *client,
 
 	spa_list_init(&p->mix);
 
-	pw_properties_set(p->props, PW_KEY_FORMAT_DSP, port_type);
+	pw_properties_set(p->props, PW_KEY_FORMAT_DSP, type_to_format_dsp(type_id));
 	pw_properties_set(p->props, PW_KEY_PORT_NAME, port_name);
 	if (flags > 0x1f) {
 		pw_properties_setf(p->props, PW_KEY_PORT_EXTRA,
@@ -5562,7 +5751,7 @@ static void *get_buffer_input_midi(struct port *p, jack_nframes_t frames)
 	/* first convert to a thread local scratch buffer, then memcpy into
 	 * the per port buffer. This makes it possible to call this function concurrently
 	 * but also have different pointers per port */
-	convert_to_midi(seq, n_seq, mb, p->client->fix_midi_events);
+	convert_to_event(seq, n_seq, mb, p->client->fix_midi_events, p->object->port.type_id);
 	memcpy(ptr, mb, sizeof(struct midi_buffer) + (mb->event_count
                               * sizeof(struct midi_event)));
 	if (mb->write_pos) {
@@ -5628,7 +5817,7 @@ void * jack_port_get_buffer (jack_port_t *port, jack_nframes_t frames)
 		if ((b = get_mix_buffer(c, mix, frames)) == NULL)
 			goto done;
 
-		if (o->port.type_id == TYPE_ID_MIDI) {
+		if (TYPE_ID_IS_EVENT(o->port.type_id)) {
 			struct spa_pod_sequence *seq[1];
 			struct spa_data *d;
 			void *pod;
@@ -5643,7 +5832,7 @@ void * jack_port_get_buffer (jack_port_t *port, jack_nframes_t frames)
 			if (!spa_pod_is_sequence(pod))
 				goto done;
 			seq[0] = pod;
-			convert_to_midi(seq, 1, ptr, c->fix_midi_events);
+			convert_to_event(seq, 1, ptr, c->fix_midi_events, o->port.type_id);
 		} else {
 			ptr = get_buffer_data(b, frames);
 		}
@@ -6207,7 +6396,7 @@ int jack_connect (jack_client_t *client,
 	if (src == NULL || dst == NULL ||
 	    !(src->port.flags & JackPortIsOutput) ||
 	    !(dst->port.flags & JackPortIsInput) ||
-	    src->port.type_id != dst->port.type_id) {
+	    !TYPE_ID_IS_COMPATIBLE(src->port.type_id, dst->port.type_id)) {
 		res = -EINVAL;
 		goto exit;
 	}
@@ -6364,6 +6553,10 @@ size_t jack_port_type_get_buffer_size (jack_client_t *client, const char *port_t
 		return jack_get_buffer_size(client) * sizeof(float);
 	else if (spa_streq(JACK_DEFAULT_MIDI_TYPE, port_type))
 		return c->max_frames * sizeof(float);
+	else if (spa_streq(JACK_DEFAULT_OSC_TYPE, port_type))
+		return c->max_frames * sizeof(float);
+	else if (spa_streq(JACK_DEFAULT_UMP_TYPE, port_type))
+		return c->max_frames * sizeof(float);
 	else if (spa_streq(JACK_DEFAULT_VIDEO_TYPE, port_type))
 		return 320 * 240 * 4 * sizeof(float);
 	else
@@ -6398,6 +6591,7 @@ void jack_port_get_latency_range (jack_port_t *port, jack_latency_callback_mode_
 	jack_nframes_t nframes, rate;
 	int direction;
 	struct spa_latency_info *info;
+	int64_t min, max;
 
 	return_if_fail(o != NULL);
 	c = o->client;
@@ -6416,10 +6610,15 @@ void jack_port_get_latency_range (jack_port_t *port, jack_latency_callback_mode_
 	rate = jack_get_sample_rate((jack_client_t*)c);
 	info = &o->port.latency[direction];
 
-	range->min = (jack_nframes_t)((info->min_quantum * nframes) +
-		info->min_rate + (info->min_ns * rate) / SPA_NSEC_PER_SEC);
-	range->max = (jack_nframes_t)((info->max_quantum * nframes) +
-		info->max_rate + (info->max_ns * rate) / SPA_NSEC_PER_SEC);
+	min = (int64_t)(info->min_quantum * nframes) +
+		info->min_rate +
+		(info->min_ns * (int64_t)rate) / (int64_t)SPA_NSEC_PER_SEC;
+	max = (int64_t)(info->max_quantum * nframes) +
+		info->max_rate +
+		(info->max_ns * (int64_t)rate) / (int64_t)SPA_NSEC_PER_SEC;
+
+	range->min = SPA_MAX(min, 0);
+	range->max = SPA_MAX(max, 0);
 
 	pw_log_debug("%p: %s get %d latency range %d %d", c, o->port.name,
 			mode, range->min, range->max);
@@ -6464,13 +6663,13 @@ void jack_port_set_latency_range (jack_port_t *port, jack_latency_callback_mode_
 		nframes = 1;
 
 	latency.min_rate = range->min;
-	if (latency.min_rate >= nframes) {
+	if (latency.min_rate >= (int32_t)nframes) {
 		latency.min_quantum = latency.min_rate / nframes;
 		latency.min_rate %= nframes;
 	}
 
 	latency.max_rate = range->max;
-	if (latency.max_rate >= nframes) {
+	if (latency.max_rate >= (int32_t)nframes) {
 		latency.max_quantum = latency.max_rate / nframes;
 		latency.max_rate %= nframes;
 	}
@@ -6626,7 +6825,7 @@ const char ** jack_get_ports (jack_client_t *client,
 			continue;
 		pw_log_debug("%p: check port type:%d flags:%08lx name:\"%s\"", c,
 				o->port.type_id, o->port.flags, o->port.name);
-		if (o->port.type_id > TYPE_ID_VIDEO)
+		if (TYPE_ID_IS_HIDDEN(o->port.type_id))
 			continue;
 		if (!SPA_FLAG_IS_SET(o->port.flags, flags))
 			continue;
@@ -7100,6 +7299,8 @@ static int transport_update(struct client* c, int active)
                                     PW_CLIENT_NODE_UPDATE_INFO,
 				    0, NULL, &c->info);
 	c->info.change_mask = 0;
+
+	pw_properties_set(c->props, PW_KEY_NODE_TRANSPORT, NULL);
 	pw_thread_loop_unlock(c->context.loop);
 
 	return 0;
diff --git a/pipewire-v4l2/src/pipewire-v4l2.c b/pipewire-v4l2/src/pipewire-v4l2.c
index 2e5a4045..691c8ec3 100644
--- a/pipewire-v4l2/src/pipewire-v4l2.c
+++ b/pipewire-v4l2/src/pipewire-v4l2.c
@@ -1696,7 +1696,7 @@ static int connect_stream(struct file *file)
 			break;
 
 		if (state == PW_STREAM_STATE_ERROR) {
-			res = -EIO;
+			res = -errno;
 			goto exit;
 		}
 		if (file->error < 0) {
diff --git a/po/LINGUAS b/po/LINGUAS
index 2532833e..a6b999f4 100644
--- a/po/LINGUAS
+++ b/po/LINGUAS
@@ -40,6 +40,7 @@ pt
 ro
 ru
 sk
+sl
 sr@latin
 sr
 sv
diff --git a/po/POTFILES.skip b/po/POTFILES.skip
index d6b76925..686d91a3 100644
--- a/po/POTFILES.skip
+++ b/po/POTFILES.skip
@@ -1 +1,2 @@
+src/daemon/systemd/system/pipewire-pulse.service.in
 src/daemon/systemd/system/pipewire.service.in
diff --git a/po/de.po b/po/de.po
index 6c1b0882..5ac68fea 100644
--- a/po/de.po
+++ b/po/de.po
@@ -9,15 +9,16 @@
 # Hedda Peters <hpeters@redhat.com>, 2009, 2012.
 # Mario Blättermann <mario.blaettermann@gmail.com>, 2016.
 # Jürgen Benvenuti <gastornis@posteo.org>, 2024.
+# Christian Kirbach <christian.kirbach@gmail.com>, 2024.
 #
 msgid ""
 msgstr ""
 "Project-Id-Version: pipewire.master-tx.de\n"
 "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/-/"
 "issues\n"
-"POT-Creation-Date: 2024-01-28 15:27+0000\n"
-"PO-Revision-Date: 2024-01-28 19:19+0100\n"
-"Last-Translator: Jürgen Benvenuti <gastornis@posteo.org>\n"
+"POT-Creation-Date: 2024-09-27 03:27+0000\n"
+"PO-Revision-Date: 2024-09-27 11:25+0200\n"
+"Last-Translator: Christian Kirbach <christian.kirbach@gmail.com>\n"
 "Language-Team: German <https://translate.fedoraproject.org/projects/pipewire/"
 "pipewire/de/>\n"
 "Language: de\n"
@@ -25,21 +26,25 @@ 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.4.2\n"
+"X-Generator: Poedit 3.4.4\n"
 
-#: src/daemon/pipewire.c:26
+#: src/daemon/pipewire.c:29
 #, c-format
 msgid ""
 "%s [options]\n"
 "  -h, --help                            Show this help\n"
+"  -v, --verbose                         Increase verbosity by one level\n"
 "      --version                         Show version\n"
 "  -c, --config                          Load config (Default %s)\n"
+"  -P  --properties                      Set context properties\n"
 msgstr ""
 "%s [Optionen]\n"
 "  -h, --help                            Diese Hilfe ausgeben\n"
+"  -v, --verbose                         Ausführlichere Ausgaben\n"
 "      --version                         Version anzeigen\n"
 "  -c, --config                          Konfiguration laden (Voreinstellung "
 "%s)\n"
+"  -P  --properties                      Kontext-Eigenschaften festlegen\n"
 
 #: src/daemon/pipewire.desktop.in:4
 msgid "PipeWire Media System"
@@ -59,26 +64,26 @@ msgstr "Tunnel zu %s%s%s"
 msgid "Dummy Output"
 msgstr "Schein-Ausgabe"
 
-#: src/modules/module-pulse-tunnel.c:774
+#: src/modules/module-pulse-tunnel.c:777
 #, c-format
 msgid "Tunnel for %s@%s"
 msgstr "Tunnel für %s@%s"
 
-#: src/modules/module-zeroconf-discover.c:315
+#: src/modules/module-zeroconf-discover.c:320
 msgid "Unknown device"
 msgstr "Unbekanntes Gerät"
 
-#: src/modules/module-zeroconf-discover.c:327
+#: src/modules/module-zeroconf-discover.c:332
 #, c-format
 msgid "%s on %s@%s"
 msgstr "%s auf %s@%s"
 
-#: src/modules/module-zeroconf-discover.c:331
+#: src/modules/module-zeroconf-discover.c:336
 #, c-format
 msgid "%s on %s"
 msgstr "%s auf %s"
 
-#: src/tools/pw-cat.c:991
+#: src/tools/pw-cat.c:973
 #, c-format
 msgid ""
 "%s [options] [<file>|-]\n"
@@ -94,7 +99,7 @@ msgstr ""
 "\n"
 "\n"
 
-#: src/tools/pw-cat.c:998
+#: src/tools/pw-cat.c:980
 #, c-format
 msgid ""
 "  -R, --remote                          Remote daemon name\n"
@@ -128,7 +133,7 @@ msgstr ""
 "  -P  --properties                      Knoteneigenschaften festlegen\n"
 "\n"
 
-#: src/tools/pw-cat.c:1016
+#: src/tools/pw-cat.c:998
 #, c-format
 msgid ""
 "      --rate                            Sample rate (req. for rec) (default "
@@ -145,6 +150,7 @@ msgid ""
 "      --volume                          Stream volume 0-1.0 (default %.3f)\n"
 "  -q  --quality                         Resampler quality (0 - 15) (default "
 "%d)\n"
+"  -a, --raw                             RAW mode\n"
 "\n"
 msgstr ""
 "      --rate                            Abtastrate (notw. für Aufzeichn.) "
@@ -162,9 +168,10 @@ msgstr ""
 "%.3f)\n"
 "  -q  --quality                         Qualität der Neu-Abtastung (0 - 15) "
 "(Vorgabe %d)\n"
+"  -a, --raw                             RAW-Modus\n"
 "\n"
 
-#: src/tools/pw-cat.c:1033
+#: src/tools/pw-cat.c:1016
 msgid ""
 "  -p, --playback                        Playback mode\n"
 "  -r, --record                          Recording mode\n"
@@ -180,7 +187,7 @@ msgstr ""
 "  -o, --encoded                         Codieren-Modus\n"
 "\n"
 
-#: src/tools/pw-cli.c:2248
+#: src/tools/pw-cli.c:2318
 #, c-format
 msgid ""
 "%s [options] [command]\n"
@@ -203,8 +210,8 @@ msgstr ""
 msgid "Pro Audio"
 msgstr "Pro Audio"
 
-#: spa/plugins/alsa/acp/acp.c:488 spa/plugins/alsa/acp/alsa-mixer.c:4633
-#: spa/plugins/bluez5/bluez5-device.c:1725
+#: spa/plugins/alsa/acp/acp.c:487 spa/plugins/alsa/acp/alsa-mixer.c:4633
+#: spa/plugins/bluez5/bluez5-device.c:1696
 msgid "Off"
 msgstr "Aus"
 
@@ -231,7 +238,7 @@ msgstr "Line-Eingang"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:2657
 #: spa/plugins/alsa/acp/alsa-mixer.c:2741
-#: spa/plugins/bluez5/bluez5-device.c:2004
+#: spa/plugins/bluez5/bluez5-device.c:1984
 msgid "Microphone"
 msgstr "Mikrofon"
 
@@ -297,7 +304,7 @@ msgid "No Bass Boost"
 msgstr "Keine Bassverstärkung"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:2672
-#: spa/plugins/bluez5/bluez5-device.c:2010
+#: spa/plugins/bluez5/bluez5-device.c:1990
 msgid "Speaker"
 msgstr "Lautsprecher"
 
@@ -316,7 +323,7 @@ msgstr "Mikrofon der Docking-Station"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:2746
 msgid "Headset Microphone"
-msgstr "Mikrofon am Kopfhörer"
+msgstr "Mikrofon am Sprechkopfhörer"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:2750
 msgid "Analog Output"
@@ -360,11 +367,11 @@ msgstr "Mehrkanal-Eingang"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:2761
 msgid "Multichannel Output"
-msgstr "Mehrkanal-Ausgang"
+msgstr "Mehrkanal-Wiedergabe"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:2762
 msgid "Game Output"
-msgstr "Spiel-Ausgabe"
+msgstr "Spiel-Ausgang"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:2763
 #: spa/plugins/alsa/acp/alsa-mixer.c:2764
@@ -381,7 +388,7 @@ msgstr "Virtuelles 7.1 Surround"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:4456
 msgid "Analog Mono"
-msgstr "Analog Mono"
+msgstr "Analoges Mono"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:4457
 msgid "Analog Mono (Left)"
@@ -400,7 +407,7 @@ msgstr "Analoges Mono (rechts)"
 #: spa/plugins/alsa/acp/alsa-mixer.c:4467
 #: spa/plugins/alsa/acp/alsa-mixer.c:4468
 msgid "Analog Stereo"
-msgstr "Analog Stereo"
+msgstr "Analoges Stereo"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:4460
 msgid "Mono"
@@ -412,9 +419,9 @@ msgstr "Stereo"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:4469
 #: spa/plugins/alsa/acp/alsa-mixer.c:4627
-#: spa/plugins/bluez5/bluez5-device.c:1992
+#: spa/plugins/bluez5/bluez5-device.c:1972
 msgid "Headset"
-msgstr "Headset"
+msgstr "Sprechkopfhörer"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:4470
 #: spa/plugins/alsa/acp/alsa-mixer.c:4628
@@ -472,7 +479,7 @@ msgstr "Analog Surround 7.1"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:4484
 msgid "Digital Stereo (IEC958)"
-msgstr "Digital Stereo (IEC958)"
+msgstr "Digitales Stereo (IEC958)"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:4485
 msgid "Digital Surround 4.0 (IEC958/AC3)"
@@ -488,7 +495,7 @@ msgstr "Digital Surround 5.1 (IEC958/DTS)"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:4488
 msgid "Digital Stereo (HDMI)"
-msgstr "Digital Stereo (HDMI)"
+msgstr "Digitales Stereo (HDMI)"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:4489
 msgid "Digital Surround 5.1 (HDMI)"
@@ -504,15 +511,15 @@ msgstr "Spiel"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:4625
 msgid "Analog Mono Duplex"
-msgstr "Analog Mono Duplex"
+msgstr "Analoges Mono Duplex"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:4626
 msgid "Analog Stereo Duplex"
-msgstr "Analog Stereo Duplex"
+msgstr "Analoges Stereo Duplex"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:4629
 msgid "Digital Stereo Duplex (IEC958)"
-msgstr "Digital Stereo Duplex (IEC958)"
+msgstr "Digitales Stereo Duplex (IEC958)"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:4630
 msgid "Multichannel Duplex"
@@ -536,7 +543,7 @@ msgstr "%s-Ausgabe"
 msgid "%s Input"
 msgstr "%s-Eingang"
 
-#: spa/plugins/alsa/acp/alsa-util.c:1211 spa/plugins/alsa/acp/alsa-util.c:1305
+#: spa/plugins/alsa/acp/alsa-util.c:1231 spa/plugins/alsa/acp/alsa-util.c:1325
 #, c-format
 msgid ""
 "snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu "
@@ -559,7 +566,7 @@ msgstr[1] ""
 "Dies ist wahrscheinlich ein Fehler im ALSA-Treiber »%s«. Bitte melden Sie "
 "dieses Problem den ALSA-Entwicklern."
 
-#: spa/plugins/alsa/acp/alsa-util.c:1277
+#: spa/plugins/alsa/acp/alsa-util.c:1297
 #, c-format
 msgid ""
 "snd_pcm_delay() returned a value that is exceptionally large: %li byte "
@@ -582,7 +589,7 @@ msgstr[1] ""
 "Dies ist wahrscheinlich ein Fehler im ALSA-Treiber »%s«. Bitte melden Sie "
 "dieses Problem den ALSA-Entwicklern."
 
-#: spa/plugins/alsa/acp/alsa-util.c:1324
+#: spa/plugins/alsa/acp/alsa-util.c:1344
 #, c-format
 msgid ""
 "snd_pcm_avail_delay() returned strange values: delay %lu is less than avail "
@@ -595,7 +602,7 @@ msgstr ""
 "Dies ist wahrscheinlich ein Fehler im ALSA-Treiber »%s«. Bitte melden Sie "
 "dieses Problem den ALSA-Entwicklern."
 
-#: spa/plugins/alsa/acp/alsa-util.c:1367
+#: spa/plugins/alsa/acp/alsa-util.c:1387
 #, c-format
 msgid ""
 "snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu byte "
@@ -630,104 +637,103 @@ msgstr "Internes Audio"
 msgid "Modem"
 msgstr "Modem"
 
-#: spa/plugins/bluez5/bluez5-device.c:1736
+#: spa/plugins/bluez5/bluez5-device.c:1707
 msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
 msgstr "Audio-Gateway (A2DP-Quelle und HSP/HFP AG)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1784
+#: spa/plugins/bluez5/bluez5-device.c:1755
 #, c-format
 msgid "High Fidelity Playback (A2DP Sink, codec %s)"
 msgstr "High Fidelity-Wiedergabe (A2DP-Senke, Codec %s)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1787
+#: spa/plugins/bluez5/bluez5-device.c:1758
 #, c-format
 msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
 msgstr "High Fidelity Duplex (A2DP-Quelle/-Senke, Codec %s)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1795
+#: spa/plugins/bluez5/bluez5-device.c:1766
 msgid "High Fidelity Playback (A2DP Sink)"
 msgstr "High Fidelity-Wiedergabe (A2DP-Senke)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1797
+#: spa/plugins/bluez5/bluez5-device.c:1768
 msgid "High Fidelity Duplex (A2DP Source/Sink)"
 msgstr "High Fidelity Duplex (A2DP-Quelle/-Senke)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1847
+#: spa/plugins/bluez5/bluez5-device.c:1818
 #, c-format
 msgid "High Fidelity Playback (BAP Sink, codec %s)"
 msgstr "High Fidelity-Wiedergabe (BAP-Senke, Codec %s)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1852
+#: spa/plugins/bluez5/bluez5-device.c:1823
 #, c-format
 msgid "High Fidelity Input (BAP Source, codec %s)"
 msgstr "High Fidelity-Eingang (BAP-Quelle, Codec %s)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1856
+#: spa/plugins/bluez5/bluez5-device.c:1827
 #, c-format
 msgid "High Fidelity Duplex (BAP Source/Sink, codec %s)"
 msgstr "High Fidelity Duplex (BAP-Quelle/-Senke, Codec %s)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1865
+#: spa/plugins/bluez5/bluez5-device.c:1836
 msgid "High Fidelity Playback (BAP Sink)"
 msgstr "High Fidelity-Wiedergabe (BAP-Senke)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1869
+#: spa/plugins/bluez5/bluez5-device.c:1840
 msgid "High Fidelity Input (BAP Source)"
 msgstr "High Fidelity-Eingang (BAP-Quelle)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1872
+#: spa/plugins/bluez5/bluez5-device.c:1843
 msgid "High Fidelity Duplex (BAP Source/Sink)"
 msgstr "High Fidelity Duplex (BAP-Quelle/-Senke)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1908
+#: spa/plugins/bluez5/bluez5-device.c:1892
 #, c-format
 msgid "Headset Head Unit (HSP/HFP, codec %s)"
-msgstr "Kopfhörer-Garnitur (HSP/HFP, Codec %s)"
-
-#: spa/plugins/bluez5/bluez5-device.c:1913
-msgid "Headset Head Unit (HSP/HFP)"
-msgstr "Kopfhörer-Garnitur (HSP/HFP)"
-
-#: spa/plugins/bluez5/bluez5-device.c:1993
-#: spa/plugins/bluez5/bluez5-device.c:1998
-#: spa/plugins/bluez5/bluez5-device.c:2005
-#: spa/plugins/bluez5/bluez5-device.c:2011
-#: spa/plugins/bluez5/bluez5-device.c:2017
-#: spa/plugins/bluez5/bluez5-device.c:2023
-#: spa/plugins/bluez5/bluez5-device.c:2029
-#: spa/plugins/bluez5/bluez5-device.c:2035
-#: spa/plugins/bluez5/bluez5-device.c:2041
+msgstr "Sprechkopfhörer-Einheit (HSP/HFP, Codec %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1973
+#: spa/plugins/bluez5/bluez5-device.c:1978
+#: spa/plugins/bluez5/bluez5-device.c:1985
+#: spa/plugins/bluez5/bluez5-device.c:1991
+#: spa/plugins/bluez5/bluez5-device.c:1997
+#: spa/plugins/bluez5/bluez5-device.c:2003
+#: spa/plugins/bluez5/bluez5-device.c:2009
+#: spa/plugins/bluez5/bluez5-device.c:2015
+#: spa/plugins/bluez5/bluez5-device.c:2021
 msgid "Handsfree"
 msgstr "Freisprech"
 
-#: spa/plugins/bluez5/bluez5-device.c:1999
+#: spa/plugins/bluez5/bluez5-device.c:1979
 msgid "Handsfree (HFP)"
 msgstr "Freisprech (HFP)"
 
-#: spa/plugins/bluez5/bluez5-device.c:2016
+#: spa/plugins/bluez5/bluez5-device.c:1996
 msgid "Headphone"
 msgstr "Kopfhörer"
 
-#: spa/plugins/bluez5/bluez5-device.c:2022
+#: spa/plugins/bluez5/bluez5-device.c:2002
 msgid "Portable"
 msgstr "Tragbar"
 
-#: spa/plugins/bluez5/bluez5-device.c:2028
+#: spa/plugins/bluez5/bluez5-device.c:2008
 msgid "Car"
 msgstr "Auto"
 
-#: spa/plugins/bluez5/bluez5-device.c:2034
+#: spa/plugins/bluez5/bluez5-device.c:2014
 msgid "HiFi"
 msgstr "HiFi"
 
-#: spa/plugins/bluez5/bluez5-device.c:2040
+#: spa/plugins/bluez5/bluez5-device.c:2020
 msgid "Phone"
 msgstr "Telefon"
 
-#: spa/plugins/bluez5/bluez5-device.c:2047
+#: spa/plugins/bluez5/bluez5-device.c:2027
 msgid "Bluetooth"
 msgstr "Bluetooth"
 
-#: spa/plugins/bluez5/bluez5-device.c:2048
+#: spa/plugins/bluez5/bluez5-device.c:2028
 msgid "Bluetooth (HFP)"
 msgstr "Bluetooth (HFP)"
+
+#~ msgid "Headset Head Unit (HSP/HFP)"
+#~ msgstr "Kopfhörer-Garnitur (HSP/HFP)"
\ No newline at end of file
diff --git a/po/fi.po b/po/fi.po
index 0a0bb148..a9cba413 100644
--- a/po/fi.po
+++ b/po/fi.po
@@ -7,13 +7,11 @@
 msgid ""
 msgstr ""
 "Project-Id-Version: git trunk\n"
-"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/"
-"issues/new\n"
-"POT-Creation-Date: 2021-04-18 16:31+0300\n"
-"PO-Revision-Date: 2021-04-18 16:38+0300\n"
-"Last-Translator: Ricky Tigg <ricky.tigg@gmail.com>\n"
-"Language-Team: Finnish <https://translate.fedoraproject.org/projects/"
-"pipewire/pipewire/fi/>\n"
+"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/issues/new\n"
+"POT-Creation-Date: 2024-10-12 11:50+0300\n"
+"PO-Revision-Date: 2024-10-12 12:04+0300\n"
+"Last-Translator: Pauli Virtanen <pav@iki.fi>\n"
+"Language-Team: Finnish <https://translate.fedoraproject.org/projects/pipewire/pipewire/fi/>\n"
 "Language: fi\n"
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=utf-8\n"
@@ -21,18 +19,22 @@ msgstr ""
 "Plural-Forms: nplurals=2; plural=n != 1;\n"
 "X-Generator: Weblate 4.5.1\n"
 
-#: src/daemon/pipewire.c:43
+#: src/daemon/pipewire.c:29
 #, c-format
 msgid ""
 "%s [options]\n"
 "  -h, --help                            Show this help\n"
+"  -v, --verbose                         Increase verbosity by one level\n"
 "      --version                         Show version\n"
 "  -c, --config                          Load config (Default %s)\n"
+"  -P  --properties                      Set context properties\n"
 msgstr ""
 "%s [valinnat]\n"
 "  -h, --help                            Näytä tämä ohje\n"
+"  -v, --verbose                         Lisää viestien yksityiskohtaisuutta\n"
 "      --version                         Näytä versio\n"
 "  -c, --config                          Lataa asetukset (oletus %s)\n"
+"  -P, --properties                      Aseta kontekstin ominaisuudet\n"
 
 #: src/daemon/pipewire.desktop.in:4
 msgid "PipeWire Media System"
@@ -42,68 +44,82 @@ msgstr "PipeWire-mediajärjestelmä"
 msgid "Start the PipeWire Media System"
 msgstr "Käynnistä PipeWire-mediajärjestelmä"
 
-#: src/examples/media-session/alsa-monitor.c:526
-#: spa/plugins/alsa/acp/compat.c:187
-msgid "Built-in Audio"
-msgstr "Sisäinen äänentoisto"
+#: src/modules/module-protocol-pulse/modules/module-tunnel-sink.c:159
+#: src/modules/module-protocol-pulse/modules/module-tunnel-source.c:159
+#, c-format
+msgid "Tunnel to %s%s%s"
+msgstr "Tunneli: %s%s%s"
 
-#: src/examples/media-session/alsa-monitor.c:530
-#: spa/plugins/alsa/acp/compat.c:192
-msgid "Modem"
-msgstr "Modeemi"
+#: src/modules/module-fallback-sink.c:40
+msgid "Dummy Output"
+msgstr "Valeulostulo"
 
-#: src/examples/media-session/alsa-monitor.c:539
+#: src/modules/module-pulse-tunnel.c:777
+#, c-format
+msgid "Tunnel for %s@%s"
+msgstr "Tunneli: %s@%s"
+
+#: src/modules/module-zeroconf-discover.c:320
 msgid "Unknown device"
 msgstr "Tuntematon laite"
 
-#: src/tools/pw-cat.c:991
+#: src/modules/module-zeroconf-discover.c:332
+#, c-format
+msgid "%s on %s@%s"
+msgstr "%s koneella %s@%s"
+
+#: src/modules/module-zeroconf-discover.c:336
+#, c-format
+msgid "%s on %s"
+msgstr "%s koneella %s"
+
+#: src/tools/pw-cat.c:973
 #, 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 [valinnat] <tiedosto>\n"
+"%s [valinnat] [<tiedosto>|-]\n"
 "  -h, --help                            Näytä tämä ohje\n"
 "      --version                         Näytä versio\n"
 "  -v, --verbose                         Näytä lisää tietoja\n"
 "\n"
 
-#: src/tools/pw-cat.c:998
+#: src/tools/pw-cat.c:980
 #, c-format
 msgid ""
 "  -R, --remote                          Remote daemon name\n"
 "      --media-type                      Set media type (default %s)\n"
 "      --media-category                  Set media category (default %s)\n"
 "      --media-role                      Set media role (default %s)\n"
-"      --target                          Set node target (default %s)\n"
+"      --target                          Set node target serial or name "
+"(default %s)\n"
 "                                          0 means don't link\n"
 "      --latency                         Set node latency (default %s)\n"
 "                                          Xunit (unit = s, ms, us, ns)\n"
 "                                          or direct samples (256)\n"
 "                                          the rate is the one of the source "
 "file\n"
-"      --list-targets                    List available targets for --target\n"
+"  -P  --properties                      Set node properties\n"
 "\n"
 msgstr ""
 "  -R, --remote                          Vastapään taustaprosessin nimi\n"
 "      --media-type                      Aseta mediatyyppi (oletus %s)\n"
 "      --media-category                  Aseta medialuokka (oletus %s)\n"
 "      --media-role                      Aseta mediarooli (oletus %s)\n"
-"      --target                          Aseta kohdesolmu (oletus %s)\n"
+"      --target                          Aseta kohteen numero/nimi (oletus %s)\n"
 "                                          0 tarkoittaa: ei linkkiä\n"
 "      --latency                         Aseta solmun viive (oletus %s)\n"
-"                                          Xyksikkö (yksikkö = s, ms, us, "
-"ns)\n"
+"                                          Xyksikkö (yksikkö = s, ms, us, ns)\n"
 "                                          tai näytteiden lukumäärä (256)\n"
-"                                          näytetaajuus on lähteen mukaan\n"
-"      --list-targets                    Näytä --target:n mahdolliset "
-"kohteet\n"
+"                                          näytetaajuus on tiedoston mukainen\n"
+"  -P  --properties                      Aseta solmun ominaisuudet\n"
 "\n"
 
-#: src/tools/pw-cat.c:1016
+#: src/tools/pw-cat.c:998
 #, c-format
 msgid ""
 "      --rate                            Sample rate (req. for rec) (default "
@@ -120,38 +136,37 @@ msgid ""
 "      --volume                          Stream volume 0-1.0 (default %.3f)\n"
 "  -q  --quality                         Resampler quality (0 - 15) (default "
 "%d)\n"
+"  -a, --raw                             RAW mode\n"
 "\n"
 msgstr ""
-"      --rate                            Näytetaajuus (pakoll. nauhoit.) "
-"(oletus %u)\n"
-"      --channels                        Kanavien määrä (pakoll. nauhoit.) "
-"(oletus %u)\n"
+"      --rate                            Näytetaajuus (pakoll. nauhoit.) (oletus %u)\n"
+"      --channels                        Kanavien määrä (pakoll. nauhoit.) (oletus %u)\n"
 "      --channel-map                     Kanavakartta\n"
-"                                            vaihtoehdot: \"stereo\", "
-"\"surround-51\",... tai\n"
-"                                            pilkulla erotetut kanavien "
-"nimet: esim. \"FL,FR\"\n"
-"      --format                          Näytemuoto %s (pakoll. nauhoit.) "
-"(oletus %s)\n"
-"      --volume                          Vuon äänenvoimakkuus 0-1.0 (oletus "
-"%.3f)\n"
-"  -q  --quality                         Resamplerin laatu (0 - 15) (oletus "
-"%d)\n"
+"                                            vaihtoehdot: \"stereo\", \"surround-51\",... tai\n"
+"                                            pilkulla erotetut kanavien nimet: esim. \"FL,FR\"\n"
+"      --format                          Näytemuoto %s (pakoll. nauhoit.) (oletus %s)\n"
+"      --volume                          Vuon äänenvoimakkuus 0-1.0 (oletus %.3f)\n"
+"  -q  --quality                         Resamplerin laatu (0 - 15) (oletus %d)\n"
+"  -a  --raw                             Muotoilemattoman äänidatan tila\n"
 "\n"
 
-#: src/tools/pw-cat.c:1033
+#: src/tools/pw-cat.c:1016
 msgid ""
 "  -p, --playback                        Playback mode\n"
 "  -r, --record                          Recording mode\n"
 "  -m, --midi                            Midi mode\n"
+"  -d, --dsd                             DSD mode\n"
+"  -o, --encoded                         Encoded mode\n"
 "\n"
 msgstr ""
 "  -p, --playback                        Toisto\n"
 "  -r, --record                          Nauhoitus\n"
 "  -m, --midi                            MIDI-tila\n"
+"  -d, --dsd                             DSD-tila\n"
+"  -o, --encoded                         Koodatun audion tila\n"
 "\n"
 
-#: src/tools/pw-cli.c:2932
+#: src/tools/pw-cli.c:2318
 #, c-format
 msgid ""
 "%s [options] [command]\n"
@@ -159,209 +174,206 @@ msgid ""
 "      --version                         Show version\n"
 "  -d, --daemon                          Start as daemon (Default false)\n"
 "  -r, --remote                          Remote daemon name\n"
+"  -m, --monitor                         Monitor activity\n"
 "\n"
 msgstr ""
 "%s [valinnat] [komento]\n"
 "  -h, --help                            Näytä tämä ohje\n"
 "      --version                         Näytä versio\n"
-"  -d, --daemon                          Käynnistä taustaprosessina (oletus: "
-"ei)\n"
+"  -d, --daemon                          Käynnistä taustaprosessina (oletus: ei)\n"
 "  -r, --remote                          Taustaprosessin nimi\n"
+"  -m, --monitor                         Seuraa tapahtumia\n"
 "\n"
 
-#: spa/plugins/alsa/acp/acp.c:290
+#: spa/plugins/alsa/acp/acp.c:327
 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:487 spa/plugins/alsa/acp/alsa-mixer.c:4633
+#: spa/plugins/bluez5/bluez5-device.c:1696
 msgid "Off"
 msgstr "Poissa"
 
-#: spa/plugins/alsa/acp/channelmap.h:466
-msgid "(invalid)"
-msgstr "(virheellinen)"
-
-#: spa/plugins/alsa/acp/alsa-mixer.c:2709
+#: spa/plugins/alsa/acp/alsa-mixer.c:2652
 msgid "Input"
 msgstr "Sisääntulo"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2710
+#: spa/plugins/alsa/acp/alsa-mixer.c:2653
 msgid "Docking Station Input"
 msgstr "Telakan sisääntulo"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2711
+#: spa/plugins/alsa/acp/alsa-mixer.c:2654
 msgid "Docking Station Microphone"
 msgstr "Telakan mikrofoni"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2712
+#: spa/plugins/alsa/acp/alsa-mixer.c:2655
 msgid "Docking Station Line In"
 msgstr "Telakan linjasisääntulo"
 
-#: 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 "Linjasisääntulo"
 
-#: 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:1984
 msgid "Microphone"
 msgstr "Mikrofoni"
 
-#: 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 "Etumikrofoni"
 
-#: 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 "Takamikrofoni"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2717
+#: spa/plugins/alsa/acp/alsa-mixer.c:2660
 msgid "External Microphone"
 msgstr "Ulkoinen mikrofoni"
 
-#: 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 "Sisäinen mikrofoni"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2719
-#: spa/plugins/alsa/acp/alsa-mixer.c:2805
+#: spa/plugins/alsa/acp/alsa-mixer.c:2662
+#: spa/plugins/alsa/acp/alsa-mixer.c:2748
 msgid "Radio"
 msgstr "Radio"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2720
-#: spa/plugins/alsa/acp/alsa-mixer.c:2806
+#: spa/plugins/alsa/acp/alsa-mixer.c:2663
+#: spa/plugins/alsa/acp/alsa-mixer.c:2749
 msgid "Video"
 msgstr "Video"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2721
+#: spa/plugins/alsa/acp/alsa-mixer.c:2664
 msgid "Automatic Gain Control"
 msgstr "Automaattinen äänenvoimakkuuden säätö"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2722
+#: spa/plugins/alsa/acp/alsa-mixer.c:2665
 msgid "No Automatic Gain Control"
 msgstr "Ei automaattista äänenvoimakkuuden säätöä"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2723
+#: spa/plugins/alsa/acp/alsa-mixer.c:2666
 msgid "Boost"
 msgstr "Vahvistus"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2724
+#: spa/plugins/alsa/acp/alsa-mixer.c:2667
 msgid "No Boost"
 msgstr "Ei vahvistusta"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2725
+#: spa/plugins/alsa/acp/alsa-mixer.c:2668
 msgid "Amplifier"
 msgstr "Vahvistin"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2726
+#: spa/plugins/alsa/acp/alsa-mixer.c:2669
 msgid "No Amplifier"
 msgstr "Ei vahvistinta"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2727
+#: spa/plugins/alsa/acp/alsa-mixer.c:2670
 msgid "Bass Boost"
 msgstr "Bassonvahvistus"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2728
+#: spa/plugins/alsa/acp/alsa-mixer.c:2671
 msgid "No Bass Boost"
 msgstr "Ei basson vahvistusta"
 
-#: 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:1990
 msgid "Speaker"
 msgstr "Kaiutin"
 
-#: 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 "Kuulokkeet"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2797
+#: spa/plugins/alsa/acp/alsa-mixer.c:2740
 msgid "Analog Input"
 msgstr "Analoginen sisääntulo"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2801
+#: spa/plugins/alsa/acp/alsa-mixer.c:2744
 msgid "Dock Microphone"
 msgstr "Telakan mikrofoni"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2803
+#: spa/plugins/alsa/acp/alsa-mixer.c:2746
 msgid "Headset Microphone"
 msgstr "Kuulokkeiden mikrofoni"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2807
+#: spa/plugins/alsa/acp/alsa-mixer.c:2750
 msgid "Analog Output"
 msgstr "Analoginen ulostulo"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2809
+#: spa/plugins/alsa/acp/alsa-mixer.c:2752
 msgid "Headphones 2"
 msgstr "Kuulokkeet 2"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2810
+#: spa/plugins/alsa/acp/alsa-mixer.c:2753
 msgid "Headphones Mono Output"
 msgstr "Kuulokkeiden monoulostulo"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2811
+#: spa/plugins/alsa/acp/alsa-mixer.c:2754
 msgid "Line Out"
 msgstr "Linjaulostulo"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2812
+#: spa/plugins/alsa/acp/alsa-mixer.c:2755
 msgid "Analog Mono Output"
 msgstr "Analoginen monoulostulo"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2813
+#: spa/plugins/alsa/acp/alsa-mixer.c:2756
 msgid "Speakers"
 msgstr "Kaiuttimet"
 
-#: 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 "Digitaalinen ulostulo (S/PDIF)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2816
+#: spa/plugins/alsa/acp/alsa-mixer.c:2759
 msgid "Digital Input (S/PDIF)"
 msgstr "Digitaalinen sisääntulo (S/PDIF)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2817
+#: spa/plugins/alsa/acp/alsa-mixer.c:2760
 msgid "Multichannel Input"
 msgstr "Monikanavainen sisääntulo"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2818
+#: spa/plugins/alsa/acp/alsa-mixer.c:2761
 msgid "Multichannel Output"
 msgstr "Monikanavainen ulostulo"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2819
+#: spa/plugins/alsa/acp/alsa-mixer.c:2762
 msgid "Game Output"
 msgstr "Peli-ulostulo"
 
-#: 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 "Puhe-ulostulo"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2822
+#: spa/plugins/alsa/acp/alsa-mixer.c:2765
 msgid "Chat Input"
 msgstr "Puhe-sisääntulo"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2823
+#: spa/plugins/alsa/acp/alsa-mixer.c:2766
 msgid "Virtual Surround 7.1"
 msgstr "Virtuaalinen tilaääni 7.1"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4527
+#: spa/plugins/alsa/acp/alsa-mixer.c:4456
 msgid "Analog Mono"
 msgstr "Analoginen mono"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4528
+#: spa/plugins/alsa/acp/alsa-mixer.c:4457
 msgid "Analog Mono (Left)"
 msgstr "Analoginen mono (vasen)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4529
+#: spa/plugins/alsa/acp/alsa-mixer.c:4458
 msgid "Analog Mono (Right)"
 msgstr "Analoginen mono (oikea)"
 
@@ -370,147 +382,147 @@ msgstr "Analoginen mono (oikea)"
 #. * 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:4459
+#: spa/plugins/alsa/acp/alsa-mixer.c:4467
+#: spa/plugins/alsa/acp/alsa-mixer.c:4468
 msgid "Analog Stereo"
 msgstr "Analoginen stereo"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4531
+#: spa/plugins/alsa/acp/alsa-mixer.c:4460
 msgid "Mono"
 msgstr "Mono"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4532
+#: spa/plugins/alsa/acp/alsa-mixer.c:4461
 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:4469
+#: spa/plugins/alsa/acp/alsa-mixer.c:4627
+#: spa/plugins/bluez5/bluez5-device.c:1972
 msgid "Headset"
 msgstr "Kuulokkeet"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4541
-#: spa/plugins/alsa/acp/alsa-mixer.c:4699
+#: spa/plugins/alsa/acp/alsa-mixer.c:4470
+#: spa/plugins/alsa/acp/alsa-mixer.c:4628
 msgid "Speakerphone"
 msgstr "Kaiutinpuhelin"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4542
-#: spa/plugins/alsa/acp/alsa-mixer.c:4543
+#: spa/plugins/alsa/acp/alsa-mixer.c:4471
+#: spa/plugins/alsa/acp/alsa-mixer.c:4472
 msgid "Multichannel"
 msgstr "Monikanavainen"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4544
+#: spa/plugins/alsa/acp/alsa-mixer.c:4473
 msgid "Analog Surround 2.1"
 msgstr "Analoginen tilaääni 2.1"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4545
+#: spa/plugins/alsa/acp/alsa-mixer.c:4474
 msgid "Analog Surround 3.0"
 msgstr "Analoginen tilaääni 3.0"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4546
+#: spa/plugins/alsa/acp/alsa-mixer.c:4475
 msgid "Analog Surround 3.1"
 msgstr "Analoginen tilaääni 3.1"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4547
+#: spa/plugins/alsa/acp/alsa-mixer.c:4476
 msgid "Analog Surround 4.0"
 msgstr "Analoginen tilaääni 4.0"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4548
+#: spa/plugins/alsa/acp/alsa-mixer.c:4477
 msgid "Analog Surround 4.1"
 msgstr "Analoginen tilaääni 4.1"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4549
+#: spa/plugins/alsa/acp/alsa-mixer.c:4478
 msgid "Analog Surround 5.0"
 msgstr "Analoginen tilaääni 5.0"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4550
+#: spa/plugins/alsa/acp/alsa-mixer.c:4479
 msgid "Analog Surround 5.1"
 msgstr "Analoginen tilaääni 5.1"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4551
+#: spa/plugins/alsa/acp/alsa-mixer.c:4480
 msgid "Analog Surround 6.0"
 msgstr "Analoginen tilaääni 6.0"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4552
+#: spa/plugins/alsa/acp/alsa-mixer.c:4481
 msgid "Analog Surround 6.1"
 msgstr "Analoginen tilaääni 6.1"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4553
+#: spa/plugins/alsa/acp/alsa-mixer.c:4482
 msgid "Analog Surround 7.0"
 msgstr "Analoginen tilaääni 7.0"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4554
+#: spa/plugins/alsa/acp/alsa-mixer.c:4483
 msgid "Analog Surround 7.1"
 msgstr "Analoginen tilaääni 7.1"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4555
+#: spa/plugins/alsa/acp/alsa-mixer.c:4484
 msgid "Digital Stereo (IEC958)"
 msgstr "Digitaalinen stereo (IEC958)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4556
+#: spa/plugins/alsa/acp/alsa-mixer.c:4485
 msgid "Digital Surround 4.0 (IEC958/AC3)"
 msgstr "Digitaalinen tilaääni 4.0 (IEC958/AC3)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4557
+#: spa/plugins/alsa/acp/alsa-mixer.c:4486
 msgid "Digital Surround 5.1 (IEC958/AC3)"
 msgstr "Digitaalinen tilaääni 5.1 (IEC958/AC3)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4558
+#: spa/plugins/alsa/acp/alsa-mixer.c:4487
 msgid "Digital Surround 5.1 (IEC958/DTS)"
 msgstr "Digitaalinen tilaääni 5.1 (IEC958/DTS)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4559
+#: spa/plugins/alsa/acp/alsa-mixer.c:4488
 msgid "Digital Stereo (HDMI)"
 msgstr "Digitaalinen stereo (HDMI)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4560
+#: spa/plugins/alsa/acp/alsa-mixer.c:4489
 msgid "Digital Surround 5.1 (HDMI)"
 msgstr "Digitaalinen tilaääni 5.1 (HDMI)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4561
+#: spa/plugins/alsa/acp/alsa-mixer.c:4490
 msgid "Chat"
 msgstr "Puhe"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4562
+#: spa/plugins/alsa/acp/alsa-mixer.c:4491
 msgid "Game"
 msgstr "Peli"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4696
+#: spa/plugins/alsa/acp/alsa-mixer.c:4625
 msgid "Analog Mono Duplex"
 msgstr "Analoginen mono, molempisuuntainen"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4697
+#: spa/plugins/alsa/acp/alsa-mixer.c:4626
 msgid "Analog Stereo Duplex"
 msgstr "Analoginen stereo, molempisuuntainen"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4700
+#: spa/plugins/alsa/acp/alsa-mixer.c:4629
 msgid "Digital Stereo Duplex (IEC958)"
 msgstr "Digitaalinen stereo, molempisuuntainen (IEC958)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4701
+#: spa/plugins/alsa/acp/alsa-mixer.c:4630
 msgid "Multichannel Duplex"
 msgstr "Monikanavainen, molempisuuntainen"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4702
+#: spa/plugins/alsa/acp/alsa-mixer.c:4631
 msgid "Stereo Duplex"
 msgstr "Stereo, molempisuuntainen"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4703
+#: spa/plugins/alsa/acp/alsa-mixer.c:4632
 msgid "Mono Chat + 7.1 Surround"
 msgstr "Mono-puhe + 7.1 tilaääni"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4806
+#: spa/plugins/alsa/acp/alsa-mixer.c:4733
 #, c-format
 msgid "%s Output"
 msgstr "%s, ulostulo"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4813
+#: spa/plugins/alsa/acp/alsa-mixer.c:4741
 #, c-format
 msgid "%s Input"
 msgstr "%s, sisääntulo"
 
-#: spa/plugins/alsa/acp/alsa-util.c:1175 spa/plugins/alsa/acp/alsa-util.c:1269
+#: spa/plugins/alsa/acp/alsa-util.c:1231 spa/plugins/alsa/acp/alsa-util.c:1325
 #, c-format
 msgid ""
 "snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu "
@@ -531,16 +543,16 @@ msgstr[1] ""
 "Tämä on todennäköisesti ohjelmavirhe ALSA-ajurissa ”%s”. Ilmoita tästä "
 "ongelmasta ALSA-kehittäjille."
 
-#: spa/plugins/alsa/acp/alsa-util.c:1241
+#: spa/plugins/alsa/acp/alsa-util.c:1297
 #, 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] ""
@@ -554,7 +566,7 @@ msgstr[1] ""
 "Tämä on todennäköisesti ohjelmavirhe ALSA-ajurissa ”%s”. Ilmoita tästä "
 "ongelmasta ALSA-kehittäjille."
 
-#: spa/plugins/alsa/acp/alsa-util.c:1288
+#: spa/plugins/alsa/acp/alsa-util.c:1344
 #, c-format
 msgid ""
 "snd_pcm_avail_delay() returned strange values: delay %lu is less than avail "
@@ -567,7 +579,7 @@ msgstr ""
 "Tämä on todennäköisesti ohjelmavirhe ALSA-ajurissa ”%s”. Ilmoita tästä "
 "ongelmasta ALSA-kehittäjille."
 
-#: spa/plugins/alsa/acp/alsa-util.c:1331
+#: spa/plugins/alsa/acp/alsa-util.c:1387
 #, c-format
 msgid ""
 "snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu byte "
@@ -590,61 +602,112 @@ msgstr[1] ""
 "Tämä on todennäköisesti ohjelmavirhe ALSA-ajurissa ”%s”. Ilmoita tästä "
 "ongelmasta ALSA-kehittäjille."
 
-#: spa/plugins/bluez5/bluez5-device.c:1010
+#: spa/plugins/alsa/acp/channelmap.h:457
+msgid "(invalid)"
+msgstr "(virheellinen)"
+
+#: spa/plugins/alsa/acp/compat.c:193
+msgid "Built-in Audio"
+msgstr "Sisäinen äänentoisto"
+
+#: spa/plugins/alsa/acp/compat.c:198
+msgid "Modem"
+msgstr "Modeemi"
+
+#: spa/plugins/bluez5/bluez5-device.c:1707
 msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
 msgstr "Ääniyhdyskäytävä (A2DP-lähde & HSP/HFP AG)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1033
+#: spa/plugins/bluez5/bluez5-device.c:1755
 #, c-format
 msgid "High Fidelity Playback (A2DP Sink, codec %s)"
 msgstr "Korkealaatuinen toisto (A2DP-kohde, %s-koodekki)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1035
+#: spa/plugins/bluez5/bluez5-device.c:1758
 #, c-format
 msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
 msgstr "Korkealaatuinen molempisuuntainen (A2DP-lähde/kohde, %s-koodekki)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1041
+#: spa/plugins/bluez5/bluez5-device.c:1766
 msgid "High Fidelity Playback (A2DP Sink)"
 msgstr "Korkealaatuinen toisto (A2DP-kohde)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1043
+#: spa/plugins/bluez5/bluez5-device.c:1768
 msgid "High Fidelity Duplex (A2DP Source/Sink)"
 msgstr "Korkealaatuinen molempisuuntainen (A2DP-lähde/kohde)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1070
+#: spa/plugins/bluez5/bluez5-device.c:1818
+#, c-format
+msgid "High Fidelity Playback (BAP Sink, codec %s)"
+msgstr "Korkealaatuinen toisto (BAP-kohde, %s-koodekki)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1823
+#, c-format
+msgid "High Fidelity Input (BAP Source, codec %s)"
+msgstr "Korkealaatuinen sisääntulo (BAP-lähde, %s-koodekki)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1827
+#, c-format
+msgid "High Fidelity Duplex (BAP Source/Sink, codec %s)"
+msgstr "Korkealaatuinen molempisuuntainen (BAP-lähde/kohde, %s-koodekki)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1836
+msgid "High Fidelity Playback (BAP Sink)"
+msgstr "Korkealaatuinen toisto (BAP-kohde)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1840
+msgid "High Fidelity Input (BAP Source)"
+msgstr "Korkealaatuinen sisääntulo (BAP-lähde)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1843
+msgid "High Fidelity Duplex (BAP Source/Sink)"
+msgstr "Korkealaatuinen molempisuuntainen (BAP-lähde/kohde)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1892
 #, c-format
 msgid "Headset Head Unit (HSP/HFP, codec %s)"
 msgstr "Kuulokemikrofoni (HSP/HFP, %s-koodekki)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1074
-msgid "Headset Head Unit (HSP/HFP)"
-msgstr "Kuulokemikrofoni (HSP/HFP)"
-
-#: spa/plugins/bluez5/bluez5-device.c:1140
+#: spa/plugins/bluez5/bluez5-device.c:1973
+#: spa/plugins/bluez5/bluez5-device.c:1978
+#: spa/plugins/bluez5/bluez5-device.c:1985
+#: spa/plugins/bluez5/bluez5-device.c:1991
+#: spa/plugins/bluez5/bluez5-device.c:1997
+#: spa/plugins/bluez5/bluez5-device.c:2003
+#: spa/plugins/bluez5/bluez5-device.c:2009
+#: spa/plugins/bluez5/bluez5-device.c:2015
+#: spa/plugins/bluez5/bluez5-device.c:2021
 msgid "Handsfree"
 msgstr "Kuulokemikrofoni"
 
-#: spa/plugins/bluez5/bluez5-device.c:1155
+#: spa/plugins/bluez5/bluez5-device.c:1979
+msgid "Handsfree (HFP)"
+msgstr "Kuulokemikrofoni (HFP)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1996
 msgid "Headphone"
 msgstr "Kuulokkeet"
 
-#: spa/plugins/bluez5/bluez5-device.c:1160
+#: spa/plugins/bluez5/bluez5-device.c:2002
 msgid "Portable"
 msgstr "Kannettava"
 
-#: spa/plugins/bluez5/bluez5-device.c:1165
+#: spa/plugins/bluez5/bluez5-device.c:2008
 msgid "Car"
 msgstr "Auto"
 
-#: spa/plugins/bluez5/bluez5-device.c:1170
+#: spa/plugins/bluez5/bluez5-device.c:2014
 msgid "HiFi"
 msgstr "Hi-Fi"
 
-#: spa/plugins/bluez5/bluez5-device.c:1175
+#: spa/plugins/bluez5/bluez5-device.c:2020
 msgid "Phone"
 msgstr "Puhelin"
 
-#: spa/plugins/bluez5/bluez5-device.c:1181
+#: spa/plugins/bluez5/bluez5-device.c:2027
 msgid "Bluetooth"
 msgstr "Bluetooth"
+
+#: spa/plugins/bluez5/bluez5-device.c:2028
+msgid "Bluetooth (HFP)"
+msgstr "Bluetooth (HFP)"
diff --git a/po/id.po b/po/id.po
index f6700a8d..f4767be5 100644
--- a/po/id.po
+++ b/po/id.po
@@ -3,24 +3,24 @@
 # This file is distributed under the same license as the pipewire package.
 #
 # Translators:
-# Andika Triwidada <andika@gmail.com>, 2011, 2012, 2018.
+# Andika Triwidada <andika@gmail.com>, 2011, 2012, 2018, 2021, 2024.
 msgid ""
 msgstr ""
 "Project-Id-Version: PipeWire master\n"
-"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/-/"
-"issues\n"
-"POT-Creation-Date: 2021-05-16 13:13+0000\n"
-"PO-Revision-Date: 2021-08-11 10:50+0700\n"
+"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/"
+"issues/new\n"
+"POT-Creation-Date: 2024-02-25 03:43+0300\n"
+"PO-Revision-Date: 2024-11-03 14:23+0700\n"
 "Last-Translator: Andika Triwidada <andika@gmail.com>\n"
 "Language-Team: Indonesia <id@li.org>\n"
 "Language: id\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 2.2.1\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+"X-Generator: Poedit 3.5\n"
 
-#: src/daemon/pipewire.c:45
+#: src/daemon/pipewire.c:26
 #, c-format
 msgid ""
 "%s [options]\n"
@@ -41,38 +41,31 @@ msgstr "Sistem Media PipeWire"
 msgid "Start the PipeWire Media System"
 msgstr "Memulai Sistem Media PipeWire"
 
-#: src/examples/media-session/alsa-monitor.c:585
-#: spa/plugins/alsa/acp/compat.c:187
-msgid "Built-in Audio"
-msgstr "Audio Bawaan"
-
-#: src/examples/media-session/alsa-monitor.c:589
-#: spa/plugins/alsa/acp/compat.c:192
-msgid "Modem"
-msgstr "Modem"
-
-#: src/examples/media-session/alsa-monitor.c:598
-#: src/modules/module-zeroconf-discover.c:290
-msgid "Unknown device"
-msgstr "Perangkat tak dikenal"
-
-#: src/modules/module-protocol-pulse/modules/module-tunnel-sink.c:182
-#: src/modules/module-protocol-pulse/modules/module-tunnel-source.c:182
+#: src/modules/module-protocol-pulse/modules/module-tunnel-sink.c:159
+#: src/modules/module-protocol-pulse/modules/module-tunnel-source.c:159
 #, c-format
-msgid "Tunnel to %s/%s"
-msgstr "Tunnel ke %s/%s"
+msgid "Tunnel to %s%s%s"
+msgstr "Tunnel ke %s%s%s"
 
-#: src/modules/module-pulse-tunnel.c:511
+#: src/modules/module-fallback-sink.c:40
+msgid "Dummy Output"
+msgstr "Keluaran Dummy"
+
+#: src/modules/module-pulse-tunnel.c:774
 #, c-format
 msgid "Tunnel for %s@%s"
 msgstr "Tunnel untuk %s@%s"
 
-#: src/modules/module-zeroconf-discover.c:302
+#: src/modules/module-zeroconf-discover.c:315
+msgid "Unknown device"
+msgstr "Perangkat tak dikenal"
+
+#: src/modules/module-zeroconf-discover.c:327
 #, c-format
 msgid "%s on %s@%s"
 msgstr "%s pada %s@%s"
 
-#: src/modules/module-zeroconf-discover.c:306
+#: src/modules/module-zeroconf-discover.c:331
 #, c-format
 msgid "%s on %s"
 msgstr "%s pada %s"
@@ -80,16 +73,16 @@ msgstr "%s pada %s"
 #: src/tools/pw-cat.c:991
 #, 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 [opsi] <berkas>\n"
-"  -h, --help                            Menampilkan bantuan ini\n"
-"      --version                         Menampilkan versi\n"
-"  -v, --verbose                         Memfungsikan pesan rinci\n"
+"%s [opsi] [<berkas>|-]\n"
+"  -h, --help                            Tampilkan bantuan ini\n"
+"      --version                         Tampilkan versi\n"
+"  -v, --verbose                         Fungsikan pesan rinci\n"
 "\n"
 
 #: src/tools/pw-cat.c:998
@@ -99,14 +92,15 @@ msgid ""
 "      --media-type                      Set media type (default %s)\n"
 "      --media-category                  Set media category (default %s)\n"
 "      --media-role                      Set media role (default %s)\n"
-"      --target                          Set node target (default %s)\n"
+"      --target                          Set node target serial or name "
+"(default %s)\n"
 "                                          0 means don't link\n"
 "      --latency                         Set node latency (default %s)\n"
 "                                          Xunit (unit = s, ms, us, ns)\n"
 "                                          or direct samples (256)\n"
 "                                          the rate is the one of the source "
 "file\n"
-"      --list-targets                    List available targets for --target\n"
+"  -P  --properties                      Set node properties\n"
 "\n"
 msgstr ""
 "  -R, --remote                          Nama daemon remote\n"
@@ -120,8 +114,7 @@ msgstr ""
 "                                          or cuplikan langsung (256)\n"
 "                                          laju adalah satu dari berkas "
 "sumber\n"
-"      --list-targets                    Daftar target yang tersedia untuk --"
-"target\n"
+"  -P  --properties                      Atur properti simpul\n"
 "\n"
 
 #: src/tools/pw-cat.c:1016
@@ -149,11 +142,11 @@ msgstr ""
 "%u)\n"
 "      --channel-map                     Peta kanal\n"
 "                                            satu dari: \"stereo\", "
-"\"surround-51\",... or\n"
+"\"surround-51\",... atau\n"
 "                                            daftar dipisah koma dari nama "
 "kanal: mis. \"FL,FR\"\n"
-"      --format                          Format cuplikan %s (req. for rec) "
-"(baku %s)\n"
+"      --format                          Format cuplikan %s (perlu untuk "
+"rekam) (baku %s)\n"
 "      --volume                          Volume stream 0-1.0 (baku %.3f)\n"
 "  -q  --quality                         Kualitas resampler (0 - 15) (baku "
 "%d)\n"
@@ -164,14 +157,18 @@ msgid ""
 "  -p, --playback                        Playback mode\n"
 "  -r, --record                          Recording mode\n"
 "  -m, --midi                            Midi mode\n"
+"  -d, --dsd                             DSD mode\n"
+"  -o, --encoded                         Encoded mode\n"
 "\n"
 msgstr ""
 "  -p, --playback                        Mode main ulang\n"
 "  -r, --record                          Mode perekaman\n"
 "  -m, --midi                            Mode midi\n"
+"  -d, --dsd                             Mode DSD\n"
+"  -o, --encoded                         Mode di-enkode\n"
 "\n"
 
-#: src/tools/pw-cli.c:2959
+#: src/tools/pw-cli.c:2252
 #, c-format
 msgid ""
 "%s [options] [command]\n"
@@ -179,6 +176,7 @@ msgid ""
 "      --version                         Show version\n"
 "  -d, --daemon                          Start as daemon (Default false)\n"
 "  -r, --remote                          Remote daemon name\n"
+"  -m, --monitor                         Monitor activity\n"
 "\n"
 msgstr ""
 "%s [opsi] [perintah]\n"
@@ -186,197 +184,198 @@ msgstr ""
 "      --version                         Tampilkan versi\n"
 "  -d, --daemon                          Mulai sebagai daemon (Baku = false)\n"
 "  -r, --remote                          Nama daemon remote\n"
+"  -m, --monitor                         Monitor activitas\n"
 "\n"
 
-#: spa/plugins/alsa/acp/acp.c:291
+#: spa/plugins/alsa/acp/acp.c:327
 msgid "Pro Audio"
 msgstr "Pro Audio"
 
-#: spa/plugins/alsa/acp/acp.c:412 spa/plugins/alsa/acp/alsa-mixer.c:4704
-#: spa/plugins/bluez5/bluez5-device.c:1020
+#: spa/plugins/alsa/acp/acp.c:488 spa/plugins/alsa/acp/alsa-mixer.c:4633
+#: spa/plugins/bluez5/bluez5-device.c:1701
 msgid "Off"
 msgstr "Mati"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2709
+#: spa/plugins/alsa/acp/alsa-mixer.c:2652
 msgid "Input"
 msgstr "Masukan"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2710
+#: spa/plugins/alsa/acp/alsa-mixer.c:2653
 msgid "Docking Station Input"
 msgstr "Masukan Docking Station"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2711
+#: spa/plugins/alsa/acp/alsa-mixer.c:2654
 msgid "Docking Station Microphone"
 msgstr "Mikrofon Docking Station"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2712
+#: spa/plugins/alsa/acp/alsa-mixer.c:2655
 msgid "Docking Station Line In"
 msgstr "Docking Station Line In"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c: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 "Line In"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2714
-#: spa/plugins/alsa/acp/alsa-mixer.c:2798
-#: spa/plugins/bluez5/bluez5-device.c:1175
+#: spa/plugins/alsa/acp/alsa-mixer.c:2657
+#: spa/plugins/alsa/acp/alsa-mixer.c:2741
+#: spa/plugins/bluez5/bluez5-device.c:1989
 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 "Mikrofon Depan"
 
-#: 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 "Mikrofon Belakang"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2717
+#: spa/plugins/alsa/acp/alsa-mixer.c:2660
 msgid "External Microphone"
 msgstr "Mikrofon Eksternal"
 
-#: 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 "Mikrofon Internal"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2719
-#: spa/plugins/alsa/acp/alsa-mixer.c:2805
+#: spa/plugins/alsa/acp/alsa-mixer.c:2662
+#: spa/plugins/alsa/acp/alsa-mixer.c:2748
 msgid "Radio"
 msgstr "Radio"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2720
-#: spa/plugins/alsa/acp/alsa-mixer.c:2806
+#: spa/plugins/alsa/acp/alsa-mixer.c:2663
+#: spa/plugins/alsa/acp/alsa-mixer.c:2749
 msgid "Video"
 msgstr "Video"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2721
+#: spa/plugins/alsa/acp/alsa-mixer.c:2664
 msgid "Automatic Gain Control"
 msgstr "Kendali Penguatan Otomatis (AGC)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2722
+#: spa/plugins/alsa/acp/alsa-mixer.c:2665
 msgid "No Automatic Gain Control"
 msgstr "Tanpa Kendali Penguatan Otomatis (AGC)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2723
+#: spa/plugins/alsa/acp/alsa-mixer.c:2666
 msgid "Boost"
 msgstr "Boost"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2724
+#: spa/plugins/alsa/acp/alsa-mixer.c:2667
 msgid "No Boost"
 msgstr "Tanpa Boost"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2725
+#: spa/plugins/alsa/acp/alsa-mixer.c:2668
 msgid "Amplifier"
 msgstr "Penguat"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2726
+#: spa/plugins/alsa/acp/alsa-mixer.c:2669
 msgid "No Amplifier"
 msgstr "Tanpa Penguat"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2727
+#: spa/plugins/alsa/acp/alsa-mixer.c:2670
 msgid "Bass Boost"
 msgstr "Boost Bass"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2728
+#: spa/plugins/alsa/acp/alsa-mixer.c:2671
 msgid "No Bass Boost"
 msgstr "Tanpa Boost Bass"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2729
-#: spa/plugins/bluez5/bluez5-device.c:1180
+#: spa/plugins/alsa/acp/alsa-mixer.c:2672
+#: spa/plugins/bluez5/bluez5-device.c:1995
 msgid "Speaker"
 msgstr "Speaker"
 
-#: 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 "Headphone"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2797
+#: spa/plugins/alsa/acp/alsa-mixer.c:2740
 msgid "Analog Input"
 msgstr "Masukan Analog"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2801
+#: spa/plugins/alsa/acp/alsa-mixer.c:2744
 msgid "Dock Microphone"
 msgstr "Mikrofon Dok"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2803
+#: spa/plugins/alsa/acp/alsa-mixer.c:2746
 msgid "Headset Microphone"
 msgstr "Mikrofon Headset"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2807
+#: spa/plugins/alsa/acp/alsa-mixer.c:2750
 msgid "Analog Output"
 msgstr "Keluaran Analog"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2809
+#: spa/plugins/alsa/acp/alsa-mixer.c:2752
 msgid "Headphones 2"
 msgstr "Headphone 2"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2810
+#: spa/plugins/alsa/acp/alsa-mixer.c:2753
 msgid "Headphones Mono Output"
 msgstr "Keluaran Mono Headphone"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2811
+#: spa/plugins/alsa/acp/alsa-mixer.c:2754
 msgid "Line Out"
 msgstr "Line Out"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2812
+#: spa/plugins/alsa/acp/alsa-mixer.c:2755
 msgid "Analog Mono Output"
 msgstr "Keluaran Mono Analog"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2813
+#: spa/plugins/alsa/acp/alsa-mixer.c:2756
 msgid "Speakers"
 msgstr "Speaker"
 
-#: 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 "Keluaran Digital (S/PDIF)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2816
+#: spa/plugins/alsa/acp/alsa-mixer.c:2759
 msgid "Digital Input (S/PDIF)"
 msgstr "Masukan Digital (S/PDIF)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2817
+#: spa/plugins/alsa/acp/alsa-mixer.c:2760
 msgid "Multichannel Input"
 msgstr "Masukan Multikanal"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2818
+#: spa/plugins/alsa/acp/alsa-mixer.c:2761
 msgid "Multichannel Output"
 msgstr "Keluaran Multikanal"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2819
+#: spa/plugins/alsa/acp/alsa-mixer.c:2762
 msgid "Game Output"
 msgstr "Keluaran Permainan"
 
-#: 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 "Keluaran Obrolan"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2822
+#: spa/plugins/alsa/acp/alsa-mixer.c:2765
 msgid "Chat Input"
 msgstr "Masukan Obrolan"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2823
+#: spa/plugins/alsa/acp/alsa-mixer.c:2766
 msgid "Virtual Surround 7.1"
 msgstr "Virtual Surround 7.1"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4527
+#: spa/plugins/alsa/acp/alsa-mixer.c:4456
 msgid "Analog Mono"
 msgstr "Analog Mono"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4528
+#: spa/plugins/alsa/acp/alsa-mixer.c:4457
 msgid "Analog Mono (Left)"
 msgstr "Analog Mono (Kiri)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4529
+#: spa/plugins/alsa/acp/alsa-mixer.c:4458
 msgid "Analog Mono (Right)"
 msgstr "Analog Mono (Kanan)"
 
@@ -385,147 +384,147 @@ msgstr "Analog Mono (Kanan)"
 #. * 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:4459
+#: spa/plugins/alsa/acp/alsa-mixer.c:4467
+#: spa/plugins/alsa/acp/alsa-mixer.c:4468
 msgid "Analog Stereo"
 msgstr "Analog Stereo"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4531
+#: spa/plugins/alsa/acp/alsa-mixer.c:4460
 msgid "Mono"
 msgstr "Mono"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4532
+#: spa/plugins/alsa/acp/alsa-mixer.c:4461
 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:1165
+#: spa/plugins/alsa/acp/alsa-mixer.c:4469
+#: spa/plugins/alsa/acp/alsa-mixer.c:4627
+#: spa/plugins/bluez5/bluez5-device.c:1977
 msgid "Headset"
 msgstr "Headset"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4541
-#: spa/plugins/alsa/acp/alsa-mixer.c:4699
+#: spa/plugins/alsa/acp/alsa-mixer.c:4470
+#: spa/plugins/alsa/acp/alsa-mixer.c:4628
 msgid "Speakerphone"
 msgstr "Speakerphone"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4542
-#: spa/plugins/alsa/acp/alsa-mixer.c:4543
+#: spa/plugins/alsa/acp/alsa-mixer.c:4471
+#: spa/plugins/alsa/acp/alsa-mixer.c:4472
 msgid "Multichannel"
 msgstr "Multikanal"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4544
+#: spa/plugins/alsa/acp/alsa-mixer.c:4473
 msgid "Analog Surround 2.1"
 msgstr "Analog Surround 2.1"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4545
+#: spa/plugins/alsa/acp/alsa-mixer.c:4474
 msgid "Analog Surround 3.0"
 msgstr "Analog Surround 3.0"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4546
+#: spa/plugins/alsa/acp/alsa-mixer.c:4475
 msgid "Analog Surround 3.1"
 msgstr "Analog Surround 3.1"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4547
+#: spa/plugins/alsa/acp/alsa-mixer.c:4476
 msgid "Analog Surround 4.0"
 msgstr "Analog Surround 4.0"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4548
+#: spa/plugins/alsa/acp/alsa-mixer.c:4477
 msgid "Analog Surround 4.1"
 msgstr "Analog Surround 4.1"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4549
+#: spa/plugins/alsa/acp/alsa-mixer.c:4478
 msgid "Analog Surround 5.0"
 msgstr "Analog Surround 5.0"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4550
+#: spa/plugins/alsa/acp/alsa-mixer.c:4479
 msgid "Analog Surround 5.1"
 msgstr "Analog Surround 5.1"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4551
+#: spa/plugins/alsa/acp/alsa-mixer.c:4480
 msgid "Analog Surround 6.0"
 msgstr "Analog Surround 6.0"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4552
+#: spa/plugins/alsa/acp/alsa-mixer.c:4481
 msgid "Analog Surround 6.1"
 msgstr "Analog Surround 6.1"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4553
+#: spa/plugins/alsa/acp/alsa-mixer.c:4482
 msgid "Analog Surround 7.0"
 msgstr "Analog Surround 7.0"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4554
+#: spa/plugins/alsa/acp/alsa-mixer.c:4483
 msgid "Analog Surround 7.1"
 msgstr "Analog Surround 7.1"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4555
+#: spa/plugins/alsa/acp/alsa-mixer.c:4484
 msgid "Digital Stereo (IEC958)"
 msgstr "Digital Stereo (IEC958)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4556
+#: spa/plugins/alsa/acp/alsa-mixer.c:4485
 msgid "Digital Surround 4.0 (IEC958/AC3)"
 msgstr "Digital Surround 4.0 (IEC958/AC3)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4557
+#: spa/plugins/alsa/acp/alsa-mixer.c:4486
 msgid "Digital Surround 5.1 (IEC958/AC3)"
 msgstr "Digital Surround 5.1 (IEC958/AC3)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4558
+#: spa/plugins/alsa/acp/alsa-mixer.c:4487
 msgid "Digital Surround 5.1 (IEC958/DTS)"
 msgstr "Surround 5.1 Digital (IEC958/DTS)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4559
+#: spa/plugins/alsa/acp/alsa-mixer.c:4488
 msgid "Digital Stereo (HDMI)"
 msgstr "Digital Stereo (HDMI)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4560
+#: spa/plugins/alsa/acp/alsa-mixer.c:4489
 msgid "Digital Surround 5.1 (HDMI)"
 msgstr "Surround 5.1 Digital (HDMI)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4561
+#: spa/plugins/alsa/acp/alsa-mixer.c:4490
 msgid "Chat"
 msgstr "Obrolan"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4562
+#: spa/plugins/alsa/acp/alsa-mixer.c:4491
 msgid "Game"
 msgstr "Permainan"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4696
+#: spa/plugins/alsa/acp/alsa-mixer.c:4625
 msgid "Analog Mono Duplex"
-msgstr "Analog Mono Duplex"
+msgstr "Dupleks Mono Analog"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4697
+#: spa/plugins/alsa/acp/alsa-mixer.c:4626
 msgid "Analog Stereo Duplex"
-msgstr "Analog Stereo Duplex"
+msgstr "Dupleks Stereo Analog"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4700
+#: spa/plugins/alsa/acp/alsa-mixer.c:4629
 msgid "Digital Stereo Duplex (IEC958)"
-msgstr "Digital Stereo Duplex (IEC958)"
+msgstr "Dupleks Stereo Digital (IEC958)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4701
+#: spa/plugins/alsa/acp/alsa-mixer.c:4630
 msgid "Multichannel Duplex"
 msgstr "Dupleks Multikanal"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4702
+#: spa/plugins/alsa/acp/alsa-mixer.c:4631
 msgid "Stereo Duplex"
 msgstr "Dupleks Stereo"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4703
+#: spa/plugins/alsa/acp/alsa-mixer.c:4632
 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:4733
 #, c-format
 msgid "%s Output"
 msgstr "Keluaran %s"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4813
+#: spa/plugins/alsa/acp/alsa-mixer.c:4741
 #, c-format
 msgid "%s Input"
 msgstr "Masukan %s"
 
-#: spa/plugins/alsa/acp/alsa-util.c:1175 spa/plugins/alsa/acp/alsa-util.c:1269
+#: spa/plugins/alsa/acp/alsa-util.c:1220 spa/plugins/alsa/acp/alsa-util.c:1314
 #, c-format
 msgid ""
 "snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu "
@@ -542,22 +541,17 @@ msgstr[0] ""
 "ms).\n"
 "Sangat mungkin ini adalah kutu pada driver ALSA '%s'. Silakan laporkan hal "
 "ini ke para pengembang ALSA."
-msgstr[1] ""
-"snd_pcm_avail() mengembalikan nilai yang luar biasa besar: %lu byte (%lu "
-"ms).\n"
-"Sangat mungkin ini adalah kutu pada driver ALSA '%s'. Silakan laporkan hal "
-"ini ke para pengembang ALSA."
 
-#: spa/plugins/alsa/acp/alsa-util.c:1241
+#: spa/plugins/alsa/acp/alsa-util.c:1286
 #, 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] ""
@@ -565,13 +559,8 @@ msgstr[0] ""
 "ms).\n"
 "Sangat mungkin ini adalah kutu pada driver ALSA '%s'. Silakan laporkan hal "
 "ini ke para pengembang ALSA."
-msgstr[1] ""
-"snd_pcm_delay() mengembalikan nilai yang luar biasa besar: %li byte (%s%lu "
-"ms).\n"
-"Sangat mungkin ini adalah kutu pada driver ALSA '%s'. Silakan laporkan hal "
-"ini ke para pengembang ALSA."
 
-#: spa/plugins/alsa/acp/alsa-util.c:1288
+#: spa/plugins/alsa/acp/alsa-util.c:1333
 #, c-format
 msgid ""
 "snd_pcm_avail_delay() returned strange values: delay %lu is less than avail "
@@ -584,7 +573,7 @@ msgstr ""
 "Paling mungkin ini adalah kutu dalam penggerak ALSA '%s'. Harap laporkan "
 "kasus ini ke para pengembang ALSA."
 
-#: spa/plugins/alsa/acp/alsa-util.c:1331
+#: spa/plugins/alsa/acp/alsa-util.c:1376
 #, c-format
 msgid ""
 "snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu byte "
@@ -601,71 +590,129 @@ msgstr[0] ""
 "(%lu ms).\n"
 "Sangat mungkin ini adalah kutu pada driver ALSA '%s'. Silakan laporkan hal "
 "ini ke para pengembang ALSA."
-msgstr[1] ""
-"snd_pcm_mmap_begin() mengembalikan nilai yang luar biasa besar: %lu byte "
-"(%lu ms).\n"
-"Sangat mungkin ini adalah kutu pada driver ALSA '%s'. Silakan laporkan hal "
-"ini ke para pengembang ALSA."
 
-#: spa/plugins/alsa/acp/channelmap.h:466
+#: spa/plugins/alsa/acp/channelmap.h:457
 msgid "(invalid)"
 msgstr "(tak valid)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1030
+#: spa/plugins/alsa/acp/compat.c:193
+msgid "Built-in Audio"
+msgstr "Audio Bawaan"
+
+#: spa/plugins/alsa/acp/compat.c:198
+msgid "Modem"
+msgstr "Modem"
+
+#: spa/plugins/bluez5/bluez5-device.c:1712
 msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
-msgstr "Audio Gateway (A2DP Source & HSP/HFP AG)"
+msgstr "Audio Gateway (Sumber A2DP & HSP/HFP AG)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1053
+#: spa/plugins/bluez5/bluez5-device.c:1760
 #, c-format
 msgid "High Fidelity Playback (A2DP Sink, codec %s)"
-msgstr "High Fidelity Playback (A2DP Sink, codec %s)"
+msgstr "Putar High Fidelity (Muara A2DP, kodek %s)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1055
+#: spa/plugins/bluez5/bluez5-device.c:1763
 #, c-format
 msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
-msgstr "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
+msgstr "Dupleks High Fidelity (Sumber/Muara A2DP, kodek %s)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1061
+#: spa/plugins/bluez5/bluez5-device.c:1771
 msgid "High Fidelity Playback (A2DP Sink)"
-msgstr "High Fidelity Playback (A2DP Sink)"
+msgstr "Putar High Fidelity (Muara A2DP)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1063
+#: spa/plugins/bluez5/bluez5-device.c:1773
 msgid "High Fidelity Duplex (A2DP Source/Sink)"
-msgstr "High Fidelity Duplex (A2DP Source/Sink)"
+msgstr "Dupleks High Fidelity (Sumber/Muara A2DP)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1090
+#: spa/plugins/bluez5/bluez5-device.c:1823
 #, c-format
-msgid "Headset Head Unit (HSP/HFP, codec %s)"
-msgstr "Headset Head Unit (HSP/HFP, codec %s)"
+msgid "High Fidelity Playback (BAP Sink, codec %s)"
+msgstr "Putar High Fidelity (Muara BAP, kodek %s)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1094
-msgid "Headset Head Unit (HSP/HFP)"
-msgstr "Headset Head Unit (HSP/HFP)"
+#: spa/plugins/bluez5/bluez5-device.c:1828
+#, c-format
+msgid "High Fidelity Input (BAP Source, codec %s)"
+msgstr "Masukan High Fidelity (Sumber BAP, kodek %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1832
+#, c-format
+msgid "High Fidelity Duplex (BAP Source/Sink, codec %s)"
+msgstr "Dupleks High Fidelity (Sumber/Muara BAP, kodek %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1841
+msgid "High Fidelity Playback (BAP Sink)"
+msgstr "Putar High Fidelity (Muara BAP)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1845
+msgid "High Fidelity Input (BAP Source)"
+msgstr "Masukan High Fidelity (Sumber BAP)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1848
+msgid "High Fidelity Duplex (BAP Source/Sink)"
+msgstr "Dupleks High Fidelity (Sumber/Muara BAP)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1170
+#: spa/plugins/bluez5/bluez5-device.c:1897
+#, c-format
+msgid "Headset Head Unit (HSP/HFP, codec %s)"
+msgstr "Headset Head Unit (HSP/HFP, kodek %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1978
+#: spa/plugins/bluez5/bluez5-device.c:1983
+#: spa/plugins/bluez5/bluez5-device.c:1990
+#: spa/plugins/bluez5/bluez5-device.c:1996
+#: spa/plugins/bluez5/bluez5-device.c:2002
+#: spa/plugins/bluez5/bluez5-device.c:2008
+#: spa/plugins/bluez5/bluez5-device.c:2014
+#: spa/plugins/bluez5/bluez5-device.c:2020
+#: spa/plugins/bluez5/bluez5-device.c:2026
 msgid "Handsfree"
 msgstr "Handsfree"
 
-#: spa/plugins/bluez5/bluez5-device.c:1185
+#: spa/plugins/bluez5/bluez5-device.c:1984
+msgid "Handsfree (HFP)"
+msgstr "Handsfree (HFP)"
+
+#: spa/plugins/bluez5/bluez5-device.c:2001
 msgid "Headphone"
 msgstr "Headphone"
 
-#: spa/plugins/bluez5/bluez5-device.c:1190
+#: spa/plugins/bluez5/bluez5-device.c:2007
 msgid "Portable"
 msgstr "Portabel"
 
-#: spa/plugins/bluez5/bluez5-device.c:1195
+#: spa/plugins/bluez5/bluez5-device.c:2013
 msgid "Car"
 msgstr "Mobil"
 
-#: spa/plugins/bluez5/bluez5-device.c:1200
+#: spa/plugins/bluez5/bluez5-device.c:2019
 msgid "HiFi"
 msgstr "HiFi"
 
-#: spa/plugins/bluez5/bluez5-device.c:1205
+#: spa/plugins/bluez5/bluez5-device.c:2025
 msgid "Phone"
 msgstr "Telepon"
 
-#: spa/plugins/bluez5/bluez5-device.c:1211
+#: spa/plugins/bluez5/bluez5-device.c:2032
 msgid "Bluetooth"
 msgstr "Bluetooth"
+
+#: spa/plugins/bluez5/bluez5-device.c:2033
+msgid "Bluetooth (HFP)"
+msgstr "Bluetooth (HFP)"
+
+#, c-format
+#~ msgid ""
+#~ "%s [options]\n"
+#~ "  -h, --help                            Show this help\n"
+#~ "  -v, --verbose                         Increase verbosity by one level\n"
+#~ "      --version                         Show version\n"
+#~ "  -c, --config                          Load config (Default %s)\n"
+#~ "  -P  --properties                      Set context properties\n"
+#~ msgstr ""
+#~ "%s [opsi]\n"
+#~ "  -h, --help                            Tampilkan bantuan ini\n"
+#~ "  -v, --verbose                         Tingkatkan kerincian satu aras\n"
+#~ "      --version                         Tampilkan versi\n"
+#~ "  -c, --config                          Muat konfig (Baku %s)\n"
+#~ "  -P  --properties                      Atur properti konteks\n"
diff --git a/po/nl.po b/po/nl.po
index 733b7eec..b1e5695a 100644
--- a/po/nl.po
+++ b/po/nl.po
@@ -257,7 +257,7 @@ msgstr "Analoge mono-uitvoer"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:2811
 msgid "Line Out"
-msgstr "Lijn-in"
+msgstr "Lijn-uit"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:2812
 msgid "Analog Mono Output"
diff --git a/po/pl.po b/po/pl.po
index 297e2fb7..6d6572a3 100644
--- a/po/pl.po
+++ b/po/pl.po
@@ -1,15 +1,15 @@
 # Polish translation for pipewire.
-# Copyright © 2008-2023 the pipewire authors.
+# Copyright © 2008-2025 the pipewire authors.
 # This file is distributed under the same license as the pipewire package.
-# Piotr DrÄ…g <piotrdrag@gmail.com>, 2008, 2012-2023.
+# Piotr DrÄ…g <piotrdrag@gmail.com>, 2008, 2012-2025.
 #
 msgid ""
 msgstr ""
 "Project-Id-Version: pipewire\n"
 "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/-/"
 "issues\n"
-"POT-Creation-Date: 2023-05-28 10:45+0000\n"
-"PO-Revision-Date: 2023-05-28 12:48+0200\n"
+"POT-Creation-Date: 2025-01-09 15:25+0000\n"
+"PO-Revision-Date: 2025-02-09 14:55+0100\n"
 "Last-Translator: Piotr DrÄ…g <piotrdrag@gmail.com>\n"
 "Language-Team: Polish <community-poland@mozilla.org>\n"
 "Language: pl\n"
@@ -19,19 +19,24 @@ msgstr ""
 "Plural-Forms: nplurals=3; plural=n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
 "|| n%100>=20) ? 1 : 2;\n"
 
-#: src/daemon/pipewire.c:26
+#: src/daemon/pipewire.c:29
 #, c-format
 msgid ""
 "%s [options]\n"
 "  -h, --help                            Show this help\n"
+"  -v, --verbose                         Increase verbosity by one level\n"
 "      --version                         Show version\n"
 "  -c, --config                          Load config (Default %s)\n"
+"  -P  --properties                      Set context properties\n"
 msgstr ""
 "%s [opcje]\n"
 "  -h, --help                            Wyświetla tę pomoc\n"
+"  -v, --verbose                         Zwiększa liczbę wyświetlanych\n"
+"                                        komunikatów o jeden poziom\n"
 "      --version                         Wyświetla wersję\n"
 "  -c, --config                          Wczytuje konfigurację (domyślnie "
 "%s)\n"
+"  -P  --properties                      Ustawia właściwości kontekstu\n"
 
 #: src/daemon/pipewire.desktop.in:4
 msgid "PipeWire Media System"
@@ -41,36 +46,36 @@ msgstr "System multimediów PipeWire"
 msgid "Start the PipeWire Media System"
 msgstr "Uruchomienie systemu multimediów PipeWire"
 
-#: src/modules/module-protocol-pulse/modules/module-tunnel-sink.c:141
-#: src/modules/module-protocol-pulse/modules/module-tunnel-source.c:141
+#: src/modules/module-protocol-pulse/modules/module-tunnel-sink.c:159
+#: src/modules/module-protocol-pulse/modules/module-tunnel-source.c:159
 #, c-format
 msgid "Tunnel to %s%s%s"
 msgstr "Tunel do %s%s%s"
 
-#: src/modules/module-fallback-sink.c:31
+#: src/modules/module-fallback-sink.c:40
 msgid "Dummy Output"
 msgstr "Głuche wyjście"
 
-#: src/modules/module-pulse-tunnel.c:844
+#: src/modules/module-pulse-tunnel.c:760
 #, c-format
 msgid "Tunnel for %s@%s"
 msgstr "Tunel dla %s@%s"
 
-#: src/modules/module-zeroconf-discover.c:315
+#: src/modules/module-zeroconf-discover.c:320
 msgid "Unknown device"
 msgstr "Nieznane urzÄ…dzenie"
 
-#: src/modules/module-zeroconf-discover.c:327
+#: src/modules/module-zeroconf-discover.c:332
 #, c-format
 msgid "%s on %s@%s"
 msgstr "%s na %s@%s"
 
-#: src/modules/module-zeroconf-discover.c:331
+#: src/modules/module-zeroconf-discover.c:336
 #, c-format
 msgid "%s on %s"
 msgstr "%s na %s"
 
-#: src/tools/pw-cat.c:974
+#: src/tools/pw-cat.c:973
 #, c-format
 msgid ""
 "%s [options] [<file>|-]\n"
@@ -85,7 +90,7 @@ msgstr ""
 "  -v, --verbose                         Wyświetla więcej komunikatów\n"
 "\n"
 
-#: src/tools/pw-cat.c:981
+#: src/tools/pw-cat.c:980
 #, c-format
 msgid ""
 "  -R, --remote                          Remote daemon name\n"
@@ -123,7 +128,7 @@ msgstr ""
 "  -P  --properties                      Ustawia właściwości węzła\n"
 "\n"
 
-#: src/tools/pw-cat.c:999
+#: src/tools/pw-cat.c:998
 #, c-format
 msgid ""
 "      --rate                            Sample rate (req. for rec) (default "
@@ -140,6 +145,7 @@ msgid ""
 "      --volume                          Stream volume 0-1.0 (default %.3f)\n"
 "  -q  --quality                         Resampler quality (0 - 15) (default "
 "%d)\n"
+"  -a, --raw                             RAW mode\n"
 "\n"
 msgstr ""
 "      --rate                            Częstotliwość próbki (wymagane do "
@@ -157,6 +163,7 @@ msgstr ""
 "(domyślnie %.3f)\n"
 "  -q  --quality                         Jakość resamplera od 0 do 15 "
 "(domyślnie %d)\n"
+"  -a, --raw                             Tryb RAW\n"
 "\n"
 
 #: src/tools/pw-cat.c:1016
@@ -175,7 +182,7 @@ msgstr ""
 "  -o, --encoded                         Tryb zakodowany\n"
 "\n"
 
-#: src/tools/pw-cli.c:2220
+#: src/tools/pw-cli.c:2306
 #, c-format
 msgid ""
 "%s [options] [command]\n"
@@ -195,12 +202,12 @@ msgstr ""
 "  -m, --monitor                         Monitoruje aktywność\n"
 "\n"
 
-#: spa/plugins/alsa/acp/acp.c:303
+#: spa/plugins/alsa/acp/acp.c:347
 msgid "Pro Audio"
 msgstr "Dźwięk w zastosowaniach profesjonalnych"
 
-#: spa/plugins/alsa/acp/acp.c:427 spa/plugins/alsa/acp/alsa-mixer.c:4648
-#: spa/plugins/bluez5/bluez5-device.c:1586
+#: spa/plugins/alsa/acp/acp.c:507 spa/plugins/alsa/acp/alsa-mixer.c:4635
+#: spa/plugins/bluez5/bluez5-device.c:1795
 msgid "Off"
 msgstr "Wyłączone"
 
@@ -227,7 +234,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:1831
+#: spa/plugins/bluez5/bluez5-device.c:2139
 msgid "Microphone"
 msgstr "Mikrofon"
 
@@ -293,7 +300,7 @@ msgid "No Bass Boost"
 msgstr "Brak podbicia basów"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:2672
-#: spa/plugins/bluez5/bluez5-device.c:1837
+#: spa/plugins/bluez5/bluez5-device.c:2145
 msgid "Speaker"
 msgstr "Głośnik"
 
@@ -375,15 +382,15 @@ msgstr "Wejście rozmowy"
 msgid "Virtual Surround 7.1"
 msgstr "Wirtualne przestrzenne 7.1"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4471
+#: spa/plugins/alsa/acp/alsa-mixer.c:4458
 msgid "Analog Mono"
 msgstr "Analogowe mono"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4472
+#: spa/plugins/alsa/acp/alsa-mixer.c:4459
 msgid "Analog Mono (Left)"
 msgstr "Analogowe mono (lewy)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4473
+#: spa/plugins/alsa/acp/alsa-mixer.c:4460
 msgid "Analog Mono (Right)"
 msgstr "Analogowe mono (prawy)"
 
@@ -392,147 +399,147 @@ msgstr "Analogowe mono (prawy)"
 #. * here would lead to the source name to become "Analog Stereo Input
 #. * Input". The same logic applies to analog-stereo-output,
 #. * multichannel-input and multichannel-output.
-#: spa/plugins/alsa/acp/alsa-mixer.c:4474
-#: spa/plugins/alsa/acp/alsa-mixer.c:4482
-#: spa/plugins/alsa/acp/alsa-mixer.c:4483
+#: spa/plugins/alsa/acp/alsa-mixer.c:4461
+#: spa/plugins/alsa/acp/alsa-mixer.c:4469
+#: spa/plugins/alsa/acp/alsa-mixer.c:4470
 msgid "Analog Stereo"
 msgstr "Analogowe stereo"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4475
+#: spa/plugins/alsa/acp/alsa-mixer.c:4462
 msgid "Mono"
 msgstr "Mono"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4476
+#: spa/plugins/alsa/acp/alsa-mixer.c:4463
 msgid "Stereo"
 msgstr "Stereo"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4484
-#: spa/plugins/alsa/acp/alsa-mixer.c:4642
-#: spa/plugins/bluez5/bluez5-device.c:1819
+#: spa/plugins/alsa/acp/alsa-mixer.c:4471
+#: spa/plugins/alsa/acp/alsa-mixer.c:4629
+#: spa/plugins/bluez5/bluez5-device.c:2127
 msgid "Headset"
 msgstr "Słuchawki z mikrofonem"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4485
-#: spa/plugins/alsa/acp/alsa-mixer.c:4643
+#: spa/plugins/alsa/acp/alsa-mixer.c:4472
+#: spa/plugins/alsa/acp/alsa-mixer.c:4630
 msgid "Speakerphone"
 msgstr "Telefon głośnomówiący"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4486
-#: spa/plugins/alsa/acp/alsa-mixer.c:4487
+#: spa/plugins/alsa/acp/alsa-mixer.c:4473
+#: spa/plugins/alsa/acp/alsa-mixer.c:4474
 msgid "Multichannel"
 msgstr "Wielokanałowe"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4488
+#: spa/plugins/alsa/acp/alsa-mixer.c:4475
 msgid "Analog Surround 2.1"
 msgstr "Analogowe przestrzenne 2.1"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4489
+#: spa/plugins/alsa/acp/alsa-mixer.c:4476
 msgid "Analog Surround 3.0"
 msgstr "Analogowe przestrzenne 3.0"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4490
+#: spa/plugins/alsa/acp/alsa-mixer.c:4477
 msgid "Analog Surround 3.1"
 msgstr "Analogowe przestrzenne 3.1"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4491
+#: spa/plugins/alsa/acp/alsa-mixer.c:4478
 msgid "Analog Surround 4.0"
 msgstr "Analogowe przestrzenne 4.0"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4492
+#: spa/plugins/alsa/acp/alsa-mixer.c:4479
 msgid "Analog Surround 4.1"
 msgstr "Analogowe przestrzenne 4.1"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4493
+#: spa/plugins/alsa/acp/alsa-mixer.c:4480
 msgid "Analog Surround 5.0"
 msgstr "Analogowe przestrzenne 5.0"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4494
+#: spa/plugins/alsa/acp/alsa-mixer.c:4481
 msgid "Analog Surround 5.1"
 msgstr "Analogowe przestrzenne 5.1"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4495
+#: spa/plugins/alsa/acp/alsa-mixer.c:4482
 msgid "Analog Surround 6.0"
 msgstr "Analogowe przestrzenne 6.0"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4496
+#: spa/plugins/alsa/acp/alsa-mixer.c:4483
 msgid "Analog Surround 6.1"
 msgstr "Analogowe przestrzenne 6.1"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4497
+#: spa/plugins/alsa/acp/alsa-mixer.c:4484
 msgid "Analog Surround 7.0"
 msgstr "Analogowe przestrzenne 7.0"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4498
+#: spa/plugins/alsa/acp/alsa-mixer.c:4485
 msgid "Analog Surround 7.1"
 msgstr "Analogowe przestrzenne 7.1"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4499
+#: spa/plugins/alsa/acp/alsa-mixer.c:4486
 msgid "Digital Stereo (IEC958)"
 msgstr "Cyfrowe stereo (IEC958)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4500
+#: spa/plugins/alsa/acp/alsa-mixer.c:4487
 msgid "Digital Surround 4.0 (IEC958/AC3)"
 msgstr "Cyfrowe przestrzenne 4.0 (IEC958/AC3)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4501
+#: spa/plugins/alsa/acp/alsa-mixer.c:4488
 msgid "Digital Surround 5.1 (IEC958/AC3)"
 msgstr "Cyfrowe przestrzenne 5.1 (IEC958/AC3)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4502
+#: spa/plugins/alsa/acp/alsa-mixer.c:4489
 msgid "Digital Surround 5.1 (IEC958/DTS)"
 msgstr "Cyfrowe przestrzenne 5.1 (IEC958/DTS)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4503
+#: spa/plugins/alsa/acp/alsa-mixer.c:4490
 msgid "Digital Stereo (HDMI)"
 msgstr "Cyfrowe stereo (HDMI)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4504
+#: spa/plugins/alsa/acp/alsa-mixer.c:4491
 msgid "Digital Surround 5.1 (HDMI)"
 msgstr "Cyfrowe przestrzenne 5.1 (HDMI)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4505
+#: spa/plugins/alsa/acp/alsa-mixer.c:4492
 msgid "Chat"
 msgstr "Rozmowa"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4506
+#: spa/plugins/alsa/acp/alsa-mixer.c:4493
 msgid "Game"
 msgstr "Gra"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4640
+#: spa/plugins/alsa/acp/alsa-mixer.c:4627
 msgid "Analog Mono Duplex"
 msgstr "Analogowy dupleks mono"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4641
+#: spa/plugins/alsa/acp/alsa-mixer.c:4628
 msgid "Analog Stereo Duplex"
 msgstr "Analogowy dupleks stereo"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4644
+#: spa/plugins/alsa/acp/alsa-mixer.c:4631
 msgid "Digital Stereo Duplex (IEC958)"
 msgstr "Cyfrowy dupleks stereo (IEC958)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4645
+#: spa/plugins/alsa/acp/alsa-mixer.c:4632
 msgid "Multichannel Duplex"
 msgstr "Dupleks wielokanałowy"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4646
+#: spa/plugins/alsa/acp/alsa-mixer.c:4633
 msgid "Stereo Duplex"
 msgstr "Dupleks stereo"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4647
+#: spa/plugins/alsa/acp/alsa-mixer.c:4634
 msgid "Mono Chat + 7.1 Surround"
 msgstr "Rozmowa mono + przestrzenne 7.1"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4748
+#: spa/plugins/alsa/acp/alsa-mixer.c:4735
 #, c-format
 msgid "%s Output"
 msgstr "Wyjście %s"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4756
+#: spa/plugins/alsa/acp/alsa-mixer.c:4743
 #, c-format
 msgid "%s Input"
 msgstr "Wejście %s"
 
-#: spa/plugins/alsa/acp/alsa-util.c:1187 spa/plugins/alsa/acp/alsa-util.c:1281
+#: spa/plugins/alsa/acp/alsa-util.c:1233 spa/plugins/alsa/acp/alsa-util.c:1327
 #, c-format
 msgid ""
 "snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu "
@@ -557,7 +564,7 @@ msgstr[2] ""
 "Prawdopodobnie jest to błąd sterownika ALSA „%s”. Proszę zgłosić ten problem "
 "programistom usługi ALSA."
 
-#: spa/plugins/alsa/acp/alsa-util.c:1253
+#: spa/plugins/alsa/acp/alsa-util.c:1299
 #, c-format
 msgid ""
 "snd_pcm_delay() returned a value that is exceptionally large: %li byte "
@@ -582,7 +589,7 @@ msgstr[2] ""
 "Prawdopodobnie jest to błąd sterownika ALSA „%s”. Proszę zgłosić ten problem "
 "programistom usługi ALSA."
 
-#: spa/plugins/alsa/acp/alsa-util.c:1300
+#: spa/plugins/alsa/acp/alsa-util.c:1346
 #, c-format
 msgid ""
 "snd_pcm_avail_delay() returned strange values: delay %lu is less than avail "
@@ -595,7 +602,7 @@ msgstr ""
 "Prawdopodobnie jest to błąd sterownika ALSA „%s”. Proszę zgłosić ten problem "
 "programistom usługi ALSA."
 
-#: spa/plugins/alsa/acp/alsa-util.c:1343
+#: spa/plugins/alsa/acp/alsa-util.c:1389
 #, c-format
 msgid ""
 "snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu byte "
@@ -624,112 +631,112 @@ msgstr[2] ""
 msgid "(invalid)"
 msgstr "(nieprawidłowe)"
 
-#: spa/plugins/alsa/acp/compat.c:189
+#: spa/plugins/alsa/acp/compat.c:193
 msgid "Built-in Audio"
 msgstr "Wbudowany dźwięk"
 
-#: spa/plugins/alsa/acp/compat.c:194
+#: spa/plugins/alsa/acp/compat.c:198
 msgid "Modem"
 msgstr "Modem"
 
-#: spa/plugins/bluez5/bluez5-device.c:1597
+#: spa/plugins/bluez5/bluez5-device.c:1806
 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:1622
+#: spa/plugins/bluez5/bluez5-device.c:1834
+msgid "Audio Streaming for Hearing Aids (ASHA Sink)"
+msgstr "Przesyłanie dźwięku do aparatów słuchowych (odpływ ASHA)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1874
 #, 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:1625
+#: spa/plugins/bluez5/bluez5-device.c:1877
 #, 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:1633
+#: spa/plugins/bluez5/bluez5-device.c:1885
 msgid "High Fidelity Playback (A2DP Sink)"
 msgstr "Odtwarzanie o wysokiej dokładności (odpływ A2DP)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1635
+#: spa/plugins/bluez5/bluez5-device.c:1887
 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:1677
+#: spa/plugins/bluez5/bluez5-device.c:1937
 #, 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:1681
+#: spa/plugins/bluez5/bluez5-device.c:1942
 #, 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:1685
+#: spa/plugins/bluez5/bluez5-device.c:1946
 #, 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:1693
+#: spa/plugins/bluez5/bluez5-device.c:1955
 msgid "High Fidelity Playback (BAP Sink)"
 msgstr "Odtwarzanie o wysokiej dokładności (odpływ BAP)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1696
+#: spa/plugins/bluez5/bluez5-device.c:1959
 msgid "High Fidelity Input (BAP Source)"
 msgstr "Wejście o wysokiej dokładności (źródło BAP)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1699
+#: spa/plugins/bluez5/bluez5-device.c:1962
 msgid "High Fidelity Duplex (BAP Source/Sink)"
 msgstr "Dupleks o wysokiej dokładności (źródło/odpływ BAP)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1735
+#: spa/plugins/bluez5/bluez5-device.c:2008
 #, 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:1740
-msgid "Headset Head Unit (HSP/HFP)"
-msgstr "Jednostka główna słuchawek z mikrofonem (HSP/HFP)"
-
-#: spa/plugins/bluez5/bluez5-device.c:1820
-#: spa/plugins/bluez5/bluez5-device.c:1825
-#: spa/plugins/bluez5/bluez5-device.c:1832
-#: spa/plugins/bluez5/bluez5-device.c:1838
-#: spa/plugins/bluez5/bluez5-device.c:1844
-#: spa/plugins/bluez5/bluez5-device.c:1850
-#: spa/plugins/bluez5/bluez5-device.c:1856
-#: spa/plugins/bluez5/bluez5-device.c:1862
-#: spa/plugins/bluez5/bluez5-device.c:1868
+#: spa/plugins/bluez5/bluez5-device.c:2128
+#: spa/plugins/bluez5/bluez5-device.c:2133
+#: spa/plugins/bluez5/bluez5-device.c:2140
+#: spa/plugins/bluez5/bluez5-device.c:2146
+#: spa/plugins/bluez5/bluez5-device.c:2152
+#: spa/plugins/bluez5/bluez5-device.c:2158
+#: spa/plugins/bluez5/bluez5-device.c:2164
+#: spa/plugins/bluez5/bluez5-device.c:2170
+#: spa/plugins/bluez5/bluez5-device.c:2176
 msgid "Handsfree"
 msgstr "Zestaw głośnomówiący"
 
-#: spa/plugins/bluez5/bluez5-device.c:1826
+#: spa/plugins/bluez5/bluez5-device.c:2134
 msgid "Handsfree (HFP)"
 msgstr "Zestaw głośnomówiący (HFP)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1843
+#: spa/plugins/bluez5/bluez5-device.c:2151
 msgid "Headphone"
 msgstr "SÅ‚uchawki"
 
-#: spa/plugins/bluez5/bluez5-device.c:1849
+#: spa/plugins/bluez5/bluez5-device.c:2157
 msgid "Portable"
 msgstr "Przenośne"
 
-#: spa/plugins/bluez5/bluez5-device.c:1855
+#: spa/plugins/bluez5/bluez5-device.c:2163
 msgid "Car"
 msgstr "Samochód"
 
-#: spa/plugins/bluez5/bluez5-device.c:1861
+#: spa/plugins/bluez5/bluez5-device.c:2169
 msgid "HiFi"
 msgstr "HiFi"
 
-#: spa/plugins/bluez5/bluez5-device.c:1867
+#: spa/plugins/bluez5/bluez5-device.c:2175
 msgid "Phone"
 msgstr "Telefon"
 
-#: spa/plugins/bluez5/bluez5-device.c:1874
+#: spa/plugins/bluez5/bluez5-device.c:2182
 msgid "Bluetooth"
 msgstr "Bluetooth"
 
-#: spa/plugins/bluez5/bluez5-device.c:1875
+#: spa/plugins/bluez5/bluez5-device.c:2183
 msgid "Bluetooth (HFP)"
 msgstr "Bluetooth (HFP)"
diff --git a/po/sl.po b/po/sl.po
new file mode 100644
index 00000000..de3f9452
--- /dev/null
+++ b/po/sl.po
@@ -0,0 +1,766 @@
+# Slovenian translation for PipeWire.
+# Copyright (C) 2024 PipeWire's COPYRIGHT HOLDER
+# This file is distributed under the same license as the PipeWire package.
+#
+# Martin <miles@filmsi.net>, 2024, 2025.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PipeWire master\n"
+"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/-/"
+"issues\n"
+"POT-Creation-Date: 2025-01-09 15:25+0000\n"
+"PO-Revision-Date: 2025-01-23 09:23+0100\n"
+"Last-Translator: Martin Srebotnjak <miles@filmsi.net>\n"
+"Language-Team: Slovenian <gnome-si@googlegroups.com>\n"
+"Language: sl\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100>=3 && n"
+"%100<=4 ? 2 : 3);\n"
+"X-Generator: Poedit 2.2.1\n"
+
+#: src/daemon/pipewire.c:29
+#, c-format
+msgid ""
+"%s [options]\n"
+"  -h, --help                            Show this help\n"
+"  -v, --verbose                         Increase verbosity by one level\n"
+"      --version                         Show version\n"
+"  -c, --config                          Load config (Default %s)\n"
+"  -P  --properties                      Set context properties\n"
+msgstr ""
+"%s [možnosti]\n"
+"  -h, --help                            Pokaži to pomoč\n"
+"  -v, --verbose                         Povečaj opisnost za eno raven\n"
+"      --version                         Pokaži različico\n"
+"  -c, --config                          Naloži prilagoditev config (privzeto "
+"%s)\n"
+"  -P  —properties                      Določi lastnosti konteksta\n"
+
+#: src/daemon/pipewire.desktop.in:4
+msgid "PipeWire Media System"
+msgstr "Medijski sistem PipeWire"
+
+#: src/daemon/pipewire.desktop.in:5
+msgid "Start the PipeWire Media System"
+msgstr "Zaženite medijski sistem PipeWire"
+
+#: src/modules/module-protocol-pulse/modules/module-tunnel-sink.c:159
+#: src/modules/module-protocol-pulse/modules/module-tunnel-source.c:159
+#, c-format
+msgid "Tunnel to %s%s%s"
+msgstr "Prehod do %s%s%s"
+
+#: src/modules/module-fallback-sink.c:40
+msgid "Dummy Output"
+msgstr "Lažni izhod"
+
+#: src/modules/module-pulse-tunnel.c:760
+#, c-format
+msgid "Tunnel for %s@%s"
+msgstr "Prehod za %s@%s"
+
+#: src/modules/module-zeroconf-discover.c:320
+msgid "Unknown device"
+msgstr "Neznana naprava"
+
+#: src/modules/module-zeroconf-discover.c:332
+#, c-format
+msgid "%s on %s@%s"
+msgstr "%s na %s@%s"
+
+#: src/modules/module-zeroconf-discover.c:336
+#, c-format
+msgid "%s on %s"
+msgstr "%s na %s"
+
+#: src/tools/pw-cat.c:973
+#, c-format
+msgid ""
+"%s [options] [<file>|-]\n"
+"  -h, --help                            Show this help\n"
+"      --version                         Show version\n"
+"  -v, --verbose                         Enable verbose operations\n"
+"\n"
+msgstr ""
+"%s [možnosti] [<datoteka>|-]\n"
+"  -h, --help                            Pokaži to pomoč\n"
+"      --version                         Pokaži različico\n"
+"  -v, --verbose                         Omogoči podrobno opisane operacije\n"
+"\n"
+"</file>\n"
+
+#: src/tools/pw-cat.c:980
+#, c-format
+msgid ""
+"  -R, --remote                          Remote daemon name\n"
+"      --media-type                      Set media type (default %s)\n"
+"      --media-category                  Set media category (default %s)\n"
+"      --media-role                      Set media role (default %s)\n"
+"      --target                          Set node target serial or name "
+"(default %s)\n"
+"                                          0 means don't link\n"
+"      --latency                         Set node latency (default %s)\n"
+"                                          Xunit (unit = s, ms, us, ns)\n"
+"                                          or direct samples (256)\n"
+"                                          the rate is the one of the source "
+"file\n"
+"  -P  --properties                      Set node properties\n"
+"\n"
+msgstr ""
+"  -R, --remote                          Ime oddaljenega demona\n"
+"      --media-type                      Nastavitev vrste medija (privzeto "
+"%s)\n"
+"      --media-category                  Nastavi kategorijo predstavnosti "
+"(privzeto %s)\n"
+"      --media-role                      Nastavi vlogo predstavnosti "
+"(privzeto %s)\n"
+"      --target                          Nastavi serijsko ali ime ciljnega "
+"vozlišča (privzeto %s)\n"
+"                                          0 pomeni, da se ne poveže\n"
+"      --latency                         Nastavi zakasnitev vozlišča "
+"(privzeto %s)\n"
+"                                          Xunit (enota = s, ms, us, ns)\n"
+"                                          ali neposredni vzorci (256)\n"
+"                                          Hitrost je enaka tisti v izvornih "
+"datotekah\n"
+"  -P  --properties                      Nastavi lastnosti vozlišča\n"
+"\n"
+
+#: src/tools/pw-cat.c:998
+#, c-format
+msgid ""
+"      --rate                            Sample rate (req. for rec) (default "
+"%u)\n"
+"      --channels                        Number of channels (req. for rec) "
+"(default %u)\n"
+"      --channel-map                     Channel map\n"
+"                                            one of: \"stereo\", "
+"\"surround-51\",... or\n"
+"                                            comma separated list of channel "
+"names: eg. \"FL,FR\"\n"
+"      --format                          Sample format %s (req. for rec) "
+"(default %s)\n"
+"      --volume                          Stream volume 0-1.0 (default %.3f)\n"
+"  -q  --quality                         Resampler quality (0 - 15) (default "
+"%d)\n"
+"  -a, --raw                             RAW mode\n"
+"\n"
+msgstr ""
+"      --rate                            Mera vzorčenja (zahtevano za rec) "
+"(privzeto %u)\n"
+"      --channels                        Å tevilo kanalov (zahteva za "
+"snemanje) (privzeto %u)\n"
+"      --channel-map                     Preslikava kanalov\n"
+"                                            Ena izmed: \"Stereo\", "
+"\"surround-51\",... ali\n"
+"                                            seznam imen kanalov, ločen z "
+"vejico: npr. \"FL,FR\"\n"
+"      --format                          Vzorčne oblike zapisa %s (zahtevano "
+"za rec) (privzeto %s)\n"
+"      --volume                          Glasnost toka 0-1.0 (privzeto %.3f)\n"
+"  -q  --quality                         Kakovost prevzorčenja (0 - 15) "
+"(privzeto %d)\n"
+"  -a, --raw                             neobdelan način (RAW)\n"
+"\n"
+"\n"
+
+#: src/tools/pw-cat.c:1016
+msgid ""
+"  -p, --playback                        Playback mode\n"
+"  -r, --record                          Recording mode\n"
+"  -m, --midi                            Midi mode\n"
+"  -d, --dsd                             DSD mode\n"
+"  -o, --encoded                         Encoded mode\n"
+"\n"
+msgstr ""
+"  -p, --playback                        Način predvajanja\n"
+"  -r, --record                          Način snemanja\n"
+"  -m, --midi                            Midi način\n"
+"  -d, --dsd                             Način DSD\n"
+"  -o, --encoded                         Kodiran način\n"
+"\n"
+
+#: src/tools/pw-cli.c:2306
+#, c-format
+msgid ""
+"%s [options] [command]\n"
+"  -h, --help                            Show this help\n"
+"      --version                         Show version\n"
+"  -d, --daemon                          Start as daemon (Default false)\n"
+"  -r, --remote                          Remote daemon name\n"
+"  -m, --monitor                         Monitor activity\n"
+"\n"
+msgstr ""
+"%s [možnosti] [ukaz]\n"
+"  -h, --help                            Pokaži to pomoč\n"
+"      --version                         Pokaži različico\n"
+"  -d, --daemon                          Začni kot zaledni proces (privzeto "
+"false)\n"
+"  -r, --remote                          Ime oddaljenega zalednega procesa\n"
+"  -m, --monitor                         Spremljaj dejavnosti\n"
+"\n"
+
+#: spa/plugins/alsa/acp/acp.c:347
+msgid "Pro Audio"
+msgstr "Profesionalni zvok"
+
+#: spa/plugins/alsa/acp/acp.c:507 spa/plugins/alsa/acp/alsa-mixer.c:4635
+#: spa/plugins/bluez5/bluez5-device.c:1795
+msgid "Off"
+msgstr "Izklopljeno"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2652
+msgid "Input"
+msgstr "Vhod"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2653
+msgid "Docking Station Input"
+msgstr "Vhod priklopne postaje"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2654
+msgid "Docking Station Microphone"
+msgstr "Mikrofon priklopne postaje"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2655
+msgid "Docking Station Line In"
+msgstr "Linijski vhod priklopne postaje"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2656
+#: spa/plugins/alsa/acp/alsa-mixer.c:2747
+msgid "Line In"
+msgstr "Linijski vhod"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2657
+#: spa/plugins/alsa/acp/alsa-mixer.c:2741
+#: spa/plugins/bluez5/bluez5-device.c:2139
+msgid "Microphone"
+msgstr "Mikrofon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2658
+#: spa/plugins/alsa/acp/alsa-mixer.c:2742
+msgid "Front Microphone"
+msgstr "Sprednji mikrofon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2659
+#: spa/plugins/alsa/acp/alsa-mixer.c:2743
+msgid "Rear Microphone"
+msgstr "Zadnji mikrofon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2660
+msgid "External Microphone"
+msgstr "Zunanji mikrofon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2661
+#: spa/plugins/alsa/acp/alsa-mixer.c:2745
+msgid "Internal Microphone"
+msgstr "Notranji mikrofon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2662
+#: spa/plugins/alsa/acp/alsa-mixer.c:2748
+msgid "Radio"
+msgstr "Radio"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2663
+#: spa/plugins/alsa/acp/alsa-mixer.c:2749
+msgid "Video"
+msgstr "Video"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2664
+msgid "Automatic Gain Control"
+msgstr "Samodejni nadzor ojačanja"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2665
+msgid "No Automatic Gain Control"
+msgstr "Brez samodejnega nadzora ojačanja"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2666
+msgid "Boost"
+msgstr "Ojačitev"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2667
+msgid "No Boost"
+msgstr "Brez ojačitve"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2668
+msgid "Amplifier"
+msgstr "Ojačevalnik"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2669
+msgid "No Amplifier"
+msgstr "Brez ojačevalnika"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2670
+msgid "Bass Boost"
+msgstr "Ojačitev nizkih tonov"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2671
+msgid "No Bass Boost"
+msgstr "Brez ojačitve nizkih tonov"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2672
+#: spa/plugins/bluez5/bluez5-device.c:2145
+msgid "Speaker"
+msgstr "Zvočnik"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2673
+#: spa/plugins/alsa/acp/alsa-mixer.c:2751
+msgid "Headphones"
+msgstr "Slušalke"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2740
+msgid "Analog Input"
+msgstr "Analogni vhod"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2744
+msgid "Dock Microphone"
+msgstr "Priklopni mikrofon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2746
+msgid "Headset Microphone"
+msgstr "Mikrofon s slušalkami"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2750
+msgid "Analog Output"
+msgstr "Analogni izhod"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2752
+msgid "Headphones 2"
+msgstr "Slušalke 2"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2753
+msgid "Headphones Mono Output"
+msgstr "Mono izhod slušalk"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2754
+msgid "Line Out"
+msgstr "Linijsk izhod"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2755
+msgid "Analog Mono Output"
+msgstr "Analogni mono izhod"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2756
+msgid "Speakers"
+msgstr "Govorniki"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2757
+msgid "HDMI / DisplayPort"
+msgstr "HDMI / DisplayPort"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2758
+msgid "Digital Output (S/PDIF)"
+msgstr "Digitalni izhod (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2759
+msgid "Digital Input (S/PDIF)"
+msgstr "Digitalni vhod (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2760
+msgid "Multichannel Input"
+msgstr "Večkanalni vhod"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2761
+msgid "Multichannel Output"
+msgstr "Večkanalni izhod"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2762
+msgid "Game Output"
+msgstr "Vhod igre"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2763
+#: spa/plugins/alsa/acp/alsa-mixer.c:2764
+msgid "Chat Output"
+msgstr "Izhod klepeta"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2765
+msgid "Chat Input"
+msgstr "Vhod klepeta"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2766
+msgid "Virtual Surround 7.1"
+msgstr "Navidezni prostorski zvok 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4458
+msgid "Analog Mono"
+msgstr "Analogni mono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4459
+msgid "Analog Mono (Left)"
+msgstr "Analogni mono (levo)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4460
+msgid "Analog Mono (Right)"
+msgstr "Analogni mono (desno)"
+
+#. 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:4461
+#: spa/plugins/alsa/acp/alsa-mixer.c:4469
+#: spa/plugins/alsa/acp/alsa-mixer.c:4470
+msgid "Analog Stereo"
+msgstr "Analogni stereo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4462
+msgid "Mono"
+msgstr "Mono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4463
+msgid "Stereo"
+msgstr "Stereo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4471
+#: spa/plugins/alsa/acp/alsa-mixer.c:4629
+#: spa/plugins/bluez5/bluez5-device.c:2127
+msgid "Headset"
+msgstr "Slušalka"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4472
+#: spa/plugins/alsa/acp/alsa-mixer.c:4630
+msgid "Speakerphone"
+msgstr "Zvočnik telefona"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4473
+#: spa/plugins/alsa/acp/alsa-mixer.c:4474
+msgid "Multichannel"
+msgstr "Večkanalno"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4475
+msgid "Analog Surround 2.1"
+msgstr "Analogni prostorski zvok 2.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4476
+msgid "Analog Surround 3.0"
+msgstr "Analogni prostorski zvok 3.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4477
+msgid "Analog Surround 3.1"
+msgstr "Analogni prostorski zvok 3.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4478
+msgid "Analog Surround 4.0"
+msgstr "Analogni prostorski zvok 4.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4479
+msgid "Analog Surround 4.1"
+msgstr "Analogni prostorski zvok 4.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4480
+msgid "Analog Surround 5.0"
+msgstr "Analogni prostorski zvok 5.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4481
+msgid "Analog Surround 5.1"
+msgstr "Analogni prostorski zvok 5.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4482
+msgid "Analog Surround 6.0"
+msgstr "Analogni prostorski zvok 6.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4483
+msgid "Analog Surround 6.1"
+msgstr "Analogni prostorski zvok 6.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4484
+msgid "Analog Surround 7.0"
+msgstr "Analogni prostorski zvok 7.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4485
+msgid "Analog Surround 7.1"
+msgstr "Analogni prostorski zvok 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4486
+msgid "Digital Stereo (IEC958)"
+msgstr "Digitalni stereo (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4487
+msgid "Digital Surround 4.0 (IEC958/AC3)"
+msgstr "Digitalni prostorski zvok 4.0 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4488
+msgid "Digital Surround 5.1 (IEC958/AC3)"
+msgstr "Digitalni prostorski zvok 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4489
+msgid "Digital Surround 5.1 (IEC958/DTS)"
+msgstr "Digitalni prostorski zvok 5.1 (IEC958/DTS)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4490
+msgid "Digital Stereo (HDMI)"
+msgstr "Digitalni stereo (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4491
+msgid "Digital Surround 5.1 (HDMI)"
+msgstr "Digitalni prostorski zvok 5.1 (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4492
+msgid "Chat"
+msgstr "Klepet"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4493
+msgid "Game"
+msgstr "Igra"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4627
+msgid "Analog Mono Duplex"
+msgstr "Analogni mono dupleks"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4628
+msgid "Analog Stereo Duplex"
+msgstr "Analogni stereo dupleks"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4631
+msgid "Digital Stereo Duplex (IEC958)"
+msgstr "Digitalni stereo dupleks (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4632
+msgid "Multichannel Duplex"
+msgstr "Večkanalni dupleks"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4633
+msgid "Stereo Duplex"
+msgstr "Stereo dupleks"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4634
+msgid "Mono Chat + 7.1 Surround"
+msgstr "Mono klepet + prostorski zvok 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4735
+#, c-format
+msgid "%s Output"
+msgstr "Izhod %s"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4743
+#, c-format
+msgid "%s Input"
+msgstr "Vhod %s"
+
+#: spa/plugins/alsa/acp/alsa-util.c:1233 spa/plugins/alsa/acp/alsa-util.c:1327
+#, c-format
+msgid ""
+"snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu "
+"ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_avail() returned a value that is exceptionally large: %lu bytes (%lu "
+"ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+"snd_pcm_avail() je vrnil vrednost, ki je izjemno velika: %lu bajt (%lu ms).\n"
+"Najverjetneje je to napaka v gonilniku ALSA »%s«. O tej težavi obvestite "
+"razvijalce ALSA."
+msgstr[1] ""
+"snd_pcm_avail() je vrnil vrednost, ki je izjemno velika: %lu bajta (%lu "
+"ms).\n"
+"Najverjetneje je to napaka v gonilniku ALSA »%s«. O tej težavi obvestite "
+"razvijalce ALSA."
+msgstr[2] ""
+"snd_pcm_avail() je vrnil vrednost, ki je izjemno velika: %lu bajti (%lu "
+"ms).\n"
+"Najverjetneje je to napaka v gonilniku ALSA »%s«. O tej težavi obvestite "
+"razvijalce ALSA."
+msgstr[3] ""
+"snd_pcm_avail() je vrnil vrednost, ki je izjemno velika: %lu bajtov (%lu "
+"ms).\n"
+"Najverjetneje je to napaka v gonilniku ALSA »%s«. O tej težavi obvestite "
+"razvijalce ALSA."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1299
+#, c-format
+msgid ""
+"snd_pcm_delay() returned a value that is exceptionally large: %li byte (%s"
+"%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_delay() returned a value that is exceptionally large: %li bytes (%s"
+"%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+"snd_pcm_delay() je vrnil vrednost, ki je izjemno velika: %li bajt (%s%lu "
+"ms).\n"
+"Najverjetneje je to napaka v gonilniku ALSA »%s«. O tej težavi obvestite "
+"razvijalce ALSA."
+msgstr[1] ""
+"snd_pcm_delay() je vrnil vrednost, ki je izjemno velika: %li bajta (%s%lu "
+"ms).\n"
+"Najverjetneje je to napaka v gonilniku ALSA »%s«. O tej težavi obvestite "
+"razvijalce ALSA."
+msgstr[2] ""
+"snd_pcm_delay() je vrnil vrednost, ki je izjemno velika: %li bajti (%s%lu "
+"ms).\n"
+"Najverjetneje je to napaka v gonilniku ALSA »%s«. O tej težavi obvestite "
+"razvijalce ALSA."
+msgstr[3] ""
+"snd_pcm_delay() je vrnil vrednost, ki je izjemno velika: %li bajtov (%s%lu "
+"ms).\n"
+"Najverjetneje je to napaka v gonilniku ALSA »%s«. O tej težavi obvestite "
+"razvijalce ALSA."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1346
+#, c-format
+msgid ""
+"snd_pcm_avail_delay() returned strange values: delay %lu is less than avail "
+"%lu.\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr ""
+"snd_pcm_avail_delay() je vrnil nenavadne vrednosti: zakasnitev %lu je manjša "
+"kot razpoložljiva %lu.\n"
+"Najverjetneje je to napaka v gonilniku ALSA »%s«. O tej težavi obvestite "
+"razvijalce ALSA."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1389
+#, c-format
+msgid ""
+"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu byte "
+"(%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu bytes "
+"(%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+"snd_pcm_mmap_begin() je vrnil vrednost, ki je izjemno velika: %lu bajt (%lu "
+"ms).\n"
+"Najverjetneje je to napaka v gonilniku ALSA »%s«. O tej težavi obvestite "
+"razvijalce ALSA."
+msgstr[1] ""
+"snd_pcm_mmap_begin() je vrnil vrednost, ki je izjemno velika: %lu bajta (%lu "
+"ms).\n"
+"Najverjetneje je to napaka v gonilniku ALSA »%s«. O tej težavi obvestite "
+"razvijalce ALSA."
+msgstr[2] ""
+"snd_pcm_mmap_begin() je vrnil vrednost, ki je izjemno velika: %lu bajti (%lu "
+"ms).\n"
+"Najverjetneje je to napaka v gonilniku ALSA »%s«. O tej težavi obvestite "
+"razvijalce ALSA."
+msgstr[3] ""
+"snd_pcm_mmap_begin() je vrnil vrednost, ki je izjemno velika: %lu bajtov "
+"(%lu ms).\n"
+"Najverjetneje je to napaka v gonilniku ALSA »%s«. O tej težavi obvestite "
+"razvijalce ALSA."
+
+#: spa/plugins/alsa/acp/channelmap.h:457
+msgid "(invalid)"
+msgstr "(neveljavno)"
+
+#: spa/plugins/alsa/acp/compat.c:193
+msgid "Built-in Audio"
+msgstr "Vgrajen zvok"
+
+#: spa/plugins/alsa/acp/compat.c:198
+msgid "Modem"
+msgstr "Modem"
+
+#: spa/plugins/bluez5/bluez5-device.c:1806
+msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
+msgstr "Zvožni prehod (vir A2DP in HSP/HFP AG)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1834
+msgid "Audio Streaming for Hearing Aids (ASHA Sink)"
+msgstr "Pretakanje zvoka za slušne aparate (ponor ASHA)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1874
+#, c-format
+msgid "High Fidelity Playback (A2DP Sink, codec %s)"
+msgstr "Predvajanje visoke ločljivosti (ponor A2DP, kodek %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1877
+#, c-format
+msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
+msgstr "Dupleks visoke ločljivosti (vir/ponor A2DP, kodek %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1885
+msgid "High Fidelity Playback (A2DP Sink)"
+msgstr "Predvajanje visoke ločljivosti (ponor A2DP)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1887
+msgid "High Fidelity Duplex (A2DP Source/Sink)"
+msgstr "Dupleks visoke ločljivosti (vir/ponor A2DP)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1937
+#, c-format
+msgid "High Fidelity Playback (BAP Sink, codec %s)"
+msgstr "Predvajanje visoke ločljivosti (ponor BAP, kodek %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1942
+#, c-format
+msgid "High Fidelity Input (BAP Source, codec %s)"
+msgstr "Vhod visoke ločljivosti (vir BAP, kodek %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1946
+#, c-format
+msgid "High Fidelity Duplex (BAP Source/Sink, codec %s)"
+msgstr "Dupleks visoke ločljivosti (vir/ponor BAP, kodek %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1955
+msgid "High Fidelity Playback (BAP Sink)"
+msgstr "Predvajanje visoke ločljivosti (ponor BAP)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1959
+msgid "High Fidelity Input (BAP Source)"
+msgstr "Vhod visoke ločljivosti (vir BAP)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1962
+msgid "High Fidelity Duplex (BAP Source/Sink)"
+msgstr "Dupleks visoke ločljivosti (vir/ponor BAP)"
+
+#: spa/plugins/bluez5/bluez5-device.c:2008
+#, c-format
+msgid "Headset Head Unit (HSP/HFP, codec %s)"
+msgstr "Naglavna enota slušalk (HSP/HFP, kodek %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:2128
+#: spa/plugins/bluez5/bluez5-device.c:2133
+#: spa/plugins/bluez5/bluez5-device.c:2140
+#: spa/plugins/bluez5/bluez5-device.c:2146
+#: spa/plugins/bluez5/bluez5-device.c:2152
+#: spa/plugins/bluez5/bluez5-device.c:2158
+#: spa/plugins/bluez5/bluez5-device.c:2164
+#: spa/plugins/bluez5/bluez5-device.c:2170
+#: spa/plugins/bluez5/bluez5-device.c:2176
+msgid "Handsfree"
+msgstr "Prostoročno telefoniranje"
+
+#: spa/plugins/bluez5/bluez5-device.c:2134
+msgid "Handsfree (HFP)"
+msgstr "Prostoročno telefoniranje (HFP)"
+
+#: spa/plugins/bluez5/bluez5-device.c:2151
+msgid "Headphone"
+msgstr "Slušalke"
+
+#: spa/plugins/bluez5/bluez5-device.c:2157
+msgid "Portable"
+msgstr "Prenosna naprava"
+
+#: spa/plugins/bluez5/bluez5-device.c:2163
+msgid "Car"
+msgstr "Avtomobil"
+
+#: spa/plugins/bluez5/bluez5-device.c:2169
+msgid "HiFi"
+msgstr "HiFi"
+
+#: spa/plugins/bluez5/bluez5-device.c:2175
+msgid "Phone"
+msgstr "Telefon"
+
+#: spa/plugins/bluez5/bluez5-device.c:2182
+msgid "Bluetooth"
+msgstr "Bluetooth"
+
+#: spa/plugins/bluez5/bluez5-device.c:2183
+msgid "Bluetooth (HFP)"
+msgstr "Bluetooth (HFP)"
diff --git a/po/sv.po b/po/sv.po
index 01e97ad1..b867795c 100644
--- a/po/sv.po
+++ b/po/sv.po
@@ -1,9 +1,9 @@
 # Swedish translation for pipewire.
-# Copyright © 2008-2023 Free Software Foundation, Inc.
+# Copyright © 2008-2024 Free Software Foundation, Inc.
 # This file is distributed under the same license as the pipewire package.
 # Daniel Nylander <po@danielnylander.se>, 2008, 2012.
 # Josef Andersson <josef.andersson@fripost.org>, 2014, 2017.
-# Anders Jonsson <anders.jonsson@norsjovallen.se>, 2021, 2022, 2023.
+# Anders Jonsson <anders.jonsson@norsjovallen.se>, 2021, 2022, 2023, 2024.
 #
 # Termer:
 # input/output: ingång/utgång (det handlar om ljud)
@@ -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: 2023-08-07 15:27+0000\n"
-"PO-Revision-Date: 2023-05-12 18:46+0200\n"
+"POT-Creation-Date: 2024-11-05 03:27+0000\n"
+"PO-Revision-Date: 2024-11-07 21:52+0100\n"
 "Last-Translator: Anders Jonsson <anders.jonsson@norsjovallen.se>\n"
 "Language-Team: Swedish <tp-sv@listor.tp-sv.se>\n"
 "Language: sv\n"
@@ -28,20 +28,24 @@ 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.2.2\n"
+"X-Generator: Poedit 3.5\n"
 
-#: src/daemon/pipewire.c:26
+#: src/daemon/pipewire.c:29
 #, c-format
 msgid ""
 "%s [options]\n"
 "  -h, --help                            Show this help\n"
+"  -v, --verbose                         Increase verbosity by one level\n"
 "      --version                         Show version\n"
 "  -c, --config                          Load config (Default %s)\n"
+"  -P  --properties                      Set context properties\n"
 msgstr ""
 "%s [flaggor]\n"
 "  -h, --help                            Visa denna hjälp\n"
+"  -v, --verbose                         Öka utförligheten en nivå\n"
 "      --version                         Visa version\n"
-"  -c, --config                          Läs in konfig (Standard %s)\n"
+"  -c, --config                          Läs in konfig (standard %s)\n"
+"  -P  --properties                      Ställ in kontextegenskaper\n"
 
 #: src/daemon/pipewire.desktop.in:4
 msgid "PipeWire Media System"
@@ -51,36 +55,36 @@ msgstr "PipeWire mediasystem"
 msgid "Start the PipeWire Media System"
 msgstr "Starta mediasystemet PipeWire"
 
-#: src/modules/module-protocol-pulse/modules/module-tunnel-sink.c:141
-#: src/modules/module-protocol-pulse/modules/module-tunnel-source.c:141
+#: src/modules/module-protocol-pulse/modules/module-tunnel-sink.c:159
+#: src/modules/module-protocol-pulse/modules/module-tunnel-source.c:159
 #, c-format
 msgid "Tunnel to %s%s%s"
 msgstr "Tunnel till %s%s%s"
 
-#: src/modules/module-fallback-sink.c:31
+#: src/modules/module-fallback-sink.c:40
 msgid "Dummy Output"
 msgstr "Attrapputgång"
 
-#: src/modules/module-pulse-tunnel.c:847
+#: src/modules/module-pulse-tunnel.c:777
 #, c-format
 msgid "Tunnel for %s@%s"
 msgstr "Tunnel för %s@%s"
 
-#: src/modules/module-zeroconf-discover.c:311
+#: src/modules/module-zeroconf-discover.c:320
 msgid "Unknown device"
 msgstr "Okänd enhet"
 
-#: src/modules/module-zeroconf-discover.c:323
+#: src/modules/module-zeroconf-discover.c:332
 #, c-format
 msgid "%s on %s@%s"
 msgstr "%s på %s@%s"
 
-#: src/modules/module-zeroconf-discover.c:327
+#: src/modules/module-zeroconf-discover.c:336
 #, c-format
 msgid "%s on %s"
 msgstr "%s på %s"
 
-#: src/tools/pw-cat.c:979
+#: src/tools/pw-cat.c:973
 #, c-format
 msgid ""
 "%s [options] [<file>|-]\n"
@@ -95,7 +99,7 @@ msgstr ""
 "  -v, --verbose                         Aktivera utförliga operationer\n"
 "\n"
 
-#: src/tools/pw-cat.c:986
+#: src/tools/pw-cat.c:980
 #, c-format
 msgid ""
 "  -R, --remote                          Remote daemon name\n"
@@ -127,7 +131,7 @@ msgstr ""
 "  -P  --properties                      Sätt nodegenskaper\n"
 "\n"
 
-#: src/tools/pw-cat.c:1004
+#: src/tools/pw-cat.c:998
 #, c-format
 msgid ""
 "      --rate                            Sample rate (req. for rec) (default "
@@ -144,6 +148,7 @@ msgid ""
 "      --volume                          Stream volume 0-1.0 (default %.3f)\n"
 "  -q  --quality                         Resampler quality (0 - 15) (default "
 "%d)\n"
+"  -a, --raw                             RAW mode\n"
 "\n"
 msgstr ""
 "      --rate                            Samplingsfrekvens (krävs för insp.) "
@@ -160,9 +165,10 @@ msgstr ""
 "      --volume                          Strömvolym 0-1.0 (standard %.3f)\n"
 "  -q  --quality                         Omsamplarkvalitet (0 - 15) (standard "
 "%d)\n"
+"  -a, --raw                             RAW-läge\n"
 "\n"
 
-#: src/tools/pw-cat.c:1021
+#: src/tools/pw-cat.c:1016
 msgid ""
 "  -p, --playback                        Playback mode\n"
 "  -r, --record                          Recording mode\n"
@@ -178,7 +184,7 @@ msgstr ""
 "  -o, --encoded                         Kodat läge\n"
 "\n"
 
-#: src/tools/pw-cli.c:2220
+#: src/tools/pw-cli.c:2318
 #, c-format
 msgid ""
 "%s [options] [command]\n"
@@ -197,12 +203,12 @@ msgstr ""
 "  -m, --monitor                         Övervaka aktivitet\n"
 "\n"
 
-#: spa/plugins/alsa/acp/acp.c:325
+#: spa/plugins/alsa/acp/acp.c:327
 msgid "Pro Audio"
 msgstr "Professionellt ljud"
 
-#: spa/plugins/alsa/acp/acp.c:449 spa/plugins/alsa/acp/alsa-mixer.c:4648
-#: spa/plugins/bluez5/bluez5-device.c:1586
+#: spa/plugins/alsa/acp/acp.c:487 spa/plugins/alsa/acp/alsa-mixer.c:4633
+#: spa/plugins/bluez5/bluez5-device.c:1705
 msgid "Off"
 msgstr "Av"
 
@@ -229,7 +235,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:1831
+#: spa/plugins/bluez5/bluez5-device.c:2026
 msgid "Microphone"
 msgstr "Mikrofon"
 
@@ -295,7 +301,7 @@ msgid "No Bass Boost"
 msgstr "Ingen basökning"
 
 #: spa/plugins/alsa/acp/alsa-mixer.c:2672
-#: spa/plugins/bluez5/bluez5-device.c:1837
+#: spa/plugins/bluez5/bluez5-device.c:2032
 msgid "Speaker"
 msgstr "Högtalare"
 
@@ -377,15 +383,15 @@ msgstr "Chatt-ingång"
 msgid "Virtual Surround 7.1"
 msgstr "Virtual surround 7.1"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4471
+#: spa/plugins/alsa/acp/alsa-mixer.c:4456
 msgid "Analog Mono"
 msgstr "Analog mono"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4472
+#: spa/plugins/alsa/acp/alsa-mixer.c:4457
 msgid "Analog Mono (Left)"
 msgstr "Analog mono (vänster)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4473
+#: spa/plugins/alsa/acp/alsa-mixer.c:4458
 msgid "Analog Mono (Right)"
 msgstr "Analog mono (höger)"
 
@@ -394,147 +400,147 @@ msgstr "Analog mono (höger)"
 #. * here would lead to the source name to become "Analog Stereo Input
 #. * Input". The same logic applies to analog-stereo-output,
 #. * multichannel-input and multichannel-output.
-#: spa/plugins/alsa/acp/alsa-mixer.c:4474
-#: spa/plugins/alsa/acp/alsa-mixer.c:4482
-#: spa/plugins/alsa/acp/alsa-mixer.c:4483
+#: spa/plugins/alsa/acp/alsa-mixer.c:4459
+#: spa/plugins/alsa/acp/alsa-mixer.c:4467
+#: spa/plugins/alsa/acp/alsa-mixer.c:4468
 msgid "Analog Stereo"
 msgstr "Analog stereo"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4475
+#: spa/plugins/alsa/acp/alsa-mixer.c:4460
 msgid "Mono"
 msgstr "Mono"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4476
+#: spa/plugins/alsa/acp/alsa-mixer.c:4461
 msgid "Stereo"
 msgstr "Stereo"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4484
-#: spa/plugins/alsa/acp/alsa-mixer.c:4642
-#: spa/plugins/bluez5/bluez5-device.c:1819
+#: spa/plugins/alsa/acp/alsa-mixer.c:4469
+#: spa/plugins/alsa/acp/alsa-mixer.c:4627
+#: spa/plugins/bluez5/bluez5-device.c:2014
 msgid "Headset"
 msgstr "Headset"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4485
-#: spa/plugins/alsa/acp/alsa-mixer.c:4643
+#: spa/plugins/alsa/acp/alsa-mixer.c:4470
+#: spa/plugins/alsa/acp/alsa-mixer.c:4628
 msgid "Speakerphone"
 msgstr "Högtalartelefon"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4486
-#: spa/plugins/alsa/acp/alsa-mixer.c:4487
+#: spa/plugins/alsa/acp/alsa-mixer.c:4471
+#: spa/plugins/alsa/acp/alsa-mixer.c:4472
 msgid "Multichannel"
 msgstr "Multikanal"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4488
+#: spa/plugins/alsa/acp/alsa-mixer.c:4473
 msgid "Analog Surround 2.1"
 msgstr "Analog surround 2.1"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4489
+#: spa/plugins/alsa/acp/alsa-mixer.c:4474
 msgid "Analog Surround 3.0"
 msgstr "Analog surround 3.0"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4490
+#: spa/plugins/alsa/acp/alsa-mixer.c:4475
 msgid "Analog Surround 3.1"
 msgstr "Analog surround 3.1"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4491
+#: spa/plugins/alsa/acp/alsa-mixer.c:4476
 msgid "Analog Surround 4.0"
 msgstr "Analog surround 4.0"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4492
+#: spa/plugins/alsa/acp/alsa-mixer.c:4477
 msgid "Analog Surround 4.1"
 msgstr "Analog surround 4.1"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4493
+#: spa/plugins/alsa/acp/alsa-mixer.c:4478
 msgid "Analog Surround 5.0"
 msgstr "Analog surround 5.0"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4494
+#: spa/plugins/alsa/acp/alsa-mixer.c:4479
 msgid "Analog Surround 5.1"
 msgstr "Analog surround 5.1"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4495
+#: spa/plugins/alsa/acp/alsa-mixer.c:4480
 msgid "Analog Surround 6.0"
 msgstr "Analog surround 6.0"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4496
+#: spa/plugins/alsa/acp/alsa-mixer.c:4481
 msgid "Analog Surround 6.1"
 msgstr "Analog surround 6.1"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4497
+#: spa/plugins/alsa/acp/alsa-mixer.c:4482
 msgid "Analog Surround 7.0"
 msgstr "Analog surround 7.0"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4498
+#: spa/plugins/alsa/acp/alsa-mixer.c:4483
 msgid "Analog Surround 7.1"
 msgstr "Analog surround 7.1"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4499
+#: spa/plugins/alsa/acp/alsa-mixer.c:4484
 msgid "Digital Stereo (IEC958)"
 msgstr "Digital stereo (IEC958)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4500
+#: spa/plugins/alsa/acp/alsa-mixer.c:4485
 msgid "Digital Surround 4.0 (IEC958/AC3)"
 msgstr "Digital surround 4.0 (IEC958/AC3)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4501
+#: spa/plugins/alsa/acp/alsa-mixer.c:4486
 msgid "Digital Surround 5.1 (IEC958/AC3)"
 msgstr "Digital surround 5.1 (IEC958/AC3)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4502
+#: spa/plugins/alsa/acp/alsa-mixer.c:4487
 msgid "Digital Surround 5.1 (IEC958/DTS)"
 msgstr "Digital surround 5.1 (IEC958/DTS)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4503
+#: spa/plugins/alsa/acp/alsa-mixer.c:4488
 msgid "Digital Stereo (HDMI)"
 msgstr "Digital stereo (HDMI)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4504
+#: spa/plugins/alsa/acp/alsa-mixer.c:4489
 msgid "Digital Surround 5.1 (HDMI)"
 msgstr "Digital surround 5.1 (HDMI)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4505
+#: spa/plugins/alsa/acp/alsa-mixer.c:4490
 msgid "Chat"
 msgstr "Chatt"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4506
+#: spa/plugins/alsa/acp/alsa-mixer.c:4491
 msgid "Game"
 msgstr "Spel"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4640
+#: spa/plugins/alsa/acp/alsa-mixer.c:4625
 msgid "Analog Mono Duplex"
 msgstr "Analog mono duplex"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4641
+#: spa/plugins/alsa/acp/alsa-mixer.c:4626
 msgid "Analog Stereo Duplex"
 msgstr "Analog stereo duplex"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4644
+#: spa/plugins/alsa/acp/alsa-mixer.c:4629
 msgid "Digital Stereo Duplex (IEC958)"
 msgstr "Digital stereo duplex (IEC958)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4645
+#: spa/plugins/alsa/acp/alsa-mixer.c:4630
 msgid "Multichannel Duplex"
 msgstr "Multikanalduplex"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4646
+#: spa/plugins/alsa/acp/alsa-mixer.c:4631
 msgid "Stereo Duplex"
 msgstr "Stereo duplex"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4647
+#: spa/plugins/alsa/acp/alsa-mixer.c:4632
 msgid "Mono Chat + 7.1 Surround"
 msgstr "Mono Chatt + 7.1 Surround"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4748
+#: spa/plugins/alsa/acp/alsa-mixer.c:4733
 #, c-format
 msgid "%s Output"
 msgstr "%s-utgång"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4756
+#: spa/plugins/alsa/acp/alsa-mixer.c:4741
 #, c-format
 msgid "%s Input"
 msgstr "%s-ingång"
 
-#: spa/plugins/alsa/acp/alsa-util.c:1187 spa/plugins/alsa/acp/alsa-util.c:1281
+#: spa/plugins/alsa/acp/alsa-util.c:1231 spa/plugins/alsa/acp/alsa-util.c:1325
 #, c-format
 msgid ""
 "snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu "
@@ -557,7 +563,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:1253
+#: spa/plugins/alsa/acp/alsa-util.c:1297
 #, c-format
 msgid ""
 "snd_pcm_delay() returned a value that is exceptionally large: %li byte "
@@ -580,7 +586,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:1300
+#: spa/plugins/alsa/acp/alsa-util.c:1344
 #, c-format
 msgid ""
 "snd_pcm_avail_delay() returned strange values: delay %lu is less than avail "
@@ -593,7 +599,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:1343
+#: spa/plugins/alsa/acp/alsa-util.c:1387
 #, c-format
 msgid ""
 "snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu byte "
@@ -620,112 +626,108 @@ msgstr[1] ""
 msgid "(invalid)"
 msgstr "(ogiltig)"
 
-#: spa/plugins/alsa/acp/compat.c:189
+#: spa/plugins/alsa/acp/compat.c:193
 msgid "Built-in Audio"
 msgstr "Inbyggt ljud"
 
-#: spa/plugins/alsa/acp/compat.c:194
+#: spa/plugins/alsa/acp/compat.c:198
 msgid "Modem"
 msgstr "Modem"
 
-#: spa/plugins/bluez5/bluez5-device.c:1597
+#: spa/plugins/bluez5/bluez5-device.c:1716
 msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
 msgstr "Audio gateway (A2DP-källa & HSP/HFP AG)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1622
+#: spa/plugins/bluez5/bluez5-device.c:1764
 #, 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:1625
+#: spa/plugins/bluez5/bluez5-device.c:1767
 #, 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:1633
+#: spa/plugins/bluez5/bluez5-device.c:1775
 msgid "High Fidelity Playback (A2DP Sink)"
 msgstr "High fidelity-uppspelning (A2DP-utgång)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1635
+#: spa/plugins/bluez5/bluez5-device.c:1777
 msgid "High Fidelity Duplex (A2DP Source/Sink)"
 msgstr "High fidelity duplex (A2DP-källa/utgång)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1677
+#: spa/plugins/bluez5/bluez5-device.c:1827
 #, 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:1681
+#: spa/plugins/bluez5/bluez5-device.c:1832
 #, 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:1685
+#: spa/plugins/bluez5/bluez5-device.c:1836
 #, 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:1693
+#: spa/plugins/bluez5/bluez5-device.c:1845
 msgid "High Fidelity Playback (BAP Sink)"
 msgstr "High fidelity-uppspelning (BAP-utgång)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1696
+#: spa/plugins/bluez5/bluez5-device.c:1849
 msgid "High Fidelity Input (BAP Source)"
 msgstr "High fidelity-ingång (BAP-källa)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1699
+#: spa/plugins/bluez5/bluez5-device.c:1852
 msgid "High Fidelity Duplex (BAP Source/Sink)"
 msgstr "High fidelity duplex (BAP-källa/utgång)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1735
+#: spa/plugins/bluez5/bluez5-device.c:1901
 #, c-format
 msgid "Headset Head Unit (HSP/HFP, codec %s)"
 msgstr "Headset-huvudenhet (HSP/HFP, kodek %s)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1740
-msgid "Headset Head Unit (HSP/HFP)"
-msgstr "Headset-huvudenhet (HSP/HFP)"
-
-#: spa/plugins/bluez5/bluez5-device.c:1820
-#: spa/plugins/bluez5/bluez5-device.c:1825
-#: spa/plugins/bluez5/bluez5-device.c:1832
-#: spa/plugins/bluez5/bluez5-device.c:1838
-#: spa/plugins/bluez5/bluez5-device.c:1844
-#: spa/plugins/bluez5/bluez5-device.c:1850
-#: spa/plugins/bluez5/bluez5-device.c:1856
-#: spa/plugins/bluez5/bluez5-device.c:1862
-#: spa/plugins/bluez5/bluez5-device.c:1868
+#: spa/plugins/bluez5/bluez5-device.c:2015
+#: spa/plugins/bluez5/bluez5-device.c:2020
+#: spa/plugins/bluez5/bluez5-device.c:2027
+#: spa/plugins/bluez5/bluez5-device.c:2033
+#: spa/plugins/bluez5/bluez5-device.c:2039
+#: spa/plugins/bluez5/bluez5-device.c:2045
+#: spa/plugins/bluez5/bluez5-device.c:2051
+#: spa/plugins/bluez5/bluez5-device.c:2057
+#: spa/plugins/bluez5/bluez5-device.c:2063
 msgid "Handsfree"
 msgstr "Handsfree"
 
-#: spa/plugins/bluez5/bluez5-device.c:1826
+#: spa/plugins/bluez5/bluez5-device.c:2021
 msgid "Handsfree (HFP)"
 msgstr "Handsfree (HFP)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1843
+#: spa/plugins/bluez5/bluez5-device.c:2038
 msgid "Headphone"
 msgstr "Hörlurar"
 
-#: spa/plugins/bluez5/bluez5-device.c:1849
+#: spa/plugins/bluez5/bluez5-device.c:2044
 msgid "Portable"
 msgstr "Bärbar"
 
-#: spa/plugins/bluez5/bluez5-device.c:1855
+#: spa/plugins/bluez5/bluez5-device.c:2050
 msgid "Car"
 msgstr "Bil"
 
-#: spa/plugins/bluez5/bluez5-device.c:1861
+#: spa/plugins/bluez5/bluez5-device.c:2056
 msgid "HiFi"
 msgstr "HiFi"
 
-#: spa/plugins/bluez5/bluez5-device.c:1867
+#: spa/plugins/bluez5/bluez5-device.c:2062
 msgid "Phone"
 msgstr "Telefon"
 
-#: spa/plugins/bluez5/bluez5-device.c:1874
+#: spa/plugins/bluez5/bluez5-device.c:2069
 msgid "Bluetooth"
 msgstr "Bluetooth"
 
-#: spa/plugins/bluez5/bluez5-device.c:1875
+#: spa/plugins/bluez5/bluez5-device.c:2070
 msgid "Bluetooth (HFP)"
 msgstr "Bluetooth (HFP)"
diff --git a/po/zh_CN.po b/po/zh_CN.po
index 53ce6e8d..1022f5d6 100644
--- a/po/zh_CN.po
+++ b/po/zh_CN.po
@@ -6,106 +6,127 @@
 # Cheng-Chia Tseng <pswo10680@gmail.com>, 2010, 2012.
 # Frank Hill <hxf.prc@gmail.com>, 2015.
 # Mingye Wang (Arthur2e5) <arthur200126@gmail.com>, 2015.
+# lumingzh <lumingzh@qq.com>, 2024.
 #
 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 10:47+0800\n"
-"PO-Revision-Date: 2021-04-18 10:56+0800\n"
-"Last-Translator: Huang-Huang Bao <i@eh5.me>\n"
-"Language-Team: Huang-Huang Bao <i@eh5.me>\n"
+"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/-/"
+"issues\n"
+"POT-Creation-Date: 2024-09-09 16:36+0000\n"
+"PO-Revision-Date: 2024-10-08 09:41+0800\n"
+"Last-Translator: lumingzh <lumingzh@qq.com>\n"
+"Language-Team: Chinese (China) <i18n-zh@googlegroups.com>\n"
 "Language: zh_CN\n"
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
 "X-Launchpad-Export-Date: 2016-03-22 13:23+0000\n"
-"X-Generator: Poedit 2.4.1\n"
+"X-Generator: Gtranslator 47.0\n"
 "Plural-Forms: nplurals=1; plural=0;\n"
 
-#: src/daemon/pipewire.c:43
+#: src/daemon/pipewire.c:29
 #, c-format
 msgid ""
 "%s [options]\n"
 "  -h, --help                            Show this help\n"
+"  -v, --verbose                         Increase verbosity by one level\n"
 "      --version                         Show version\n"
 "  -c, --config                          Load config (Default %s)\n"
+"  -P  --properties                      Set context properties\n"
 msgstr ""
 "%s [选项]\n"
 "  -h, --help                            显示此帮助信息\n"
+"  -v, --verbose                         增加一级的详尽程度\n"
 "      --version                         显示版本\n"
-"  -c, --config                          加载配置文件 (默认 %s)\n"
+"  -c, --config                          加载配置 (默认 %s)\n"
+"  -P  --properties                      设置上下文属性\n"
 
 #: src/daemon/pipewire.desktop.in:4
 msgid "PipeWire Media System"
-msgstr "PipeWire多媒体系统"
+msgstr "PipeWire 多媒体系统"
 
 #: src/daemon/pipewire.desktop.in:5
 msgid "Start the PipeWire Media System"
-msgstr "启动PipeWire多媒体系统"
+msgstr "启动 PipeWire 多媒体系统"
 
-#: src/examples/media-session/alsa-monitor.c:526
-#: spa/plugins/alsa/acp/compat.c:187
-msgid "Built-in Audio"
-msgstr "内置音频"
+#: src/modules/module-protocol-pulse/modules/module-tunnel-sink.c:159
+#: src/modules/module-protocol-pulse/modules/module-tunnel-source.c:159
+#, c-format
+msgid "Tunnel to %s%s%s"
+msgstr "至 %s%s%s 的隧道"
 
-#: src/examples/media-session/alsa-monitor.c:530
-#: spa/plugins/alsa/acp/compat.c:192
-msgid "Modem"
-msgstr "调制解调器"
+#: src/modules/module-fallback-sink.c:40
+msgid "Dummy Output"
+msgstr "虚拟输出"
 
-#: src/examples/media-session/alsa-monitor.c:539
+#: src/modules/module-pulse-tunnel.c:774
+#, c-format
+msgid "Tunnel for %s@%s"
+msgstr "用于 %s@%s 的隧道"
+
+#: src/modules/module-zeroconf-discover.c:318
 msgid "Unknown device"
 msgstr "未知设备"
 
-#: src/tools/pw-cat.c:991
+#: src/modules/module-zeroconf-discover.c:330
+#, c-format
+msgid "%s on %s@%s"
+msgstr "%2$s@%3$s 上的 %1$s"
+
+#: src/modules/module-zeroconf-discover.c:334
+#, c-format
+msgid "%s on %s"
+msgstr "%2$s 上的 %1$s"
+
+#: src/tools/pw-cat.c:996
 #, 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 [选项] <文件>\n"
+"%s [选项] [<文件>|-]\n"
 "  -h, --help                            显示此帮助信息\n"
 "      --version                         显示版本\n"
 "  -v, --verbose                         输出详细操作\n"
 "\n"
 
-#: src/tools/pw-cat.c:998
+#: src/tools/pw-cat.c:1003
 #, c-format
 msgid ""
 "  -R, --remote                          Remote daemon name\n"
 "      --media-type                      Set media type (default %s)\n"
 "      --media-category                  Set media category (default %s)\n"
 "      --media-role                      Set media role (default %s)\n"
-"      --target                          Set node target (default %s)\n"
+"      --target                          Set node target serial or name "
+"(default %s)\n"
 "                                          0 means don't link\n"
 "      --latency                         Set node latency (default %s)\n"
 "                                          Xunit (unit = s, ms, us, ns)\n"
 "                                          or direct samples (256)\n"
 "                                          the rate is the one of the source "
 "file\n"
-"      --list-targets                    List available targets for --target\n"
+"  -P  --properties                      Set node properties\n"
 "\n"
 msgstr ""
 "  -R, --remote                          远程守护程序名\n"
 "      --media-type                      设置媒体类型 (默认 %s)\n"
 "      --media-category                  设置媒体类别 (默认 %s)\n"
 "      --media-role                      设置媒体角色 (默认 %s)\n"
-"      --target                          设置目标节点 (默认 %s)\n"
-"                                          设为0则不链接节点\n"
+"      --target                          设置节点目标序列或名称 (默认 %s)\n"
+"                                          设为 0 则不链接节点\n"
 "      --latency                         设置节点延迟 (默认 %s)\n"
 "                                          时间 (单位可为 s, ms, us, ns)\n"
 "                                          或样本数 (如256)\n"
 "                                          对应的采样率则是媒体源文件采样率的"
 "其一\n"
-"      --list-targets                    列出可用的 --target 目标\n"
+"  -P  --properties                      设置节点属性\n"
 "\n"
 
-#: src/tools/pw-cat.c:1016
+#: src/tools/pw-cat.c:1021
 #, c-format
 msgid ""
 "      --rate                            Sample rate (req. for rec) (default "
@@ -122,6 +143,7 @@ msgid ""
 "      --volume                          Stream volume 0-1.0 (default %.3f)\n"
 "  -q  --quality                         Resampler quality (0 - 15) (default "
 "%d)\n"
+"  -a, --raw                             RAW mode\n"
 "\n"
 msgstr ""
 "      --rate                            采样率 (录制模式需要) (默认 %u)\n"
@@ -135,21 +157,26 @@ msgstr ""
 "%s)\n"
 "      --volume                          媒体流音量 0-1.0 (默认 %.3f)\n"
 "  -q  --quality                         重采样质量 (0 - 15) (默认 %d)\n"
+"  -a, --raw                             原生模式\n"
 "\n"
 
-#: src/tools/pw-cat.c:1033
+#: src/tools/pw-cat.c:1039
 msgid ""
 "  -p, --playback                        Playback mode\n"
 "  -r, --record                          Recording mode\n"
 "  -m, --midi                            Midi mode\n"
+"  -d, --dsd                             DSD mode\n"
+"  -o, --encoded                         Encoded mode\n"
 "\n"
 msgstr ""
 "  -p, --playback                        回放模式\n"
 "  -r, --record                          录制模式\n"
-"  -m, --midi                            Midi模式\n"
+"  -m, --midi                            Midi 模式\n"
+"  -d, --dsd                             DSD 模式\n"
+"  -o, --encoded                         编码模式\n"
 "\n"
 
-#: src/tools/pw-cli.c:2932
+#: src/tools/pw-cli.c:2285
 #, c-format
 msgid ""
 "%s [options] [command]\n"
@@ -157,208 +184,205 @@ msgid ""
 "      --version                         Show version\n"
 "  -d, --daemon                          Start as daemon (Default false)\n"
 "  -r, --remote                          Remote daemon name\n"
+"  -m, --monitor                         Monitor activity\n"
 "\n"
 msgstr ""
 "%s [选项] [命令]\n"
 "  -h, --help                            显示此帮助信息\n"
 "      --version                         显示版本\n"
 "  -d, --daemon                          以守护程序方式启动 (默认关闭)\n"
-"  -r, --remote                          远程守护程序名\n"
+"  -m, --monitor                         监视器活动\n"
 "\n"
 
-#: spa/plugins/alsa/acp/acp.c:290
+#: spa/plugins/alsa/acp/acp.c:327
 msgid "Pro Audio"
 msgstr "专业音频"
 
-#: 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:488 spa/plugins/alsa/acp/alsa-mixer.c:4633
+#: spa/plugins/bluez5/bluez5-device.c:1701
 msgid "Off"
 msgstr "å…³"
 
-#: spa/plugins/alsa/acp/channelmap.h:466
-msgid "(invalid)"
-msgstr "(无效)"
-
-#: spa/plugins/alsa/acp/alsa-mixer.c:2709
+#: spa/plugins/alsa/acp/alsa-mixer.c:2652
 msgid "Input"
 msgstr "输入"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2710
+#: spa/plugins/alsa/acp/alsa-mixer.c:2653
 msgid "Docking Station Input"
 msgstr "扩展坞输入"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2711
+#: spa/plugins/alsa/acp/alsa-mixer.c:2654
 msgid "Docking Station Microphone"
 msgstr "扩展坞话筒"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2712
+#: spa/plugins/alsa/acp/alsa-mixer.c:2655
 msgid "Docking Station Line In"
 msgstr "扩展坞线输入"
 
-#: 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 "输入插孔"
 
-#: 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:1989
 msgid "Microphone"
 msgstr "话筒"
 
-#: 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 "前麦克风"
 
-#: 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 "后麦克风"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2717
+#: spa/plugins/alsa/acp/alsa-mixer.c:2660
 msgid "External Microphone"
 msgstr "外部话筒"
 
-#: 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 "内部话筒"
 
-#: 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 "无线电"
 
-#: 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 "视频"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2721
+#: spa/plugins/alsa/acp/alsa-mixer.c:2664
 msgid "Automatic Gain Control"
 msgstr "自动增益控制"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2722
+#: spa/plugins/alsa/acp/alsa-mixer.c:2665
 msgid "No Automatic Gain Control"
 msgstr "无自动增益控制"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2723
+#: spa/plugins/alsa/acp/alsa-mixer.c:2666
 msgid "Boost"
 msgstr "增强"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2724
+#: spa/plugins/alsa/acp/alsa-mixer.c:2667
 msgid "No Boost"
 msgstr "无增强"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2725
+#: spa/plugins/alsa/acp/alsa-mixer.c:2668
 msgid "Amplifier"
 msgstr "功放"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2726
+#: spa/plugins/alsa/acp/alsa-mixer.c:2669
 msgid "No Amplifier"
 msgstr "无功放"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2727
+#: spa/plugins/alsa/acp/alsa-mixer.c:2670
 msgid "Bass Boost"
 msgstr "重低音增强"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2728
+#: spa/plugins/alsa/acp/alsa-mixer.c:2671
 msgid "No Bass Boost"
 msgstr "无重低音增强"
 
-#: 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:1995
 msgid "Speaker"
 msgstr "扬声器"
 
-#: 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 "模拟耳机"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2797
+#: spa/plugins/alsa/acp/alsa-mixer.c:2740
 msgid "Analog Input"
 msgstr "模拟输入"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2801
+#: spa/plugins/alsa/acp/alsa-mixer.c:2744
 msgid "Dock Microphone"
 msgstr "扩展坞麦克风"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2803
+#: spa/plugins/alsa/acp/alsa-mixer.c:2746
 msgid "Headset Microphone"
 msgstr "头挂麦克风"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2807
+#: spa/plugins/alsa/acp/alsa-mixer.c:2750
 msgid "Analog Output"
 msgstr "模拟输出"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2809
+#: spa/plugins/alsa/acp/alsa-mixer.c:2752
 msgid "Headphones 2"
 msgstr "模拟耳机 2"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2810
+#: spa/plugins/alsa/acp/alsa-mixer.c:2753
 msgid "Headphones Mono Output"
 msgstr "模拟单声道输出"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2811
+#: spa/plugins/alsa/acp/alsa-mixer.c:2754
 msgid "Line Out"
 msgstr "线缆输出"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2812
+#: spa/plugins/alsa/acp/alsa-mixer.c:2755
 msgid "Analog Mono Output"
 msgstr "模拟单声道输出"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2813
+#: spa/plugins/alsa/acp/alsa-mixer.c:2756
 msgid "Speakers"
 msgstr "扬声器"
 
-#: 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 "数字输出 (S/PDIF)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2816
+#: spa/plugins/alsa/acp/alsa-mixer.c:2759
 msgid "Digital Input (S/PDIF)"
 msgstr "数字输入 (S/PDIF)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2817
+#: spa/plugins/alsa/acp/alsa-mixer.c:2760
 msgid "Multichannel Input"
 msgstr "多声道输入"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2818
+#: spa/plugins/alsa/acp/alsa-mixer.c:2761
 msgid "Multichannel Output"
 msgstr "多声道输出"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2819
+#: spa/plugins/alsa/acp/alsa-mixer.c:2762
 msgid "Game Output"
 msgstr "游戏输出"
 
-#: 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 "语音输出"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2822
+#: spa/plugins/alsa/acp/alsa-mixer.c:2765
 msgid "Chat Input"
 msgstr "语音输入"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:2823
+#: spa/plugins/alsa/acp/alsa-mixer.c:2766
 msgid "Virtual Surround 7.1"
 msgstr "虚拟环绕 7.1"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4527
+#: spa/plugins/alsa/acp/alsa-mixer.c:4456
 msgid "Analog Mono"
 msgstr "模拟单声道"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4528
+#: spa/plugins/alsa/acp/alsa-mixer.c:4457
 msgid "Analog Mono (Left)"
 msgstr "模拟单声道 (左声道)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4529
+#: spa/plugins/alsa/acp/alsa-mixer.c:4458
 msgid "Analog Mono (Right)"
 msgstr "模拟单声道 (右声道)"
 
@@ -367,147 +391,147 @@ msgstr "模拟单声道 (右声道)"
 #. * 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:4459
+#: spa/plugins/alsa/acp/alsa-mixer.c:4467
+#: spa/plugins/alsa/acp/alsa-mixer.c:4468
 msgid "Analog Stereo"
 msgstr "模拟立体声"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4531
+#: spa/plugins/alsa/acp/alsa-mixer.c:4460
 msgid "Mono"
 msgstr "单声道"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4532
+#: spa/plugins/alsa/acp/alsa-mixer.c:4461
 msgid "Stereo"
 msgstr "立体声"
 
-#: 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:4469
+#: spa/plugins/alsa/acp/alsa-mixer.c:4627
+#: spa/plugins/bluez5/bluez5-device.c:1977
 msgid "Headset"
 msgstr "耳机"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4541
-#: spa/plugins/alsa/acp/alsa-mixer.c:4699
+#: spa/plugins/alsa/acp/alsa-mixer.c:4470
+#: spa/plugins/alsa/acp/alsa-mixer.c:4628
 msgid "Speakerphone"
 msgstr "扬声麦克风"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4542
-#: spa/plugins/alsa/acp/alsa-mixer.c:4543
+#: spa/plugins/alsa/acp/alsa-mixer.c:4471
+#: spa/plugins/alsa/acp/alsa-mixer.c:4472
 msgid "Multichannel"
 msgstr "多声道"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4544
+#: spa/plugins/alsa/acp/alsa-mixer.c:4473
 msgid "Analog Surround 2.1"
 msgstr "模拟环绕 2.1"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4545
+#: spa/plugins/alsa/acp/alsa-mixer.c:4474
 msgid "Analog Surround 3.0"
 msgstr "模拟环绕 3.0"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4546
+#: spa/plugins/alsa/acp/alsa-mixer.c:4475
 msgid "Analog Surround 3.1"
 msgstr "模拟环绕 3.1"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4547
+#: spa/plugins/alsa/acp/alsa-mixer.c:4476
 msgid "Analog Surround 4.0"
 msgstr "模拟环绕 4.0"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4548
+#: spa/plugins/alsa/acp/alsa-mixer.c:4477
 msgid "Analog Surround 4.1"
 msgstr "模拟环绕 4.1"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4549
+#: spa/plugins/alsa/acp/alsa-mixer.c:4478
 msgid "Analog Surround 5.0"
 msgstr "模拟环绕 5.0"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4550
+#: spa/plugins/alsa/acp/alsa-mixer.c:4479
 msgid "Analog Surround 5.1"
 msgstr "模拟环绕 5.1"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4551
+#: spa/plugins/alsa/acp/alsa-mixer.c:4480
 msgid "Analog Surround 6.0"
 msgstr "模拟环绕 6.0"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4552
+#: spa/plugins/alsa/acp/alsa-mixer.c:4481
 msgid "Analog Surround 6.1"
 msgstr "模拟环绕 6.1"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4553
+#: spa/plugins/alsa/acp/alsa-mixer.c:4482
 msgid "Analog Surround 7.0"
 msgstr "模拟环绕 7.0"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4554
+#: spa/plugins/alsa/acp/alsa-mixer.c:4483
 msgid "Analog Surround 7.1"
 msgstr "模拟环绕 7.1"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4555
+#: spa/plugins/alsa/acp/alsa-mixer.c:4484
 msgid "Digital Stereo (IEC958)"
-msgstr "数字立体声(IEC958)"
+msgstr "数字立体声 (IEC958)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4556
+#: spa/plugins/alsa/acp/alsa-mixer.c:4485
 msgid "Digital Surround 4.0 (IEC958/AC3)"
-msgstr "数字环绕 4.0(IEC958/AC3)"
+msgstr "数字环绕 4.0 (IEC958/AC3)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4557
+#: spa/plugins/alsa/acp/alsa-mixer.c:4486
 msgid "Digital Surround 5.1 (IEC958/AC3)"
-msgstr "数字环绕 5.1(IEC958/AC3)"
+msgstr "数字环绕 5.1 (IEC958/AC3)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4558
+#: spa/plugins/alsa/acp/alsa-mixer.c:4487
 msgid "Digital Surround 5.1 (IEC958/DTS)"
-msgstr "数字环绕 5.1(IEC958/DTS)"
+msgstr "数字环绕 5.1 (IEC958/DTS)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4559
+#: spa/plugins/alsa/acp/alsa-mixer.c:4488
 msgid "Digital Stereo (HDMI)"
-msgstr "数字立体声(HDMI)"
+msgstr "数字立体声 (HDMI)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4560
+#: spa/plugins/alsa/acp/alsa-mixer.c:4489
 msgid "Digital Surround 5.1 (HDMI)"
-msgstr "数字环绕 5.1(HDMI)"
+msgstr "数字环绕 5.1 (HDMI)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4561
+#: spa/plugins/alsa/acp/alsa-mixer.c:4490
 msgid "Chat"
 msgstr "语音"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4562
+#: spa/plugins/alsa/acp/alsa-mixer.c:4491
 msgid "Game"
 msgstr "游戏"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4696
+#: spa/plugins/alsa/acp/alsa-mixer.c:4625
 msgid "Analog Mono Duplex"
 msgstr "模拟单声道双工"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4697
+#: spa/plugins/alsa/acp/alsa-mixer.c:4626
 msgid "Analog Stereo Duplex"
 msgstr "模拟立体声双工"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4700
+#: spa/plugins/alsa/acp/alsa-mixer.c:4629
 msgid "Digital Stereo Duplex (IEC958)"
-msgstr "数字立体声双工(IEC958)"
+msgstr "数字立体声双工 (IEC958)"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4701
+#: spa/plugins/alsa/acp/alsa-mixer.c:4630
 msgid "Multichannel Duplex"
 msgstr "多声道双工"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4702
+#: spa/plugins/alsa/acp/alsa-mixer.c:4631
 msgid "Stereo Duplex"
 msgstr "模拟立体声双工"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4703
+#: spa/plugins/alsa/acp/alsa-mixer.c:4632
 msgid "Mono Chat + 7.1 Surround"
 msgstr "单声道语音 + 7.1 环绕声"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4806
+#: spa/plugins/alsa/acp/alsa-mixer.c:4733
 #, c-format
 msgid "%s Output"
 msgstr "%s 输出"
 
-#: spa/plugins/alsa/acp/alsa-mixer.c:4813
+#: spa/plugins/alsa/acp/alsa-mixer.c:4741
 #, c-format
 msgid "%s Input"
 msgstr "%s 输入"
 
-#: spa/plugins/alsa/acp/alsa-util.c:1175 spa/plugins/alsa/acp/alsa-util.c:1269
+#: spa/plugins/alsa/acp/alsa-util.c:1231 spa/plugins/alsa/acp/alsa-util.c:1325
 #, c-format
 msgid ""
 "snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu "
@@ -523,23 +547,23 @@ msgstr[0] ""
 "snd_pcm_avail() 返回的值非常大:%lu 字节(%lu 毫秒)。\n"
 "这很可能是由 ALSA 驱动程序 %s 的缺陷导致的。请向 ALSA 开发者报告这个问题。"
 
-#: spa/plugins/alsa/acp/alsa-util.c:1241
+#: spa/plugins/alsa/acp/alsa-util.c:1297
 #, 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] ""
 "snd_pcm_delay() 返回的值非常大:%li 字节(%s%lu 毫秒)。\n"
 "这很可能是由 ALSA 驱动程序 %s 的缺陷导致的。请向 ALSA 开发者报告这个问题。"
 
-#: spa/plugins/alsa/acp/alsa-util.c:1288
+#: spa/plugins/alsa/acp/alsa-util.c:1344
 #, c-format
 msgid ""
 "snd_pcm_avail_delay() returned strange values: delay %lu is less than avail "
@@ -550,7 +574,7 @@ msgstr ""
 "snd_pcm_avail_delay() 返回的值非常很奇怪:延迟 %lu 小于可用 (avail) %lu。\n"
 "这很可能是由 ALSA 驱动程序 %s 的缺陷导致的。请向 ALSA 开发者报告这个问题。"
 
-#: spa/plugins/alsa/acp/alsa-util.c:1331
+#: spa/plugins/alsa/acp/alsa-util.c:1387
 #, c-format
 msgid ""
 "snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu byte "
@@ -566,61 +590,115 @@ msgstr[0] ""
 "snd_pcm_mmap_begin() 返回的值非常大:%lu 字节(%lu ms)。\n"
 "这很可能是由 ALSA 驱动程序 %s 的缺陷导致的。请向 ALSA 开发者报告这个问题。"
 
-#: spa/plugins/bluez5/bluez5-device.c:1010
+#: spa/plugins/alsa/acp/channelmap.h:457
+msgid "(invalid)"
+msgstr "(无效)"
+
+#: spa/plugins/alsa/acp/compat.c:193
+msgid "Built-in Audio"
+msgstr "内置音频"
+
+#: spa/plugins/alsa/acp/compat.c:198
+msgid "Modem"
+msgstr "调制解调器"
+
+#: spa/plugins/bluez5/bluez5-device.c:1712
 msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
 msgstr "音频网关 (A2DP 信源 或 HSP/HFP 网关)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1033
+#: spa/plugins/bluez5/bluez5-device.c:1760
 #, c-format
 msgid "High Fidelity Playback (A2DP Sink, codec %s)"
 msgstr "高保真回放 (A2DP 信宿, 编码 %s)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1035
+#: spa/plugins/bluez5/bluez5-device.c:1763
 #, c-format
 msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
 msgstr "高保真双工 (A2DP 信源/信宿, 编码 %s)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1041
+#: spa/plugins/bluez5/bluez5-device.c:1771
 msgid "High Fidelity Playback (A2DP Sink)"
 msgstr "高保真回放 (A2DP 信宿)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1043
+#: spa/plugins/bluez5/bluez5-device.c:1773
 msgid "High Fidelity Duplex (A2DP Source/Sink)"
 msgstr "高保真双工 (A2DP 信源/信宿)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1070
+#: spa/plugins/bluez5/bluez5-device.c:1823
+#, c-format
+msgid "High Fidelity Playback (BAP Sink, codec %s)"
+msgstr "高保真回放 (BAP 信宿, 编码 %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1828
+#, c-format
+msgid "High Fidelity Input (BAP Source, codec %s)"
+msgstr "高保真输入 (BAP 信源, 编码 %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1832
+#, c-format
+msgid "High Fidelity Duplex (BAP Source/Sink, codec %s)"
+msgstr "高保真双工 (BAP 信源/信宿, 编码 %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1841
+msgid "High Fidelity Playback (BAP Sink)"
+msgstr "高保真回放 (BAP 信宿)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1845
+msgid "High Fidelity Input (BAP Source)"
+msgstr "高保真输入 (BAP 信源)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1848
+msgid "High Fidelity Duplex (BAP Source/Sink)"
+msgstr "高保真双工 (BAP 信源/信宿)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1897
 #, c-format
 msgid "Headset Head Unit (HSP/HFP, codec %s)"
 msgstr "头戴式耳机单元 (HSP/HFP, 编码 %s)"
 
-#: spa/plugins/bluez5/bluez5-device.c:1074
-msgid "Headset Head Unit (HSP/HFP)"
-msgstr "头戴式耳机单元 (HSP/HFP)"
-
-#: spa/plugins/bluez5/bluez5-device.c:1140
+#: spa/plugins/bluez5/bluez5-device.c:1978
+#: spa/plugins/bluez5/bluez5-device.c:1983
+#: spa/plugins/bluez5/bluez5-device.c:1990
+#: spa/plugins/bluez5/bluez5-device.c:1996
+#: spa/plugins/bluez5/bluez5-device.c:2002
+#: spa/plugins/bluez5/bluez5-device.c:2008
+#: spa/plugins/bluez5/bluez5-device.c:2014
+#: spa/plugins/bluez5/bluez5-device.c:2020
+#: spa/plugins/bluez5/bluez5-device.c:2026
 msgid "Handsfree"
 msgstr "免手操作"
 
-#: spa/plugins/bluez5/bluez5-device.c:1155
+#: spa/plugins/bluez5/bluez5-device.c:1984
+msgid "Handsfree (HFP)"
+msgstr "免手操作 (HFP)"
+
+#: spa/plugins/bluez5/bluez5-device.c:2001
 msgid "Headphone"
 msgstr "头戴耳机"
 
-#: spa/plugins/bluez5/bluez5-device.c:1160
+#: spa/plugins/bluez5/bluez5-device.c:2007
 msgid "Portable"
 msgstr "便携式"
 
-#: spa/plugins/bluez5/bluez5-device.c:1165
+#: spa/plugins/bluez5/bluez5-device.c:2013
 msgid "Car"
 msgstr "车内"
 
-#: spa/plugins/bluez5/bluez5-device.c:1170
+#: spa/plugins/bluez5/bluez5-device.c:2019
 msgid "HiFi"
 msgstr "高保真"
 
-#: spa/plugins/bluez5/bluez5-device.c:1175
+#: spa/plugins/bluez5/bluez5-device.c:2025
 msgid "Phone"
 msgstr "电话"
 
-#: spa/plugins/bluez5/bluez5-device.c:1181
+#: spa/plugins/bluez5/bluez5-device.c:2032
 msgid "Bluetooth"
 msgstr "蓝牙"
+
+#: spa/plugins/bluez5/bluez5-device.c:2033
+msgid "Bluetooth (HFP)"
+msgstr "蓝牙 (HFP)"
+
+#~ msgid "Headset Head Unit (HSP/HFP)"
+#~ msgstr "头戴式耳机单元 (HSP/HFP)"
diff --git a/spa/examples/local-libcamera.c b/spa/examples/local-libcamera.c
index 2a4d8283..d6e9f80d 100644
--- a/spa/examples/local-libcamera.c
+++ b/spa/examples/local-libcamera.c
@@ -43,7 +43,7 @@
 static SPA_LOG_IMPL(default_log);
 
 #define MAX_BUFFERS     8
-
+#define LOOP_TIMEOUT_MS 100
 #define USE_BUFFER 		false
 
 struct buffer {
@@ -401,57 +401,33 @@ static int negotiate_formats(struct data *data)
 	return 0;
 }
 
-static void *loop(void *user_data)
-{
-	struct data *data = user_data;
-
-	printf("enter thread\n");
-    spa_loop_control_enter(data->control);
-
-    while (data->running) {
-		spa_loop_control_iterate(data->control, -1);
-	}
-
-	printf("leave thread\n");
-    spa_loop_control_leave(data->control);
-	return NULL;
-}
-
-static void run_async_source(struct data *data)
+static void loop(struct data *data)
 {
-	int res, err;
+	int res;
 	struct spa_command cmd;
 	SDL_Event event;
-	bool running = true;
 
 	printf("starting...\n\n");
 	cmd = SPA_NODE_COMMAND_INIT(SPA_NODE_COMMAND_Start);
 	if ((res = spa_node_send_command(data->source, &cmd)) < 0)
 		printf("got error %d\n", res);
 
-	spa_loop_control_leave(data->control);
-
 	data->running = true;
-	if ((err = pthread_create(&data->thread, NULL, loop, data)) != 0) {
-		printf("can't create thread: %d %s", err, strerror(err));
-		data->running = false;
-	}
 
-	while (running && SDL_WaitEvent(&event)) {
-		switch (event.type) {
-		case SDL_QUIT:
-			running = false;
-			break;
+	while (data->running) {
+		// must be called from the thread that created the renderer
+		while (SDL_PollEvent(&event)) {
+			switch (event.type) {
+			case SDL_QUIT:
+				data->running = false;
+				break;
+			}
 		}
-	}
 
-	if (data->running) {
-		data->running = false;
-		pthread_join(data->thread, NULL);
+		// small timeout to make sure we don't starve the SDL loop
+		spa_loop_control_iterate(data->control, LOOP_TIMEOUT_MS);
 	}
 
-	spa_loop_control_enter(data->control);
-
 	printf("pausing...\n\n");
 	cmd = SPA_NODE_COMMAND_INIT(SPA_NODE_COMMAND_Pause);
 	if ((res = spa_node_send_command(data->source, &cmd)) < 0)
@@ -531,7 +507,7 @@ int main(int argc, char *argv[])
 	}
 
 	spa_loop_control_enter(data.control);
-	run_async_source(&data);
+	loop(&data);
 	spa_loop_control_leave(data.control);
 
 	SDL_DestroyRenderer(data.renderer);
diff --git a/spa/examples/local-v4l2.c b/spa/examples/local-v4l2.c
index 4e7000da..46f224ee 100644
--- a/spa/examples/local-v4l2.c
+++ b/spa/examples/local-v4l2.c
@@ -39,6 +39,7 @@
 static SPA_LOG_IMPL(default_log);
 
 #define MAX_BUFFERS     8
+#define LOOP_TIMEOUT_MS 100
 
 struct buffer {
 	struct spa_buffer buffer;
@@ -396,56 +397,34 @@ static int negotiate_formats(struct data *data)
 	return 0;
 }
 
-static void *loop(void *user_data)
+static void loop(struct data *data)
 {
-	struct data *data = user_data;
-
-	printf("enter thread\n");
-        spa_loop_control_enter(data->control);
-
-	while (data->running) {
-		spa_loop_control_iterate(data->control, -1);
-	}
-
-	printf("leave thread\n");
-        spa_loop_control_leave(data->control);
-	return NULL;
-}
-
-static void run_async_source(struct data *data)
-{
-	int res, err;
+	int res;
 	struct spa_command cmd;
 	SDL_Event event;
-	bool running = true;
 
+	printf("starting...\n\n");
 	cmd = SPA_NODE_COMMAND_INIT(SPA_NODE_COMMAND_Start);
 	if ((res = spa_node_send_command(data->source, &cmd)) < 0)
 		printf("got error %d\n", res);
 
-	spa_loop_control_leave(data->control);
-
 	data->running = true;
-	if ((err = pthread_create(&data->thread, NULL, loop, data)) != 0) {
-		printf("can't create thread: %d %s", err, strerror(err));
-		data->running = false;
-	}
 
-	while (running && SDL_WaitEvent(&event)) {
-		switch (event.type) {
-		case SDL_QUIT:
-			running = false;
-			break;
+	while (data->running) {
+		// must be called from the thread that created the renderer
+		while (SDL_PollEvent(&event)) {
+			switch (event.type) {
+			case SDL_QUIT:
+				data->running = false;
+				break;
+			}
 		}
-	}
 
-	if (data->running) {
-		data->running = false;
-		pthread_join(data->thread, NULL);
+		// small timeout to make sure we don't starve the SDL loop
+		spa_loop_control_iterate(data->control, LOOP_TIMEOUT_MS);
 	}
 
-	spa_loop_control_enter(data->control);
-
+	printf("pausing...\n\n");
 	cmd = SPA_NODE_COMMAND_INIT(SPA_NODE_COMMAND_Pause);
 	if ((res = spa_node_send_command(data->source, &cmd)) < 0)
 		printf("got error %d\n", res);
@@ -524,7 +503,7 @@ int main(int argc, char *argv[])
 	}
 
 	spa_loop_control_enter(data.control);
-	run_async_source(&data);
+	loop(&data);
 	spa_loop_control_leave(data.control);
 
 	SDL_DestroyRenderer(data.renderer);
diff --git a/spa/examples/local-videotestsrc.c b/spa/examples/local-videotestsrc.c
new file mode 100644
index 00000000..3743e24d
--- /dev/null
+++ b/spa/examples/local-videotestsrc.c
@@ -0,0 +1,535 @@
+/* Spa */
+/* SPDX-FileCopyrightText: Copyright © 2020 Collabora Ltd. */
+/*                         @author Raghavendra Rao Sidlagatta <raghavendra.rao@collabora.com> */
+/* SPDX-License-Identifier: MIT */
+
+/*
+ [title]
+ Example using libspa-videotestsrc, with only \ref api_spa
+ [title]
+ */
+
+#include "config.h"
+
+#include <string.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <dlfcn.h>
+#include <poll.h>
+#include <pthread.h>
+#include <errno.h>
+#include <sys/mman.h>
+
+#include <SDL2/SDL.h>
+
+#include <spa/support/plugin.h>
+#include <spa/utils/names.h>
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/support/log-impl.h>
+#include <spa/support/loop.h>
+#include <spa/node/node.h>
+#include <spa/node/io.h>
+#include <spa/node/utils.h>
+#include <spa/param/param.h>
+#include <spa/param/props.h>
+#include <spa/param/video/format-utils.h>
+#include <spa/debug/pod.h>
+
+#define WIDTH   640
+#define HEIGHT  480
+
+static SPA_LOG_IMPL(default_log);
+
+#define MAX_BUFFERS     8
+#define LOOP_TIMEOUT_MS 100
+#define USE_BUFFER 		true
+
+struct buffer {
+	struct spa_buffer buffer;
+	struct spa_meta metas[1];
+	struct spa_meta_header header;
+	struct spa_data datas[1];
+	struct spa_chunk chunks[1];
+	SDL_Texture *texture;
+};
+
+struct data {
+	const char *plugin_dir;
+	struct spa_log *log;
+	struct spa_system *system;
+	struct spa_loop *loop;
+	struct spa_loop_control *control;
+	struct spa_loop_utils *loop_utils;
+
+	struct spa_support support[6];
+	uint32_t n_support;
+
+	struct spa_node *source;
+	struct spa_hook listener;
+	struct spa_io_buffers source_output[1];
+
+	SDL_Renderer *renderer;
+	SDL_Window *window;
+	SDL_Texture *texture;
+
+	bool use_buffer;
+
+	bool running;
+	pthread_t thread;
+
+	struct spa_buffer *bp[MAX_BUFFERS];
+	struct buffer buffers[MAX_BUFFERS];
+	unsigned int n_buffers;
+};
+
+static int load_handle(struct data *data, struct spa_handle **handle, const char *lib, const char *name)
+{
+	int res;
+	void *hnd;
+	spa_handle_factory_enum_func_t enum_func;
+	uint32_t i;
+
+	char *path = NULL;
+
+	if ((path = spa_aprintf("%s/%s", data->plugin_dir, lib)) == NULL) {
+		return -ENOMEM;
+	}
+	if ((hnd = dlopen(path, RTLD_NOW)) == NULL) {
+		printf("can't load %s: %s\n", path, dlerror());
+		free(path);
+		return -errno;
+	}
+	free(path);
+	if ((enum_func = dlsym(hnd, SPA_HANDLE_FACTORY_ENUM_FUNC_NAME)) == NULL) {
+		printf("can't find enum function\n");
+		return -errno;
+	}
+
+	for (i = 0;;) {
+		const struct spa_handle_factory *factory;
+
+		if ((res = enum_func(&factory, &i)) <= 0) {
+			if (res != 0)
+				printf("can't enumerate factories: %s\n", spa_strerror(res));
+			break;
+		}
+		if (!spa_streq(factory->name, name))
+			continue;
+
+		*handle = calloc(1, spa_handle_factory_get_size(factory, NULL));
+		if ((res = spa_handle_factory_init(factory, *handle,
+						NULL, data->support,
+						data->n_support)) < 0) {
+			printf("can't make factory instance: %d\n", res);
+			return res;
+		}
+		return 0;
+	}
+	return -EBADF;
+}
+
+static int make_node(struct data *data, struct spa_node **node, const char *lib, const char *name)
+{
+	struct spa_handle *handle = NULL;
+	void *iface;
+	int res;
+
+	if ((res = load_handle(data, &handle, lib, name)) < 0)
+		return res;
+
+	if ((res = spa_handle_get_interface(handle, SPA_TYPE_INTERFACE_Node, &iface)) < 0) {
+		printf("can't get interface %d\n", res);
+		return res;
+	}
+	*node = iface;
+	return 0;
+}
+
+static int on_source_ready(void *_data, int status)
+{
+	struct data *data = _data;
+	int res;
+	struct buffer *b;
+	void *sdata, *ddata;
+	int sstride, dstride;
+	int i;
+	uint8_t *src, *dst;
+	struct spa_data *datas;
+	struct spa_io_buffers *io = &data->source_output[0];
+
+	if (io->status != SPA_STATUS_HAVE_DATA ||
+		io->buffer_id >= MAX_BUFFERS)
+		return -EINVAL;
+
+	b = &data->buffers[io->buffer_id];
+	io->status = SPA_STATUS_NEED_DATA;
+
+	datas = b->buffer.datas;
+
+	if (b->texture) {
+		SDL_Texture *texture = b->texture;
+
+		SDL_UnlockTexture(texture);
+		if (SDL_RenderClear(data->renderer) < 0) {
+			fprintf(stderr, "Couldn't render clear: %s\n", SDL_GetError());
+			return -EIO;
+		}
+		if (SDL_RenderCopy(data->renderer, texture, NULL, NULL) < 0) {
+			fprintf(stderr, "Couldn't render copy: %s\n", SDL_GetError());
+			return -EIO;
+		}
+		SDL_RenderPresent(data->renderer);
+
+		if (SDL_LockTexture(texture, NULL, &sdata, &sstride) < 0) {
+			fprintf(stderr, "Couldn't lock texture: %s\n", SDL_GetError());
+			return -EIO;
+		}
+
+		datas[0].data = sdata;
+	} else {
+		uint8_t *map;
+
+		if (SDL_LockTexture(data->texture, NULL, &ddata, &dstride) < 0) {
+			fprintf(stderr, "Couldn't lock texture: %s\n", SDL_GetError());
+			return -EIO;
+		}
+		sdata = datas[0].data;
+		if (datas[0].type == SPA_DATA_MemFd ||
+		    datas[0].type == SPA_DATA_DmaBuf) {
+			map = mmap(NULL, datas[0].maxsize, PROT_READ,
+				   MAP_PRIVATE, datas[0].fd, datas[0].mapoffset);
+			if (map == MAP_FAILED)
+				return -errno;
+			sdata = map;
+		} else if (datas[0].type == SPA_DATA_MemPtr) {
+			map = NULL;
+			sdata = datas[0].data;
+		} else
+			return -EIO;
+
+		sstride = datas[0].chunk->stride;
+
+		for (i = 0; i < HEIGHT; i++) {
+			src = ((uint8_t *) sdata + i * sstride);
+			dst = ((uint8_t *) ddata + i * dstride);
+			memcpy(dst, src, SPA_MIN(sstride, dstride));
+		}
+		SDL_UnlockTexture(data->texture);
+
+		SDL_RenderClear(data->renderer);
+		SDL_RenderCopy(data->renderer, data->texture, NULL, NULL);
+		SDL_RenderPresent(data->renderer);
+
+		if (map)
+			munmap(map, datas[0].maxsize);
+	}
+
+	if ((res = spa_node_process(data->source)) < 0)
+		printf("got process error %d\n", res);
+
+	return 0;
+}
+
+static const struct spa_node_callbacks source_callbacks = {
+	SPA_VERSION_NODE_CALLBACKS,
+	.ready = on_source_ready,
+};
+
+static int make_nodes(struct data *data, uint32_t pattern)
+{
+	int res;
+	struct spa_pod *props;
+	struct spa_pod_builder b = { 0 };
+	uint8_t buffer[256];
+	uint32_t index;
+
+	if ((res =
+	     make_node(data, &data->source,
+		     "videotestsrc/libspa-videotestsrc.so",
+		     "videotestsrc")) < 0) {
+		printf("can't create videotestsrc: %d\n", res);
+		return res;
+	}
+
+	spa_node_set_callbacks(data->source, &source_callbacks, data);
+
+	index = 0;
+	spa_pod_builder_init(&b, buffer, sizeof(buffer));
+	if ((res = spa_node_enum_params_sync(data->source, SPA_PARAM_Props,
+			&index, NULL, &props, &b)) == 1) {
+		spa_debug_pod(0, NULL, props);
+	}
+
+	spa_pod_builder_init(&b, buffer, sizeof(buffer));
+	props = spa_pod_builder_add_object(&b,
+		SPA_TYPE_OBJECT_Props, 0,
+		SPA_PROP_patternType, SPA_POD_Int(pattern));
+
+	if ((res = spa_node_set_param(data->source, SPA_PARAM_Props, 0, props)) < 0)
+		printf("got set_props error %d\n", res);
+
+	return res;
+}
+
+static int setup_buffers(struct data *data)
+{
+	int i;
+
+	for (i = 0; i < MAX_BUFFERS; i++) {
+		struct buffer *b = &data->buffers[i];
+
+		data->bp[i] = &b->buffer;
+
+		b->texture = NULL;
+
+		b->buffer.metas = b->metas;
+		b->buffer.n_metas = 1;
+		b->buffer.datas = b->datas;
+		b->buffer.n_datas = 1;
+
+		b->header.flags = 0;
+		b->header.seq = 0;
+		b->header.pts = 0;
+		b->header.dts_offset = 0;
+		b->metas[0].type = SPA_META_Header;
+		b->metas[0].data = &b->header;
+		b->metas[0].size = sizeof(b->header);
+
+		b->datas[0].type = SPA_DATA_DmaBuf;
+		b->datas[0].flags = 0;
+		b->datas[0].fd = -1;
+		b->datas[0].mapoffset = 0;
+		b->datas[0].maxsize = 0;
+		b->datas[0].data = NULL;
+		b->datas[0].chunk = &b->chunks[0];
+		b->datas[0].chunk->offset = 0;
+		b->datas[0].chunk->size = 0;
+		b->datas[0].chunk->stride = 0;
+	}
+	data->n_buffers = MAX_BUFFERS;
+	return 0;
+}
+
+static int sdl_alloc_buffers(struct data *data)
+{
+	int i;
+
+	for (i = 0; i < MAX_BUFFERS; i++) {
+		struct buffer *b = &data->buffers[i];
+		SDL_Texture *texture;
+		void *ptr;
+		int stride;
+
+		texture = SDL_CreateTexture(data->renderer,
+					    SDL_PIXELFORMAT_RGB24,
+					    SDL_TEXTUREACCESS_STREAMING, WIDTH, HEIGHT);
+		if (!texture) {
+			printf("can't create texture: %s\n", SDL_GetError());
+			return -ENOMEM;
+		}
+		if (SDL_LockTexture(texture, NULL, &ptr, &stride) < 0) {
+			fprintf(stderr, "Couldn't lock texture: %s\n", SDL_GetError());
+			return -EIO;
+		}
+		b->texture = texture;
+
+		b->datas[0].type = SPA_DATA_DmaBuf;
+		b->datas[0].maxsize = stride * HEIGHT;
+		b->datas[0].data = ptr;
+		b->datas[0].chunk->offset = 0;
+		b->datas[0].chunk->size = stride * HEIGHT;
+		b->datas[0].chunk->stride = stride;
+	}
+	return 0;
+}
+
+static int negotiate_formats(struct data *data)
+{
+	int res;
+	struct spa_pod *format;
+	uint8_t buffer[256];
+	struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
+
+	data->source_output[0] = SPA_IO_BUFFERS_INIT;
+
+	if ((res =
+	     spa_node_port_set_io(data->source,
+				  SPA_DIRECTION_OUTPUT, 0,
+				  SPA_IO_Buffers,
+				  &data->source_output[0], sizeof(data->source_output[0]))) < 0)
+		return res;
+
+	format = spa_format_video_raw_build(&b, 0,
+			&SPA_VIDEO_INFO_RAW_INIT(
+				.format =  SPA_VIDEO_FORMAT_RGB,
+				.size = SPA_RECTANGLE(WIDTH, HEIGHT),
+				.framerate = SPA_FRACTION(25,1)));
+
+	if ((res = spa_node_port_set_param(data->source,
+					   SPA_DIRECTION_OUTPUT, 0,
+					   SPA_PARAM_Format, 0,
+					   format)) < 0)
+		return res;
+
+
+	setup_buffers(data);
+
+	if (data->use_buffer) {
+		if ((res = sdl_alloc_buffers(data)) < 0)
+			return res;
+
+		if ((res = spa_node_port_use_buffers(data->source,
+						SPA_DIRECTION_OUTPUT, 0, 0,
+						data->bp, data->n_buffers)) < 0) {
+			printf("can't allocate buffers: %s\n", spa_strerror(res));
+			return -1;
+		}
+	} else {
+		unsigned int n_buffers;
+
+		data->texture = SDL_CreateTexture(data->renderer,
+						  SDL_PIXELFORMAT_RGB24,
+						  SDL_TEXTUREACCESS_STREAMING, WIDTH, HEIGHT);
+		if (!data->texture) {
+			printf("can't create texture: %s\n", SDL_GetError());
+			return -1;
+		}
+		n_buffers = MAX_BUFFERS;
+		if ((res = spa_node_port_use_buffers(data->source,
+						SPA_DIRECTION_OUTPUT, 0,
+						SPA_NODE_BUFFERS_FLAG_ALLOC,
+						data->bp, n_buffers)) < 0) {
+			printf("can't allocate buffers: %s\n", spa_strerror(res));
+			return -1;
+		}
+		data->n_buffers = n_buffers;
+	}
+	return 0;
+}
+
+static void loop(struct data *data)
+{
+	int res;
+	struct spa_command cmd;
+	SDL_Event event;
+
+	printf("starting...\n\n");
+	cmd = SPA_NODE_COMMAND_INIT(SPA_NODE_COMMAND_Start);
+	if ((res = spa_node_send_command(data->source, &cmd)) < 0)
+		printf("got error %d\n", res);
+
+	data->running = true;
+
+	while (data->running) {
+		// must be called from the thread that created the renderer
+		while (SDL_PollEvent(&event)) {
+			switch (event.type) {
+			case SDL_QUIT:
+				data->running = false;
+				break;
+			}
+		}
+
+		// small timeout to make sure we don't starve the SDL loop
+		spa_loop_control_iterate(data->control, LOOP_TIMEOUT_MS);
+	}
+
+	printf("pausing...\n\n");
+	cmd = SPA_NODE_COMMAND_INIT(SPA_NODE_COMMAND_Pause);
+	if ((res = spa_node_send_command(data->source, &cmd)) < 0)
+		printf("got error %d\n", res);
+}
+
+int main(int argc, char *argv[])
+{
+	struct data data = { 0 };
+	int res;
+	const char *str;
+	struct spa_handle *handle = NULL;
+	void *iface;
+	uint32_t pattern = 0;
+
+	if ((str = getenv("SPA_PLUGIN_DIR")) == NULL)
+		str = PLUGINDIR;
+	data.plugin_dir = str;
+
+	if ((res = load_handle(&data, &handle,
+					"support/libspa-support.so",
+					SPA_NAME_SUPPORT_SYSTEM)) < 0)
+		return res;
+
+	if ((res = spa_handle_get_interface(handle, SPA_TYPE_INTERFACE_System, &iface)) < 0) {
+		printf("can't get System interface %d\n", res);
+		return res;
+	}
+	data.system = iface;
+	data.support[data.n_support++] = SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_System, data.system);
+	data.support[data.n_support++] = SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_DataSystem, data.system);
+
+	if ((res = load_handle(&data, &handle,
+					"support/libspa-support.so",
+					SPA_NAME_SUPPORT_LOOP)) < 0)
+		return res;
+
+	if ((res = spa_handle_get_interface(handle, SPA_TYPE_INTERFACE_Loop, &iface)) < 0) {
+		printf("can't get interface %d\n", res);
+		return res;
+	}
+	data.loop = iface;
+	if ((res = spa_handle_get_interface(handle, SPA_TYPE_INTERFACE_LoopControl, &iface)) < 0) {
+		printf("can't get interface %d\n", res);
+		return res;
+	}
+	data.control = iface;
+	if ((res = spa_handle_get_interface(handle, SPA_TYPE_INTERFACE_LoopUtils, &iface)) < 0) {
+		printf("can't get interface %d\n", res);
+		return res;
+	}
+	data.loop_utils = iface;
+
+	data.use_buffer = USE_BUFFER;
+
+	data.log = &default_log.log;
+
+	if ((str = getenv("SPA_DEBUG")))
+		data.log->level = atoi(str);
+
+	data.support[data.n_support++] = SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_Log, data.log);
+	data.support[data.n_support++] = SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_Loop, data.loop);
+	data.support[data.n_support++] = SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_DataLoop, data.loop);
+	data.support[data.n_support++] = SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_LoopUtils, data.loop_utils);
+
+	if (SDL_Init(SDL_INIT_VIDEO) < 0) {
+		printf("can't initialize SDL: %s\n", SDL_GetError());
+		return -1;
+	}
+
+	if (SDL_CreateWindowAndRenderer
+	    (WIDTH, HEIGHT, SDL_WINDOW_RESIZABLE, &data.window, &data.renderer)) {
+		printf("can't create window: %s\n", SDL_GetError());
+		return -1;
+	}
+
+	if (argv[1] != NULL)
+		pattern = atoi(argv[1]);
+
+	if ((res = make_nodes(&data, pattern)) < 0) {
+		printf("can't make nodes: %d\n", res);
+		return -1;
+	}
+
+	if ((res = negotiate_formats(&data)) < 0) {
+		printf("can't negotiate nodes: %d\n", res);
+		return -1;
+	}
+
+	spa_loop_control_enter(data.control);
+	loop(&data);
+	spa_loop_control_leave(data.control);
+
+	SDL_DestroyRenderer(data.renderer);
+
+	return 0;
+}
diff --git a/spa/examples/meson.build b/spa/examples/meson.build
index fdd54fdc..8d0fc492 100644
--- a/spa/examples/meson.build
+++ b/spa/examples/meson.build
@@ -4,6 +4,7 @@ spa_examples = [
   'example-control',
   'local-libcamera',
   'local-v4l2',
+  'local-videotestsrc',
 ]
 
 if not get_option('examples').allowed() or not get_option('spa-plugins').allowed()
@@ -12,6 +13,7 @@ endif
 
 spa_examples_extra_deps = {
   'local-v4l2': [sdl_dep],
+  'local-videotestsrc': [sdl_dep],
   'local-libcamera': [sdl_dep, libcamera_dep],
 }
 
diff --git a/spa/include/spa/buffer/alloc.h b/spa/include/spa/buffer/alloc.h
index 8b9e55e5..dc8f4cc6 100644
--- a/spa/include/spa/buffer/alloc.h
+++ b/spa/include/spa/buffer/alloc.h
@@ -11,6 +11,14 @@ extern "C" {
 
 #include <spa/buffer/buffer.h>
 
+#ifndef SPA_API_BUFFER_ALLOC
+ #ifdef SPA_API_IMPL
+  #define SPA_API_BUFFER_ALLOC SPA_API_IMPL
+ #else
+  #define SPA_API_BUFFER_ALLOC static inline
+ #endif
+#endif
+
 /**
  * \addtogroup spa_buffer
  * \{
@@ -58,7 +66,7 @@ struct spa_buffer_alloc_info {
  * \param data_aligns \a n_datas alignments
  * \return 0 on success.
  * */
-static inline int spa_buffer_alloc_fill_info(struct spa_buffer_alloc_info *info,
+SPA_API_BUFFER_ALLOC int spa_buffer_alloc_fill_info(struct spa_buffer_alloc_info *info,
 					     uint32_t n_metas, struct spa_meta metas[],
 					     uint32_t n_datas, struct spa_data datas[],
 					     uint32_t data_aligns[])
@@ -179,7 +187,7 @@ static inline int spa_buffer_alloc_fill_info(struct spa_buffer_alloc_info *info,
  * \param data_mem memory to hold the meta, chunk and memory
  * \return a struct \ref spa_buffer in \a skel_mem
  */
-static inline struct spa_buffer *
+SPA_API_BUFFER_ALLOC struct spa_buffer *
 spa_buffer_alloc_layout(struct spa_buffer_alloc_info *info,
 			void *skel_mem, void *data_mem)
 {
@@ -257,7 +265,7 @@ spa_buffer_alloc_layout(struct spa_buffer_alloc_info *info,
  * \return 0 on success.
  *
  */
-static inline int
+SPA_API_BUFFER_ALLOC int
 spa_buffer_alloc_layout_array(struct spa_buffer_alloc_info *info,
 			      uint32_t n_buffers, struct spa_buffer *buffers[],
 			      void *skel_mem, void *data_mem)
@@ -292,7 +300,7 @@ spa_buffer_alloc_layout_array(struct spa_buffer_alloc_info *info,
  *     allocation failed.
  *
  */
-static inline struct spa_buffer **
+SPA_API_BUFFER_ALLOC struct spa_buffer **
 spa_buffer_alloc_array(uint32_t n_buffers, uint32_t flags,
 		       uint32_t n_metas, struct spa_meta metas[],
 		       uint32_t n_datas, struct spa_data datas[],
diff --git a/spa/include/spa/buffer/buffer.h b/spa/include/spa/buffer/buffer.h
index 7b3b7c00..03fd13b2 100644
--- a/spa/include/spa/buffer/buffer.h
+++ b/spa/include/spa/buffer/buffer.h
@@ -12,6 +12,14 @@ extern "C" {
 #include <spa/utils/defs.h>
 #include <spa/buffer/meta.h>
 
+#ifndef SPA_API_BUFFER
+ #ifdef SPA_API_IMPL
+  #define SPA_API_BUFFER SPA_API_IMPL
+ #else
+  #define SPA_API_BUFFER static inline
+ #endif
+#endif
+
 /** \defgroup spa_buffer Buffers
  *
  * Buffers describe the data and metadata that is exchanged between
@@ -91,7 +99,7 @@ struct spa_buffer {
 };
 
 /** Find metadata in a buffer */
-static inline struct spa_meta *spa_buffer_find_meta(const struct spa_buffer *b, uint32_t type)
+SPA_API_BUFFER struct spa_meta *spa_buffer_find_meta(const struct spa_buffer *b, uint32_t type)
 {
 	uint32_t i;
 
@@ -102,7 +110,7 @@ static inline struct spa_meta *spa_buffer_find_meta(const struct spa_buffer *b,
 	return NULL;
 }
 
-static inline void *spa_buffer_find_meta_data(const struct spa_buffer *b, uint32_t type, size_t size)
+SPA_API_BUFFER void *spa_buffer_find_meta_data(const struct spa_buffer *b, uint32_t type, size_t size)
 {
 	struct spa_meta *m;
 	if ((m = spa_buffer_find_meta(b, type)) && m->size >= size)
diff --git a/spa/include/spa/buffer/meta.h b/spa/include/spa/buffer/meta.h
index 844a8a25..b484cfb0 100644
--- a/spa/include/spa/buffer/meta.h
+++ b/spa/include/spa/buffer/meta.h
@@ -12,6 +12,14 @@ extern "C" {
 #include <spa/utils/defs.h>
 #include <spa/pod/pod.h>
 
+#ifndef SPA_API_META
+ #ifdef SPA_API_IMPL
+  #define SPA_API_META SPA_API_IMPL
+ #else
+  #define SPA_API_META static inline
+ #endif
+#endif
+
 /**
  * \addtogroup spa_buffer
  * \{
@@ -46,14 +54,13 @@ struct spa_meta {
 	void *data;		/**< pointer to metadata */
 };
 
-static inline void *spa_meta_first(const struct spa_meta *m) {
+SPA_API_META 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) {
+
+SPA_API_META 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))
 
 /**
@@ -80,19 +87,16 @@ struct spa_meta_region {
 	struct spa_region region;
 };
 
-static inline bool spa_meta_region_is_valid(const struct spa_meta_region *m) {
+SPA_API_META 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)++)
 
-#define spa_meta_bitmap_is_valid(m)	((m)->format != 0)
-
 /**
  * Bitmap information
  *
@@ -112,7 +116,9 @@ struct spa_meta_bitmap {
 					  *  info. */
 };
 
-#define spa_meta_cursor_is_valid(m)	((m)->id != 0)
+SPA_API_META bool spa_meta_bitmap_is_valid(const struct spa_meta_bitmap *m) {
+	return m->format != 0;
+}
 
 /**
  * Cursor information
@@ -132,6 +138,10 @@ struct spa_meta_cursor {
 					  *  struct spa_meta_bitmap at the offset. */
 };
 
+SPA_API_META bool spa_meta_cursor_is_valid(const struct spa_meta_cursor *m) {
+	return m->id != 0;
+}
+
 /** a timed set of events associated with the buffer */
 struct spa_meta_control {
 	struct spa_pod_sequence sequence;
@@ -168,9 +178,10 @@ struct spa_meta_videotransform {
  * Metadata to describe the time on the timeline when the buffer
  * can be acquired and when it can be reused.
  *
- * This metadata will usually also require negotiation of 2 extra
- * buffer datas of type SPA_DATA_SyncObj with 2 fds for the acquire
- * and release timelines respectively.
+ * This metadata will require negotiation of 2 extra fds for the acquire
+ * and release timelines respectively.  One way to achieve this is to place
+ * this metadata as SPA_PARAM_BUFFERS_metaType when negotiating a buffer
+ * layout with 2 extra fds.
  */
 struct spa_meta_sync_timeline {
 	uint32_t flags;
diff --git a/spa/include/spa/control/control.h b/spa/include/spa/control/control.h
index 1d955d3e..1c1ec81f 100644
--- a/spa/include/spa/control/control.h
+++ b/spa/include/spa/control/control.h
@@ -24,9 +24,12 @@ extern "C" {
 /** Different Control types */
 enum spa_control_type {
 	SPA_CONTROL_Invalid,
-	SPA_CONTROL_Properties,		/**< data contains a SPA_TYPE_OBJECT_Props */
-	SPA_CONTROL_Midi,		/**< data contains a spa_pod_bytes with raw midi data */
-	SPA_CONTROL_OSC,		/**< data contains a spa_pod_bytes with an OSC packet */
+	SPA_CONTROL_Properties,		/**< SPA_TYPE_OBJECT_Props */
+	SPA_CONTROL_Midi,		/**< spa_pod_bytes with raw midi data (deprecated, use SPA_CONTROL_UMP) */
+	SPA_CONTROL_OSC,		/**< spa_pod_bytes with an OSC packet */
+	SPA_CONTROL_UMP,		/**< spa_pod_bytes with raw UMP (universal MIDI packet)
+					  *  data. The UMP 32 bit words are stored in native endian
+					  *  format. */
 
 	_SPA_CONTROL_LAST,		/**< not part of ABI */
 };
diff --git a/spa/include/spa/control/type-info.h b/spa/include/spa/control/type-info.h
index 2ff3ce18..6c7bd6a9 100644
--- a/spa/include/spa/control/type-info.h
+++ b/spa/include/spa/control/type-info.h
@@ -15,7 +15,7 @@ extern "C" {
  */
 
 #include <spa/utils/defs.h>
-#include <spa/utils/type-info.h>
+#include <spa/utils/type.h>
 #include <spa/control/control.h>
 
 /* base for parameter object enumerations */
@@ -27,6 +27,7 @@ static const struct spa_type_info spa_type_control[] = {
 	{ SPA_CONTROL_Properties, SPA_TYPE_Int, SPA_TYPE_INFO_CONTROL_BASE "Properties", NULL },
 	{ SPA_CONTROL_Midi, SPA_TYPE_Int, SPA_TYPE_INFO_CONTROL_BASE "Midi", NULL },
 	{ SPA_CONTROL_OSC, SPA_TYPE_Int, SPA_TYPE_INFO_CONTROL_BASE "OSC", NULL },
+	{ SPA_CONTROL_UMP, SPA_TYPE_Int, SPA_TYPE_INFO_CONTROL_BASE "UMP", NULL },
 	{ 0, 0, NULL, NULL },
 };
 
diff --git a/spa/include/spa/control/ump-utils.h b/spa/include/spa/control/ump-utils.h
new file mode 100644
index 00000000..9f57efb9
--- /dev/null
+++ b/spa/include/spa/control/ump-utils.h
@@ -0,0 +1,230 @@
+/* Simple Plugin API */
+/* SPDX-FileCopyrightText: Copyright © 2024 Wim Taymans */
+/* SPDX-License-Identifier: MIT */
+
+
+#ifndef SPA_CONTROL_UMP_UTILS_H
+#define SPA_CONTROL_UMP_UTILS_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <errno.h>
+#include <spa/utils/defs.h>
+
+#ifndef SPA_API_CONTROL_UMP_UTILS
+ #ifdef SPA_API_IMPL
+  #define SPA_API_CONTROL_UMP_UTILS SPA_API_IMPL
+ #else
+  #define SPA_API_CONTROL_UMP_UTILS static inline
+ #endif
+#endif
+/**
+ * \addtogroup spa_control
+ * \{
+ */
+
+SPA_API_CONTROL_UMP_UTILS size_t spa_ump_message_size(uint8_t message_type)
+{
+	static const uint32_t ump_sizes[] = {
+		[0x0] = 1, /* Utility messages */
+		[0x1] = 1, /* System messages */
+		[0x2] = 1, /* MIDI 1.0 messages */
+		[0x3] = 2, /* 7bit SysEx messages */
+		[0x4] = 2, /* MIDI 2.0 messages */
+		[0x5] = 4, /* 8bit data message */
+		[0x6] = 1,
+		[0x7] = 1,
+		[0x8] = 2,
+		[0x9] = 2,
+		[0xa] = 2,
+		[0xb] = 3,
+		[0xc] = 3,
+		[0xd] = 4, /* Flexible data messages */
+		[0xe] = 4,
+		[0xf] = 4, /* Stream messages */
+	};
+	return ump_sizes[message_type & 0xf];
+}
+
+SPA_API_CONTROL_UMP_UTILS int spa_ump_to_midi(uint32_t *ump, size_t ump_size,
+		uint8_t *midi, size_t midi_maxsize)
+{
+	int size = 0;
+
+	if (ump_size < 4)
+		return 0;
+	if (midi_maxsize < 8)
+		return -ENOSPC;
+
+	switch (ump[0] >> 28) {
+	case 0x1: /* System Real Time and System Common Messages (except System Exclusive) */
+		midi[size++] = (ump[0] >> 16) & 0xff;
+		if (midi[0] >= 0xf1 && midi[0] <= 0xf3) {
+			midi[size++] = (ump[0] >> 8) & 0x7f;
+			if (midi[0] == 0xf2)
+				midi[size++] = ump[0] & 0x7f;
+		}
+		break;
+	case 0x2: /* MIDI 1.0 Channel Voice Messages */
+		midi[size++] = (ump[0] >> 16);
+		midi[size++] = (ump[0] >> 8);
+		if (midi[0] < 0xc0 || midi[0] > 0xdf)
+			midi[size++] = (ump[0]);
+		break;
+	case 0x3: /* Data Messages (including System Exclusive) */
+	{
+		uint8_t status, i, bytes;
+
+		if (ump_size < 8)
+			return 0;
+
+		status = (ump[0] >> 20) & 0xf;
+		bytes = SPA_CLAMP((ump[0] >> 16) & 0xf, 0u, 6u);
+
+		if (status == 0 || status == 1)
+			midi[size++] = 0xf0;
+		for (i = 0 ; i < bytes; i++)
+			/* ump[0] >> 8 | ump[0] | ump[1] >> 24 | ump[1] >>16 ... */
+			midi[size++] = ump[(i+2)/4] >> ((5-i)%4 * 8);
+		if (status == 0 || status == 3)
+			midi[size++] = 0xf7;
+		break;
+	}
+	case 0x4: /* MIDI 2.0 Channel Voice Messages */
+		if (ump_size < 8)
+			return 0;
+		midi[size++] = (ump[0] >> 16) | 0x80;
+		if (midi[0] < 0xc0 || midi[0] > 0xdf)
+			midi[size++] = (ump[0] >> 8) & 0x7f;
+		midi[size++] = (ump[1] >> 25);
+		break;
+
+	case 0x0: /* Utility Messages */
+	case 0x5: /* Data Messages */
+	default:
+		return 0;
+	}
+	return size;
+}
+
+SPA_API_CONTROL_UMP_UTILS int spa_ump_from_midi(uint8_t **midi, size_t *midi_size,
+		uint32_t *ump, size_t ump_maxsize, uint8_t group, uint64_t *state)
+{
+	int size = 0;
+	uint32_t i, prefix = group << 24, to_consume = 0, bytes;
+	uint8_t status, *m = (*midi), end;
+
+	if (*midi_size < 1)
+		return 0;
+	if (ump_maxsize < 16)
+		return -ENOSPC;
+
+	status = m[0];
+
+	/* SysEx */
+	if (*state == 0) {
+		if (status == 0xf0)
+			*state = 1; /* sysex start */
+		else if (status == 0xf7)
+			*state = 2; /* sysex continue */
+	}
+	if (*state & 3) {
+		prefix |= 0x30000000;
+		if (status & 0x80) {
+			m++;
+			to_consume++;
+		}
+		bytes = SPA_CLAMP(*midi_size - to_consume, 0u, 7u);
+		if (bytes > 0) {
+			end = m[bytes-1];
+			if (end & 0x80) {
+				bytes--; /* skip terminator */
+				to_consume++;
+			}
+			else
+				end = 0xf0; /* pretend there is a continue terminator */
+
+			bytes = SPA_CLAMP(bytes, 0u, 6u);
+			to_consume += bytes;
+
+			if (end == 0xf7) {
+				if (*state == 2) {
+					/* continue and done */
+					prefix |= 0x3 << 20;
+					*state = 0;
+				}
+			} else if (*state == 1) {
+				/* first packet but not finished */
+				prefix |= 0x1 << 20;
+				*state = 2; /* sysex continue */
+			} else {
+				/* continue and not finished */
+				prefix |= 0x2 << 20;
+			}
+			ump[size++] = prefix | bytes << 16;
+			ump[size++] = 0;
+			for (i = 0 ; i < bytes; i++)
+				/* ump[0] |= (m[0] & 0x7f) << 8
+				 * ump[0] |= (m[1] & 0x7f)
+				 * ump[1] |= (m[2] & 0x7f) << 24
+				 * ... */
+				ump[(i+2)/4] |= (m[i] & 0x7f) << ((5-i)%4 * 8);
+		}
+	} else {
+		/* regular messages */
+		switch (status) {
+		case 0x80 ... 0x8f:
+		case 0x90 ... 0x9f:
+		case 0xa0 ... 0xaf:
+		case 0xb0 ... 0xbf:
+		case 0xe0 ... 0xef:
+			to_consume = 3;
+			prefix |= 0x20000000;
+			break;
+		case 0xc0 ... 0xdf:
+			to_consume = 2;
+			prefix |= 0x20000000;
+			break;
+		case 0xf2:
+			to_consume = 3;
+			prefix = 0x10000000;
+			break;
+		case 0xf1: case 0xf3:
+			to_consume = 2;
+			prefix = 0x10000000;
+			break;
+		case 0xf4 ... 0xff:
+			to_consume = 1;
+			prefix = 0x10000000;
+			break;
+		default:
+			return -EIO;
+		}
+		if (*midi_size < to_consume) {
+			to_consume = *midi_size;
+		} else {
+			prefix |= status << 16;
+			if (to_consume > 1)
+				prefix |= (m[1] & 0x7f) << 8;
+			if (to_consume > 2)
+				prefix |= (m[2] & 0x7f);
+			ump[size++] = prefix;
+		}
+	}
+	(*midi_size) -= to_consume;
+	(*midi) += to_consume;
+
+	return size * 4;
+}
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+}  /* extern "C" */
+#endif
+
+#endif /* SPA_CONTROL_UMP_UTILS_H */
diff --git a/spa/include/spa/debug/buffer.h b/spa/include/spa/debug/buffer.h
index 8efa411c..eea48ae4 100644
--- a/spa/include/spa/debug/buffer.h
+++ b/spa/include/spa/debug/buffer.h
@@ -23,7 +23,15 @@ extern "C" {
 #include <spa/debug/types.h>
 #include <spa/buffer/type-info.h>
 
-static inline int spa_debugc_buffer(struct spa_debug_context *ctx, int indent, const struct spa_buffer *buffer)
+#ifndef SPA_API_DEBUG_BUFFER
+ #ifdef SPA_API_IMPL
+  #define SPA_API_DEBUG_BUFFER SPA_API_IMPL
+ #else
+  #define SPA_API_DEBUG_BUFFER static inline
+ #endif
+#endif
+
+SPA_API_DEBUG_BUFFER int spa_debugc_buffer(struct spa_debug_context *ctx, int indent, const struct spa_buffer *buffer)
 {
 	uint32_t i;
 
@@ -98,7 +106,7 @@ static inline int spa_debugc_buffer(struct spa_debug_context *ctx, int indent, c
 	return 0;
 }
 
-static inline int spa_debug_buffer(int indent, const struct spa_buffer *buffer)
+SPA_API_DEBUG_BUFFER int spa_debug_buffer(int indent, const struct spa_buffer *buffer)
 {
 	return spa_debugc_buffer(NULL, indent, buffer);
 }
diff --git a/spa/include/spa/debug/context.h b/spa/include/spa/debug/context.h
index bd0186b0..13002f66 100644
--- a/spa/include/spa/debug/context.h
+++ b/spa/include/spa/debug/context.h
@@ -26,13 +26,22 @@ extern "C" {
 #define spa_debug(_fmt,...)	spa_debugn(_fmt"\n", ## __VA_ARGS__)
 #endif
 
+
+#ifndef SPA_API_DEBUG_CONTEXT
+ #ifdef SPA_API_IMPL
+  #define SPA_API_DEBUG_CONTEXT SPA_API_IMPL
+ #else
+  #define SPA_API_DEBUG_CONTEXT static inline
+ #endif
+#endif
+
 struct spa_debug_context {
 	void (*log) (struct spa_debug_context *ctx, const char *fmt, ...) SPA_PRINTF_FUNC(2, 3);
 };
 
 #define spa_debugc(_c,_fmt,...)	(_c)?((_c)->log((_c),_fmt, ## __VA_ARGS__)):(void)spa_debug(_fmt, ## __VA_ARGS__)
 
-static inline void spa_debugc_error_location(struct spa_debug_context *c,
+SPA_API_DEBUG_CONTEXT void spa_debugc_error_location(struct spa_debug_context *c,
 		struct spa_error_location *loc)
 {
 	int i, skip = loc->col > 80 ? loc->col - 40 : 0, lc = loc->col-skip-1;
diff --git a/spa/include/spa/debug/dict.h b/spa/include/spa/debug/dict.h
index 2eb3672a..5657b2d9 100644
--- a/spa/include/spa/debug/dict.h
+++ b/spa/include/spa/debug/dict.h
@@ -17,7 +17,15 @@ extern "C" {
 #include <spa/debug/context.h>
 #include <spa/utils/dict.h>
 
-static inline int spa_debugc_dict(struct spa_debug_context *ctx, int indent, const struct spa_dict *dict)
+#ifndef SPA_API_DEBUG_DICT
+ #ifdef SPA_API_IMPL
+  #define SPA_API_DEBUG_DICT SPA_API_IMPL
+ #else
+  #define SPA_API_DEBUG_DICT static inline
+ #endif
+#endif
+
+SPA_API_DEBUG_DICT int spa_debugc_dict(struct spa_debug_context *ctx, int indent, const struct spa_dict *dict)
 {
 	const struct spa_dict_item *item;
 	spa_debugc(ctx, "%*sflags:%08x n_items:%d", indent, "", dict->flags, dict->n_items);
@@ -27,7 +35,7 @@ static inline int spa_debugc_dict(struct spa_debug_context *ctx, int indent, con
 	return 0;
 }
 
-static inline int spa_debug_dict(int indent, const struct spa_dict *dict)
+SPA_API_DEBUG_DICT int spa_debug_dict(int indent, const struct spa_dict *dict)
 {
 	return spa_debugc_dict(NULL, indent, dict);
 }
diff --git a/spa/include/spa/debug/file.h b/spa/include/spa/debug/file.h
index 115264f9..17ce46b7 100644
--- a/spa/include/spa/debug/file.h
+++ b/spa/include/spa/debug/file.h
@@ -26,13 +26,21 @@ extern "C" {
  * \{
  */
 
+#ifndef SPA_API_DEBUG_FILE
+ #ifdef SPA_API_IMPL
+  #define SPA_API_DEBUG_FILE SPA_API_IMPL
+ #else
+  #define SPA_API_DEBUG_FILE static inline
+ #endif
+#endif
+
 struct spa_debug_file_ctx {
 	struct spa_debug_context ctx;
 	FILE *f;
 };
 
 SPA_PRINTF_FUNC(2,3)
-static inline void spa_debug_file_log(struct spa_debug_context *ctx, const char *fmt, ...)
+SPA_API_DEBUG_FILE void spa_debug_file_log(struct spa_debug_context *ctx, const char *fmt, ...)
 {
 	struct spa_debug_file_ctx *c = SPA_CONTAINER_OF(ctx, struct spa_debug_file_ctx, ctx);
 	va_list args;
diff --git a/spa/include/spa/debug/format.h b/spa/include/spa/debug/format.h
index c92b43e7..ad7f2048 100644
--- a/spa/include/spa/debug/format.h
+++ b/spa/include/spa/debug/format.h
@@ -21,7 +21,16 @@ extern "C" {
 #include <spa/param/type-info.h>
 #include <spa/param/format-utils.h>
 
-static inline int
+#ifndef SPA_API_DEBUG_FORMAT
+ #ifdef SPA_API_IMPL
+  #define SPA_API_DEBUG_FORMAT SPA_API_IMPL
+ #else
+  #define SPA_API_DEBUG_FORMAT static inline
+ #endif
+#endif
+
+
+SPA_API_DEBUG_FORMAT int
 spa_debug_strbuf_format_value(struct spa_strbuf *buffer, const struct spa_type_info *info,
 		uint32_t type, void *body, uint32_t size)
 {
@@ -96,7 +105,7 @@ spa_debug_strbuf_format_value(struct spa_strbuf *buffer, const struct spa_type_i
 	return 0;
 }
 
-static inline int
+SPA_API_DEBUG_FORMAT int
 spa_debug_format_value(const struct spa_type_info *info,
 		uint32_t type, void *body, uint32_t size)
 {
@@ -108,7 +117,7 @@ spa_debug_format_value(const struct spa_type_info *info,
 	return 0;
 }
 
-static inline int spa_debugc_format(struct spa_debug_context *ctx, int indent,
+SPA_API_DEBUG_FORMAT int spa_debugc_format(struct spa_debug_context *ctx, int indent,
 		const struct spa_type_info *info, const struct spa_pod *format)
 {
 	const char *media_type;
@@ -198,7 +207,7 @@ static inline int spa_debugc_format(struct spa_debug_context *ctx, int indent,
 	return 0;
 }
 
-static inline int spa_debug_format(int indent,
+SPA_API_DEBUG_FORMAT int spa_debug_format(int indent,
 		const struct spa_type_info *info, const struct spa_pod *format)
 {
 	return spa_debugc_format(NULL, indent, info, format);
diff --git a/spa/include/spa/debug/log.h b/spa/include/spa/debug/log.h
index 89eaee10..05c3bd50 100644
--- a/spa/include/spa/debug/log.h
+++ b/spa/include/spa/debug/log.h
@@ -20,6 +20,14 @@ extern "C" {
 #include <spa/debug/mem.h>
 #include <spa/debug/pod.h>
 
+#ifndef SPA_API_DEBUG_LOG
+ #ifdef SPA_API_IMPL
+  #define SPA_API_DEBUG_LOG SPA_API_IMPL
+ #else
+  #define SPA_API_DEBUG_LOG static inline
+ #endif
+#endif
+
 /**
  * \addtogroup spa_debug
  * \{
@@ -36,7 +44,7 @@ struct spa_debug_log_ctx {
 };
 
 SPA_PRINTF_FUNC(2,3)
-static inline void spa_debug_log_log(struct spa_debug_context *ctx, const char *fmt, ...)
+SPA_API_DEBUG_LOG void spa_debug_log_log(struct spa_debug_context *ctx, const char *fmt, ...)
 {
 	struct spa_debug_log_ctx *c = SPA_CONTAINER_OF(ctx, struct spa_debug_log_ctx, ctx);
 	va_list args;
diff --git a/spa/include/spa/debug/mem.h b/spa/include/spa/debug/mem.h
index 462a5f93..96662948 100644
--- a/spa/include/spa/debug/mem.h
+++ b/spa/include/spa/debug/mem.h
@@ -18,7 +18,15 @@ extern "C" {
 
 #include <spa/debug/context.h>
 
-static inline int spa_debugc_mem(struct spa_debug_context *ctx, int indent, const void *data, size_t size)
+#ifndef SPA_API_DEBUG_MEM
+ #ifdef SPA_API_IMPL
+  #define SPA_API_DEBUG_MEM SPA_API_IMPL
+ #else
+  #define SPA_API_DEBUG_MEM static inline
+ #endif
+#endif
+
+SPA_API_DEBUG_MEM int spa_debugc_mem(struct spa_debug_context *ctx, int indent, const void *data, size_t size)
 {
 	const uint8_t *t = (const uint8_t*)data;
 	char buffer[512];
@@ -36,7 +44,7 @@ static inline int spa_debugc_mem(struct spa_debug_context *ctx, int indent, cons
 	return 0;
 }
 
-static inline int spa_debug_mem(int indent, const void *data, size_t size)
+SPA_API_DEBUG_MEM int spa_debug_mem(int indent, const void *data, size_t size)
 {
 	return spa_debugc_mem(NULL, indent, data, size);
 }
diff --git a/spa/include/spa/debug/node.h b/spa/include/spa/debug/node.h
index 592d6346..baa273ff 100644
--- a/spa/include/spa/debug/node.h
+++ b/spa/include/spa/debug/node.h
@@ -18,7 +18,15 @@ extern "C" {
 #include <spa/debug/context.h>
 #include <spa/debug/dict.h>
 
-static inline int spa_debugc_port_info(struct spa_debug_context *ctx, int indent, const struct spa_port_info *info)
+#ifndef SPA_API_DEBUG_NODE
+ #ifdef SPA_API_IMPL
+  #define SPA_API_DEBUG_NODE SPA_API_IMPL
+ #else
+  #define SPA_API_DEBUG_NODE static inline
+ #endif
+#endif
+
+SPA_API_DEBUG_NODE int spa_debugc_port_info(struct spa_debug_context *ctx, int indent, const struct spa_port_info *info)
 {
         spa_debugc(ctx, "%*s" "struct spa_port_info %p:", indent, "", info);
         spa_debugc(ctx, "%*s" " flags: \t%08" PRIx64, indent, "", info->flags);
@@ -31,7 +39,7 @@ static inline int spa_debugc_port_info(struct spa_debug_context *ctx, int indent
         return 0;
 }
 
-static inline int spa_debug_port_info(int indent, const struct spa_port_info *info)
+SPA_API_DEBUG_NODE int spa_debug_port_info(int indent, const struct spa_port_info *info)
 {
 	return spa_debugc_port_info(NULL, indent, info);
 }
diff --git a/spa/include/spa/debug/pod.h b/spa/include/spa/debug/pod.h
index a31b78e3..9db6f4b0 100644
--- a/spa/include/spa/debug/pod.h
+++ b/spa/include/spa/debug/pod.h
@@ -20,7 +20,15 @@ extern "C" {
 #include <spa/pod/pod.h>
 #include <spa/pod/iter.h>
 
-static inline int
+#ifndef SPA_API_DEBUG_POD
+ #ifdef SPA_API_IMPL
+  #define SPA_API_DEBUG_POD SPA_API_IMPL
+ #else
+  #define SPA_API_DEBUG_POD static inline
+ #endif
+#endif
+
+SPA_API_DEBUG_POD int
 spa_debugc_pod_value(struct spa_debug_context *ctx, int indent, const struct spa_type_info *info,
 		uint32_t type, void *body, uint32_t size)
 {
@@ -60,13 +68,13 @@ spa_debugc_pod_value(struct spa_debug_context *ctx, int indent, const struct spa
 	case SPA_TYPE_Rectangle:
 	{
 		struct spa_rectangle *r = (struct spa_rectangle *)body;
-		spa_debugc(ctx, "%*s" "Rectangle %dx%d", indent, "", r->width, r->height);
+		spa_debugc(ctx, "%*s" "Rectangle %" PRIu32 "x%" PRIu32 "", indent, "", r->width, r->height);
 		break;
 	}
 	case SPA_TYPE_Fraction:
 	{
 		struct spa_fraction *f = (struct spa_fraction *)body;
-		spa_debugc(ctx, "%*s" "Fraction %d/%d", indent, "", f->num, f->denom);
+		spa_debugc(ctx, "%*s" "Fraction %" PRIu32 "/%" PRIu32 "", indent, "", f->num, f->denom);
 		break;
 	}
 	case SPA_TYPE_Bitmap:
@@ -174,7 +182,7 @@ spa_debugc_pod_value(struct spa_debug_context *ctx, int indent, const struct spa
 	return 0;
 }
 
-static inline int spa_debugc_pod(struct spa_debug_context *ctx, int indent,
+SPA_API_DEBUG_POD int spa_debugc_pod(struct spa_debug_context *ctx, int indent,
 		const struct spa_type_info *info, const struct spa_pod *pod)
 {
 	return spa_debugc_pod_value(ctx, indent, info ? info : SPA_TYPE_ROOT,
@@ -183,14 +191,14 @@ static inline int spa_debugc_pod(struct spa_debug_context *ctx, int indent,
 			SPA_POD_BODY_SIZE(pod));
 }
 
-static inline int
+SPA_API_DEBUG_POD int
 spa_debug_pod_value(int indent, const struct spa_type_info *info,
 		uint32_t type, void *body, uint32_t size)
 {
 	return spa_debugc_pod_value(NULL, indent, info, type, body, size);
 }
 
-static inline int spa_debug_pod(int indent,
+SPA_API_DEBUG_POD int spa_debug_pod(int indent,
 		const struct spa_type_info *info, const struct spa_pod *pod)
 {
 	return spa_debugc_pod(NULL, indent, info, pod);
diff --git a/spa/include/spa/debug/types.h b/spa/include/spa/debug/types.h
index 6e53a547..d7ca8366 100644
--- a/spa/include/spa/debug/types.h
+++ b/spa/include/spa/debug/types.h
@@ -18,7 +18,16 @@ extern "C" {
 
 #include <string.h>
 
-static inline const struct spa_type_info *spa_debug_type_find(const struct spa_type_info *info, uint32_t type)
+#ifndef SPA_API_DEBUG_TYPES
+ #ifdef SPA_API_IMPL
+  #define SPA_API_DEBUG_TYPES SPA_API_IMPL
+ #else
+  #define SPA_API_DEBUG_TYPES static inline
+ #endif
+#endif
+
+
+SPA_API_DEBUG_TYPES const struct spa_type_info *spa_debug_type_find(const struct spa_type_info *info, uint32_t type)
 {
 	const struct spa_type_info *res;
 
@@ -37,22 +46,19 @@ static inline const struct spa_type_info *spa_debug_type_find(const struct spa_t
 	return NULL;
 }
 
-static inline const char *spa_debug_type_short_name(const char *name)
+SPA_API_DEBUG_TYPES const char *spa_debug_type_short_name(const char *name)
 {
-	const char *h;
-	if ((h = strrchr(name, ':')) != NULL)
-		name = h + 1;
-	return name;
+	return spa_type_short_name(name);
 }
 
-static inline const char *spa_debug_type_find_name(const struct spa_type_info *info, uint32_t type)
+SPA_API_DEBUG_TYPES const char *spa_debug_type_find_name(const struct spa_type_info *info, uint32_t type)
 {
 	if ((info = spa_debug_type_find(info, type)) == NULL)
 		return NULL;
 	return info->name;
 }
 
-static inline const char *spa_debug_type_find_short_name(const struct spa_type_info *info, uint32_t type)
+SPA_API_DEBUG_TYPES const char *spa_debug_type_find_short_name(const struct spa_type_info *info, uint32_t type)
 {
 	const char *str;
 	if ((str = spa_debug_type_find_name(info, type)) == NULL)
@@ -60,7 +66,7 @@ static inline const char *spa_debug_type_find_short_name(const struct spa_type_i
 	return spa_debug_type_short_name(str);
 }
 
-static inline uint32_t spa_debug_type_find_type(const struct spa_type_info *info, const char *name)
+SPA_API_DEBUG_TYPES uint32_t spa_debug_type_find_type(const struct spa_type_info *info, const char *name)
 {
 	if (info == NULL)
 		info = SPA_TYPE_ROOT;
@@ -76,7 +82,7 @@ static inline uint32_t spa_debug_type_find_type(const struct spa_type_info *info
 	return SPA_ID_INVALID;
 }
 
-static inline const struct spa_type_info *spa_debug_type_find_short(const struct spa_type_info *info, const char *name)
+SPA_API_DEBUG_TYPES const struct spa_type_info *spa_debug_type_find_short(const struct spa_type_info *info, const char *name)
 {
 	while (info && info->name) {
 		if (strcmp(spa_debug_type_short_name(info->name), name) == 0)
@@ -90,7 +96,7 @@ static inline const struct spa_type_info *spa_debug_type_find_short(const struct
 	return NULL;
 }
 
-static inline uint32_t spa_debug_type_find_type_short(const struct spa_type_info *info, const char *name)
+SPA_API_DEBUG_TYPES uint32_t spa_debug_type_find_type_short(const struct spa_type_info *info, const char *name)
 {
 	if ((info = spa_debug_type_find_short(info, name)) == NULL)
 		return SPA_ID_INVALID;
diff --git a/spa/include/spa/filter-graph/filter-graph.h b/spa/include/spa/filter-graph/filter-graph.h
new file mode 100644
index 00000000..05904c7f
--- /dev/null
+++ b/spa/include/spa/filter-graph/filter-graph.h
@@ -0,0 +1,150 @@
+/* Simple Plugin API */
+/* SPDX-FileCopyrightText: Copyright © 2024 Wim Taymans */
+/* SPDX-License-Identifier: MIT */
+
+#ifndef SPA_FILTER_GRAPH_H
+#define SPA_FILTER_GRAPH_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <sys/types.h>
+
+#include <spa/utils/defs.h>
+#include <spa/utils/hook.h>
+#include <spa/pod/builder.h>
+
+#ifndef SPA_API_FILTER_GRAPH
+ #ifdef SPA_API_IMPL
+  #define SPA_API_FILTER_GRAPH SPA_API_IMPL
+ #else
+  #define SPA_API_FILTER_GRAPH static inline
+ #endif
+#endif
+
+/** \defgroup spa_filter_graph Filter Graph
+ * a graph of filters
+ */
+
+/**
+ * \addtogroup spa_filter_graph
+ * \{
+ */
+
+/**
+ * A graph of filters
+ */
+#define SPA_TYPE_INTERFACE_FilterGraph	SPA_TYPE_INFO_INTERFACE_BASE "FilterGraph"
+
+#define SPA_VERSION_FILTER_GRAPH		0
+struct spa_filter_graph { struct spa_interface iface; };
+
+struct spa_filter_graph_info {
+	uint32_t n_inputs;
+	uint32_t n_outputs;
+
+#define SPA_FILTER_GRAPH_CHANGE_MASK_FLAGS		(1u<<0)
+#define SPA_FILTER_GRAPH_CHANGE_MASK_PROPS		(1u<<1)
+	uint64_t change_mask;
+
+	uint64_t flags;
+	struct spa_dict *props;
+};
+
+struct spa_filter_graph_events {
+#define SPA_VERSION_FILTER_GRAPH_EVENTS	0
+	uint32_t version;
+
+	void (*info) (void *object, const struct spa_filter_graph_info *info);
+
+	void (*apply_props) (void *object, enum spa_direction direction, const struct spa_pod *props);
+
+	void (*props_changed) (void *object, enum spa_direction direction);
+};
+
+struct spa_filter_graph_methods {
+#define SPA_VERSION_FILTER_GRAPH_METHODS	0
+	uint32_t version;
+
+	int (*add_listener) (void *object,
+			struct spa_hook *listener,
+			const struct spa_filter_graph_events *events,
+			void *data);
+
+	int (*enum_prop_info) (void *object, uint32_t idx, struct spa_pod_builder *b,
+			struct spa_pod **param);
+	int (*get_props) (void *object, struct spa_pod_builder *b, struct spa_pod **props);
+	int (*set_props) (void *object, enum spa_direction direction, const struct spa_pod *props);
+
+	int (*activate) (void *object, const struct spa_dict *props);
+	int (*deactivate) (void *object);
+
+	int (*reset) (void *object);
+
+	int (*process) (void *object, const void *in[], void *out[], uint32_t n_samples);
+};
+
+SPA_API_FILTER_GRAPH int spa_filter_graph_add_listener(struct spa_filter_graph *object,
+			struct spa_hook *listener,
+			const struct spa_filter_graph_events *events, void *data)
+{
+	return spa_api_method_r(int, -ENOTSUP,
+			spa_filter_graph, &object->iface, add_listener, 0, listener,
+			events, data);
+}
+
+SPA_API_FILTER_GRAPH int spa_filter_graph_enum_prop_info(struct spa_filter_graph *object,
+		uint32_t idx, struct spa_pod_builder *b, struct spa_pod **param)
+{
+	return spa_api_method_r(int, -ENOTSUP,
+			spa_filter_graph, &object->iface, enum_prop_info, 0, idx, b, param);
+}
+SPA_API_FILTER_GRAPH int spa_filter_graph_get_props(struct spa_filter_graph *object,
+		struct spa_pod_builder *b, struct spa_pod **props)
+{
+	return spa_api_method_r(int, -ENOTSUP,
+			spa_filter_graph, &object->iface, get_props, 0, b, props);
+}
+
+SPA_API_FILTER_GRAPH int spa_filter_graph_set_props(struct spa_filter_graph *object,
+		enum spa_direction direction, const struct spa_pod *props)
+{
+	return spa_api_method_r(int, -ENOTSUP,
+			spa_filter_graph, &object->iface, set_props, 0, direction, props);
+}
+
+SPA_API_FILTER_GRAPH int spa_filter_graph_activate(struct spa_filter_graph *object, const struct spa_dict *props)
+{
+	return spa_api_method_r(int, -ENOTSUP,
+			spa_filter_graph, &object->iface, activate, 0, props);
+}
+SPA_API_FILTER_GRAPH int spa_filter_graph_deactivate(struct spa_filter_graph *object)
+{
+	return spa_api_method_r(int, -ENOTSUP,
+			spa_filter_graph, &object->iface, deactivate, 0);
+}
+
+SPA_API_FILTER_GRAPH int spa_filter_graph_reset(struct spa_filter_graph *object)
+{
+	return spa_api_method_r(int, -ENOTSUP,
+			spa_filter_graph, &object->iface, reset, 0);
+}
+
+SPA_API_FILTER_GRAPH int spa_filter_graph_process(struct spa_filter_graph *object,
+			const void *in[], void *out[], uint32_t n_samples)
+{
+	return spa_api_method_r(int, -ENOTSUP,
+			spa_filter_graph, &object->iface, process, 0, in, out, n_samples);
+}
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+}  /* extern "C" */
+#endif
+
+#endif /* SPA_FILTER_GRAPH_H */
+
diff --git a/spa/include/spa/graph/graph.h b/spa/include/spa/graph/graph.h
index 117f6eef..537e6e75 100644
--- a/spa/include/spa/graph/graph.h
+++ b/spa/include/spa/graph/graph.h
@@ -25,6 +25,15 @@ extern "C" {
 #include <spa/node/node.h>
 #include <spa/node/io.h>
 
+#ifndef SPA_API_GRAPH
+ #ifdef SPA_API_IMPL
+  #define SPA_API_GRAPH SPA_API_IMPL
+ #else
+  #define SPA_API_GRAPH static inline
+ #endif
+#endif
+
+
 #ifndef spa_debug
 #define spa_debug(...)
 #endif
@@ -40,7 +49,7 @@ struct spa_graph_state {
 	int32_t pending;		/**< number of pending signals */
 };
 
-static inline void spa_graph_state_reset(struct spa_graph_state *state)
+SPA_API_GRAPH void spa_graph_state_reset(struct spa_graph_state *state)
 {
 	state->pending = state->required;
 }
@@ -56,7 +65,7 @@ struct spa_graph_link {
 
 #define spa_graph_state_dec(s) (SPA_ATOMIC_DEC(s->pending) == 0)
 
-static inline int spa_graph_link_trigger(struct spa_graph_link *link)
+SPA_API_GRAPH int spa_graph_link_trigger(struct spa_graph_link *link)
 {
 	struct spa_graph_state *state = link->state;
 
@@ -118,7 +127,7 @@ struct spa_graph_port {
 	struct spa_graph_port *peer;	/**< peer */
 };
 
-static inline int spa_graph_node_trigger(struct spa_graph_node *node)
+SPA_API_GRAPH int spa_graph_node_trigger(struct spa_graph_node *node)
 {
 	struct spa_graph_link *l;
 	spa_debug("node %p trigger", node);
@@ -127,7 +136,7 @@ static inline int spa_graph_node_trigger(struct spa_graph_node *node)
 	return 0;
 }
 
-static inline int spa_graph_run(struct spa_graph *graph)
+SPA_API_GRAPH int spa_graph_run(struct spa_graph *graph)
 {
 	struct spa_graph_node *n, *t;
 	struct spa_list pending;
@@ -152,27 +161,27 @@ static inline int spa_graph_run(struct spa_graph *graph)
 	return 0;
 }
 
-static inline int spa_graph_finish(struct spa_graph *graph)
+SPA_API_GRAPH int spa_graph_finish(struct spa_graph *graph)
 {
 	spa_debug("graph %p finish", graph);
 	if (graph->parent)
 		return spa_graph_node_trigger(graph->parent);
 	return 0;
 }
-static inline int spa_graph_link_signal_node(void *data)
+SPA_API_GRAPH int spa_graph_link_signal_node(void *data)
 {
 	struct spa_graph_node *node = (struct spa_graph_node *)data;
 	spa_debug("node %p call process", node);
 	return spa_graph_node_process(node);
 }
 
-static inline int spa_graph_link_signal_graph(void *data)
+SPA_API_GRAPH int spa_graph_link_signal_graph(void *data)
 {
 	struct spa_graph_node *node = (struct spa_graph_node *)data;
 	return spa_graph_finish(node->graph);
 }
 
-static inline void spa_graph_init(struct spa_graph *graph, struct spa_graph_state *state)
+SPA_API_GRAPH void spa_graph_init(struct spa_graph *graph, struct spa_graph_state *state)
 {
 	spa_list_init(&graph->nodes);
 	graph->flags = 0;
@@ -180,7 +189,7 @@ static inline void spa_graph_init(struct spa_graph *graph, struct spa_graph_stat
 	spa_debug("graph %p init state %p", graph, state);
 }
 
-static inline void
+SPA_API_GRAPH void
 spa_graph_link_add(struct spa_graph_node *out,
 		   struct spa_graph_state *state,
 		   struct spa_graph_link *link)
@@ -191,14 +200,14 @@ spa_graph_link_add(struct spa_graph_node *out,
 	spa_list_append(&out->links, &link->link);
 }
 
-static inline void spa_graph_link_remove(struct spa_graph_link *link)
+SPA_API_GRAPH void spa_graph_link_remove(struct spa_graph_link *link)
 {
 	link->state->required--;
 	spa_debug("link %p state %p remove %d", link, link->state, link->state->required);
 	spa_list_remove(&link->link);
 }
 
-static inline void
+SPA_API_GRAPH void
 spa_graph_node_init(struct spa_graph_node *node, struct spa_graph_state *state)
 {
 	spa_list_init(&node->ports[SPA_DIRECTION_INPUT]);
@@ -215,7 +224,7 @@ spa_graph_node_init(struct spa_graph_node *node, struct spa_graph_state *state)
 }
 
 
-static inline int spa_graph_node_impl_sub_process(void *data SPA_UNUSED, struct spa_graph_node *node)
+SPA_API_GRAPH int spa_graph_node_impl_sub_process(void *data SPA_UNUSED, struct spa_graph_node *node)
 {
 	struct spa_graph *graph = node->subgraph;
 	spa_debug("node %p: sub process %p", node, graph);
@@ -227,7 +236,7 @@ static const struct spa_graph_node_callbacks spa_graph_node_sub_impl_default = {
 	.process = spa_graph_node_impl_sub_process,
 };
 
-static inline void spa_graph_node_set_subgraph(struct spa_graph_node *node,
+SPA_API_GRAPH void spa_graph_node_set_subgraph(struct spa_graph_node *node,
 		struct spa_graph *subgraph)
 {
 	node->subgraph = subgraph;
@@ -235,7 +244,7 @@ static inline void spa_graph_node_set_subgraph(struct spa_graph_node *node,
 	spa_debug("node %p set subgraph %p", node, subgraph);
 }
 
-static inline void
+SPA_API_GRAPH void
 spa_graph_node_set_callbacks(struct spa_graph_node *node,
 		const struct spa_graph_node_callbacks *callbacks,
 		void *data)
@@ -243,7 +252,7 @@ spa_graph_node_set_callbacks(struct spa_graph_node *node,
 	node->callbacks = SPA_CALLBACKS_INIT(callbacks, data);
 }
 
-static inline void
+SPA_API_GRAPH void
 spa_graph_node_add(struct spa_graph *graph,
 		   struct spa_graph_node *node)
 {
@@ -255,7 +264,7 @@ spa_graph_node_add(struct spa_graph *graph,
 	spa_graph_link_add(node, graph->state, &node->graph_link);
 }
 
-static inline void spa_graph_node_remove(struct spa_graph_node *node)
+SPA_API_GRAPH void spa_graph_node_remove(struct spa_graph_node *node)
 {
 	spa_debug("node %p remove from graph %p, state %p required %d",
 			node, node->graph, node->state, node->state->required);
@@ -265,7 +274,7 @@ static inline void spa_graph_node_remove(struct spa_graph_node *node)
 }
 
 
-static inline void
+SPA_API_GRAPH void
 spa_graph_port_init(struct spa_graph_port *port,
 		    enum spa_direction direction,
 		    uint32_t port_id,
@@ -277,7 +286,7 @@ spa_graph_port_init(struct spa_graph_port *port,
 	port->flags = flags;
 }
 
-static inline void
+SPA_API_GRAPH void
 spa_graph_port_add(struct spa_graph_node *node,
 		   struct spa_graph_port *port)
 {
@@ -286,13 +295,13 @@ spa_graph_port_add(struct spa_graph_node *node,
 	spa_list_append(&node->ports[port->direction], &port->link);
 }
 
-static inline void spa_graph_port_remove(struct spa_graph_port *port)
+SPA_API_GRAPH void spa_graph_port_remove(struct spa_graph_port *port)
 {
 	spa_debug("port %p remove", port);
 	spa_list_remove(&port->link);
 }
 
-static inline void
+SPA_API_GRAPH void
 spa_graph_port_link(struct spa_graph_port *out, struct spa_graph_port *in)
 {
 	spa_debug("port %p link to %p %p %p", out, in, in->node, in->node->state);
@@ -300,7 +309,7 @@ spa_graph_port_link(struct spa_graph_port *out, struct spa_graph_port *in)
 	in->peer = out;
 }
 
-static inline void
+SPA_API_GRAPH void
 spa_graph_port_unlink(struct spa_graph_port *port)
 {
 	spa_debug("port %p unlink from %p", port, port->peer);
@@ -310,7 +319,7 @@ spa_graph_port_unlink(struct spa_graph_port *port)
 	}
 }
 
-static inline int spa_graph_node_impl_process(void *data, struct spa_graph_node *node)
+SPA_API_GRAPH int spa_graph_node_impl_process(void *data, struct spa_graph_node *node)
 {
 	struct spa_node *n = (struct spa_node *)data;
 	struct spa_graph_state *state = node->state;
@@ -322,7 +331,7 @@ static inline int spa_graph_node_impl_process(void *data, struct spa_graph_node
         return state->status;
 }
 
-static inline int spa_graph_node_impl_reuse_buffer(void *data, struct spa_graph_node *node SPA_UNUSED,
+SPA_API_GRAPH int spa_graph_node_impl_reuse_buffer(void *data, struct spa_graph_node *node SPA_UNUSED,
 		uint32_t port_id, uint32_t buffer_id)
 {
 	struct spa_node *n = (struct spa_node *)data;
diff --git a/spa/include/spa/interfaces/audio/aec.h b/spa/include/spa/interfaces/audio/aec.h
index 67319f1d..48626a23 100644
--- a/spa/include/spa/interfaces/audio/aec.h
+++ b/spa/include/spa/interfaces/audio/aec.h
@@ -14,6 +14,14 @@
 extern "C" {
 #endif
 
+#ifndef SPA_API_AUDIO_AEC
+ #ifdef SPA_API_IMPL
+  #define SPA_API_AUDIO_AEC SPA_API_IMPL
+ #else
+  #define SPA_API_AUDIO_AEC static inline
+ #endif
+#endif
+
 #define SPA_TYPE_INTERFACE_AUDIO_AEC SPA_TYPE_INFO_INTERFACE_BASE "Audio:AEC"
 
 #define SPA_VERSION_AUDIO_AEC   1
@@ -68,26 +76,69 @@ struct spa_audio_aec_methods {
 			struct spa_audio_info_raw *out_info);
 };
 
-#define spa_audio_aec_method(o,method,version,...)			\
-({									\
-	int _res = -ENOTSUP;						\
-	struct spa_audio_aec *_o = (o);					\
-	spa_interface_call_res(&_o->iface,				\
-			struct spa_audio_aec_methods, _res,		\
-			method, (version), ##__VA_ARGS__);		\
-	_res;								\
-})
-
-#define spa_audio_aec_add_listener(o,...)	spa_audio_aec_method(o, add_listener, 0, __VA_ARGS__)
-#define spa_audio_aec_init(o,...)		spa_audio_aec_method(o, init, 0, __VA_ARGS__)
-#define spa_audio_aec_run(o,...)		spa_audio_aec_method(o, run, 0, __VA_ARGS__)
-#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__)
-#define spa_audio_aec_init2(o,...)		spa_audio_aec_method(o, init2, 3, __VA_ARGS__)
+SPA_API_AUDIO_AEC int spa_audio_aec_add_listener(struct spa_audio_aec *object,
+			struct spa_hook *listener,
+			const struct spa_audio_aec_events *events,
+			void *data)
+{
+	return spa_api_method_r(int, -ENOTSUP,
+			spa_audio_aec, &object->iface, add_listener, 0, listener, events, data);
+}
+
+SPA_API_AUDIO_AEC int spa_audio_aec_init(struct spa_audio_aec *object,
+		const struct spa_dict *args, const struct spa_audio_info_raw *info)
+{
+	return spa_api_method_r(int, -ENOTSUP,
+			spa_audio_aec, &object->iface, init, 0, args, info);
+}
+SPA_API_AUDIO_AEC int spa_audio_aec_run(struct spa_audio_aec *object,
+		const float *rec[], const float *play[], float *out[], uint32_t n_samples)
+{
+	return spa_api_method_r(int, -ENOTSUP,
+			spa_audio_aec, &object->iface, run, 0, rec, play, out, n_samples);
+}
+SPA_API_AUDIO_AEC int spa_audio_aec_set_props(struct spa_audio_aec *object, const struct spa_dict *args)
+{
+	return spa_api_method_r(int, -ENOTSUP,
+			spa_audio_aec, &object->iface, set_props, 0, args);
+}
+SPA_API_AUDIO_AEC int spa_audio_aec_activate(struct spa_audio_aec *object)
+{
+	return spa_api_method_r(int, -ENOTSUP,
+			spa_audio_aec, &object->iface, activate, 1);
+}
+SPA_API_AUDIO_AEC int spa_audio_aec_deactivate(struct spa_audio_aec *object)
+{
+	return spa_api_method_r(int, -ENOTSUP,
+			spa_audio_aec, &object->iface, deactivate, 1);
+}
+SPA_API_AUDIO_AEC int spa_audio_aec_enum_props(struct spa_audio_aec *object,
+		int index, struct spa_pod_builder* builder)
+{
+	return spa_api_method_r(int, -ENOTSUP,
+			spa_audio_aec, &object->iface, enum_props, 2, index, builder);
+}
+SPA_API_AUDIO_AEC int spa_audio_aec_get_params(struct spa_audio_aec *object,
+		struct spa_pod_builder* builder)
+{
+	return spa_api_method_r(int, -ENOTSUP,
+			spa_audio_aec, &object->iface, get_params, 2, builder);
+}
+SPA_API_AUDIO_AEC int spa_audio_aec_set_params(struct spa_audio_aec *object,
+		const struct spa_pod *args)
+{
+	return spa_api_method_r(int, -ENOTSUP,
+			spa_audio_aec, &object->iface, set_params, 2, args);
+}
+SPA_API_AUDIO_AEC int spa_audio_aec_init2(struct spa_audio_aec *object,
+		const struct spa_dict *args,
+		struct spa_audio_info_raw *play_info,
+		struct spa_audio_info_raw *rec_info,
+		struct spa_audio_info_raw *out_info)
+{
+	return spa_api_method_r(int, -ENOTSUP,
+			spa_audio_aec, &object->iface, init2, 3, args, play_info, rec_info, out_info);
+}
 
 #ifdef __cplusplus
 }  /* extern "C" */
diff --git a/spa/include/spa/monitor/device.h b/spa/include/spa/monitor/device.h
index 2201ffdb..73b4a94f 100644
--- a/spa/include/spa/monitor/device.h
+++ b/spa/include/spa/monitor/device.h
@@ -14,6 +14,14 @@ extern "C" {
 #include <spa/utils/dict.h>
 #include <spa/pod/event.h>
 
+#ifndef SPA_API_DEVICE
+ #ifdef SPA_API_IMPL
+  #define SPA_API_DEVICE SPA_API_IMPL
+ #else
+  #define SPA_API_DEVICE static inline
+ #endif
+#endif
+
 /**
  * \defgroup spa_device Device
  *
@@ -220,20 +228,34 @@ struct spa_device_methods {
 			  const struct spa_pod *param);
 };
 
-#define spa_device_method(o,method,version,...)				\
-({									\
-	int _res = -ENOTSUP;						\
-	struct spa_device *_o = (o);					\
-	spa_interface_call_res(&_o->iface,				\
-			struct spa_device_methods, _res,		\
-			method, (version), ##__VA_ARGS__);		\
-	_res;								\
-})
-
-#define spa_device_add_listener(d,...)	spa_device_method(d, add_listener, 0, __VA_ARGS__)
-#define spa_device_sync(d,...)		spa_device_method(d, sync, 0, __VA_ARGS__)
-#define spa_device_enum_params(d,...)	spa_device_method(d, enum_params, 0, __VA_ARGS__)
-#define spa_device_set_param(d,...)	spa_device_method(d, set_param, 0, __VA_ARGS__)
+SPA_API_DEVICE int spa_device_add_listener(struct spa_device *object,
+			struct spa_hook *listener,
+			const struct spa_device_events *events,
+			void *data)
+{
+	return spa_api_method_r(int, -ENOTSUP, spa_device, &object->iface, add_listener, 0,
+			listener, events, data);
+
+}
+SPA_API_DEVICE int spa_device_sync(struct spa_device *object, int seq)
+{
+	return spa_api_method_r(int, -ENOTSUP, spa_device, &object->iface, sync, 0,
+			seq);
+}
+SPA_API_DEVICE int spa_device_enum_params(struct spa_device *object, int seq,
+			    uint32_t id, uint32_t index, uint32_t max,
+			    const struct spa_pod *filter)
+{
+	return spa_api_method_r(int, -ENOTSUP, spa_device, &object->iface, enum_params, 0,
+			seq, id, index, max, filter);
+}
+SPA_API_DEVICE int spa_device_set_param(struct spa_device *object,
+			  uint32_t id, uint32_t flags,
+			  const struct spa_pod *param)
+{
+	return spa_api_method_r(int, -ENOTSUP, spa_device, &object->iface, set_param, 0,
+			id, flags, param);
+}
 
 #define SPA_KEY_DEVICE_ENUM_API		"device.enum.api"	/**< the api used to discover this
 								  *  device */
@@ -271,7 +293,7 @@ struct spa_device_methods {
 								  *  "webcam", "microphone", "headset",
 								  *  "headphone", "hands-free", "car", "hifi",
 								  *  "computer", "portable" */
-#define SPA_KEY_DEVICE_PROFILE		"device.profile	"	/**< profile for the device */
+#define SPA_KEY_DEVICE_PROFILE		"device.profile"	/**< profile for the device */
 #define SPA_KEY_DEVICE_PROFILE_SET	"device.profile-set"	/**< profile set for the device */
 #define SPA_KEY_DEVICE_STRING		"device.string"		/**< device string in the underlying
 								  *  layer's format. E.g. "surround51:0" */
diff --git a/spa/include/spa/monitor/utils.h b/spa/include/spa/monitor/utils.h
index 93f8f41b..4337eceb 100644
--- a/spa/include/spa/monitor/utils.h
+++ b/spa/include/spa/monitor/utils.h
@@ -12,6 +12,14 @@ extern "C" {
 #include <spa/pod/builder.h>
 #include <spa/monitor/device.h>
 
+#ifndef SPA_API_DEVICE_UTILS
+ #ifdef SPA_API_IMPL
+  #define SPA_API_DEVICE_UTILS SPA_API_IMPL
+ #else
+  #define SPA_API_DEVICE_UTILS static inline
+ #endif
+#endif
+
 /**
  * \addtogroup spa_device
  * \{
@@ -22,7 +30,7 @@ struct spa_result_device_params_data {
 	struct spa_result_device_params data;
 };
 
-static inline void spa_result_func_device_params(void *data, int seq SPA_UNUSED, int res SPA_UNUSED,
+SPA_API_DEVICE_UTILS void spa_result_func_device_params(void *data, int seq SPA_UNUSED, int res SPA_UNUSED,
 		uint32_t type SPA_UNUSED, const void *result)
 {
 	struct spa_result_device_params_data *d =
@@ -36,7 +44,7 @@ static inline void spa_result_func_device_params(void *data, int seq SPA_UNUSED,
 	d->data.param = spa_pod_builder_deref(d->builder, offset);
 }
 
-static inline int spa_device_enum_params_sync(struct spa_device *device,
+SPA_API_DEVICE_UTILS int spa_device_enum_params_sync(struct spa_device *device,
 			uint32_t id, uint32_t *index,
 			const struct spa_pod *filter,
 			struct spa_pod **param,
diff --git a/spa/include/spa/node/io.h b/spa/include/spa/node/io.h
index 409fdde1..c1c725eb 100644
--- a/spa/include/spa/node/io.h
+++ b/spa/include/spa/node/io.h
@@ -128,6 +128,9 @@ struct spa_io_clock {
 #define SPA_IO_CLOCK_FLAG_FREEWHEEL	(1u<<0) /* graph is freewheeling */
 #define SPA_IO_CLOCK_FLAG_XRUN_RECOVER	(1u<<1) /* recovering from xrun */
 #define SPA_IO_CLOCK_FLAG_LAZY		(1u<<2) /* lazy scheduling */
+#define SPA_IO_CLOCK_FLAG_NO_RATE	(1u<<3) /* the rate of the clock is only approximately.
+						 * it is recommended to use the nsec as a clock source.
+						 * The rate_diff contains the measured inaccuracy. */
 	uint32_t flags;			/**< Clock flags */
 	uint32_t id;			/**< Unique clock id, set by host application */
 	char name[64];			/**< Clock name prefixed with API, set by node when it receives
@@ -302,14 +305,50 @@ struct spa_io_position {
 	struct spa_io_segment segments[SPA_IO_POSITION_MAX_SEGMENTS];	/**< segments */
 };
 
-/** rate matching */
+/**
+ * Rate matching.
+ *
+ * It is usually set on the nodes that process resampled data, by
+ * the component (audioadapter) that handles resampling between graph
+ * and node rates. The \a flags and \a rate fields may be modified by the node.
+ *
+ * The node can request a correction to the resampling rate in its process(), by setting
+ * \ref SPA_IO_RATE_MATCH_ACTIVE on \a flags, and setting \a rate to the desired rate
+ * correction.  Usually the rate is obtained from DLL or other adaptive mechanism that
+ * e.g. drives the node buffer fill level toward a specific value.
+ *
+ * When resampling to (graph->node) direction, the number of samples produced
+ * by the resampler varies on each cycle, as the rates are not commensurate.
+ *
+ * When resampling to (node->graph) direction, the number of samples consumed by the
+ * resampler varies. Node output ports in process() should produce \a size number of
+ * samples to match what the resampler needs to produce one graph quantum of output
+ * samples.
+ *
+ * Resampling filters introduce processing delay, given by \a delay and \a delay_frac, in
+ * samples at node rate. The delay varies on each cycle e.g. when resampling between
+ * noncommensurate rates.
+ *
+ * The first sample output (graph->node) or consumed (node->graph) by the resampler is
+ * offset by \a delay + \a delay_frac / 1e9 node samples relative to the nominal graph
+ * cycle start position:
+ *
+ * \code{.unparsed}
+ * first_resampled_sample_nsec =
+ *	first_original_sample_nsec
+ *	- (rate_match->delay * SPA_NSEC_PER_SEC + rate_match->delay_frac) / node_rate
+ * \endcode
+ */
 struct spa_io_rate_match {
-	uint32_t delay;			/**< extra delay in samples for resampler */
+	uint32_t delay;			/**< resampling delay, in samples at
+					 * node rate */
 	uint32_t size;			/**< requested input size for resampler */
-	double rate;			/**< rate for resampler */
+	double rate;			/**< rate for resampler (set by node) */
 #define SPA_IO_RATE_MATCH_FLAG_ACTIVE	(1 << 0)
-	uint32_t flags;			/**< extra flags */
-	uint32_t padding[7];
+	uint32_t flags;			/**< extra flags (set by node) */
+	int32_t delay_frac;		/**< resampling delay fractional part,
+					 * in units of nanosamples (1/10^9 sample) at node rate */
+	uint32_t padding[6];
 };
 
 /** async buffers */
diff --git a/spa/include/spa/node/node.h b/spa/include/spa/node/node.h
index 44e45da8..ae9f6354 100644
--- a/spa/include/spa/node/node.h
+++ b/spa/include/spa/node/node.h
@@ -27,6 +27,14 @@ extern "C" {
 #include <spa/node/event.h>
 #include <spa/node/command.h>
 
+#ifndef SPA_API_NODE
+ #ifdef SPA_API_IMPL
+  #define SPA_API_NODE SPA_API_IMPL
+ #else
+  #define SPA_API_NODE static inline
+ #endif
+#endif
+
 
 #define SPA_TYPE_INTERFACE_Node		SPA_TYPE_INFO_INTERFACE_BASE "Node"
 
@@ -633,44 +641,120 @@ struct spa_node_methods {
 	int (*process) (void *object);
 };
 
-#define spa_node_method(o,method,version,...)				\
-({									\
-	int _res = -ENOTSUP;						\
-	struct spa_node *_n = o;					\
-	spa_interface_call_res(&_n->iface,				\
-			struct spa_node_methods, _res,			\
-			method, version, ##__VA_ARGS__);		\
-	_res;								\
-})
-
-#define spa_node_method_fast(o,method,version,...)			\
-({									\
-	int _res;							\
-	struct spa_node *_n = o;					\
-	spa_interface_call_fast_res(&_n->iface,				\
-			struct spa_node_methods, _res,			\
-			method, version, ##__VA_ARGS__);		\
-	_res;								\
-})
-
-#define spa_node_add_listener(n,...)		spa_node_method(n, add_listener, 0, __VA_ARGS__)
-#define spa_node_set_callbacks(n,...)		spa_node_method(n, set_callbacks, 0, __VA_ARGS__)
-#define spa_node_sync(n,...)			spa_node_method(n, sync, 0, __VA_ARGS__)
-#define spa_node_enum_params(n,...)		spa_node_method(n, enum_params, 0, __VA_ARGS__)
-#define spa_node_set_param(n,...)		spa_node_method(n, set_param, 0, __VA_ARGS__)
-#define spa_node_set_io(n,...)			spa_node_method(n, set_io, 0, __VA_ARGS__)
-#define spa_node_send_command(n,...)		spa_node_method(n, send_command, 0, __VA_ARGS__)
-#define spa_node_add_port(n,...)		spa_node_method(n, add_port, 0, __VA_ARGS__)
-#define spa_node_remove_port(n,...)		spa_node_method(n, remove_port, 0, __VA_ARGS__)
-#define spa_node_port_enum_params(n,...)	spa_node_method(n, port_enum_params, 0, __VA_ARGS__)
-#define spa_node_port_set_param(n,...)		spa_node_method(n, port_set_param, 0, __VA_ARGS__)
-#define spa_node_port_use_buffers(n,...)	spa_node_method(n, port_use_buffers, 0, __VA_ARGS__)
-#define spa_node_port_set_io(n,...)		spa_node_method(n, port_set_io, 0, __VA_ARGS__)
-
-#define spa_node_port_reuse_buffer(n,...)	spa_node_method(n, port_reuse_buffer, 0, __VA_ARGS__)
-#define spa_node_port_reuse_buffer_fast(n,...)	spa_node_method_fast(n, port_reuse_buffer, 0, __VA_ARGS__)
-#define spa_node_process(n)			spa_node_method(n, process, 0)
-#define spa_node_process_fast(n)		spa_node_method_fast(n, process, 0)
+
+SPA_API_NODE int spa_node_add_listener(struct spa_node *object,
+			struct spa_hook *listener,
+			const struct spa_node_events *events,
+			void *data)
+{
+	return spa_api_method_r(int, -ENOTSUP, spa_node, &object->iface, add_listener, 0,
+			listener, events, data);
+}
+SPA_API_NODE int spa_node_set_callbacks(struct spa_node *object,
+			      const struct spa_node_callbacks *callbacks,
+			      void *data)
+{
+	return spa_api_method_r(int, -ENOTSUP, spa_node, &object->iface, set_callbacks, 0,
+			callbacks, data);
+}
+SPA_API_NODE int spa_node_sync(struct spa_node *object, int seq)
+{
+	return spa_api_method_r(int, -ENOTSUP, spa_node, &object->iface, sync, 0,
+			seq);
+}
+SPA_API_NODE int spa_node_enum_params(struct spa_node *object, int seq,
+			    uint32_t id, uint32_t start, uint32_t max,
+			    const struct spa_pod *filter)
+{
+	return spa_api_method_r(int, -ENOTSUP, spa_node, &object->iface, enum_params, 0,
+			seq, id, start, max, filter);
+}
+SPA_API_NODE int spa_node_set_param(struct spa_node *object,
+			  uint32_t id, uint32_t flags,
+			  const struct spa_pod *param)
+{
+	return spa_api_method_r(int, -ENOTSUP, spa_node, &object->iface, set_param, 0,
+			id, flags, param);
+}
+SPA_API_NODE int spa_node_set_io(struct spa_node *object,
+		       uint32_t id, void *data, size_t size)
+{
+	return spa_api_method_r(int, -ENOTSUP, spa_node, &object->iface, set_io, 0,
+			id, data, size);
+}
+SPA_API_NODE int spa_node_send_command(struct spa_node *object,
+		const struct spa_command *command)
+{
+	return spa_api_method_r(int, -ENOTSUP, spa_node, &object->iface, send_command, 0,
+			command);
+}
+SPA_API_NODE int spa_node_add_port(struct spa_node *object,
+			enum spa_direction direction, uint32_t port_id,
+			const struct spa_dict *props)
+{
+	return spa_api_method_r(int, -ENOTSUP, spa_node, &object->iface, add_port, 0,
+			direction, port_id, props);
+}
+SPA_API_NODE int spa_node_remove_port(struct spa_node *object,
+			enum spa_direction direction, uint32_t port_id)
+{
+	return spa_api_method_r(int, -ENOTSUP, spa_node, &object->iface, remove_port, 0,
+			direction, port_id);
+}
+SPA_API_NODE int spa_node_port_enum_params(struct spa_node *object, int seq,
+				 enum spa_direction direction, uint32_t port_id,
+				 uint32_t id, uint32_t start, uint32_t max,
+				 const struct spa_pod *filter)
+{
+	return spa_api_method_r(int, -ENOTSUP, spa_node, &object->iface, port_enum_params, 0,
+			seq, direction, port_id, id, start, max, filter);
+}
+SPA_API_NODE int spa_node_port_set_param(struct spa_node *object,
+			       enum spa_direction direction,
+			       uint32_t port_id,
+			       uint32_t id, uint32_t flags,
+			       const struct spa_pod *param)
+{
+	return spa_api_method_r(int, -ENOTSUP, spa_node, &object->iface, port_set_param, 0,
+			direction, port_id, id, flags, param);
+}
+SPA_API_NODE int spa_node_port_use_buffers(struct spa_node *object,
+				 enum spa_direction direction,
+				 uint32_t port_id,
+				 uint32_t flags,
+				 struct spa_buffer **buffers,
+				 uint32_t n_buffers)
+{
+	return spa_api_method_r(int, -ENOTSUP, spa_node, &object->iface, port_use_buffers, 0,
+			direction, port_id, flags, buffers, n_buffers);
+}
+SPA_API_NODE int spa_node_port_set_io(struct spa_node *object,
+			    enum spa_direction direction,
+			    uint32_t port_id,
+			    uint32_t id, void *data, size_t size)
+{
+	return spa_api_method_r(int, -ENOTSUP, spa_node, &object->iface, port_set_io, 0,
+			direction, port_id, id, data, size);
+}
+
+SPA_API_NODE int spa_node_port_reuse_buffer(struct spa_node *object, uint32_t port_id, uint32_t buffer_id)
+{
+	return spa_api_method_r(int, -ENOTSUP, spa_node, &object->iface, port_reuse_buffer, 0,
+			port_id, buffer_id);
+}
+SPA_API_NODE int spa_node_port_reuse_buffer_fast(struct spa_node *object, uint32_t port_id, uint32_t buffer_id)
+{
+	return spa_api_method_fast_r(int, -ENOTSUP, spa_node, &object->iface, port_reuse_buffer, 0,
+			port_id, buffer_id);
+}
+SPA_API_NODE int spa_node_process(struct spa_node *object)
+{
+	return spa_api_method_r(int, -ENOTSUP, spa_node, &object->iface, process, 0);
+}
+SPA_API_NODE int spa_node_process_fast(struct spa_node *object)
+{
+	return spa_api_method_fast_r(int, -ENOTSUP, spa_node, &object->iface, process, 0);
+}
 
 /**
  * \}
diff --git a/spa/include/spa/node/utils.h b/spa/include/spa/node/utils.h
index 01d249ab..b7724e92 100644
--- a/spa/include/spa/node/utils.h
+++ b/spa/include/spa/node/utils.h
@@ -18,12 +18,20 @@ extern "C" {
 
 #include <spa/node/node.h>
 
+#ifndef SPA_API_NODE_UTILS
+ #ifdef SPA_API_IMPL
+  #define SPA_API_NODE_UTILS SPA_API_IMPL
+ #else
+  #define SPA_API_NODE_UTILS static inline
+ #endif
+#endif
+
 struct spa_result_node_params_data {
 	struct spa_pod_builder *builder;
 	struct spa_result_node_params data;
 };
 
-static inline void spa_result_func_node_params(void *data,
+SPA_API_NODE_UTILS void spa_result_func_node_params(void *data,
 		int seq SPA_UNUSED, int res SPA_UNUSED, uint32_t type SPA_UNUSED, const void *result)
 {
 	struct spa_result_node_params_data *d =
@@ -37,7 +45,7 @@ static inline void spa_result_func_node_params(void *data,
 	d->data.param = spa_pod_builder_deref(d->builder, offset);
 }
 
-static inline int spa_node_enum_params_sync(struct spa_node *node,
+SPA_API_NODE_UTILS int spa_node_enum_params_sync(struct spa_node *node,
 			uint32_t id, uint32_t *index,
 			const struct spa_pod *filter,
 			struct spa_pod **param,
@@ -70,7 +78,7 @@ static inline int spa_node_enum_params_sync(struct spa_node *node,
 	return res;
 }
 
-static inline int spa_node_port_enum_params_sync(struct spa_node *node,
+SPA_API_NODE_UTILS int spa_node_port_enum_params_sync(struct spa_node *node,
 			enum spa_direction direction, uint32_t port_id,
 			uint32_t id, uint32_t *index,
 			const struct spa_pod *filter,
diff --git a/spa/include/spa/param/audio/aac-utils.h b/spa/include/spa/param/audio/aac-utils.h
index 07d436ac..01f226bb 100644
--- a/spa/include/spa/param/audio/aac-utils.h
+++ b/spa/include/spa/param/audio/aac-utils.h
@@ -19,7 +19,15 @@ extern "C" {
 #include <spa/param/audio/format.h>
 #include <spa/param/format-utils.h>
 
-static inline int
+#ifndef SPA_API_AUDIO_AAC_UTILS
+ #ifdef SPA_API_IMPL
+  #define SPA_API_AUDIO_AAC_UTILS SPA_API_IMPL
+ #else
+  #define SPA_API_AUDIO_AAC_UTILS static inline
+ #endif
+#endif
+
+SPA_API_AUDIO_AAC_UTILS int
 spa_format_audio_aac_parse(const struct spa_pod *format, struct spa_audio_info_aac *info)
 {
 	int res;
@@ -33,7 +41,7 @@ spa_format_audio_aac_parse(const struct spa_pod *format, struct spa_audio_info_a
 	return res;
 }
 
-static inline struct spa_pod *
+SPA_API_AUDIO_AAC_UTILS struct spa_pod *
 spa_format_audio_aac_build(struct spa_pod_builder *builder, uint32_t id,
 			   const struct spa_audio_info_aac *info)
 {
diff --git a/spa/include/spa/param/audio/alac-utils.h b/spa/include/spa/param/audio/alac-utils.h
index 262c2af3..898a84e5 100644
--- a/spa/include/spa/param/audio/alac-utils.h
+++ b/spa/include/spa/param/audio/alac-utils.h
@@ -19,7 +19,16 @@ extern "C" {
 #include <spa/param/audio/format.h>
 #include <spa/param/format-utils.h>
 
-static inline int
+#ifndef SPA_API_AUDIO_ALAC_UTILS
+ #ifdef SPA_API_IMPL
+  #define SPA_API_AUDIO_ALAC_UTILS SPA_API_IMPL
+ #else
+  #define SPA_API_AUDIO_ALAC_UTILS static inline
+ #endif
+#endif
+
+
+SPA_API_AUDIO_ALAC_UTILS int
 spa_format_audio_alac_parse(const struct spa_pod *format, struct spa_audio_info_alac *info)
 {
 	int res;
@@ -30,7 +39,7 @@ spa_format_audio_alac_parse(const struct spa_pod *format, struct spa_audio_info_
 	return res;
 }
 
-static inline struct spa_pod *
+SPA_API_AUDIO_ALAC_UTILS struct spa_pod *
 spa_format_audio_alac_build(struct spa_pod_builder *builder, uint32_t id,
 			    const struct spa_audio_info_alac *info)
 {
diff --git a/spa/include/spa/param/audio/amr-utils.h b/spa/include/spa/param/audio/amr-utils.h
index 3ecbda8f..cfe6aa5d 100644
--- a/spa/include/spa/param/audio/amr-utils.h
+++ b/spa/include/spa/param/audio/amr-utils.h
@@ -19,7 +19,16 @@ extern "C" {
 #include <spa/param/audio/format.h>
 #include <spa/param/format-utils.h>
 
-static inline int
+#ifndef SPA_API_AUDIO_AMR_UTILS
+ #ifdef SPA_API_IMPL
+  #define SPA_API_AUDIO_AMR_UTILS SPA_API_IMPL
+ #else
+  #define SPA_API_AUDIO_AMR_UTILS static inline
+ #endif
+#endif
+
+
+SPA_API_AUDIO_AMR_UTILS int
 spa_format_audio_amr_parse(const struct spa_pod *format, struct spa_audio_info_amr *info)
 {
 	int res;
@@ -31,7 +40,7 @@ spa_format_audio_amr_parse(const struct spa_pod *format, struct spa_audio_info_a
 	return res;
 }
 
-static inline struct spa_pod *
+SPA_API_AUDIO_AMR_UTILS struct spa_pod *
 spa_format_audio_amr_build(struct spa_pod_builder *builder, uint32_t id,
 			   const struct spa_audio_info_amr *info)
 {
diff --git a/spa/include/spa/param/audio/ape-utils.h b/spa/include/spa/param/audio/ape-utils.h
index 76ae6d89..d05c596c 100644
--- a/spa/include/spa/param/audio/ape-utils.h
+++ b/spa/include/spa/param/audio/ape-utils.h
@@ -19,7 +19,15 @@ extern "C" {
 #include <spa/param/audio/format.h>
 #include <spa/param/format-utils.h>
 
-static inline int
+#ifndef SPA_API_AUDIO_APE_UTILS
+ #ifdef SPA_API_IMPL
+  #define SPA_API_AUDIO_APE_UTILS SPA_API_IMPL
+ #else
+  #define SPA_API_AUDIO_APE_UTILS static inline
+ #endif
+#endif
+
+SPA_API_AUDIO_APE_UTILS int
 spa_format_audio_ape_parse(const struct spa_pod *format, struct spa_audio_info_ape *info)
 {
 	int res;
@@ -30,7 +38,7 @@ spa_format_audio_ape_parse(const struct spa_pod *format, struct spa_audio_info_a
 	return res;
 }
 
-static inline struct spa_pod *
+SPA_API_AUDIO_APE_UTILS struct spa_pod *
 spa_format_audio_ape_build(struct spa_pod_builder *builder, uint32_t id,
 			   const struct spa_audio_info_ape *info)
 {
diff --git a/spa/include/spa/param/audio/dsd-utils.h b/spa/include/spa/param/audio/dsd-utils.h
index 5f4585aa..3f7065b2 100644
--- a/spa/include/spa/param/audio/dsd-utils.h
+++ b/spa/include/spa/param/audio/dsd-utils.h
@@ -19,7 +19,15 @@ extern "C" {
 #include <spa/param/format-utils.h>
 #include <spa/param/audio/format.h>
 
-static inline int
+#ifndef SPA_API_AUDIO_DSD_UTILS
+ #ifdef SPA_API_IMPL
+  #define SPA_API_AUDIO_DSD_UTILS SPA_API_IMPL
+ #else
+  #define SPA_API_AUDIO_DSD_UTILS static inline
+ #endif
+#endif
+
+SPA_API_AUDIO_DSD_UTILS int
 spa_format_audio_dsd_parse(const struct spa_pod *format, struct spa_audio_info_dsd *info)
 {
 	struct spa_pod *position = NULL;
@@ -39,7 +47,7 @@ spa_format_audio_dsd_parse(const struct spa_pod *format, struct spa_audio_info_d
 	return res;
 }
 
-static inline struct spa_pod *
+SPA_API_AUDIO_DSD_UTILS struct spa_pod *
 spa_format_audio_dsd_build(struct spa_pod_builder *builder, uint32_t id,
 			   const struct spa_audio_info_dsd *info)
 {
diff --git a/spa/include/spa/param/audio/dsp-utils.h b/spa/include/spa/param/audio/dsp-utils.h
index 621c370c..af107f1e 100644
--- a/spa/include/spa/param/audio/dsp-utils.h
+++ b/spa/include/spa/param/audio/dsp-utils.h
@@ -19,7 +19,15 @@ extern "C" {
 #include <spa/param/audio/format.h>
 #include <spa/param/format-utils.h>
 
-static inline int
+#ifndef SPA_API_AUDIO_DSP_UTILS
+ #ifdef SPA_API_IMPL
+  #define SPA_API_AUDIO_DSP_UTILS SPA_API_IMPL
+ #else
+  #define SPA_API_AUDIO_DSP_UTILS static inline
+ #endif
+#endif
+
+SPA_API_AUDIO_DSP_UTILS int
 spa_format_audio_dsp_parse(const struct spa_pod *format, struct spa_audio_info_dsp *info)
 {
 	int res;
@@ -29,7 +37,7 @@ spa_format_audio_dsp_parse(const struct spa_pod *format, struct spa_audio_info_d
 	return res;
 }
 
-static inline struct spa_pod *
+SPA_API_AUDIO_DSP_UTILS struct spa_pod *
 spa_format_audio_dsp_build(struct spa_pod_builder *builder, uint32_t id,
 			   const struct spa_audio_info_dsp *info)
 {
diff --git a/spa/include/spa/param/audio/flac-utils.h b/spa/include/spa/param/audio/flac-utils.h
index 007436c9..bc3d8afc 100644
--- a/spa/include/spa/param/audio/flac-utils.h
+++ b/spa/include/spa/param/audio/flac-utils.h
@@ -19,7 +19,15 @@ extern "C" {
 #include <spa/param/audio/format.h>
 #include <spa/param/format-utils.h>
 
-static inline int
+#ifndef SPA_API_AUDIO_FLAC_UTILS
+ #ifdef SPA_API_IMPL
+  #define SPA_API_AUDIO_FLAC_UTILS SPA_API_IMPL
+ #else
+  #define SPA_API_AUDIO_FLAC_UTILS static inline
+ #endif
+#endif
+
+SPA_API_AUDIO_FLAC_UTILS int
 spa_format_audio_flac_parse(const struct spa_pod *format, struct spa_audio_info_flac *info)
 {
 	int res;
@@ -30,7 +38,7 @@ spa_format_audio_flac_parse(const struct spa_pod *format, struct spa_audio_info_
 	return res;
 }
 
-static inline struct spa_pod *
+SPA_API_AUDIO_FLAC_UTILS struct spa_pod *
 spa_format_audio_flac_build(struct spa_pod_builder *builder, uint32_t id,
 			    const struct spa_audio_info_flac *info)
 {
diff --git a/spa/include/spa/param/audio/format-utils.h b/spa/include/spa/param/audio/format-utils.h
index 1fad6292..4ff40faa 100644
--- a/spa/include/spa/param/audio/format-utils.h
+++ b/spa/include/spa/param/audio/format-utils.h
@@ -34,7 +34,15 @@ extern "C" {
  * \{
  */
 
-static inline int
+#ifndef SPA_API_AUDIO_FORMAT_UTILS
+ #ifdef SPA_API_IMPL
+  #define SPA_API_AUDIO_FORMAT_UTILS SPA_API_IMPL
+ #else
+  #define SPA_API_AUDIO_FORMAT_UTILS static inline
+ #endif
+#endif
+
+SPA_API_AUDIO_FORMAT_UTILS int
 spa_format_audio_parse(const struct spa_pod *format, struct spa_audio_info *info)
 {
 	int res;
@@ -76,7 +84,7 @@ spa_format_audio_parse(const struct spa_pod *format, struct spa_audio_info *info
 	return -ENOTSUP;
 }
 
-static inline struct spa_pod *
+SPA_API_AUDIO_FORMAT_UTILS struct spa_pod *
 spa_format_audio_build(struct spa_pod_builder *builder, uint32_t id,
 		       const struct spa_audio_info *info)
 {
diff --git a/spa/include/spa/param/audio/iec958-types.h b/spa/include/spa/param/audio/iec958-types.h
index 6388a24f..adcffdc9 100644
--- a/spa/include/spa/param/audio/iec958-types.h
+++ b/spa/include/spa/param/audio/iec958-types.h
@@ -17,6 +17,14 @@ extern "C" {
  * \{
  */
 
+#ifndef SPA_API_AUDIO_IEC958_TYPES
+ #ifdef SPA_API_IMPL
+  #define SPA_API_AUDIO_IEC958_TYPES SPA_API_IMPL
+ #else
+  #define SPA_API_AUDIO_IEC958_TYPES static inline
+ #endif
+#endif
+
 #define SPA_TYPE_INFO_AudioIEC958Codec		SPA_TYPE_INFO_ENUM_BASE "AudioIEC958Codec"
 #define SPA_TYPE_INFO_AUDIO_IEC958_CODEC_BASE	SPA_TYPE_INFO_AudioIEC958Codec ":"
 
@@ -33,6 +41,14 @@ static const struct spa_type_info spa_type_audio_iec958_codec[] = {
 	{ 0, 0, NULL, NULL },
 };
 
+SPA_API_AUDIO_IEC958_TYPES uint32_t spa_type_audio_iec958_codec_from_short_name(const char *name)
+{
+	return spa_type_from_short_name(name, spa_type_audio_iec958_codec, SPA_AUDIO_IEC958_CODEC_UNKNOWN);
+}
+SPA_API_AUDIO_IEC958_TYPES const char * spa_type_audio_iec958_codec_to_short_name(uint32_t type)
+{
+	return spa_type_to_short_name(type, spa_type_audio_iec958_codec, "UNKNOWN");
+}
 /**
  * \}
  */
diff --git a/spa/include/spa/param/audio/iec958-utils.h b/spa/include/spa/param/audio/iec958-utils.h
index 56bade23..1c4ec105 100644
--- a/spa/include/spa/param/audio/iec958-utils.h
+++ b/spa/include/spa/param/audio/iec958-utils.h
@@ -19,7 +19,15 @@ extern "C" {
 #include <spa/param/audio/format.h>
 #include <spa/param/format-utils.h>
 
-static inline int
+#ifndef SPA_API_AUDIO_IEC958_UTILS
+ #ifdef SPA_API_IMPL
+  #define SPA_API_AUDIO_IEC958_UTILS SPA_API_IMPL
+ #else
+  #define SPA_API_AUDIO_IEC958_UTILS static inline
+ #endif
+#endif
+
+SPA_API_AUDIO_IEC958_UTILS int
 spa_format_audio_iec958_parse(const struct spa_pod *format, struct spa_audio_info_iec958 *info)
 {
 	int res;
@@ -30,7 +38,7 @@ spa_format_audio_iec958_parse(const struct spa_pod *format, struct spa_audio_inf
 	return res;
 }
 
-static inline struct spa_pod *
+SPA_API_AUDIO_IEC958_UTILS struct spa_pod *
 spa_format_audio_iec958_build(struct spa_pod_builder *builder, uint32_t id,
 			      const struct spa_audio_info_iec958 *info)
 {
diff --git a/spa/include/spa/param/audio/layout.h b/spa/include/spa/param/audio/layout.h
index 8b05ba03..545ceae3 100644
--- a/spa/include/spa/param/audio/layout.h
+++ b/spa/include/spa/param/audio/layout.h
@@ -9,9 +9,7 @@
 extern "C" {
 #endif
 
-#if !defined(__FreeBSD__) && !defined(__MidnightBSD__)
-#include <endian.h>
-#endif
+#include <spa/utils/endian.h>
 
 /**
  * \addtogroup spa_param
diff --git a/spa/include/spa/param/audio/mp3-utils.h b/spa/include/spa/param/audio/mp3-utils.h
index 4c1ace26..481000e2 100644
--- a/spa/include/spa/param/audio/mp3-utils.h
+++ b/spa/include/spa/param/audio/mp3-utils.h
@@ -19,7 +19,15 @@ extern "C" {
 #include <spa/param/audio/format.h>
 #include <spa/param/format-utils.h>
 
-static inline int
+#ifndef SPA_API_AUDIO_MP3_UTILS
+ #ifdef SPA_API_IMPL
+  #define SPA_API_AUDIO_MP3_UTILS SPA_API_IMPL
+ #else
+  #define SPA_API_AUDIO_MP3_UTILS static inline
+ #endif
+#endif
+
+SPA_API_AUDIO_MP3_UTILS int
 spa_format_audio_mp3_parse(const struct spa_pod *format, struct spa_audio_info_mp3 *info)
 {
 	int res;
@@ -30,7 +38,7 @@ spa_format_audio_mp3_parse(const struct spa_pod *format, struct spa_audio_info_m
 	return res;
 }
 
-static inline struct spa_pod *
+SPA_API_AUDIO_MP3_UTILS struct spa_pod *
 spa_format_audio_mp3_build(struct spa_pod_builder *builder, uint32_t id,
 			   const struct spa_audio_info_mp3 *info)
 {
diff --git a/spa/include/spa/param/audio/ra-utils.h b/spa/include/spa/param/audio/ra-utils.h
index 0f267fb1..79e96514 100644
--- a/spa/include/spa/param/audio/ra-utils.h
+++ b/spa/include/spa/param/audio/ra-utils.h
@@ -19,7 +19,15 @@ extern "C" {
 #include <spa/param/audio/format.h>
 #include <spa/param/format-utils.h>
 
-static inline int
+#ifndef SPA_API_AUDIO_RA_UTILS
+ #ifdef SPA_API_IMPL
+  #define SPA_API_AUDIO_RA_UTILS SPA_API_IMPL
+ #else
+  #define SPA_API_AUDIO_RA_UTILS static inline
+ #endif
+#endif
+
+SPA_API_AUDIO_RA_UTILS int
 spa_format_audio_ra_parse(const struct spa_pod *format, struct spa_audio_info_ra *info)
 {
 	int res;
@@ -30,7 +38,7 @@ spa_format_audio_ra_parse(const struct spa_pod *format, struct spa_audio_info_ra
 	return res;
 }
 
-static inline struct spa_pod *
+SPA_API_AUDIO_RA_UTILS struct spa_pod *
 spa_format_audio_ra_build(struct spa_pod_builder *builder, uint32_t id,
 			  const struct spa_audio_info_ra *info)
 {
diff --git a/spa/include/spa/param/audio/raw-json.h b/spa/include/spa/param/audio/raw-json.h
new file mode 100644
index 00000000..07f0e0c4
--- /dev/null
+++ b/spa/include/spa/param/audio/raw-json.h
@@ -0,0 +1,105 @@
+/* Simple Plugin API */
+/* SPDX-FileCopyrightText: Copyright © 2024 Wim Taymans */
+/* SPDX-License-Identifier: MIT */
+
+#ifndef SPA_AUDIO_RAW_JSON_H
+#define SPA_AUDIO_RAW_JSON_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \addtogroup spa_param
+ * \{
+ */
+
+#include <spa/utils/dict.h>
+#include <spa/utils/json.h>
+#include <spa/param/audio/raw.h>
+#include <spa/param/audio/raw-types.h>
+
+#ifndef SPA_API_AUDIO_RAW_JSON
+ #ifdef SPA_API_IMPL
+  #define SPA_API_AUDIO_RAW_JSON SPA_API_IMPL
+ #else
+  #define SPA_API_AUDIO_RAW_JSON static inline
+ #endif
+#endif
+
+SPA_API_AUDIO_RAW_JSON int
+spa_audio_parse_position(const char *str, size_t len,
+		uint32_t *position, uint32_t *n_channels)
+{
+	struct spa_json iter;
+        char v[256];
+	uint32_t channels = 0;
+
+        if (spa_json_begin_array_relax(&iter, str, len) <= 0)
+                return 0;
+
+        while (spa_json_get_string(&iter, v, sizeof(v)) > 0 &&
+		channels < SPA_AUDIO_MAX_CHANNELS) {
+                position[channels++] = spa_type_audio_channel_from_short_name(v);
+        }
+	*n_channels = channels;
+	return channels;
+}
+
+SPA_API_AUDIO_RAW_JSON int
+spa_audio_info_raw_update(struct spa_audio_info_raw *info, const char *key, const char *val, bool force)
+{
+	uint32_t v;
+	if (spa_streq(key, SPA_KEY_AUDIO_FORMAT)) {
+		if (force || info->format == 0)
+			info->format = (enum spa_audio_format)spa_type_audio_format_from_short_name(val);
+	} else if (spa_streq(key, SPA_KEY_AUDIO_RATE)) {
+		if (spa_atou32(val, &v, 0) && (force || info->rate == 0))
+			info->rate = v;
+	} else if (spa_streq(key, SPA_KEY_AUDIO_CHANNELS)) {
+		if (spa_atou32(val, &v, 0) && (force || info->channels == 0))
+			info->channels = SPA_MIN(v, SPA_AUDIO_MAX_CHANNELS);
+	} else if (spa_streq(key, SPA_KEY_AUDIO_POSITION)) {
+		if (force || info->channels == 0) {
+			if (spa_audio_parse_position(val, strlen(val), info->position, &info->channels) > 0)
+				SPA_FLAG_CLEAR(info->flags, SPA_AUDIO_FLAG_UNPOSITIONED);
+		}
+	}
+	return 0;
+}
+
+SPA_API_AUDIO_RAW_JSON int SPA_SENTINEL
+spa_audio_info_raw_init_dict_keys(struct spa_audio_info_raw *info,
+		const struct spa_dict *defaults,
+		const struct spa_dict *dict, ...)
+{
+	spa_zero(*info);
+	SPA_FLAG_SET(info->flags, SPA_AUDIO_FLAG_UNPOSITIONED);
+	if (dict) {
+		const char *val, *key;
+		va_list args;
+		va_start(args, dict);
+		while ((key = va_arg(args, const char *))) {
+			if ((val = spa_dict_lookup(dict, key)) == NULL)
+				continue;
+			spa_audio_info_raw_update(info, key, val, true);
+		}
+		va_end(args);
+	}
+	if (defaults) {
+		const struct spa_dict_item *it;
+		spa_dict_for_each(it, defaults)
+			spa_audio_info_raw_update(info, it->key, it->value, false);
+	}
+	return 0;
+}
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+}  /* extern "C" */
+#endif
+
+#endif /* SPA_AUDIO_RAW_JSON_H */
diff --git a/spa/include/spa/param/audio/raw-types.h b/spa/include/spa/param/audio/raw-types.h
index 5dd65d77..9aa9591c 100644
--- a/spa/include/spa/param/audio/raw-types.h
+++ b/spa/include/spa/param/audio/raw-types.h
@@ -15,8 +15,17 @@ extern "C" {
  */
 
 #include <spa/utils/type.h>
+#include <spa/utils/string.h>
 #include <spa/param/audio/raw.h>
 
+#ifndef SPA_API_AUDIO_RAW_TYPES
+ #ifdef SPA_API_IMPL
+  #define SPA_API_AUDIO_RAW_TYPES SPA_API_IMPL
+ #else
+  #define SPA_API_AUDIO_RAW_TYPES static inline
+ #endif
+#endif
+
 #define SPA_TYPE_INFO_AudioFormat		SPA_TYPE_INFO_ENUM_BASE "AudioFormat"
 #define SPA_TYPE_INFO_AUDIO_FORMAT_BASE		SPA_TYPE_INFO_AudioFormat ":"
 
@@ -128,6 +137,15 @@ static const struct spa_type_info spa_type_audio_format[] = {
 	{ 0, 0, NULL, NULL },
 };
 
+SPA_API_AUDIO_RAW_TYPES uint32_t spa_type_audio_format_from_short_name(const char *name)
+{
+	return spa_type_from_short_name(name, spa_type_audio_format, SPA_AUDIO_FORMAT_UNKNOWN);
+}
+SPA_API_AUDIO_RAW_TYPES const char * spa_type_audio_format_to_short_name(uint32_t type)
+{
+	return spa_type_to_short_name(type, spa_type_audio_format, "UNKNOWN");
+}
+
 #define SPA_TYPE_INFO_AudioFlags	SPA_TYPE_INFO_FLAGS_BASE "AudioFlags"
 #define SPA_TYPE_INFO_AUDIO_FLAGS_BASE	SPA_TYPE_INFO_AudioFlags ":"
 
@@ -247,6 +265,16 @@ static const struct spa_type_info spa_type_audio_channel[] = {
 	{ 0, 0, NULL, NULL },
 };
 
+SPA_API_AUDIO_RAW_TYPES uint32_t spa_type_audio_channel_from_short_name(const char *name)
+{
+	return spa_type_from_short_name(name, spa_type_audio_channel, SPA_AUDIO_CHANNEL_UNKNOWN);
+}
+SPA_API_AUDIO_RAW_TYPES const char * spa_type_audio_channel_to_short_name(uint32_t type)
+{
+	return spa_type_to_short_name(type, spa_type_audio_channel, "UNK");
+}
+
+
 /**
  * \}
  */
diff --git a/spa/include/spa/param/audio/raw-utils.h b/spa/include/spa/param/audio/raw-utils.h
index 8790c452..178e3dd1 100644
--- a/spa/include/spa/param/audio/raw-utils.h
+++ b/spa/include/spa/param/audio/raw-utils.h
@@ -19,7 +19,15 @@ extern "C" {
 #include <spa/param/audio/format.h>
 #include <spa/param/format-utils.h>
 
-static inline int
+#ifndef SPA_API_AUDIO_RAW_UTILS
+ #ifdef SPA_API_IMPL
+  #define SPA_API_AUDIO_RAW_UTILS SPA_API_IMPL
+ #else
+  #define SPA_API_AUDIO_RAW_UTILS static inline
+ #endif
+#endif
+
+SPA_API_AUDIO_RAW_UTILS int
 spa_format_audio_raw_parse(const struct spa_pod *format, struct spa_audio_info_raw *info)
 {
 	struct spa_pod *position = NULL;
@@ -38,7 +46,7 @@ spa_format_audio_raw_parse(const struct spa_pod *format, struct spa_audio_info_r
 	return res;
 }
 
-static inline struct spa_pod *
+SPA_API_AUDIO_RAW_UTILS struct spa_pod *
 spa_format_audio_raw_build(struct spa_pod_builder *builder, uint32_t id,
 			   const struct spa_audio_info_raw *info)
 {
diff --git a/spa/include/spa/param/audio/raw.h b/spa/include/spa/param/audio/raw.h
index a357d559..8bed3f8a 100644
--- a/spa/include/spa/param/audio/raw.h
+++ b/spa/include/spa/param/audio/raw.h
@@ -11,9 +11,7 @@ extern "C" {
 
 #include <stdint.h>
 
-#if !defined(__FreeBSD__) && !defined(__MidnightBSD__)
-#include <endian.h>
-#endif
+#include <spa/utils/endian.h>
 
 /**
  * \addtogroup spa_param
diff --git a/spa/include/spa/param/audio/vorbis-utils.h b/spa/include/spa/param/audio/vorbis-utils.h
index 9f3fee1d..bc901e61 100644
--- a/spa/include/spa/param/audio/vorbis-utils.h
+++ b/spa/include/spa/param/audio/vorbis-utils.h
@@ -19,7 +19,15 @@ extern "C" {
 #include <spa/param/audio/format.h>
 #include <spa/param/format-utils.h>
 
-static inline int
+#ifndef SPA_API_AUDIO_VORBIS_UTILS
+ #ifdef SPA_API_IMPL
+  #define SPA_API_AUDIO_VORBIS_UTILS SPA_API_IMPL
+ #else
+  #define SPA_API_AUDIO_VORBIS_UTILS static inline
+ #endif
+#endif
+
+SPA_API_AUDIO_VORBIS_UTILS int
 spa_format_audio_vorbis_parse(const struct spa_pod *format, struct spa_audio_info_vorbis *info)
 {
 	int res;
@@ -30,7 +38,7 @@ spa_format_audio_vorbis_parse(const struct spa_pod *format, struct spa_audio_inf
 	return res;
 }
 
-static inline struct spa_pod *
+SPA_API_AUDIO_VORBIS_UTILS struct spa_pod *
 spa_format_audio_vorbis_build(struct spa_pod_builder *builder, uint32_t id,
 			      const struct spa_audio_info_vorbis *info)
 {
diff --git a/spa/include/spa/param/audio/wma-utils.h b/spa/include/spa/param/audio/wma-utils.h
index 9517e122..ca15f7d0 100644
--- a/spa/include/spa/param/audio/wma-utils.h
+++ b/spa/include/spa/param/audio/wma-utils.h
@@ -19,7 +19,15 @@ extern "C" {
 #include <spa/param/audio/format.h>
 #include <spa/param/format-utils.h>
 
-static inline int
+#ifndef SPA_API_AUDIO_WMA_UTILS
+ #ifdef SPA_API_IMPL
+  #define SPA_API_AUDIO_WMA_UTILS SPA_API_IMPL
+ #else
+  #define SPA_API_AUDIO_WMA_UTILS static inline
+ #endif
+#endif
+
+SPA_API_AUDIO_WMA_UTILS int
 spa_format_audio_wma_parse(const struct spa_pod *format, struct spa_audio_info_wma *info)
 {
 	int res;
@@ -33,7 +41,7 @@ spa_format_audio_wma_parse(const struct spa_pod *format, struct spa_audio_info_w
 	return res;
 }
 
-static inline struct spa_pod *
+SPA_API_AUDIO_WMA_UTILS struct spa_pod *
 spa_format_audio_wma_build(struct spa_pod_builder *builder, uint32_t id,
 			   const struct spa_audio_info_wma *info)
 {
diff --git a/spa/include/spa/param/bluetooth/audio.h b/spa/include/spa/param/bluetooth/audio.h
index abdf7ccb..c95e22d6 100644
--- a/spa/include/spa/param/bluetooth/audio.h
+++ b/spa/include/spa/param/bluetooth/audio.h
@@ -44,6 +44,9 @@ enum spa_bluetooth_audio_codec {
 
 	/* BAP */
 	SPA_BLUETOOTH_AUDIO_CODEC_LC3 = 0x200,
+
+	/* ASHA */
+	SPA_BLUETOOTH_AUDIO_CODEC_G722 = 0x300,
 };
 
 /**
diff --git a/spa/include/spa/param/bluetooth/type-info.h b/spa/include/spa/param/bluetooth/type-info.h
index 878c5b9b..7d9cd365 100644
--- a/spa/include/spa/param/bluetooth/type-info.h
+++ b/spa/include/spa/param/bluetooth/type-info.h
@@ -47,6 +47,8 @@ static const struct spa_type_info spa_type_bluetooth_audio_codec[] = {
 
 	{ SPA_BLUETOOTH_AUDIO_CODEC_LC3, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "lc3", NULL },
 
+	{ SPA_BLUETOOTH_AUDIO_CODEC_G722, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "g722", NULL },
+
 	{ 0, 0, NULL, NULL },
 };
 
diff --git a/spa/include/spa/param/format-types.h b/spa/include/spa/param/format-types.h
index b7f52d71..8daaa103 100644
--- a/spa/include/spa/param/format-types.h
+++ b/spa/include/spa/param/format-types.h
@@ -19,6 +19,7 @@ extern "C" {
 
 #include <spa/param/audio/type-info.h>
 #include <spa/param/video/type-info.h>
+#include <spa/control/type-info.h>
 
 #define SPA_TYPE_INFO_Format			SPA_TYPE_INFO_PARAM_BASE "Format"
 #define SPA_TYPE_INFO_FORMAT_BASE		SPA_TYPE_INFO_Format ":"
@@ -103,6 +104,9 @@ static const struct spa_type_info spa_type_media_subtype[] = {
 #define SPA_TYPE_INFO_FORMAT_VIDEO_H264		SPA_TYPE_INFO_FORMAT_VIDEO_BASE "H264"
 #define SPA_TYPE_INFO_FORMAT_VIDEO_H264_BASE	SPA_TYPE_INFO_FORMAT_VIDEO_H264 ":"
 
+#define SPA_TYPE_INFO_FormatControl		SPA_TYPE_INFO_FORMAT_BASE "Control"
+#define SPA_TYPE_INFO_FORMAT_CONTROL_BASE	SPA_TYPE_INFO_FormatControl ":"
+
 static const struct spa_type_info spa_type_format[] = {
 	{ SPA_FORMAT_START, SPA_TYPE_Id, SPA_TYPE_INFO_FORMAT_BASE, spa_type_param, },
 
@@ -158,6 +162,8 @@ static const struct spa_type_info spa_type_format[] = {
 
 	{ SPA_FORMAT_VIDEO_H264_streamFormat, SPA_TYPE_Id, SPA_TYPE_INFO_FORMAT_VIDEO_H264_BASE "streamFormat", NULL },
 	{ SPA_FORMAT_VIDEO_H264_alignment, SPA_TYPE_Id, SPA_TYPE_INFO_FORMAT_VIDEO_H264_BASE "alignment", NULL },
+
+	{ SPA_FORMAT_CONTROL_types, SPA_TYPE_Id, SPA_TYPE_INFO_FORMAT_CONTROL_BASE "types", spa_type_control },
 	{ 0, 0, NULL, NULL },
 };
 
diff --git a/spa/include/spa/param/format-utils.h b/spa/include/spa/param/format-utils.h
index fb5cb39a..27fc5f58 100644
--- a/spa/include/spa/param/format-utils.h
+++ b/spa/include/spa/param/format-utils.h
@@ -18,7 +18,15 @@ extern "C" {
 #include <spa/pod/parser.h>
 #include <spa/param/format.h>
 
-static inline int
+#ifndef SPA_API_FORMAT_UTILS
+ #ifdef SPA_API_IMPL
+  #define SPA_API_FORMAT_UTILS SPA_API_IMPL
+ #else
+  #define SPA_API_FORMAT_UTILS static inline
+ #endif
+#endif
+
+SPA_API_FORMAT_UTILS int
 spa_format_parse(const struct spa_pod *format, uint32_t *media_type, uint32_t *media_subtype)
 {
 	return spa_pod_parse_object(format,
diff --git a/spa/include/spa/param/format.h b/spa/include/spa/param/format.h
index 3f075fd3..eb3b851b 100644
--- a/spa/include/spa/param/format.h
+++ b/spa/include/spa/param/format.h
@@ -141,6 +141,8 @@ enum spa_format {
 	SPA_FORMAT_START_Stream = 0x50000,
 	/* Application Format keys */
 	SPA_FORMAT_START_Application = 0x60000,
+	SPA_FORMAT_CONTROL_types,		/**< possible control types (flags choice Int,
+						  *  mask of enum spa_control_type) */
 };
 
 #define SPA_KEY_FORMAT_DSP		"format.dsp"		/**< a predefined DSP format,
diff --git a/spa/include/spa/param/latency-utils.h b/spa/include/spa/param/latency-utils.h
index 83df433f..45f817eb 100644
--- a/spa/include/spa/param/latency-utils.h
+++ b/spa/include/spa/param/latency-utils.h
@@ -20,7 +20,15 @@ extern "C" {
 #include <spa/pod/parser.h>
 #include <spa/param/latency.h>
 
-static inline int
+#ifndef SPA_API_LATENCY_UTILS
+ #ifdef SPA_API_IMPL
+  #define SPA_API_LATENCY_UTILS SPA_API_IMPL
+ #else
+  #define SPA_API_LATENCY_UTILS static inline
+ #endif
+#endif
+
+SPA_API_LATENCY_UTILS int
 spa_latency_info_compare(const struct spa_latency_info *a, const struct spa_latency_info *b)
 {
 	if (a->min_quantum == b->min_quantum &&
@@ -33,29 +41,35 @@ spa_latency_info_compare(const struct spa_latency_info *a, const struct spa_late
 	return 1;
 }
 
-static inline void
+SPA_API_LATENCY_UTILS void
 spa_latency_info_combine_start(struct spa_latency_info *info, enum spa_direction direction)
 {
 	*info = SPA_LATENCY_INFO(direction,
 			.min_quantum = FLT_MAX,
-			.max_quantum = 0.0f,
-			.min_rate = UINT32_MAX,
-			.max_rate = 0,
-			.min_ns = UINT64_MAX,
-			.max_ns = 0);
+			.max_quantum = FLT_MIN,
+			.min_rate = INT32_MAX,
+			.max_rate = INT32_MIN,
+			.min_ns = INT64_MAX,
+			.max_ns = INT64_MIN);
 }
-static inline void
+SPA_API_LATENCY_UTILS void
 spa_latency_info_combine_finish(struct spa_latency_info *info)
 {
 	if (info->min_quantum == FLT_MAX)
 		info->min_quantum = 0;
-	if (info->min_rate == UINT32_MAX)
+	if (info->max_quantum == FLT_MIN)
+		info->max_quantum = 0;
+	if (info->min_rate == INT32_MAX)
 		info->min_rate = 0;
-	if (info->min_ns == UINT64_MAX)
+	if (info->max_rate == INT32_MIN)
+		info->max_rate = 0;
+	if (info->min_ns == INT64_MAX)
 		info->min_ns = 0;
+	if (info->max_ns == INT64_MIN)
+		info->max_ns = 0;
 }
 
-static inline int
+SPA_API_LATENCY_UTILS int
 spa_latency_info_combine(struct spa_latency_info *info, const struct spa_latency_info *other)
 {
 	if (info->direction != other->direction)
@@ -75,7 +89,7 @@ spa_latency_info_combine(struct spa_latency_info *info, const struct spa_latency
 	return 0;
 }
 
-static inline int
+SPA_API_LATENCY_UTILS int
 spa_latency_parse(const struct spa_pod *latency, struct spa_latency_info *info)
 {
 	int res;
@@ -94,7 +108,7 @@ spa_latency_parse(const struct spa_pod *latency, struct spa_latency_info *info)
 	return 0;
 }
 
-static inline struct spa_pod *
+SPA_API_LATENCY_UTILS struct spa_pod *
 spa_latency_build(struct spa_pod_builder *builder, uint32_t id, const struct spa_latency_info *info)
 {
 	return (struct spa_pod *)spa_pod_builder_add_object(builder,
@@ -108,7 +122,7 @@ spa_latency_build(struct spa_pod_builder *builder, uint32_t id, const struct spa
 			SPA_PARAM_LATENCY_maxNs, SPA_POD_Long(info->max_ns));
 }
 
-static inline int
+SPA_API_LATENCY_UTILS int
 spa_process_latency_parse(const struct spa_pod *latency, struct spa_process_latency_info *info)
 {
 	int res;
@@ -122,7 +136,7 @@ spa_process_latency_parse(const struct spa_pod *latency, struct spa_process_late
 	return 0;
 }
 
-static inline struct spa_pod *
+SPA_API_LATENCY_UTILS struct spa_pod *
 spa_process_latency_build(struct spa_pod_builder *builder, uint32_t id,
 		const struct spa_process_latency_info *info)
 {
@@ -133,7 +147,7 @@ spa_process_latency_build(struct spa_pod_builder *builder, uint32_t id,
 			SPA_PARAM_PROCESS_LATENCY_ns, SPA_POD_Long(info->ns));
 }
 
-static inline int
+SPA_API_LATENCY_UTILS int
 spa_process_latency_info_add(const struct spa_process_latency_info *process,
 		struct spa_latency_info *info)
 {
@@ -146,6 +160,17 @@ spa_process_latency_info_add(const struct spa_process_latency_info *process,
 	return 0;
 }
 
+SPA_API_LATENCY_UTILS int
+spa_process_latency_info_compare(const struct spa_process_latency_info *a,
+		const struct spa_process_latency_info *b)
+{
+	if (a->quantum == b->quantum &&
+	    a->rate == b->rate &&
+	    a->ns == b->ns)
+		return 0;
+	return 1;
+}
+
 /**
  * \}
  */
diff --git a/spa/include/spa/param/latency.h b/spa/include/spa/param/latency.h
index 456c8b6c..4087941c 100644
--- a/spa/include/spa/param/latency.h
+++ b/spa/include/spa/param/latency.h
@@ -50,10 +50,10 @@ struct spa_latency_info {
 	enum spa_direction direction;
 	float min_quantum;
 	float max_quantum;
-	uint32_t min_rate;
-	uint32_t max_rate;
-	uint64_t min_ns;
-	uint64_t max_ns;
+	int32_t min_rate;
+	int32_t max_rate;
+	int64_t min_ns;
+	int64_t max_ns;
 };
 
 #define SPA_LATENCY_INFO(dir,...) ((struct spa_latency_info) { .direction = (dir), ## __VA_ARGS__ })
@@ -74,8 +74,8 @@ enum spa_param_process_latency {
 /** Helper structure for managing process latency objects */
 struct spa_process_latency_info {
 	float quantum;
-	uint32_t rate;
-	uint64_t ns;
+	int32_t rate;
+	int64_t ns;
 };
 
 #define SPA_PROCESS_LATENCY_INFO_INIT(...)	((struct spa_process_latency_info) { __VA_ARGS__ })
diff --git a/spa/include/spa/param/param-types.h b/spa/include/spa/param/param-types.h
index 4bed3651..ebb8d988 100644
--- a/spa/include/spa/param/param-types.h
+++ b/spa/include/spa/param/param-types.h
@@ -55,6 +55,11 @@ static const struct spa_type_info spa_type_prop_float_array[] = {
 	{ 0, 0, NULL, NULL },
 };
 
+static const struct spa_type_info spa_type_prop_int_array[] = {
+	{ SPA_PROP_START, SPA_TYPE_Int, SPA_TYPE_INFO_BASE "intArray", NULL, },
+	{ 0, 0, NULL, NULL },
+};
+
 static const struct spa_type_info spa_type_prop_channel_map[] = {
 	{ SPA_PROP_START, SPA_TYPE_Id, SPA_TYPE_INFO_BASE "channelMap", spa_type_audio_channel, },
 	{ 0, 0, NULL, NULL },
diff --git a/spa/include/spa/param/profiler-types.h b/spa/include/spa/param/profiler-types.h
index 91cbf112..57f3f369 100644
--- a/spa/include/spa/param/profiler-types.h
+++ b/spa/include/spa/param/profiler-types.h
@@ -26,6 +26,7 @@ static const struct spa_type_info spa_type_profiler[] = {
 	{ SPA_PROFILER_clock, SPA_TYPE_Struct, SPA_TYPE_INFO_PROFILER_BASE "clock", NULL, },
 	{ SPA_PROFILER_driverBlock, SPA_TYPE_Struct, SPA_TYPE_INFO_PROFILER_BASE "driverBlock", NULL, },
 	{ SPA_PROFILER_followerBlock, SPA_TYPE_Struct, SPA_TYPE_INFO_PROFILER_BASE "followerBlock", NULL, },
+	{ SPA_PROFILER_followerClock, SPA_TYPE_Struct, SPA_TYPE_INFO_PROFILER_BASE "followerClock", NULL, },
 	{ 0, 0, NULL, NULL },
 };
 
diff --git a/spa/include/spa/param/profiler.h b/spa/include/spa/param/profiler.h
index 5d1c9453..36af0fc2 100644
--- a/spa/include/spa/param/profiler.h
+++ b/spa/include/spa/param/profiler.h
@@ -40,7 +40,9 @@ enum spa_profiler {
 							  *      Long : clock delay,
 							  *      Double : clock rate_diff,
 							  *      Long : clock next_nsec,
-							  *      Int : transport_state)) */
+							  *      Int : transport_state,
+							  *      Int : clock cycle,
+							  *      Long : xrun duration)) */
 	SPA_PROFILER_driverBlock,			/**< generic driver info block
 							  *  (Struct(
 							  *      Int : driver_id,
@@ -65,7 +67,18 @@ enum spa_profiler {
 							  *      Int : status,
 							  *      Fraction : latency,
 							  *      Int : xrun_count))  */
-
+	SPA_PROFILER_followerClock,			/**< follower clock information
+							  *  (Struct(
+							  *      Int : clock id,
+							  *      String: clock name,
+							  *      Long : clock nsec,
+							  *      Fraction : clock rate,
+							  *      Long : clock position,
+							  *      Long : clock duration,
+							  *      Long : clock delay,
+							  *      Double : clock rate_diff,
+							  *      Long : clock next_nsec,
+							  *      Long : xrun duration)) */
 	SPA_PROFILER_START_CUSTOM	= 0x1000000,
 };
 
diff --git a/spa/include/spa/param/props-types.h b/spa/include/spa/param/props-types.h
index e00f2f87..e6612599 100644
--- a/spa/include/spa/param/props-types.h
+++ b/spa/include/spa/param/props-types.h
@@ -64,14 +64,14 @@ static const struct spa_type_info spa_type_props[] = {
 	{ SPA_PROP_volumeRampStepTime, SPA_TYPE_Int, SPA_TYPE_INFO_PROPS_BASE "volumeRampStepTime", NULL },
 	{ SPA_PROP_volumeRampScale, SPA_TYPE_Id, SPA_TYPE_INFO_PROPS_BASE "volumeRampScale", NULL },
 
-	{ SPA_PROP_brightness, SPA_TYPE_Int, SPA_TYPE_INFO_PROPS_BASE "brightness", NULL },
-	{ SPA_PROP_contrast, SPA_TYPE_Int, SPA_TYPE_INFO_PROPS_BASE "contrast", NULL },
-	{ SPA_PROP_saturation, SPA_TYPE_Int, SPA_TYPE_INFO_PROPS_BASE "saturation", NULL },
+	{ SPA_PROP_brightness, SPA_TYPE_Float, SPA_TYPE_INFO_PROPS_BASE "brightness", NULL },
+	{ SPA_PROP_contrast, SPA_TYPE_Float, SPA_TYPE_INFO_PROPS_BASE "contrast", NULL },
+	{ SPA_PROP_saturation, SPA_TYPE_Float, SPA_TYPE_INFO_PROPS_BASE "saturation", NULL },
 	{ SPA_PROP_hue, SPA_TYPE_Int, SPA_TYPE_INFO_PROPS_BASE "hue", NULL },
 	{ SPA_PROP_gamma, SPA_TYPE_Int, SPA_TYPE_INFO_PROPS_BASE "gamma", NULL },
 	{ SPA_PROP_exposure, SPA_TYPE_Int, SPA_TYPE_INFO_PROPS_BASE "exposure", NULL },
-	{ SPA_PROP_gain, SPA_TYPE_Int, SPA_TYPE_INFO_PROPS_BASE "gain", NULL },
-	{ SPA_PROP_sharpness, SPA_TYPE_Int, SPA_TYPE_INFO_PROPS_BASE "sharpness", NULL },
+	{ SPA_PROP_gain, SPA_TYPE_Float, SPA_TYPE_INFO_PROPS_BASE "gain", NULL },
+	{ SPA_PROP_sharpness, SPA_TYPE_Float, SPA_TYPE_INFO_PROPS_BASE "sharpness", NULL },
 
 	{ SPA_PROP_params, SPA_TYPE_Struct, SPA_TYPE_INFO_PROPS_BASE "params", NULL },
 	{ 0, 0, NULL, NULL },
diff --git a/spa/include/spa/param/route-types.h b/spa/include/spa/param/route-types.h
index 619a9e2e..78ced495 100644
--- a/spa/include/spa/param/route-types.h
+++ b/spa/include/spa/param/route-types.h
@@ -32,9 +32,9 @@ static const struct spa_type_info spa_type_param_route[] = {
 	{ SPA_PARAM_ROUTE_priority, SPA_TYPE_Int, SPA_TYPE_INFO_PARAM_ROUTE_BASE "priority", NULL, },
 	{ SPA_PARAM_ROUTE_available, SPA_TYPE_Id, SPA_TYPE_INFO_PARAM_ROUTE_BASE "available", spa_type_param_availability, },
 	{ SPA_PARAM_ROUTE_info, SPA_TYPE_Struct, SPA_TYPE_INFO_PARAM_ROUTE_BASE "info", NULL, },
-	{ SPA_PARAM_ROUTE_profiles, SPA_TYPE_Int, SPA_TYPE_INFO_PARAM_ROUTE_BASE "profiles", NULL, },
+	{ SPA_PARAM_ROUTE_profiles, SPA_TYPE_Array, SPA_TYPE_INFO_PARAM_ROUTE_BASE "profiles", spa_type_prop_int_array, },
 	{ SPA_PARAM_ROUTE_props, SPA_TYPE_OBJECT_Props, SPA_TYPE_INFO_PARAM_ROUTE_BASE "props", NULL, },
-	{ SPA_PARAM_ROUTE_devices, SPA_TYPE_Int, SPA_TYPE_INFO_PARAM_ROUTE_BASE "devices", NULL, },
+	{ SPA_PARAM_ROUTE_devices, SPA_TYPE_Array, SPA_TYPE_INFO_PARAM_ROUTE_BASE "devices", spa_type_prop_int_array, },
 	{ SPA_PARAM_ROUTE_profile, SPA_TYPE_Int, SPA_TYPE_INFO_PARAM_ROUTE_BASE "profile", NULL, },
 	{ SPA_PARAM_ROUTE_save, SPA_TYPE_Bool, SPA_TYPE_INFO_PARAM_ROUTE_BASE "save", NULL, },
 	{ 0, 0, NULL, NULL },
diff --git a/spa/include/spa/param/tag-utils.h b/spa/include/spa/param/tag-utils.h
index 2bce7b19..ba8a952c 100644
--- a/spa/include/spa/param/tag-utils.h
+++ b/spa/include/spa/param/tag-utils.h
@@ -21,14 +21,22 @@ extern "C" {
 #include <spa/pod/parser.h>
 #include <spa/param/tag.h>
 
-static inline int
+#ifndef SPA_API_TAG_UTILS
+ #ifdef SPA_API_IMPL
+  #define SPA_API_TAG_UTILS SPA_API_IMPL
+ #else
+  #define SPA_API_TAG_UTILS static inline
+ #endif
+#endif
+
+SPA_API_TAG_UTILS int
 spa_tag_compare(const struct spa_pod *a, const struct spa_pod *b)
 {
 	return ((a == b) || (a && b && SPA_POD_SIZE(a) == SPA_POD_SIZE(b) &&
 	    memcmp(a, b, SPA_POD_SIZE(b)) == 0)) ? 0 : 1;
 }
 
-static inline int
+SPA_API_TAG_UTILS int
 spa_tag_parse(const struct spa_pod *tag, struct spa_tag_info *info, void **state)
 {
 	int res;
@@ -57,7 +65,7 @@ spa_tag_parse(const struct spa_pod *tag, struct spa_tag_info *info, void **state
 	return 0;
 }
 
-static inline int
+SPA_API_TAG_UTILS int
 spa_tag_info_parse(const struct spa_tag_info *info, struct spa_dict *dict, struct spa_dict_item *items)
 {
 	struct spa_pod_parser prs;
@@ -90,7 +98,7 @@ spa_tag_info_parse(const struct spa_tag_info *info, struct spa_dict *dict, struc
 	return 0;
 }
 
-static inline void
+SPA_API_TAG_UTILS void
 spa_tag_build_start(struct spa_pod_builder *builder, struct spa_pod_frame *f,
 		uint32_t id, enum spa_direction direction)
 {
@@ -100,7 +108,7 @@ spa_tag_build_start(struct spa_pod_builder *builder, struct spa_pod_frame *f,
 			0);
 }
 
-static inline void
+SPA_API_TAG_UTILS void
 spa_tag_build_add_info(struct spa_pod_builder *builder, const struct spa_pod *info)
 {
 	spa_pod_builder_add(builder,
@@ -108,7 +116,7 @@ spa_tag_build_add_info(struct spa_pod_builder *builder, const struct spa_pod *in
 			0);
 }
 
-static inline void
+SPA_API_TAG_UTILS void
 spa_tag_build_add_dict(struct spa_pod_builder *builder, const struct spa_dict *dict)
 {
 	uint32_t i, n_items;
@@ -126,7 +134,7 @@ spa_tag_build_add_dict(struct spa_pod_builder *builder, const struct spa_dict *d
         spa_pod_builder_pop(builder, &f);
 }
 
-static inline struct spa_pod *
+SPA_API_TAG_UTILS struct spa_pod *
 spa_tag_build_end(struct spa_pod_builder *builder, struct spa_pod_frame *f)
 {
 	return (struct spa_pod*)spa_pod_builder_pop(builder, f);
diff --git a/spa/include/spa/param/video/dsp-utils.h b/spa/include/spa/param/video/dsp-utils.h
index 193e3194..6e76309b 100644
--- a/spa/include/spa/param/video/dsp-utils.h
+++ b/spa/include/spa/param/video/dsp-utils.h
@@ -18,7 +18,15 @@ extern "C" {
 #include <spa/pod/builder.h>
 #include <spa/param/video/dsp.h>
 
-static inline int
+#ifndef SPA_API_VIDEO_DSP_UTILS
+ #ifdef SPA_API_IMPL
+  #define SPA_API_VIDEO_DSP_UTILS SPA_API_IMPL
+ #else
+  #define SPA_API_VIDEO_DSP_UTILS static inline
+ #endif
+#endif
+
+SPA_API_VIDEO_DSP_UTILS int
 spa_format_video_dsp_parse(const struct spa_pod *format,
 			   struct spa_video_info_dsp *info)
 {
@@ -36,7 +44,7 @@ spa_format_video_dsp_parse(const struct spa_pod *format,
 		SPA_FORMAT_VIDEO_modifier,		SPA_POD_OPT_Long(&info->modifier));
 }
 
-static inline struct spa_pod *
+SPA_API_VIDEO_DSP_UTILS struct spa_pod *
 spa_format_video_dsp_build(struct spa_pod_builder *builder, uint32_t id,
 			   const struct spa_video_info_dsp *info)
 {
diff --git a/spa/include/spa/param/video/format-utils.h b/spa/include/spa/param/video/format-utils.h
index a04b67b2..9efbef61 100644
--- a/spa/include/spa/param/video/format-utils.h
+++ b/spa/include/spa/param/video/format-utils.h
@@ -16,7 +16,15 @@ extern "C" {
 #include <spa/param/video/h264-utils.h>
 #include <spa/param/video/mjpg-utils.h>
 
-static inline int
+#ifndef SPA_API_VIDEO_FORMAT_UTILS
+ #ifdef SPA_API_IMPL
+  #define SPA_API_VIDEO_FORMAT_UTILS SPA_API_IMPL
+ #else
+  #define SPA_API_VIDEO_FORMAT_UTILS static inline
+ #endif
+#endif
+
+SPA_API_VIDEO_FORMAT_UTILS int
 spa_format_video_parse(const struct spa_pod *format, struct spa_video_info *info)
 {
 	int res;
@@ -40,7 +48,7 @@ spa_format_video_parse(const struct spa_pod *format, struct spa_video_info *info
 	return -ENOTSUP;
 }
 
-static inline struct spa_pod *
+SPA_API_VIDEO_FORMAT_UTILS struct spa_pod *
 spa_format_video_build(struct spa_pod_builder *builder, uint32_t id,
 		       const struct spa_video_info *info)
 {
diff --git a/spa/include/spa/param/video/h264-utils.h b/spa/include/spa/param/video/h264-utils.h
index fbf22c95..fa693329 100644
--- a/spa/include/spa/param/video/h264-utils.h
+++ b/spa/include/spa/param/video/h264-utils.h
@@ -18,7 +18,15 @@ extern "C" {
 #include <spa/pod/builder.h>
 #include <spa/param/video/h264.h>
 
-static inline int
+#ifndef SPA_API_VIDEO_H264_UTILS
+ #ifdef SPA_API_IMPL
+  #define SPA_API_VIDEO_H264_UTILS SPA_API_IMPL
+ #else
+  #define SPA_API_VIDEO_H264_UTILS static inline
+ #endif
+#endif
+
+SPA_API_VIDEO_H264_UTILS int
 spa_format_video_h264_parse(const struct spa_pod *format,
 			    struct spa_video_info_h264 *info)
 {
@@ -31,7 +39,7 @@ spa_format_video_h264_parse(const struct spa_pod *format,
 			SPA_FORMAT_VIDEO_H264_alignment,	SPA_POD_OPT_Id(&info->alignment));
 }
 
-static inline struct spa_pod *
+SPA_API_VIDEO_H264_UTILS struct spa_pod *
 spa_format_video_h264_build(struct spa_pod_builder *builder, uint32_t id,
 			    const struct spa_video_info_h264 *info)
 {
diff --git a/spa/include/spa/param/video/mjpg-utils.h b/spa/include/spa/param/video/mjpg-utils.h
index b6bec003..f1aa27af 100644
--- a/spa/include/spa/param/video/mjpg-utils.h
+++ b/spa/include/spa/param/video/mjpg-utils.h
@@ -18,7 +18,15 @@ extern "C" {
 #include <spa/pod/builder.h>
 #include <spa/param/video/mjpg.h>
 
-static inline int
+#ifndef SPA_API_VIDEO_MJPG_UTILS
+ #ifdef SPA_API_IMPL
+  #define SPA_API_VIDEO_MJPG_UTILS SPA_API_IMPL
+ #else
+  #define SPA_API_VIDEO_MJPG_UTILS static inline
+ #endif
+#endif
+
+SPA_API_VIDEO_MJPG_UTILS int
 spa_format_video_mjpg_parse(const struct spa_pod *format,
 			    struct spa_video_info_mjpg *info)
 {
@@ -29,7 +37,7 @@ spa_format_video_mjpg_parse(const struct spa_pod *format,
 			SPA_FORMAT_VIDEO_maxFramerate,	SPA_POD_OPT_Fraction(&info->max_framerate));
 }
 
-static inline struct spa_pod *
+SPA_API_VIDEO_MJPG_UTILS struct spa_pod *
 spa_format_video_mjpg_build(struct spa_pod_builder *builder, uint32_t id,
 			    const struct spa_video_info_mjpg *info)
 {
diff --git a/spa/include/spa/param/video/raw-types.h b/spa/include/spa/param/video/raw-types.h
index 30d144c7..bca0c8d4 100644
--- a/spa/include/spa/param/video/raw-types.h
+++ b/spa/include/spa/param/video/raw-types.h
@@ -16,11 +16,20 @@ extern "C" {
 #include <spa/utils/type.h>
 #include <spa/param/video/raw.h>
 
+#ifndef SPA_API_VIDEO_RAW_TYPES
+ #ifdef SPA_API_IMPL
+  #define SPA_API_VIDEO_RAW_TYPES SPA_API_IMPL
+ #else
+  #define SPA_API_VIDEO_RAW_TYPES static inline
+ #endif
+#endif
+
 #define SPA_TYPE_INFO_VideoFormat		SPA_TYPE_INFO_ENUM_BASE "VideoFormat"
 #define SPA_TYPE_INFO_VIDEO_FORMAT_BASE		SPA_TYPE_INFO_VideoFormat ":"
 
 static const struct spa_type_info spa_type_video_format[] = {
-	{ SPA_VIDEO_FORMAT_ENCODED,	SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "encoded", NULL },
+	{ SPA_VIDEO_FORMAT_UNKNOWN,	SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "UNKNOWN", NULL },
+	{ SPA_VIDEO_FORMAT_ENCODED,	SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "ENCODED", NULL },
 	{ SPA_VIDEO_FORMAT_I420,	SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "I420", NULL },
 	{ SPA_VIDEO_FORMAT_YV12,	SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "YV12", NULL },
 	{ SPA_VIDEO_FORMAT_YUY2,	SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "YUY2", NULL },
@@ -110,6 +119,15 @@ static const struct spa_type_info spa_type_video_format[] = {
 	{ 0, 0, NULL, NULL },
 };
 
+SPA_API_VIDEO_RAW_TYPES uint32_t spa_type_video_format_from_short_name(const char *name)
+{
+	return spa_type_from_short_name(name, spa_type_video_format, SPA_VIDEO_FORMAT_UNKNOWN);
+}
+SPA_API_VIDEO_RAW_TYPES const char * spa_type_video_format_to_short_name(uint32_t type)
+{
+	return spa_type_to_short_name(type, spa_type_video_format, "UNKNOWN");
+}
+
 #define SPA_TYPE_INFO_VideoFlags	SPA_TYPE_INFO_FLAGS_BASE "VideoFlags"
 #define SPA_TYPE_INFO_VIDEO_FLAGS_BASE	SPA_TYPE_INFO_VideoFlags ":"
 
diff --git a/spa/include/spa/param/video/raw-utils.h b/spa/include/spa/param/video/raw-utils.h
index 41d9cc1e..8a5a2778 100644
--- a/spa/include/spa/param/video/raw-utils.h
+++ b/spa/include/spa/param/video/raw-utils.h
@@ -18,7 +18,15 @@ extern "C" {
 #include <spa/pod/builder.h>
 #include <spa/param/video/raw.h>
 
-static inline int
+#ifndef SPA_API_VIDEO_RAW_UTILS
+ #ifdef SPA_API_IMPL
+  #define SPA_API_VIDEO_RAW_UTILS SPA_API_IMPL
+ #else
+  #define SPA_API_VIDEO_RAW_UTILS static inline
+ #endif
+#endif
+
+SPA_API_VIDEO_RAW_UTILS int
 spa_format_video_raw_parse(const struct spa_pod *format,
 			   struct spa_video_info_raw *info)
 {
@@ -49,7 +57,7 @@ spa_format_video_raw_parse(const struct spa_pod *format,
 		SPA_FORMAT_VIDEO_colorPrimaries,	SPA_POD_OPT_Id(&info->color_primaries));
 }
 
-static inline struct spa_pod *
+SPA_API_VIDEO_RAW_UTILS struct spa_pod *
 spa_format_video_raw_build(struct spa_pod_builder *builder, uint32_t id,
 			   const struct spa_video_info_raw *info)
 {
diff --git a/spa/include/spa/pod/builder.h b/spa/include/spa/pod/builder.h
index 0564d94e..553f7551 100644
--- a/spa/include/spa/pod/builder.h
+++ b/spa/include/spa/pod/builder.h
@@ -24,6 +24,14 @@ extern "C" {
 #include <spa/pod/iter.h>
 #include <spa/pod/vararg.h>
 
+#ifndef SPA_API_POD_BUILDER
+ #ifdef SPA_API_IMPL
+  #define SPA_API_POD_BUILDER SPA_API_IMPL
+ #else
+  #define SPA_API_POD_BUILDER static inline
+ #endif
+#endif
+
 struct spa_pod_builder_state {
 	uint32_t offset;
 #define SPA_POD_BUILDER_FLAG_BODY	(1<<0)
@@ -49,22 +57,22 @@ struct spa_pod_builder {
 	struct spa_callbacks callbacks;
 };
 
-#define SPA_POD_BUILDER_INIT(buffer,size)  ((struct spa_pod_builder){ (buffer), (size), 0, {0}, {0} })
+#define SPA_POD_BUILDER_INIT(buffer,size)  ((struct spa_pod_builder){ (buffer), (size), 0, {0,0,NULL},{NULL,NULL}})
 
-static inline void
+SPA_API_POD_BUILDER void
 spa_pod_builder_get_state(struct spa_pod_builder *builder, struct spa_pod_builder_state *state)
 {
 	*state = builder->state;
 }
 
-static inline void
+SPA_API_POD_BUILDER void
 spa_pod_builder_set_callbacks(struct spa_pod_builder *builder,
 		const struct spa_pod_builder_callbacks *callbacks, void *data)
 {
 	builder->callbacks = SPA_CALLBACKS_INIT(callbacks, data);
 }
 
-static inline void
+SPA_API_POD_BUILDER void
 spa_pod_builder_reset(struct spa_pod_builder *builder, struct spa_pod_builder_state *state)
 {
 	struct spa_pod_frame *f;
@@ -74,12 +82,12 @@ spa_pod_builder_reset(struct spa_pod_builder *builder, struct spa_pod_builder_st
 		f->pod.size -= size;
 }
 
-static inline void spa_pod_builder_init(struct spa_pod_builder *builder, void *data, uint32_t size)
+SPA_API_POD_BUILDER void spa_pod_builder_init(struct spa_pod_builder *builder, void *data, uint32_t size)
 {
 	*builder = SPA_POD_BUILDER_INIT(data, size);
 }
 
-static inline struct spa_pod *
+SPA_API_POD_BUILDER struct spa_pod *
 spa_pod_builder_deref(struct spa_pod_builder *builder, uint32_t offset)
 {
 	uint32_t size = builder->size;
@@ -91,7 +99,7 @@ spa_pod_builder_deref(struct spa_pod_builder *builder, uint32_t offset)
 	return NULL;
 }
 
-static inline struct spa_pod *
+SPA_API_POD_BUILDER struct spa_pod *
 spa_pod_builder_frame(struct spa_pod_builder *builder, struct spa_pod_frame *frame)
 {
 	if (frame->offset + SPA_POD_SIZE(&frame->pod) <= builder->size)
@@ -99,7 +107,7 @@ spa_pod_builder_frame(struct spa_pod_builder *builder, struct spa_pod_frame *fra
 	return NULL;
 }
 
-static inline void
+SPA_API_POD_BUILDER void
 spa_pod_builder_push(struct spa_pod_builder *builder,
 		     struct spa_pod_frame *frame,
 		     const struct spa_pod *pod,
@@ -115,21 +123,30 @@ spa_pod_builder_push(struct spa_pod_builder *builder,
 		builder->state.flags = SPA_POD_BUILDER_FLAG_FIRST | SPA_POD_BUILDER_FLAG_BODY;
 }
 
-static inline int spa_pod_builder_raw(struct spa_pod_builder *builder, const void *data, uint32_t size)
+SPA_API_POD_BUILDER int spa_pod_builder_raw(struct spa_pod_builder *builder, const void *data, uint32_t size)
 {
 	int res = 0;
 	struct spa_pod_frame *f;
 	uint32_t offset = builder->state.offset;
+	size_t data_offset = -1;
 
 	if (offset + size > builder->size) {
+		/* data could be inside the data we will realloc */
+		if (spa_ptrinside(builder->data, builder->size, data, size, NULL))
+			data_offset = SPA_PTRDIFF(data, builder->data);
+
 		res = -ENOSPC;
 		if (offset <= builder->size)
 			spa_callbacks_call_res(&builder->callbacks,
 					struct spa_pod_builder_callbacks, res,
 					overflow, 0, offset + size);
 	}
-	if (res == 0 && data)
+	if (res == 0 && data) {
+		if (data_offset != (size_t) -1)
+			data = SPA_PTROFF(builder->data, data_offset, const void);
+
 		memcpy(SPA_PTROFF(builder->data, offset, void), data, size);
+	}
 
 	builder->state.offset += size;
 
@@ -139,14 +156,14 @@ static inline int spa_pod_builder_raw(struct spa_pod_builder *builder, const voi
 	return res;
 }
 
-static inline int spa_pod_builder_pad(struct spa_pod_builder *builder, uint32_t size)
+SPA_API_POD_BUILDER int spa_pod_builder_pad(struct spa_pod_builder *builder, uint32_t size)
 {
 	uint64_t zeroes = 0;
 	size = SPA_ROUND_UP_N(size, 8) - size;
 	return size ? spa_pod_builder_raw(builder, &zeroes, size) : 0;
 }
 
-static inline int
+SPA_API_POD_BUILDER int
 spa_pod_builder_raw_padded(struct spa_pod_builder *builder, const void *data, uint32_t size)
 {
 	int r, res = spa_pod_builder_raw(builder, data, size);
@@ -155,7 +172,7 @@ spa_pod_builder_raw_padded(struct spa_pod_builder *builder, const void *data, ui
 	return res;
 }
 
-static inline void *spa_pod_builder_pop(struct spa_pod_builder *builder, struct spa_pod_frame *frame)
+SPA_API_POD_BUILDER void *spa_pod_builder_pop(struct spa_pod_builder *builder, struct spa_pod_frame *frame)
 {
 	struct spa_pod *pod;
 
@@ -172,7 +189,7 @@ static inline void *spa_pod_builder_pop(struct spa_pod_builder *builder, struct
 	return pod;
 }
 
-static inline int
+SPA_API_POD_BUILDER int
 spa_pod_builder_primitive(struct spa_pod_builder *builder, const struct spa_pod *p)
 {
 	const void *data;
@@ -198,13 +215,13 @@ spa_pod_builder_primitive(struct spa_pod_builder *builder, const struct spa_pod
 
 #define SPA_POD_INIT_None() SPA_POD_INIT(0, SPA_TYPE_None)
 
-static inline int spa_pod_builder_none(struct spa_pod_builder *builder)
+SPA_API_POD_BUILDER int spa_pod_builder_none(struct spa_pod_builder *builder)
 {
 	const struct spa_pod p = SPA_POD_INIT_None();
 	return spa_pod_builder_primitive(builder, &p);
 }
 
-static inline int spa_pod_builder_child(struct spa_pod_builder *builder, uint32_t size, uint32_t type)
+SPA_API_POD_BUILDER int spa_pod_builder_child(struct spa_pod_builder *builder, uint32_t size, uint32_t type)
 {
 	const struct spa_pod p = SPA_POD_INIT(size,type);
 	SPA_FLAG_CLEAR(builder->state.flags, SPA_POD_BUILDER_FLAG_FIRST);
@@ -213,7 +230,7 @@ static inline int spa_pod_builder_child(struct spa_pod_builder *builder, uint32_
 
 #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)
+SPA_API_POD_BUILDER int spa_pod_builder_bool(struct spa_pod_builder *builder, bool val)
 {
 	const struct spa_pod_bool p = SPA_POD_INIT_Bool(val);
 	return spa_pod_builder_primitive(builder, &p.pod);
@@ -221,7 +238,7 @@ static inline int spa_pod_builder_bool(struct spa_pod_builder *builder, bool val
 
 #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)
+SPA_API_POD_BUILDER int spa_pod_builder_id(struct spa_pod_builder *builder, uint32_t val)
 {
 	const struct spa_pod_id p = SPA_POD_INIT_Id(val);
 	return spa_pod_builder_primitive(builder, &p.pod);
@@ -229,7 +246,7 @@ static inline int spa_pod_builder_id(struct spa_pod_builder *builder, uint32_t v
 
 #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)
+SPA_API_POD_BUILDER int spa_pod_builder_int(struct spa_pod_builder *builder, int32_t val)
 {
 	const struct spa_pod_int p = SPA_POD_INIT_Int(val);
 	return spa_pod_builder_primitive(builder, &p.pod);
@@ -237,7 +254,7 @@ static inline int spa_pod_builder_int(struct spa_pod_builder *builder, int32_t v
 
 #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)
+SPA_API_POD_BUILDER int spa_pod_builder_long(struct spa_pod_builder *builder, int64_t val)
 {
 	const struct spa_pod_long p = SPA_POD_INIT_Long(val);
 	return spa_pod_builder_primitive(builder, &p.pod);
@@ -245,7 +262,7 @@ static inline int spa_pod_builder_long(struct spa_pod_builder *builder, int64_t
 
 #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)
+SPA_API_POD_BUILDER int spa_pod_builder_float(struct spa_pod_builder *builder, float val)
 {
 	const struct spa_pod_float p = SPA_POD_INIT_Float(val);
 	return spa_pod_builder_primitive(builder, &p.pod);
@@ -253,7 +270,7 @@ static inline int spa_pod_builder_float(struct spa_pod_builder *builder, float v
 
 #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)
+SPA_API_POD_BUILDER int spa_pod_builder_double(struct spa_pod_builder *builder, double val)
 {
 	const struct spa_pod_double p = SPA_POD_INIT_Double(val);
 	return spa_pod_builder_primitive(builder, &p.pod);
@@ -261,7 +278,7 @@ static inline int spa_pod_builder_double(struct spa_pod_builder *builder, double
 
 #define SPA_POD_INIT_String(len) ((struct spa_pod_string){ { (len), SPA_TYPE_String } })
 
-static inline int
+SPA_API_POD_BUILDER int
 spa_pod_builder_write_string(struct spa_pod_builder *builder, const char *str, uint32_t len)
 {
 	int r, res;
@@ -273,7 +290,7 @@ spa_pod_builder_write_string(struct spa_pod_builder *builder, const char *str, u
 	return res;
 }
 
-static inline int
+SPA_API_POD_BUILDER int
 spa_pod_builder_string_len(struct spa_pod_builder *builder, const char *str, uint32_t len)
 {
 	const struct spa_pod_string p = SPA_POD_INIT_String(len+1);
@@ -283,7 +300,7 @@ spa_pod_builder_string_len(struct spa_pod_builder *builder, const char *str, uin
 	return res;
 }
 
-static inline int spa_pod_builder_string(struct spa_pod_builder *builder, const char *str)
+SPA_API_POD_BUILDER int spa_pod_builder_string(struct spa_pod_builder *builder, const char *str)
 {
 	uint32_t len = str ? strlen(str) : 0;
 	return spa_pod_builder_string_len(builder, str ? str : "", len);
@@ -291,7 +308,7 @@ static inline int spa_pod_builder_string(struct spa_pod_builder *builder, const
 
 #define SPA_POD_INIT_Bytes(len) ((struct spa_pod_bytes){ { (len), SPA_TYPE_Bytes } })
 
-static inline int
+SPA_API_POD_BUILDER int
 spa_pod_builder_bytes(struct spa_pod_builder *builder, const void *bytes, uint32_t len)
 {
 	const struct spa_pod_bytes p = SPA_POD_INIT_Bytes(len);
@@ -300,7 +317,7 @@ spa_pod_builder_bytes(struct spa_pod_builder *builder, const void *bytes, uint32
 		res = r;
 	return res;
 }
-static inline void *
+SPA_API_POD_BUILDER void *
 spa_pod_builder_reserve_bytes(struct spa_pod_builder *builder, uint32_t len)
 {
 	uint32_t offset = builder->state.offset;
@@ -311,7 +328,7 @@ spa_pod_builder_reserve_bytes(struct spa_pod_builder *builder, uint32_t len)
 
 #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_API_POD_BUILDER int
 spa_pod_builder_pointer(struct spa_pod_builder *builder, uint32_t type, const void *val)
 {
 	const struct spa_pod_pointer p = SPA_POD_INIT_Pointer(type, val);
@@ -320,7 +337,7 @@ spa_pod_builder_pointer(struct spa_pod_builder *builder, uint32_t type, const vo
 
 #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)
+SPA_API_POD_BUILDER int spa_pod_builder_fd(struct spa_pod_builder *builder, int64_t fd)
 {
 	const struct spa_pod_fd p = SPA_POD_INIT_Fd(fd);
 	return spa_pod_builder_primitive(builder, &p.pod);
@@ -328,7 +345,7 @@ static inline int spa_pod_builder_fd(struct spa_pod_builder *builder, int64_t fd
 
 #define SPA_POD_INIT_Rectangle(val) ((struct spa_pod_rectangle){ { sizeof(struct spa_rectangle), SPA_TYPE_Rectangle }, (val) })
 
-static inline int
+SPA_API_POD_BUILDER int
 spa_pod_builder_rectangle(struct spa_pod_builder *builder, uint32_t width, uint32_t height)
 {
 	const struct spa_pod_rectangle p = SPA_POD_INIT_Rectangle(SPA_RECTANGLE(width, height));
@@ -337,14 +354,14 @@ spa_pod_builder_rectangle(struct spa_pod_builder *builder, uint32_t width, uint3
 
 #define SPA_POD_INIT_Fraction(val) ((struct spa_pod_fraction){ { sizeof(struct spa_fraction), SPA_TYPE_Fraction }, (val) })
 
-static inline int
+SPA_API_POD_BUILDER int
 spa_pod_builder_fraction(struct spa_pod_builder *builder, uint32_t num, uint32_t denom)
 {
 	const struct spa_pod_fraction p = SPA_POD_INIT_Fraction(SPA_FRACTION(num, denom));
 	return spa_pod_builder_primitive(builder, &p.pod);
 }
 
-static inline int
+SPA_API_POD_BUILDER int
 spa_pod_builder_push_array(struct spa_pod_builder *builder, struct spa_pod_frame *frame)
 {
 	const struct spa_pod_array p =
@@ -356,7 +373,7 @@ spa_pod_builder_push_array(struct spa_pod_builder *builder, struct spa_pod_frame
 	return res;
 }
 
-static inline int
+SPA_API_POD_BUILDER int
 spa_pod_builder_array(struct spa_pod_builder *builder,
 		      uint32_t child_size, uint32_t child_type, uint32_t n_elems, const void *elems)
 {
@@ -378,7 +395,7 @@ spa_pod_builder_array(struct spa_pod_builder *builder,
 	{ { { (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_API_POD_BUILDER int
 spa_pod_builder_push_choice(struct spa_pod_builder *builder, struct spa_pod_frame *frame,
 		uint32_t type, uint32_t flags)
 {
@@ -393,7 +410,7 @@ spa_pod_builder_push_choice(struct spa_pod_builder *builder, struct spa_pod_fram
 
 #define SPA_POD_INIT_Struct(size) ((struct spa_pod_struct){ { (size), SPA_TYPE_Struct } })
 
-static inline int
+SPA_API_POD_BUILDER int
 spa_pod_builder_push_struct(struct spa_pod_builder *builder, struct spa_pod_frame *frame)
 {
 	const struct spa_pod_struct p = SPA_POD_INIT_Struct(0);
@@ -405,7 +422,7 @@ spa_pod_builder_push_struct(struct spa_pod_builder *builder, struct spa_pod_fram
 
 #define SPA_POD_INIT_Object(size,type,id,...)	((struct spa_pod_object){ { (size), SPA_TYPE_Object }, { (type), (id) }, ##__VA_ARGS__ })
 
-static inline int
+SPA_API_POD_BUILDER int
 spa_pod_builder_push_object(struct spa_pod_builder *builder, struct spa_pod_frame *frame,
 		uint32_t type, uint32_t id)
 {
@@ -420,7 +437,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) } })
 
-static inline int
+SPA_API_POD_BUILDER int
 spa_pod_builder_prop(struct spa_pod_builder *builder, uint32_t key, uint32_t flags)
 {
 	const struct { uint32_t key; uint32_t flags; } p = { key, flags };
@@ -430,7 +447,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 } })
 
-static inline int
+SPA_API_POD_BUILDER int
 spa_pod_builder_push_sequence(struct spa_pod_builder *builder, struct spa_pod_frame *frame, uint32_t unit)
 {
 	const struct spa_pod_sequence p =
@@ -441,14 +458,14 @@ spa_pod_builder_push_sequence(struct spa_pod_builder *builder, struct spa_pod_fr
 	return res;
 }
 
-static inline int
+SPA_API_POD_BUILDER int
 spa_pod_builder_control(struct spa_pod_builder *builder, uint32_t offset, uint32_t type)
 {
 	const struct { uint32_t offset; uint32_t type; } p = { offset, type };
 	return spa_pod_builder_raw(builder, &p, sizeof(p));
 }
 
-static inline uint32_t spa_choice_from_id(char id)
+SPA_API_POD_BUILDER uint32_t spa_choice_from_id(char id)
 {
 	switch (id) {
 	case 'r':
@@ -560,7 +577,7 @@ do {										\
 	}									\
 } while(false)
 
-static inline int
+SPA_API_POD_BUILDER int
 spa_pod_builder_addv(struct spa_pod_builder *builder, va_list args)
 {
 	int res = 0;
@@ -618,7 +635,7 @@ spa_pod_builder_addv(struct spa_pod_builder *builder, va_list args)
 	return res;
 }
 
-static inline int spa_pod_builder_add(struct spa_pod_builder *builder, ...)
+SPA_API_POD_BUILDER int spa_pod_builder_add(struct spa_pod_builder *builder, ...)
 {
 	int res;
 	va_list args;
@@ -658,7 +675,7 @@ static inline int spa_pod_builder_add(struct spa_pod_builder *builder, ...)
 })
 
 /** Copy a pod structure */
-static inline struct spa_pod *
+SPA_API_POD_BUILDER struct spa_pod *
 spa_pod_copy(const struct spa_pod *pod)
 {
 	size_t size;
diff --git a/spa/include/spa/pod/compare.h b/spa/include/spa/pod/compare.h
index f41e4811..50898756 100644
--- a/spa/include/spa/pod/compare.h
+++ b/spa/include/spa/pod/compare.h
@@ -20,12 +20,20 @@ extern "C" {
 #include <spa/pod/iter.h>
 #include <spa/pod/builder.h>
 
+#ifndef SPA_API_POD_COMPARE
+ #ifdef SPA_API_IMPL
+  #define SPA_API_POD_COMPARE SPA_API_IMPL
+ #else
+  #define SPA_API_POD_COMPARE static inline
+ #endif
+#endif
+
 /**
  * \addtogroup spa_pod
  * \{
  */
 
-static inline int spa_pod_compare_value(uint32_t type, const void *r1, const void *r2, uint32_t size)
+SPA_API_POD_COMPARE int spa_pod_compare_value(uint32_t type, const void *r1, const void *r2, uint32_t size)
 {
 	switch (type) {
 	case SPA_TYPE_None:
@@ -72,7 +80,7 @@ static inline int spa_pod_compare_value(uint32_t type, const void *r1, const voi
 	return 0;
 }
 
-static inline int spa_pod_compare(const struct spa_pod *pod1,
+SPA_API_POD_COMPARE int spa_pod_compare(const struct spa_pod *pod1,
 				  const struct spa_pod *pod2)
 {
 	int res = 0;
diff --git a/spa/include/spa/pod/dynamic.h b/spa/include/spa/pod/dynamic.h
index 9edff2d0..e9998cdb 100644
--- a/spa/include/spa/pod/dynamic.h
+++ b/spa/include/spa/pod/dynamic.h
@@ -12,6 +12,14 @@ extern "C" {
 #include <spa/pod/builder.h>
 #include <spa/utils/cleanup.h>
 
+#ifndef SPA_API_POD_DYNAMIC
+ #ifdef SPA_API_IMPL
+  #define SPA_API_POD_DYNAMIC SPA_API_IMPL
+ #else
+  #define SPA_API_POD_DYNAMIC static inline
+ #endif
+#endif
+
 struct spa_pod_dynamic_builder {
 	struct spa_pod_builder b;
 	void *data;
@@ -37,7 +45,7 @@ static int spa_pod_dynamic_builder_overflow(void *data, uint32_t size)
         return 0;
 }
 
-static inline void spa_pod_dynamic_builder_init(struct spa_pod_dynamic_builder *builder,
+SPA_API_POD_DYNAMIC void spa_pod_dynamic_builder_init(struct spa_pod_dynamic_builder *builder,
 		void *data, uint32_t size, uint32_t extend)
 {
 	static const struct spa_pod_builder_callbacks spa_pod_dynamic_builder_callbacks = {
@@ -50,7 +58,7 @@ static inline void spa_pod_dynamic_builder_init(struct spa_pod_dynamic_builder *
 	builder->data = data;
 }
 
-static inline void spa_pod_dynamic_builder_clean(struct spa_pod_dynamic_builder *builder)
+SPA_API_POD_DYNAMIC void spa_pod_dynamic_builder_clean(struct spa_pod_dynamic_builder *builder)
 {
 	if (builder->data != builder->b.data)
 		free(builder->b.data);
diff --git a/spa/include/spa/pod/filter.h b/spa/include/spa/pod/filter.h
index 3a682e1a..6a47472e 100644
--- a/spa/include/spa/pod/filter.h
+++ b/spa/include/spa/pod/filter.h
@@ -20,12 +20,20 @@ extern "C" {
 #include <spa/pod/builder.h>
 #include <spa/pod/compare.h>
 
+#ifndef SPA_API_POD_FILTER
+ #ifdef SPA_API_IMPL
+  #define SPA_API_POD_FILTER SPA_API_IMPL
+ #else
+  #define SPA_API_POD_FILTER static inline
+ #endif
+#endif
+
 /**
  * \addtogroup spa_pod
  * \{
  */
 
-static inline int spa_pod_choice_fix_default(struct spa_pod_choice *choice)
+SPA_API_POD_FILTER int spa_pod_choice_fix_default(struct spa_pod_choice *choice)
 {
 	void *val, *alt;
 	int i, nvals;
@@ -77,7 +85,7 @@ static inline int spa_pod_choice_fix_default(struct spa_pod_choice *choice)
 	return 0;
 }
 
-static inline int spa_pod_filter_flags_value(struct spa_pod_builder *b,
+SPA_API_POD_FILTER int spa_pod_filter_flags_value(struct spa_pod_builder *b,
 		uint32_t type, const void *r1, const void *r2, uint32_t size SPA_UNUSED)
 {
 	switch (type) {
@@ -103,7 +111,7 @@ static inline int spa_pod_filter_flags_value(struct spa_pod_builder *b,
 	return 1;
 }
 
-static inline int spa_pod_filter_is_step_of(uint32_t type, const void *r1,
+SPA_API_POD_FILTER int spa_pod_filter_is_step_of(uint32_t type, const void *r1,
 		const void *r2, uint32_t size SPA_UNUSED)
 {
 	switch (type) {
@@ -125,7 +133,7 @@ static inline int spa_pod_filter_is_step_of(uint32_t type, const void *r1,
 	return 0;
 }
 
-static inline int
+SPA_API_POD_FILTER int
 spa_pod_filter_prop(struct spa_pod_builder *b,
 	    const struct spa_pod_prop *p1,
 	    const struct spa_pod_prop *p2)
@@ -322,7 +330,7 @@ spa_pod_filter_prop(struct spa_pod_builder *b,
 	return 0;
 }
 
-static inline int spa_pod_filter_part(struct spa_pod_builder *b,
+SPA_API_POD_FILTER int spa_pod_filter_part(struct spa_pod_builder *b,
 	       const struct spa_pod *pod, uint32_t pod_size,
 	       const struct spa_pod *filter, uint32_t filter_size)
 {
@@ -422,7 +430,7 @@ static inline int spa_pod_filter_part(struct spa_pod_builder *b,
 	return res;
 }
 
-static inline int
+SPA_API_POD_FILTER int
 spa_pod_filter(struct spa_pod_builder *b,
 	       struct spa_pod **result,
 	       const struct spa_pod *pod,
diff --git a/spa/include/spa/pod/iter.h b/spa/include/spa/pod/iter.h
index 93a23ad9..1bd56996 100644
--- a/spa/include/spa/pod/iter.h
+++ b/spa/include/spa/pod/iter.h
@@ -14,6 +14,14 @@ extern "C" {
 
 #include <spa/pod/pod.h>
 
+#ifndef SPA_API_POD_ITER
+ #ifdef SPA_API_IMPL
+  #define SPA_API_POD_ITER SPA_API_IMPL
+ #else
+  #define SPA_API_POD_ITER static inline
+ #endif
+#endif
+
 /**
  * \addtogroup spa_pod
  * \{
@@ -26,7 +34,7 @@ struct spa_pod_frame {
 	uint32_t flags;
 };
 
-static inline bool spa_pod_is_inside(const void *pod, uint32_t size, const void *iter)
+SPA_API_POD_ITER bool spa_pod_is_inside(const void *pod, uint32_t size, const void *iter)
 {
 	size_t remaining;
 
@@ -34,17 +42,17 @@ static inline bool spa_pod_is_inside(const void *pod, uint32_t size, const void
 	       remaining >= SPA_POD_BODY_SIZE(iter);
 }
 
-static inline void *spa_pod_next(const void *iter)
+SPA_API_POD_ITER void *spa_pod_next(const void *iter)
 {
 	return SPA_PTROFF(iter, SPA_ROUND_UP_N(SPA_POD_SIZE(iter), 8), void);
 }
 
-static inline struct spa_pod_prop *spa_pod_prop_first(const struct spa_pod_object_body *body)
+SPA_API_POD_ITER struct spa_pod_prop *spa_pod_prop_first(const struct spa_pod_object_body *body)
 {
 	return SPA_PTROFF(body, sizeof(struct spa_pod_object_body), struct spa_pod_prop);
 }
 
-static inline bool spa_pod_prop_is_inside(const struct spa_pod_object_body *body,
+SPA_API_POD_ITER bool spa_pod_prop_is_inside(const struct spa_pod_object_body *body,
 		uint32_t size, const struct spa_pod_prop *iter)
 {
 	size_t remaining;
@@ -53,17 +61,17 @@ static inline bool spa_pod_prop_is_inside(const struct spa_pod_object_body *body
 	       remaining >= iter->value.size;
 }
 
-static inline struct spa_pod_prop *spa_pod_prop_next(const struct spa_pod_prop *iter)
+SPA_API_POD_ITER struct spa_pod_prop *spa_pod_prop_next(const struct spa_pod_prop *iter)
 {
 	return SPA_PTROFF(iter, SPA_ROUND_UP_N(SPA_POD_PROP_SIZE(iter), 8), struct spa_pod_prop);
 }
 
-static inline struct spa_pod_control *spa_pod_control_first(const struct spa_pod_sequence_body *body)
+SPA_API_POD_ITER struct spa_pod_control *spa_pod_control_first(const struct spa_pod_sequence_body *body)
 {
 	return SPA_PTROFF(body, sizeof(struct spa_pod_sequence_body), struct spa_pod_control);
 }
 
-static inline bool spa_pod_control_is_inside(const struct spa_pod_sequence_body *body,
+SPA_API_POD_ITER bool spa_pod_control_is_inside(const struct spa_pod_sequence_body *body,
 		uint32_t size, const struct spa_pod_control *iter)
 {
 	size_t remaining;
@@ -72,7 +80,7 @@ static inline bool spa_pod_control_is_inside(const struct spa_pod_sequence_body
 	       remaining >= iter->value.size;
 }
 
-static inline struct spa_pod_control *spa_pod_control_next(const struct spa_pod_control *iter)
+SPA_API_POD_ITER struct spa_pod_control *spa_pod_control_next(const struct spa_pod_control *iter)
 {
 	return SPA_PTROFF(iter, SPA_ROUND_UP_N(SPA_POD_CONTROL_SIZE(iter), 8), struct spa_pod_control);
 }
@@ -118,7 +126,7 @@ static inline struct spa_pod_control *spa_pod_control_next(const struct spa_pod_
 	SPA_POD_SEQUENCE_BODY_FOREACH(&(seq)->body, SPA_POD_BODY_SIZE(seq), iter)
 
 
-static inline void *spa_pod_from_data(void *data, size_t maxsize, off_t offset, size_t size)
+SPA_API_POD_ITER void *spa_pod_from_data(void *data, size_t maxsize, off_t offset, size_t size)
 {
 	void *pod;
 	if (size < sizeof(struct spa_pod) || offset + size > maxsize)
@@ -129,17 +137,17 @@ static inline void *spa_pod_from_data(void *data, size_t maxsize, off_t offset,
 	return pod;
 }
 
-static inline int spa_pod_is_none(const struct spa_pod *pod)
+SPA_API_POD_ITER int spa_pod_is_none(const struct spa_pod *pod)
 {
 	return (SPA_POD_TYPE(pod) == SPA_TYPE_None);
 }
 
-static inline int spa_pod_is_bool(const struct spa_pod *pod)
+SPA_API_POD_ITER int spa_pod_is_bool(const struct spa_pod *pod)
 {
 	return (SPA_POD_TYPE(pod) == SPA_TYPE_Bool && SPA_POD_BODY_SIZE(pod) >= sizeof(int32_t));
 }
 
-static inline int spa_pod_get_bool(const struct spa_pod *pod, bool *value)
+SPA_API_POD_ITER int spa_pod_get_bool(const struct spa_pod *pod, bool *value)
 {
 	if (!spa_pod_is_bool(pod))
 		return -EINVAL;
@@ -147,12 +155,12 @@ static inline int spa_pod_get_bool(const struct spa_pod *pod, bool *value)
 	return 0;
 }
 
-static inline int spa_pod_is_id(const struct spa_pod *pod)
+SPA_API_POD_ITER int spa_pod_is_id(const struct spa_pod *pod)
 {
 	return (SPA_POD_TYPE(pod) == SPA_TYPE_Id && SPA_POD_BODY_SIZE(pod) >= sizeof(uint32_t));
 }
 
-static inline int spa_pod_get_id(const struct spa_pod *pod, uint32_t *value)
+SPA_API_POD_ITER int spa_pod_get_id(const struct spa_pod *pod, uint32_t *value)
 {
 	if (!spa_pod_is_id(pod))
 		return -EINVAL;
@@ -160,12 +168,12 @@ static inline int spa_pod_get_id(const struct spa_pod *pod, uint32_t *value)
 	return 0;
 }
 
-static inline int spa_pod_is_int(const struct spa_pod *pod)
+SPA_API_POD_ITER int spa_pod_is_int(const struct spa_pod *pod)
 {
 	return (SPA_POD_TYPE(pod) == SPA_TYPE_Int && SPA_POD_BODY_SIZE(pod) >= sizeof(int32_t));
 }
 
-static inline int spa_pod_get_int(const struct spa_pod *pod, int32_t *value)
+SPA_API_POD_ITER int spa_pod_get_int(const struct spa_pod *pod, int32_t *value)
 {
 	if (!spa_pod_is_int(pod))
 		return -EINVAL;
@@ -173,12 +181,12 @@ static inline int spa_pod_get_int(const struct spa_pod *pod, int32_t *value)
 	return 0;
 }
 
-static inline int spa_pod_is_long(const struct spa_pod *pod)
+SPA_API_POD_ITER int spa_pod_is_long(const struct spa_pod *pod)
 {
 	return (SPA_POD_TYPE(pod) == SPA_TYPE_Long && SPA_POD_BODY_SIZE(pod) >= sizeof(int64_t));
 }
 
-static inline int spa_pod_get_long(const struct spa_pod *pod, int64_t *value)
+SPA_API_POD_ITER int spa_pod_get_long(const struct spa_pod *pod, int64_t *value)
 {
 	if (!spa_pod_is_long(pod))
 		return -EINVAL;
@@ -186,12 +194,12 @@ static inline int spa_pod_get_long(const struct spa_pod *pod, int64_t *value)
 	return 0;
 }
 
-static inline int spa_pod_is_float(const struct spa_pod *pod)
+SPA_API_POD_ITER int spa_pod_is_float(const struct spa_pod *pod)
 {
 	return (SPA_POD_TYPE(pod) == SPA_TYPE_Float && SPA_POD_BODY_SIZE(pod) >= sizeof(float));
 }
 
-static inline int spa_pod_get_float(const struct spa_pod *pod, float *value)
+SPA_API_POD_ITER int spa_pod_get_float(const struct spa_pod *pod, float *value)
 {
 	if (!spa_pod_is_float(pod))
 		return -EINVAL;
@@ -199,12 +207,12 @@ static inline int spa_pod_get_float(const struct spa_pod *pod, float *value)
 	return 0;
 }
 
-static inline int spa_pod_is_double(const struct spa_pod *pod)
+SPA_API_POD_ITER int spa_pod_is_double(const struct spa_pod *pod)
 {
 	return (SPA_POD_TYPE(pod) == SPA_TYPE_Double && SPA_POD_BODY_SIZE(pod) >= sizeof(double));
 }
 
-static inline int spa_pod_get_double(const struct spa_pod *pod, double *value)
+SPA_API_POD_ITER int spa_pod_get_double(const struct spa_pod *pod, double *value)
 {
 	if (!spa_pod_is_double(pod))
 		return -EINVAL;
@@ -212,7 +220,7 @@ static inline int spa_pod_get_double(const struct spa_pod *pod, double *value)
 	return 0;
 }
 
-static inline int spa_pod_is_string(const struct spa_pod *pod)
+SPA_API_POD_ITER int spa_pod_is_string(const struct spa_pod *pod)
 {
 	const char *s = (const char *)SPA_POD_CONTENTS(struct spa_pod_string, pod);
 	return (SPA_POD_TYPE(pod) == SPA_TYPE_String &&
@@ -220,7 +228,7 @@ static inline int spa_pod_is_string(const struct spa_pod *pod)
 			s[SPA_POD_BODY_SIZE(pod)-1] == '\0');
 }
 
-static inline int spa_pod_get_string(const struct spa_pod *pod, const char **value)
+SPA_API_POD_ITER int spa_pod_get_string(const struct spa_pod *pod, const char **value)
 {
 	if (!spa_pod_is_string(pod))
 		return -EINVAL;
@@ -228,7 +236,7 @@ static inline int spa_pod_get_string(const struct spa_pod *pod, const char **val
 	return 0;
 }
 
-static inline int spa_pod_copy_string(const struct spa_pod *pod, size_t maxlen, char *dest)
+SPA_API_POD_ITER int spa_pod_copy_string(const struct spa_pod *pod, size_t maxlen, char *dest)
 {
 	const char *s = (const char *)SPA_POD_CONTENTS(struct spa_pod_string, pod);
 	if (!spa_pod_is_string(pod) || maxlen < 1)
@@ -238,12 +246,12 @@ static inline int spa_pod_copy_string(const struct spa_pod *pod, size_t maxlen,
 	return 0;
 }
 
-static inline int spa_pod_is_bytes(const struct spa_pod *pod)
+SPA_API_POD_ITER int spa_pod_is_bytes(const struct spa_pod *pod)
 {
 	return SPA_POD_TYPE(pod) == SPA_TYPE_Bytes;
 }
 
-static inline int spa_pod_get_bytes(const struct spa_pod *pod, const void **value, uint32_t *len)
+SPA_API_POD_ITER int spa_pod_get_bytes(const struct spa_pod *pod, const void **value, uint32_t *len)
 {
 	if (!spa_pod_is_bytes(pod))
 		return -EINVAL;
@@ -252,13 +260,13 @@ static inline int spa_pod_get_bytes(const struct spa_pod *pod, const void **valu
 	return 0;
 }
 
-static inline int spa_pod_is_pointer(const struct spa_pod *pod)
+SPA_API_POD_ITER int spa_pod_is_pointer(const struct spa_pod *pod)
 {
 	return (SPA_POD_TYPE(pod) == SPA_TYPE_Pointer &&
 			SPA_POD_BODY_SIZE(pod) >= sizeof(struct spa_pod_pointer_body));
 }
 
-static inline int spa_pod_get_pointer(const struct spa_pod *pod, uint32_t *type, const void **value)
+SPA_API_POD_ITER int spa_pod_get_pointer(const struct spa_pod *pod, uint32_t *type, const void **value)
 {
 	if (!spa_pod_is_pointer(pod))
 		return -EINVAL;
@@ -267,13 +275,13 @@ static inline int spa_pod_get_pointer(const struct spa_pod *pod, uint32_t *type,
 	return 0;
 }
 
-static inline int spa_pod_is_fd(const struct spa_pod *pod)
+SPA_API_POD_ITER int spa_pod_is_fd(const struct spa_pod *pod)
 {
 	return (SPA_POD_TYPE(pod) == SPA_TYPE_Fd &&
 			SPA_POD_BODY_SIZE(pod) >= sizeof(int64_t));
 }
 
-static inline int spa_pod_get_fd(const struct spa_pod *pod, int64_t *value)
+SPA_API_POD_ITER int spa_pod_get_fd(const struct spa_pod *pod, int64_t *value)
 {
 	if (!spa_pod_is_fd(pod))
 		return -EINVAL;
@@ -281,13 +289,13 @@ static inline int spa_pod_get_fd(const struct spa_pod *pod, int64_t *value)
 	return 0;
 }
 
-static inline int spa_pod_is_rectangle(const struct spa_pod *pod)
+SPA_API_POD_ITER int spa_pod_is_rectangle(const struct spa_pod *pod)
 {
 	return (SPA_POD_TYPE(pod) == SPA_TYPE_Rectangle &&
 			SPA_POD_BODY_SIZE(pod) >= sizeof(struct spa_rectangle));
 }
 
-static inline int spa_pod_get_rectangle(const struct spa_pod *pod, struct spa_rectangle *value)
+SPA_API_POD_ITER int spa_pod_get_rectangle(const struct spa_pod *pod, struct spa_rectangle *value)
 {
 	if (!spa_pod_is_rectangle(pod))
 		return -EINVAL;
@@ -295,39 +303,39 @@ static inline int spa_pod_get_rectangle(const struct spa_pod *pod, struct spa_re
 	return 0;
 }
 
-static inline int spa_pod_is_fraction(const struct spa_pod *pod)
+SPA_API_POD_ITER int spa_pod_is_fraction(const struct spa_pod *pod)
 {
 	return (SPA_POD_TYPE(pod) == SPA_TYPE_Fraction &&
 			SPA_POD_BODY_SIZE(pod) >= sizeof(struct spa_fraction));
 }
 
-static inline int spa_pod_get_fraction(const struct spa_pod *pod, struct spa_fraction *value)
+SPA_API_POD_ITER int spa_pod_get_fraction(const struct spa_pod *pod, struct spa_fraction *value)
 {
 	spa_return_val_if_fail(spa_pod_is_fraction(pod), -EINVAL);
 	*value = SPA_POD_VALUE(struct spa_pod_fraction, pod);
 	return 0;
 }
 
-static inline int spa_pod_is_bitmap(const struct spa_pod *pod)
+SPA_API_POD_ITER int spa_pod_is_bitmap(const struct spa_pod *pod)
 {
 	return (SPA_POD_TYPE(pod) == SPA_TYPE_Bitmap &&
 			SPA_POD_BODY_SIZE(pod) >= sizeof(uint8_t));
 }
 
-static inline int spa_pod_is_array(const struct spa_pod *pod)
+SPA_API_POD_ITER int spa_pod_is_array(const struct spa_pod *pod)
 {
 	return (SPA_POD_TYPE(pod) == SPA_TYPE_Array &&
 			SPA_POD_BODY_SIZE(pod) >= sizeof(struct spa_pod_array_body));
 }
 
-static inline void *spa_pod_get_array(const struct spa_pod *pod, uint32_t *n_values)
+SPA_API_POD_ITER void *spa_pod_get_array(const struct spa_pod *pod, uint32_t *n_values)
 {
 	spa_return_val_if_fail(spa_pod_is_array(pod), NULL);
 	*n_values = SPA_POD_ARRAY_N_VALUES(pod);
 	return SPA_POD_ARRAY_VALUES(pod);
 }
 
-static inline uint32_t spa_pod_copy_array(const struct spa_pod *pod, uint32_t type,
+SPA_API_POD_ITER uint32_t spa_pod_copy_array(const struct spa_pod *pod, uint32_t type,
 		void *values, uint32_t max_values)
 {
 	uint32_t n_values;
@@ -339,13 +347,13 @@ static inline uint32_t spa_pod_copy_array(const struct spa_pod *pod, uint32_t ty
 	return n_values;
 }
 
-static inline int spa_pod_is_choice(const struct spa_pod *pod)
+SPA_API_POD_ITER int spa_pod_is_choice(const struct spa_pod *pod)
 {
 	return (SPA_POD_TYPE(pod) == SPA_TYPE_Choice &&
 			SPA_POD_BODY_SIZE(pod) >= sizeof(struct spa_pod_choice_body));
 }
 
-static inline struct spa_pod *spa_pod_get_values(const struct spa_pod *pod, uint32_t *n_vals, uint32_t *choice)
+SPA_API_POD_ITER struct spa_pod *spa_pod_get_values(const struct spa_pod *pod, uint32_t *n_vals, uint32_t *choice)
 {
 	if (pod->type == SPA_TYPE_Choice) {
 		*n_vals = SPA_POD_CHOICE_N_VALUES(pod);
@@ -359,34 +367,34 @@ static inline struct spa_pod *spa_pod_get_values(const struct spa_pod *pod, uint
 	}
 }
 
-static inline int spa_pod_is_struct(const struct spa_pod *pod)
+SPA_API_POD_ITER int spa_pod_is_struct(const struct spa_pod *pod)
 {
 	return (SPA_POD_TYPE(pod) == SPA_TYPE_Struct);
 }
 
-static inline int spa_pod_is_object(const struct spa_pod *pod)
+SPA_API_POD_ITER int spa_pod_is_object(const struct spa_pod *pod)
 {
 	return (SPA_POD_TYPE(pod) == SPA_TYPE_Object &&
 			SPA_POD_BODY_SIZE(pod) >= sizeof(struct spa_pod_object_body));
 }
 
-static inline bool spa_pod_is_object_type(const struct spa_pod *pod, uint32_t type)
+SPA_API_POD_ITER bool spa_pod_is_object_type(const struct spa_pod *pod, uint32_t type)
 {
 	return (pod && spa_pod_is_object(pod) && SPA_POD_OBJECT_TYPE(pod) == type);
 }
 
-static inline bool spa_pod_is_object_id(const struct spa_pod *pod, uint32_t id)
+SPA_API_POD_ITER bool spa_pod_is_object_id(const struct spa_pod *pod, uint32_t id)
 {
 	return (pod && spa_pod_is_object(pod) && SPA_POD_OBJECT_ID(pod) == id);
 }
 
-static inline int spa_pod_is_sequence(const struct spa_pod *pod)
+SPA_API_POD_ITER int spa_pod_is_sequence(const struct spa_pod *pod)
 {
 	return (SPA_POD_TYPE(pod) == SPA_TYPE_Sequence &&
 			SPA_POD_BODY_SIZE(pod) >= sizeof(struct spa_pod_sequence_body));
 }
 
-static inline const struct spa_pod_prop *spa_pod_object_find_prop(const struct spa_pod_object *pod,
+SPA_API_POD_ITER const struct spa_pod_prop *spa_pod_object_find_prop(const struct spa_pod_object *pod,
 		const struct spa_pod_prop *start, uint32_t key)
 {
 	const struct spa_pod_prop *first, *res;
@@ -406,7 +414,7 @@ static inline const struct spa_pod_prop *spa_pod_object_find_prop(const struct s
 	return NULL;
 }
 
-static inline const struct spa_pod_prop *spa_pod_find_prop(const struct spa_pod *pod,
+SPA_API_POD_ITER const struct spa_pod_prop *spa_pod_find_prop(const struct spa_pod *pod,
 		const struct spa_pod_prop *start, uint32_t key)
 {
 	if (!spa_pod_is_object(pod))
@@ -414,7 +422,7 @@ static inline const struct spa_pod_prop *spa_pod_find_prop(const struct spa_pod
 	return spa_pod_object_find_prop((const struct spa_pod_object *)pod, start, key);
 }
 
-static inline int spa_pod_object_fixate(struct spa_pod_object *pod)
+SPA_API_POD_ITER int spa_pod_object_fixate(struct spa_pod_object *pod)
 {
 	struct spa_pod_prop *res;
 	SPA_POD_OBJECT_FOREACH(pod, res) {
@@ -425,14 +433,14 @@ static inline int spa_pod_object_fixate(struct spa_pod_object *pod)
 	return 0;
 }
 
-static inline int spa_pod_fixate(struct spa_pod *pod)
+SPA_API_POD_ITER int spa_pod_fixate(struct spa_pod *pod)
 {
 	if (!spa_pod_is_object(pod))
 		return -EINVAL;
 	return spa_pod_object_fixate((struct spa_pod_object *)pod);
 }
 
-static inline int spa_pod_object_is_fixated(const struct spa_pod_object *pod)
+SPA_API_POD_ITER int spa_pod_object_is_fixated(const struct spa_pod_object *pod)
 {
 	struct spa_pod_prop *res;
 	SPA_POD_OBJECT_FOREACH(pod, res) {
@@ -443,7 +451,7 @@ static inline int spa_pod_object_is_fixated(const struct spa_pod_object *pod)
 	return 1;
 }
 
-static inline int spa_pod_object_has_props(const struct spa_pod_object *pod)
+SPA_API_POD_ITER int spa_pod_object_has_props(const struct spa_pod_object *pod)
 {
 	struct spa_pod_prop *res;
 	SPA_POD_OBJECT_FOREACH(pod, res)
@@ -451,7 +459,7 @@ static inline int spa_pod_object_has_props(const struct spa_pod_object *pod)
 	return 0;
 }
 
-static inline int spa_pod_is_fixated(const struct spa_pod *pod)
+SPA_API_POD_ITER int spa_pod_is_fixated(const struct spa_pod *pod)
 {
 	if (!spa_pod_is_object(pod))
 		return -EINVAL;
diff --git a/spa/include/spa/pod/parser.h b/spa/include/spa/pod/parser.h
index 083f9117..d2aa206b 100644
--- a/spa/include/spa/pod/parser.h
+++ b/spa/include/spa/pod/parser.h
@@ -15,6 +15,14 @@ extern "C" {
 #include <spa/pod/iter.h>
 #include <spa/pod/vararg.h>
 
+#ifndef SPA_API_POD_PARSER
+ #ifdef SPA_API_IMPL
+  #define SPA_API_POD_PARSER SPA_API_IMPL
+ #else
+  #define SPA_API_POD_PARSER static inline
+ #endif
+#endif
+
 /**
  * \addtogroup spa_pod
  * \{
@@ -33,33 +41,33 @@ struct spa_pod_parser {
 	struct spa_pod_parser_state state;
 };
 
-#define SPA_POD_PARSER_INIT(buffer,size)  ((struct spa_pod_parser){ (buffer), (size), 0, {0} })
+#define SPA_POD_PARSER_INIT(buffer,size)  ((struct spa_pod_parser){ (buffer), (size), 0, {0,0,NULL}})
 
-static inline void spa_pod_parser_init(struct spa_pod_parser *parser,
+SPA_API_POD_PARSER void spa_pod_parser_init(struct spa_pod_parser *parser,
 				       const void *data, uint32_t size)
 {
 	*parser = SPA_POD_PARSER_INIT(data, size);
 }
 
-static inline void spa_pod_parser_pod(struct spa_pod_parser *parser,
+SPA_API_POD_PARSER void spa_pod_parser_pod(struct spa_pod_parser *parser,
 				      const struct spa_pod *pod)
 {
 	spa_pod_parser_init(parser, pod, SPA_POD_SIZE(pod));
 }
 
-static inline void
+SPA_API_POD_PARSER void
 spa_pod_parser_get_state(struct spa_pod_parser *parser, struct spa_pod_parser_state *state)
 {
 	*state = parser->state;
 }
 
-static inline void
+SPA_API_POD_PARSER void
 spa_pod_parser_reset(struct spa_pod_parser *parser, struct spa_pod_parser_state *state)
 {
 	parser->state = *state;
 }
 
-static inline struct spa_pod *
+SPA_API_POD_PARSER struct spa_pod *
 spa_pod_parser_deref(struct spa_pod_parser *parser, uint32_t offset, uint32_t size)
 {
 	/* Cast to uint64_t to avoid wraparound.  Add 8 for the pod itself. */
@@ -78,12 +86,12 @@ spa_pod_parser_deref(struct spa_pod_parser *parser, uint32_t offset, uint32_t si
 	return NULL;
 }
 
-static inline struct spa_pod *spa_pod_parser_frame(struct spa_pod_parser *parser, struct spa_pod_frame *frame)
+SPA_API_POD_PARSER struct spa_pod *spa_pod_parser_frame(struct spa_pod_parser *parser, struct spa_pod_frame *frame)
 {
 	return SPA_PTROFF(parser->data, frame->offset, struct spa_pod);
 }
 
-static inline void spa_pod_parser_push(struct spa_pod_parser *parser,
+SPA_API_POD_PARSER void spa_pod_parser_push(struct spa_pod_parser *parser,
 		      struct spa_pod_frame *frame, const struct spa_pod *pod, uint32_t offset)
 {
 	frame->pod = *pod;
@@ -93,19 +101,19 @@ static inline void spa_pod_parser_push(struct spa_pod_parser *parser,
 	parser->state.frame = frame;
 }
 
-static inline struct spa_pod *spa_pod_parser_current(struct spa_pod_parser *parser)
+SPA_API_POD_PARSER struct spa_pod *spa_pod_parser_current(struct spa_pod_parser *parser)
 {
 	struct spa_pod_frame *f = parser->state.frame;
 	uint32_t size = f ? f->offset + SPA_POD_SIZE(&f->pod) : parser->size;
 	return spa_pod_parser_deref(parser, parser->state.offset, size);
 }
 
-static inline void spa_pod_parser_advance(struct spa_pod_parser *parser, const struct spa_pod *pod)
+SPA_API_POD_PARSER void spa_pod_parser_advance(struct spa_pod_parser *parser, const struct spa_pod *pod)
 {
 	parser->state.offset += SPA_ROUND_UP_N(SPA_POD_SIZE(pod), 8);
 }
 
-static inline struct spa_pod *spa_pod_parser_next(struct spa_pod_parser *parser)
+SPA_API_POD_PARSER struct spa_pod *spa_pod_parser_next(struct spa_pod_parser *parser)
 {
 	struct spa_pod *pod = spa_pod_parser_current(parser);
 	if (pod)
@@ -113,7 +121,7 @@ static inline struct spa_pod *spa_pod_parser_next(struct spa_pod_parser *parser)
 	return pod;
 }
 
-static inline int spa_pod_parser_pop(struct spa_pod_parser *parser,
+SPA_API_POD_PARSER int spa_pod_parser_pop(struct spa_pod_parser *parser,
 		      struct spa_pod_frame *frame)
 {
 	parser->state.frame = frame->parent;
@@ -121,7 +129,7 @@ static inline int spa_pod_parser_pop(struct spa_pod_parser *parser,
 	return 0;
 }
 
-static inline int spa_pod_parser_get_bool(struct spa_pod_parser *parser, bool *value)
+SPA_API_POD_PARSER int spa_pod_parser_get_bool(struct spa_pod_parser *parser, bool *value)
 {
 	int res = -EPIPE;
 	const struct spa_pod *pod = spa_pod_parser_current(parser);
@@ -130,7 +138,7 @@ static inline int spa_pod_parser_get_bool(struct spa_pod_parser *parser, bool *v
 	return res;
 }
 
-static inline int spa_pod_parser_get_id(struct spa_pod_parser *parser, uint32_t *value)
+SPA_API_POD_PARSER int spa_pod_parser_get_id(struct spa_pod_parser *parser, uint32_t *value)
 {
 	int res = -EPIPE;
 	const struct spa_pod *pod = spa_pod_parser_current(parser);
@@ -139,7 +147,7 @@ static inline int spa_pod_parser_get_id(struct spa_pod_parser *parser, uint32_t
 	return res;
 }
 
-static inline int spa_pod_parser_get_int(struct spa_pod_parser *parser, int32_t *value)
+SPA_API_POD_PARSER int spa_pod_parser_get_int(struct spa_pod_parser *parser, int32_t *value)
 {
 	int res = -EPIPE;
 	const struct spa_pod *pod = spa_pod_parser_current(parser);
@@ -148,7 +156,7 @@ static inline int spa_pod_parser_get_int(struct spa_pod_parser *parser, int32_t
 	return res;
 }
 
-static inline int spa_pod_parser_get_long(struct spa_pod_parser *parser, int64_t *value)
+SPA_API_POD_PARSER int spa_pod_parser_get_long(struct spa_pod_parser *parser, int64_t *value)
 {
 	int res = -EPIPE;
 	const struct spa_pod *pod = spa_pod_parser_current(parser);
@@ -157,7 +165,7 @@ static inline int spa_pod_parser_get_long(struct spa_pod_parser *parser, int64_t
 	return res;
 }
 
-static inline int spa_pod_parser_get_float(struct spa_pod_parser *parser, float *value)
+SPA_API_POD_PARSER int spa_pod_parser_get_float(struct spa_pod_parser *parser, float *value)
 {
 	int res = -EPIPE;
 	const struct spa_pod *pod = spa_pod_parser_current(parser);
@@ -166,7 +174,7 @@ static inline int spa_pod_parser_get_float(struct spa_pod_parser *parser, float
 	return res;
 }
 
-static inline int spa_pod_parser_get_double(struct spa_pod_parser *parser, double *value)
+SPA_API_POD_PARSER int spa_pod_parser_get_double(struct spa_pod_parser *parser, double *value)
 {
 	int res = -EPIPE;
 	const struct spa_pod *pod = spa_pod_parser_current(parser);
@@ -175,7 +183,7 @@ static inline int spa_pod_parser_get_double(struct spa_pod_parser *parser, doubl
 	return res;
 }
 
-static inline int spa_pod_parser_get_string(struct spa_pod_parser *parser, const char **value)
+SPA_API_POD_PARSER int spa_pod_parser_get_string(struct spa_pod_parser *parser, const char **value)
 {
 	int res = -EPIPE;
 	const struct spa_pod *pod = spa_pod_parser_current(parser);
@@ -184,7 +192,7 @@ static inline int spa_pod_parser_get_string(struct spa_pod_parser *parser, const
 	return res;
 }
 
-static inline int spa_pod_parser_get_bytes(struct spa_pod_parser *parser, const void **value, uint32_t *len)
+SPA_API_POD_PARSER int spa_pod_parser_get_bytes(struct spa_pod_parser *parser, const void **value, uint32_t *len)
 {
 	int res = -EPIPE;
 	const struct spa_pod *pod = spa_pod_parser_current(parser);
@@ -193,7 +201,7 @@ static inline int spa_pod_parser_get_bytes(struct spa_pod_parser *parser, const
 	return res;
 }
 
-static inline int spa_pod_parser_get_pointer(struct spa_pod_parser *parser, uint32_t *type, const void **value)
+SPA_API_POD_PARSER int spa_pod_parser_get_pointer(struct spa_pod_parser *parser, uint32_t *type, const void **value)
 {
 	int res = -EPIPE;
 	const struct spa_pod *pod = spa_pod_parser_current(parser);
@@ -202,7 +210,7 @@ static inline int spa_pod_parser_get_pointer(struct spa_pod_parser *parser, uint
 	return res;
 }
 
-static inline int spa_pod_parser_get_fd(struct spa_pod_parser *parser, int64_t *value)
+SPA_API_POD_PARSER int spa_pod_parser_get_fd(struct spa_pod_parser *parser, int64_t *value)
 {
 	int res = -EPIPE;
 	const struct spa_pod *pod = spa_pod_parser_current(parser);
@@ -211,7 +219,7 @@ static inline int spa_pod_parser_get_fd(struct spa_pod_parser *parser, int64_t *
 	return res;
 }
 
-static inline int spa_pod_parser_get_rectangle(struct spa_pod_parser *parser, struct spa_rectangle *value)
+SPA_API_POD_PARSER int spa_pod_parser_get_rectangle(struct spa_pod_parser *parser, struct spa_rectangle *value)
 {
 	int res = -EPIPE;
 	const struct spa_pod *pod = spa_pod_parser_current(parser);
@@ -220,7 +228,7 @@ static inline int spa_pod_parser_get_rectangle(struct spa_pod_parser *parser, st
 	return res;
 }
 
-static inline int spa_pod_parser_get_fraction(struct spa_pod_parser *parser, struct spa_fraction *value)
+SPA_API_POD_PARSER int spa_pod_parser_get_fraction(struct spa_pod_parser *parser, struct spa_fraction *value)
 {
 	int res = -EPIPE;
 	const struct spa_pod *pod = spa_pod_parser_current(parser);
@@ -229,7 +237,7 @@ static inline int spa_pod_parser_get_fraction(struct spa_pod_parser *parser, str
 	return res;
 }
 
-static inline int spa_pod_parser_get_pod(struct spa_pod_parser *parser, struct spa_pod **value)
+SPA_API_POD_PARSER int spa_pod_parser_get_pod(struct spa_pod_parser *parser, struct spa_pod **value)
 {
 	struct spa_pod *pod = spa_pod_parser_current(parser);
 	if (pod == NULL)
@@ -238,7 +246,7 @@ static inline int spa_pod_parser_get_pod(struct spa_pod_parser *parser, struct s
 	spa_pod_parser_advance(parser, pod);
 	return 0;
 }
-static inline int spa_pod_parser_push_struct(struct spa_pod_parser *parser,
+SPA_API_POD_PARSER int spa_pod_parser_push_struct(struct spa_pod_parser *parser,
 		struct spa_pod_frame *frame)
 {
 	const struct spa_pod *pod = spa_pod_parser_current(parser);
@@ -251,7 +259,7 @@ static inline int spa_pod_parser_push_struct(struct spa_pod_parser *parser,
 	return 0;
 }
 
-static inline int spa_pod_parser_push_object(struct spa_pod_parser *parser,
+SPA_API_POD_PARSER int spa_pod_parser_push_object(struct spa_pod_parser *parser,
 		struct spa_pod_frame *frame, uint32_t type, uint32_t *id)
 {
 	const struct spa_pod *pod = spa_pod_parser_current(parser);
@@ -268,7 +276,7 @@ static inline int spa_pod_parser_push_object(struct spa_pod_parser *parser,
 	return 0;
 }
 
-static inline bool spa_pod_parser_can_collect(const struct spa_pod *pod, char type)
+SPA_API_POD_PARSER bool spa_pod_parser_can_collect(const struct spa_pod *pod, char type)
 {
 	if (pod == NULL)
 		return false;
@@ -443,7 +451,7 @@ do {											\
 	}										\
 } while(false)
 
-static inline int spa_pod_parser_getv(struct spa_pod_parser *parser, va_list args)
+SPA_API_POD_PARSER int spa_pod_parser_getv(struct spa_pod_parser *parser, va_list args)
 {
 	struct spa_pod_frame *f = parser->state.frame;
         uint32_t ftype = f ? f->pod.type : (uint32_t)SPA_TYPE_Struct;
@@ -496,7 +504,7 @@ static inline int spa_pod_parser_getv(struct spa_pod_parser *parser, va_list arg
 	return count;
 }
 
-static inline int spa_pod_parser_get(struct spa_pod_parser *parser, ...)
+SPA_API_POD_PARSER int spa_pod_parser_get(struct spa_pod_parser *parser, ...)
 {
 	int res;
 	va_list args;
diff --git a/spa/include/spa/support/cpu.h b/spa/include/spa/support/cpu.h
index 350dee78..ce8551e7 100644
--- a/spa/include/spa/support/cpu.h
+++ b/spa/include/spa/support/cpu.h
@@ -10,10 +10,19 @@ extern "C" {
 #endif
 
 #include <stdarg.h>
+#include <errno.h>
 
 #include <spa/utils/defs.h>
 #include <spa/utils/hook.h>
 
+#ifndef SPA_API_CPU
+ #ifdef SPA_API_IMPL
+  #define SPA_API_CPU SPA_API_IMPL
+ #else
+  #define SPA_API_CPU static inline
+ #endif
+#endif
+
 /** \defgroup spa_cpu CPU
  * Querying CPU properties
  */
@@ -68,6 +77,9 @@ struct spa_cpu { struct spa_interface iface; };
 #define SPA_CPU_FLAG_NEON		(1 << 5)
 #define SPA_CPU_FLAG_ARMV8		(1 << 6)
 
+/* RISCV specific */
+#define SPA_CPU_FLAG_RISCV_V		(1 << 0)
+
 #define SPA_CPU_FORCE_AUTODETECT	((uint32_t)-1)
 
 #define SPA_CPU_VM_NONE			(0)
@@ -87,7 +99,7 @@ struct spa_cpu { struct spa_interface iface; };
 #define SPA_CPU_VM_ACRN			(1 << 13)
 #define SPA_CPU_VM_POWERVM		(1 << 14)
 
-static inline const char *spa_cpu_vm_type_to_string(uint32_t vm_type)
+SPA_API_CPU const char *spa_cpu_vm_type_to_string(uint32_t vm_type)
 {
 	switch(vm_type) {
 	case SPA_CPU_VM_NONE:
@@ -156,21 +168,30 @@ struct spa_cpu_methods {
 	int (*zero_denormals) (void *object, bool enable);
 };
 
-#define spa_cpu_method(o,method,version,...)				\
-({									\
-	int _res = -ENOTSUP;						\
-	struct spa_cpu *_c = o;						\
-	spa_interface_call_res(&_c->iface,				\
-			struct spa_cpu_methods, _res,			\
-			method, version, ##__VA_ARGS__);		\
-	_res;								\
-})
-#define spa_cpu_get_flags(c)		spa_cpu_method(c, get_flags, 0)
-#define spa_cpu_force_flags(c,f)	spa_cpu_method(c, force_flags, 0, f)
-#define spa_cpu_get_count(c)		spa_cpu_method(c, get_count, 0)
-#define spa_cpu_get_max_align(c)	spa_cpu_method(c, get_max_align, 0)
-#define spa_cpu_get_vm_type(c)		spa_cpu_method(c, get_vm_type, 1)
-#define spa_cpu_zero_denormals(c,e)	spa_cpu_method(c, zero_denormals, 2, e)
+SPA_API_CPU uint32_t spa_cpu_get_flags(struct spa_cpu *c)
+{
+	return spa_api_method_r(uint32_t, 0, spa_cpu, &c->iface, get_flags, 0);
+}
+SPA_API_CPU int spa_cpu_force_flags(struct spa_cpu *c, uint32_t flags)
+{
+	return spa_api_method_r(int, -ENOTSUP, spa_cpu, &c->iface, force_flags, 0, flags);
+}
+SPA_API_CPU uint32_t spa_cpu_get_count(struct spa_cpu *c)
+{
+	return spa_api_method_r(uint32_t, 0, spa_cpu, &c->iface, get_count, 0);
+}
+SPA_API_CPU uint32_t spa_cpu_get_max_align(struct spa_cpu *c)
+{
+	return spa_api_method_r(uint32_t, 0, spa_cpu, &c->iface, get_max_align, 0);
+}
+SPA_API_CPU uint32_t spa_cpu_get_vm_type(struct spa_cpu *c)
+{
+	return spa_api_method_r(uint32_t, 0, spa_cpu, &c->iface, get_vm_type, 1);
+}
+SPA_API_CPU int spa_cpu_zero_denormals(struct spa_cpu *c, bool enable)
+{
+	return spa_api_method_r(int, -ENOTSUP, spa_cpu, &c->iface, zero_denormals, 2, enable);
+}
 
 /** keys can be given when initializing the cpu handle */
 #define SPA_KEY_CPU_FORCE		"cpu.force"		/**< force cpu flags */
diff --git a/spa/include/spa/support/dbus.h b/spa/include/spa/support/dbus.h
index 83deb1d6..3908bfe5 100644
--- a/spa/include/spa/support/dbus.h
+++ b/spa/include/spa/support/dbus.h
@@ -11,6 +11,14 @@ extern "C" {
 
 #include <spa/support/loop.h>
 
+#ifndef SPA_API_DBUS
+ #ifdef SPA_API_IMPL
+  #define SPA_API_DBUS SPA_API_IMPL
+ #else
+  #define SPA_API_DBUS static inline
+ #endif
+#endif
+
 /** \defgroup spa_dbus DBus
  * DBus communication
  */
@@ -79,29 +87,27 @@ struct spa_dbus_connection {
 			void *data);
 };
 
-#define spa_dbus_connection_call(c,method,vers,...)			\
-({									\
-	if (SPA_LIKELY(SPA_CALLBACK_CHECK(c,method,vers)))		\
-		c->method((c), ## __VA_ARGS__);				\
-})
-
-#define spa_dbus_connection_call_vp(c,method,vers,...)			\
-({									\
-	void *_res = NULL;						\
-	if (SPA_LIKELY(SPA_CALLBACK_CHECK(c,method,vers)))		\
-		_res = c->method((c), ## __VA_ARGS__);			\
-	_res;								\
-})
-
 /** \copydoc spa_dbus_connection.get
  * \sa spa_dbus_connection.get */
-#define spa_dbus_connection_get(c)		spa_dbus_connection_call_vp(c,get,0)
+SPA_API_DBUS void *spa_dbus_connection_get(struct spa_dbus_connection *conn)
+{
+	return spa_api_func_r(void *, NULL, conn, get, 0);
+}
 /** \copydoc spa_dbus_connection.destroy
  * \sa spa_dbus_connection.destroy */
-#define spa_dbus_connection_destroy(c)		spa_dbus_connection_call(c,destroy,0)
+SPA_API_DBUS void spa_dbus_connection_destroy(struct spa_dbus_connection *conn)
+{
+	spa_api_func_v(conn, destroy, 0);
+}
 /** \copydoc spa_dbus_connection.add_listener
  * \sa spa_dbus_connection.add_listener */
-#define spa_dbus_connection_add_listener(c,...)	spa_dbus_connection_call(c,add_listener,1,__VA_ARGS__)
+SPA_API_DBUS void spa_dbus_connection_add_listener(struct spa_dbus_connection *conn,
+		struct spa_hook *listener,
+		const struct spa_dbus_connection_events *events,
+		void *data)
+{
+	spa_api_func_v(conn, add_listener, 1, listener, events, data);
+}
 
 struct spa_dbus_methods {
 #define SPA_VERSION_DBUS_METHODS	0
@@ -126,14 +132,11 @@ struct spa_dbus_methods {
 /** \copydoc spa_dbus_methods.get_connection
  * \sa spa_dbus_methods.get_connection
  */
-static inline struct spa_dbus_connection *
+SPA_API_DBUS struct spa_dbus_connection *
 spa_dbus_get_connection(struct spa_dbus *dbus, enum spa_dbus_type type)
 {
-	struct spa_dbus_connection *res = NULL;
-	spa_interface_call_res(&dbus->iface,
-                        struct spa_dbus_methods, res,
-			get_connection, 0, type);
-	return res;
+	return spa_api_method_r(struct spa_dbus_connection *, NULL,
+			spa_dbus, &dbus->iface, get_connection, 0, type);
 }
 
 /**
diff --git a/spa/include/spa/support/i18n.h b/spa/include/spa/support/i18n.h
index 56660e68..3b258873 100644
--- a/spa/include/spa/support/i18n.h
+++ b/spa/include/spa/support/i18n.h
@@ -12,6 +12,14 @@ extern "C" {
 #include <spa/utils/hook.h>
 #include <spa/utils/defs.h>
 
+#ifndef SPA_API_I18N
+ #ifdef SPA_API_IMPL
+  #define SPA_API_I18N SPA_API_IMPL
+ #else
+  #define SPA_API_I18N static inline
+ #endif
+#endif
+
 /** \defgroup spa_i18n I18N
  * Gettext interface
  */
@@ -53,27 +61,19 @@ struct spa_i18n_methods {
 };
 
 SPA_FORMAT_ARG_FUNC(2)
-static inline const char *
+SPA_API_I18N const char *
 spa_i18n_text(struct spa_i18n *i18n, const char *msgid)
 {
-	const char *res = msgid;
-	if (SPA_LIKELY(i18n != NULL))
-		spa_interface_call_res(&i18n->iface,
-	                        struct spa_i18n_methods, res,
-				text, 0, msgid);
-	return res;
+	return spa_api_method_null_r(const char *, msgid, spa_i18n, i18n, &i18n->iface,
+			text, 0, msgid);
 }
 
-static inline const char *
+SPA_API_I18N const char *
 spa_i18n_ntext(struct spa_i18n *i18n, const char *msgid,
 		const char *msgid_plural, unsigned long int n)
 {
-	const char *res = n == 1 ? msgid : msgid_plural;
-	if (SPA_LIKELY(i18n != NULL))
-		spa_interface_call_res(&i18n->iface,
-	                        struct spa_i18n_methods, res,
-				ntext, 0, msgid, msgid_plural, n);
-	return res;
+	return spa_api_method_null_r(const char *, n == 1 ? msgid : msgid_plural,
+			spa_i18n, i18n, &i18n->iface, ntext, 0, msgid, msgid_plural, n);
 }
 
 /**
diff --git a/spa/include/spa/support/log.h b/spa/include/spa/support/log.h
index fba50e75..e1441440 100644
--- a/spa/include/spa/support/log.h
+++ b/spa/include/spa/support/log.h
@@ -15,6 +15,14 @@ extern "C" {
 #include <spa/utils/defs.h>
 #include <spa/utils/hook.h>
 
+#ifndef SPA_API_LOG
+ #ifdef SPA_API_IMPL
+  #define SPA_API_LOG SPA_API_IMPL
+ #else
+  #define SPA_API_LOG static inline
+ #endif
+#endif
+
 /** \defgroup spa_log Log
  * Logging interface
  */
@@ -213,7 +221,7 @@ struct spa_log_methods {
 #define SPA_LOG_TOPIC(v, t) \
    (struct spa_log_topic){ .version = (v), .topic = (t)}
 
-static inline void spa_log_topic_init(struct spa_log *log, struct spa_log_topic *topic)
+SPA_API_LOG void spa_log_topic_init(struct spa_log *log, struct spa_log_topic *topic)
 {
 	if (SPA_UNLIKELY(!log))
 		return;
@@ -221,7 +229,7 @@ static inline void spa_log_topic_init(struct spa_log *log, struct spa_log_topic
 	spa_interface_call(&log->iface, struct spa_log_methods, topic_init, 1, topic);
 }
 
-static inline bool spa_log_level_topic_enabled(const struct spa_log *log,
+SPA_API_LOG bool spa_log_level_topic_enabled(const struct spa_log *log,
 					       const struct spa_log_topic *topic,
 					       enum spa_log_level level)
 {
@@ -255,20 +263,22 @@ static inline bool spa_log_level_topic_enabled(const struct spa_log *log,
 })
 
 /* Transparently calls to version 0 logv if v1 is not supported */
-#define spa_log_logtv(l,lev,topic,...)					\
-({									\
-	struct spa_log *_l = l;						\
-	if (SPA_UNLIKELY(spa_log_level_topic_enabled(_l, topic, lev))) { \
-		struct spa_interface *_if = &_l->iface;			\
-		if (!spa_interface_call(_if,				\
-				struct spa_log_methods, logtv, 1,	\
-				lev, topic,				\
-				__VA_ARGS__))				\
-		    spa_interface_call(_if,				\
-				struct spa_log_methods, logv, 0,	\
-				lev, __VA_ARGS__);			\
-	}								\
-})
+SPA_PRINTF_FUNC(7, 0)
+SPA_API_LOG void spa_log_logtv(struct spa_log *l, enum spa_log_level level,
+		const struct spa_log_topic *topic, const char *file, int line,
+		const char *func, const char *fmt, va_list args)
+{
+	if (SPA_UNLIKELY(spa_log_level_topic_enabled(l, topic, level))) {
+		struct spa_interface *i = &l->iface;
+		if (!spa_interface_call(i,
+				struct spa_log_methods, logtv, 1,
+				level, topic,
+				file, line, func, fmt, args))
+		    spa_interface_call(i,
+				struct spa_log_methods, logv, 0,
+				level, file, line, func, fmt, args);
+	}
+}
 
 #define spa_logt_lev(l,lev,t,...)					\
 	spa_log_logt(l,lev,t,__FILE__,__LINE__,__func__,__VA_ARGS__)
@@ -369,7 +379,8 @@ static inline bool spa_log_level_topic_enabled(const struct spa_log *log,
 								  *  colors even when not logging to a terminal */
 #define SPA_KEY_LOG_FILE		"log.file"		/**< log to the specified file instead of
 								  *  stderr. */
-#define SPA_KEY_LOG_TIMESTAMP		"log.timestamp"		/**< log timestamps */
+#define SPA_KEY_LOG_TIMESTAMP		"log.timestamp"		/**< log timestamp type (local, realtime, monotonic, monotonic-raw).
+								 *   boolean true means local. */
 #define SPA_KEY_LOG_LINE		"log.line"		/**< log file and line numbers */
 #define SPA_KEY_LOG_PATTERNS		"log.patterns"		/**< Spa:String:JSON array of [ {"pattern" : level}, ... ] */
 
diff --git a/spa/include/spa/support/loop.h b/spa/include/spa/support/loop.h
index 7dc55f3a..520a465d 100644
--- a/spa/include/spa/support/loop.h
+++ b/spa/include/spa/support/loop.h
@@ -9,10 +9,20 @@
 extern "C" {
 #endif
 
+#include <errno.h>
+
 #include <spa/utils/defs.h>
 #include <spa/utils/hook.h>
 #include <spa/support/system.h>
 
+#ifndef SPA_API_LOOP
+ #ifdef SPA_API_IMPL
+  #define SPA_API_LOOP SPA_API_IMPL
+ #else
+  #define SPA_API_LOOP static inline
+ #endif
+#endif
+
 /** \defgroup spa_loop Loop
  * Event loop interface
  */
@@ -125,21 +135,29 @@ struct spa_loop_methods {
 		       void *user_data);
 };
 
-#define spa_loop_method(o,method,version,...)				\
-({									\
-	int _res = -ENOTSUP;						\
-	struct spa_loop *_o = o;					\
-	spa_interface_call_res(&_o->iface,				\
-			struct spa_loop_methods, _res,			\
-			method, version, ##__VA_ARGS__);		\
-	_res;								\
-})
-
-#define spa_loop_add_source(l,...)	spa_loop_method(l,add_source,0,##__VA_ARGS__)
-#define spa_loop_update_source(l,...)	spa_loop_method(l,update_source,0,##__VA_ARGS__)
-#define spa_loop_remove_source(l,...)	spa_loop_method(l,remove_source,0,##__VA_ARGS__)
-#define spa_loop_invoke(l,...)		spa_loop_method(l,invoke,0,##__VA_ARGS__)
-
+SPA_API_LOOP int spa_loop_add_source(struct spa_loop *object, struct spa_source *source)
+{
+	return spa_api_method_r(int, -ENOTSUP,
+			spa_loop, &object->iface, add_source, 0, source);
+}
+SPA_API_LOOP int spa_loop_update_source(struct spa_loop *object, struct spa_source *source)
+{
+	return spa_api_method_r(int, -ENOTSUP,
+			spa_loop, &object->iface, update_source, 0, source);
+}
+SPA_API_LOOP int spa_loop_remove_source(struct spa_loop *object, struct spa_source *source)
+{
+	return spa_api_method_r(int, -ENOTSUP,
+			spa_loop, &object->iface, remove_source, 0, source);
+}
+SPA_API_LOOP int spa_loop_invoke(struct spa_loop *object,
+		spa_invoke_func_t func, uint32_t seq, const void *data,
+		size_t size, bool block, void *user_data)
+{
+	return spa_api_method_r(int, -ENOTSUP,
+			spa_loop, &object->iface, invoke, 0, func, seq, data,
+			size, block, user_data);
+}
 
 /** Control hooks. These hooks can't be removed from their
  *  callbacks and must be removed from a safe place (when the loop
@@ -155,24 +173,42 @@ struct spa_loop_control_hooks {
 	void (*after) (void *data);
 };
 
-#define spa_loop_control_hook_before(l)							\
-({											\
-	struct spa_hook_list *_l = l;							\
-	struct spa_hook *_h;								\
-	spa_list_for_each_reverse(_h, &_l->list, link)					\
-		spa_callbacks_call_fast(&_h->cb, struct spa_loop_control_hooks, before, 0);	\
-})
-
-#define spa_loop_control_hook_after(l)							\
-({											\
-	struct spa_hook_list *_l = l;							\
-	struct spa_hook *_h;								\
-	spa_list_for_each(_h, &_l->list, link)						\
-		spa_callbacks_call_fast(&_h->cb, struct spa_loop_control_hooks, after, 0);	\
-})
+SPA_API_LOOP void spa_loop_control_hook_before(struct spa_hook_list *l)
+{
+	struct spa_hook *h;
+	spa_list_for_each_reverse(h, &l->list, link)
+		spa_callbacks_call_fast(&h->cb, struct spa_loop_control_hooks, before, 0);
+}
+
+SPA_API_LOOP void spa_loop_control_hook_after(struct spa_hook_list *l)
+{
+	struct spa_hook *h;
+	spa_list_for_each(h, &l->list, link)
+		spa_callbacks_call_fast(&h->cb, struct spa_loop_control_hooks, after, 0);
+}
 
 /**
  * Control an event loop
+ *
+ * The event loop control function provide API to run the event loop.
+ *
+ * The below (pseudo)code is a minimal example outlining the use of the loop
+ * control:
+ * \code{.c}
+ * spa_loop_control_enter(loop);
+ * while (running) {
+ *   spa_loop_control_iterate(loop, -1);
+ * }
+ * spa_loop_control_leave(loop);
+ * \endcode
+ *
+ * It is also possible to add the loop to an existing event loop by using the
+ * spa_loop_control_get_fd() call. This fd will become readable when activity
+ * has been detected on the sources in the loop. spa_loop_control_iterate() with
+ * a 0 timeout should be called to process the pending sources.
+ *
+ * spa_loop_control_enter() and spa_loop_control_leave() should be called once
+ * from the thread that will run the iterate() function.
  */
 struct spa_loop_control_methods {
 	/* the version of this structure. This can be used to expand this
@@ -180,10 +216,19 @@ struct spa_loop_control_methods {
 #define SPA_VERSION_LOOP_CONTROL_METHODS	1
 	uint32_t version;
 
+	/** get the loop fd
+	 * \param object the control to query
+	 *
+	 * Get the fd of this loop control. This fd will be readable when a
+	 * source in the loop has activity. The user should call iterate()
+	 * with a 0 timeout to schedule one iteration of the loop and dispatch
+	 * the sources.
+	 * \return the fd of the loop
+	 */
 	int (*get_fd) (void *object);
 
 	/** Add a hook
-	 * \param ctrl the control to change
+	 * \param object the control to change
 	 * \param hooks the hooks to add
 	 *
 	 * Adds hooks to the loop controlled by \a ctrl.
@@ -194,18 +239,19 @@ struct spa_loop_control_methods {
 			  void *data);
 
 	/** Enter a loop
-	 * \param ctrl the control
+	 * \param object the control
 	 *
-	 * Start an iteration of the loop. This function should be called
-	 * before calling iterate and is typically used to capture the thread
-	 * that this loop will run in.
+	 * This function should be called before calling iterate and is
+	 * typically used to capture the thread that this loop will run in.
+	 * It should ideally be called once from the thread that will run
+	 * the loop.
 	 */
 	void (*enter) (void *object);
 	/** Leave a loop
-	 * \param ctrl the control
+	 * \param object the control
 	 *
-	 * Ends the iteration of a loop. This should be called after calling
-	 * iterate.
+	 * It should ideally be called once after calling iterate when the loop
+	 * will no longer be iterated from the thread that called enter().
 	 */
 	void (*leave) (void *object);
 
@@ -231,42 +277,43 @@ struct spa_loop_control_methods {
 	int (*check) (void *object);
 };
 
-#define spa_loop_control_method_v(o,method,version,...)			\
-({									\
-	struct spa_loop_control *_o = o;				\
-	spa_interface_call(&_o->iface,					\
-			struct spa_loop_control_methods,		\
-			method, version, ##__VA_ARGS__);		\
-})
-
-#define spa_loop_control_method_r(o,method,version,...)			\
-({									\
-	int _res = -ENOTSUP;						\
-	struct spa_loop_control *_o = o;				\
-	spa_interface_call_res(&_o->iface,				\
-			struct spa_loop_control_methods, _res,		\
-			method, version, ##__VA_ARGS__);		\
-	_res;								\
-})
-
-#define spa_loop_control_method_fast_r(o,method,version,...)		\
-({									\
-	int _res;							\
-	struct spa_loop_control *_o = o;				\
-	spa_interface_call_fast_res(&_o->iface,				\
-			struct spa_loop_control_methods, _res,		\
-			method, version, ##__VA_ARGS__);		\
-	_res;								\
-})
-
-#define spa_loop_control_get_fd(l)		spa_loop_control_method_r(l,get_fd,0)
-#define spa_loop_control_add_hook(l,...)	spa_loop_control_method_v(l,add_hook,0,__VA_ARGS__)
-#define spa_loop_control_enter(l)		spa_loop_control_method_v(l,enter,0)
-#define spa_loop_control_leave(l)		spa_loop_control_method_v(l,leave,0)
-#define spa_loop_control_iterate(l,...)		spa_loop_control_method_r(l,iterate,0,__VA_ARGS__)
-#define spa_loop_control_check(l)		spa_loop_control_method_r(l,check,1)
-
-#define spa_loop_control_iterate_fast(l,...)	spa_loop_control_method_fast_r(l,iterate,0,__VA_ARGS__)
+SPA_API_LOOP int spa_loop_control_get_fd(struct spa_loop_control *object)
+{
+	return spa_api_method_r(int, -ENOTSUP,
+			spa_loop_control, &object->iface, get_fd, 0);
+}
+SPA_API_LOOP void spa_loop_control_add_hook(struct spa_loop_control *object,
+		struct spa_hook *hook, const struct spa_loop_control_hooks *hooks,
+		void *data)
+{
+	spa_api_method_v(spa_loop_control, &object->iface, add_hook, 0,
+			hook, hooks, data);
+}
+SPA_API_LOOP void spa_loop_control_enter(struct spa_loop_control *object)
+{
+	spa_api_method_v(spa_loop_control, &object->iface, enter, 0);
+}
+SPA_API_LOOP void spa_loop_control_leave(struct spa_loop_control *object)
+{
+	spa_api_method_v(spa_loop_control, &object->iface, leave, 0);
+}
+SPA_API_LOOP int spa_loop_control_iterate(struct spa_loop_control *object,
+		int timeout)
+{
+	return spa_api_method_r(int, -ENOTSUP,
+			spa_loop_control, &object->iface, iterate, 0, timeout);
+}
+SPA_API_LOOP int spa_loop_control_iterate_fast(struct spa_loop_control *object,
+		int timeout)
+{
+	return spa_api_method_fast_r(int, -ENOTSUP,
+			spa_loop_control, &object->iface, iterate, 0, timeout);
+}
+SPA_API_LOOP int spa_loop_control_check(struct spa_loop_control *object)
+{
+	return spa_api_method_r(int, -ENOTSUP,
+			spa_loop_control, &object->iface, check, 1);
+}
 
 typedef void (*spa_source_io_func_t) (void *data, int fd, uint32_t mask);
 typedef void (*spa_source_idle_func_t) (void *data);
@@ -317,44 +364,71 @@ struct spa_loop_utils_methods {
 	void (*destroy_source) (void *object, struct spa_source *source);
 };
 
-#define spa_loop_utils_method_v(o,method,version,...)			\
-({									\
-	struct spa_loop_utils *_o = o;					\
-	spa_interface_call(&_o->iface,					\
-			struct spa_loop_utils_methods,			\
-			method, version, ##__VA_ARGS__);		\
-})
-
-#define spa_loop_utils_method_r(o,method,version,...)			\
-({									\
-	int _res = -ENOTSUP;						\
-	struct spa_loop_utils *_o = o;					\
-	spa_interface_call_res(&_o->iface,				\
-			struct spa_loop_utils_methods, _res,		\
-			method, version, ##__VA_ARGS__);		\
-	_res;								\
-})
-#define spa_loop_utils_method_s(o,method,version,...)			\
-({									\
-	struct spa_source *_res = NULL;					\
-	struct spa_loop_utils *_o = o;					\
-	spa_interface_call_res(&_o->iface,				\
-			struct spa_loop_utils_methods, _res,		\
-			method, version, ##__VA_ARGS__);		\
-	_res;								\
-})
-
-
-#define spa_loop_utils_add_io(l,...)		spa_loop_utils_method_s(l,add_io,0,__VA_ARGS__)
-#define spa_loop_utils_update_io(l,...)		spa_loop_utils_method_r(l,update_io,0,__VA_ARGS__)
-#define spa_loop_utils_add_idle(l,...)		spa_loop_utils_method_s(l,add_idle,0,__VA_ARGS__)
-#define spa_loop_utils_enable_idle(l,...)	spa_loop_utils_method_r(l,enable_idle,0,__VA_ARGS__)
-#define spa_loop_utils_add_event(l,...)		spa_loop_utils_method_s(l,add_event,0,__VA_ARGS__)
-#define spa_loop_utils_signal_event(l,...)	spa_loop_utils_method_r(l,signal_event,0,__VA_ARGS__)
-#define spa_loop_utils_add_timer(l,...)		spa_loop_utils_method_s(l,add_timer,0,__VA_ARGS__)
-#define spa_loop_utils_update_timer(l,...)	spa_loop_utils_method_r(l,update_timer,0,__VA_ARGS__)
-#define spa_loop_utils_add_signal(l,...)	spa_loop_utils_method_s(l,add_signal,0,__VA_ARGS__)
-#define spa_loop_utils_destroy_source(l,...)	spa_loop_utils_method_v(l,destroy_source,0,__VA_ARGS__)
+SPA_API_LOOP struct spa_source *
+spa_loop_utils_add_io(struct spa_loop_utils *object, int fd, uint32_t mask,
+		bool close, spa_source_io_func_t func, void *data)
+{
+	return spa_api_method_r(struct spa_source *, NULL,
+			spa_loop_utils, &object->iface, add_io, 0, fd, mask, close, func, data);
+}
+SPA_API_LOOP int spa_loop_utils_update_io(struct spa_loop_utils *object,
+		struct spa_source *source, uint32_t mask)
+{
+	return spa_api_method_r(int, -ENOTSUP,
+			spa_loop_utils, &object->iface, update_io, 0, source, mask);
+}
+SPA_API_LOOP struct spa_source *
+spa_loop_utils_add_idle(struct spa_loop_utils *object, bool enabled,
+		spa_source_idle_func_t func, void *data)
+{
+	return spa_api_method_r(struct spa_source *, NULL,
+			spa_loop_utils, &object->iface, add_idle, 0, enabled, func, data);
+}
+SPA_API_LOOP int spa_loop_utils_enable_idle(struct spa_loop_utils *object,
+		struct spa_source *source, bool enabled)
+{
+	return spa_api_method_r(int, -ENOTSUP,
+			spa_loop_utils, &object->iface, enable_idle, 0, source, enabled);
+}
+SPA_API_LOOP struct spa_source *
+spa_loop_utils_add_event(struct spa_loop_utils *object, spa_source_event_func_t func, void *data)
+{
+	return spa_api_method_r(struct spa_source *, NULL,
+			spa_loop_utils, &object->iface, add_event, 0, func, data);
+}
+SPA_API_LOOP int spa_loop_utils_signal_event(struct spa_loop_utils *object,
+		struct spa_source *source)
+{
+	return spa_api_method_r(int, -ENOTSUP,
+			spa_loop_utils, &object->iface, signal_event, 0, source);
+}
+SPA_API_LOOP struct spa_source *
+spa_loop_utils_add_timer(struct spa_loop_utils *object, spa_source_timer_func_t func, void *data)
+{
+	return spa_api_method_r(struct spa_source *, NULL,
+			spa_loop_utils, &object->iface, add_timer, 0, func, data);
+}
+SPA_API_LOOP int spa_loop_utils_update_timer(struct spa_loop_utils *object,
+		struct spa_source *source, struct timespec *value,
+		struct timespec *interval, bool absolute)
+{
+	return spa_api_method_r(int, -ENOTSUP,
+			spa_loop_utils, &object->iface, update_timer, 0, source,
+			value, interval, absolute);
+}
+SPA_API_LOOP struct spa_source *
+spa_loop_utils_add_signal(struct spa_loop_utils *object, int signal_number,
+		spa_source_signal_func_t func, void *data)
+{
+	return spa_api_method_r(struct spa_source *, NULL,
+			spa_loop_utils, &object->iface, add_signal, 0,
+			signal_number, func, data);
+}
+SPA_API_LOOP void spa_loop_utils_destroy_source(struct spa_loop_utils *object,
+		struct spa_source *source)
+{
+	spa_api_method_v(spa_loop_utils, &object->iface, destroy_source, 0, source);
+}
 
 /**
  * \}
diff --git a/spa/include/spa/support/plugin-loader.h b/spa/include/spa/support/plugin-loader.h
index 9b32ebe1..9540853c 100644
--- a/spa/include/spa/support/plugin-loader.h
+++ b/spa/include/spa/support/plugin-loader.h
@@ -12,6 +12,14 @@ extern "C" {
 #include <spa/utils/hook.h>
 #include <spa/utils/dict.h>
 
+#ifndef SPA_API_PLUGIN_LOADER
+ #ifdef SPA_API_IMPL
+  #define SPA_API_PLUGIN_LOADER SPA_API_IMPL
+ #else
+  #define SPA_API_PLUGIN_LOADER static inline
+ #endif
+#endif
+
 /** \defgroup spa_plugin_loader Plugin Loader
  * SPA plugin loader
  */
@@ -48,26 +56,18 @@ struct spa_plugin_loader_methods {
 	int (*unload)(void *object, struct spa_handle *handle);
 };
 
-static inline struct spa_handle *
+SPA_API_PLUGIN_LOADER struct spa_handle *
 spa_plugin_loader_load(struct spa_plugin_loader *loader, const char *factory_name, const struct spa_dict *info)
 {
-	struct spa_handle *res = NULL;
-	if (SPA_LIKELY(loader != NULL))
-		spa_interface_call_res(&loader->iface,
-				struct spa_plugin_loader_methods, res,
-				load, 0, factory_name, info);
-	return res;
+	return spa_api_method_null_r(struct spa_handle *, NULL, spa_plugin_loader, loader, &loader->iface,
+			load, 0, factory_name, info);
 }
 
-static inline int
+SPA_API_PLUGIN_LOADER int
 spa_plugin_loader_unload(struct spa_plugin_loader *loader, struct spa_handle *handle)
 {
-	int res = -1;
-	if (SPA_LIKELY(loader != NULL))
-		spa_interface_call_res(&loader->iface,
-				struct spa_plugin_loader_methods, res,
-				unload, 0, handle);
-	return res;
+	return spa_api_method_null_r(int, -1, spa_plugin_loader, loader, &loader->iface,
+			unload, 0, handle);
 }
 
 /**
diff --git a/spa/include/spa/support/plugin.h b/spa/include/spa/support/plugin.h
index e2e6d469..576c1950 100644
--- a/spa/include/spa/support/plugin.h
+++ b/spa/include/spa/support/plugin.h
@@ -9,9 +9,20 @@
 extern "C" {
 #endif
 
+#include <errno.h>
+
 #include <spa/utils/defs.h>
+#include <spa/utils/hook.h>
 #include <spa/utils/dict.h>
 
+#ifndef SPA_API_PLUGIN
+ #ifdef SPA_API_IMPL
+  #define SPA_API_PLUGIN SPA_API_IMPL
+ #else
+  #define SPA_API_PLUGIN static inline
+ #endif
+#endif
+
 /**
  * \defgroup spa_handle Plugin Handle
  * SPA plugin handle and factory interfaces
@@ -35,12 +46,12 @@ struct spa_handle {
 	 *
 	 * \param handle a spa_handle
 	 * \param type the interface type
-	 * \param interface result to hold the interface.
+	 * \param iface result to hold the interface.
 	 * \return 0 on success
 	 *         -ENOTSUP when there are no interfaces
 	 *         -EINVAL when handle or info is NULL
 	 */
-	int (*get_interface) (struct spa_handle *handle, const char *type, void **interface);
+	int (*get_interface) (struct spa_handle *handle, const char *type, void **iface);
 	/**
 	 * Clean up the memory of \a handle. After this, \a handle should not be used
 	 * anymore.
@@ -51,8 +62,17 @@ struct spa_handle {
 	int (*clear) (struct spa_handle *handle);
 };
 
-#define spa_handle_get_interface(h,...)	(h)->get_interface((h),__VA_ARGS__)
-#define spa_handle_clear(h)		(h)->clear((h))
+SPA_API_PLUGIN int
+spa_handle_get_interface(struct spa_handle *object,
+		const char *type, void **iface)
+{
+	return spa_api_func_r(int, -ENOTSUP, object, get_interface, 0, type, iface);
+}
+SPA_API_PLUGIN int
+spa_handle_clear(struct spa_handle *object)
+{
+	return spa_api_func_r(int, -ENOTSUP, object, clear, 0);
+}
 
 /**
  * This structure lists the information about available interfaces on
@@ -73,7 +93,7 @@ struct spa_support {
 };
 
 /** Find a support item of the given type */
-static inline void *spa_support_find(const struct spa_support *support,
+SPA_API_PLUGIN void *spa_support_find(const struct spa_support *support,
 				     uint32_t n_support,
 				     const char *type)
 {
@@ -158,9 +178,27 @@ struct spa_handle_factory {
 				    uint32_t *index);
 };
 
-#define spa_handle_factory_get_size(h,...)		(h)->get_size((h),__VA_ARGS__)
-#define spa_handle_factory_init(h,...)			(h)->init((h),__VA_ARGS__)
-#define spa_handle_factory_enum_interface_info(h,...)	(h)->enum_interface_info((h),__VA_ARGS__)
+SPA_API_PLUGIN size_t
+spa_handle_factory_get_size(const struct spa_handle_factory *object,
+		const struct spa_dict *params)
+{
+	return spa_api_func_r(size_t, 0, object, get_size, 1, params);
+}
+SPA_API_PLUGIN int
+spa_handle_factory_init(const struct spa_handle_factory *object,
+		struct spa_handle *handle, const struct spa_dict *info,
+		const struct spa_support *support, uint32_t n_support)
+{
+	return spa_api_func_r(int, -ENOTSUP, object, init, 1, handle, info,
+			support, n_support);
+}
+SPA_API_PLUGIN int
+spa_handle_factory_enum_interface_info(const struct spa_handle_factory *object,
+		const struct spa_interface_info **info, uint32_t *index)
+{
+	return spa_api_func_r(int, -ENOTSUP, object, enum_interface_info, 1,
+			info, index);
+}
 
 /**
  * The function signature of the entry point in a plugin.
diff --git a/spa/include/spa/support/system.h b/spa/include/spa/support/system.h
index 9ea41bce..aa140c95 100644
--- a/spa/include/spa/support/system.h
+++ b/spa/include/spa/support/system.h
@@ -12,11 +12,20 @@ extern "C" {
 struct itimerspec;
 
 #include <time.h>
+#include <errno.h>
 #include <sys/types.h>
 
 #include <spa/utils/defs.h>
 #include <spa/utils/hook.h>
 
+#ifndef SPA_API_SYSTEM
+ #ifdef SPA_API_IMPL
+  #define SPA_API_SYSTEM SPA_API_IMPL
+ #else
+  #define SPA_API_SYSTEM static inline
+ #endif
+#endif
+
 /** \defgroup spa_system System
  * I/O, clock, polling, timer, and signal interfaces
  */
@@ -97,41 +106,106 @@ struct spa_system_methods {
 	int (*signalfd_read) (void *object, int fd, int *signal);
 };
 
-#define spa_system_method_r(o,method,version,...)			\
-({									\
-	volatile int _res = -ENOTSUP;					\
-	struct spa_system *_o = o;					\
-	spa_interface_call_fast_res(&_o->iface,				\
-			struct spa_system_methods, _res,		\
-			method, version, ##__VA_ARGS__);		\
-	_res;								\
-})
-
-#define spa_system_read(s,...)			spa_system_method_r(s,read,0,__VA_ARGS__)
-#define spa_system_write(s,...)			spa_system_method_r(s,write,0,__VA_ARGS__)
-#define spa_system_ioctl(s,...)			spa_system_method_r(s,ioctl,0,__VA_ARGS__)
-#define spa_system_close(s,...)			spa_system_method_r(s,close,0,__VA_ARGS__)
-
-#define spa_system_clock_gettime(s,...)		spa_system_method_r(s,clock_gettime,0,__VA_ARGS__)
-#define spa_system_clock_getres(s,...)		spa_system_method_r(s,clock_getres,0,__VA_ARGS__)
-
-#define spa_system_pollfd_create(s,...)		spa_system_method_r(s,pollfd_create,0,__VA_ARGS__)
-#define spa_system_pollfd_add(s,...)		spa_system_method_r(s,pollfd_add,0,__VA_ARGS__)
-#define spa_system_pollfd_mod(s,...)		spa_system_method_r(s,pollfd_mod,0,__VA_ARGS__)
-#define spa_system_pollfd_del(s,...)		spa_system_method_r(s,pollfd_del,0,__VA_ARGS__)
-#define spa_system_pollfd_wait(s,...)		spa_system_method_r(s,pollfd_wait,0,__VA_ARGS__)
-
-#define spa_system_timerfd_create(s,...)	spa_system_method_r(s,timerfd_create,0,__VA_ARGS__)
-#define spa_system_timerfd_settime(s,...)	spa_system_method_r(s,timerfd_settime,0,__VA_ARGS__)
-#define spa_system_timerfd_gettime(s,...)	spa_system_method_r(s,timerfd_gettime,0,__VA_ARGS__)
-#define spa_system_timerfd_read(s,...)		spa_system_method_r(s,timerfd_read,0,__VA_ARGS__)
-
-#define spa_system_eventfd_create(s,...)	spa_system_method_r(s,eventfd_create,0,__VA_ARGS__)
-#define spa_system_eventfd_write(s,...)		spa_system_method_r(s,eventfd_write,0,__VA_ARGS__)
-#define spa_system_eventfd_read(s,...)		spa_system_method_r(s,eventfd_read,0,__VA_ARGS__)
-
-#define spa_system_signalfd_create(s,...)	spa_system_method_r(s,signalfd_create,0,__VA_ARGS__)
-#define spa_system_signalfd_read(s,...)		spa_system_method_r(s,signalfd_read,0,__VA_ARGS__)
+SPA_API_SYSTEM ssize_t spa_system_read(struct spa_system *object, int fd, void *buf, size_t count)
+{
+	return spa_api_method_fast_r(ssize_t, -ENOTSUP, spa_system, &object->iface, read, 0, fd, buf, count);
+}
+SPA_API_SYSTEM ssize_t spa_system_write(struct spa_system *object, int fd, const void *buf, size_t count)
+{
+	return spa_api_method_fast_r(ssize_t, -ENOTSUP, spa_system, &object->iface, write, 0, fd, buf, count);
+}
+#define spa_system_ioctl(object,fd,request,...)	\
+	spa_api_method_fast_r(int, -ENOTSUP, spa_system, &object->iface, ioctl, 0, fd, request, ##__VA_ARGS__)
+
+SPA_API_SYSTEM int spa_system_close(struct spa_system *object, int fd)
+{
+	return spa_api_method_fast_r(int, -ENOTSUP, spa_system, &object->iface, close, 0, fd);
+}
+SPA_API_SYSTEM int spa_system_clock_gettime(struct spa_system *object,
+			int clockid, struct timespec *value)
+{
+	return spa_api_method_fast_r(int, -ENOTSUP, spa_system, &object->iface, clock_gettime, 0, clockid, value);
+}
+SPA_API_SYSTEM int spa_system_clock_getres(struct spa_system *object,
+			int clockid, struct timespec *res)
+{
+	return spa_api_method_fast_r(int, -ENOTSUP, spa_system, &object->iface, clock_getres, 0, clockid, res);
+}
+
+SPA_API_SYSTEM int spa_system_pollfd_create(struct spa_system *object, int flags)
+{
+	return spa_api_method_fast_r(int, -ENOTSUP, spa_system, &object->iface, pollfd_create, 0, flags);
+}
+SPA_API_SYSTEM int spa_system_pollfd_add(struct spa_system *object, int pfd, int fd, uint32_t events, void *data)
+{
+	return spa_api_method_fast_r(int, -ENOTSUP, spa_system, &object->iface, pollfd_add, 0, pfd, fd, events, data);
+}
+SPA_API_SYSTEM int spa_system_pollfd_mod(struct spa_system *object, int pfd, int fd, uint32_t events, void *data)
+{
+	return spa_api_method_fast_r(int, -ENOTSUP, spa_system, &object->iface, pollfd_mod, 0, pfd, fd, events, data);
+}
+SPA_API_SYSTEM int spa_system_pollfd_del(struct spa_system *object, int pfd, int fd)
+{
+	return spa_api_method_fast_r(int, -ENOTSUP, spa_system, &object->iface, pollfd_del, 0, pfd, fd);
+}
+SPA_API_SYSTEM int spa_system_pollfd_wait(struct spa_system *object, int pfd,
+			struct spa_poll_event *ev, int n_ev, int timeout)
+{
+	return spa_api_method_fast_r(int, -ENOTSUP, spa_system, &object->iface, pollfd_wait, 0, pfd, ev, n_ev, timeout);
+}
+
+SPA_API_SYSTEM int spa_system_timerfd_create(struct spa_system *object, int clockid, int flags)
+{
+	return spa_api_method_fast_r(int, -ENOTSUP, spa_system, &object->iface, timerfd_create, 0, clockid, flags);
+}
+
+SPA_API_SYSTEM int spa_system_timerfd_settime(struct spa_system *object,
+			int fd, int flags,
+			const struct itimerspec *new_value,
+			struct itimerspec *old_value)
+{
+	return spa_api_method_fast_r(int, -ENOTSUP, spa_system, &object->iface, timerfd_settime, 0,
+			fd, flags, new_value, old_value);
+}
+
+SPA_API_SYSTEM int spa_system_timerfd_gettime(struct spa_system *object,
+			int fd, struct itimerspec *curr_value)
+{
+	return spa_api_method_fast_r(int, -ENOTSUP, spa_system, &object->iface, timerfd_gettime, 0,
+			fd, curr_value);
+}
+SPA_API_SYSTEM int spa_system_timerfd_read(struct spa_system *object, int fd, uint64_t *expirations)
+{
+	return spa_api_method_fast_r(int, -ENOTSUP, spa_system, &object->iface, timerfd_read, 0,
+			fd, expirations);
+}
+
+SPA_API_SYSTEM int spa_system_eventfd_create(struct spa_system *object, int flags)
+{
+	return spa_api_method_fast_r(int, -ENOTSUP, spa_system, &object->iface, eventfd_create, 0, flags);
+}
+SPA_API_SYSTEM int spa_system_eventfd_write(struct spa_system *object, int fd, uint64_t count)
+{
+	return spa_api_method_fast_r(int, -ENOTSUP, spa_system, &object->iface, eventfd_write, 0,
+			fd, count);
+}
+SPA_API_SYSTEM int spa_system_eventfd_read(struct spa_system *object, int fd, uint64_t *count)
+{
+	return spa_api_method_fast_r(int, -ENOTSUP, spa_system, &object->iface, eventfd_read, 0,
+			fd, count);
+}
+
+SPA_API_SYSTEM int spa_system_signalfd_create(struct spa_system *object, int signal, int flags)
+{
+	return spa_api_method_fast_r(int, -ENOTSUP, spa_system, &object->iface, signalfd_create, 0,
+			signal, flags);
+}
+
+SPA_API_SYSTEM int spa_system_signalfd_read(struct spa_system *object, int fd, int *signal)
+{
+	return spa_api_method_fast_r(int, -ENOTSUP, spa_system, &object->iface, signalfd_read, 0,
+			fd, signal);
+}
 
 /**
  * \}
diff --git a/spa/include/spa/support/thread.h b/spa/include/spa/support/thread.h
index f691e3a3..b69cb688 100644
--- a/spa/include/spa/support/thread.h
+++ b/spa/include/spa/support/thread.h
@@ -16,6 +16,14 @@ extern "C" {
 #include <spa/utils/hook.h>
 #include <spa/utils/dict.h>
 
+#ifndef SPA_API_THREAD
+ #ifdef SPA_API_IMPL
+  #define SPA_API_THREAD SPA_API_IMPL
+ #else
+  #define SPA_API_THREAD static inline
+ #endif
+#endif
+
 /** \defgroup spa_thread Thread
  * Threading utility interfaces
  */
@@ -58,61 +66,51 @@ struct spa_thread_utils_methods {
 
 /** \copydoc spa_thread_utils_methods.create
  * \sa spa_thread_utils_methods.create */
-static inline struct spa_thread *spa_thread_utils_create(struct spa_thread_utils *o,
+SPA_API_THREAD struct spa_thread *spa_thread_utils_create(struct spa_thread_utils *o,
 		const struct spa_dict *props, void *(*start_routine)(void*), void *arg)
 {
-	struct spa_thread *res = NULL;
-	spa_interface_call_res(&o->iface,
-			struct spa_thread_utils_methods, res, create, 0,
+	return spa_api_method_r(struct spa_thread *, NULL,
+			spa_thread_utils, &o->iface, create, 0,
 			props, start_routine, arg);
-	return res;
 }
 
 /** \copydoc spa_thread_utils_methods.join
  * \sa spa_thread_utils_methods.join */
-static inline int spa_thread_utils_join(struct spa_thread_utils *o,
+SPA_API_THREAD int spa_thread_utils_join(struct spa_thread_utils *o,
 		struct spa_thread *thread, void **retval)
 {
-	int res = -ENOTSUP;
-	spa_interface_call_res(&o->iface,
-			struct spa_thread_utils_methods, res, join, 0,
+	return spa_api_method_r(int, -ENOTSUP,
+			spa_thread_utils, &o->iface, join, 0,
 			thread, retval);
-	return res;
 }
 
 /** \copydoc spa_thread_utils_methods.get_rt_range
  * \sa spa_thread_utils_methods.get_rt_range */
-static inline int spa_thread_utils_get_rt_range(struct spa_thread_utils *o,
+SPA_API_THREAD int spa_thread_utils_get_rt_range(struct spa_thread_utils *o,
 		const struct spa_dict *props, int *min, int *max)
 {
-	int res = -ENOTSUP;
-	spa_interface_call_res(&o->iface,
-			struct spa_thread_utils_methods, res, get_rt_range, 0,
+	return spa_api_method_r(int, -ENOTSUP,
+			spa_thread_utils, &o->iface, get_rt_range, 0,
 			props, min, max);
-	return res;
 }
 
 /** \copydoc spa_thread_utils_methods.acquire_rt
  * \sa spa_thread_utils_methods.acquire_rt */
-static inline int spa_thread_utils_acquire_rt(struct spa_thread_utils *o,
+SPA_API_THREAD int spa_thread_utils_acquire_rt(struct spa_thread_utils *o,
 		struct spa_thread *thread, int priority)
 {
-	int res = -ENOTSUP;
-	spa_interface_call_res(&o->iface,
-			struct spa_thread_utils_methods, res, acquire_rt, 0,
+	return spa_api_method_r(int, -ENOTSUP,
+			spa_thread_utils, &o->iface, acquire_rt, 0,
 			thread, priority);
-	return res;
 }
 
 /** \copydoc spa_thread_utils_methods.drop_rt
  * \sa spa_thread_utils_methods.drop_rt */
-static inline int spa_thread_utils_drop_rt(struct spa_thread_utils *o,
+SPA_API_THREAD int spa_thread_utils_drop_rt(struct spa_thread_utils *o,
 		struct spa_thread *thread)
 {
-	int res = -ENOTSUP;
-	spa_interface_call_res(&o->iface,
-			struct spa_thread_utils_methods, res, drop_rt, 0, thread);
-	return res;
+	return spa_api_method_r(int, -ENOTSUP,
+			spa_thread_utils, &o->iface, drop_rt, 0, thread);
 }
 
 #define SPA_KEY_THREAD_NAME		"thread.name"		/* the thread name */
diff --git a/spa/include/spa/utils/defs.h b/spa/include/spa/utils/defs.h
index 474073af..1c1a73ab 100644
--- a/spa/include/spa/utils/defs.h
+++ b/spa/include/spa/utils/defs.h
@@ -133,10 +133,10 @@ 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; (ptr) < (arr) + SPA_N_ELEMENTS(arr); (ptr)++)
 
 #define SPA_FOR_EACH_ELEMENT_VAR(arr, var) \
-	for (__typeof__((arr)[0])* var = arr; (void*)(var) < SPA_PTROFF(arr, sizeof(arr), void); (var)++)
+	for (__typeof__((arr)[0])* var = arr; (var) < (arr) + SPA_N_ELEMENTS(arr); (var)++)
 
 #define SPA_ABS(a)			\
 ({					\
@@ -254,6 +254,20 @@ struct spa_fraction {
 #define SPA_WARN_UNUSED_RESULT
 #endif
 
+#ifndef SPA_API_IMPL
+#define SPA_API_PROTO static inline
+#define SPA_API_IMPL static inline
+#endif
+
+#ifndef SPA_API_UTILS_DEFS
+ #ifdef SPA_API_IMPL
+  #define SPA_API_UTILS_DEFS SPA_API_IMPL
+ #else
+  #define SPA_API_UTILS_DEFS static inline
+ #endif
+#endif
+
+
 #if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L
 #define SPA_RESTRICT restrict
 #elif defined(__GNUC__) && __GNUC__ >= 4
@@ -278,6 +292,13 @@ struct spa_fraction {
 #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_SCALE32(val,num,denom)				\
+({								\
+	uint64_t _val = (val);					\
+	uint64_t _denom = (denom);				\
+	(uint32_t)(((_val) * (num)) / (_denom));		\
+})
+
 #define SPA_SCALE32_UP(val,num,denom)				\
 ({								\
 	uint64_t _val = (val);					\
@@ -300,7 +321,7 @@ struct spa_fraction {
 #endif
 #endif
 
-static inline bool spa_ptrinside(const void *p1, size_t s1, const void *p2, size_t s2,
+SPA_API_UTILS_DEFS bool spa_ptrinside(const void *p1, size_t s1, const void *p2, size_t s2,
                                  size_t *remaining)
 {
 	if (SPA_LIKELY((uintptr_t)p1 <= (uintptr_t)p2 && s2 <= s1 &&
@@ -315,7 +336,7 @@ static inline bool spa_ptrinside(const void *p1, size_t s1, const void *p2, size
 	}
 }
 
-static inline bool spa_ptr_inside_and_aligned(const void *p1, size_t s1,
+SPA_API_UTILS_DEFS bool spa_ptr_inside_and_aligned(const void *p1, size_t s1,
                                               const void *p2, size_t s2, size_t align,
                                               size_t *remaining)
 {
diff --git a/spa/include/spa/utils/dict.h b/spa/include/spa/utils/dict.h
index 126f469e..c88a833f 100644
--- a/spa/include/spa/utils/dict.h
+++ b/spa/include/spa/utils/dict.h
@@ -13,6 +13,14 @@ extern "C" {
 
 #include <spa/utils/defs.h>
 
+#ifndef SPA_API_DICT
+ #ifdef SPA_API_IMPL
+  #define SPA_API_DICT SPA_API_IMPL
+ #else
+  #define SPA_API_DICT static inline
+ #endif
+#endif
+
 /**
  * \defgroup spa_dict Dictionary
  * Dictionary data structure
@@ -28,7 +36,8 @@ struct spa_dict_item {
 	const char *value;
 };
 
-#define SPA_DICT_ITEM_INIT(key,value) ((struct spa_dict_item) { (key), (value) })
+#define SPA_DICT_ITEM(key,value) ((struct spa_dict_item) { (key), (value) })
+#define SPA_DICT_ITEM_INIT(key,value) SPA_DICT_ITEM(key,value)
 
 struct spa_dict {
 #define SPA_DICT_FLAG_SORTED	(1<<0)		/**< items are sorted */
@@ -37,22 +46,26 @@ 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(items,n_items) ((struct spa_dict) { 0, (n_items), (items) })
+#define SPA_DICT_ARRAY(items) SPA_DICT((items),SPA_N_ELEMENTS(items))
+#define SPA_DICT_ITEMS(...) SPA_DICT_ARRAY(((struct spa_dict_item[]) { __VA_ARGS__}))
+
+#define SPA_DICT_INIT(items,n_items) SPA_DICT(items,n_items)
+#define SPA_DICT_INIT_ARRAY(items) SPA_DICT_ARRAY(items)
 
 #define spa_dict_for_each(item, dict)				\
 	for ((item) = (dict)->items;				\
 	     (item) < &(dict)->items[(dict)->n_items];		\
 	     (item)++)
 
-static inline int spa_dict_item_compare(const void *i1, const void *i2)
+SPA_API_DICT int spa_dict_item_compare(const void *i1, const void *i2)
 {
 	const struct spa_dict_item *it1 = (const struct spa_dict_item *)i1,
 	      *it2 = (const struct spa_dict_item *)i2;
 	return strcmp(it1->key, it2->key);
 }
 
-static inline void spa_dict_qsort(struct spa_dict *dict)
+SPA_API_DICT void spa_dict_qsort(struct spa_dict *dict)
 {
 	if (dict->n_items > 0)
 		qsort((void*)dict->items, dict->n_items, sizeof(struct spa_dict_item),
@@ -60,7 +73,7 @@ static inline void spa_dict_qsort(struct spa_dict *dict)
 	SPA_FLAG_SET(dict->flags, SPA_DICT_FLAG_SORTED);
 }
 
-static inline const struct spa_dict_item *spa_dict_lookup_item(const struct spa_dict *dict,
+SPA_API_DICT const struct spa_dict_item *spa_dict_lookup_item(const struct spa_dict *dict,
 							       const char *key)
 {
 	const struct spa_dict_item *item;
@@ -83,7 +96,7 @@ static inline const struct spa_dict_item *spa_dict_lookup_item(const struct spa_
 	return NULL;
 }
 
-static inline const char *spa_dict_lookup(const struct spa_dict *dict, const char *key)
+SPA_API_DICT const char *spa_dict_lookup(const struct spa_dict *dict, const char *key)
 {
 	const struct spa_dict_item *item = spa_dict_lookup_item(dict, key);
 	return item ? item->value : NULL;
diff --git a/spa/include/spa/utils/dll.h b/spa/include/spa/utils/dll.h
index 0372d63e..7b8fd207 100644
--- a/spa/include/spa/utils/dll.h
+++ b/spa/include/spa/utils/dll.h
@@ -12,6 +12,16 @@ extern "C" {
 #include <stddef.h>
 #include <math.h>
 
+#include <spa/utils/defs.h>
+
+#ifndef SPA_API_DLL
+ #ifdef SPA_API_IMPL
+  #define SPA_API_DLL SPA_API_IMPL
+ #else
+  #define SPA_API_DLL static inline
+ #endif
+#endif
+
 #define SPA_DLL_BW_MAX		0.128
 #define SPA_DLL_BW_MIN		0.016
 
@@ -21,13 +31,13 @@ struct spa_dll {
 	double w0, w1, w2;
 };
 
-static inline void spa_dll_init(struct spa_dll *dll)
+SPA_API_DLL void spa_dll_init(struct spa_dll *dll)
 {
 	dll->bw = 0.0;
 	dll->z1 = dll->z2 = dll->z3 = 0.0;
 }
 
-static inline void spa_dll_set_bw(struct spa_dll *dll, double bw, unsigned period, unsigned rate)
+SPA_API_DLL void spa_dll_set_bw(struct spa_dll *dll, double bw, unsigned period, unsigned rate)
 {
 	double w = 2 * M_PI * bw * period / rate;
 	dll->w0 = 1.0 - exp (-20.0 * w);
@@ -36,7 +46,7 @@ static inline void spa_dll_set_bw(struct spa_dll *dll, double bw, unsigned perio
 	dll->bw = bw;
 }
 
-static inline double spa_dll_update(struct spa_dll *dll, double err)
+SPA_API_DLL double spa_dll_update(struct spa_dll *dll, double err)
 {
 	dll->z1 += dll->w0 * (dll->w1 * err - dll->z1);
 	dll->z2 += dll->w0 * (dll->z1 - dll->z2);
diff --git a/spa/include/spa/utils/endian.h b/spa/include/spa/utils/endian.h
new file mode 100644
index 00000000..2d002d45
--- /dev/null
+++ b/spa/include/spa/utils/endian.h
@@ -0,0 +1,26 @@
+/* Spa */
+/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */
+/* SPDX-License-Identifier: MIT */
+
+#ifndef SPA_ENDIAN_H
+#define SPA_ENDIAN_H
+
+#if defined(__FreeBSD__) || defined(__MidnightBSD__)
+#include <sys/endian.h>
+#define bswap_16 bswap16
+#define bswap_32 bswap32
+#define bswap_64 bswap64
+#elif defined(_MSC_VER) && defined(_WIN32)
+#include <stdlib.h>
+#define __LITTLE_ENDIAN 1234
+#define __BIG_ENDIAN 4321
+#define __BYTE_ORDER __LITTLE_ENDIAN
+#define bswap_16 _byteswap_ushort
+#define bswap_32 _byteswap_ulong
+#define bswap_64 _byteswap_uint64
+#else
+#include <endian.h>
+#include <byteswap.h>
+#endif
+
+#endif /* SPA_ENDIAN_H */
diff --git a/spa/include/spa/utils/hook.h b/spa/include/spa/utils/hook.h
index aea18d28..dbbb0197 100644
--- a/spa/include/spa/utils/hook.h
+++ b/spa/include/spa/utils/hook.h
@@ -12,6 +12,14 @@ extern "C" {
 #include <spa/utils/defs.h>
 #include <spa/utils/list.h>
 
+#ifndef SPA_API_HOOK
+ #ifdef SPA_API_IMPL
+  #define SPA_API_HOOK SPA_API_IMPL
+ #else
+  #define SPA_API_HOOK static inline
+ #endif
+#endif
+
 /** \defgroup spa_interfaces Interfaces
  *
  * \brief Generic implementation of implementation-independent interfaces
@@ -158,14 +166,14 @@ struct spa_interface {
 	const type *_f = (const type *) (callbacks)->funcs;			\
 	bool _res = SPA_CALLBACK_CHECK(_f,method,vers);				\
 	if (SPA_LIKELY(_res))							\
-		_f->method((callbacks)->data, ## __VA_ARGS__);			\
+		(_f->method)((callbacks)->data, ## __VA_ARGS__);		\
 	_res;									\
 })
 
 #define spa_callbacks_call_fast(callbacks,type,method,vers,...)			\
 ({										\
 	const type *_f = (const type *) (callbacks)->funcs;			\
-	_f->method((callbacks)->data, ## __VA_ARGS__);				\
+	(_f->method)((callbacks)->data, ## __VA_ARGS__);			\
 	true;									\
 })
 
@@ -199,13 +207,13 @@ struct spa_interface {
 ({										\
 	const type *_f = (const type *) (callbacks)->funcs;			\
 	if (SPA_LIKELY(SPA_CALLBACK_CHECK(_f,method,vers)))			\
-		res = _f->method((callbacks)->data, ## __VA_ARGS__);		\
+		res = (_f->method)((callbacks)->data, ## __VA_ARGS__);		\
 	res;									\
 })
 #define spa_callbacks_call_fast_res(callbacks,type,res,method,vers,...)		\
 ({										\
 	const type *_f = (const type *) (callbacks)->funcs;			\
-	res = _f->method((callbacks)->data, ## __VA_ARGS__);			\
+	res = (_f->method)((callbacks)->data, ## __VA_ARGS__);			\
 })
 
 /**
@@ -245,6 +253,73 @@ struct spa_interface {
 #define spa_interface_call_fast_res(iface,method_type,res,method,vers,...)		\
 	spa_callbacks_call_fast_res(&(iface)->cb,method_type,res,method,vers,##__VA_ARGS__)
 
+
+#define spa_api_func_v(o,method,version,...)				\
+({									\
+	if (SPA_LIKELY(SPA_CALLBACK_CHECK(o,method,version)))		\
+		((o)->method)(o, ##__VA_ARGS__);			\
+})
+#define spa_api_func_r(rtype,def,o,method,version,...)			\
+({									\
+	rtype _res = def;						\
+	if (SPA_LIKELY(SPA_CALLBACK_CHECK(o,method,version)))		\
+		_res = ((o)->method)(o, ##__VA_ARGS__);			\
+	_res;								\
+})
+#define spa_api_func_fast(o,method,...)					\
+({									\
+	((o)->method)(o, ##__VA_ARGS__);				\
+})
+
+#define spa_api_method_v(type,o,method,version,...)			\
+({									\
+	struct spa_interface *_i = o;			\
+	spa_interface_call(_i, struct type ##_methods,			\
+			method, version, ##__VA_ARGS__);		\
+})
+#define spa_api_method_r(rtype,def,type,o,method,version,...)		\
+({									\
+	rtype _res = def;						\
+	struct spa_interface *_i = o;			\
+	spa_interface_call_res(_i, struct type ##_methods,		\
+			_res, method, version, ##__VA_ARGS__);		\
+	_res;								\
+})
+#define spa_api_method_null_v(type,co,o,method,version,...)		\
+({									\
+	struct type *_co = co;						\
+	if (SPA_LIKELY(_co != NULL)) {					\
+		struct spa_interface *_i = o;				\
+		spa_interface_call(_i, struct type ##_methods,		\
+			method, version, ##__VA_ARGS__);		\
+	}								\
+})
+#define spa_api_method_null_r(rtype,def,type,co,o,method,version,...)	\
+({									\
+	rtype _res = def;						\
+	struct type *_co = co;						\
+	if (SPA_LIKELY(_co != NULL)) {					\
+		struct spa_interface *_i = o;				\
+		spa_interface_call_res(_i, struct type ##_methods,	\
+				_res, method, version, ##__VA_ARGS__);	\
+	}								\
+	_res;								\
+})
+#define spa_api_method_fast_v(type,o,method,version,...)		\
+({									\
+	struct spa_interface *_i = o;					\
+	spa_interface_call_fast(_i, struct type ##_methods,		\
+			method, version, ##__VA_ARGS__);		\
+})
+#define spa_api_method_fast_r(rtype,def,type,o,method,version,...)	\
+({									\
+	rtype _res = def;						\
+	struct spa_interface *_i = o;					\
+	spa_interface_call_fast_res(_i, struct type ##_methods,		\
+			_res, method, version, ##__VA_ARGS__);		\
+	_res;								\
+})
+
 /**
  * \}
  */
@@ -348,18 +423,18 @@ struct spa_hook {
 };
 
 /** Initialize a hook list to the empty list*/
-static inline void spa_hook_list_init(struct spa_hook_list *list)
+SPA_API_HOOK void spa_hook_list_init(struct spa_hook_list *list)
 {
 	spa_list_init(&list->list);
 }
 
-static inline bool spa_hook_list_is_empty(struct spa_hook_list *list)
+SPA_API_HOOK bool spa_hook_list_is_empty(struct spa_hook_list *list)
 {
 	return spa_list_is_empty(&list->list);
 }
 
 /** Append a hook. */
-static inline void spa_hook_list_append(struct spa_hook_list *list,
+SPA_API_HOOK void spa_hook_list_append(struct spa_hook_list *list,
 					struct spa_hook *hook,
 					const void *funcs, void *data)
 {
@@ -369,7 +444,7 @@ static inline void spa_hook_list_append(struct spa_hook_list *list,
 }
 
 /** Prepend a hook */
-static inline void spa_hook_list_prepend(struct spa_hook_list *list,
+SPA_API_HOOK void spa_hook_list_prepend(struct spa_hook_list *list,
 					 struct spa_hook *hook,
 					 const void *funcs, void *data)
 {
@@ -379,7 +454,7 @@ static inline void spa_hook_list_prepend(struct spa_hook_list *list,
 }
 
 /** Remove a hook */
-static inline void spa_hook_remove(struct spa_hook *hook)
+SPA_API_HOOK void spa_hook_remove(struct spa_hook *hook)
 {
 	if (spa_list_is_initialized(&hook->link))
 		spa_list_remove(&hook->link);
@@ -388,14 +463,14 @@ static inline void spa_hook_remove(struct spa_hook *hook)
 }
 
 /** Remove all hooks from the list */
-static inline void spa_hook_list_clean(struct spa_hook_list *list)
+SPA_API_HOOK void spa_hook_list_clean(struct spa_hook_list *list)
 {
 	struct spa_hook *h;
 	spa_list_consume(h, &list->list, link)
 		spa_hook_remove(h);
 }
 
-static inline void
+SPA_API_HOOK void
 spa_hook_list_isolate(struct spa_hook_list *list,
 		struct spa_hook_list *save,
 		struct spa_hook *hook,
@@ -409,7 +484,7 @@ spa_hook_list_isolate(struct spa_hook_list *list,
 	spa_hook_list_append(list, hook, funcs, data);
 }
 
-static inline void
+SPA_API_HOOK void
 spa_hook_list_join(struct spa_hook_list *list,
 		struct spa_hook_list *save)
 {
diff --git a/spa/include/spa/utils/json-core.h b/spa/include/spa/utils/json-core.h
new file mode 100644
index 00000000..31bf772f
--- /dev/null
+++ b/spa/include/spa/utils/json-core.h
@@ -0,0 +1,635 @@
+/* Simple Plugin API */
+/* SPDX-FileCopyrightText: Copyright © 2020 Wim Taymans */
+/* SPDX-License-Identifier: MIT */
+
+#ifndef SPA_UTILS_JSON_H
+#define SPA_UTILS_JSON_H
+
+#ifdef __cplusplus
+extern "C" {
+#else
+#include <stdbool.h>
+#endif
+#include <stddef.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <string.h>
+#include <math.h>
+#include <float.h>
+
+#include <spa/utils/defs.h>
+#include <spa/utils/string.h>
+
+#ifndef SPA_API_JSON
+ #ifdef SPA_API_IMPL
+  #define SPA_API_JSON SPA_API_IMPL
+ #else
+  #define SPA_API_JSON static inline
+ #endif
+#endif
+
+/** \defgroup spa_json JSON
+ * Relaxed JSON variant parsing
+ */
+
+/**
+ * \addtogroup spa_json
+ * \{
+ */
+
+/* a simple JSON compatible tokenizer */
+struct spa_json {
+	const char *cur;
+	const char *end;
+	struct spa_json *parent;
+#define SPA_JSON_ERROR_FLAG	0x100
+	uint32_t state;
+	uint32_t depth;
+};
+
+#define SPA_JSON_INIT(data,size) ((struct spa_json) { (data), (data)+(size), NULL, 0, 0 })
+
+SPA_API_JSON 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), (iter)->state & 0xff0, 0 })
+
+SPA_API_JSON 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, NULL, (iter)->state, 0 })
+
+SPA_API_JSON void spa_json_save(struct spa_json * iter, struct spa_json * save)
+{
+	*save = SPA_JSON_SAVE(iter);
+}
+
+#define SPA_JSON_START(iter,p) ((struct spa_json) { (p), (iter)->end, NULL, 0, 0 })
+
+SPA_API_JSON void spa_json_start(struct spa_json * iter, struct spa_json * sub, const char *pos)
+{
+	*sub = SPA_JSON_START(iter,pos);
+}
+
+/** Get the next token. \a value points to the token and the return value
+ * is the length. Returns -1 on parse error, 0 on end of input. */
+SPA_API_JSON int spa_json_next(struct spa_json * iter, const char **value)
+{
+	int utf8_remain = 0, err = 0;
+	enum {
+		__NONE, __STRUCT, __BARE, __STRING, __UTF8, __ESC, __COMMENT,
+		__ARRAY_FLAG = 0x10,		/* in array context */
+		__PREV_ARRAY_FLAG = 0x20,	/* depth=0 array context flag */
+		__KEY_FLAG = 0x40,		/* inside object key */
+		__SUB_FLAG = 0x80,		/* not at top-level */
+		__FLAGS = 0xff0,
+		__ERROR_SYSTEM = SPA_JSON_ERROR_FLAG,
+		__ERROR_INVALID_ARRAY_SEPARATOR,
+		__ERROR_EXPECTED_OBJECT_KEY,
+		__ERROR_EXPECTED_OBJECT_VALUE,
+		__ERROR_TOO_DEEP_NESTING,
+		__ERROR_EXPECTED_ARRAY_CLOSE,
+		__ERROR_EXPECTED_OBJECT_CLOSE,
+		__ERROR_MISMATCHED_BRACKET,
+		__ERROR_ESCAPE_NOT_ALLOWED,
+		__ERROR_CHARACTERS_NOT_ALLOWED,
+		__ERROR_INVALID_ESCAPE,
+		__ERROR_INVALID_STATE,
+		__ERROR_UNFINISHED_STRING,
+	};
+	uint64_t array_stack[8] = {0};		/* array context flags of depths 1...512 */
+
+	*value = iter->cur;
+
+	if (iter->state & SPA_JSON_ERROR_FLAG)
+		return -1;
+
+	for (; iter->cur < iter->end; iter->cur++) {
+		unsigned char cur = (unsigned char)*iter->cur;
+		uint32_t flag;
+
+#define _SPA_ERROR(reason)	{ err = __ERROR_ ## reason; goto error; }
+ again:
+		flag = iter->state & __FLAGS;
+		switch (iter->state & ~__FLAGS) {
+		case __NONE:
+			flag &= ~(__KEY_FLAG | __PREV_ARRAY_FLAG);
+			iter->state = __STRUCT | flag;
+			iter->depth = 0;
+			goto again;
+		case __STRUCT:
+			switch (cur) {
+			case '\0': case '\t': case ' ': case '\r': case '\n': case ',':
+				continue;
+			case ':': case '=':
+				if (flag & __ARRAY_FLAG)
+					_SPA_ERROR(INVALID_ARRAY_SEPARATOR);
+				if (!(flag & __KEY_FLAG))
+					_SPA_ERROR(EXPECTED_OBJECT_KEY);
+				iter->state |= __SUB_FLAG;
+				continue;
+			case '#':
+				iter->state = __COMMENT | flag;
+				continue;
+			case '"':
+				if (flag & __KEY_FLAG)
+					flag |= __SUB_FLAG;
+				if (!(flag & __ARRAY_FLAG))
+					SPA_FLAG_UPDATE(flag, __KEY_FLAG, !(flag & __KEY_FLAG));
+				*value = iter->cur;
+				iter->state = __STRING | flag;
+				continue;
+			case '[': case '{':
+				if (!(flag & __ARRAY_FLAG)) {
+					/* At top-level we may be either in object context
+					 * or in single-item context, and then we need to
+					 * accept array/object here.
+					 */
+					if ((iter->state & __SUB_FLAG) && !(flag & __KEY_FLAG))
+						_SPA_ERROR(EXPECTED_OBJECT_KEY);
+					SPA_FLAG_CLEAR(flag, __KEY_FLAG);
+				}
+				iter->state = __STRUCT | __SUB_FLAG | flag;
+				SPA_FLAG_UPDATE(iter->state, __ARRAY_FLAG, cur == '[');
+
+				/* We need to remember previous array state across calls
+				 * for depth=0, so store that in state. Others bits go to
+				 * temporary stack.
+				 */
+				if (iter->depth == 0) {
+					SPA_FLAG_UPDATE(iter->state, __PREV_ARRAY_FLAG, flag & __ARRAY_FLAG);
+				} else if (((iter->depth-1) >> 6) < SPA_N_ELEMENTS(array_stack)) {
+					uint64_t mask = 1ULL << ((iter->depth-1) & 0x3f);
+					SPA_FLAG_UPDATE(array_stack[(iter->depth-1) >> 6], mask, flag & __ARRAY_FLAG);
+				} else {
+					/* too deep */
+					_SPA_ERROR(TOO_DEEP_NESTING);
+				}
+
+				*value = iter->cur;
+				if (++iter->depth > 1)
+					continue;
+				iter->cur++;
+				return 1;
+			case '}': case ']':
+				if ((flag & __ARRAY_FLAG) && cur != ']')
+					_SPA_ERROR(EXPECTED_ARRAY_CLOSE);
+				if (!(flag & __ARRAY_FLAG) && cur != '}')
+					_SPA_ERROR(EXPECTED_OBJECT_CLOSE);
+				if (flag & __KEY_FLAG) {
+					/* incomplete key-value pair */
+					_SPA_ERROR(EXPECTED_OBJECT_VALUE);
+				}
+				iter->state = __STRUCT | __SUB_FLAG | flag;
+				if (iter->depth == 0) {
+					if (iter->parent)
+						iter->parent->cur = iter->cur;
+					else
+						_SPA_ERROR(MISMATCHED_BRACKET);
+					return 0;
+				}
+				--iter->depth;
+				if (iter->depth == 0) {
+					SPA_FLAG_UPDATE(iter->state, __ARRAY_FLAG, flag & __PREV_ARRAY_FLAG);
+				} else if (((iter->depth-1) >> 6) < SPA_N_ELEMENTS(array_stack)) {
+					uint64_t mask = 1ULL << ((iter->depth-1) & 0x3f);
+					SPA_FLAG_UPDATE(iter->state, __ARRAY_FLAG,
+							SPA_FLAG_IS_SET(array_stack[(iter->depth-1) >> 6], mask));
+				} else {
+					/* too deep */
+					_SPA_ERROR(TOO_DEEP_NESTING);
+				}
+				continue;
+			case '\\':
+				/* disallow bare escape */
+				_SPA_ERROR(ESCAPE_NOT_ALLOWED);
+			default:
+				/* allow bare ascii */
+				if (!(cur >= 32 && cur <= 126))
+					_SPA_ERROR(CHARACTERS_NOT_ALLOWED);
+				if (flag & __KEY_FLAG)
+					flag |= __SUB_FLAG;
+				if (!(flag & __ARRAY_FLAG))
+					SPA_FLAG_UPDATE(flag, __KEY_FLAG, !(flag & __KEY_FLAG));
+				*value = iter->cur;
+				iter->state = __BARE | flag;
+			}
+			continue;
+		case __BARE:
+			switch (cur) {
+			case '\0':
+			case '\t': case ' ': case '\r': case '\n':
+			case '"': case '#':
+			case ':': case ',': case '=': case ']': case '}':
+				iter->state = __STRUCT | flag;
+				if (iter->depth > 0)
+					goto again;
+				return iter->cur - *value;
+			case '\\':
+				/* disallow bare escape */
+				_SPA_ERROR(ESCAPE_NOT_ALLOWED);
+			default:
+				/* allow bare ascii */
+				if (cur >= 32 && cur <= 126)
+					continue;
+			}
+			_SPA_ERROR(CHARACTERS_NOT_ALLOWED);
+		case __STRING:
+			switch (cur) {
+			case '\\':
+				iter->state = __ESC | flag;
+				continue;
+			case '"':
+				iter->state = __STRUCT | flag;
+				if (iter->depth > 0)
+					continue;
+				return ++iter->cur - *value;
+			case 240 ... 247:
+				utf8_remain++;
+				SPA_FALLTHROUGH;
+			case 224 ... 239:
+				utf8_remain++;
+				SPA_FALLTHROUGH;
+			case 192 ... 223:
+				utf8_remain++;
+				iter->state = __UTF8 | flag;
+				continue;
+			default:
+				if (cur >= 32 && cur <= 127)
+					continue;
+			}
+			_SPA_ERROR(CHARACTERS_NOT_ALLOWED);
+		case __UTF8:
+			switch (cur) {
+			case 128 ... 191:
+				if (--utf8_remain == 0)
+					iter->state = __STRING | flag;
+				continue;
+			}
+			_SPA_ERROR(CHARACTERS_NOT_ALLOWED);
+		case __ESC:
+			switch (cur) {
+			case '"': case '\\': case '/': case 'b': case 'f':
+			case 'n': case 'r': case 't': case 'u':
+				iter->state = __STRING | flag;
+				continue;
+			}
+			_SPA_ERROR(INVALID_ESCAPE);
+		case __COMMENT:
+			switch (cur) {
+			case '\n': case '\r':
+				iter->state = __STRUCT | flag;
+			}
+			break;
+		default:
+			_SPA_ERROR(INVALID_STATE);
+		}
+
+	}
+	if (iter->depth != 0 || iter->parent)
+		_SPA_ERROR(MISMATCHED_BRACKET);
+
+	switch (iter->state & ~__FLAGS) {
+	case __STRING: case __UTF8: case __ESC:
+		/* string/escape not closed */
+		_SPA_ERROR(UNFINISHED_STRING);
+	case __COMMENT:
+		/* trailing comment */
+		return 0;
+	}
+
+	if ((iter->state & __SUB_FLAG) && (iter->state & __KEY_FLAG)) {
+		/* incomplete key-value pair */
+		_SPA_ERROR(EXPECTED_OBJECT_VALUE);
+	}
+
+	if ((iter->state & ~__FLAGS) != __STRUCT) {
+		iter->state = __STRUCT | (iter->state & __FLAGS);
+		return iter->cur - *value;
+	}
+	return 0;
+#undef _SPA_ERROR
+
+error:
+	iter->state = err;
+	while (iter->parent) {
+		if (iter->parent->state & SPA_JSON_ERROR_FLAG)
+			break;
+		iter->parent->state = err;
+		iter->parent->cur = iter->cur;
+		iter = iter->parent;
+	}
+	return -1;
+}
+
+/**
+ * Return if there was a parse error, and its possible location.
+ *
+ * \since 1.1.0
+ */
+SPA_API_JSON bool spa_json_get_error(struct spa_json *iter, const char *start,
+		struct spa_error_location *loc)
+{
+	static const char *reasons[] = {
+		"System error",
+		"Invalid array separator",
+		"Expected object key",
+		"Expected object value",
+		"Too deep nesting",
+		"Expected array close bracket",
+		"Expected object close brace",
+		"Mismatched bracket",
+		"Escape not allowed",
+		"Character not allowed",
+		"Invalid escape",
+		"Invalid state",
+		"Unfinished string",
+		"Expected key separator",
+	};
+
+	if (!(iter->state & SPA_JSON_ERROR_FLAG))
+		return false;
+
+	if (loc) {
+		int linepos = 1, colpos = 1, code;
+		const char *p, *l;
+
+		for (l = p = start; p && p != iter->cur; ++p) {
+			if (*p == '\n') {
+				linepos++;
+				colpos = 1;
+				l = p+1;
+			} else {
+				colpos++;
+			}
+		}
+		code = SPA_CLAMP(iter->state & 0xff, 0u, SPA_N_ELEMENTS(reasons)-1);
+		loc->line = linepos;
+		loc->col = colpos;
+		loc->location = l;
+		loc->len = SPA_PTRDIFF(iter->end, loc->location) / sizeof(char);
+		loc->reason = code == 0 ? strerror(errno) : reasons[code];
+	}
+	return true;
+}
+
+SPA_API_JSON int spa_json_is_container(const char *val, int len)
+{
+	return len > 0 && (*val == '{'  || *val == '[');
+}
+
+/* object */
+SPA_API_JSON int spa_json_is_object(const char *val, int len)
+{
+	return len > 0 && *val == '{';
+}
+
+/* array */
+SPA_API_JSON bool spa_json_is_array(const char *val, int len)
+{
+	return len > 0 && *val == '[';
+}
+
+/* null */
+SPA_API_JSON bool spa_json_is_null(const char *val, int len)
+{
+	return len == 4 && strncmp(val, "null", 4) == 0;
+}
+
+/* float */
+SPA_API_JSON int spa_json_parse_float(const char *val, int len, float *result)
+{
+	char buf[96];
+	char *end;
+	int pos;
+
+	if (len <= 0 || len >= (int)sizeof(buf))
+		return 0;
+
+	for (pos = 0; pos < len; ++pos) {
+		switch (val[pos]) {
+		case '+': case '-': case '0' ... '9': case '.': case 'e': case 'E': break;
+		default: return 0;
+		}
+	}
+
+	memcpy(buf, val, len);
+	buf[len] = '\0';
+
+	*result = spa_strtof(buf, &end);
+	return len > 0 && end == buf + len;
+}
+
+SPA_API_JSON bool spa_json_is_float(const char *val, int len)
+{
+	float dummy;
+	return spa_json_parse_float(val, len, &dummy);
+}
+
+SPA_API_JSON char *spa_json_format_float(char *str, int size, float val)
+{
+	if (SPA_UNLIKELY(!isnormal(val))) {
+		if (isinf(val))
+			val = signbit(val) ? FLT_MIN : FLT_MAX;
+		else
+			val = 0.0f;
+	}
+	return spa_dtoa(str, size, val);
+}
+
+/* int */
+SPA_API_JSON int spa_json_parse_int(const char *val, int len, int *result)
+{
+	char buf[64];
+	char *end;
+
+	if (len <= 0 || len >= (int)sizeof(buf))
+		return 0;
+
+	memcpy(buf, val, len);
+	buf[len] = '\0';
+
+	*result = strtol(buf, &end, 0);
+	return len > 0 && end == buf + len;
+}
+SPA_API_JSON bool spa_json_is_int(const char *val, int len)
+{
+	int dummy;
+	return spa_json_parse_int(val, len, &dummy);
+}
+
+/* bool */
+SPA_API_JSON bool spa_json_is_true(const char *val, int len)
+{
+	return len == 4 && strncmp(val, "true", 4) == 0;
+}
+
+SPA_API_JSON bool spa_json_is_false(const char *val, int len)
+{
+	return len == 5 && strncmp(val, "false", 5) == 0;
+}
+
+SPA_API_JSON bool spa_json_is_bool(const char *val, int len)
+{
+	return spa_json_is_true(val, len) || spa_json_is_false(val, len);
+}
+
+SPA_API_JSON int spa_json_parse_bool(const char *val, int len, bool *result)
+{
+	if ((*result = spa_json_is_true(val, len)))
+		return 1;
+	if (!(*result = !spa_json_is_false(val, len)))
+		return 1;
+	return -1;
+}
+
+/* string */
+SPA_API_JSON bool spa_json_is_string(const char *val, int len)
+{
+	return len > 1 && *val == '"';
+}
+
+SPA_API_JSON int spa_json_parse_hex(const char *p, int num, uint32_t *res)
+{
+	int i;
+	*res = 0;
+	for (i = 0; i < num; i++) {
+		char v = p[i];
+		if (v >= '0' && v <= '9')
+			v = v - '0';
+		else if (v >= 'a' && v <= 'f')
+			v = v - 'a' + 10;
+		else if (v >= 'A' && v <= 'F')
+			v = v - 'A' + 10;
+		else
+			return -1;
+		*res = (*res << 4) | v;
+	}
+	return 1;
+}
+
+SPA_API_JSON int spa_json_parse_stringn(const char *val, int len, char *result, int maxlen)
+{
+	const char *p;
+	if (maxlen <= len)
+		return -ENOSPC;
+	if (!spa_json_is_string(val, len)) {
+		if (result != val)
+			memmove(result, val, len);
+		result += len;
+	} else {
+		for (p = val+1; p < val + len; p++) {
+			if (*p == '\\') {
+				p++;
+				if (*p == 'n')
+					*result++ = '\n';
+				else if (*p == 'r')
+					*result++ = '\r';
+				else if (*p == 'b')
+					*result++ = '\b';
+				else if (*p == 't')
+					*result++ = '\t';
+				else if (*p == 'f')
+					*result++ = '\f';
+				else if (*p == 'u') {
+					uint8_t prefix[] = { 0, 0xc0, 0xe0, 0xf0 };
+					uint32_t idx, n, v, cp, enc[] = { 0x80, 0x800, 0x10000 };
+					if (val + len - p < 5 ||
+					    spa_json_parse_hex(p+1, 4, &cp) < 0) {
+						*result++ = *p;
+						continue;
+					}
+					p += 4;
+
+					if (cp >= 0xd800 && cp <= 0xdbff) {
+						if (val + len - p < 7 ||
+						    p[1] != '\\' || p[2] != 'u' ||
+						    spa_json_parse_hex(p+3, 4, &v) < 0 ||
+						    v < 0xdc00 || v > 0xdfff)
+							continue;
+						p += 6;
+						cp = 0x010000 + (((cp & 0x3ff) << 10) | (v & 0x3ff));
+					} else if (cp >= 0xdc00 && cp <= 0xdfff)
+						continue;
+
+					for (idx = 0; idx < 3; idx++)
+						if (cp < enc[idx])
+							break;
+					for (n = idx; n > 0; n--, cp >>= 6)
+						result[n] = (cp | 0x80) & 0xbf;
+					*result++ = (cp | prefix[idx]) & 0xff;
+					result += idx;
+				} else
+					*result++ = *p;
+			} else if (*p == '\"') {
+				break;
+			} else
+				*result++ = *p;
+		}
+	}
+	*result = '\0';
+	return 1;
+}
+
+SPA_API_JSON int spa_json_parse_string(const char *val, int len, char *result)
+{
+	return spa_json_parse_stringn(val, len, result, len+1);
+}
+
+SPA_API_JSON int spa_json_encode_string(char *str, int size, const char *val)
+{
+	int len = 0;
+	static const char hex[] = { "0123456789abcdef" };
+#define __PUT(c) { if (len < size) *str++ = c; len++; }
+	__PUT('"');
+	while (*val) {
+		switch (*val) {
+		case '\n':
+			__PUT('\\'); __PUT('n');
+			break;
+		case '\r':
+			__PUT('\\'); __PUT('r');
+			break;
+		case '\b':
+			__PUT('\\'); __PUT('b');
+			break;
+		case '\t':
+			__PUT('\\'); __PUT('t');
+			break;
+		case '\f':
+			__PUT('\\'); __PUT('f');
+			break;
+		case '\\':
+		case '"':
+			__PUT('\\'); __PUT(*val);
+			break;
+		default:
+			if (*val > 0 && *val < 0x20) {
+				__PUT('\\'); __PUT('u');
+				__PUT('0'); __PUT('0');
+				__PUT(hex[((*val)>>4)&0xf]); __PUT(hex[(*val)&0xf]);
+			} else {
+				__PUT(*val);
+			}
+			break;
+		}
+		val++;
+	}
+	__PUT('"');
+	__PUT('\0');
+#undef __PUT
+	return len-1;
+}
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_UTILS_JSON_H */
diff --git a/spa/include/spa/utils/json-pod.h b/spa/include/spa/utils/json-pod.h
index 0c9bdb8a..22f2b281 100644
--- a/spa/include/spa/utils/json-pod.h
+++ b/spa/include/spa/utils/json-pod.h
@@ -15,6 +15,14 @@ extern "C" {
 #include <spa/pod/builder.h>
 #include <spa/debug/types.h>
 
+#ifndef SPA_API_JSON_POD
+ #ifdef SPA_API_IMPL
+  #define SPA_API_JSON_POD SPA_API_IMPL
+ #else
+  #define SPA_API_JSON_POD static inline
+ #endif
+#endif
+
 /** \defgroup spa_json_pod JSON to POD
  * JSON to POD conversion
  */
@@ -24,7 +32,7 @@ extern "C" {
  * \{
  */
 
-static inline int spa_json_to_pod_part(struct spa_pod_builder *b, uint32_t flags, uint32_t id,
+SPA_API_JSON_POD int spa_json_to_pod_part(struct spa_pod_builder *b, uint32_t flags, uint32_t id,
 		const struct spa_type_info *info, struct spa_json *iter, const char *value, int len)
 {
 	const struct spa_type_info *ti;
@@ -42,10 +50,8 @@ static inline int spa_json_to_pod_part(struct spa_pod_builder *b, uint32_t flags
 		spa_pod_builder_push_object(b, &f[0], info->parent, id);
 
 		spa_json_enter(iter, &it[0]);
-		while (spa_json_get_string(&it[0], key, sizeof(key)) > 0) {
+		while ((l = spa_json_object_next(&it[0], key, sizeof(key), &v)) > 0) {
 			const struct spa_type_info *pi;
-			if ((l = spa_json_next(&it[0], &v)) <= 0)
-				break;
 			if ((pi = spa_debug_type_find_short(ti->values, key)) != NULL)
 				type = pi->type;
 			else if (!spa_atou32(key, &type, 0))
@@ -137,17 +143,32 @@ static inline int spa_json_to_pod_part(struct spa_pod_builder *b, uint32_t flags
 	return 0;
 }
 
-static inline int spa_json_to_pod(struct spa_pod_builder *b, uint32_t flags,
-		const struct spa_type_info *info, const char *value, int len)
+SPA_API_JSON_POD int spa_json_to_pod_checked(struct spa_pod_builder *b, uint32_t flags,
+		const struct spa_type_info *info, const char *value, int len,
+		struct spa_error_location *loc)
 {
 	struct spa_json iter;
 	const char *val;
+	int res;
+
+	if (loc)
+		spa_zero(*loc);
+
+	if ((res = spa_json_begin(&iter, value, len, &val)) <= 0)
+		goto error;
 
-	spa_json_init(&iter, value, len);
-	if ((len = spa_json_next(&iter, &val)) <= 0)
-		return -EINVAL;
+	res = spa_json_to_pod_part(b, flags, info->type, info, &iter, val, len);
 
-	return spa_json_to_pod_part(b, flags, info->type, info, &iter, val, len);
+error:
+	if (res < 0 && loc)
+		spa_json_get_error(&iter, value, loc);
+	return res;
+}
+
+SPA_API_JSON_POD int spa_json_to_pod(struct spa_pod_builder *b, uint32_t flags,
+		const struct spa_type_info *info, const char *value, int len)
+{
+	return spa_json_to_pod_checked(b, flags, info, value, len, NULL);
 }
 
 /**
diff --git a/spa/include/spa/utils/json.h b/spa/include/spa/utils/json.h
index 0cca1710..a36554d1 100644
--- a/spa/include/spa/utils/json.h
+++ b/spa/include/spa/utils/json.h
@@ -2,8 +2,8 @@
 /* SPDX-FileCopyrightText: Copyright © 2020 Wim Taymans */
 /* SPDX-License-Identifier: MIT */
 
-#ifndef SPA_UTILS_JSON_H
-#define SPA_UTILS_JSON_H
+#ifndef SPA_UTILS_JSON_UTILS_H
+#define SPA_UTILS_JSON_UTILS_H
 
 #ifdef __cplusplus
 extern "C" {
@@ -17,11 +17,18 @@ extern "C" {
 #include <math.h>
 #include <float.h>
 
-#include <spa/utils/defs.h>
-#include <spa/utils/string.h>
+#include <spa/utils/json-core.h>
 
-/** \defgroup spa_json JSON
- * Relaxed JSON variant parsing
+#ifndef SPA_API_JSON_UTILS
+ #ifdef SPA_API_IMPL
+  #define SPA_API_JSON_UTILS SPA_API_IMPL
+ #else
+  #define SPA_API_JSON_UTILS static inline
+ #endif
+#endif
+
+/** \defgroup spa_json_utils JSON Utils
+ * Relaxed JSON variant parsing Utils
  */
 
 /**
@@ -29,356 +36,85 @@ extern "C" {
  * \{
  */
 
-/* a simple JSON compatible tokenizer */
-struct spa_json {
-	const char *cur;
-	const char *end;
-	struct spa_json *parent;
-#define SPA_JSON_ERROR_FLAG	0x100
-	uint32_t state;
-	uint32_t depth;
-};
-
-#define SPA_JSON_INIT(data,size) ((struct spa_json) { (data), (data)+(size), 0, 0, 0 })
-
-static inline void spa_json_init(struct spa_json * iter, const char *data, size_t size)
+SPA_API_JSON_UTILS int spa_json_begin(struct spa_json * iter, const char *data, size_t size, const char **val)
 {
-	*iter =  SPA_JSON_INIT(data, size);
+	spa_json_init(iter, data, size);
+	return spa_json_next(iter, val);
 }
-#define SPA_JSON_ENTER(iter) ((struct spa_json) { (iter)->cur, (iter)->end, (iter), (iter)->state & 0xff0, 0 })
 
-static inline void spa_json_enter(struct spa_json * iter, struct spa_json * sub)
+/* float */
+SPA_API_JSON_UTILS int spa_json_get_float(struct spa_json *iter, float *res)
 {
-	*sub = SPA_JSON_ENTER(iter);
+	const char *value;
+	int len;
+	if ((len = spa_json_next(iter, &value)) <= 0)
+		return len;
+	return spa_json_parse_float(value, len, res);
 }
 
-#define SPA_JSON_SAVE(iter) ((struct spa_json) { (iter)->cur, (iter)->end, NULL, (iter)->state, 0 })
-
-/** Get the next token. \a value points to the token and the return value
- * is the length. Returns -1 on parse error, 0 on end of input. */
-static inline int spa_json_next(struct spa_json * iter, const char **value)
+/* int */
+SPA_API_JSON_UTILS int spa_json_get_int(struct spa_json *iter, int *res)
 {
-	int utf8_remain = 0, err = 0;
-	enum {
-		__NONE, __STRUCT, __BARE, __STRING, __UTF8, __ESC, __COMMENT,
-		__ARRAY_FLAG = 0x10,		/* in array context */
-		__PREV_ARRAY_FLAG = 0x20,	/* depth=0 array context flag */
-		__KEY_FLAG = 0x40,		/* inside object key */
-		__SUB_FLAG = 0x80,		/* not at top-level */
-		__FLAGS = 0xff0,
-		__ERROR_SYSTEM = SPA_JSON_ERROR_FLAG,
-		__ERROR_INVALID_ARRAY_SEPARATOR,
-		__ERROR_EXPECTED_OBJECT_KEY,
-		__ERROR_EXPECTED_OBJECT_VALUE,
-		__ERROR_TOO_DEEP_NESTING,
-		__ERROR_EXPECTED_ARRAY_CLOSE,
-		__ERROR_EXPECTED_OBJECT_CLOSE,
-		__ERROR_MISMATCHED_BRACKET,
-		__ERROR_ESCAPE_NOT_ALLOWED,
-		__ERROR_CHARACTERS_NOT_ALLOWED,
-		__ERROR_INVALID_ESCAPE,
-		__ERROR_INVALID_STATE,
-		__ERROR_UNFINISHED_STRING,
-	};
-	uint64_t array_stack[8] = {0};		/* array context flags of depths 1...512 */
-
-	*value = iter->cur;
-
-	if (iter->state & SPA_JSON_ERROR_FLAG)
-		return -1;
-
-	for (; iter->cur < iter->end; iter->cur++) {
-		unsigned char cur = (unsigned char)*iter->cur;
-		uint32_t flag;
-
-#define _SPA_ERROR(reason)	{ err = __ERROR_ ## reason; goto error; }
- again:
-		flag = iter->state & __FLAGS;
-		switch (iter->state & ~__FLAGS) {
-		case __NONE:
-			flag &= ~(__KEY_FLAG | __PREV_ARRAY_FLAG);
-			iter->state = __STRUCT | flag;
-			iter->depth = 0;
-			goto again;
-		case __STRUCT:
-			switch (cur) {
-			case '\0': case '\t': case ' ': case '\r': case '\n': case ',':
-				continue;
-			case ':': case '=':
-				if (flag & __ARRAY_FLAG)
-					_SPA_ERROR(INVALID_ARRAY_SEPARATOR);
-				if (!(flag & __KEY_FLAG))
-					_SPA_ERROR(EXPECTED_OBJECT_KEY);
-				iter->state |= __SUB_FLAG;
-				continue;
-			case '#':
-				iter->state = __COMMENT | flag;
-				continue;
-			case '"':
-				if (flag & __KEY_FLAG)
-					flag |= __SUB_FLAG;
-				if (!(flag & __ARRAY_FLAG))
-					SPA_FLAG_UPDATE(flag, __KEY_FLAG, !(flag & __KEY_FLAG));
-				*value = iter->cur;
-				iter->state = __STRING | flag;
-				continue;
-			case '[': case '{':
-				if (!(flag & __ARRAY_FLAG)) {
-					/* At top-level we may be either in object context
-					 * or in single-item context, and then we need to
-					 * accept array/object here.
-					 */
-					if ((iter->state & __SUB_FLAG) && !(flag & __KEY_FLAG))
-						_SPA_ERROR(EXPECTED_OBJECT_KEY);
-					SPA_FLAG_CLEAR(flag, __KEY_FLAG);
-				}
-				iter->state = __STRUCT | __SUB_FLAG | flag;
-				SPA_FLAG_UPDATE(iter->state, __ARRAY_FLAG, cur == '[');
-
-				/* We need to remember previous array state across calls
-				 * for depth=0, so store that in state. Others bits go to
-				 * temporary stack.
-				 */
-				if (iter->depth == 0) {
-					SPA_FLAG_UPDATE(iter->state, __PREV_ARRAY_FLAG, flag & __ARRAY_FLAG);
-				} else if (((iter->depth-1) >> 6) < SPA_N_ELEMENTS(array_stack)) {
-					uint64_t mask = 1ULL << ((iter->depth-1) & 0x3f);
-					SPA_FLAG_UPDATE(array_stack[(iter->depth-1) >> 6], mask, flag & __ARRAY_FLAG);
-				} else {
-					/* too deep */
-					_SPA_ERROR(TOO_DEEP_NESTING);
-				}
-
-				*value = iter->cur;
-				if (++iter->depth > 1)
-					continue;
-				iter->cur++;
-				return 1;
-			case '}': case ']':
-				if ((flag & __ARRAY_FLAG) && cur != ']')
-					_SPA_ERROR(EXPECTED_ARRAY_CLOSE);
-				if (!(flag & __ARRAY_FLAG) && cur != '}')
-					_SPA_ERROR(EXPECTED_OBJECT_CLOSE);
-				if (flag & __KEY_FLAG) {
-					/* incomplete key-value pair */
-					_SPA_ERROR(EXPECTED_OBJECT_VALUE);
-				}
-				iter->state = __STRUCT | __SUB_FLAG | flag;
-				if (iter->depth == 0) {
-					if (iter->parent)
-						iter->parent->cur = iter->cur;
-					else
-						_SPA_ERROR(MISMATCHED_BRACKET);
-					return 0;
-				}
-				--iter->depth;
-				if (iter->depth == 0) {
-					SPA_FLAG_UPDATE(iter->state, __ARRAY_FLAG, flag & __PREV_ARRAY_FLAG);
-				} else if (((iter->depth-1) >> 6) < SPA_N_ELEMENTS(array_stack)) {
-					uint64_t mask = 1ULL << ((iter->depth-1) & 0x3f);
-					SPA_FLAG_UPDATE(iter->state, __ARRAY_FLAG,
-							SPA_FLAG_IS_SET(array_stack[(iter->depth-1) >> 6], mask));
-				} else {
-					/* too deep */
-					_SPA_ERROR(TOO_DEEP_NESTING);
-				}
-				continue;
-			case '\\':
-				/* disallow bare escape */
-				_SPA_ERROR(ESCAPE_NOT_ALLOWED);
-			default:
-				/* allow bare ascii */
-				if (!(cur >= 32 && cur <= 126))
-					_SPA_ERROR(CHARACTERS_NOT_ALLOWED);
-				if (flag & __KEY_FLAG)
-					flag |= __SUB_FLAG;
-				if (!(flag & __ARRAY_FLAG))
-					SPA_FLAG_UPDATE(flag, __KEY_FLAG, !(flag & __KEY_FLAG));
-				*value = iter->cur;
-				iter->state = __BARE | flag;
-			}
-			continue;
-		case __BARE:
-			switch (cur) {
-			case '\0':
-			case '\t': case ' ': case '\r': case '\n':
-			case '"': case '#':
-			case ':': case ',': case '=': case ']': case '}':
-				iter->state = __STRUCT | flag;
-				if (iter->depth > 0)
-					goto again;
-				return iter->cur - *value;
-			case '\\':
-				/* disallow bare escape */
-				_SPA_ERROR(ESCAPE_NOT_ALLOWED);
-			default:
-				/* allow bare ascii */
-				if (cur >= 32 && cur <= 126)
-					continue;
-			}
-			_SPA_ERROR(CHARACTERS_NOT_ALLOWED);
-		case __STRING:
-			switch (cur) {
-			case '\\':
-				iter->state = __ESC | flag;
-				continue;
-			case '"':
-				iter->state = __STRUCT | flag;
-				if (iter->depth > 0)
-					continue;
-				return ++iter->cur - *value;
-			case 240 ... 247:
-				utf8_remain++;
-				SPA_FALLTHROUGH;
-			case 224 ... 239:
-				utf8_remain++;
-				SPA_FALLTHROUGH;
-			case 192 ... 223:
-				utf8_remain++;
-				iter->state = __UTF8 | flag;
-				continue;
-			default:
-				if (cur >= 32 && cur <= 127)
-					continue;
-			}
-			_SPA_ERROR(CHARACTERS_NOT_ALLOWED);
-		case __UTF8:
-			switch (cur) {
-			case 128 ... 191:
-				if (--utf8_remain == 0)
-					iter->state = __STRING | flag;
-				continue;
-			}
-			_SPA_ERROR(CHARACTERS_NOT_ALLOWED);
-		case __ESC:
-			switch (cur) {
-			case '"': case '\\': case '/': case 'b': case 'f':
-			case 'n': case 'r': case 't': case 'u':
-				iter->state = __STRING | flag;
-				continue;
-			}
-			_SPA_ERROR(INVALID_ESCAPE);
-		case __COMMENT:
-			switch (cur) {
-			case '\n': case '\r':
-				iter->state = __STRUCT | flag;
-			}
-			break;
-		default:
-			_SPA_ERROR(INVALID_STATE);
-		}
-
-	}
-	if (iter->depth != 0 || iter->parent)
-		_SPA_ERROR(MISMATCHED_BRACKET);
-
-	switch (iter->state & ~__FLAGS) {
-	case __STRING: case __UTF8: case __ESC:
-		/* string/escape not closed */
-		_SPA_ERROR(UNFINISHED_STRING);
-	case __COMMENT:
-		/* trailing comment */
-		return 0;
-	}
-
-	if ((iter->state & __SUB_FLAG) && (iter->state & __KEY_FLAG)) {
-		/* incomplete key-value pair */
-		_SPA_ERROR(EXPECTED_OBJECT_VALUE);
-	}
-
-	if ((iter->state & ~__FLAGS) != __STRUCT) {
-		iter->state = __STRUCT | (iter->state & __FLAGS);
-		return iter->cur - *value;
-	}
-	return 0;
-#undef _SPA_ERROR
-
-error:
-	iter->state = err;
-	while (iter->parent) {
-		if (iter->parent->state & SPA_JSON_ERROR_FLAG)
-			break;
-		iter->parent->state = err;
-		iter->parent->cur = iter->cur;
-		iter = iter->parent;
-	}
-	return -1;
+	const char *value;
+	int len;
+	if ((len = spa_json_next(iter, &value)) <= 0)
+		return len;
+	return spa_json_parse_int(value, len, res);
 }
 
-/**
- * Return it there was a parse error, and its possible location.
- *
- * \since 1.1.0
- */
-static inline bool spa_json_get_error(struct spa_json *iter, const char *start,
-		struct spa_error_location *loc)
+/* bool */
+SPA_API_JSON_UTILS int spa_json_get_bool(struct spa_json *iter, bool *res)
 {
-	static const char *reasons[] = {
-		"System error",
-		"Invalid array separator",
-		"Expected object key",
-		"Expected object value",
-		"Too deep nesting",
-		"Expected array close bracket",
-		"Expected object close brace",
-		"Mismatched bracket",
-		"Escape not allowed",
-		"Character not allowed",
-		"Invalid escape",
-		"Invalid state",
-		"Unfinished string",
-		"Expected key separator",
-	};
-
-	if (!(iter->state & SPA_JSON_ERROR_FLAG))
-		return false;
-
-	if (loc) {
-		int linepos = 1, colpos = 1, code;
-		const char *p, *l;
+	const char *value;
+	int len;
+	if ((len = spa_json_next(iter, &value)) <= 0)
+		return len;
+	return spa_json_parse_bool(value, len, res);
+}
 
-		for (l = p = start; p && p != iter->cur; ++p) {
-			if (*p == '\n') {
-				linepos++;
-				colpos = 1;
-				l = p+1;
-			} else {
-				colpos++;
-			}
-		}
-		code = SPA_CLAMP(iter->state & 0xff, 0u, SPA_N_ELEMENTS(reasons)-1);
-		loc->line = linepos;
-		loc->col = colpos;
-		loc->location = l;
-		loc->len = SPA_PTRDIFF(iter->end, loc->location) / sizeof(char);
-		loc->reason = code == 0 ? strerror(errno) : reasons[code];
-	}
-	return true;
+/* string */
+SPA_API_JSON_UTILS int spa_json_get_string(struct spa_json *iter, char *res, int maxlen)
+{
+	const char *value;
+	int len;
+	if ((len = spa_json_next(iter, &value)) <= 0)
+		return len;
+	return spa_json_parse_stringn(value, len, res, maxlen);
 }
 
-static inline int spa_json_enter_container(struct spa_json *iter, struct spa_json *sub, char type)
+
+SPA_API_JSON_UTILS int spa_json_enter_container(struct spa_json *iter, struct spa_json *sub, char type)
 {
 	const char *value;
 	int len;
 	if ((len = spa_json_next(iter, &value)) <= 0)
 		return len;
+	if (!spa_json_is_container(value, len))
+		return -EPROTO;
 	if (*value != type)
-		return -1;
+		return -EINVAL;
 	spa_json_enter(iter, sub);
 	return 1;
 }
 
-static inline int spa_json_is_container(const char *val, int len)
+SPA_API_JSON_UTILS int spa_json_begin_container(struct spa_json * iter,
+		const char *data, size_t size, char type, bool relax)
 {
-	return len > 0 && (*val == '{'  || *val == '[');
+	int res;
+	spa_json_init(iter, data, size);
+	res = spa_json_enter_container(iter, iter, type);
+	if (res == -EPROTO && relax)
+		spa_json_init(iter, data, size);
+	else if (res <= 0)
+		return res;
+	return 1;
 }
-
 /**
  * Return length of container at current position, starting at \a value.
  *
  * \return Length of container including {} or [], or 0 on error.
  */
-static inline int spa_json_container_len(struct spa_json *iter, const char *value, int len SPA_UNUSED)
+SPA_API_JSON_UTILS int spa_json_container_len(struct spa_json *iter, const char *value, int len SPA_UNUSED)
 {
 	const char *val;
 	struct spa_json sub;
@@ -391,287 +127,88 @@ static inline int spa_json_container_len(struct spa_json *iter, const char *valu
 }
 
 /* object */
-static inline int spa_json_is_object(const char *val, int len)
-{
-	return len > 0 && *val == '{';
-}
-static inline int spa_json_enter_object(struct spa_json *iter, struct spa_json *sub)
+SPA_API_JSON_UTILS int spa_json_enter_object(struct spa_json *iter, struct spa_json *sub)
 {
 	return spa_json_enter_container(iter, sub, '{');
 }
-
-/* array */
-static inline bool spa_json_is_array(const char *val, int len)
-{
-	return len > 0 && *val == '[';
-}
-static inline int spa_json_enter_array(struct spa_json *iter, struct spa_json *sub)
-{
-	return spa_json_enter_container(iter, sub, '[');
-}
-
-/* null */
-static inline bool spa_json_is_null(const char *val, int len)
-{
-	return len == 4 && strncmp(val, "null", 4) == 0;
-}
-
-/* float */
-static inline int spa_json_parse_float(const char *val, int len, float *result)
+SPA_API_JSON_UTILS int spa_json_begin_object_relax(struct spa_json * iter, const char *data, size_t size)
 {
-	char buf[96];
-	char *end;
-	int pos;
-
-	if (len <= 0 || len >= (int)sizeof(buf))
-		return 0;
-
-	for (pos = 0; pos < len; ++pos) {
-		switch (val[pos]) {
-		case '+': case '-': case '0' ... '9': case '.': case 'e': case 'E': break;
-		default: return 0;
-		}
-	}
-
-	memcpy(buf, val, len);
-	buf[len] = '\0';
-
-	*result = spa_strtof(buf, &end);
-	return len > 0 && end == buf + len;
-}
-
-static inline bool spa_json_is_float(const char *val, int len)
-{
-	float dummy;
-	return spa_json_parse_float(val, len, &dummy);
+	return spa_json_begin_container(iter, data, size, '{', true);
 }
-static inline int spa_json_get_float(struct spa_json *iter, float *res)
+SPA_API_JSON_UTILS int spa_json_begin_object(struct spa_json * iter, const char *data, size_t size)
 {
-	const char *value;
-	int len;
-	if ((len = spa_json_next(iter, &value)) <= 0)
-		return len;
-	return spa_json_parse_float(value, len, res);
+	return spa_json_begin_container(iter, data, size, '{', false);
 }
 
-static inline char *spa_json_format_float(char *str, int size, float val)
+SPA_API_JSON_UTILS int spa_json_object_next(struct spa_json *iter, char *key, int maxkeylen, const char **value)
 {
-	if (SPA_UNLIKELY(!isnormal(val))) {
-		if (isinf(val))
-			val = signbit(val) ? FLT_MIN : FLT_MAX;
-		else
-			val = 0.0f;
+	int res1, res2;
+	while (true) {
+		res1 = spa_json_get_string(iter, key, maxkeylen);
+		if (res1 <= 0 && res1 != -ENOSPC)
+			return res1;
+		res2 = spa_json_next(iter, value);
+		if (res2 <= 0 || res1 != -ENOSPC)
+			return res2;
 	}
-	return spa_dtoa(str, size, val);
-}
-
-/* int */
-static inline int spa_json_parse_int(const char *val, int len, int *result)
-{
-	char buf[64];
-	char *end;
-
-	if (len <= 0 || len >= (int)sizeof(buf))
-		return 0;
-
-	memcpy(buf, val, len);
-	buf[len] = '\0';
-
-	*result = strtol(buf, &end, 0);
-	return len > 0 && end == buf + len;
-}
-static inline bool spa_json_is_int(const char *val, int len)
-{
-	int dummy;
-	return spa_json_parse_int(val, len, &dummy);
-}
-static inline int spa_json_get_int(struct spa_json *iter, int *res)
-{
-	const char *value;
-	int len;
-	if ((len = spa_json_next(iter, &value)) <= 0)
-		return len;
-	return spa_json_parse_int(value, len, res);
 }
 
-/* bool */
-static inline bool spa_json_is_true(const char *val, int len)
+SPA_API_JSON_UTILS int spa_json_object_find(struct spa_json *iter, const char *key, const char **value)
 {
-	return len == 4 && strncmp(val, "true", 4) == 0;
-}
+	struct spa_json obj = SPA_JSON_SAVE(iter);
+	int res, len = strlen(key) + 3;
+	char k[len];
 
-static inline bool spa_json_is_false(const char *val, int len)
-{
-	return len == 5 && strncmp(val, "false", 5) == 0;
+	while ((res = spa_json_object_next(&obj, k, len, value)) > 0)
+		if (spa_streq(k, key))
+			return res;
+	return -ENOENT;
 }
 
-static inline bool spa_json_is_bool(const char *val, int len)
+SPA_API_JSON_UTILS int spa_json_str_object_find(const char *obj, size_t obj_len,
+		const char *key, char *value, size_t maxlen)
 {
-	return spa_json_is_true(val, len) || spa_json_is_false(val, len);
-}
+	struct spa_json iter;
+	int l;
+	const char *v;
 
-static inline int spa_json_parse_bool(const char *val, int len, bool *result)
-{
-	if ((*result = spa_json_is_true(val, len)))
-		return 1;
-	if (!(*result = !spa_json_is_false(val, len)))
-		return 1;
-	return -1;
-}
-static inline int spa_json_get_bool(struct spa_json *iter, bool *res)
-{
-	const char *value;
-	int len;
-	if ((len = spa_json_next(iter, &value)) <= 0)
-		return len;
-	return spa_json_parse_bool(value, len, res);
-}
-
-/* string */
-static inline bool spa_json_is_string(const char *val, int len)
-{
-	return len > 1 && *val == '"';
+	if (spa_json_begin_object(&iter, obj, obj_len) <= 0)
+		return -EINVAL;
+	if ((l = spa_json_object_find(&iter, key, &v)) <= 0)
+		return l;
+	return spa_json_parse_stringn(v, l, value, maxlen);
 }
 
-static inline int spa_json_parse_hex(const char *p, int num, uint32_t *res)
+/* array */
+SPA_API_JSON_UTILS int spa_json_enter_array(struct spa_json *iter, struct spa_json *sub)
 {
-	int i;
-	*res = 0;
-	for (i = 0; i < num; i++) {
-		char v = p[i];
-		if (v >= '0' && v <= '9')
-			v = v - '0';
-		else if (v >= 'a' && v <= 'f')
-			v = v - 'a' + 10;
-		else if (v >= 'A' && v <= 'F')
-			v = v - 'A' + 10;
-		else
-			return -1;
-		*res = (*res << 4) | v;
-	}
-	return 1;
+	return spa_json_enter_container(iter, sub, '[');
 }
-
-static inline int spa_json_parse_stringn(const char *val, int len, char *result, int maxlen)
+SPA_API_JSON_UTILS int spa_json_begin_array_relax(struct spa_json * iter, const char *data, size_t size)
 {
-	const char *p;
-	if (maxlen <= len)
-		return -1;
-	if (!spa_json_is_string(val, len)) {
-		if (result != val)
-			memmove(result, val, len);
-		result += len;
-	} else {
-		for (p = val+1; p < val + len; p++) {
-			if (*p == '\\') {
-				p++;
-				if (*p == 'n')
-					*result++ = '\n';
-				else if (*p == 'r')
-					*result++ = '\r';
-				else if (*p == 'b')
-					*result++ = '\b';
-				else if (*p == 't')
-					*result++ = '\t';
-				else if (*p == 'f')
-					*result++ = '\f';
-				else if (*p == 'u') {
-					uint8_t prefix[] = { 0, 0xc0, 0xe0, 0xf0 };
-					uint32_t idx, n, v, cp, enc[] = { 0x80, 0x800, 0x10000 };
-					if (val + len - p < 5 ||
-					    spa_json_parse_hex(p+1, 4, &cp) < 0) {
-						*result++ = *p;
-						continue;
-					}
-					p += 4;
-
-					if (cp >= 0xd800 && cp <= 0xdbff) {
-						if (val + len - p < 7 ||
-						    p[1] != '\\' || p[2] != 'u' ||
-						    spa_json_parse_hex(p+3, 4, &v) < 0 ||
-						    v < 0xdc00 || v > 0xdfff)
-							continue;
-						p += 6;
-						cp = 0x010000 + (((cp & 0x3ff) << 10) | (v & 0x3ff));
-					} else if (cp >= 0xdc00 && cp <= 0xdfff)
-						continue;
-
-					for (idx = 0; idx < 3; idx++)
-						if (cp < enc[idx])
-							break;
-					for (n = idx; n > 0; n--, cp >>= 6)
-						result[n] = (cp | 0x80) & 0xbf;
-					*result++ = (cp | prefix[idx]) & 0xff;
-					result += idx;
-				} else
-					*result++ = *p;
-			} else if (*p == '\"') {
-				break;
-			} else
-				*result++ = *p;
-		}
-	}
-	*result = '\0';
-	return 1;
+	return spa_json_begin_container(iter, data, size, '[', true);
 }
-
-static inline int spa_json_parse_string(const char *val, int len, char *result)
+SPA_API_JSON_UTILS int spa_json_begin_array(struct spa_json * iter, const char *data, size_t size)
 {
-	return spa_json_parse_stringn(val, len, result, len+1);
+	return spa_json_begin_container(iter, data, size, '[', false);
 }
 
-static inline int spa_json_get_string(struct spa_json *iter, char *res, int maxlen)
-{
-	const char *value;
-	int len;
-	if ((len = spa_json_next(iter, &value)) <= 0)
-		return len;
-	return spa_json_parse_stringn(value, len, res, maxlen);
+#define spa_json_make_str_array_unpack(maxlen,type,conv)			\
+{										\
+	struct spa_json iter;							\
+	char v[maxlen];								\
+	uint32_t count = 0;							\
+        if (spa_json_begin_array_relax(&iter, arr, arr_len) <= 0)		\
+		return -EINVAL;							\
+	while (spa_json_get_string(&iter, v, sizeof(v)) > 0 && count < max)	\
+		values[count++] = conv(v);					\
+	return count;								\
 }
 
-static inline int spa_json_encode_string(char *str, int size, const char *val)
+SPA_API_JSON_UTILS int spa_json_str_array_uint32(const char *arr, size_t arr_len,
+		uint32_t *values, size_t max)
 {
-	int len = 0;
-	static const char hex[] = { "0123456789abcdef" };
-#define __PUT(c) { if (len < size) *str++ = c; len++; }
-	__PUT('"');
-	while (*val) {
-		switch (*val) {
-		case '\n':
-			__PUT('\\'); __PUT('n');
-			break;
-		case '\r':
-			__PUT('\\'); __PUT('r');
-			break;
-		case '\b':
-			__PUT('\\'); __PUT('b');
-			break;
-		case '\t':
-			__PUT('\\'); __PUT('t');
-			break;
-		case '\f':
-			__PUT('\\'); __PUT('f');
-			break;
-		case '\\':
-		case '"':
-			__PUT('\\'); __PUT(*val);
-			break;
-		default:
-			if (*val > 0 && *val < 0x20) {
-				__PUT('\\'); __PUT('u');
-				__PUT('0'); __PUT('0');
-				__PUT(hex[((*val)>>4)&0xf]); __PUT(hex[(*val)&0xf]);
-			} else {
-				__PUT(*val);
-			}
-			break;
-		}
-		val++;
-	}
-	__PUT('"');
-	__PUT('\0');
-#undef __PUT
-	return len-1;
+	spa_json_make_str_array_unpack(32,uint32_t, atoi);
 }
 
 /**
@@ -682,4 +219,4 @@ static inline int spa_json_encode_string(char *str, int size, const char *val)
 } /* extern "C" */
 #endif
 
-#endif /* SPA_UTILS_JSON_H */
+#endif /* SPA_UTILS_JSON_UTILS_H */
diff --git a/spa/include/spa/utils/keys.h b/spa/include/spa/utils/keys.h
index 48b94c50..12a8cfab 100644
--- a/spa/include/spa/utils/keys.h
+++ b/spa/include/spa/utils/keys.h
@@ -45,6 +45,14 @@ extern "C" {
 #define SPA_KEY_API_ALSA_DISABLE_LONGNAME	\
 					"api.alsa.disable-longname"	/**< if card long name should not be passed to MIDI port */
 #define SPA_KEY_API_ALSA_BIND_CTLS	"api.alsa.bind-ctls"		/**< alsa controls to bind as params */
+#define SPA_KEY_API_ALSA_SPLIT_ENABLE	"api.alsa.split-enable"		/**< For UCM devices with split PCMs, don't split to
+									 * multiple PCMs using alsa-lib plugins, but instead
+									 * add api.alsa.split properties to emitted nodes
+									 * with PCM splitting information.
+									 */
+#define SPA_KEY_API_ALSA_SPLIT_PARENT	"api.alsa.split.parent"		/**< PCM is UCM SplitPCM parent PCM,
+									 * to be opened with SplitPCM set.
+									 */
 
 /** info from alsa card_info */
 #define SPA_KEY_API_ALSA_CARD_ID	"api.alsa.card.id"		/**< id from card_info */
@@ -67,6 +75,15 @@ extern "C" {
 #define SPA_KEY_API_ALSA_PCM_SUBCLASS	"api.alsa.pcm.subclass"		/**< subclass from pcm_info as string */
 #define SPA_KEY_API_ALSA_PCM_SYNC_ID	"api.alsa.pcm.sync-id"		/**< sync id */
 
+#define SPA_KEY_API_ALSA_SPLIT_POSITION "api.alsa.split.position"	/**< (SPA JSON list) If present, this is a
+									 * virtual device corresponding to a subset of
+									 * channels in an underlying PCM, listed in this
+									 * property. The \ref SPA_KEY_API_ALSA_PATH
+									 * contains the underlying split PCM. */
+#define SPA_KEY_API_ALSA_SPLIT_HW_POSITION \
+					"api.alsa.split.hw-position"	/**< (SPA JSON list) Channel map of the
+									 * underlying split PCM. */
+
 /** keys for v4l2 api */
 #define SPA_KEY_API_V4L2		"api.v4l2"			/**< key for the v4l2 api */
 #define SPA_KEY_API_V4L2_PATH		"api.v4l2.path"			/**< v4l2 device path as can be
diff --git a/spa/include/spa/utils/list.h b/spa/include/spa/utils/list.h
index 0c03854b..60c89f23 100644
--- a/spa/include/spa/utils/list.h
+++ b/spa/include/spa/utils/list.h
@@ -9,6 +9,16 @@
 extern "C" {
 #endif
 
+#include <spa/utils/defs.h>
+
+#ifndef SPA_API_LIST
+ #ifdef SPA_API_IMPL
+  #define SPA_API_LIST SPA_API_IMPL
+ #else
+  #define SPA_API_LIST static inline
+ #endif
+#endif
+
 /**
  * \defgroup spa_list List
  * Doubly linked list data structure
@@ -26,19 +36,19 @@ struct spa_list {
 
 #define SPA_LIST_INIT(list) ((struct spa_list){ (list), (list) })
 
-static inline void spa_list_init(struct spa_list *list)
+SPA_API_LIST void spa_list_init(struct spa_list *list)
 {
 	*list = SPA_LIST_INIT(list);
 }
 
-static inline int spa_list_is_initialized(struct spa_list *list)
+SPA_API_LIST int spa_list_is_initialized(struct spa_list *list)
 {
 	return !!list->prev;
 }
 
 #define spa_list_is_empty(l)  ((l)->next == (l))
 
-static inline void spa_list_insert(struct spa_list *list, struct spa_list *elem)
+SPA_API_LIST void spa_list_insert(struct spa_list *list, struct spa_list *elem)
 {
 	elem->prev = list;
 	elem->next = list->next;
@@ -46,7 +56,7 @@ static inline void spa_list_insert(struct spa_list *list, struct spa_list *elem)
 	elem->next->prev = elem;
 }
 
-static inline void spa_list_insert_list(struct spa_list *list, struct spa_list *other)
+SPA_API_LIST void spa_list_insert_list(struct spa_list *list, struct spa_list *other)
 {
 	if (spa_list_is_empty(other))
 		return;
@@ -56,7 +66,7 @@ static inline void spa_list_insert_list(struct spa_list *list, struct spa_list *
 	list->next = other->next;
 }
 
-static inline void spa_list_remove(struct spa_list *elem)
+SPA_API_LIST void spa_list_remove(struct spa_list *elem)
 {
 	elem->prev->next = elem->next;
 	elem->next->prev = elem->prev;
diff --git a/spa/include/spa/utils/names.h b/spa/include/spa/utils/names.h
index f2f73bb4..a3f51901 100644
--- a/spa/include/spa/utils/names.h
+++ b/spa/include/spa/utils/names.h
@@ -73,6 +73,8 @@ extern "C" {
 #define SPA_NAME_VIDEO_CONVERT		"video.convert"			/**< converts raw video from one format
 									  *  to another. Must include at least
 									  *  format and scaling */
+#define SPA_NAME_VIDEO_CONVERT_DUMMY	"video.convert.dummy"		/**< a dummy converter as fallback for the
+									  *  videoadapter node */
 #define SPA_NAME_VIDEO_ADAPT		"video.adapt"			/**< combination of a node and a
 									  *  video.convert. */
 /** keys for alsa factory names */
diff --git a/spa/include/spa/utils/ratelimit.h b/spa/include/spa/utils/ratelimit.h
index ef1b9412..2af2c26b 100644
--- a/spa/include/spa/utils/ratelimit.h
+++ b/spa/include/spa/utils/ratelimit.h
@@ -12,6 +12,16 @@ extern "C" {
 #include <inttypes.h>
 #include <stddef.h>
 
+#include <spa/utils/defs.h>
+
+#ifndef SPA_API_RATELIMIT
+ #ifdef SPA_API_IMPL
+  #define SPA_API_RATELIMIT SPA_API_IMPL
+ #else
+  #define SPA_API_RATELIMIT static inline
+ #endif
+#endif
+
 struct spa_ratelimit {
 	uint64_t interval;
 	uint64_t begin;
@@ -20,7 +30,7 @@ struct spa_ratelimit {
 	unsigned n_suppressed;
 };
 
-static inline int spa_ratelimit_test(struct spa_ratelimit *r, uint64_t now)
+SPA_API_RATELIMIT int spa_ratelimit_test(struct spa_ratelimit *r, uint64_t now)
 {
 	unsigned suppressed = 0;
 	if (r->begin + r->interval < now) {
diff --git a/spa/include/spa/utils/result.h b/spa/include/spa/utils/result.h
index 7f389c2c..312a6bb0 100644
--- a/spa/include/spa/utils/result.h
+++ b/spa/include/spa/utils/result.h
@@ -19,8 +19,18 @@ extern "C" {
  * \{
  */
 
+#include <errno.h>
+
 #include <spa/utils/defs.h>
-#include <spa/utils/list.h>
+
+#ifndef SPA_API_RESULT
+ #ifdef SPA_API_IMPL
+  #define SPA_API_RESULT SPA_API_IMPL
+ #else
+  #define SPA_API_RESULT static inline
+ #endif
+#endif
+
 
 #define SPA_ASYNC_BIT			(1 << 30)
 #define SPA_ASYNC_SEQ_MASK		(SPA_ASYNC_BIT - 1)
@@ -33,13 +43,13 @@ extern "C" {
 #define SPA_RESULT_ASYNC_SEQ(res)	((res) & SPA_ASYNC_SEQ_MASK)
 #define SPA_RESULT_RETURN_ASYNC(seq)	(SPA_ASYNC_BIT | SPA_RESULT_ASYNC_SEQ(seq))
 
-#define spa_strerror(err)		\
-({					\
-	int _err = -(err);		\
-	if (SPA_RESULT_IS_ASYNC(err))	\
-		_err = EINPROGRESS;	\
-	strerror(_err);			\
-})
+SPA_API_RESULT const char *spa_strerror(int err)
+{
+	int _err = -(err);
+	if (SPA_RESULT_IS_ASYNC(err))
+		_err = EINPROGRESS;
+	return strerror(_err);
+}
 
 /**
  * \}
diff --git a/spa/include/spa/utils/ringbuffer.h b/spa/include/spa/utils/ringbuffer.h
index 51b502d7..e8c5d625 100644
--- a/spa/include/spa/utils/ringbuffer.h
+++ b/spa/include/spa/utils/ringbuffer.h
@@ -25,6 +25,14 @@ struct spa_ringbuffer;
 
 #include <spa/utils/defs.h>
 
+#ifndef SPA_API_RINGBUFFER
+ #ifdef SPA_API_IMPL
+  #define SPA_API_RINGBUFFER SPA_API_IMPL
+ #else
+  #define SPA_API_RINGBUFFER static inline
+ #endif
+#endif
+
 /**
  * A ringbuffer type.
  */
@@ -40,7 +48,7 @@ struct spa_ringbuffer {
  *
  * \param rbuf a spa_ringbuffer
  */
-static inline void spa_ringbuffer_init(struct spa_ringbuffer *rbuf)
+SPA_API_RINGBUFFER void spa_ringbuffer_init(struct spa_ringbuffer *rbuf)
 {
 	*rbuf = SPA_RINGBUFFER_INIT();
 }
@@ -51,7 +59,7 @@ static inline void spa_ringbuffer_init(struct spa_ringbuffer *rbuf)
  * \param rbuf a spa_ringbuffer
  * \param size the target size of \a rbuf
  */
-static inline void spa_ringbuffer_set_avail(struct spa_ringbuffer *rbuf, uint32_t size)
+SPA_API_RINGBUFFER void spa_ringbuffer_set_avail(struct spa_ringbuffer *rbuf, uint32_t size)
 {
 	rbuf->readindex = 0;
 	rbuf->writeindex = size;
@@ -67,7 +75,7 @@ static inline void spa_ringbuffer_set_avail(struct spa_ringbuffer *rbuf, uint32_
  *         there was an underrun. values > rbuf->size means there
  *         was an overrun.
  */
-static inline int32_t spa_ringbuffer_get_read_index(struct spa_ringbuffer *rbuf, uint32_t *index)
+SPA_API_RINGBUFFER int32_t spa_ringbuffer_get_read_index(struct spa_ringbuffer *rbuf, uint32_t *index)
 {
 	*index = __atomic_load_n(&rbuf->readindex, __ATOMIC_RELAXED);
 	return (int32_t) (__atomic_load_n(&rbuf->writeindex, __ATOMIC_ACQUIRE) - *index);
@@ -84,7 +92,7 @@ static inline int32_t spa_ringbuffer_get_read_index(struct spa_ringbuffer *rbuf,
  * \param data destination memory
  * \param len number of bytes to read
  */
-static inline void
+SPA_API_RINGBUFFER void
 spa_ringbuffer_read_data(struct spa_ringbuffer *rbuf SPA_UNUSED,
 			 const void *buffer, uint32_t size,
 			 uint32_t offset, void *data, uint32_t len)
@@ -101,7 +109,7 @@ spa_ringbuffer_read_data(struct spa_ringbuffer *rbuf SPA_UNUSED,
  * \param rbuf a spa_ringbuffer
  * \param index new index
  */
-static inline void spa_ringbuffer_read_update(struct spa_ringbuffer *rbuf, int32_t index)
+SPA_API_RINGBUFFER void spa_ringbuffer_read_update(struct spa_ringbuffer *rbuf, int32_t index)
 {
 	__atomic_store_n(&rbuf->readindex, index, __ATOMIC_RELEASE);
 }
@@ -117,7 +125,7 @@ static inline void spa_ringbuffer_read_update(struct spa_ringbuffer *rbuf, int32
  *         was an overrun. Subtract from the buffer size to get
  *         the number of bytes available for writing.
  */
-static inline int32_t spa_ringbuffer_get_write_index(struct spa_ringbuffer *rbuf, uint32_t *index)
+SPA_API_RINGBUFFER int32_t spa_ringbuffer_get_write_index(struct spa_ringbuffer *rbuf, uint32_t *index)
 {
 	*index = __atomic_load_n(&rbuf->writeindex, __ATOMIC_RELAXED);
 	return (int32_t) (*index - __atomic_load_n(&rbuf->readindex, __ATOMIC_ACQUIRE));
@@ -134,7 +142,7 @@ static inline int32_t spa_ringbuffer_get_write_index(struct spa_ringbuffer *rbuf
  * \param data source memory
  * \param len number of bytes to write
  */
-static inline void
+SPA_API_RINGBUFFER void
 spa_ringbuffer_write_data(struct spa_ringbuffer *rbuf SPA_UNUSED,
 			  void *buffer, uint32_t size,
 			  uint32_t offset, const void *data, uint32_t len)
@@ -151,7 +159,7 @@ spa_ringbuffer_write_data(struct spa_ringbuffer *rbuf SPA_UNUSED,
  * \param rbuf a spa_ringbuffer
  * \param index new index
  */
-static inline void spa_ringbuffer_write_update(struct spa_ringbuffer *rbuf, int32_t index)
+SPA_API_RINGBUFFER void spa_ringbuffer_write_update(struct spa_ringbuffer *rbuf, int32_t index)
 {
 	__atomic_store_n(&rbuf->writeindex, index, __ATOMIC_RELEASE);
 }
diff --git a/spa/include/spa/utils/string.h b/spa/include/spa/utils/string.h
index 6ee9e2cd..060ef7d6 100644
--- a/spa/include/spa/utils/string.h
+++ b/spa/include/spa/utils/string.h
@@ -17,6 +17,14 @@ extern "C" {
 
 #include <spa/utils/defs.h>
 
+#ifndef SPA_API_STRING
+ #ifdef SPA_API_IMPL
+  #define SPA_API_STRING SPA_API_IMPL
+ #else
+  #define SPA_API_STRING static inline
+ #endif
+#endif
+
 /**
  * \defgroup spa_string String handling
  * String handling utilities
@@ -33,7 +41,7 @@ extern "C" {
  * If both \a a and \a b are NULL, the two are considered equal.
  *
  */
-static inline bool spa_streq(const char *s1, const char *s2)
+SPA_API_STRING bool spa_streq(const char *s1, const char *s2)
 {
 	return SPA_LIKELY(s1 && s2) ? strcmp(s1, s2) == 0 : s1 == s2;
 }
@@ -43,7 +51,7 @@ static inline bool spa_streq(const char *s1, const char *s2)
  *
  * If both \a a and \a b are NULL, the two are considered equal.
  */
-static inline bool spa_strneq(const char *s1, const char *s2, size_t len)
+SPA_API_STRING bool spa_strneq(const char *s1, const char *s2, size_t len)
 {
 	return SPA_LIKELY(s1 && s2) ? strncmp(s1, s2, len) == 0 : s1 == s2;
 }
@@ -54,7 +62,7 @@ static inline bool spa_strneq(const char *s1, const char *s2, size_t len)
  * A \a s is NULL, it never starts with the given \a prefix. A \a prefix of
  * NULL is a bug in the caller.
  */
-static inline bool spa_strstartswith(const char *s, const char *prefix)
+SPA_API_STRING bool spa_strstartswith(const char *s, const char *prefix)
 {
 	if (SPA_UNLIKELY(s == NULL))
 		return false;
@@ -70,7 +78,7 @@ static inline bool spa_strstartswith(const char *s, const char *prefix)
  * A \a s is NULL, it never ends with the given \a suffix. A \a suffix of
  * NULL is a bug in the caller.
  */
-static inline bool spa_strendswith(const char *s, const char *suffix)
+SPA_API_STRING bool spa_strendswith(const char *s, const char *suffix)
 {
 	size_t l1, l2;
 
@@ -92,7 +100,7 @@ static inline bool spa_strendswith(const char *s, const char *suffix)
  *
  * \return true on success, false otherwise
  */
-static inline bool spa_atoi32(const char *str, int32_t *val, int base)
+SPA_API_STRING bool spa_atoi32(const char *str, int32_t *val, int base)
 {
 	char *endptr;
 	long v;
@@ -120,7 +128,7 @@ static inline bool spa_atoi32(const char *str, int32_t *val, int base)
  *
  * \return true on success, false otherwise
  */
-static inline bool spa_atou32(const char *str, uint32_t *val, int base)
+SPA_API_STRING bool spa_atou32(const char *str, uint32_t *val, int base)
 {
 	char *endptr;
 	unsigned long long v;
@@ -148,7 +156,7 @@ static inline bool spa_atou32(const char *str, uint32_t *val, int base)
  *
  * \return true on success, false otherwise
  */
-static inline bool spa_atoi64(const char *str, int64_t *val, int base)
+SPA_API_STRING bool spa_atoi64(const char *str, int64_t *val, int base)
 {
 	char *endptr;
 	long long v;
@@ -173,7 +181,7 @@ static inline bool spa_atoi64(const char *str, int64_t *val, int base)
  *
  * \return true on success, false otherwise
  */
-static inline bool spa_atou64(const char *str, uint64_t *val, int base)
+SPA_API_STRING bool spa_atou64(const char *str, uint64_t *val, int base)
 {
 	char *endptr;
 	unsigned long long v;
@@ -196,7 +204,7 @@ static inline bool spa_atou64(const char *str, uint64_t *val, int base)
  *
  * \return true on success, false otherwise
  */
-static inline bool spa_atob(const char *str)
+SPA_API_STRING bool spa_atob(const char *str)
 {
 	return spa_streq(str, "true") || spa_streq(str, "1");
 }
@@ -210,7 +218,7 @@ static inline bool spa_atob(const char *str)
  * number on error.
  */
 SPA_PRINTF_FUNC(3, 0)
-static inline int spa_vscnprintf(char *buffer, size_t size, const char *format, va_list args)
+SPA_API_STRING int spa_vscnprintf(char *buffer, size_t size, const char *format, va_list args)
 {
 	int r;
 
@@ -233,7 +241,7 @@ static inline int spa_vscnprintf(char *buffer, size_t size, const char *format,
  * number on error.
  */
 SPA_PRINTF_FUNC(3, 4)
-static inline int spa_scnprintf(char *buffer, size_t size, const char *format, ...)
+SPA_API_STRING int spa_scnprintf(char *buffer, size_t size, const char *format, ...)
 {
 	int r;
 	va_list args;
@@ -253,7 +261,7 @@ static inline int spa_scnprintf(char *buffer, size_t size, const char *format, .
  *
  * \return the result float.
  */
-static inline float spa_strtof(const char *str, char **endptr)
+SPA_API_STRING float spa_strtof(const char *str, char **endptr)
 {
 #ifndef __LOCALE_C_ONLY
 	static locale_t locale = NULL;
@@ -279,7 +287,7 @@ static inline float spa_strtof(const char *str, char **endptr)
  *
  * \return true on success, false otherwise
  */
-static inline bool spa_atof(const char *str, float *val)
+SPA_API_STRING bool spa_atof(const char *str, float *val)
 {
 	char *endptr;
 	float v;
@@ -303,7 +311,7 @@ static inline bool spa_atof(const char *str, float *val)
  *
  * \return the result float.
  */
-static inline double spa_strtod(const char *str, char **endptr)
+SPA_API_STRING double spa_strtod(const char *str, char **endptr)
 {
 #ifndef __LOCALE_C_ONLY
 	static locale_t locale = NULL;
@@ -329,7 +337,7 @@ static inline double spa_strtod(const char *str, char **endptr)
  *
  * \return true on success, false otherwise
  */
-static inline bool spa_atod(const char *str, double *val)
+SPA_API_STRING bool spa_atod(const char *str, double *val)
 {
 	char *endptr;
 	double v;
@@ -346,7 +354,7 @@ static inline bool spa_atod(const char *str, double *val)
 	return true;
 }
 
-static inline char *spa_dtoa(char *str, size_t size, double val)
+SPA_API_STRING char *spa_dtoa(char *str, size_t size, double val)
 {
 	int i, l;
 	l = spa_scnprintf(str, size, "%f", val);
@@ -362,7 +370,7 @@ struct spa_strbuf {
 	size_t pos;
 };
 
-static inline void spa_strbuf_init(struct spa_strbuf *buf, char *buffer, size_t maxsize)
+SPA_API_STRING void spa_strbuf_init(struct spa_strbuf *buf, char *buffer, size_t maxsize)
 {
 	buf->buffer = buffer;
 	buf->maxsize = maxsize;
@@ -372,7 +380,7 @@ static inline void spa_strbuf_init(struct spa_strbuf *buf, char *buffer, size_t
 }
 
 SPA_PRINTF_FUNC(2, 3)
-static inline int spa_strbuf_append(struct spa_strbuf *buf, const char *fmt, ...)
+SPA_API_STRING int spa_strbuf_append(struct spa_strbuf *buf, const char *fmt, ...)
 {
 	size_t remain = buf->maxsize - buf->pos;
 	ssize_t written;
diff --git a/spa/include/spa/utils/type-info.h b/spa/include/spa/utils/type-info.h
index 1d9cd29b..9ee2f3ab 100644
--- a/spa/include/spa/utils/type-info.h
+++ b/spa/include/spa/utils/type-info.h
@@ -20,10 +20,6 @@ extern "C" {
 #define SPA_TYPE_ROOT	spa_types
 #endif
 
-static inline bool spa_type_is_a(const char *type, const char *parent)
-{
-	return type != NULL && parent != NULL && strncmp(type, parent, strlen(parent)) == 0;
-}
 
 #include <spa/utils/type.h>
 #include <spa/utils/enum-types.h>
diff --git a/spa/include/spa/utils/type.h b/spa/include/spa/utils/type.h
index 65610c11..88de2c62 100644
--- a/spa/include/spa/utils/type.h
+++ b/spa/include/spa/utils/type.h
@@ -10,6 +10,15 @@ extern "C" {
 #endif
 
 #include <spa/utils/defs.h>
+#include <spa/utils/string.h>
+
+#ifndef SPA_API_TYPE
+ #ifdef SPA_API_IMPL
+  #define SPA_API_TYPE SPA_API_IMPL
+ #else
+  #define SPA_API_TYPE static inline
+ #endif
+#endif
 
 /** \defgroup spa_types Types
  * Data type information enumerations
@@ -123,6 +132,47 @@ struct spa_type_info {
 	const struct spa_type_info *values;
 };
 
+SPA_API_TYPE bool spa_type_is_a(const char *type, const char *parent)
+{
+	return type != NULL && parent != NULL && strncmp(type, parent, strlen(parent)) == 0;
+}
+
+SPA_API_TYPE const char *spa_type_short_name(const char *name)
+{
+	const char *h;
+	if ((h = strrchr(name, ':')) != NULL)
+		name = h + 1;
+	return name;
+}
+
+SPA_API_TYPE uint32_t spa_type_from_short_name(const char *name,
+		const struct spa_type_info *info, uint32_t unknown)
+{
+	int i;
+	for (i = 0; info[i].name; i++) {
+		if (spa_streq(name, spa_type_short_name(info[i].name)))
+			return info[i].type;
+	}
+	return unknown;
+}
+SPA_API_TYPE const char * spa_type_to_name(uint32_t type,
+		const struct spa_type_info *info, const char *unknown)
+{
+	int i;
+	for (i = 0; info[i].name; i++) {
+		if (info[i].type == type)
+			return info[i].name;
+	}
+	return unknown;
+}
+
+SPA_API_TYPE const char * spa_type_to_short_name(uint32_t type,
+		const struct spa_type_info *info, const char *unknown)
+{
+	const char *n = spa_type_to_name(type, info, unknown);
+	return n ? spa_type_short_name(n) : NULL;
+}
+
 /**
  * \}
  */
diff --git a/spa/lib/lib.c b/spa/lib/lib.c
new file mode 100644
index 00000000..e2acb9cc
--- /dev/null
+++ b/spa/lib/lib.c
@@ -0,0 +1,161 @@
+
+#define SPA_API_IMPL	SPA_EXPORT
+#include <spa/utils/defs.h>
+#include <spa/buffer/alloc.h>
+#include <spa/buffer/buffer.h>
+#include <spa/buffer/type-info.h>
+#include <spa/control/control.h>
+#include <spa/control/type-info.h>
+#include <spa/control/ump-utils.h>
+#include <spa/debug/buffer.h>
+#include <spa/debug/context.h>
+#include <spa/debug/dict.h>
+#include <spa/debug/file.h>
+#include <spa/debug/format.h>
+#include <spa/debug/log.h>
+#include <spa/debug/mem.h>
+#include <spa/debug/node.h>
+#include <spa/debug/pod.h>
+#include <spa/debug/types.h>
+#include <spa/filter-graph/filter-graph.h>
+#include <spa/graph/graph.h>
+#include <spa/interfaces/audio/aec.h>
+#include <spa/monitor/device.h>
+#include <spa/monitor/event.h>
+#include <spa/monitor/type-info.h>
+#include <spa/monitor/utils.h>
+#include <spa/node/command.h>
+#include <spa/node/event.h>
+#include <spa/node/io.h>
+#include <spa/node/keys.h>
+#include <spa/node/node.h>
+#include <spa/node/type-info.h>
+#include <spa/node/utils.h>
+#include <spa/param/audio/aac.h>
+#include <spa/param/audio/aac-types.h>
+#include <spa/param/audio/aac-utils.h>
+#include <spa/param/audio/alac.h>
+#include <spa/param/audio/alac-utils.h>
+#include <spa/param/audio/amr.h>
+#include <spa/param/audio/amr-types.h>
+#include <spa/param/audio/amr-utils.h>
+#include <spa/param/audio/ape.h>
+#include <spa/param/audio/ape-utils.h>
+#include <spa/param/audio/compressed.h>
+#include <spa/param/audio/dsd.h>
+#include <spa/param/audio/dsd-utils.h>
+#include <spa/param/audio/dsp.h>
+#include <spa/param/audio/dsp-utils.h>
+#include <spa/param/audio/flac.h>
+#include <spa/param/audio/flac-utils.h>
+#include <spa/param/audio/format.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/param/audio/iec958.h>
+#include <spa/param/audio/iec958-types.h>
+#include <spa/param/audio/iec958-utils.h>
+#include <spa/param/audio/layout.h>
+#include <spa/param/audio/mp3.h>
+#include <spa/param/audio/mp3-types.h>
+#include <spa/param/audio/mp3-utils.h>
+#include <spa/param/audio/opus.h>
+#include <spa/param/audio/ra.h>
+#include <spa/param/audio/ra-utils.h>
+#include <spa/param/audio/raw.h>
+#include <spa/param/audio/raw-json.h>
+#include <spa/param/audio/raw-types.h>
+#include <spa/param/audio/raw-utils.h>
+#include <spa/param/audio/type-info.h>
+#include <spa/param/audio/vorbis.h>
+#include <spa/param/audio/vorbis-utils.h>
+#include <spa/param/audio/wma.h>
+#include <spa/param/audio/wma-types.h>
+#include <spa/param/audio/wma-utils.h>
+#include <spa/param/bluetooth/audio.h>
+#include <spa/param/bluetooth/type-info.h>
+#include <spa/param/buffers.h>
+#include <spa/param/buffers-types.h>
+#include <spa/param/format.h>
+#include <spa/param/format-types.h>
+#include <spa/param/format-utils.h>
+#include <spa/param/latency.h>
+#include <spa/param/latency-types.h>
+#include <spa/param/latency-utils.h>
+#include <spa/param/param.h>
+#include <spa/param/param-types.h>
+#include <spa/param/port-config.h>
+#include <spa/param/port-config-types.h>
+#include <spa/param/profile.h>
+#include <spa/param/profiler.h>
+#include <spa/param/profiler-types.h>
+#include <spa/param/profile-types.h>
+#include <spa/param/props.h>
+#include <spa/param/props-types.h>
+#include <spa/param/route.h>
+#include <spa/param/route-types.h>
+#include <spa/param/tag.h>
+#include <spa/param/tag-types.h>
+#include <spa/param/tag-utils.h>
+#include <spa/param/type-info.h>
+#include <spa/param/video/chroma.h>
+#include <spa/param/video/color.h>
+#include <spa/param/video/dsp.h>
+#include <spa/param/video/dsp-utils.h>
+#include <spa/param/video/encoded.h>
+#include <spa/param/video/format.h>
+#include <spa/param/video/format-utils.h>
+#include <spa/param/video/h264.h>
+#include <spa/param/video/h264-utils.h>
+#include <spa/param/video/mjpg.h>
+#include <spa/param/video/mjpg-utils.h>
+#include <spa/param/video/multiview.h>
+#include <spa/param/video/raw.h>
+#include <spa/param/video/raw-types.h>
+#include <spa/param/video/raw-utils.h>
+#include <spa/param/video/type-info.h>
+#include <spa/pod/builder.h>
+#include <spa/pod/command.h>
+#include <spa/pod/compare.h>
+#include <spa/pod/dynamic.h>
+#include <spa/pod/event.h>
+#include <spa/pod/filter.h>
+#include <spa/pod/iter.h>
+#include <spa/pod/parser.h>
+#include <spa/pod/pod.h>
+#include <spa/pod/vararg.h>
+#include <spa/support/cpu.h>
+#include <spa/support/dbus.h>
+#include <spa/support/i18n.h>
+#include <spa/support/log.h>
+#include <spa/support/log-impl.h>
+#include <spa/support/loop.h>
+#include <spa/support/plugin.h>
+#include <spa/support/plugin-loader.h>
+#include <spa/support/system.h>
+#include <spa/support/thread.h>
+#include <spa/utils/ansi.h>
+#include <spa/utils/atomic.h>
+#include <spa/utils/cleanup.h>
+#include <spa/utils/defs.h>
+#include <spa/utils/dict.h>
+#include <spa/utils/dll.h>
+#include <spa/utils/endian.h>
+#include <spa/utils/enum-types.h>
+#include <spa/utils/hook.h>
+#include <spa/utils/json-core.h>
+#include <spa/utils/json.h>
+#include <spa/utils/json-pod.h>
+#include <spa/utils/keys.h>
+#include <spa/utils/list.h>
+#include <spa/utils/names.h>
+#include <spa/utils/ratelimit.h>
+#include <spa/utils/result.h>
+#include <spa/utils/ringbuffer.h>
+#include <spa/utils/string.h>
+#include <spa/utils/type.h>
+#include <spa/utils/type-info.h>
+
+
+
+
+
+
diff --git a/spa/lib/meson.build b/spa/lib/meson.build
new file mode 100644
index 00000000..a12c3043
--- /dev/null
+++ b/spa/lib/meson.build
@@ -0,0 +1,6 @@
+spa_lib = shared_library('spa',
+  [ 'lib.c' ],
+  include_directories : [ configinc ],
+  dependencies : [ spa_dep, pthread_lib, mathlib ],
+  install : true,
+  install_dir : spa_plugindir )
diff --git a/spa/meson.build b/spa/meson.build
index cf25609d..9b5f8960 100644
--- a/spa/meson.build
+++ b/spa/meson.build
@@ -43,7 +43,7 @@ if get_option('spa-plugins').allowed()
   endif
 
   # plugin-specific dependencies
-  alsa_dep = dependency('alsa', required: get_option('alsa'))
+  alsa_dep = dependency('alsa', version : '>=1.2.10', required: get_option('alsa'))
   summary({'ALSA': alsa_dep.found()}, bool_yn: true, section: 'Backend')
 
   bluez_dep = dependency('bluez', version : '>= 4.101', required: get_option('bluez5'))
@@ -89,6 +89,9 @@ if get_option('spa-plugins').allowed()
       summary({'ModemManager': mm_dep.found()}, bool_yn: true, section: 'Bluetooth backends')
     endif
     cdata.set('HAVE_LC3', get_option('bluez5-codec-lc3').allowed() and lc3_dep.found())
+    g722_codec_option = get_option('bluez5-codec-g722')
+    summary({'G722': g722_codec_option.allowed()}, bool_yn: true, section: 'Bluetooth audio codecs')
+    cdata.set('HAVE_G722', g722_codec_option.allowed())
   endif
 
   have_vulkan = false
@@ -111,10 +114,20 @@ if get_option('spa-plugins').allowed()
   cdata.set('HAVE_LIBUDEV', libudev_dep.found())
   summary({'Udev': libudev_dep.found()}, bool_yn: true, section: 'Backend')
 
-  cdata.set('HAVE_SPA_PLUGINS', '1')
+  libmysofa_dep = dependency('libmysofa', required : get_option('libmysofa'))
+  summary({'libmysofa': libmysofa_dep.found()}, bool_yn: true, section: 'filter-graph')
+
+  lilv_lib = dependency('lilv-0', required: get_option('lv2'))
+  summary({'lilv (for lv2 plugins)': lilv_lib.found()}, bool_yn: true, section: 'filter-graph')
+
+  ebur128_lib = dependency('libebur128', required: get_option('ebur128').enabled())
+  summary({'EBUR128': ebur128_lib.found()}, bool_yn: true, section: 'filter-graph')
+
+  cdata.set('HAVE_SPA_PLUGINS', true)
   subdir('plugins')
 endif
 
 subdir('tools')
 subdir('tests')
 subdir('examples')
+subdir('lib')
diff --git a/spa/plugins/aec/aec-webrtc.cpp b/spa/plugins/aec/aec-webrtc.cpp
index 354ad940..74255aae 100644
--- a/spa/plugins/aec/aec-webrtc.cpp
+++ b/spa/plugins/aec/aec-webrtc.cpp
@@ -28,7 +28,11 @@ struct impl_data {
 	struct spa_audio_aec aec;
 
 	struct spa_log *log;
+#if defined(HAVE_WEBRTC) || defined(HAVE_WEBRTC1)
 	std::unique_ptr<webrtc::AudioProcessing> apm;
+#elif defined(HAVE_WEBRTC2)
+	rtc::scoped_refptr<webrtc::AudioProcessing> apm;
+#endif
 	spa_audio_info_raw rec_info;
 	spa_audio_info_raw out_info;
 	spa_audio_info_raw play_info;
@@ -70,10 +74,9 @@ static int parse_mic_geometry(struct impl_data *impl, const char *mic_geometry,
 {
 	int res;
 	size_t i;
-	struct spa_json it[2];
+	struct spa_json it[1];
 
-	spa_json_init(&it[0], mic_geometry, strlen(mic_geometry));
-	if (spa_json_enter_array(&it[0], &it[1]) <= 0) {
+	if (spa_json_begin_array(&it[0], mic_geometry, strlen(mic_geometry)) <= 0) {
 		spa_log_error(impl->log, "Error: webrtc.mic-geometry expects an array");
 		return -EINVAL;
 	}
@@ -81,7 +84,7 @@ static int parse_mic_geometry(struct impl_data *impl, const char *mic_geometry,
 	for (i = 0; i < geometry.size(); i++) {
 		float f[3];
 
-		if ((res = parse_point(&it[1], f)) < 0) {
+		if ((res = parse_point(&it[0], f)) < 0) {
 			spa_log_error(impl->log, "Error: can't parse webrtc.mic-geometry points: %d", res);
 			return res;
 		}
@@ -104,16 +107,17 @@ static int webrtc_init2(void *object, const struct spa_dict *args,
 
 	bool high_pass_filter = webrtc_get_spa_bool(args, "webrtc.high_pass_filter", true);
 	bool noise_suppression = webrtc_get_spa_bool(args, "webrtc.noise_suppression", true);
-	bool voice_detection = webrtc_get_spa_bool(args, "webrtc.voice_detection", true);
-#ifdef HAVE_WEBRTC
+#if defined(HAVE_WEBRTC)
 	bool extended_filter = webrtc_get_spa_bool(args, "webrtc.extended_filter", true);
 	bool delay_agnostic = webrtc_get_spa_bool(args, "webrtc.delay_agnostic", true);
 	// Disable experimental flags by default
 	bool experimental_agc = webrtc_get_spa_bool(args, "webrtc.experimental_agc", false);
 	bool experimental_ns = webrtc_get_spa_bool(args, "webrtc.experimental_ns", false);
 
+	bool voice_detection = webrtc_get_spa_bool(args, "webrtc.voice_detection", true);
 	bool beamforming = webrtc_get_spa_bool(args, "webrtc.beamforming", false);
-#else
+#elif defined(HAVE_WEBRTC1)
+	bool voice_detection = webrtc_get_spa_bool(args, "webrtc.voice_detection", true);
 	bool transient_suppression = webrtc_get_spa_bool(args, "webrtc.transient_suppression", true);
 #endif
 	// Note: AGC seems to mess up with Agnostic Delay Detection, especially with speech,
@@ -124,7 +128,7 @@ static int webrtc_init2(void *object, const struct spa_dict *args,
 	// This filter will modify playback buffer (when calling ProcessReverseStream), but now
 	// playback buffer modifications are discarded.
 
-#ifdef HAVE_WEBRTC
+#if defined(HAVE_WEBRTC)
 	webrtc::Config config;
 	config.Set<webrtc::ExtendedFilter>(new webrtc::ExtendedFilter(extended_filter));
 	config.Set<webrtc::DelayAgnostic>(new webrtc::DelayAgnostic(delay_agnostic));
@@ -168,7 +172,7 @@ static int webrtc_init2(void *object, const struct spa_dict *args,
 			config.Set<webrtc::Beamforming>(new webrtc::Beamforming(true, geometry));
 		}
 	}
-#else
+#elif defined(HAVE_WEBRTC1)
 	webrtc::AudioProcessing::Config config;
 	config.echo_canceller.enabled = true;
 	config.pipeline.multi_channel_capture = rec_info->channels > 1;
@@ -185,20 +189,43 @@ static int webrtc_init2(void *object, const struct spa_dict *args,
 	// FIXME: expose pre/postamp gain
 	config.transient_suppression.enabled = transient_suppression;
 	config.voice_detection.enabled = voice_detection;
+#elif defined(HAVE_WEBRTC2)
+	webrtc::AudioProcessing::Config config;
+	config.pipeline.multi_channel_capture = rec_info->channels > 1;
+	config.pipeline.multi_channel_render = play_info->channels > 1;
+	// FIXME: Example code enables both gain controllers, but that seems sus
+	config.gain_controller1.enabled = gain_control;
+	config.gain_controller1.mode = webrtc::AudioProcessing::Config::GainController1::Mode::kAdaptiveDigital;
+	config.gain_controller2.enabled = gain_control;
+	config.high_pass_filter.enabled = high_pass_filter;
+	config.noise_suppression.enabled = noise_suppression;
+	config.noise_suppression.level = webrtc::AudioProcessing::Config::NoiseSuppression::kHigh;
+	// FIXME: expose pre/postamp gain
 #endif
 
+#if defined(HAVE_WEBRTC) || defined(HAVE_WEBRTC1)
 	webrtc::ProcessingConfig pconfig = {{
 		webrtc::StreamConfig(rec_info->rate, rec_info->channels, false), /* input stream */
 		webrtc::StreamConfig(out_info->rate, out_info->channels, false), /* output stream */
 		webrtc::StreamConfig(play_info->rate, play_info->channels, false), /* reverse input stream */
 		webrtc::StreamConfig(play_info->rate, play_info->channels, false), /* reverse output stream */
 	}};
+#elif defined(HAVE_WEBRTC2)
+	webrtc::ProcessingConfig pconfig = {{
+		webrtc::StreamConfig(rec_info->rate, rec_info->channels), /* input stream */
+		webrtc::StreamConfig(out_info->rate, out_info->channels), /* output stream */
+		webrtc::StreamConfig(play_info->rate, play_info->channels), /* reverse input stream */
+		webrtc::StreamConfig(play_info->rate, play_info->channels), /* reverse output stream */
+	}};
+#endif
 
-#ifdef HAVE_WEBRTC
+#if defined(HAVE_WEBRTC)
 	auto apm = std::unique_ptr<webrtc::AudioProcessing>(webrtc::AudioProcessing::Create(config));
-#else
+#elif defined(HAVE_WEBRTC1)
 	auto apm = std::unique_ptr<webrtc::AudioProcessing>(webrtc::AudioProcessingBuilder().Create());
-
+	apm->ApplyConfig(config);
+#elif defined(HAVE_WEBRTC2)
+	auto apm = webrtc::AudioProcessingBuilder().Create();
 	apm->ApplyConfig(config);
 #endif
 
@@ -251,12 +278,21 @@ static int webrtc_run(void *object, const float *rec[], const float *play[], flo
 	auto impl = static_cast<struct impl_data*>(object);
 	int res;
 
+#if defined(HAVE_WEBRTC) || defined(HAVE_WEBRTC1)
 	webrtc::StreamConfig play_config =
 		webrtc::StreamConfig(impl->play_info.rate, impl->play_info.channels, false);
 	webrtc::StreamConfig rec_config =
 		webrtc::StreamConfig(impl->rec_info.rate, impl->rec_info.channels, false);
 	webrtc::StreamConfig out_config =
 		webrtc::StreamConfig(impl->out_info.rate, impl->out_info.channels, false);
+#elif defined(HAVE_WEBRTC2)
+	webrtc::StreamConfig play_config =
+		webrtc::StreamConfig(impl->play_info.rate, impl->play_info.channels);
+	webrtc::StreamConfig rec_config =
+		webrtc::StreamConfig(impl->rec_info.rate, impl->rec_info.channels);
+	webrtc::StreamConfig out_config =
+		webrtc::StreamConfig(impl->out_info.rate, impl->out_info.channels);
+#endif
 	unsigned int num_blocks = n_samples * 1000 / impl->play_info.rate / 10;
 
 	if (n_samples * 1000 / impl->play_info.rate % 10 != 0) {
diff --git a/spa/plugins/alsa/90-pipewire-alsa.rules b/spa/plugins/alsa/90-pipewire-alsa.rules
index e19a0951..9ef3d533 100644
--- a/spa/plugins/alsa/90-pipewire-alsa.rules
+++ b/spa/plugins/alsa/90-pipewire-alsa.rules
@@ -19,8 +19,8 @@ SUBSYSTEM!="sound", GOTO="pipewire_end"
 ACTION!="change", GOTO="pipewire_end"
 KERNEL!="card*", GOTO="pipewire_end"
 SUBSYSTEMS=="usb", GOTO="pipewire_check_usb"
-SUBSYSTEMS=="pci", GOTO="pipewire_check_pci"
 SUBSYSTEMS=="firewire", GOTO="pipewire_firewire_quirk"
+SUBSYSTEMS=="pci", GOTO="pipewire_check_pci"
 
 SUBSYSTEMS=="platform", DRIVERS=="thinkpad_acpi", ENV{ACP_IGNORE}="1"
 
diff --git a/spa/plugins/alsa/acp-tool.c b/spa/plugins/alsa/acp-tool.c
index 0f72a4b6..cff9edb8 100644
--- a/spa/plugins/alsa/acp-tool.c
+++ b/spa/plugins/alsa/acp-tool.c
@@ -10,8 +10,11 @@
 #include <time.h>
 #include <stdbool.h>
 #include <getopt.h>
+#include <alloca.h>
 
+#include <spa/debug/context.h>
 #include <spa/utils/string.h>
+#include <spa/utils/json.h>
 
 #include <acp/acp.h>
 
@@ -587,40 +590,34 @@ static int do_probe(struct data *data)
 	uint32_t n_items = 0;
 	struct acp_dict_item items[64];
 	struct acp_dict props;
+	struct spa_json it;
 
 	acp_set_log_func(log_func, data);
 	acp_set_log_level(data->verbose);
 
 	items[n_items++] = ACP_DICT_ITEM_INIT("use-ucm", "true");
 	items[n_items++] = ACP_DICT_ITEM_INIT("verbose", data->verbose ? "true" : "false");
-	if (data->properties != NULL) {
-		char *p = data->properties, *e, f;
 
-		while (*p) {
-			const char *k, *v;
-
-			if ((e = strchr(p, '=')) == NULL)
-				break;
-			*e = '\0';
-			k = p;
-			p = e+1;
-
-			if (*p == '\"') {
-				p++;
-				f = '\"';
-			} else {
-				f = ' ';
-			}
-			if ((e = strchr(p, f)) == NULL &&
-			    (e = strchr(p, '\0')) == NULL)
-				break;
-			*e = '\0';
-			v = p;
-			p = e+1;
+	if (spa_json_begin_object_relax(&it, data->properties, strlen(data->properties)) > 0) {
+		char key[1024];
+		const char *value;
+		int len;
+		struct spa_error_location loc;
+		while ((len = spa_json_object_next(&it, key, sizeof(key), &value)) > 0) {
+			char *k = alloca(strlen(key) + 1);
+			char *v = alloca(len + 1);
+			memcpy(k, key, strlen(key) + 1);
+			spa_json_parse_stringn(value, len, v, len + 1);
 			items[n_items++] = ACP_DICT_ITEM_INIT(k, v);
-			if (n_items == 64)
+			if (n_items >= SPA_N_ELEMENTS(items))
 				break;
 		}
+		if (spa_json_get_error(&it, data->properties, &loc)) {
+			struct spa_debug_context *c = NULL;
+			spa_debugc(c, "invalid --properties: %s", loc.reason);
+			spa_debugc_error_location(c, &loc);
+			return -EINVAL;
+		}
 	}
 	props = ACP_DICT_INIT(items, n_items);
 
@@ -714,7 +711,7 @@ int main(int argc, char *argv[])
 {
 	int c, res;
 	int longopt_index = 0, ret;
-	struct data data = { 0, };
+	struct data data = { .properties = strdup("") };
 
 	data.verbose = 1;
 
@@ -735,6 +732,7 @@ int main(int argc, char *argv[])
 			data.card_index = ret;
 			break;
 		case 'p':
+			free(data.properties);
 			data.properties = strdup(optarg);
 			break;
                 default:
diff --git a/spa/plugins/alsa/acp/acp.c b/spa/plugins/alsa/acp/acp.c
index 505681d5..7b49c8be 100644
--- a/spa/plugins/alsa/acp/acp.c
+++ b/spa/plugins/alsa/acp/acp.c
@@ -8,6 +8,7 @@
 
 #include <spa/utils/string.h>
 #include <spa/utils/json.h>
+#include <spa/param/audio/iec958-types.h>
 
 int _acp_log_level = 1;
 acp_log_func _acp_log_func;
@@ -229,6 +230,25 @@ static void init_device(pa_card *impl, pa_alsa_device *dev, pa_alsa_direction_t
 		dev->device.direction = ACP_DIRECTION_CAPTURE;
 		pa_proplist_update(dev->proplist, PA_UPDATE_REPLACE, m->input_proplist);
 	}
+	if (m->split) {
+		char pos[2048];
+		struct spa_strbuf b;
+		int i;
+
+		spa_strbuf_init(&b, pos, sizeof(pos));
+		spa_strbuf_append(&b, "[");
+		for (i = 0; i < m->split->channels; ++i)
+			spa_strbuf_append(&b, "%sAUX%d", ((i == 0) ? "" : ","), m->split->idx[i]);
+		spa_strbuf_append(&b, "]");
+		pa_proplist_sets(dev->proplist, "api.alsa.split.position", pos);
+
+		spa_strbuf_init(&b, pos, sizeof(pos));
+		spa_strbuf_append(&b, "[");
+		for (i = 0; i < m->split->hw_channels; ++i)
+			spa_strbuf_append(&b, "%sAUX%d", ((i == 0) ? "" : ","), i);
+		spa_strbuf_append(&b, "]");
+		pa_proplist_sets(dev->proplist, "api.alsa.split.hw-position", pos);
+	}
 	pa_proplist_sets(dev->proplist, PA_PROP_DEVICE_PROFILE_NAME, m->name);
 	pa_proplist_sets(dev->proplist, PA_PROP_DEVICE_PROFILE_DESCRIPTION, m->description);
 	pa_proplist_setf(dev->proplist, "card.profile.device", "%u", index);
@@ -452,17 +472,16 @@ static int add_pro_profile(pa_card *impl, uint32_t index)
 
 static bool contains_string(const char *arr, const char *str)
 {
-	struct spa_json it[2];
+	struct spa_json it[1];
 	char v[256];
 
 	if (arr == NULL || str == NULL)
 		return false;
 
-	spa_json_init(&it[0], arr, strlen(arr));
-        if (spa_json_enter_array(&it[0], &it[1]) <= 0)
-                spa_json_init(&it[1], arr, strlen(arr));
+        if (spa_json_begin_array_relax(&it[0], arr, strlen(arr)) <= 0)
+		return false;
 
-	while (spa_json_get_string(&it[1], v, sizeof(v)) > 0) {
+	while (spa_json_get_string(&it[0], v, sizeof(v)) > 0) {
 		if (spa_streq(v, str))
 			return true;
 	}
@@ -952,12 +971,58 @@ static pa_device_port* find_port_with_eld_device(pa_card *impl, int device)
 	return NULL;
 }
 
+static void acp_iec958_codec_mask_to_json(uint64_t codecs, char *buf, size_t maxsize)
+{
+	struct spa_strbuf b;
+	const struct spa_type_info *info;
+
+	spa_strbuf_init(&b, buf, maxsize);
+	for (info = spa_type_audio_iec958_codec; info->name; ++info)
+		if ((codecs & (1ULL << info->type)) && info->type != SPA_AUDIO_IEC958_CODEC_UNKNOWN)
+			spa_strbuf_append(&b, "%s\"%s\"", (b.pos ? "," : "["),
+					spa_type_audio_iec958_codec_to_short_name(info->type));
+	if (b.pos)
+		spa_strbuf_append(&b, "]");
+}
+
+void acp_iec958_codecs_to_json(const uint32_t *codecs, size_t n_codecs, char *buf, size_t maxsize)
+{
+	struct spa_strbuf b;
+
+	spa_strbuf_init(&b, buf, maxsize);
+	spa_strbuf_append(&b, "[");
+	for (size_t i = 0; i < n_codecs; ++i)
+		spa_strbuf_append(&b, "%s\"%s\"", (i ? "," : ""),
+				spa_type_audio_iec958_codec_to_short_name(codecs[i]));
+	spa_strbuf_append(&b, "]");
+}
+
+size_t acp_iec958_codecs_from_json(const char *str, uint32_t *codecs, size_t max_codecs)
+{
+	struct spa_json it;
+	char v[256];
+	size_t n_codecs = 0;
+
+	if (spa_json_begin_array_relax(&it, str, strlen(str)) <= 0)
+		return 0;
+
+	while (spa_json_get_string(&it, v, sizeof(v)) > 0) {
+		uint32_t type = spa_type_audio_iec958_codec_from_short_name(v);
+		if (type != SPA_AUDIO_IEC958_CODEC_UNKNOWN)
+			codecs[n_codecs++] = type;
+		if (n_codecs >= max_codecs)
+			break;
+	}
+
+	return n_codecs;
+}
+
 static int hdmi_eld_changed(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), *elem;
 	int device, i;
-	const char *old_monitor_name;
+	const char *old_monitor_name, *old_iec958_codec_list;
 	pa_device_port *p;
 	pa_hdmi_eld eld;
 	bool changed = false;
@@ -995,6 +1060,18 @@ static int hdmi_eld_changed(snd_mixer_elem_t *melem, unsigned int mask)
 		changed |= (old_monitor_name == NULL) || (!spa_streq(old_monitor_name, eld.monitor_name));
 		pa_proplist_sets(p->proplist, PA_PROP_DEVICE_PRODUCT_NAME, eld.monitor_name);
 	}
+
+	old_iec958_codec_list = pa_proplist_gets(p->proplist, ACP_KEY_IEC958_CODECS_DETECTED);
+	if (eld.iec958_codecs == 0) {
+		changed |= old_iec958_codec_list != NULL;
+		pa_proplist_unset(p->proplist, ACP_KEY_IEC958_CODECS_DETECTED);
+	} else {
+		char codecs[512];
+		acp_iec958_codec_mask_to_json(eld.iec958_codecs, codecs, sizeof(codecs));
+		changed |= (old_iec958_codec_list == NULL) || (!spa_streq(old_iec958_codec_list, codecs));
+		pa_proplist_sets(p->proplist, ACP_KEY_IEC958_CODECS_DETECTED, codecs);
+	}
+
 	pa_proplist_as_dict(p->proplist, &p->port.props);
 
 	if (changed && mask != 0 && impl->events && impl->events->props_changed)
@@ -1387,7 +1464,8 @@ static int setup_mixer(pa_card *impl, pa_alsa_device *dev, bool ignore_dB)
 			data = PA_DEVICE_PORT_DATA(dev->active_port);
 			dev->mixer_path = data->path;
 
-			pa_alsa_path_select(data->path, data->setting, dev->mixer_handle, dev->muted);
+			if (!impl->disable_mixer_path)
+				pa_alsa_path_select(data->path, data->setting, dev->mixer_handle, dev->muted);
 		} else {
 			pa_alsa_ucm_port_data *data;
 
@@ -1396,7 +1474,8 @@ static int setup_mixer(pa_card *impl, pa_alsa_device *dev, bool ignore_dB)
 			/* Now activate volume controls, if any */
 			if (data->path) {
 				dev->mixer_path = data->path;
-				pa_alsa_path_select(dev->mixer_path, NULL, dev->mixer_handle, dev->muted);
+				if (!impl->disable_mixer_path)
+					pa_alsa_path_select(dev->mixer_path, NULL, dev->mixer_handle, dev->muted);
 			}
 		}
 	} else {
@@ -1405,8 +1484,9 @@ static int setup_mixer(pa_card *impl, pa_alsa_device *dev, bool ignore_dB)
 
 		if (dev->mixer_path) {
 			/* Hmm, we have only a single path, then let's activate it */
-			pa_alsa_path_select(dev->mixer_path, dev->mixer_path->settings,
-					dev->mixer_handle, dev->muted);
+			if (!impl->disable_mixer_path)
+				pa_alsa_path_select(dev->mixer_path, dev->mixer_path->settings,
+						dev->mixer_handle, dev->muted);
 		} else
 			return 0;
 	}
@@ -1450,6 +1530,9 @@ static int device_enable(pa_card *impl, pa_alsa_mapping *mapping, pa_alsa_device
 {
 	const char *mod_name;
 	uint32_t i, port_index;
+	const char *codecs;
+	pa_device_port *p;
+	void *state = NULL;
 	int res;
 
 	if (impl->use_ucm &&
@@ -1469,7 +1552,7 @@ static int device_enable(pa_card *impl, pa_alsa_mapping *mapping, pa_alsa_device
 
 	/* Synchronize priority values, as it may have changed when setting the profile */
 	for (i = 0; i < impl->card.n_ports; i++) {
-		pa_device_port *p = (pa_device_port *)impl->card.ports[i];
+		p = (pa_device_port *)impl->card.ports[i];
 		p->port.priority = p->priority;
 	}
 
@@ -1500,6 +1583,15 @@ static int device_enable(pa_card *impl, pa_alsa_mapping *mapping, pa_alsa_device
 	else
 		dev->muted = false;
 
+	while ((p = pa_hashmap_iterate(dev->ports, &state, NULL))) {
+		codecs = pa_proplist_gets(p->proplist, ACP_KEY_IEC958_CODECS_DETECTED);
+		if (codecs) {
+			dev->device.n_codecs = acp_iec958_codecs_from_json(codecs, dev->device.codecs,
+									   ACP_N_ELEMENTS(dev->device.codecs));
+			break;
+		}
+	}
+
 	return 0;
 }
 
@@ -1669,6 +1761,8 @@ struct acp_card *acp_card_new(uint32_t index, const struct acp_dict *props)
 			impl->use_ucm = spa_atob(s);
 		if ((s = acp_dict_lookup(props, "api.alsa.soft-mixer")) != NULL)
 			impl->soft_mixer = spa_atob(s);
+		if ((s = acp_dict_lookup(props, "api.alsa.disable-mixer-path")) != NULL)
+			impl->disable_mixer_path = spa_atob(s);
 		if ((s = acp_dict_lookup(props, "api.alsa.ignore-dB")) != NULL)
 			impl->ignore_dB = spa_atob(s);
 		if ((s = acp_dict_lookup(props, "device.profile-set")) != NULL)
@@ -1683,8 +1777,17 @@ struct acp_card *acp_card_new(uint32_t index, const struct acp_dict *props)
 			impl->rate = atoi(s);
 		if ((s = acp_dict_lookup(props, "api.acp.pro-channels")) != NULL)
 			impl->pro_channels = atoi(s);
+		if ((s = acp_dict_lookup(props, "api.alsa.split-enable")) != NULL)
+			impl->ucm.split_enable = spa_atob(s);
 	}
 
+#if SND_LIB_VERSION < 0x10207
+	if (impl->ucm.split_enable)
+		pa_log_info("alsa-lib too old for PipeWire-side UCM SplitPCM");
+
+	impl->ucm.split_enable = false;		/* API addition in 1.2.7 */
+#endif
+
 	impl->ucm.default_sample_spec.format = PA_SAMPLE_S16NE;
 	impl->ucm.default_sample_spec.rate = impl->rate;
 	impl->ucm.default_sample_spec.channels = 2;
@@ -1753,11 +1856,11 @@ struct acp_card *acp_card_new(uint32_t index, const struct acp_dict *props)
 	if (!impl->auto_profile && profile == NULL)
 		profile = "off";
 
+	init_eld_ctls(impl);
+
 	profile_index = acp_card_find_best_profile_index(&impl->card, profile);
 	acp_card_set_profile(&impl->card, profile_index, 0);
 
-	init_eld_ctls(impl);
-
 	return &impl->card;
 error:
 	pa_alsa_refcnt_dec();
@@ -1874,6 +1977,7 @@ int acp_card_handle_events(struct acp_card *card)
 static void sync_mixer(pa_alsa_device *d, pa_device_port *port)
 {
 	pa_alsa_setting *setting = NULL;
+	pa_card *impl = d->card;
 
 	if (!d->mixer_path)
 		return;
@@ -1886,7 +1990,7 @@ static void sync_mixer(pa_alsa_device *d, pa_device_port *port)
 		setting = data->setting;
 	}
 
-	if (d->mixer_handle)
+	if (d->mixer_handle && !impl->disable_mixer_path)
 		pa_alsa_path_select(d->mixer_path, setting, d->mixer_handle, d->muted);
 
 	if (d->set_mute)
diff --git a/spa/plugins/alsa/acp/acp.h b/spa/plugins/alsa/acp/acp.h
index fadf7853..4b9f2c49 100644
--- a/spa/plugins/alsa/acp/acp.h
+++ b/spa/plugins/alsa/acp/acp.h
@@ -148,6 +148,12 @@ const char *acp_available_str(enum acp_available status);
 		 * like an ALSA control name, but applications must not assume any such relationship.
 		 * The group naming scheme can change without a warning.
 		 */
+#define ACP_KEY_IEC958_CODECS_DETECTED "iec958.codecs.detected"
+		/**< A list of IEC958 passthrough formats which have been auto-detected as being
+		 * supported by a given node. This only serves as a hint, as the auto-detected
+		 * values may be incorrect and/or might change, e.g. when external devices such
+		 * as receivers are powered on or off.
+		 */
 
 struct acp_device;
 
@@ -294,6 +300,9 @@ typedef void (*acp_log_func) (void *data,
 void acp_set_log_func(acp_log_func, void *data);
 void acp_set_log_level(int level);
 
+void acp_iec958_codecs_to_json(const uint32_t *codecs, size_t n_codecs, char *buf, size_t maxsize);
+size_t acp_iec958_codecs_from_json(const char *str, uint32_t *codecs, size_t max_codecs);
+
 #ifdef __cplusplus
 }
 #endif
diff --git a/spa/plugins/alsa/acp/alsa-mixer.c b/spa/plugins/alsa/acp/alsa-mixer.c
index bd4942c7..1f494ee0 100644
--- a/spa/plugins/alsa/acp/alsa-mixer.c
+++ b/spa/plugins/alsa/acp/alsa-mixer.c
@@ -3787,6 +3787,8 @@ void pa_alsa_mapping_free(pa_alsa_mapping *m) {
     pa_proplist_free(m->input_proplist);
     pa_proplist_free(m->output_proplist);
 
+    pa_xfree(m->split);
+
     pa_assert(!m->input_pcm);
     pa_assert(!m->output_pcm);
 
diff --git a/spa/plugins/alsa/acp/alsa-mixer.h b/spa/plugins/alsa/acp/alsa-mixer.h
index 687e8b53..cbfac4ab 100644
--- a/spa/plugins/alsa/acp/alsa-mixer.h
+++ b/spa/plugins/alsa/acp/alsa-mixer.h
@@ -327,6 +327,8 @@ struct pa_alsa_mapping {
     pa_sample_spec sample_spec;
     pa_channel_map channel_map;
 
+    pa_alsa_ucm_split *split;
+
     char **device_strings;
 
     char **input_path_names;
diff --git a/spa/plugins/alsa/acp/alsa-ucm.c b/spa/plugins/alsa/acp/alsa-ucm.c
index 3d3f19bb..9061a1f2 100644
--- a/spa/plugins/alsa/acp/alsa-ucm.c
+++ b/spa/plugins/alsa/acp/alsa-ucm.c
@@ -326,6 +326,90 @@ static const char *get_jack_mixer_device(pa_alsa_ucm_device *dev, bool is_sink)
     return dev_name;
 }
 
+static PA_PRINTF_FUNC(2,3) const char *ucm_get_string(snd_use_case_mgr_t *uc_mgr, const char *fmt, ...)
+{
+    char *id;
+    const char *value;
+    va_list args;
+    int err;
+
+    va_start(args, fmt);
+    id = pa_vsprintf_malloc(fmt, args);
+    va_end(args);
+    err = snd_use_case_get(uc_mgr, id, &value);
+    if (err >= 0)
+        pa_log_debug("Got %s: %s", id, value);
+    pa_xfree(id);
+    if (err < 0) {
+        errno = -err;
+        return NULL;
+    }
+    return value;
+}
+
+static pa_alsa_ucm_split *ucm_get_split_channels(pa_alsa_ucm_device *device, snd_use_case_mgr_t *uc_mgr, const char *prefix) {
+    pa_alsa_ucm_split *split;
+    const char *value;
+    const char *device_name;
+    int i;
+    uint32_t hw_channels;
+
+    device_name = pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_NAME);
+    if (!device_name)
+        return NULL;
+
+    value = ucm_get_string(uc_mgr, "%sChannels/%s", prefix, device_name);
+    if (pa_atou(value, &hw_channels) < 0)
+        return NULL;
+
+    split = pa_xnew0(pa_alsa_ucm_split, 1);
+
+    for (i = 0; i < PA_CHANNELS_MAX; i++) {
+        uint32_t idx;
+        snd_pcm_chmap_t *map;
+
+        value = ucm_get_string(uc_mgr, "%sChannel%d/%s", prefix, i, device_name);
+        if (pa_atou(value, &idx) < 0)
+            break;
+
+        if (idx >= hw_channels)
+            goto fail;
+
+        value = ucm_get_string(uc_mgr, "%sChannelPos%d/%s", prefix, i, device_name);
+        if (!value)
+            goto fail;
+
+        map = snd_pcm_chmap_parse_string(value);
+        if (!map)
+            goto fail;
+
+        if (map->channels == 1) {
+            pa_log_debug("Split %s channel %d -> device %s channel %d: %s (%d)",
+                         prefix, (int)idx, device_name, i, value, map->pos[0]);
+            split->idx[i] = idx;
+            split->pos[i] = map->pos[0];
+            free(map);
+        } else {
+            free(map);
+            goto fail;
+        }
+    }
+
+    if (i == 0) {
+        pa_xfree(split);
+        return NULL;
+    }
+
+    split->channels = i;
+    split->hw_channels = hw_channels;
+    return split;
+
+fail:
+    pa_log_warn("Invalid SplitPCM ALSA UCM rule for device %s", device_name);
+    pa_xfree(split);
+    return NULL;
+}
+
 /* Create a property list for this ucm device */
 static int ucm_get_device_property(
         pa_alsa_ucm_device *device,
@@ -470,6 +554,9 @@ static int ucm_get_device_property(
           pa_hashmap_put(device->capture_volumes, pa_xstrdup(pa_proplist_gets(verb->proplist, PA_ALSA_PROP_UCM_NAME)), vol);
     }
 
+    device->playback_split = ucm_get_split_channels(device, uc_mgr, "Playback");
+    device->capture_split = ucm_get_split_channels(device, uc_mgr, "Capture");
+
     if (PA_UCM_PLAYBACK_PRIORITY_UNSET(device) || PA_UCM_CAPTURE_PRIORITY_UNSET(device)) {
         /* get priority from static table */
         for (i = 0; dev_info[i].id; i++) {
@@ -868,21 +955,30 @@ int pa_alsa_ucm_query_profiles(pa_alsa_ucm_config *ucm, int card_index) {
     char *card_name;
     const char **verb_list, *value;
     int num_verbs, i, err = 0;
+    const char *split_prefix = ucm->split_enable ? "<<<SplitPCM=1>>>" : "";
 
     /* support multiple card instances, address card directly by index */
-    card_name = pa_sprintf_malloc("hw:%i", card_index);
+    card_name = pa_sprintf_malloc("%shw:%i", split_prefix, card_index);
     if (card_name == NULL)
         return -PA_ALSA_ERR_UNSPECIFIED;
     err = snd_use_case_mgr_open(&ucm->ucm_mgr, card_name);
     if (err < 0) {
+        char *ucm_card_name;
+
         /* fallback longname: is UCM available for this card ? */
         pa_xfree(card_name);
-        err = snd_card_get_name(card_index, &card_name);
+        err = snd_card_get_name(card_index, &ucm_card_name);
         if (err < 0) {
             pa_log("Card can't get card_name from card_index %d", card_index);
             err = -PA_ALSA_ERR_UNSPECIFIED;
             goto name_fail;
         }
+        card_name = pa_sprintf_malloc("%s%s", split_prefix, ucm_card_name);
+        free(ucm_card_name);
+        if (card_name == NULL) {
+            err = -PA_ALSA_ERR_UNSPECIFIED;
+            goto name_fail;
+        }
 
         err = snd_use_case_mgr_open(&ucm->ucm_mgr, card_name);
         if (err < 0) {
@@ -955,6 +1051,54 @@ name_fail:
     return err;
 }
 
+static void ucm_verb_set_split_leaders(pa_alsa_ucm_verb *verb) {
+    pa_alsa_ucm_device *d, *d2;
+
+    /* Set first virtual device in each split HW PCM as the split leader */
+
+    PA_LLIST_FOREACH(d, verb->devices) {
+        if (d->playback_split)
+            d->playback_split->leader = true;
+        if (d->capture_split)
+            d->capture_split->leader = true;
+    }
+
+    PA_LLIST_FOREACH(d, verb->devices) {
+        const char *sink = pa_proplist_gets(d->proplist, PA_ALSA_PROP_UCM_SINK);
+        const char *source = pa_proplist_gets(d->proplist, PA_ALSA_PROP_UCM_SOURCE);
+
+        if (d->playback_split) {
+            if (!sink)
+                d->playback_split->leader = false;
+
+            if (d->playback_split->leader) {
+                PA_LLIST_FOREACH(d2, verb->devices) {
+                    const char *sink2 = pa_proplist_gets(d2->proplist, PA_ALSA_PROP_UCM_SINK);
+
+                    if (d == d2 || !d2->playback_split || !sink || !sink2 || !pa_streq(sink, sink2))
+                        continue;
+                    d2->playback_split->leader = false;
+                }
+            }
+        }
+
+        if (d->capture_split) {
+            if (!source)
+                d->capture_split->leader = false;
+
+            if (d->capture_split->leader) {
+                PA_LLIST_FOREACH(d2, verb->devices) {
+                    const char *source2 = pa_proplist_gets(d2->proplist, PA_ALSA_PROP_UCM_SOURCE);
+
+                    if (d == d2 || !d2->capture_split || !source || !source2 || !pa_streq(source, source2))
+                        continue;
+                    d2->capture_split->leader = false;
+                }
+            }
+        }
+    }
+}
+
 int pa_alsa_ucm_get_verb(snd_use_case_mgr_t *uc_mgr, const char *verb_name, const char *verb_desc, pa_alsa_ucm_verb **p_verb) {
     pa_alsa_ucm_device *d;
     pa_alsa_ucm_modifier *mod;
@@ -994,6 +1138,9 @@ int pa_alsa_ucm_get_verb(snd_use_case_mgr_t *uc_mgr, const char *verb_name, cons
         /* Devices properties */
         ucm_get_device_property(d, uc_mgr, verb, dev_name);
     }
+
+    ucm_verb_set_split_leaders(verb);
+
     /* make conflicting or supported device mutual */
     PA_LLIST_FOREACH(d, verb->devices)
         append_lost_relationship(d);
@@ -1092,14 +1239,14 @@ fail:
     }
 }
 
-static void ucm_add_port_props(
-       pa_device_port *port,
-       bool is_sink)
-{
+static void proplist_set_icon_name(
+        pa_proplist *proplist,
+        pa_device_port_type_t type,
+        bool is_sink) {
     const char *icon;
 
     if (is_sink) {
-        switch (port->type) {
+        switch (type) {
             case PA_DEVICE_PORT_TYPE_HEADPHONES:
                 icon = "audio-headphones";
                 break;
@@ -1112,7 +1259,7 @@ static void ucm_add_port_props(
                 break;
         }
     } else {
-        switch (port->type) {
+        switch (type) {
             case PA_DEVICE_PORT_TYPE_HEADSET:
                 icon = "audio-headset";
                 break;
@@ -1123,7 +1270,7 @@ static void ucm_add_port_props(
         }
     }
 
-    pa_proplist_sets(port->proplist, "device.icon_name", icon);
+    pa_proplist_sets(proplist, "device.icon_name", icon);
 }
 
 static char *devset_name(pa_idxset *devices, const char *sep) {
@@ -1229,6 +1376,13 @@ static unsigned devset_capture_priority(pa_idxset *devices, bool invert) {
     return (unsigned) priority;
 }
 
+static void ucm_add_port_props(
+       pa_device_port *port,
+       bool is_sink)
+{
+    proplist_set_icon_name(port->proplist, port->type, is_sink);
+}
+
 void pa_alsa_ucm_add_port(
         pa_hashmap *hash,
         pa_alsa_ucm_mapping_context *context,
@@ -1283,28 +1437,30 @@ void pa_alsa_ucm_add_port(
         pa_hashmap_put(ports, port->name, port);
         pa_log_debug("Add port %s: %s", port->name, port->description);
         ucm_add_port_props(port, is_sink);
+    }
 
-        PA_HASHMAP_FOREACH_KV(verb_name, vol, is_sink ? dev->playback_volumes : dev->capture_volumes, state) {
-            pa_alsa_path *path = pa_alsa_path_synthesize(vol->mixer_elem,
-                                                         is_sink ? PA_ALSA_DIRECTION_OUTPUT : PA_ALSA_DIRECTION_INPUT);
-
-            if (!path)
-                pa_log_warn("Failed to set up volume control: %s", vol->mixer_elem);
-            else {
-                if (vol->master_elem) {
-                    pa_alsa_element *e = pa_alsa_element_get(path, vol->master_elem, false);
-                    e->switch_use = PA_ALSA_SWITCH_MUTE;
-                    e->volume_use = PA_ALSA_VOLUME_MERGE;
-                }
+    data = PA_DEVICE_PORT_DATA(port);
+    PA_HASHMAP_FOREACH_KV(verb_name, vol, is_sink ? dev->playback_volumes : dev->capture_volumes, state) {
+        if (pa_hashmap_get(data->paths, verb_name))
+            continue;
+        pa_alsa_path *path = pa_alsa_path_synthesize(vol->mixer_elem,
+                                                     is_sink ? PA_ALSA_DIRECTION_OUTPUT : PA_ALSA_DIRECTION_INPUT);
+        if (!path)
+            pa_log_warn("Failed to set up volume control: %s", vol->mixer_elem);
+        else {
+            if (vol->master_elem) {
+                pa_alsa_element *e = pa_alsa_element_get(path, vol->master_elem, false);
+                e->switch_use = PA_ALSA_SWITCH_MUTE;
+                e->volume_use = PA_ALSA_VOLUME_MERGE;
+            }
 
-                pa_hashmap_put(data->paths, pa_xstrdup(verb_name), path);
+            pa_hashmap_put(data->paths, pa_xstrdup(verb_name), path);
 
-                /* Add path also to already created empty path set */
-                if (is_sink)
-                    pa_hashmap_put(dev->playback_mapping->output_path_set->paths, pa_xstrdup(vol->mixer_elem), path);
-                else
-                    pa_hashmap_put(dev->capture_mapping->input_path_set->paths, pa_xstrdup(vol->mixer_elem), path);
-            }
+            /* Add path also to already created empty path set */
+            if (is_sink)
+                pa_hashmap_put(dev->playback_mapping->output_path_set->paths, pa_xstrdup(vol->mixer_elem), path);
+            else
+                pa_hashmap_put(dev->capture_mapping->input_path_set->paths, pa_xstrdup(vol->mixer_elem), path);
         }
     }
 
@@ -1363,15 +1519,19 @@ static bool devset_supports_device(pa_idxset *devices, pa_alsa_ucm_device *dev)
             if (!pa_idxset_contains(d->supported_devices, dev))
                 return false;
 
-        /* PlaybackPCM must not be the same as any selected device */
+        /* PlaybackPCM must not be the same as any selected device, except when both split */
         sink2 = pa_proplist_gets(d->proplist, PA_ALSA_PROP_UCM_SINK);
-        if (sink && sink2 && pa_streq(sink, sink2))
-            return false;
+        if (sink && sink2 && pa_streq(sink, sink2)) {
+            if (!(dev->playback_split && d->playback_split))
+                return false;
+        }
 
-        /* CapturePCM must not be the same as any selected device */
+        /* CapturePCM must not be the same as any selected device, except when both split */
         source2 = pa_proplist_gets(d->proplist, PA_ALSA_PROP_UCM_SOURCE);
-        if (source && source2 && pa_streq(source, source2))
-            return false;
+        if (source && source2 && pa_streq(source, source2)) {
+            if (!(dev->capture_split && d->capture_split))
+                return false;
+        }
     }
 
     return true;
@@ -1425,22 +1585,20 @@ static pa_idxset *iterate_device_subsets(pa_idxset *devices, void **state) {
 static pa_idxset *iterate_maximal_device_subsets(pa_idxset *devices, void **state) {
     uint32_t idx;
     pa_alsa_ucm_device *dev;
-    pa_idxset *subset;
+    pa_idxset *subset = NULL;
 
     pa_assert(devices);
     pa_assert(state);
 
-    subset = iterate_device_subsets(devices, state);
-    if (!subset)
-        return subset;
-
-    /* Skip this group if it's incomplete, by checking if we can add any
-     * other device. If we can, this iteration is a subset of another
-     * group that we already returned or eventually return. */
-    PA_IDXSET_FOREACH(dev, devices, idx) {
-        if (!pa_idxset_contains(subset, dev) && devset_supports_device(subset, dev)) {
-            pa_idxset_free(subset, NULL);
-            return iterate_maximal_device_subsets(devices, state);
+    while (subset == NULL && (subset = iterate_device_subsets(devices, state))) {
+        /* Skip this group if it's incomplete, by checking if we can add any
+         * other device. If we can, this iteration is a subset of another
+         * group that we already returned or eventually return. */
+        PA_IDXSET_FOREACH(dev, devices, idx) {
+            if (subset && !pa_idxset_contains(subset, dev) && devset_supports_device(subset, dev)) {
+                pa_idxset_free(subset, NULL);
+                subset = NULL;
+            }
         }
     }
 
@@ -1677,6 +1835,8 @@ static void alsa_mapping_add_ucm_device(pa_alsa_mapping *m, pa_alsa_ucm_device *
     else
         device->capture_mapping = m;
 
+    proplist_set_icon_name(m->proplist, device->type, is_sink);
+
     mdev = get_mixer_device(device, is_sink);
     if (mdev)
         pa_proplist_sets(m->proplist, "alsa.mixer_device", mdev);
@@ -1744,6 +1904,69 @@ static pa_alsa_mapping* ucm_alsa_mapping_get(pa_alsa_ucm_config *ucm, pa_alsa_pr
     return m;
 }
 
+static const struct {
+    enum snd_pcm_chmap_position pos;
+    pa_channel_position_t channel;
+} chmap_info[] = {
+    [SND_CHMAP_MONO] = { SND_CHMAP_MONO, PA_CHANNEL_POSITION_MONO },
+    [SND_CHMAP_FL] = { SND_CHMAP_FL, PA_CHANNEL_POSITION_FRONT_LEFT },
+    [SND_CHMAP_FR] = { SND_CHMAP_FR, PA_CHANNEL_POSITION_FRONT_RIGHT },
+    [SND_CHMAP_RL] = { SND_CHMAP_RL, PA_CHANNEL_POSITION_REAR_LEFT },
+    [SND_CHMAP_RR] = { SND_CHMAP_RR, PA_CHANNEL_POSITION_REAR_RIGHT },
+    [SND_CHMAP_FC] = { SND_CHMAP_FC, PA_CHANNEL_POSITION_FRONT_CENTER },
+    [SND_CHMAP_LFE] = { SND_CHMAP_LFE, PA_CHANNEL_POSITION_LFE },
+    [SND_CHMAP_SL] = { SND_CHMAP_SL, PA_CHANNEL_POSITION_SIDE_LEFT },
+    [SND_CHMAP_SR] = { SND_CHMAP_SR, PA_CHANNEL_POSITION_SIDE_RIGHT },
+    [SND_CHMAP_RC] = { SND_CHMAP_RC, PA_CHANNEL_POSITION_REAR_CENTER },
+    [SND_CHMAP_FLC] = { SND_CHMAP_FLC, PA_CHANNEL_POSITION_FRONT_LEFT_OF_CENTER },
+    [SND_CHMAP_FRC] = { SND_CHMAP_FRC, PA_CHANNEL_POSITION_FRONT_RIGHT_OF_CENTER },
+    /* XXX: missing channel positions, mapped to aux... */
+    /* [SND_CHMAP_RLC] = { SND_CHMAP_RLC, PA_CHANNEL_POSITION_REAR_LEFT_OF_CENTER }, */
+    /* [SND_CHMAP_RRC] = { SND_CHMAP_RRC, PA_CHANNEL_POSITION_REAR_RIGHT_OF_CENTER }, */
+    /* [SND_CHMAP_FLW] = { SND_CHMAP_FLW, PA_CHANNEL_POSITION_FRONT_LEFT_WIDE }, */
+    /* [SND_CHMAP_FRW] = { SND_CHMAP_FRW, PA_CHANNEL_POSITION_FRONT_RIGHT_WIDE }, */
+    /* [SND_CHMAP_FLH] = { SND_CHMAP_FLH, PA_CHANNEL_POSITION_FRONT_LEFT_HIGH }, */
+    /* [SND_CHMAP_FCH] = { SND_CHMAP_FCH, PA_CHANNEL_POSITION_FRONT_CENTER_HIGH }, */
+    /* [SND_CHMAP_FRH] = { SND_CHMAP_FRH, PA_CHANNEL_POSITION_FRONT_RIGHT_HIGH }, */
+    [SND_CHMAP_TC] = { SND_CHMAP_TC, PA_CHANNEL_POSITION_TOP_CENTER },
+    [SND_CHMAP_TFL] = { SND_CHMAP_TFL, PA_CHANNEL_POSITION_TOP_FRONT_LEFT },
+    [SND_CHMAP_TFR] = { SND_CHMAP_TFR, PA_CHANNEL_POSITION_TOP_FRONT_RIGHT },
+    [SND_CHMAP_TFC] = { SND_CHMAP_TFC, PA_CHANNEL_POSITION_TOP_FRONT_CENTER },
+    [SND_CHMAP_TRL] = { SND_CHMAP_TRL, PA_CHANNEL_POSITION_TOP_REAR_LEFT },
+    [SND_CHMAP_TRR] = { SND_CHMAP_TRR, PA_CHANNEL_POSITION_TOP_REAR_RIGHT },
+    [SND_CHMAP_TRC] = { SND_CHMAP_TRC, PA_CHANNEL_POSITION_TOP_REAR_CENTER },
+    /* [SND_CHMAP_TFLC] = { SND_CHMAP_TFLC, PA_CHANNEL_POSITION_TOP_FRONT_LEFT_OF_CENTER }, */
+    /* [SND_CHMAP_TFRC] = { SND_CHMAP_TFRC, PA_CHANNEL_POSITION_TOP_FRONT_RIGHT_OF_CENTER }, */
+    /* [SND_CHMAP_TSL] = { SND_CHMAP_TSL, PA_CHANNEL_POSITION_TOP_SIDE_LEFT }, */
+    /* [SND_CHMAP_TSR] = { SND_CHMAP_TSR, PA_CHANNEL_POSITION_TOP_SIDE_RIGHT }, */
+    /* [SND_CHMAP_LLFE] = { SND_CHMAP_LLFE, PA_CHANNEL_POSITION_LEFT_LFE }, */
+    /* [SND_CHMAP_RLFE] = { SND_CHMAP_RLFE, PA_CHANNEL_POSITION_RIGHT_LFE }, */
+    /* [SND_CHMAP_BC] = { SND_CHMAP_BC, PA_CHANNEL_POSITION_BOTTOM_CENTER }, */
+    /* [SND_CHMAP_BLC] = { SND_CHMAP_BLC, PA_CHANNEL_POSITION_BOTTOM_LEFT_OF_CENTER }, */
+    /* [SND_CHMAP_BRC] = { SND_CHMAP_BRC, PA_CHANNEL_POSITION_BOTTOM_RIGHT_OF_CENTER }, */
+};
+
+static void ucm_split_to_channel_map(pa_channel_map *m, const pa_alsa_ucm_split *s)
+{
+    const int n = sizeof(chmap_info) / sizeof(chmap_info[0]);
+    int i;
+    int aux = 0;
+
+    for (i = 0; i < s->channels; ++i) {
+        int p = s->pos[i];
+
+        if (p >= 0 && p < n && (int)chmap_info[p].pos == p)
+            m->map[i] = chmap_info[p].channel;
+        else
+            m->map[i] = PA_CHANNEL_POSITION_AUX0 + aux++;
+
+        if (aux >= 32)
+            break;
+    }
+
+    m->channels = i;
+}
+
 static int ucm_create_mapping_direction(
         pa_alsa_ucm_config *ucm,
         pa_alsa_profile_set *ps,
@@ -1788,6 +2011,14 @@ static int ucm_create_mapping_direction(
     if (channels < m->channel_map.channels)
         pa_channel_map_init_extend(&m->channel_map, channels, PA_CHANNEL_MAP_ALSA);
 
+    if (is_sink && device->playback_split) {
+        m->split = pa_xmemdup(device->playback_split, sizeof(*m->split));
+        ucm_split_to_channel_map(&m->channel_map, m->split);
+    } else if (!is_sink && device->capture_split) {
+        m->split = pa_xmemdup(device->capture_split, sizeof(*m->split));
+        ucm_split_to_channel_map(&m->channel_map, m->split);
+    }
+
     alsa_mapping_add_ucm_device(m, device);
 
     return 0;
@@ -2159,11 +2390,22 @@ static snd_pcm_t* mapping_open_pcm(pa_alsa_ucm_config *ucm, pa_alsa_mapping *m,
     snd_pcm_uframes_t try_period_size, try_buffer_size;
     bool exact_channels = m->channel_map.channels > 0;
 
-    if (exact_channels) {
-        try_map = m->channel_map;
-        try_ss.channels = try_map.channels;
-    } else
-        pa_channel_map_init_extend(&try_map, try_ss.channels, PA_CHANNEL_MAP_ALSA);
+    if (!m->split) {
+        if (exact_channels) {
+            try_map = m->channel_map;
+            try_ss.channels = try_map.channels;
+        } else
+            pa_channel_map_init_extend(&try_map, try_ss.channels, PA_CHANNEL_MAP_ALSA);
+    } else {
+        if (!m->split->leader) {
+            errno = EINVAL;
+            return NULL;
+        }
+
+        exact_channels = true;
+        try_ss.channels = m->split->hw_channels;
+        pa_channel_map_init_extend(&try_map, try_ss.channels, PA_CHANNEL_MAP_AUX);
+    }
 
     try_period_size =
         pa_usec_to_bytes(ucm->default_fragment_size_msec * PA_USEC_PER_MSEC, &try_ss) /
@@ -2182,6 +2424,32 @@ static snd_pcm_t* mapping_open_pcm(pa_alsa_ucm_config *ucm, pa_alsa_mapping *m,
     return pcm;
 }
 
+static void pa_alsa_init_proplist_split_pcm(pa_idxset *mappings, pa_alsa_mapping *leader, pa_direction_t direction)
+{
+    pa_proplist *props = pa_proplist_new();
+    uint32_t idx;
+    pa_alsa_mapping *m;
+
+    if (direction == PA_DIRECTION_OUTPUT)
+        pa_alsa_init_proplist_pcm(NULL, props, leader->output_pcm);
+    else
+        pa_alsa_init_proplist_pcm(NULL, props, leader->input_pcm);
+
+    PA_IDXSET_FOREACH(m, mappings, idx) {
+        if (!m->split)
+            continue;
+        if (!pa_streq(m->device_strings[0], leader->device_strings[0]))
+            continue;
+
+	if (direction == PA_DIRECTION_OUTPUT)
+	    pa_proplist_update(m->output_proplist, PA_UPDATE_REPLACE, props);
+        else
+            pa_proplist_update(m->input_proplist, PA_UPDATE_REPLACE, props);
+    }
+
+    pa_proplist_free(props);
+}
+
 static void profile_finalize_probing(pa_alsa_profile *p) {
     pa_alsa_mapping *m;
     uint32_t idx;
@@ -2193,7 +2461,11 @@ static void profile_finalize_probing(pa_alsa_profile *p) {
         if (!m->output_pcm)
             continue;
 
-        pa_alsa_init_proplist_pcm(NULL, m->output_proplist, m->output_pcm);
+        if (!m->split)
+            pa_alsa_init_proplist_pcm(NULL, m->output_proplist, m->output_pcm);
+        else
+            pa_alsa_init_proplist_split_pcm(p->output_mappings, m, PA_DIRECTION_OUTPUT);
+
         pa_alsa_close(&m->output_pcm);
     }
 
@@ -2204,7 +2476,11 @@ static void profile_finalize_probing(pa_alsa_profile *p) {
         if (!m->input_pcm)
             continue;
 
-        pa_alsa_init_proplist_pcm(NULL, m->input_proplist, m->input_pcm);
+        if (!m->split)
+            pa_alsa_init_proplist_pcm(NULL, m->input_proplist, m->input_pcm);
+        else
+            pa_alsa_init_proplist_split_pcm(p->input_mappings, m, PA_DIRECTION_INPUT);
+
         pa_alsa_close(&m->input_pcm);
     }
 }
@@ -2257,6 +2533,9 @@ static void ucm_probe_profile_set(pa_alsa_ucm_config *ucm, pa_alsa_profile_set *
                 continue;
             }
 
+            if (m->split && !m->split->leader)
+                continue;
+
             m->output_pcm = mapping_open_pcm(ucm, m, SND_PCM_STREAM_PLAYBACK);
             if (!m->output_pcm) {
                 p->supported = false;
@@ -2272,6 +2551,9 @@ static void ucm_probe_profile_set(pa_alsa_ucm_config *ucm, pa_alsa_profile_set *
                     continue;
                 }
 
+                if (m->split && !m->split->leader)
+                    continue;
+
                 m->input_pcm = mapping_open_pcm(ucm, m, SND_PCM_STREAM_CAPTURE);
                 if (!m->input_pcm) {
                     p->supported = false;
@@ -2361,6 +2643,9 @@ static void free_verb(pa_alsa_ucm_verb *verb) {
 
         pa_xfree(di->eld_mixer_device_name);
 
+        pa_xfree(di->playback_split);
+        pa_xfree(di->capture_split);
+
         pa_xfree(di);
     }
 
diff --git a/spa/plugins/alsa/acp/alsa-ucm.h b/spa/plugins/alsa/acp/alsa-ucm.h
index b03e7311..74dbe821 100644
--- a/spa/plugins/alsa/acp/alsa-ucm.h
+++ b/spa/plugins/alsa/acp/alsa-ucm.h
@@ -145,6 +145,7 @@ typedef struct pa_alsa_ucm_mapping_context pa_alsa_ucm_mapping_context;
 typedef struct pa_alsa_ucm_profile_context pa_alsa_ucm_profile_context;
 typedef struct pa_alsa_ucm_port_data pa_alsa_ucm_port_data;
 typedef struct pa_alsa_ucm_volume pa_alsa_ucm_volume;
+typedef struct pa_alsa_ucm_split pa_alsa_ucm_split;
 
 int pa_alsa_ucm_query_profiles(pa_alsa_ucm_config *ucm, int card_index);
 pa_alsa_profile_set* pa_alsa_ucm_add_profile_set(pa_alsa_ucm_config *ucm, pa_channel_map *default_channel_map);
@@ -177,6 +178,15 @@ void pa_alsa_ucm_roled_stream_end(pa_alsa_ucm_config *ucm, const char *role, pa_
 
 /* UCM - Use Case Manager is available on some audio cards */
 
+struct pa_alsa_ucm_split {
+    /* UCM SplitPCM channel remapping */
+    bool leader;
+    int hw_channels;
+    int channels;
+    int idx[PA_CHANNELS_MAX];
+    enum snd_pcm_chmap_position pos[PA_CHANNELS_MAX];
+};
+
 struct pa_alsa_ucm_device {
     PA_LLIST_FIELDS(pa_alsa_ucm_device);
 
@@ -215,6 +225,9 @@ struct pa_alsa_ucm_device {
 
     char *eld_mixer_device_name;
     int eld_device;
+
+    pa_alsa_ucm_split *playback_split;
+    pa_alsa_ucm_split *capture_split;
 };
 
 void pa_alsa_ucm_device_update_available(pa_alsa_ucm_device *device);
@@ -254,6 +267,7 @@ struct pa_alsa_ucm_config {
     pa_channel_map default_channel_map;
     unsigned default_fragment_size_msec;
     unsigned default_n_fragments;
+    bool split_enable;
 
     snd_use_case_mgr_t *ucm_mgr;
     pa_alsa_ucm_verb *active_verb;
diff --git a/spa/plugins/alsa/acp/alsa-util.c b/spa/plugins/alsa/acp/alsa-util.c
index 9c4638f1..96d6020c 100644
--- a/spa/plugins/alsa/acp/alsa-util.c
+++ b/spa/plugins/alsa/acp/alsa-util.c
@@ -26,6 +26,8 @@
 #include "alsa-util.h"
 #include "alsa-mixer.h"
 
+#include <spa/param/audio/format.h>
+
 #ifdef HAVE_UDEV
 #include <modules/udev-util.h>
 #endif
@@ -1972,7 +1974,7 @@ int pa_alsa_get_hdmi_eld(snd_hctl_elem_t *elem, pa_hdmi_eld *eld) {
     snd_ctl_elem_info_t *info;
     snd_ctl_elem_value_t *value;
     uint8_t *elddata;
-    unsigned int eldsize, mnl;
+    unsigned int eldsize, mnl, sad_count;
     unsigned int device;
 
     pa_assert(eld != NULL);
@@ -2010,5 +2012,54 @@ int pa_alsa_get_hdmi_eld(snd_hctl_elem_t *elem, pa_hdmi_eld *eld) {
     if (mnl)
         pa_log_debug("Monitor name in ELD info is '%s' (for device=%d)", eld->monitor_name, device);
 
+    /* Fetch Short Audio Descriptors */
+    sad_count = (elddata[5] & 0xf0) >> 4;
+    pa_log_debug("SAD count in ELD info is %u (for device=%d)", sad_count, device);
+    if (20 + mnl + 3 * sad_count > eldsize) {
+        pa_log_debug("Invalid SAD count (%u) in ELD info (for device=%d)", sad_count, device);
+        sad_count = 0;
+    }
+
+    eld->iec958_codecs = 0;
+    for (unsigned i = 0; i < sad_count; i++) {
+        uint8_t *sad = &elddata[20 + mnl + 3 * i];
+
+        /* https://en.wikipedia.org/wiki/Extended_Display_Identification_Data#Audio_Data_Blocks */
+        switch ((sad[0] & 0x78) >> 3) {
+            case 1:
+                eld->iec958_codecs |= 1ULL << SPA_AUDIO_IEC958_CODEC_PCM;
+                break;
+            case 2:
+                eld->iec958_codecs |= 1ULL << SPA_AUDIO_IEC958_CODEC_AC3;
+                break;
+            case 3:
+                eld->iec958_codecs |= 1ULL << SPA_AUDIO_IEC958_CODEC_MPEG;
+                break;
+            case 4:
+                eld->iec958_codecs |= 1ULL << SPA_AUDIO_IEC958_CODEC_MPEG;
+                break;
+            case 5:
+                eld->iec958_codecs |= 1ULL << SPA_AUDIO_IEC958_CODEC_MPEG;
+                break;
+            case 6:
+                eld->iec958_codecs |= 1ULL << SPA_AUDIO_IEC958_CODEC_MPEG2_AAC;
+                break;
+            case 7:
+                eld->iec958_codecs |= 1ULL << SPA_AUDIO_IEC958_CODEC_DTS;
+                break;
+            case 10:
+                eld->iec958_codecs |= 1ULL << SPA_AUDIO_IEC958_CODEC_EAC3;
+                break;
+            case 11:
+                eld->iec958_codecs |= 1ULL << SPA_AUDIO_IEC958_CODEC_DTSHD;
+                break;
+            case 12:
+                eld->iec958_codecs |= 1ULL << SPA_AUDIO_IEC958_CODEC_TRUEHD;
+                break;
+            default:
+                eld->iec958_codecs |= 1ULL << SPA_AUDIO_IEC958_CODEC_UNKNOWN;
+                break;
+        }
+    }
     return 0;
 }
diff --git a/spa/plugins/alsa/acp/alsa-util.h b/spa/plugins/alsa/acp/alsa-util.h
index e576dc9b..26c2698c 100644
--- a/spa/plugins/alsa/acp/alsa-util.h
+++ b/spa/plugins/alsa/acp/alsa-util.h
@@ -175,6 +175,7 @@ void pa_alsa_mixer_free(pa_alsa_mixer *mixer);
 typedef struct pa_hdmi_eld pa_hdmi_eld;
 struct pa_hdmi_eld {
     char monitor_name[17];
+    uint64_t iec958_codecs;
 };
 
 int pa_alsa_get_hdmi_eld(snd_hctl_elem_t *elem, pa_hdmi_eld *eld);
diff --git a/spa/plugins/alsa/acp/card.h b/spa/plugins/alsa/acp/card.h
index c58f89ab..c1126fe2 100644
--- a/spa/plugins/alsa/acp/card.h
+++ b/spa/plugins/alsa/acp/card.h
@@ -44,6 +44,7 @@ struct pa_card {
 
 	bool use_ucm;
 	bool soft_mixer;
+	bool disable_mixer_path;
 	bool auto_profile;
 	bool auto_port;
 	bool ignore_dB;
diff --git a/spa/plugins/alsa/acp/compat.h b/spa/plugins/alsa/acp/compat.h
index 356ef074..7660e1c2 100644
--- a/spa/plugins/alsa/acp/compat.h
+++ b/spa/plugins/alsa/acp/compat.h
@@ -414,6 +414,14 @@ static PA_PRINTF_FUNC(1,2) inline char *pa_sprintf_malloc(const char *fmt, ...)
 	return res;
 }
 
+static PA_PRINTF_FUNC(1,0) inline char *pa_vsprintf_malloc(const char *fmt, va_list args)
+{
+	char *res;
+	if (vasprintf(&res, fmt, args) < 0)
+		res = NULL;
+	return res;
+}
+
 #define pa_fopen_cloexec(f,m)	fopen(f,m"e")
 
 static inline char *pa_path_get_filename(const char *p)
diff --git a/spa/plugins/alsa/alsa-acp-device.c b/spa/plugins/alsa/alsa-acp-device.c
index ee8e6f15..5d46feed 100644
--- a/spa/plugins/alsa/alsa-acp-device.c
+++ b/spa/plugins/alsa/alsa-acp-device.c
@@ -157,6 +157,7 @@ static int emit_node(struct impl *this, struct acp_device *dev)
 	char device_name[128], path[210], channels[16], ch[12], routes[16];
 	char card_index[16], card_name[64], *p;
 	char positions[SPA_AUDIO_MAX_CHANNELS * 12];
+	char codecs[512];
 	struct spa_device_object_info info;
 	struct acp_card *card = this->card;
 	const char *stream, *card_id;
@@ -174,7 +175,7 @@ static int emit_node(struct impl *this, struct acp_device *dev)
 
 	info.change_mask = SPA_DEVICE_OBJECT_CHANGE_MASK_PROPS;
 
-	items = alloca((dev->props.n_items + 9) * sizeof(*items));
+	items = alloca((dev->props.n_items + 11) * sizeof(*items));
 	n_items = 0;
 
 	snprintf(card_index, sizeof(card_index), "%d", card->index);
@@ -191,6 +192,7 @@ static int emit_node(struct impl *this, struct acp_device *dev)
 	items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_ALSA_PCM_CARD, card_index);
 	items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_ALSA_PCM_STREAM, stream);
 	items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_PORT_GROUP, stream);
+	items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_ICON_NAME, "audio-card-analog");
 
 	snprintf(channels, sizeof(channels), "%d", dev->format.channels);
 	items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_AUDIO_CHANNELS, channels);
@@ -202,6 +204,11 @@ static int emit_node(struct impl *this, struct acp_device *dev)
 	}
 	items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_AUDIO_POSITION, positions);
 
+	if (dev->n_codecs > 0) {
+		acp_iec958_codecs_to_json(dev->codecs, dev->n_codecs, codecs, sizeof(codecs));
+		items[n_items++] = SPA_DICT_ITEM_INIT("iec958.codecs", codecs);
+	}
+
 	snprintf(routes, sizeof(routes), "%d", dev->n_ports);
 	items[n_items++] = SPA_DICT_ITEM_INIT("device.routes", routes);
 
@@ -754,6 +761,32 @@ static uint32_t find_route_by_name(struct acp_card *card, const char *name)
 	return SPA_ID_INVALID;
 }
 
+static bool check_active_profile_port(struct impl *this, uint32_t device, uint32_t port_index)
+{
+	struct acp_port *p;
+	uint32_t i;
+
+	if (port_index >= this->card->n_ports)
+		return false;
+	p = this->card->ports[port_index];
+
+	/* Port must be in active profile */
+	for (i = 0; i < p->n_profiles; i++)
+		if (p->profiles[i]->index == this->card->active_profile_index)
+			break;
+	if (i == p->n_profiles)
+		return false;
+
+	/* Port must correspond to the device */
+	for (i = 0; i< p->n_devices; i++)
+		if (p->devices[i]->index == device)
+			break;
+	if (i == p->n_devices)
+		return false;
+
+	return true;
+}
+
 static int impl_set_param(void *object,
 			  uint32_t id, uint32_t flags,
 			  const struct spa_pod *param)
@@ -831,6 +864,8 @@ static int impl_set_param(void *object,
 			idx = find_route_by_name(this->card, name);
 		if (idx == SPA_ID_INVALID)
 			return -EINVAL;
+		if (!check_active_profile_port(this, device, idx))
+			return -EINVAL;
 
 		acp_device_set_port(dev, idx, save ? ACP_PORT_SAVE : 0);
 		if (props)
diff --git a/spa/plugins/alsa/alsa-pcm-sink.c b/spa/plugins/alsa/alsa-pcm-sink.c
index 49e6ee55..6c340cc6 100644
--- a/spa/plugins/alsa/alsa-pcm-sink.c
+++ b/spa/plugins/alsa/alsa-pcm-sink.c
@@ -27,6 +27,7 @@ static void reset_props(struct props *props)
 {
 	strncpy(props->device, default_device, 64);
 	props->use_chmap = DEFAULT_USE_CHMAP;
+	spa_scnprintf(props->media_class, sizeof(props->media_class), "%s", "Audio/Sink");
 }
 
 static int impl_node_enum_params(void *object, int seq,
@@ -757,7 +758,8 @@ impl_node_port_set_io(void *object,
 		break;
 	case SPA_IO_RateMatch:
 		this->rate_match = data;
-		spa_alsa_update_rate_match(this);
+		if (this->rate_match)
+			spa_alsa_update_rate_match(this);
 		break;
 	default:
 		return -ENOENT;
diff --git a/spa/plugins/alsa/alsa-pcm-source.c b/spa/plugins/alsa/alsa-pcm-source.c
index 5ee9a4c9..a86e8aa0 100644
--- a/spa/plugins/alsa/alsa-pcm-source.c
+++ b/spa/plugins/alsa/alsa-pcm-source.c
@@ -28,6 +28,7 @@ static void reset_props(struct props *props)
 {
 	strncpy(props->device, default_device, 64);
 	props->use_chmap = DEFAULT_USE_CHMAP;
+	spa_scnprintf(props->media_class, sizeof(props->media_class), "%s", "Audio/Source");
 }
 
 static int impl_node_enum_params(void *object, int seq,
@@ -687,7 +688,8 @@ impl_node_port_set_io(void *object,
 		break;
 	case SPA_IO_RateMatch:
 		this->rate_match = data;
-		spa_alsa_update_rate_match(this);
+		if (this->rate_match)
+			spa_alsa_update_rate_match(this);
 		break;
 	default:
 		return -ENOENT;
diff --git a/spa/plugins/alsa/alsa-pcm.c b/spa/plugins/alsa/alsa-pcm.c
index b0789793..36834eae 100644
--- a/spa/plugins/alsa/alsa-pcm.c
+++ b/spa/plugins/alsa/alsa-pcm.c
@@ -33,13 +33,16 @@ static struct card *find_card(uint32_t index)
 	return NULL;
 }
 
-static struct card *ensure_card(uint32_t index, bool ucm)
+static struct card *ensure_card(uint32_t index, bool ucm, bool ucm_split)
 {
 	struct card *c;
-	char card_name[64];
+	char card_name[128];
 	const char *alibpref = NULL;
 	int err;
 
+	if (index == SPA_ID_INVALID)
+		return NULL;
+
 	if ((c = find_card(index)) != NULL)
 		return c;
 
@@ -48,7 +51,9 @@ static struct card *ensure_card(uint32_t index, bool ucm)
 	c->index = index;
 
 	if (ucm) {
-		snprintf(card_name, sizeof(card_name), "hw:%i", index);
+		const char *split_prefix = ucm_split ? "<<<SplitPCM=1>>>" : "";
+
+		snprintf(card_name, sizeof(card_name), "%shw:%i", split_prefix, index);
 		err = snd_use_case_mgr_open(&c->ucm, card_name);
 		if (err < 0) {
 			char *name;
@@ -56,7 +61,7 @@ static struct card *ensure_card(uint32_t index, bool ucm)
 			if (err < 0)
 				goto error;
 
-			snprintf(card_name, sizeof(card_name), "%s", name);
+			snprintf(card_name, sizeof(card_name), "%s%s", split_prefix, name);
 			free(name);
 
 			err = snd_use_case_mgr_open(&c->ucm, card_name);
@@ -78,6 +83,9 @@ error:
 
 static void release_card(struct card *c)
 {
+	if (!c)
+		return;
+
 	spa_assert(c->ref > 0);
 
 	if (--c->ref > 0)
@@ -91,6 +99,64 @@ static void release_card(struct card *c)
 	free(c);
 }
 
+#define CHECK(s,msg,...) if ((err = (s)) < 0) { spa_log_error(state->log, msg ": %s", ##__VA_ARGS__, snd_strerror(err)); return err; }
+
+static int write_bind_ctl_param(struct state *state, const char *name, const char *param) {
+	int err;
+	unsigned int count, idx;
+	char _name[1024];
+
+	for (unsigned int i = 0; i < state->num_bind_ctls; i++) {
+		snd_ctl_elem_info_t *info = state->bound_ctls[i].info;
+		bool changed = false;
+		int type;
+
+		if(!state->bound_ctls[i].value || !info)
+			continue;
+
+		snprintf(_name, sizeof(_name), "api.alsa.bind-ctl.%s",
+				snd_ctl_elem_info_get_name(info));
+
+		if (!spa_streq(name, _name))
+			continue;
+
+		type = snd_ctl_elem_info_get_type(info);
+		count = snd_ctl_elem_info_get_count(info);
+
+		switch (type) {
+		case SND_CTL_ELEM_TYPE_BOOLEAN: {
+				bool b = spa_atob(param);
+
+				for (idx = 0; idx < count; idx++)
+					snd_ctl_elem_value_set_boolean(state->bound_ctls[i].value, idx, b);
+				changed = true;
+			}
+			break;
+
+		case SND_CTL_ELEM_TYPE_INTEGER: {
+				long l = (long) atoi(param);
+
+				for (idx = 0; idx < count; idx++)
+					snd_ctl_elem_value_set_integer(state->bound_ctls[i].value, idx, l);
+				changed = true;
+			}
+			break;
+
+		default:
+			spa_log_warn(state->log, "%s ctl '%s' not supported",
+					snd_ctl_elem_type_name(snd_ctl_elem_info_get_type(info)),
+					snd_ctl_elem_info_get_name(info));
+			break;
+		}
+
+		if(changed)
+			CHECK(snd_ctl_elem_write(state->ctl, state->bound_ctls[i].value), "snd_ctl_elem_write");
+		return 0;
+	}
+
+	return 0;
+}
+
 static int alsa_set_param(struct state *state, const char *k, const char *s)
 {
 	int fmt_change = 0;
@@ -101,7 +167,7 @@ static int alsa_set_param(struct state *state, const char *k, const char *s)
 		state->default_rate = atoi(s);
 		fmt_change++;
 	} else if (spa_streq(k, SPA_KEY_AUDIO_FORMAT)) {
-		state->default_format = spa_alsa_format_from_name(s, strlen(s));
+		state->default_format = spa_type_audio_format_from_short_name(s);
 		fmt_change++;
 	} else if (spa_streq(k, SPA_KEY_AUDIO_POSITION)) {
 		spa_alsa_parse_position(&state->default_pos, s, strlen(s));
@@ -144,6 +210,13 @@ static int alsa_set_param(struct state *state, const char *k, const char *s)
 	} else if (spa_streq(k, "clock.name")) {
 		spa_scnprintf(state->clock_name,
 				sizeof(state->clock_name), "%s", s);
+	} else if (spa_strstartswith(k,  "api.alsa.bind-ctl.")) {
+		write_bind_ctl_param(state, k, s);
+		fmt_change++;
+	} else if (spa_streq(k, SPA_KEY_MEDIA_CLASS)) {
+		spa_scnprintf(state->props.media_class, sizeof(state->props.media_class), "%s", s);
+	} else if (spa_streq(k, "api.alsa.split.parent")) {
+		state->is_split_parent = true;
 	} else
 		return 0;
 
@@ -628,7 +701,6 @@ int spa_alsa_parse_prop_params(struct state *state, struct spa_pod *params)
 	return changed;
 }
 
-#define CHECK(s,msg,...) if ((err = (s)) < 0) { spa_log_error(state->log, msg ": %s", ##__VA_ARGS__, snd_strerror(err)); return err; }
 
 static ssize_t log_write(void *cookie, const char *buf, size_t size)
 {
@@ -657,7 +729,7 @@ static void silence_error_handler(const char *file, int line,
 static void fill_device_name(struct state *state, const char *params, char device_name[], size_t len)
 {
 	spa_scnprintf(device_name, len, "%s%s%s",
-			state->card->ucm_prefix ? state->card->ucm_prefix : "",
+			state->card && state->card->ucm_prefix ? state->card->ucm_prefix : "",
 			state->props.device, params ? params : "");
 }
 
@@ -911,16 +983,15 @@ int spa_alsa_init(struct state *state, const struct spa_dict *info)
 		} else if (spa_streq(k, "clock.quantum-limit")) {
 			spa_atou32(s, &state->quantum_limit, 0);
 		} else if (spa_streq(k, SPA_KEY_API_ALSA_BIND_CTLS)) {
-			struct spa_json it[2];
+			struct spa_json it[1];
 			char v[256];
 			unsigned int i = 0;
 
 			/* Read a list of ALSA control names to bind as params */
-			spa_json_init(&it[0], s, strlen(s));
-			if (spa_json_enter_array(&it[0], &it[1]) <= 0)
-				spa_json_init(&it[1], s, strlen(s));
+			if (spa_json_begin_array_relax(&it[0], s, strlen(s)) <= 0)
+				continue;
 
-			while (spa_json_get_string(&it[1], v, sizeof(v)) > 0 &&
+			while (spa_json_get_string(&it[0], v, sizeof(v)) > 0 &&
 					i < SPA_N_ELEMENTS(state->bound_ctls)) {
 				snprintf(state->bound_ctls[i].name,
 						sizeof(state->bound_ctls[i].name), "%s", v);
@@ -938,13 +1009,12 @@ int spa_alsa_init(struct state *state, const struct spa_dict *info)
 		/* If we don't have a card index, see if we have a *:<idx> string */
 		sscanf(state->props.device, "%*[^:]:%u", &state->card_index);
 		if (state->card_index == SPA_ID_INVALID) {
-			spa_log_error(state->log, "Could not determine card index, maybe set %s",
-					SPA_KEY_API_ALSA_CARD);
-			return -EINVAL;
+			spa_log_info(state->log, "Could not determine card index. %s and/or clock.name "
+					"may need to be configured manually", SPA_KEY_API_ALSA_PCM_CARD);
 		}
 	}
 
-	if (state->clock_name[0] == '\0')
+	if (state->clock_name[0] == '\0' && state->card_index != SPA_ID_INVALID)
 		snprintf(state->clock_name, sizeof(state->clock_name),
 				"api.alsa.%s-%u",
 				state->stream == SND_PCM_STREAM_PLAYBACK ? "p" : "c",
@@ -956,11 +1026,8 @@ int spa_alsa_init(struct state *state, const struct spa_dict *info)
 		state->iec958_codecs |= 1ULL << SPA_AUDIO_IEC958_CODEC_PCM;
 	}
 
-	state->card = ensure_card(state->card_index, state->open_ucm);
-	if (state->card == NULL) {
-		spa_log_error(state->log, "can't create card %u", state->card_index);
-		return -errno;
-	}
+	state->card = ensure_card(state->card_index, state->open_ucm, state->is_split_parent);
+
 	state->log_file = fopencookie(state, "w", io_funcs);
 	if (state->log_file == NULL) {
 		spa_log_error(state->log, "can't create log file");
@@ -1203,7 +1270,7 @@ int spa_alsa_close(struct state *state)
 	else
 		state->n_fds = 0;
 
-	if (state->have_format)
+	if (state->have_format && state->card)
 		state->card->format_ref--;
 
 	state->have_format = false;
@@ -1423,7 +1490,7 @@ static int add_rate(struct state *state, uint32_t scale, uint32_t interleave, bo
 	if (max < min)
 		return 0;
 
-	if (!state->multi_rate && state->card->format_ref > 0)
+	if (!state->multi_rate && state->card && state->card->format_ref > 0)
 		rate = state->card->rate;
 	else
 		rate = state->default_rate;
@@ -1439,8 +1506,8 @@ static int add_rate(struct state *state, uint32_t scale, uint32_t interleave, bo
 
 	rate = SPA_CLAMP(rate, min, max);
 
-	spa_log_debug(state->log, "rate:%u multi:%d card:%d def:%d",
-			rate, state->multi_rate, state->card->rate, state->default_rate);
+	spa_log_debug(state->log, "rate:%u multi:%d card:%u def:%d",
+			rate, state->multi_rate, state->card ? state->card->rate : 0, state->default_rate);
 
 	spa_pod_builder_prop(b, SPA_FORMAT_AUDIO_rate, 0);
 
@@ -2014,7 +2081,7 @@ int spa_alsa_set_format(struct state *state, struct spa_audio_info *fmt, uint32_
 		unsigned aes3;
 
 		spa_log_info(state->log, "using IEC958 Codec:%s rate:%d",
-				spa_debug_type_find_short_name(spa_type_audio_iec958_codec, f->codec),
+				spa_type_audio_iec958_codec_to_short_name(f->codec),
 				f->rate);
 
 		rformat = SND_PCM_FORMAT_S16_LE;
@@ -2172,6 +2239,7 @@ int spa_alsa_set_format(struct state *state, struct spa_audio_info *fmt, uint32_
 	}
 
 	if (!state->multi_rate &&
+	    state->card &&
 	    state->card->format_ref > 0 &&
 	    state->card->rate != rrate) {
 		spa_log_error(state->log, "%p: card already opened at rate:%i",
@@ -2217,7 +2285,7 @@ int spa_alsa_set_format(struct state *state, struct spa_audio_info *fmt, uint32_
 	state->driver_rate.denom = 0;
 
 	state->have_format = true;
-	if (state->card->format_ref++ == 0)
+	if (state->card && state->card->format_ref++ == 0)
 		state->card->rate = rrate;
 
 	dir = 0;
@@ -2716,7 +2784,7 @@ static int get_status(struct state *state, uint64_t current_time, snd_pcm_uframe
 static int update_time(struct state *state, uint64_t current_time, snd_pcm_sframes_t delay,
 		snd_pcm_sframes_t target, bool follower)
 {
-	double err, corr;
+	double err, corr, avg;
 	int32_t diff;
 
 	if (state->disable_tsched && !follower) {
@@ -2754,22 +2822,36 @@ static int update_time(struct state *state, uint64_t current_time, snd_pcm_sfram
 			err = -state->max_error;
 	}
 
-	if (!follower || state->matching)
+	if (!follower || state->matching) {
 		corr = spa_dll_update(&state->dll, err);
-	else
+
+		avg = (state->err_avg * state->err_wdw + (err - state->err_avg)) / (state->err_wdw + 1.0);
+		state->err_var = (state->err_var * state->err_wdw +
+				(err - state->err_avg) * (err - avg)) / (state->err_wdw + 1.0);
+		state->err_avg = avg;
+	} else {
 		corr = 1.0;
+	}
 
 	if (diff < 0)
 		state->next_time += (uint64_t)(diff / corr * 1e9 / state->rate);
 
 	if (SPA_UNLIKELY((state->next_time - state->base_time) > BW_PERIOD)) {
+		double bw;
+
 		state->base_time = state->next_time;
 
+		bw = (fabs(state->err_avg) + sqrt(fabs(state->err_var)))/1000.0;
+
 		spa_log_debug(state->log, "%s: follower:%d match:%d rate:%f "
-				"bw:%f thr:%u del:%ld target:%ld err:%f max:%f",
+				"bw:%f thr:%u del:%ld target:%ld err:%f max_err:%f max_resync: %f var:%f:%f:%f",
 				state->name, follower, state->matching,
 				corr, state->dll.bw, state->threshold, delay, target,
-				err, state->max_error);
+				err, state->max_error, state->max_resync, state->err_avg, state->err_var, bw);
+
+		spa_dll_set_bw(&state->dll,
+				SPA_CLAMPD(bw, 0.001, SPA_DLL_BW_MAX),
+				state->threshold, state->rate);
 	}
 
 	if (state->rate_match) {
@@ -2786,7 +2868,7 @@ static int update_time(struct state *state, uint64_t current_time, snd_pcm_sfram
 
 	state->next_time += (uint64_t)(state->threshold / corr * 1e9 / state->rate);
 
-	if (SPA_LIKELY(!follower && state->clock)) {
+	if (SPA_LIKELY(state->clock)) {
 		state->clock->nsec = current_time;
 		state->clock->rate = state->driver_rate;
 		state->clock->position += state->clock->duration;
@@ -2872,8 +2954,9 @@ static inline int check_position_config(struct state *state, bool starting)
 		state->driver_duration = target_duration;
 		state->driver_rate = target_rate;
 		state->threshold = SPA_SCALE32_UP(state->driver_duration, state->rate, state->driver_rate.denom);
-		state->max_error = SPA_MAX(256.0f, state->threshold / 2.0f);
-		state->max_resync = SPA_MIN(state->threshold, state->max_error);
+		state->max_error = SPA_MAX(256.0f, (state->threshold + state->headroom) / 2.0f);
+		state->max_resync = SPA_MIN(state->threshold + state->headroom, state->max_error);
+		state->err_wdw = (double)state->driver_rate.denom/state->driver_duration;
 		state->resample = !state->pitch_elem &&
 			(((uint32_t)state->rate != state->driver_rate.denom) || state->matching);
 		state->alsa_sync = true;
@@ -3024,10 +3107,14 @@ again:
 
 	if (state->use_mmap && written > 0) {
 		if (SPA_UNLIKELY((commitres = snd_pcm_mmap_commit(hndl, offset, written)) < 0)) {
-			spa_log_error(state->log, "%s: snd_pcm_mmap_commit error: %s",
-					state->name, snd_strerror(commitres));
-			if (commitres != -EPIPE && commitres != -ESTRPIPE)
+			if (commitres == -EPIPE || commitres == -ESTRPIPE) {
+				spa_log_warn(state->log, "%s: snd_pcm_mmap_commit error: %s",
+						state->name, snd_strerror(commitres));
+			} else {
+				spa_log_error(state->log, "%s: snd_pcm_mmap_commit error: %s",
+						state->name, snd_strerror(commitres));
 				return res;
+			}
 		}
 		if (commitres > 0 && written != (snd_pcm_uframes_t) commitres) {
 			spa_log_warn(state->log, "%s: mmap_commit wrote %ld instead of %ld",
@@ -3617,7 +3704,7 @@ int spa_alsa_start(struct state *state)
 		}
 
 		/* We only add the source to the data loop if we're driving.
-		 * This is done in setup_sources() */
+		 * This is done in add_sources() */
 		for (int i = 0; i < state->n_fds; i++) {
 			state->source[i].func = alsa_irq_wakeup_event;
 			state->source[i].data = state;
@@ -3744,8 +3831,7 @@ void spa_alsa_emit_node_info(struct state *state, bool full)
 		char latency[64] = "", period[64] = "", nperiods[64] = "", headroom[64] = "";
 
 		items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_API, "alsa");
-		items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_MEDIA_CLASS,
-				state->stream == SND_PCM_STREAM_PLAYBACK ? "Audio/Sink" : "Audio/Source");
+		items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_MEDIA_CLASS, state->props.media_class);
 		items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_NODE_DRIVER, "true");
 
 		if (state->have_format)
diff --git a/spa/plugins/alsa/alsa-pcm.h b/spa/plugins/alsa/alsa-pcm.h
index 3dc1ec9d..bd77abc2 100644
--- a/spa/plugins/alsa/alsa-pcm.h
+++ b/spa/plugins/alsa/alsa-pcm.h
@@ -30,6 +30,7 @@ extern "C" {
 #include <spa/param/param.h>
 #include <spa/param/latency-utils.h>
 #include <spa/param/audio/format-utils.h>
+#include <spa/param/audio/raw-json.h>
 #include <spa/param/tag-utils.h>
 
 #include "alsa.h"
@@ -49,6 +50,7 @@ struct props {
 	char device[64];
 	char device_name[128];
 	char card_name[128];
+	char media_class[128];
 	bool use_chmap;
 };
 
@@ -152,6 +154,7 @@ struct state {
 	unsigned int disable_mmap:1;
 	unsigned int disable_batch:1;
 	unsigned int disable_tsched:1;
+	unsigned int is_split_parent:1;
 	char clock_name[64];
 	uint32_t quantum_limit;
 
@@ -243,6 +246,7 @@ struct state {
 	struct spa_dll dll;
 	double max_error;
 	double max_resync;
+	double err_avg, err_var, err_wdw;
 
 	struct spa_latency_info latency[2];
 	struct spa_process_latency_info process_latency;
@@ -303,79 +307,31 @@ void spa_alsa_recycle_buffer(struct state *state, uint32_t buffer_id);
 void spa_alsa_emit_node_info(struct state *state, bool full);
 void spa_alsa_emit_port_info(struct state *state, bool full);
 
-static inline uint32_t spa_alsa_format_from_name(const char *name, size_t len)
-{
-	int i;
-	for (i = 0; spa_type_audio_format[i].name; i++) {
-		if (strncmp(name, spa_debug_type_short_name(spa_type_audio_format[i].name), len) == 0)
-			return spa_type_audio_format[i].type;
-	}
-	return SPA_AUDIO_FORMAT_UNKNOWN;
-}
-
-static inline uint32_t spa_alsa_channel_from_name(const char *name)
-{
-	int i;
-	for (i = 0; spa_type_audio_channel[i].name; i++) {
-		if (strcmp(name, spa_debug_type_short_name(spa_type_audio_channel[i].name)) == 0)
-			return spa_type_audio_channel[i].type;
-	}
-	return SPA_AUDIO_CHANNEL_UNKNOWN;
-}
-
 static inline void spa_alsa_parse_position(struct channel_map *map, const char *val, size_t len)
 {
-	struct spa_json it[2];
-	char v[256];
-
-	spa_json_init(&it[0], val, len);
-        if (spa_json_enter_array(&it[0], &it[1]) <= 0)
-                spa_json_init(&it[1], val, len);
-
-	map->channels = 0;
-	while (spa_json_get_string(&it[1], v, sizeof(v)) > 0 &&
-	    map->channels < SPA_AUDIO_MAX_CHANNELS) {
-		map->pos[map->channels++] = spa_alsa_channel_from_name(v);
-	}
+	spa_audio_parse_position(val, len, map->pos, &map->channels);
 }
 
 static inline uint32_t spa_alsa_parse_rates(uint32_t *rates, uint32_t max, const char *val, size_t len)
 {
-	struct spa_json it[2];
-	char v[256];
-	uint32_t count;
-
-	spa_json_init(&it[0], val, len);
-        if (spa_json_enter_array(&it[0], &it[1]) <= 0)
-                spa_json_init(&it[1], val, len);
-
-	count = 0;
-	while (spa_json_get_string(&it[1], v, sizeof(v)) > 0 && count < max)
-		rates[count++] = atoi(v);
-	return count;
+	return spa_json_str_array_uint32(val, len, rates, max);
 }
 
 static inline uint32_t spa_alsa_iec958_codec_from_name(const char *name)
 {
-	int i;
-	for (i = 0; spa_type_audio_iec958_codec[i].name; i++) {
-		if (strcmp(name, spa_debug_type_short_name(spa_type_audio_iec958_codec[i].name)) == 0)
-			return spa_type_audio_iec958_codec[i].type;
-	}
-	return SPA_AUDIO_IEC958_CODEC_UNKNOWN;
+	return spa_type_audio_iec958_codec_from_short_name(name);
 }
 
 static inline void spa_alsa_parse_iec958_codecs(uint64_t *codecs, const char *val, size_t len)
 {
-	struct spa_json it[2];
+	struct spa_json it[1];
 	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);
+        if (spa_json_begin_array_relax(&it[0], val, len) <= 0)
+		return;
 
 	*codecs = 0;
-	while (spa_json_get_string(&it[1], v, sizeof(v)) > 0)
+	while (spa_json_get_string(&it[0], v, sizeof(v)) > 0)
 		*codecs |= 1ULL << spa_alsa_iec958_codec_from_name(v);
 }
 
diff --git a/spa/plugins/alsa/alsa-seq-bridge.c b/spa/plugins/alsa/alsa-seq-bridge.c
index 3592fbd7..f85b41e2 100644
--- a/spa/plugins/alsa/alsa-seq-bridge.c
+++ b/spa/plugins/alsa/alsa-seq-bridge.c
@@ -17,18 +17,20 @@
 #include <spa/monitor/device.h>
 #include <spa/param/audio/format.h>
 #include <spa/param/latency-utils.h>
+#include <spa/control/control.h>
 #include <spa/pod/filter.h>
 
 #include "alsa-seq.h"
 
 #define DEFAULT_DEVICE		"default"
 #define DEFAULT_CLOCK_NAME	"clock.system.monotonic"
+#define DEFAULT_DISABLE_LONGNAME true
 
 static void reset_props(struct props *props)
 {
 	strncpy(props->device, DEFAULT_DEVICE, sizeof(props->device));
 	strncpy(props->clock_name, DEFAULT_CLOCK_NAME, sizeof(props->clock_name));
-	props->disable_longname = 0;
+	props->disable_longname = DEFAULT_DISABLE_LONGNAME;
 }
 
 static int impl_node_enum_params(void *object, int seq,
@@ -227,9 +229,11 @@ static void emit_port_info(struct seq_state *this, struct seq_port *port, bool f
 	if (port->info.change_mask) {
 		struct spa_dict_item items[6];
 		uint32_t n_items = 0;
-		int id;
+		int card_id;
 		snd_seq_port_info_t *info;
 		snd_seq_client_info_t *client_info;
+		const char *client_name, *port_name, *dir, *pn;
+		char prefix[32] = "";
 		char card[8];
 		char name[256];
 		char path[128];
@@ -244,59 +248,40 @@ static void emit_port_info(struct seq_state *this, struct seq_port *port, bool f
 		snd_seq_get_any_client_info(this->sys.hndl,
 				port->addr.client, client_info);
 
-		int card_id;
+		card_id = snd_seq_client_info_get_card(client_info);
+		client_name = snd_seq_client_info_get_name(client_info);
+		port_name = snd_seq_port_info_get_name(info);
+		dir = port->direction == SPA_DIRECTION_OUTPUT ? "capture" : "playback";
 
-		// Failed to obtain card number (software device) or disabled
-		if (this->props.disable_longname || (card_id = snd_seq_client_info_get_card(client_info)) < 0) {
-			snprintf(name, sizeof(name), "%s:(%s_%d) %s",
-					snd_seq_client_info_get_name(client_info),
-					port->direction == SPA_DIRECTION_OUTPUT ? "capture" : "playback",
-					port->addr.port,
-					snd_seq_port_info_get_name(info));
-		} else {
-			char *longname;
-			if (snd_card_get_longname(card_id, &longname) == 0) {
-				snprintf(name, sizeof(name), "%s:(%s_%d) %s",
-						longname,
-						port->direction == SPA_DIRECTION_OUTPUT ? "capture" : "playback",
-						port->addr.port,
-						snd_seq_port_info_get_name(info));
-				free(longname);
-			} else {
-				// At least add card number to be distinct
-				snprintf(name, sizeof(name), "%s %d:(%s_%d) %s",
-						snd_seq_client_info_get_name(client_info),
-						card_id,
-						port->direction == SPA_DIRECTION_OUTPUT ? "capture" : "playback",
-						port->addr.port,
-						snd_seq_port_info_get_name(info));
-			}
-		}
+		if (!this->props.disable_longname)
+			snprintf(prefix, sizeof(prefix), "[%d:%d] ",
+					port->addr.client, port->addr.port);
+
+		pn = port_name;
+		if (spa_strstartswith(pn, client_name))
+			pn += strlen(client_name);
+
+		snprintf(name, sizeof(name), "%s%s%s (%s)", prefix,
+				client_name, pn, dir);
 		clean_name(name);
 
 		snprintf(stream, sizeof(stream), "client_%d", port->addr.client);
 		clean_name(stream);
 
 		snprintf(path, sizeof(path), "alsa:seq:%s:%s:%s_%d",
-				this->props.device,
-				stream,
-				port->direction == SPA_DIRECTION_OUTPUT ? "capture" : "playback",
-				port->addr.port);
+				this->props.device, stream, dir, port->addr.port);
 		clean_name(path);
 
-		snprintf(alias, sizeof(alias), "%s:%s",
-				snd_seq_client_info_get_name(client_info),
-				snd_seq_port_info_get_name(info));
+		snprintf(alias, sizeof(alias), "%s:%s", client_name, port_name);
 		clean_name(alias);
 
-
-		items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_FORMAT_DSP, "8 bit raw midi");
+		items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_FORMAT_DSP, "32 bit raw UMP");
 		items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_OBJECT_PATH, path);
 		items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_PORT_NAME, name);
 		items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_PORT_ALIAS, alias);
 		items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_PORT_GROUP, stream);
-		if ((id = snd_seq_client_info_get_card(client_info)) != -1) {
-			snprintf(card, sizeof(card), "%d", id);
+		if (card_id != -1) {
+			snprintf(card, sizeof(card), "%d", card_id);
 			items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_ALSA_CARD, card);
 		}
 		port->info.props = &SPA_DICT_INIT(items, n_items);
@@ -544,7 +529,8 @@ impl_node_port_enum_params(void *object, int seq,
 		param = spa_pod_builder_add_object(&b,
 			SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat,
 			SPA_FORMAT_mediaType,      SPA_POD_Id(SPA_MEDIA_TYPE_application),
-			SPA_FORMAT_mediaSubtype,   SPA_POD_Id(SPA_MEDIA_SUBTYPE_control));
+			SPA_FORMAT_mediaSubtype,   SPA_POD_Id(SPA_MEDIA_SUBTYPE_control),
+			SPA_FORMAT_CONTROL_types,  SPA_POD_CHOICE_FLAGS_Int(1u<<SPA_CONTROL_UMP));
 		break;
 
 	case SPA_PARAM_Format:
@@ -555,7 +541,8 @@ impl_node_port_enum_params(void *object, int seq,
 		param = spa_pod_builder_add_object(&b,
 			SPA_TYPE_OBJECT_Format, SPA_PARAM_Format,
 			SPA_FORMAT_mediaType,      SPA_POD_Id(SPA_MEDIA_TYPE_application),
-			SPA_FORMAT_mediaSubtype,   SPA_POD_Id(SPA_MEDIA_SUBTYPE_control));
+			SPA_FORMAT_mediaSubtype,   SPA_POD_Id(SPA_MEDIA_SUBTYPE_control),
+			SPA_FORMAT_CONTROL_types,  SPA_POD_Int(1u<<SPA_CONTROL_UMP));
 		break;
 
 	case SPA_PARAM_Buffers:
@@ -648,6 +635,7 @@ static int port_set_format(void *object, struct seq_port *port,
 		port->have_format = false;
 	} else {
 		struct spa_audio_info info = { 0 };
+		uint32_t types;
 
 		if ((err = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0)
 			return err;
@@ -656,6 +644,13 @@ static int port_set_format(void *object, struct seq_port *port,
 		    info.media_subtype != SPA_MEDIA_SUBTYPE_control)
 			return -EINVAL;
 
+		if ((err = spa_pod_parse_object(format,
+				SPA_TYPE_OBJECT_Format, NULL,
+				SPA_FORMAT_CONTROL_types,  SPA_POD_Int(&types))) < 0)
+			return err;
+		if (types != 1u << SPA_CONTROL_UMP)
+			return -EINVAL;
+
 		port->current_format = info;
 		port->have_format = true;
 	}
diff --git a/spa/plugins/alsa/alsa-seq.c b/spa/plugins/alsa/alsa-seq.c
index 95947dfc..2a4ebd2c 100644
--- a/spa/plugins/alsa/alsa-seq.c
+++ b/spa/plugins/alsa/alsa-seq.c
@@ -16,6 +16,7 @@
 #include <spa/pod/filter.h>
 #include <spa/support/system.h>
 #include <spa/control/control.h>
+#include <spa/control/ump-utils.h>
 
 #include "alsa.h"
 
@@ -33,9 +34,16 @@ static int seq_open(struct seq_state *state, struct seq_conn *conn, bool with_qu
 	if ((res = snd_seq_open(&conn->hndl,
 			   props->device,
 			   SND_SEQ_OPEN_DUPLEX,
-			   0)) < 0) {
+			   0)) < 0)
+		return res;
+
+	if ((res = snd_seq_set_client_midi_version(conn->hndl, SND_SEQ_CLIENT_UMP_MIDI_2_0)) < 0) {
+		snd_seq_close(conn->hndl);
+		spa_log_info(state->log, "%p: ALSA failed to enable UMP MIDI: %s",
+				state, snd_strerror(res));
 		return res;
 	}
+
 	return 0;
 }
 
@@ -164,7 +172,7 @@ static void init_ports(struct seq_state *state)
 	}
 }
 
-static void debug_event(struct seq_state *state, snd_seq_event_t *ev)
+static void debug_event(struct seq_state *state, snd_seq_ump_event_t *ev)
 {
 	if (SPA_LIKELY(!spa_log_level_topic_enabled(state->log, SPA_LOG_TOPIC_DEFAULT, SPA_LOG_LEVEL_TRACE)))
 		return;
@@ -191,10 +199,10 @@ static void debug_event(struct seq_state *state, snd_seq_event_t *ev)
 static void alsa_seq_on_sys(struct spa_source *source)
 {
 	struct seq_state *state = source->data;
-	snd_seq_event_t *ev;
+	snd_seq_ump_event_t *ev;
 	int res;
 
-	while (snd_seq_event_input(state->sys.hndl, &ev) > 0) {
+	while (snd_seq_ump_event_input(state->sys.hndl, &ev) > 0) {
 		const snd_seq_addr_t *addr = &ev->data.addr;
 
 		if (addr->client == state->event.addr.client)
@@ -240,7 +248,6 @@ static void alsa_seq_on_sys(struct spa_source *source)
 			break;
 
 		}
-		snd_seq_free_event(ev);
         }
 }
 
@@ -522,15 +529,15 @@ static int process_recycle(struct seq_state *state)
 
 static int process_read(struct seq_state *state)
 {
-	snd_seq_event_t *ev;
+	snd_seq_ump_event_t *ev;
 	struct seq_stream *stream = &state->streams[SPA_DIRECTION_OUTPUT];
 	uint32_t i;
+	uint32_t *data;
 	long size;
-	uint8_t data[MAX_EVENT_SIZE];
 	int res;
 
 	/* copy all new midi events into their port buffers */
-	while ((res = snd_seq_event_input(state->event.hndl, &ev)) > 0) {
+	while ((res = snd_seq_ump_event_input(state->event.hndl, &ev)) > 0) {
 		const snd_seq_addr_t *addr = &ev->source;
 		struct seq_port *port;
 		uint64_t ev_time, diff;
@@ -552,11 +559,8 @@ static int process_read(struct seq_state *state)
 			continue;
 		}
 
-		snd_midi_event_reset_decode(stream->codec);
-		if ((size = snd_midi_event_decode(stream->codec, data, MAX_EVENT_SIZE, ev)) < 0) {
-			spa_log_warn(state->log, "decode failed: %s", snd_strerror(size));
-			continue;
-		}
+		data = (uint32_t*)&ev->ump[0];
+		size = spa_ump_message_size(snd_ump_msg_hdr_type(ev->ump[0])) * 4;
 
 		/* queue_time is the estimated current time of the queue as calculated by
 		 * the DLL. Calculate the age of the event. */
@@ -576,11 +580,9 @@ static int process_read(struct seq_state *state)
 		spa_log_trace_fp(state->log, "event %d time:%"PRIu64" offset:%d size:%ld port:%d.%d",
 				ev->type, ev_time, offset, size, addr->client, addr->port);
 
-		spa_pod_builder_control(&port->builder, offset, SPA_CONTROL_Midi);
+		spa_pod_builder_control(&port->builder, offset, SPA_CONTROL_UMP);
 		spa_pod_builder_bytes(&port->builder, data, size);
 
-		snd_seq_free_event(ev);
-
 		/* make sure we can fit at least one control event of max size otherwise
 		 * we keep the event in the queue and try to copy it in the next cycle */
 		if (port->builder.state.offset +
@@ -659,10 +661,9 @@ static int process_write(struct seq_state *state)
 		struct spa_pod_sequence *pod;
 		struct spa_data *d;
 		struct spa_pod_control *c;
-		snd_seq_event_t ev;
+		snd_seq_ump_event_t ev;
 		uint64_t out_time;
 		snd_seq_real_time_t out_rt;
-		long size = 0;
 
 		if (!port->valid || io == NULL)
 			continue;
@@ -686,53 +687,33 @@ static int process_write(struct seq_state *state)
 		}
 
 		SPA_POD_SEQUENCE_FOREACH(pod, c) {
-			long s, body_size;
+			size_t body_size;
 			uint8_t *body;
 
-			if (c->type != SPA_CONTROL_Midi)
+			if (c->type != SPA_CONTROL_UMP)
 				continue;
 
 			body = SPA_POD_BODY(&c->value);
 			body_size = SPA_POD_BODY_SIZE(&c->value);
+			spa_zero(ev);
+
+			memcpy(ev.ump, body, SPA_MIN(sizeof(ev.ump), (size_t)body_size));
+
+			snd_seq_ev_set_source(&ev, state->event.addr.port);
+			snd_seq_ev_set_dest(&ev, port->addr.client, port->addr.port);
+
+			out_time = state->queue_time + NSEC_FROM_CLOCK(&state->rate, c->offset);
+
+			out_rt.tv_nsec = out_time % SPA_NSEC_PER_SEC;
+			out_rt.tv_sec = out_time / SPA_NSEC_PER_SEC;
+			snd_seq_ev_schedule_real(&ev, state->event.queue_id, 0, &out_rt);
+
+			spa_log_trace_fp(state->log, "event %d time:%"PRIu64" offset:%d size:%zd port:%d.%d",
+				ev.type, out_time, c->offset, body_size, port->addr.client, port->addr.port);
 
-			while (body_size > 0) {
-				if (size == 0)
-					/* only reset when we start decoding a new message */
-					snd_seq_ev_clear(&ev);
-
-				if ((s = snd_midi_event_encode(stream->codec,
-							body, body_size, &ev)) < 0) {
-					spa_log_warn(state->log, "failed to encode event: %s",
-							snd_strerror(s));
-					snd_midi_event_reset_encode(stream->codec);
-					size = 0;
-					break;
-				}
-				body += s;
-				body_size -= s;
-				size += s;
-				if (ev.type == SND_SEQ_EVENT_NONE)
-					/* this can happen when the event is not complete yet, like
-					 * a sysex message and we need to encode some more data. */
-					break;
-
-				snd_seq_ev_set_source(&ev, state->event.addr.port);
-				snd_seq_ev_set_dest(&ev, port->addr.client, port->addr.port);
-
-				out_time = state->queue_time + NSEC_FROM_CLOCK(&state->rate, c->offset);
-
-				out_rt.tv_nsec = out_time % SPA_NSEC_PER_SEC;
-				out_rt.tv_sec = out_time / SPA_NSEC_PER_SEC;
-				snd_seq_ev_schedule_real(&ev, state->event.queue_id, 0, &out_rt);
-
-				spa_log_trace_fp(state->log, "event %d time:%"PRIu64" offset:%d size:%ld port:%d.%d",
-					ev.type, out_time, c->offset, size, port->addr.client, port->addr.port);
-
-				if ((err = snd_seq_event_output(state->event.hndl, &ev)) < 0) {
-					spa_log_warn(state->log, "failed to output event: %s",
-							snd_strerror(err));
-				}
-				size = 0;
+			if ((err = snd_seq_ump_event_output(state->event.hndl, &ev)) < 0) {
+				spa_log_warn(state->log, "failed to output event: %s",
+						snd_strerror(err));
 			}
 		}
 	}
@@ -809,7 +790,7 @@ static int update_time(struct seq_state *state, uint64_t nsec, bool follower)
 	}
 	state->next_time += (uint64_t)(state->threshold / corr * 1e9 / state->rate.denom);
 
-	if (!follower && state->clock) {
+	if (SPA_LIKELY(state->clock)) {
 		state->clock->nsec = nsec;
 		state->clock->rate = state->rate;
 		state->clock->position += state->clock->duration;
diff --git a/spa/plugins/alsa/alsa-seq.h b/spa/plugins/alsa/alsa-seq.h
index 23f71d63..0f2c192c 100644
--- a/spa/plugins/alsa/alsa-seq.h
+++ b/spa/plugins/alsa/alsa-seq.h
@@ -13,6 +13,7 @@ extern "C" {
 #include <math.h>
 
 #include <alsa/asoundlib.h>
+#include <alsa/ump_msg.h>
 
 #include <spa/support/plugin.h>
 #include <spa/support/loop.h>
diff --git a/spa/plugins/alsa/mixer/profile-sets/hdmi-ac3.conf b/spa/plugins/alsa/mixer/profile-sets/hdmi-ac3.conf
new file mode 100644
index 00000000..ea6cc9e7
--- /dev/null
+++ b/spa/plugins/alsa/mixer/profile-sets/hdmi-ac3.conf
@@ -0,0 +1,110 @@
+# 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/>.
+
+; Profile set with HDMI/AC3 profiles.
+;
+; You can use udev rules to enable these, for example:
+;
+; ATTRS{subsystem_vendor}=="0x1849", ATTRS{subsystem_device}=="0xaaf0", ENV{ACP_PROFILE_SET}="hdmi-ac3.conf"
+
+.include default.conf
+
+[Mapping hdmi-ac3-surround]
+description = Digital Surround 5.1 (HDMI/AC3)
+device-strings = plug:{SLAVE="a52:%f,'hw:%f,3'"}
+paths-output = hdmi-output-0
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+priority = 1
+direction = output
+
+[Mapping hdmi-ac3-surround-extra1]
+description = Digital Surround 5.1 (HDMI 2/AC3)
+device-strings = plug:{SLAVE="a52:%f,'hw:%f,7'"}
+paths-output = hdmi-output-1
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+priority = 1
+direction = output
+
+[Mapping hdmi-ac3-surround-extra2]
+description = Digital Surround 5.1 (HDMI 3/AC3)
+device-strings = plug:{SLAVE="a52:%f,'hw:%f,8'"}
+paths-output = hdmi-output-2
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+priority = 1
+direction = output
+
+[Mapping hdmi-ac3-surround-extra3]
+description = Digital Surround 5.1 (HDMI 4/AC3)
+device-strings = plug:{SLAVE="a52:%f,'hw:%f,9'"}
+paths-output = hdmi-output-3
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+priority = 1
+direction = output
+
+[Mapping hdmi-ac3-surround-extra4]
+description = Digital Surround 5.1 (HDMI 5/AC3)
+device-strings = plug:{SLAVE="a52:%f,'hw:%f,10'"}
+paths-output = hdmi-output-4
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+priority = 1
+direction = output
+
+[Mapping hdmi-ac3-surround-extra5]
+description = Digital Surround 5.1 (HDMI 6/AC3)
+device-strings = plug:{SLAVE="a52:%f,'hw:%f,11'"}
+paths-output = hdmi-output-5
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+priority = 1
+direction = output
+
+[Mapping hdmi-ac3-surround-extra6]
+description = Digital Surround 5.1 (HDMI 7/AC3)
+device-strings = plug:{SLAVE="a52:%f,'hw:%f,12'"}
+paths-output = hdmi-output-6
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+priority = 1
+direction = output
+
+[Mapping hdmi-ac3-surround-extra7]
+description = Digital Surround 5.1 (HDMI 8/AC3)
+device-strings = plug:{SLAVE="a52:%f,'hw:%f,13'"}
+paths-output = hdmi-output-7
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+priority = 1
+direction = output
+
+[Mapping hdmi-ac3-surround-extra8]
+description = Digital Surround 5.1 (HDMI 9/AC3)
+device-strings = plug:{SLAVE="a52:%f,'hw:%f,14'"}
+paths-output = hdmi-output-8
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+priority = 1
+direction = output
+
+[Mapping hdmi-ac3-surround-extra9]
+description = Digital Surround 5.1 (HDMI 10/AC3)
+device-strings = plug:{SLAVE="a52:%f,'hw:%f,15'"}
+paths-output = hdmi-output-9
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+priority = 1
+direction = output
+
+[Mapping hdmi-ac3-surround-extra10]
+description = Digital Surround 5.1 (HDMI 11/AC3)
+device-strings = plug:{SLAVE="a52:%f,'hw:%f,16'"}
+paths-output = hdmi-output-10
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+priority = 1
+direction = output
diff --git a/spa/plugins/alsa/mixer/samples/USB Device 0x46d:0x9a4--USB Mixer b/spa/plugins/alsa/mixer/samples/USB Device 0x46d_0x9a4--USB Mixer
similarity index 100%
rename from spa/plugins/alsa/mixer/samples/USB Device 0x46d:0x9a4--USB Mixer
rename to spa/plugins/alsa/mixer/samples/USB Device 0x46d_0x9a4--USB Mixer
diff --git a/spa/plugins/audioconvert/audioadapter.c b/spa/plugins/audioconvert/audioadapter.c
index 5e91f36f..6e393bd0 100644
--- a/spa/plugins/audioconvert/audioadapter.c
+++ b/spa/plugins/audioconvert/audioadapter.c
@@ -3,6 +3,7 @@
 /* SPDX-License-Identifier: MIT */
 
 #include <spa/support/plugin.h>
+#include <spa/support/plugin-loader.h>
 #include <spa/support/log.h>
 #include <spa/support/cpu.h>
 
@@ -43,6 +44,7 @@ struct impl {
 
 	struct spa_log *log;
 	struct spa_cpu *cpu;
+	struct spa_plugin_loader *ploader;
 
 	uint32_t max_align;
 	enum spa_direction direction;
@@ -57,9 +59,11 @@ struct impl {
 	int in_set_param;
 
 	struct spa_handle *hnd_convert;
+	bool unload_handle;
 	struct spa_node *convert;
 	struct spa_hook convert_listener;
-	uint64_t convert_flags;
+	uint64_t convert_port_flags;
+	char *convertname;
 
 	uint32_t n_buffers;
 	struct spa_buffer **buffers;
@@ -94,16 +98,42 @@ struct impl {
 	unsigned int started:1;
 	unsigned int ready:1;
 	unsigned int async:1;
-	unsigned int passthrough:1;
+	enum spa_param_port_config_mode mode;
 	unsigned int follower_removing:1;
 	unsigned int in_recalc;
 
 	unsigned int warned:1;
 	unsigned int driver:1;
+
+	int in_enum_sync;
 };
 
 /** \endcond */
 
+static int node_enum_params_sync(struct impl *impl, struct spa_node *node,
+		uint32_t id, uint32_t *index, const struct spa_pod *filter,
+		struct spa_pod **param, struct spa_pod_builder *builder)
+{
+	int res;
+	impl->in_enum_sync++;
+	res = spa_node_enum_params_sync(node, id, index, filter, param, builder);
+	impl->in_enum_sync--;
+	return res;
+}
+
+static int node_port_enum_params_sync(struct impl *impl, struct spa_node *node,
+		enum spa_direction direction, uint32_t port_id,
+		uint32_t id, uint32_t *index, const struct spa_pod *filter,
+		struct spa_pod **param, struct spa_pod_builder *builder)
+{
+	int res;
+	impl->in_enum_sync++;
+	res = spa_node_port_enum_params_sync(node, direction, port_id, id, index,
+			filter, param, builder);
+	impl->in_enum_sync--;
+	return res;
+}
+
 static int follower_enum_params(struct impl *this,
 				 uint32_t id,
 				 uint32_t idx,
@@ -112,15 +142,16 @@ static int follower_enum_params(struct impl *this,
 				 struct spa_pod_builder *builder)
 {
 	int res;
-	if (result->next < 0x100000) {
-		if ((res = spa_node_enum_params_sync(this->convert,
+	if (result->next < 0x100000 &&
+	    this->follower != this->target) {
+		if ((res = node_enum_params_sync(this, this->target,
 				id, &result->next, filter, &result->param, builder)) == 1)
 			return res;
 		result->next = 0x100000;
 	}
 	if (result->next < 0x200000 && this->follower_params_flags[idx] & SPA_PARAM_INFO_READ) {
 		result->next &= 0xfffff;
-		if ((res = spa_node_enum_params_sync(this->follower,
+		if ((res = node_enum_params_sync(this, this->follower,
 				id, &result->next, filter, &result->param, builder)) == 1) {
 			result->next |= 0x100000;
 			return res;
@@ -137,6 +168,9 @@ static int convert_enum_port_config(struct impl *this,
 	struct spa_pod *f1, *f2 = NULL;
 	int res;
 
+	if (this->convert == NULL)
+		return 0;
+
 	f1 = spa_pod_builder_add_object(builder,
 		SPA_TYPE_OBJECT_ParamPortConfig, id,
 			SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(this->direction));
@@ -180,9 +214,8 @@ next:
 
 	switch (id) {
 	case SPA_PARAM_EnumPortConfig:
-		return convert_enum_port_config(this, seq, id, start, num, filter, &b.b);
 	case SPA_PARAM_PortConfig:
-		if (this->passthrough) {
+		if (this->mode == SPA_PARAM_PORT_CONFIG_MODE_passthrough) {
 			switch (result.index) {
 			case 0:
 				result.param = spa_pod_builder_add_object(&b.b,
@@ -216,7 +249,7 @@ next:
 	case SPA_PARAM_Format:
 	case SPA_PARAM_Latency:
 	case SPA_PARAM_Tag:
-		res = spa_node_port_enum_params_sync(this->follower,
+		res = node_port_enum_params_sync(this, this->follower,
 				this->direction, 0,
 				id, &result.next, filter, &result.param, &b.b);
 		break;
@@ -261,14 +294,15 @@ static int link_io(struct impl *this)
 		spa_log_debug(this->log, "%p: set RateMatch on follower disabled %d %s", this,
 			res, spa_strerror(res));
 	}
-	else if ((res = spa_node_port_set_io(this->convert,
-			SPA_DIRECTION_REVERSE(this->direction), 0,
-			SPA_IO_RateMatch,
-			rate_match, rate_match_size)) < 0) {
-		spa_log_warn(this->log, "%p: set RateMatch on convert failed %d %s", this,
-			res, spa_strerror(res));
+	else if (this->follower != this->target) {
+		if ((res = spa_node_port_set_io(this->target,
+				SPA_DIRECTION_REVERSE(this->direction), 0,
+				SPA_IO_RateMatch,
+				rate_match, rate_match_size)) < 0) {
+			spa_log_warn(this->log, "%p: set RateMatch on target failed %d %s", this,
+				res, spa_strerror(res));
+		}
 	}
-
 	return 0;
 }
 
@@ -291,7 +325,7 @@ static int activate_io(struct impl *this, bool active)
 			res, spa_strerror(res));
 		return res;
 	}
-	else if ((res = spa_node_port_set_io(this->convert,
+	else if ((res = spa_node_port_set_io(this->target,
 			SPA_DIRECTION_REVERSE(this->direction), 0,
 			SPA_IO_Buffers, data, size)) < 0) {
 		spa_log_warn(this->log, "%p: set Buffers on convert failed %d %s", this,
@@ -366,7 +400,7 @@ static int debug_params(struct impl *this, struct spa_node *node,
 	state = 0;
 	while (true) {
 		spa_pod_builder_init(&b, buffer, sizeof(buffer));
-		res = spa_node_port_enum_params_sync(node,
+		res = node_port_enum_params_sync(this, node,
 					direction, port_id,
 					id, &state,
 					NULL, &param, &b);
@@ -400,12 +434,15 @@ static int negotiate_buffers(struct impl *this)
 
 	spa_log_debug(this->log, "%p: n_buffers:%d", this, this->n_buffers);
 
+	if (this->follower == this->target)
+		return 0;
+
 	if (this->n_buffers > 0)
 		return 0;
 
 	state = 0;
 	param = NULL;
-	if ((res = spa_node_port_enum_params_sync(this->follower,
+	if ((res = node_port_enum_params_sync(this, this->follower,
 				this->direction, 0,
 				SPA_PARAM_Buffers, &state,
 				param, &param, &b)) < 0) {
@@ -419,11 +456,11 @@ static int negotiate_buffers(struct impl *this)
 	}
 
 	state = 0;
-	if ((res = spa_node_port_enum_params_sync(this->convert,
+	if ((res = node_port_enum_params_sync(this, this->target,
 				SPA_DIRECTION_REVERSE(this->direction), 0,
 				SPA_PARAM_Buffers, &state,
 				param, &param, &b)) != 1) {
-		debug_params(this, this->convert,
+		debug_params(this, this->target,
 				SPA_DIRECTION_REVERSE(this->direction), 0,
 				SPA_PARAM_Buffers, param, "convert buffers", res);
 		return -ENOTSUP;
@@ -433,8 +470,8 @@ static int negotiate_buffers(struct impl *this)
 
 	spa_pod_fixate(param);
 
-	follower_flags = this->follower_flags;
-	conv_flags = this->convert_flags;
+	follower_flags = this->follower_port_flags;
+	conv_flags = this->convert_port_flags;
 
 	follower_alloc = SPA_FLAG_IS_SET(follower_flags, SPA_PORT_FLAG_CAN_ALLOC_BUFFERS);
 	conv_alloc = SPA_FLAG_IS_SET(conv_flags, SPA_PORT_FLAG_CAN_ALLOC_BUFFERS);
@@ -481,7 +518,7 @@ static int negotiate_buffers(struct impl *this)
 		return -errno;
 	this->n_buffers = buffers;
 
-	if ((res = spa_node_port_use_buffers(this->convert,
+	if ((res = spa_node_port_use_buffers(this->target,
 		       SPA_DIRECTION_REVERSE(this->direction), 0,
 		       conv_alloc ? SPA_NODE_BUFFERS_FLAG_ALLOC : 0,
 		       this->buffers, this->n_buffers)) < 0)
@@ -512,10 +549,6 @@ static int configure_format(struct impl *this, uint32_t flags, const struct spa_
 
 	spa_log_debug(this->log, "%p: configure format:", this);
 
-	if (format == NULL && !this->have_format)
-		return 0;
-
-
 	if (format == NULL) {
 		if (!this->have_format)
 			return 0;
@@ -538,7 +571,7 @@ static int configure_format(struct impl *this, uint32_t flags, const struct spa_
 
 		/* format was changed to nearest compatible format */
 
-		if ((res = spa_node_port_enum_params_sync(this->follower,
+		if ((res = node_port_enum_params_sync(this, this->follower,
 					this->direction, 0,
 					SPA_PARAM_Format, &state,
 					NULL, &fmt, &b)) != 1)
@@ -548,7 +581,7 @@ static int configure_format(struct impl *this, uint32_t flags, const struct spa_
 	}
 
 	if (this->target != this->follower) {
-		if ((res = spa_node_port_set_param(this->convert,
+		if ((res = spa_node_port_set_param(this->target,
 					   SPA_DIRECTION_REVERSE(this->direction), 0,
 					   SPA_PARAM_Format, flags,
 					   format)) < 0)
@@ -558,7 +591,7 @@ static int configure_format(struct impl *this, uint32_t flags, const struct spa_
 	this->have_format = format != NULL;
 	clear_buffers(this);
 
-	if (format != NULL && this->target != this->follower)
+	if (format != NULL)
 		res = negotiate_buffers(this);
 
 	return res;
@@ -570,6 +603,9 @@ static int configure_convert(struct impl *this, uint32_t mode)
 	uint8_t buffer[1024];
 	struct spa_pod *param;
 
+	if (this->convert == NULL)
+		return 0;
+
 	spa_pod_builder_init(&b, buffer, sizeof(buffer));
 
 	spa_log_debug(this->log, "%p: configure convert %p", this, this->target);
@@ -603,7 +639,7 @@ static int recalc_latency(struct impl *this, struct spa_node *src, enum spa_dire
 
 	while (true) {
 		spa_pod_builder_init(&b, buffer, sizeof(buffer));
-		if ((res = spa_node_port_enum_params_sync(src,
+		if ((res = node_port_enum_params_sync(this, src,
 						direction, port_id, SPA_PARAM_Latency,
 						&index, NULL, &param, &b)) != 1) {
 			param = NULL;
@@ -644,7 +680,7 @@ static int recalc_tag(struct impl *this, struct spa_node *src, enum spa_directio
 	while (true) {
 		void *tag_state = NULL;
 		spa_pod_builder_reset(&b.b, &state);
-		if ((res = spa_node_port_enum_params_sync(src,
+		if ((res = node_port_enum_params_sync(this, src,
 						direction, port_id, SPA_PARAM_Tag,
 						&index, NULL, &param, &b.b)) != 1) {
 			param = NULL;
@@ -660,15 +696,20 @@ static int recalc_tag(struct impl *this, struct spa_node *src, enum spa_directio
 }
 
 
-static int reconfigure_mode(struct impl *this, bool passthrough,
+static int reconfigure_mode(struct impl *this, enum spa_param_port_config_mode mode,
                 enum spa_direction direction, struct spa_pod *format)
 {
 	int res = 0;
 	struct spa_hook l;
+	bool passthrough = mode == SPA_PARAM_PORT_CONFIG_MODE_passthrough;
+	bool old_passthrough = this->mode == SPA_PARAM_PORT_CONFIG_MODE_passthrough;
 
 	spa_log_debug(this->log, "%p: passthrough mode %d", this, passthrough);
 
-	if (this->passthrough != passthrough) {
+	if (!passthrough && this->convert == NULL)
+		return -ENOTSUP;
+
+	if (old_passthrough != passthrough) {
 		if (passthrough) {
 			/* remove converter split/merge ports */
 			configure_convert(this, SPA_PARAM_PORT_CONFIG_MODE_none);
@@ -688,8 +729,9 @@ static int reconfigure_mode(struct impl *this, bool passthrough,
 	if ((res = configure_format(this, SPA_NODE_PARAM_FLAG_NEAREST, format)) < 0)
 		return res;
 
-	if (this->passthrough != passthrough) {
-		this->passthrough = passthrough;
+	this->mode = mode;
+
+	if (old_passthrough != passthrough) {
 		if (passthrough) {
 			/* add follower ports */
 			spa_zero(l);
@@ -697,7 +739,7 @@ static int reconfigure_mode(struct impl *this, bool passthrough,
 			spa_hook_remove(&l);
 		} else {
 			/* add converter ports */
-			configure_convert(this, SPA_PARAM_PORT_CONFIG_MODE_dsp);
+			configure_convert(this, mode);
 		}
 		link_io(this);
 	}
@@ -730,12 +772,9 @@ static int impl_node_set_param(void *object, uint32_t id, uint32_t flags,
 		if (param == NULL)
 			return -EINVAL;
 
-		if ((res = spa_format_parse(param, &info.media_type, &info.media_subtype)) < 0)
-			return res;
-		if (info.media_type != SPA_MEDIA_TYPE_audio ||
-			info.media_subtype != SPA_MEDIA_SUBTYPE_raw)
-				return -EINVAL;
-		if (spa_format_audio_raw_parse(param, &info.info.raw) < 0)
+		if (spa_format_audio_parse(param, &info) < 0)
+			return -EINVAL;
+		if (info.media_subtype != SPA_MEDIA_SUBTYPE_raw)
 			return -EINVAL;
 
 		this->follower_current_format = info;
@@ -763,28 +802,27 @@ static int impl_node_set_param(void *object, uint32_t id, uint32_t flags,
 			struct spa_audio_info info;
 
 			spa_zero(info);
-			if ((res = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0)
+			if ((res = spa_format_audio_parse(format, &info)) < 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) {
+			if (info.media_subtype == SPA_MEDIA_SUBTYPE_raw)
 				info.info.raw.rate = 0;
-				this->default_format = info;
-			}
+			else
+				return -ENOTSUP;
+
+			this->default_format = info;
 		}
 
 		switch (mode) {
 		case SPA_PARAM_PORT_CONFIG_MODE_none:
 			return -ENOTSUP;
 		case SPA_PARAM_PORT_CONFIG_MODE_passthrough:
-			if ((res = reconfigure_mode(this, true, dir, format)) < 0)
+			if ((res = reconfigure_mode(this, mode, dir, format)) < 0)
 				return res;
 			break;
 		case SPA_PARAM_PORT_CONFIG_MODE_convert:
 		case SPA_PARAM_PORT_CONFIG_MODE_dsp:
-			if ((res = reconfigure_mode(this, false, dir, NULL)) < 0)
+			if ((res = reconfigure_mode(this, mode, dir, NULL)) < 0)
 				return res;
 			break;
 		default:
@@ -795,7 +833,7 @@ static int impl_node_set_param(void *object, uint32_t id, uint32_t flags,
 			if ((res = spa_node_set_param(this->target, id, flags, param)) < 0)
 				return res;
 
-			res = recalc_latency(this, this->follower, this->direction, 0, this->convert);
+			res = recalc_latency(this, this->follower, this->direction, 0, this->target);
 		}
 		break;
 	}
@@ -881,15 +919,18 @@ static struct spa_pod *merge_objects(struct impl *this, struct spa_pod_builder *
 
 static int negotiate_format(struct impl *this)
 {
-	uint32_t state;
+	uint32_t fstate, tstate;
 	struct spa_pod *format, *def;
 	uint8_t buffer[4096];
 	struct spa_pod_builder b = { 0 };
-	int res;
+	int res, fres;
 
 	spa_log_debug(this->log, "%p: have_format:%d recheck:%d", this, this->have_format,
 			this->recheck_format);
 
+	if (this->target == this->follower)
+		return 0;
+
 	if (this->have_format && !this->recheck_format)
 		return 0;
 
@@ -900,38 +941,58 @@ static int negotiate_format(struct impl *this)
 	spa_node_send_command(this->follower,
 			&SPA_NODE_COMMAND_INIT(SPA_NODE_COMMAND_ParamBegin));
 
-	state = 0;
-	format = NULL;
-	if ((res = spa_node_port_enum_params_sync(this->follower,
-				this->direction, 0,
-				SPA_PARAM_EnumFormat, &state,
-				format, &format, &b)) < 0) {
+	/* first try the ideal converter format, which is likely passthrough */
+	tstate = 0;
+	fres = node_port_enum_params_sync(this, this->target,
+				SPA_DIRECTION_REVERSE(this->direction), 0,
+				SPA_PARAM_EnumFormat, &tstate,
+				NULL, &format, &b);
+	if (fres == 1) {
+		fstate = 0;
+		res = node_port_enum_params_sync(this, this->follower,
+					this->direction, 0,
+					SPA_PARAM_EnumFormat, &fstate,
+					format, &format, &b);
+		if (res == 1)
+			goto found;
+	}
+
+	/* then try something the follower can accept */
+	for (fstate = 0;;) {
+		format = NULL;
+		res = node_port_enum_params_sync(this, this->follower,
+					this->direction, 0,
+					SPA_PARAM_EnumFormat, &fstate,
+					NULL, &format, &b);
+
 		if (res == -ENOENT)
 			format = NULL;
-		else {
-			debug_params(this, this->follower, this->direction, 0,
-					SPA_PARAM_EnumFormat, format, "follower format", res);
-			goto done;
-		}
+		else if (res <= 0)
+			break;
+
+		tstate = 0;
+		fres = node_port_enum_params_sync(this, this->target,
+					SPA_DIRECTION_REVERSE(this->direction), 0,
+					SPA_PARAM_EnumFormat, &tstate,
+					format, &format, &b);
+		if (fres == 0 && res == 1)
+			continue;
+
+		res = fres;
+		break;
 	}
-	state = 0;
-	if ((res = spa_node_port_enum_params_sync(this->convert,
-				SPA_DIRECTION_REVERSE(this->direction), 0,
-				SPA_PARAM_EnumFormat, &state,
-				format, &format, &b)) != 1) {
-		debug_params(this, this->convert,
+found:
+	if (format == NULL) {
+		debug_params(this, this->follower, this->direction, 0,
+				SPA_PARAM_EnumFormat, format, "follower format", res);
+		debug_params(this, this->target,
 				SPA_DIRECTION_REVERSE(this->direction), 0,
 				SPA_PARAM_EnumFormat, format, "convert format", res);
 		res = -ENOTSUP;
 		goto done;
 	}
-	if (format == NULL) {
-		res = -ENOTSUP;
-		goto done;
-	}
-
-	def = spa_format_audio_raw_build(&b,
-			SPA_PARAM_Format, &this->default_format.info.raw);
+	def = spa_format_audio_build(&b,
+			SPA_PARAM_Format, &this->default_format);
 
 	format = merge_objects(this, &b, SPA_PARAM_Format,
 			(struct spa_pod_object*)format,
@@ -961,12 +1022,10 @@ 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->target != this->follower) {
-			if (this->started)
-				return 0;
-			if ((res = negotiate_format(this)) < 0)
-				return res;
-		}
+		if (this->started)
+			return 0;
+		if ((res = negotiate_format(this)) < 0)
+			return res;
 		this->ready = true;
 		this->warned = false;
 		break;
@@ -1091,6 +1150,8 @@ static void follower_convert_port_info(void *data,
 			this->direction == SPA_DIRECTION_INPUT ?
 				"Input" : "Output", info, info->change_mask);
 
+	this->convert_port_flags = info->flags;
+
 	if (info->change_mask & SPA_PORT_CHANGE_MASK_PARAMS) {
 		for (i = 0; i < info->n_params; i++) {
 			uint32_t idx;
@@ -1117,14 +1178,14 @@ static void follower_convert_port_info(void *data,
 
 			if (idx == IDX_Latency) {
 				this->in_recalc++;
-				res = recalc_latency(this, this->convert, direction, port_id, this->follower);
+				res = recalc_latency(this, this->target, direction, port_id, this->follower);
 				this->in_recalc--;
 				spa_log_debug(this->log, "latency: %d (%s)", res,
 						spa_strerror(res));
 			}
 			if (idx == IDX_Tag) {
 				this->in_recalc++;
-				res = recalc_tag(this, this->convert, direction, port_id, this->follower);
+				res = recalc_tag(this, this->target, direction, port_id, this->follower);
 				this->in_recalc--;
 				spa_log_debug(this->log, "tag: %d (%s)", res,
 						spa_strerror(res));
@@ -1151,7 +1212,10 @@ static void convert_port_info(void *data,
 			port_id--;
 	} else if (info) {
 		pi = *info;
-		pi.flags = this->follower_port_flags;
+		pi.flags = this->follower_port_flags &
+			(SPA_PORT_FLAG_LIVE |
+			 SPA_PORT_FLAG_PHYSICAL |
+			 SPA_PORT_FLAG_TERMINAL);
 		info = &pi;
 	}
 
@@ -1166,7 +1230,7 @@ static void convert_result(void *data, int seq, int res, uint32_t type, const vo
 {
 	struct impl *this = data;
 
-	if (this->target == this->follower)
+	if (this->target == this->follower || this->in_enum_sync)
 		return;
 
 	spa_log_trace(this->log, "%p: result %d %d", this, seq, res);
@@ -1195,7 +1259,7 @@ static void follower_info(void *data, const struct spa_node_info *info)
 
 	if (info->max_input_ports > 0)
 		this->direction = SPA_DIRECTION_INPUT;
-        else
+	else
 		this->direction = SPA_DIRECTION_OUTPUT;
 
 	if (this->direction == SPA_DIRECTION_INPUT) {
@@ -1272,10 +1336,7 @@ static void follower_port_info(void *data,
 	      return;
 	}
 
-	this->follower_port_flags = info->flags &
-		(SPA_PORT_FLAG_LIVE |
-		 SPA_PORT_FLAG_PHYSICAL |
-		 SPA_PORT_FLAG_TERMINAL);
+	this->follower_port_flags = info->flags;
 
 	spa_log_debug(this->log, "%p: follower port info %s %p %08"PRIx64" recalc:%u", this,
 			this->direction == SPA_DIRECTION_INPUT ?
@@ -1346,7 +1407,7 @@ static void follower_result(void *data, int seq, int res, uint32_t type, const v
 {
 	struct impl *this = data;
 
-	if (this->target != this->follower)
+	if (this->target != this->follower || this->in_enum_sync)
 		return;
 
 	spa_log_trace(this->log, "%p: result %d %d", this, seq, res);
@@ -1379,6 +1440,20 @@ static const struct spa_node_events follower_node_events = {
 	.event = follower_event,
 };
 
+static void follower_probe_info(void *data, const struct spa_node_info *info)
+{
+	struct impl *this = data;
+	if (info->max_input_ports > 0)
+		this->direction = SPA_DIRECTION_INPUT;
+        else
+		this->direction = SPA_DIRECTION_OUTPUT;
+}
+
+static const struct spa_node_events follower_probe_events = {
+	SPA_VERSION_NODE_EVENTS,
+	.info = follower_probe_info,
+};
+
 static int follower_ready(void *data, int status)
 {
 	struct impl *this = data;
@@ -1396,7 +1471,7 @@ static int follower_ready(void *data, int status)
 		if (this->direction == SPA_DIRECTION_OUTPUT) {
 			int retry = MAX_RETRY;
 			while (retry--) {
-				status = spa_node_process_fast(this->convert);
+				status = spa_node_process_fast(this->target);
 				if (status & SPA_STATUS_HAVE_DATA)
 					break;
 
@@ -1419,7 +1494,7 @@ static int follower_reuse_buffer(void *data, uint32_t port_id, uint32_t buffer_i
 	struct impl *this = data;
 
 	if (this->target != this->follower)
-		res = spa_node_port_reuse_buffer(this->convert, port_id, buffer_id);
+		res = spa_node_port_reuse_buffer(this->target, port_id, buffer_id);
 	else
 		res = spa_node_call_reuse_buffer(&this->callbacks, port_id, buffer_id);
 
@@ -1461,10 +1536,11 @@ static int impl_node_add_listener(void *object,
 		spa_node_add_listener(this->follower, &l, &follower_node_events, this);
 		spa_hook_remove(&l);
 
-		spa_zero(l);
-		spa_node_add_listener(this->convert, &l, &convert_node_events, this);
-		spa_hook_remove(&l);
-
+		if (this->follower != this->target) {
+			spa_zero(l);
+			spa_node_add_listener(this->target, &l, &convert_node_events, this);
+			spa_hook_remove(&l);
+		}
 		this->add_listener = false;
 
 		emit_node_info(this, true);
@@ -1525,6 +1601,55 @@ impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_
 	return spa_node_remove_port(this->target, direction, port_id);
 }
 
+static int
+port_enum_formats_for_convert(struct impl *this, int seq, enum spa_direction direction,
+		uint32_t port_id, uint32_t id, uint32_t start, uint32_t num,
+		const struct spa_pod *filter)
+{
+	uint8_t buffer[4096];
+	struct spa_pod_builder b = { 0 };
+	int res;
+	uint32_t count = 0;
+	struct spa_result_node_params result;
+
+	spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+	result.id = id;
+	result.next = start;
+next:
+	result.index = result.next;
+
+	if (result.next < 0x100000) {
+		/* Enumerate follower formats first, until we have enough or we run out */
+		if ((res = node_port_enum_params_sync(this, this->follower, direction, port_id, id,
+						&result.next, filter, &result.param, &b)) != 1) {
+			if (res == 0 || res == -ENOENT) {
+				result.next = 0x100000;
+				goto next;
+			} else {
+				spa_log_error(this->log, "could not enum follower format: %s", spa_strerror(res));
+				return res;
+			}
+		}
+	} else if (result.next < 0x200000) {
+		/* Then enumerate converter formats */
+		result.next &= 0xfffff;
+		if ((res = node_port_enum_params_sync(this, this->convert, direction, port_id, id,
+						&result.next, filter, &result.param, &b)) != 1) {
+			return res;
+		} else {
+			result.next |= 0x100000;
+		}
+	}
+
+	spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+	if (++count < num)
+		goto next;
+
+	return 0;
+}
+
 static int
 impl_node_port_enum_params(void *object, int seq,
 			   enum spa_direction direction, uint32_t port_id,
@@ -1539,10 +1664,15 @@ impl_node_port_enum_params(void *object, int seq,
 	if (direction != this->direction)
 		port_id++;
 
-	spa_log_debug(this->log, "%p: %d %u", this, seq, id);
+	spa_log_debug(this->log, "%p: %d %u %u %u", this, seq, id, start, num);
 
-	return spa_node_port_enum_params(this->target, seq, direction, port_id, id,
-			start, num, filter);
+	/* We only need special handling for EnumFormat in convert mode */
+	if (id == SPA_PARAM_EnumFormat && this->mode == SPA_PARAM_PORT_CONFIG_MODE_convert)
+		return port_enum_formats_for_convert(this, seq, direction, port_id, id,
+				start, num, filter);
+	else
+		return spa_node_port_enum_params(this->target, seq, direction, port_id, id,
+				start, num, filter);
 }
 
 static int
@@ -1645,7 +1775,7 @@ static int impl_node_process(void *object)
 		 * First we run the converter to process the input for the follower
 		 * then if it produced data, we run the follower. */
 		while (retry--) {
-			status = spa_node_process_fast(this->convert);
+			status = spa_node_process_fast(this->target);
 			/* schedule the follower when the converter needed
 			 * a recycled buffer */
 			if (status == -EPIPE || status == 0)
@@ -1677,7 +1807,7 @@ static int impl_node_process(void *object)
 			/* output node (source). First run the converter to make
 			 * sure we push out any queued data. Then when it needs
 			 * more data, schedule the follower. */
-			status = spa_node_process_fast(this->convert);
+			status = spa_node_process_fast(this->target);
 			if (status == 0)
 				status = SPA_STATUS_NEED_DATA;
 			else if (status < 0)
@@ -1733,6 +1863,70 @@ static const struct spa_node_methods impl_node = {
 	.process = impl_node_process,
 };
 
+static int load_converter(struct impl *this, const struct spa_dict *info,
+		const struct spa_support *support, uint32_t n_support)
+{
+	const char* factory_name = NULL;
+	struct spa_handle *hnd_convert = NULL;
+	void *iface_conv = NULL;
+	bool unload_handle = false;
+	struct spa_dict_item *items;
+	struct spa_dict cinfo;
+	char direction[16];
+	uint32_t i;
+
+	items = alloca((info->n_items + 1) * sizeof(struct spa_dict_item));
+	cinfo = SPA_DICT(items, 0);
+	for (i = 0; i < info->n_items; i++)
+		items[cinfo.n_items++] = info->items[i];
+
+	snprintf(direction, sizeof(direction), "%s",
+			SPA_DIRECTION_REVERSE(this->direction) == SPA_DIRECTION_INPUT ?
+			"input" : "output");
+	items[cinfo.n_items++] = SPA_DICT_ITEM("convert.direction", direction);
+
+	factory_name = spa_dict_lookup(&cinfo, "audio.adapt.converter");
+	if (factory_name == NULL)
+		factory_name = SPA_NAME_AUDIO_CONVERT;
+
+	if (spa_streq(factory_name, SPA_NAME_AUDIO_CONVERT)) {
+		size_t size = spa_handle_factory_get_size(&spa_audioconvert_factory, &cinfo);
+
+		hnd_convert = calloc(1, size);
+		if (hnd_convert == NULL)
+			return -errno;
+
+		spa_handle_factory_init(&spa_audioconvert_factory,
+				hnd_convert, &cinfo, support, n_support);
+	} else if (this->ploader) {
+		hnd_convert = spa_plugin_loader_load(this->ploader, factory_name, &cinfo);
+		if (!hnd_convert)
+			return -EINVAL;
+		unload_handle = true;
+	} else {
+		return -ENOTSUP;
+	}
+
+	spa_handle_get_interface(hnd_convert, SPA_TYPE_INTERFACE_Node, &iface_conv);
+	if (iface_conv == NULL) {
+		if (unload_handle)
+			spa_plugin_loader_unload(this->ploader, hnd_convert);
+		else {
+			spa_handle_clear(hnd_convert);
+			free(hnd_convert);
+		}
+		return -EINVAL;
+	}
+
+	this->hnd_convert = hnd_convert;
+	this->convert = iface_conv;
+	this->unload_handle = unload_handle;
+	this->convertname = strdup(factory_name);
+
+	return 0;
+}
+
+
 static int do_auto_port_config(struct impl *this, const char *str)
 {
 	uint32_t state = 0, i;
@@ -1741,22 +1935,21 @@ static int do_auto_port_config(struct impl *this, const char *str)
 #define POSITION_PRESERVE 0
 #define POSITION_AUX 1
 #define POSITION_UNKNOWN 2
-	int res, position = POSITION_PRESERVE;
+	int l, res, position = POSITION_PRESERVE;
 	struct spa_pod *param;
-	uint32_t media_type, media_subtype;
 	bool have_format = false, monitor = false, control = false;
 	struct spa_audio_info format = { 0, };
 	enum spa_param_port_config_mode mode = SPA_PARAM_PORT_CONFIG_MODE_none;
-	struct spa_json it[2];
+	struct spa_json it[1];
 	char key[1024], val[256];
+	const char *v;
 
-	spa_json_init(&it[0], str, strlen(str));
-	if (spa_json_enter_object(&it[0], &it[1]) <= 0)
+	if (spa_json_begin_object(&it[0], str, strlen(str)) <= 0)
 		return -EINVAL;
 
-	while (spa_json_get_string(&it[1], key, sizeof(key)) > 0) {
-		if (spa_json_get_string(&it[1], val, sizeof(val)) <= 0)
-			break;
+	while ((l = spa_json_object_next(&it[0], key, sizeof(key), &v)) > 0) {
+		if (spa_json_parse_stringn(v, l, val, sizeof(val)) <= 0)
+			continue;
 
 		if (spa_streq(key, "mode")) {
 			mode = spa_debug_type_find_type_short(spa_type_param_port_config_mode, val);
@@ -1778,40 +1971,22 @@ static int do_auto_port_config(struct impl *this, const char *str)
 
 	while (true) {
 		struct spa_audio_info info = { 0, };
-		struct spa_pod *position = NULL;
-		uint32_t n_position = 0;
 
 		spa_pod_builder_init(&b, buffer, sizeof(buffer));
-		if ((res = spa_node_port_enum_params_sync(this->follower,
+		if ((res = node_port_enum_params_sync(this, this->follower,
 					this->direction, 0,
 					SPA_PARAM_EnumFormat, &state,
 					NULL, &param, &b)) != 1)
 			break;
 
-		if ((res = spa_format_parse(param, &media_type, &media_subtype)) < 0)
-			continue;
-
-		if (media_type != SPA_MEDIA_TYPE_audio ||
-		    media_subtype != SPA_MEDIA_SUBTYPE_raw)
+		if ((res = spa_format_audio_parse(param, &info)) < 0)
 			continue;
 
 		spa_pod_object_fixate((struct spa_pod_object*)param);
 
-		if (spa_pod_parse_object(param,
-				SPA_TYPE_OBJECT_Format, NULL,
-				SPA_FORMAT_AUDIO_format,        SPA_POD_Id(&info.info.raw.format),
-				SPA_FORMAT_AUDIO_rate,          SPA_POD_Int(&info.info.raw.rate),
-				SPA_FORMAT_AUDIO_channels,      SPA_POD_Int(&info.info.raw.channels),
-				SPA_FORMAT_AUDIO_position,      SPA_POD_OPT_Pod(&position)) < 0)
-			continue;
-
-		if (position != NULL)
-			n_position = spa_pod_copy_array(position, SPA_TYPE_Id,
-					info.info.raw.position, SPA_AUDIO_MAX_CHANNELS);
-		if (n_position == 0 || n_position != info.info.raw.channels)
-			SPA_FLAG_SET(info.info.raw.flags, SPA_AUDIO_FLAG_UNPOSITIONED);
-
-		if (format.info.raw.channels >= info.info.raw.channels)
+		if (info.media_subtype == SPA_MEDIA_SUBTYPE_raw &&
+		    format.media_subtype == SPA_MEDIA_SUBTYPE_raw &&
+		    format.info.raw.channels >= info.info.raw.channels)
 			continue;
 
 		format = info;
@@ -1820,16 +1995,18 @@ static int do_auto_port_config(struct impl *this, const char *str)
 	if (!have_format)
 		return -ENOENT;
 
-	if (position == POSITION_AUX) {
-		for (i = 0; i < format.info.raw.channels; i++)
-			format.info.raw.position[i] = SPA_AUDIO_CHANNEL_START_Aux + i;
-	} else if (position == POSITION_UNKNOWN) {
-		for (i = 0; i < format.info.raw.channels; i++)
-			format.info.raw.position[i] = SPA_AUDIO_CHANNEL_UNKNOWN;
+	if (format.media_subtype == SPA_MEDIA_SUBTYPE_raw) {
+		if (position == POSITION_AUX) {
+			for (i = 0; i < format.info.raw.channels; i++)
+				format.info.raw.position[i] = SPA_AUDIO_CHANNEL_START_Aux + i;
+		} else if (position == POSITION_UNKNOWN) {
+			for (i = 0; i < format.info.raw.channels; i++)
+				format.info.raw.position[i] = SPA_AUDIO_CHANNEL_UNKNOWN;
+		}
 	}
 
 	spa_pod_builder_init(&b, buffer, sizeof(buffer));
-	param = spa_format_audio_raw_build(&b, SPA_PARAM_Format, &format.info.raw);
+	param = spa_format_audio_build(&b, SPA_PARAM_Format, &format);
 	param = spa_pod_builder_add_object(&b,
 		SPA_TYPE_OBJECT_ParamPortConfig, SPA_PARAM_PortConfig,
 		SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(this->direction),
@@ -1870,7 +2047,15 @@ static int impl_clear(struct spa_handle *handle)
 	spa_hook_remove(&this->follower_listener);
 	spa_node_set_callbacks(this->follower, NULL, NULL);
 
-	spa_handle_clear(this->hnd_convert);
+	if (this->hnd_convert) {
+		if (this->unload_handle)
+			spa_plugin_loader_unload(this->ploader, this->hnd_convert);
+		else {
+			spa_handle_clear(this->hnd_convert);
+			free(this->hnd_convert);
+		}
+		free(this->convertname);
+	}
 
 	clear_buffers(this);
 	return 0;
@@ -1881,10 +2066,7 @@ static size_t
 impl_get_size(const struct spa_handle_factory *factory,
 	      const struct spa_dict *params)
 {
-	size_t size;
-
-	size = spa_handle_factory_get_size(&spa_audioconvert_factory, params);
-	size += sizeof(struct impl);
+	size_t size = sizeof(struct impl);
 
 	return size;
 }
@@ -1897,8 +2079,9 @@ impl_init(const struct spa_handle_factory *factory,
 	  uint32_t n_support)
 {
 	struct impl *this;
-	void *iface;
 	const char *str;
+	int ret;
+	struct spa_hook probe_listener;
 
 	spa_return_val_if_fail(factory != NULL, -EINVAL);
 	spa_return_val_if_fail(handle != NULL, -EINVAL);
@@ -1913,6 +2096,8 @@ impl_init(const struct spa_handle_factory *factory,
 
 	this->cpu = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_CPU);
 
+	this->ploader = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_PluginLoader);
+
 	if (info == NULL ||
 	    (str = spa_dict_lookup(info, "audio.adapt.follower")) == NULL)
 		return -EINVAL;
@@ -1931,17 +2116,25 @@ impl_init(const struct spa_handle_factory *factory,
 			SPA_VERSION_NODE,
 			&impl_node, this);
 
-	this->hnd_convert = SPA_PTROFF(this, sizeof(struct impl), struct spa_handle);
-	spa_handle_factory_init(&spa_audioconvert_factory,
-				this->hnd_convert,
-				info, support, n_support);
+	/* just probe the ports to get the direction */
+	spa_zero(probe_listener);
+	spa_node_add_listener(this->follower, &probe_listener, &follower_probe_events, this);
+	spa_hook_remove(&probe_listener);
 
-	spa_handle_get_interface(this->hnd_convert, SPA_TYPE_INTERFACE_Node, &iface);
-	if (iface == NULL)
-		return -EINVAL;
+	ret = load_converter(this, info, support, n_support);
+	spa_log_info(this->log, "%p: loaded converter %s, hnd %p, convert %p", this,
+			this->convertname, this->hnd_convert, this->convert);
+	if (ret < 0)
+		return ret;
 
-	this->convert = iface;
-	this->target = this->convert;
+	if (this->convert == NULL) {
+		this->target = this->follower;
+		this->mode = SPA_PARAM_PORT_CONFIG_MODE_passthrough;
+	} else {
+		this->target = this->convert;
+		/* the actual mode is selected below */
+		this->mode = SPA_PARAM_PORT_CONFIG_MODE_none;
+	}
 
 	this->info_all = SPA_NODE_CHANGE_MASK_FLAGS |
 		SPA_NODE_CHANGE_MASK_PROPS |
@@ -1965,14 +2158,16 @@ impl_init(const struct spa_handle_factory *factory,
 			&this->follower_listener, &follower_node_events, this);
 	spa_node_set_callbacks(this->follower, &follower_node_callbacks, this);
 
-	spa_node_add_listener(this->convert,
-			&this->convert_listener, &convert_node_events, this);
-
-	if (info && (str = spa_dict_lookup(info, "adapter.auto-port-config")) != NULL)
-		do_auto_port_config(this, str);
-	else
-		configure_convert(this, SPA_PARAM_PORT_CONFIG_MODE_dsp);
-
+	if (this->convert) {
+		spa_node_add_listener(this->convert,
+				&this->convert_listener, &convert_node_events, this);
+		if (info && (str = spa_dict_lookup(info, "adapter.auto-port-config")) != NULL)
+			do_auto_port_config(this, str);
+		else
+			configure_convert(this, SPA_PARAM_PORT_CONFIG_MODE_dsp);
+	} else {
+		reconfigure_mode(this, SPA_PARAM_PORT_CONFIG_MODE_passthrough, this->direction, NULL);
+	}
 	link_io(this);
 
 	return 0;
diff --git a/spa/plugins/audioconvert/audioconvert.c b/spa/plugins/audioconvert/audioconvert.c
index 897cdf8f..47859e65 100644
--- a/spa/plugins/audioconvert/audioconvert.c
+++ b/spa/plugins/audioconvert/audioconvert.c
@@ -11,6 +11,7 @@
 #include <spa/support/cpu.h>
 #include <spa/support/loop.h>
 #include <spa/support/log.h>
+#include <spa/support/plugin-loader.h>
 #include <spa/utils/result.h>
 #include <spa/utils/list.h>
 #include <spa/utils/json.h>
@@ -22,12 +23,15 @@
 #include <spa/node/utils.h>
 #include <spa/node/keys.h>
 #include <spa/param/audio/format-utils.h>
+#include <spa/param/audio/raw-json.h>
 #include <spa/param/param.h>
 #include <spa/param/latency-utils.h>
 #include <spa/param/tag-utils.h>
 #include <spa/pod/filter.h>
 #include <spa/pod/dynamic.h>
 #include <spa/debug/types.h>
+#include <spa/control/ump-utils.h>
+#include <spa/filter-graph/filter-graph.h>
 
 #include "volume-ops.h"
 #include "fmt-ops.h"
@@ -46,6 +50,8 @@ SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.audioconvert");
 #define MAX_BUFFERS	32
 #define MAX_DATAS	SPA_AUDIO_MAX_CHANNELS
 #define MAX_PORTS	(SPA_AUDIO_MAX_CHANNELS+1)
+#define MAX_STAGES	64
+#define MAX_GRAPH	9	/* 8 active + 1 replacement slot */
 
 #define DEFAULT_MUTE		false
 #define DEFAULT_VOLUME		VOLUME_NORM
@@ -93,6 +99,7 @@ struct props {
 	double rate;
 	char wav_path[512];
 	unsigned int lock_volumes:1;
+	unsigned int filter_graph_disabled:1;
 };
 
 static void props_reset(struct props *props)
@@ -114,6 +121,7 @@ static void props_reset(struct props *props)
 	props->rate = 1.0;
 	spa_zero(props->wav_path);
 	props->lock_volumes = false;
+	props->filter_graph_disabled = false;
 }
 
 struct buffer {
@@ -187,6 +195,47 @@ struct dir {
 	unsigned int control:1;
 };
 
+struct stage_context {
+#define CTX_DATA_SRC		0
+#define CTX_DATA_DST		1
+#define CTX_DATA_REMAP_DST	2
+#define CTX_DATA_REMAP_SRC	3
+#define CTX_DATA_TMP_0		4
+#define CTX_DATA_TMP_1		5
+#define CTX_DATA_MAX		6
+	void **datas[CTX_DATA_MAX];
+	uint32_t in_samples;
+	uint32_t n_samples;
+	uint32_t n_out;
+	uint32_t src_idx;
+	uint32_t dst_idx;
+	uint32_t final_idx;
+	uint32_t n_datas;
+	struct port *ctrlport;
+};
+
+struct stage {
+	struct impl *impl;
+	bool passthrough;
+	uint32_t in_idx;
+	uint32_t out_idx;
+	uint32_t n_in;
+	uint32_t n_out;
+	void *data;
+	void (*run) (struct stage *stage, struct stage_context *c);
+};
+
+struct filter_graph {
+	struct impl *impl;
+	int order;
+	struct spa_handle *handle;
+	struct spa_filter_graph *graph;
+	struct spa_hook listener;
+	uint32_t n_inputs;
+	uint32_t n_outputs;
+	bool active;
+};
+
 struct impl {
 	struct spa_handle handle;
 	struct spa_node node;
@@ -194,6 +243,17 @@ struct impl {
 	struct spa_log *log;
 	struct spa_cpu *cpu;
 	struct spa_loop *data_loop;
+	struct spa_plugin_loader *loader;
+
+	uint32_t n_graph;
+	uint32_t graph_index[MAX_GRAPH];
+
+	struct filter_graph filter_graph[MAX_GRAPH];
+	int in_filter_props;
+	int filter_props_count;
+
+	struct stage stages[MAX_STAGES];
+	uint32_t n_stages;
 
 	uint32_t cpu_flags;
 	uint32_t max_align;
@@ -240,6 +300,9 @@ struct impl {
 	unsigned int rate_adjust:1;
 	unsigned int port_ignore_latency:1;
 	unsigned int monitor_passthrough:1;
+	unsigned int resample_passthrough:1;
+
+	bool recalc;
 
 	char group_name[128];
 
@@ -302,7 +365,7 @@ static void emit_port_info(struct impl *this, struct port *port, bool full)
 				items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_PORT_IGNORE_LATENCY, "true");
 		} else if (PORT_IS_CONTROL(this, port->direction, port->id)) {
 			items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_PORT_NAME, "control");
-			items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_FORMAT_DSP, "8 bit raw midi");
+			items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_FORMAT_DSP, "32 bit raw UMP");
 		}
 		if (this->group_name[0] != '\0')
 			items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_PORT_GROUP, this->group_name);
@@ -408,6 +471,7 @@ static int impl_node_enum_params(void *object, int seq,
 	uint8_t buffer[4096];
 	struct spa_result_node_params result;
 	uint32_t count = 0;
+	int res;
 
 	spa_return_val_if_fail(this != NULL, -EINVAL);
 	spa_return_val_if_fail(num != 0, -EINVAL);
@@ -738,8 +802,30 @@ static int impl_node_enum_params(void *object, int seq,
 				SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(p->lock_volumes),
 				SPA_PROP_INFO_params, SPA_POD_Bool(true));
 			break;
+		case 28:
+			param = spa_pod_builder_add_object(&b,
+				SPA_TYPE_OBJECT_PropInfo, id,
+				SPA_PROP_INFO_name, SPA_POD_String("audioconvert.filter-graph.disable"),
+				SPA_PROP_INFO_description, SPA_POD_String("Disable Filter graph updates"),
+				SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(p->filter_graph_disabled),
+				SPA_PROP_INFO_params, SPA_POD_Bool(true));
+			break;
+		case 29:
+			param = spa_pod_builder_add_object(&b,
+				SPA_TYPE_OBJECT_PropInfo, id,
+				SPA_PROP_INFO_name, SPA_POD_String("audioconvert.filter-graph"),
+				SPA_PROP_INFO_description, SPA_POD_String("A filter graph to load"),
+				SPA_PROP_INFO_type, SPA_POD_String(""),
+				SPA_PROP_INFO_params, SPA_POD_Bool(true));
+			break;
 		default:
-			return 0;
+			if (this->filter_graph[0].graph) {
+				res = spa_filter_graph_enum_prop_info(this->filter_graph[0].graph,
+						result.index - 30, &b, &param);
+				if (res <= 0)
+					return res;
+			} else
+				return 0;
 		}
 		break;
 	}
@@ -818,11 +904,27 @@ static int impl_node_enum_params(void *object, int seq,
 			spa_pod_builder_string(&b, p->wav_path);
 			spa_pod_builder_string(&b, "channelmix.lock-volumes");
 			spa_pod_builder_bool(&b, p->lock_volumes);
+			spa_pod_builder_string(&b, "audioconvert.filter-graph.disable");
+			spa_pod_builder_bool(&b, p->filter_graph_disabled);
+			spa_pod_builder_string(&b, "audioconvert.filter-graph");
+			spa_pod_builder_string(&b, "");
 			spa_pod_builder_pop(&b, &f[1]);
 			param = spa_pod_builder_pop(&b, &f[0]);
 			break;
 		default:
-			return 0;
+			if (result.index > MAX_GRAPH)
+				return 0;
+
+			if (this->filter_graph[result.index-1].graph == NULL)
+				goto next;
+
+			res = spa_filter_graph_get_props(this->filter_graph[result.index-1].graph,
+						&b, &param);
+			if (res < 0)
+				return res;
+			if (res == 0)
+				goto next;
+			break;
 		}
 		break;
 	}
@@ -859,8 +961,197 @@ static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size)
 	return 0;
 }
 
+static void graph_info(void *object, const struct spa_filter_graph_info *info)
+{
+	struct filter_graph *g = object;
+	if (!g->active)
+		return;
+	g->n_inputs = info->n_inputs;
+	g->n_outputs = info->n_outputs;
+}
+
+static int apply_props(struct impl *impl, const struct spa_pod *props);
+
+static void graph_apply_props(void *object, enum spa_direction direction, const struct spa_pod *props)
+{
+	struct filter_graph *g = object;
+	struct impl *impl = g->impl;
+	if (!g->active)
+		return;
+	if (apply_props(impl, props) > 0)
+		emit_node_info(impl, false);
+}
+
+static void graph_props_changed(void *object, enum spa_direction direction)
+{
+	struct filter_graph *g = object;
+	struct impl *impl = g->impl;
+	if (!g->active)
+		return;
+	impl->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS;
+	impl->params[IDX_Props].user++;
+}
+
+struct spa_filter_graph_events graph_events = {
+	SPA_VERSION_FILTER_GRAPH_EVENTS,
+	.info = graph_info,
+	.apply_props = graph_apply_props,
+	.props_changed = graph_props_changed,
+};
+
+static int setup_filter_graph(struct impl *this, struct spa_filter_graph *graph)
+{
+	int res;
+	char rate_str[64];
+	struct dir *in;
+
+	if (graph == NULL)
+		return 0;
+
+	in = &this->dir[SPA_DIRECTION_INPUT];
+	snprintf(rate_str, sizeof(rate_str), "%d", in->format.info.raw.rate);
+
+	spa_filter_graph_deactivate(graph);
+	res = spa_filter_graph_activate(graph,
+				     &SPA_DICT_ITEMS(
+					     SPA_DICT_ITEM(SPA_KEY_AUDIO_RATE, rate_str)));
+	return res;
+}
+
+static int do_sync_filter_graph(struct spa_loop *loop, bool async, uint32_t seq,
+		const void *data, size_t size, void *user_data)
+{
+	struct impl *impl = user_data;
+	uint32_t i, j;
+	impl->n_graph = 0;
+	for (i = 0; i < MAX_GRAPH; i++) {
+		struct filter_graph *g = &impl->filter_graph[i];
+		if (g->graph == NULL || !g->active)
+			continue;
+		impl->graph_index[impl->n_graph++] = i;
+
+		for (j = impl->n_graph-1; j > 0; j--) {
+			if (impl->filter_graph[impl->graph_index[j]].order >=
+			    impl->filter_graph[impl->graph_index[j-1]].order)
+				break;
+			SPA_SWAP(impl->graph_index[j], impl->graph_index[j-1]);
+		}
+	}
+	impl->recalc = true;
+	return 0;
+}
+
+static void clean_filter_handles(struct impl *impl, bool force)
+{
+	uint32_t i;
+	for (i = 0; i < MAX_GRAPH; i++) {
+		struct filter_graph *g = &impl->filter_graph[i];
+		if (!g->active || force) {
+			if (g->graph)
+				spa_hook_remove(&g->listener);
+			if (g->handle)
+				spa_plugin_loader_unload(impl->loader, g->handle);
+			spa_zero(*g);
+		}
+	}
+}
+
+static int load_filter_graph(struct impl *impl, const char *graph, int order)
+{
+	char qlimit[64];
+	int res;
+	void *iface;
+	struct spa_handle *new_handle = NULL;
+	uint32_t i, idx, n_graph;
+	struct filter_graph *pending, *old_active = NULL;
+
+	if (impl->props.filter_graph_disabled)
+		return -EPERM;
+
+	/* find graph spot */
+	idx = SPA_ID_INVALID;
+	n_graph = 0;
+	for (i = 0; i < MAX_GRAPH; i++) {
+		pending = &impl->filter_graph[i];
+		/* find the first free spot for our new filter */
+		if (!pending->active && idx == SPA_ID_INVALID)
+			idx = i;
+		/* deactivate an existing filter of the same order */
+		if (pending->active) {
+			if (pending->order == order)
+				old_active = pending;
+			else
+				n_graph++;
+		}
+	}
+	/* we can at most have MAX_GRAPH-1 active filters */
+	if (n_graph >= MAX_GRAPH-1)
+		return -ENOSPC;
+
+	pending = &impl->filter_graph[idx];
+	pending->impl = impl;
+	pending->order = order;
+
+	if (graph != NULL && graph[0] != '\0') {
+		snprintf(qlimit, sizeof(qlimit), "%u", impl->quantum_limit);
+
+		new_handle = spa_plugin_loader_load(impl->loader, "filter.graph",
+				&SPA_DICT_ITEMS(
+					SPA_DICT_ITEM(SPA_KEY_LIBRARY_NAME, "filter-graph/libspa-filter-graph"),
+					SPA_DICT_ITEM("clock.quantum-limit", qlimit),
+					SPA_DICT_ITEM("filter.graph", graph)));
+		if (new_handle == NULL)
+			goto error;
+
+		res = spa_handle_get_interface(new_handle, SPA_TYPE_INTERFACE_FilterGraph, &iface);
+		if (res < 0 || iface == NULL)
+			goto error;
+
+		/* prepare new filter and swap it */
+		res = setup_filter_graph(impl, iface);
+		if (res < 0)
+			goto error;
+		pending->graph = iface;
+		pending->active = true;
+		spa_log_info(impl->log, "loading filter-graph order:%d in %d active:%d",
+				order, idx, n_graph + 1);
+	} else {
+		pending->active = false;
+		spa_log_info(impl->log, "removing filter-graph order:%d active:%d",
+				order, n_graph);
+	}
+	if (old_active)
+		old_active->active = false;
+
+	/* we call this here on the pending_graph so that the n_input/n_output is updated
+	 * before we switch */
+	if (pending->active)
+		spa_filter_graph_add_listener(pending->graph,
+				&pending->listener, &graph_events, pending);
+
+	spa_loop_invoke(impl->data_loop, do_sync_filter_graph, 0, NULL, 0, true, impl);
+
+	if (pending->active)
+		pending->handle = new_handle;
+
+	if (impl->in_filter_props == 0)
+		clean_filter_handles(impl, false);
+
+	impl->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS;
+	impl->params[IDX_PropInfo].user++;
+	impl->params[IDX_Props].user++;
+
+	return 0;
+error:
+	if (new_handle != NULL)
+		spa_plugin_loader_unload(impl->loader, new_handle);
+	return -ENOTSUP;
+}
+
 static int audioconvert_set_param(struct impl *this, const char *k, const char *s)
 {
+	int res;
+
 	if (spa_streq(k, "monitor.channel-volumes"))
 		this->monitor_channel_volumes = spa_atob(s);
 	else if (spa_streq(k, "channelmix.disable"))
@@ -901,6 +1192,13 @@ static int audioconvert_set_param(struct impl *this, const char *k, const char *
 	}
 	else if (spa_streq(k, "channelmix.lock-volumes"))
 		this->props.lock_volumes = spa_atob(s);
+	else if (spa_strstartswith(k, "audioconvert.filter-graph")) {
+		int order = atoi(k+ strlen("audioconvert.filter-graph."));
+		if ((res = load_filter_graph(this, s, order)) < 0) {
+			spa_log_warn(this->log, "Can't load filter-graph %d: %s",
+					order, spa_strerror(res));
+		}
+	}
 	else
 		return 0;
 	return 1;
@@ -1224,7 +1522,8 @@ static int apply_props(struct impl *this, const struct spa_pod *param)
 			}
 			break;
 		case SPA_PROP_params:
-			changed += parse_prop_params(this, &prop->value);
+			if (this->filter_props_count == 0)
+				changed += parse_prop_params(this, &prop->value);
 			break;
 		default:
 			break;
@@ -1237,6 +1536,7 @@ static int apply_props(struct impl *this, const struct spa_pod *param)
 			p->have_soft_volume = false;
 
 		set_volume(this);
+		this->recalc = true;
 	}
 
 	if (!p->lock_volumes && vol_ramp_params_changed) {
@@ -1253,23 +1553,26 @@ static int apply_props(struct impl *this, const struct spa_pod *param)
 
 		this->vol_ramp_sequence = (struct spa_pod_sequence *) sequence;
 		this->vol_ramp_offset = 0;
+		this->recalc = true;
 	}
 	return changed;
 }
 
 static int apply_midi(struct impl *this, const struct spa_pod *value)
 {
-	const uint8_t *val = SPA_POD_BODY(value);
-	uint32_t size = SPA_POD_BODY_SIZE(value);
 	struct props *p = &this->props;
+	uint8_t data[8];
+	int size;
 
+	size = spa_ump_to_midi(SPA_POD_BODY(value), SPA_POD_BODY_SIZE(value),
+			data, sizeof(data));
 	if (size < 3)
 		return -EINVAL;
 
-	if ((val[0] & 0xf0) != 0xb0 || val[1] != 7)
+	if ((data[0] & 0xf0) != 0xb0 || data[1] != 7)
 		return 0;
 
-	p->volume = val[2] / 127.0f;
+	p->volume = data[2] / 127.0f;
 	set_volume(this);
 	return 1;
 }
@@ -1343,11 +1646,6 @@ static int reconfigure_mode(struct impl *this, enum spa_param_port_config_mode m
 		i = dir->n_ports++;
 		init_port(this, direction, i, 0, false, false, true);
 	}
-	/* when output is convert mode, we are in OUTPUT (merge) mode, we always output all
-	 * the incoming data to output. When output is DSP, we need to output quantum size
-	 * chunks. */
-	this->direction = this->dir[SPA_DIRECTION_OUTPUT].mode == SPA_PARAM_PORT_CONFIG_MODE_convert ?
-		SPA_DIRECTION_OUTPUT : SPA_DIRECTION_INPUT;
 
 	this->info.change_mask |= SPA_NODE_CHANGE_MASK_FLAGS | SPA_NODE_CHANGE_MASK_PARAMS;
 	this->info.flags &= ~SPA_NODE_FLAG_NEED_CONFIGURE;
@@ -1415,9 +1713,29 @@ static int impl_node_set_param(void *object, uint32_t id, uint32_t flags,
 		break;
 	}
 	case SPA_PARAM_Props:
-		if (apply_props(this, param) > 0)
+	{
+		uint32_t i;
+		bool have_graph = false;
+		this->filter_props_count = 0;
+		for (i = 0; i < MAX_GRAPH; i++) {
+			struct filter_graph *g = &this->filter_graph[i];
+			if (!g->active)
+				continue;
+
+			have_graph = true;
+
+			this->in_filter_props++;
+			spa_filter_graph_set_props(g->graph,
+					SPA_DIRECTION_INPUT, param);
+			this->filter_props_count++;
+			this->in_filter_props--;
+		}
+		if (!have_graph && apply_props(this, param) > 0)
 			emit_node_info(this, false);
+
+		clean_filter_handles(this, false);
 		break;
+	}
 	default:
 		return -ENOENT;
 	}
@@ -1839,21 +2157,33 @@ static int ensure_tmp(struct impl *this, uint32_t maxsize, uint32_t maxports)
 static uint32_t resample_update_rate_match(struct impl *this, bool passthrough, uint32_t size, uint32_t queued)
 {
 	uint32_t delay, match_size;
+	int32_t delay_frac;
 
 	if (passthrough) {
 		delay = 0;
+		delay_frac = 0;
 		match_size = size;
 	} else {
-		double rate = this->rate_scale / this->props.rate;
+		/* Only apply rate_scale if we're working in DSP mode (i.e. in driver rate) */
+		double scale = this->dir[SPA_DIRECTION_REVERSE(this->direction)].mode == SPA_PARAM_PORT_CONFIG_MODE_dsp ?
+			this->rate_scale : 1.0;
+		double rate = scale / this->props.rate;
+		double fdelay;
+
 		if (this->io_rate_match &&
 		    SPA_FLAG_IS_SET(this->io_rate_match->flags, SPA_IO_RATE_MATCH_FLAG_ACTIVE))
 			rate *= this->io_rate_match->rate;
 		resample_update_rate(&this->resample, rate);
-		delay = resample_delay(&this->resample);
-		if (this->direction == SPA_DIRECTION_INPUT)
+		fdelay = resample_delay(&this->resample) + resample_phase(&this->resample);
+		if (this->direction == SPA_DIRECTION_INPUT) {
 			match_size = resample_in_len(&this->resample, size);
-		else
+		} else {
+			fdelay *= rate * this->resample.o_rate / this->resample.i_rate;
 			match_size = resample_out_len(&this->resample, size);
+		}
+
+		delay = (uint32_t)round(fdelay);
+		delay_frac = (int32_t)((fdelay - delay) * 1e9);
 	}
 	match_size -= SPA_MIN(match_size, queued);
 
@@ -1861,6 +2191,7 @@ static uint32_t resample_update_rate_match(struct impl *this, bool passthrough,
 
 	if (this->io_rate_match) {
 		this->io_rate_match->delay = delay + queued;
+		this->io_rate_match->delay_frac = delay_frac;
 		this->io_rate_match->size = match_size;
 	}
 	return match_size;
@@ -1934,6 +2265,13 @@ static int setup_convert(struct impl *this)
 
 	if ((res = setup_in_convert(this)) < 0)
 		return res;
+	for (i = 0; i < MAX_GRAPH; i++) {
+		struct filter_graph *g = &this->filter_graph[i];
+		if (!g->active)
+			continue;
+		if ((res = setup_filter_graph(this, g->graph)) < 0)
+			return res;
+	}
 	if ((res = setup_channelmix(this)) < 0)
 		return res;
 	if ((res = setup_resample(this)) < 0)
@@ -1957,6 +2295,7 @@ static int setup_convert(struct impl *this)
 	resample_update_rate_match(this, resample_is_passthrough(this), duration, 0);
 
 	this->setup = true;
+	this->recalc = true;
 
 	emit_node_info(this, false);
 
@@ -1965,6 +2304,12 @@ static int setup_convert(struct impl *this)
 
 static void reset_node(struct impl *this)
 {
+	uint32_t i;
+	for (i = 0; i < MAX_GRAPH; i++) {
+		struct filter_graph *g = &this->filter_graph[i];
+		if (g->graph)
+			spa_filter_graph_deactivate(g->graph);
+	}
 	if (this->resample.reset)
 		resample_reset(&this->resample);
 	this->in_offset = 0;
@@ -2071,7 +2416,9 @@ static int port_enum_formats(void *object,
 			*param = spa_pod_builder_add_object(builder,
 				SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat,
 				SPA_FORMAT_mediaType,      SPA_POD_Id(SPA_MEDIA_TYPE_application),
-				SPA_FORMAT_mediaSubtype,   SPA_POD_Id(SPA_MEDIA_SUBTYPE_control));
+				SPA_FORMAT_mediaSubtype,   SPA_POD_Id(SPA_MEDIA_SUBTYPE_control),
+				SPA_FORMAT_CONTROL_types,  SPA_POD_CHOICE_FLAGS_Int(
+					(1u<<SPA_CONTROL_UMP) | (1u<<SPA_CONTROL_Properties)));
 		} else {
 			struct spa_pod_frame f[1];
 			uint32_t rate = this->io_position ?
@@ -2177,7 +2524,9 @@ impl_node_port_enum_params(void *object, int seq,
 			param = spa_pod_builder_add_object(&b,
 				SPA_TYPE_OBJECT_Format,  id,
 				SPA_FORMAT_mediaType,    SPA_POD_Id(SPA_MEDIA_TYPE_application),
-				SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_control));
+				SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_control),
+				SPA_FORMAT_CONTROL_types,  SPA_POD_Int(
+					(1u<<SPA_CONTROL_UMP) | (1u<<SPA_CONTROL_Properties)));
 		else
 			param = spa_format_audio_raw_build(&b, id, &port->format.info.raw);
 		break;
@@ -2190,29 +2539,30 @@ impl_node_port_enum_params(void *object, int seq,
 		if (result.index > 0)
 			return 0;
 
-		if (PORT_IS_DSP(this, direction, port_id)) {
-			/* DSP ports always use the quantum_limit as the buffer
-			 * size. */
-			size = this->quantum_limit;
-		} else {
+		size = this->quantum_limit;
+
+		if (!PORT_IS_DSP(this, direction, port_id)) {
 			uint32_t irate, orate;
 			struct dir *dir = &this->dir[direction];
 
 			/* Convert ports are scaled so that they can always
-			 * provide one quantum of data */
+			 * provide one quantum of data. irate is the rate of the
+			 * data before it goes into the resampler. */
 			irate = dir->format.info.raw.rate;
+			/* scale the size for adaptive resampling */
+			size += size/2;
 
-			/* collect the other port rate */
+			/* collect the other port rate. This is the output of the resampler
+			 * and is usually one quantum. */
 			dir = &this->dir[SPA_DIRECTION_REVERSE(direction)];
 			if (dir->mode == SPA_PARAM_PORT_CONFIG_MODE_dsp)
-				orate = this->io_position ?  this->io_position->clock.target_rate.denom : DEFAULT_RATE;
+				orate = this->io_position ? this->io_position->clock.target_rate.denom : DEFAULT_RATE;
 			else
 				orate = dir->format.info.raw.rate;
 
-			/* always keep some extra room for adaptive resampling */
-			size = this->quantum_limit * 2;
-			/*  scale the buffer size when we can. */
-			if (irate != 0 && orate != 0)
+			/* scale the buffer size when we can. Only do this when we downsample because
+			 * then we need to ask more input data for one quantum. */
+			if (irate != 0 && orate != 0 && irate > orate)
 				size = SPA_SCALE32_UP(size, irate, orate);
 		}
 
@@ -2741,30 +3091,6 @@ static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t
 	return 0;
 }
 
-static void handle_wav(struct impl *this, const void **src, uint32_t n_samples)
-{
-	if (SPA_UNLIKELY(this->props.wav_path[0])) {
-		if (this->wav_file == NULL) {
-			struct wav_file_info info;
-
-			info.info = this->dir[this->direction].format;
-
-			this->wav_file = wav_file_open(this->props.wav_path,
-					"w", &info);
-			if (this->wav_file == NULL)
-				spa_log_warn(this->log, "can't open wav path: %m");
-		}
-		if (this->wav_file) {
-			wav_file_write(this->wav_file, src, n_samples);
-		} else {
-			spa_zero(this->props.wav_path);
-		}
-	} else if (this->wav_file != NULL) {
-		wav_file_close(this->wav_file);
-		this->wav_file = NULL;
-	}
-}
-
 static int channelmix_process_apply_sequence(struct impl *this,
 			const struct spa_pod_sequence *sequence, uint32_t *processed_offset,
 			void *SPA_RESTRICT dst[], const void *SPA_RESTRICT src[],
@@ -2808,7 +3134,7 @@ static int channelmix_process_apply_sequence(struct impl *this,
 
 		if (prev) {
 			switch (prev->type) {
-			case SPA_CONTROL_Midi:
+			case SPA_CONTROL_UMP:
 				apply_midi(this, &prev->value);
 				break;
 			case SPA_CONTROL_Properties:
@@ -2856,25 +3182,385 @@ static uint64_t get_time_ns(struct impl *impl)
 	return SPA_TIMESPEC_TO_NSEC(&now);
 }
 
+static void run_wav_stage(struct stage *stage, struct stage_context *c)
+{
+	struct impl *impl = stage->impl;
+	const void **src = (const void **)c->datas[stage->in_idx];
+
+	if (SPA_UNLIKELY(impl->props.wav_path[0])) {
+		if (impl->wav_file == NULL) {
+			struct wav_file_info info;
+
+			info.info = impl->dir[impl->direction].format;
+
+			impl->wav_file = wav_file_open(impl->props.wav_path,
+					"w", &info);
+			if (impl->wav_file == NULL)
+				spa_log_warn(impl->log, "can't open wav path: %m");
+		}
+		if (impl->wav_file) {
+			wav_file_write(impl->wav_file, src, c->n_samples);
+		} else {
+			spa_zero(impl->props.wav_path);
+		}
+	} else if (impl->wav_file != NULL) {
+		wav_file_close(impl->wav_file);
+		impl->wav_file = NULL;
+		impl->recalc = true;
+	}
+}
+
+static void add_wav_stage(struct impl *impl, struct stage_context *ctx)
+{
+	struct stage *s = &impl->stages[impl->n_stages];
+	s->impl = impl;
+	s->passthrough = false;
+	s->in_idx = ctx->src_idx;
+	s->out_idx = ctx->src_idx;
+	s->n_in = ctx->n_datas;
+	s->n_out = ctx->n_datas;
+	s->data = NULL;
+	s->run = run_wav_stage;
+	spa_log_trace(impl->log, "%p: stage %d", impl, impl->n_stages);
+	impl->n_stages++;
+}
+
+static void run_dst_remap_stage(struct stage *s, struct stage_context *c)
+{
+	struct impl *impl = s->impl;
+	struct dir *dir = &impl->dir[SPA_DIRECTION_OUTPUT];
+	uint32_t i;
+	for (i = 0; i < s->n_in; i++) {
+		c->datas[s->out_idx][i] = c->datas[s->in_idx][dir->remap[i]];
+		spa_log_trace_fp(impl->log, "%p: output remap %d -> %d", impl, i, dir->remap[i]);
+	}
+}
+static void add_dst_remap_stage(struct impl *impl, struct stage_context *ctx)
+{
+	struct stage *s = &impl->stages[impl->n_stages];
+	s->impl = impl;
+	s->passthrough = false;
+	s->in_idx = ctx->dst_idx;
+	s->out_idx = CTX_DATA_REMAP_DST;
+	s->n_in = ctx->n_datas;
+	s->n_out = ctx->n_datas;
+	s->data = NULL;
+	s->run = run_dst_remap_stage;
+	spa_log_trace(impl->log, "%p: stage %d", impl, impl->n_stages);
+	impl->n_stages++;
+	ctx->dst_idx = CTX_DATA_REMAP_DST;
+	ctx->final_idx = CTX_DATA_REMAP_DST;
+}
+
+static void run_src_remap_stage(struct stage *s, struct stage_context *c)
+{
+	struct impl *impl = s->impl;
+	struct dir *dir = &impl->dir[SPA_DIRECTION_INPUT];
+	uint32_t i;
+	for (i = 0; i < dir->conv.n_channels; i++) {
+		c->datas[s->out_idx][dir->remap[i]] = c->datas[s->in_idx][i];
+		spa_log_trace_fp(impl->log, "%p: input remap %d -> %d", impl, dir->remap[i], i);
+	}
+}
+static void add_src_remap_stage(struct impl *impl, struct stage_context *ctx)
+{
+	struct stage *s = &impl->stages[impl->n_stages];
+	s->impl = impl;
+	s->passthrough = false;
+	s->in_idx = ctx->src_idx;
+	s->out_idx = CTX_DATA_REMAP_SRC;
+	s->n_in = ctx->n_datas;
+	s->n_out = ctx->n_datas;
+	s->data = NULL;
+	s->run = run_src_remap_stage;
+	spa_log_trace(impl->log, "%p: stage %d", impl, impl->n_stages);
+	impl->n_stages++;
+	ctx->src_idx = CTX_DATA_REMAP_SRC;
+}
+
+static void run_src_convert_stage(struct stage *s, struct stage_context *c)
+{
+	struct impl *impl = s->impl;
+	struct dir *dir = &impl->dir[SPA_DIRECTION_INPUT];
+	void *remap_src_datas[MAX_PORTS], **dst;
+
+	spa_log_trace_fp(impl->log, "%p: input convert %d", impl, c->n_samples);
+	if (dir->need_remap) {
+		uint32_t i;
+		for (i = 0; i < dir->conv.n_channels; i++) {
+			remap_src_datas[i] = c->datas[s->out_idx][dir->remap[i]];
+			spa_log_trace_fp(impl->log, "%p: input remap %d -> %d", impl, dir->remap[i], i);
+		}
+		dst = remap_src_datas;
+	} else {
+		dst = c->datas[s->out_idx];
+	}
+	convert_process(&dir->conv, dst, (const void**)c->datas[s->in_idx], c->n_samples);
+}
+static void add_src_convert_stage(struct impl *impl, struct stage_context *ctx)
+{
+	struct stage *s = &impl->stages[impl->n_stages];
+	s->impl = impl;
+	s->passthrough = false;
+	s->in_idx = ctx->src_idx;
+	s->out_idx = ctx->dst_idx;
+	s->n_in = ctx->n_datas;
+	s->n_out = ctx->n_datas;
+	s->data = NULL;
+	s->run = run_src_convert_stage;
+	spa_log_trace(impl->log, "%p: stage %d", impl, impl->n_stages);
+	impl->n_stages++;
+	ctx->src_idx = ctx->dst_idx;
+}
+
+static void run_resample_stage(struct stage *s, struct stage_context *c)
+{
+	struct impl *impl = s->impl;
+	uint32_t in_len = c->n_samples;
+	uint32_t out_len = c->n_out;
+
+	resample_process(&impl->resample, (const void**)c->datas[s->in_idx], &in_len,
+			c->datas[s->out_idx], &out_len);
+
+	spa_log_trace_fp(impl->log, "%p: resample %d/%d -> %d/%d", impl,
+				c->n_samples, in_len, c->n_out, out_len);
+	c->in_samples = in_len;
+	c->n_samples = out_len;
+}
+static void add_resample_stage(struct impl *impl, struct stage_context *ctx)
+{
+	struct stage *s = &impl->stages[impl->n_stages];
+	s->impl = impl;
+	s->passthrough = false;
+	s->in_idx = ctx->src_idx;
+	s->out_idx = ctx->dst_idx;
+	s->n_in = ctx->n_datas;
+	s->n_out = ctx->n_datas;
+	s->data = NULL;
+	s->run = run_resample_stage;
+	spa_log_trace(impl->log, "%p: stage %d", impl, impl->n_stages);
+	impl->n_stages++;
+	ctx->src_idx = ctx->dst_idx;
+}
+
+static void run_channelmix_stage(struct stage *s, struct stage_context *c)
+{
+	struct impl *impl = s->impl;
+	void **out_datas = c->datas[s->out_idx];
+	const void **in_datas = (const void**)c->datas[s->in_idx];
+	struct port *ctrlport = c->ctrlport;
+
+	spa_log_trace_fp(impl->log, "%p: channelmix %d", impl, c->n_samples);
+	if (ctrlport != NULL && ctrlport->ctrl != NULL) {
+		if (channelmix_process_apply_sequence(impl, ctrlport->ctrl,
+					&ctrlport->ctrl_offset, out_datas, in_datas, c->n_samples) == 1) {
+			ctrlport->io->status = SPA_STATUS_OK;
+			ctrlport->ctrl = NULL;
+		}
+	} else if (impl->vol_ramp_sequence) {
+		if (channelmix_process_apply_sequence(impl, impl->vol_ramp_sequence,
+				&impl->vol_ramp_offset, out_datas, in_datas, c->n_samples) == 1) {
+			free(impl->vol_ramp_sequence);
+			impl->vol_ramp_sequence = NULL;
+		}
+	} else {
+		channelmix_process(&impl->mix, out_datas, in_datas, c->n_samples);
+	}
+}
+
+static void run_filter_stage(struct stage *s, struct stage_context *c)
+{
+	struct filter_graph *fg = s->data;
+
+	spa_log_trace_fp(s->impl->log, "%p: filter-graph %d", s->impl, c->n_samples);
+	spa_filter_graph_process(fg->graph, (const void **)c->datas[s->in_idx],
+			c->datas[s->out_idx], c->n_samples);
+}
+static void add_filter_stage(struct impl *impl, uint32_t i, struct filter_graph *fg, struct stage_context *ctx)
+{
+	struct stage *s = &impl->stages[impl->n_stages];
+	s->impl = impl;
+	s->passthrough = false;
+	s->in_idx = ctx->src_idx;
+	s->out_idx = ctx->dst_idx;
+	s->n_in = ctx->n_datas;
+	s->n_out = ctx->n_datas;
+	s->data = fg;
+	s->run = run_filter_stage;
+	spa_log_trace(impl->log, "%p: stage %d", impl, impl->n_stages);
+	impl->n_stages++;
+	ctx->src_idx = ctx->dst_idx;
+}
+
+static void add_channelmix_stage(struct impl *impl, struct stage_context *ctx)
+{
+	struct stage *s = &impl->stages[impl->n_stages];
+	s->impl = impl;
+	s->passthrough = false;
+	s->in_idx = ctx->src_idx;
+	s->out_idx = ctx->dst_idx;
+	s->n_in = ctx->n_datas;
+	s->n_out = ctx->n_datas;
+	s->data = NULL;
+	s->run = run_channelmix_stage;
+	spa_log_trace(impl->log, "%p: stage %d", impl, impl->n_stages);
+	impl->n_stages++;
+	ctx->src_idx = ctx->dst_idx;
+}
+
+static void run_dst_convert_stage(struct stage *s, struct stage_context *c)
+{
+	struct impl *impl = s->impl;
+	struct dir *dir = &impl->dir[SPA_DIRECTION_OUTPUT];
+	void *remap_datas[MAX_PORTS], **src;
+
+	spa_log_trace_fp(impl->log, "%p: output convert %d", impl, c->n_samples);
+	if (dir->need_remap) {
+		uint32_t i;
+		for (i = 0; i < dir->conv.n_channels; i++) {
+			remap_datas[dir->remap[i]] = c->datas[s->in_idx][i];
+			spa_log_trace_fp(impl->log, "%p: output remap %d -> %d", impl, i, dir->remap[i]);
+		}
+		src = remap_datas;
+	} else {
+		src = c->datas[s->in_idx];
+	}
+	convert_process(&dir->conv, c->datas[s->out_idx], (const void **)src, c->n_samples);
+}
+static void add_dst_convert_stage(struct impl *impl, struct stage_context *ctx)
+{
+	struct stage *s = &impl->stages[impl->n_stages];
+	s->impl = impl;
+	s->passthrough = false;
+	s->in_idx = ctx->src_idx;
+	s->out_idx = ctx->final_idx;
+	s->n_in = ctx->n_datas;
+	s->n_out = ctx->n_datas;
+	s->data = NULL;
+	s->run = run_dst_convert_stage;
+	spa_log_trace(impl->log, "%p: stage %d", impl, impl->n_stages);
+	impl->n_stages++;
+	ctx->src_idx = s->out_idx;
+}
+
+static void recalc_stages(struct impl *this, struct stage_context *ctx)
+{
+	struct dir *dir;
+	bool filter_passthrough, in_passthrough, mix_passthrough, resample_passthrough, out_passthrough;
+	int tmp = 0;
+	struct port *ctrlport = ctx->ctrlport;
+	bool in_need_remap, out_need_remap;
+	uint32_t i;
+
+	this->recalc = false;
+	this->n_stages = 0;
+
+	dir = &this->dir[SPA_DIRECTION_INPUT];
+	in_passthrough = dir->conv.is_passthrough;
+	in_need_remap = dir->need_remap;
+
+	dir = &this->dir[SPA_DIRECTION_OUTPUT];
+	out_passthrough = dir->conv.is_passthrough;
+	out_need_remap = dir->need_remap;
+
+	resample_passthrough = resample_is_passthrough(this);
+	filter_passthrough = this->n_graph == 0;
+	this->resample_passthrough = resample_passthrough;
+	mix_passthrough = SPA_FLAG_IS_SET(this->mix.flags, CHANNELMIX_FLAG_IDENTITY) &&
+		(ctrlport == NULL || ctrlport->ctrl == NULL) && (this->vol_ramp_sequence == NULL);
+
+	if (in_passthrough && filter_passthrough && mix_passthrough && resample_passthrough)
+		out_passthrough = false;
+
+	if (out_passthrough && out_need_remap)
+		add_dst_remap_stage(this, ctx);
+
+	if (this->direction == SPA_DIRECTION_INPUT &&
+	    (this->props.wav_path[0] || this->wav_file != NULL))
+		add_wav_stage(this, ctx);
+
+	if (!in_passthrough) {
+		if (filter_passthrough && mix_passthrough && resample_passthrough && out_passthrough)
+			ctx->dst_idx = ctx->final_idx;
+		else
+			ctx->dst_idx = CTX_DATA_TMP_0 + ((tmp++) & 1);
+
+		add_src_convert_stage(this, ctx);
+	} else {
+		if (in_need_remap)
+			add_src_remap_stage(this, ctx);
+	}
+
+	if (this->direction == SPA_DIRECTION_INPUT) {
+		if (!resample_passthrough) {
+			if (filter_passthrough && mix_passthrough && out_passthrough)
+				ctx->dst_idx = ctx->final_idx;
+			else
+				ctx->dst_idx = CTX_DATA_TMP_0 + ((tmp++) & 1);
+
+			add_resample_stage(this, ctx);
+			resample_passthrough = true;
+		}
+	}
+	if (!filter_passthrough) {
+		for (i = 0; i < this->n_graph; i++) {
+			struct filter_graph *fg = &this->filter_graph[this->graph_index[i]];
+
+			if (mix_passthrough && resample_passthrough && out_passthrough &&
+			    i + 1 == this->n_graph)
+				ctx->dst_idx = ctx->final_idx;
+			else
+				ctx->dst_idx = CTX_DATA_TMP_0 + ((tmp++) & 1);
+
+			add_filter_stage(this, i, fg, ctx);
+		}
+	}
+	if (!mix_passthrough) {
+		if (resample_passthrough && out_passthrough)
+			ctx->dst_idx = ctx->final_idx;
+		else
+			ctx->dst_idx = CTX_DATA_TMP_0 + ((tmp++) & 1);
+
+		add_channelmix_stage(this, ctx);
+	}
+	if (this->direction == SPA_DIRECTION_OUTPUT) {
+		if (!resample_passthrough) {
+			if (out_passthrough)
+				ctx->dst_idx = ctx->final_idx;
+			else
+				ctx->dst_idx = CTX_DATA_TMP_0 + ((tmp++) & 1);
+
+			add_resample_stage(this, ctx);
+		}
+	}
+	if (!out_passthrough) {
+		add_dst_convert_stage(this, ctx);
+	}
+	if (this->direction == SPA_DIRECTION_OUTPUT &&
+	    (this->props.wav_path[0] || this->wav_file != NULL))
+		add_wav_stage(this, ctx);
+
+	spa_log_trace(this->log, "got %u processing stages", this->n_stages);
+}
+
 static int impl_node_process(void *object)
 {
 	struct impl *this = object;
-	const void *src_datas[MAX_PORTS], **in_datas;
+	const void *src_datas[MAX_PORTS];
 	void *dst_datas[MAX_PORTS], *remap_src_datas[MAX_PORTS], *remap_dst_datas[MAX_PORTS];
-	void **out_datas, **dst_remap;
 	uint32_t i, j, n_src_datas = 0, n_dst_datas = 0, n_mon_datas = 0, remap;
 	uint32_t n_samples, max_in, n_out, max_out, quant_samples;
 	struct port *port, *ctrlport = NULL;
 	struct buffer *buf, *out_bufs[MAX_PORTS];
 	struct spa_data *bd;
 	struct dir *dir;
-	int tmp = 0, res = 0, suppressed;
-	bool in_passthrough, mix_passthrough, resample_passthrough, out_passthrough;
+	int res = 0, suppressed;
 	bool in_avail = false, flush_in = false, flush_out = false;
 	bool draining = false, in_empty = this->out_offset == 0;
-	struct spa_io_buffers *io, *ctrlio = NULL;
+	struct spa_io_buffers *io;
 	const struct spa_pod_sequence *ctrl = NULL;
 	uint64_t current_time;
+	struct stage_context ctx;
 
 	/* calculate quantum scale, this is how many samples we need to produce or
 	 * consume. Also update the rate scale, this is sent to the resampler to adjust
@@ -2910,7 +3596,6 @@ static int impl_node_process(void *object)
 	}
 
 	dir = &this->dir[SPA_DIRECTION_INPUT];
-	in_passthrough = dir->conv.is_passthrough;
 	max_in = UINT32_MAX;
 
 	/* collect input port data */
@@ -2974,7 +3659,6 @@ static int impl_node_process(void *object)
 					spa_log_trace_fp(this->log, "%p: control %d", this,
 							i * port->blocks + j);
 					ctrlport = port;
-					ctrlio = io;
 					ctrl = spa_pod_from_data(bd->data, bd->maxsize,
 							bd->chunk->offset, bd->chunk->size);
 					if (ctrl && !spa_pod_is_sequence(&ctrl->pod))
@@ -2982,6 +3666,7 @@ static int impl_node_process(void *object)
 					if (ctrl != ctrlport->ctrl) {
 						ctrlport->ctrl = ctrl;
 						ctrlport->ctrl_offset = 0;
+						this->recalc = true;
 					}
 				} else  {
 					max_in = SPA_MIN(max_in, size / port->stride);
@@ -2997,8 +3682,9 @@ static int impl_node_process(void *object)
 			}
 		}
 	}
-
-	resample_passthrough = resample_is_passthrough(this);
+	bool resample_passthrough = resample_is_passthrough(this);
+	if (this->resample_passthrough != resample_passthrough)
+		this->recalc = true;
 
 	/* calculate how many samples we are going to produce. */
 	if (this->direction == SPA_DIRECTION_INPUT) {
@@ -3133,122 +3819,30 @@ static int impl_node_process(void *object)
 		flush_in = true;
 	}
 
-	mix_passthrough = SPA_FLAG_IS_SET(this->mix.flags, CHANNELMIX_FLAG_IDENTITY) &&
-		(ctrlport == NULL || ctrlport->ctrl == NULL) && (this->vol_ramp_sequence == NULL);
-
-	out_passthrough = dir->conv.is_passthrough;
-	if (in_passthrough && mix_passthrough && resample_passthrough)
-		out_passthrough = false;
-
-	if (out_passthrough && dir->need_remap) {
-		for (i = 0; i < dir->conv.n_channels; i++) {
-			remap_dst_datas[i] = dst_datas[dir->remap[i]];
-			spa_log_trace_fp(this->log, "%p: output remap %d -> %d", this, i, dir->remap[i]);
-		}
-		dst_remap = (void **)remap_dst_datas;
-	} else {
-		dst_remap = (void **)dst_datas;
+	ctx.datas[CTX_DATA_SRC] = (void **)src_datas;
+	ctx.datas[CTX_DATA_DST] = dst_datas;
+	ctx.datas[CTX_DATA_REMAP_DST] = remap_dst_datas;
+	ctx.datas[CTX_DATA_REMAP_SRC] = remap_src_datas;
+	ctx.datas[CTX_DATA_TMP_0] = (void**)this->tmp_datas[0];
+	ctx.datas[CTX_DATA_TMP_1] = (void**)this->tmp_datas[1];
+	ctx.in_samples = n_samples;
+	ctx.n_samples = n_samples;
+	ctx.n_out = n_out;
+	ctx.src_idx = CTX_DATA_SRC;
+	ctx.dst_idx = CTX_DATA_DST;
+	ctx.final_idx = CTX_DATA_DST;
+	ctx.n_datas = dir->conv.n_channels;
+	ctx.ctrlport = ctrlport;
+
+	if (this->recalc)
+		recalc_stages(this, &ctx);
+
+	for (i = 0; i < this->n_stages; i++) {
+		struct stage *s = &this->stages[i];
+		s->run(s, &ctx);
 	}
-
-	if (this->direction == SPA_DIRECTION_INPUT)
-		handle_wav(this, src_datas, n_samples);
-
-	dir = &this->dir[SPA_DIRECTION_INPUT];
-	if (!in_passthrough) {
-		if (mix_passthrough && resample_passthrough && out_passthrough)
-			out_datas = (void **)dst_remap;
-		else
-			out_datas = (void **)this->tmp_datas[(tmp++) & 1];
-
-		if (dir->need_remap) {
-			for (i = 0; i < dir->conv.n_channels; i++) {
-				remap_src_datas[i] = out_datas[dir->remap[i]];
-				spa_log_trace_fp(this->log, "%p: input remap %d -> %d", this, dir->remap[i], i);
-			}
-		} else {
-			for (i = 0; i < dir->conv.n_channels; i++)
-				remap_src_datas[i] = out_datas[i];
-		}
-
-		spa_log_trace_fp(this->log, "%p: input convert %d", this, n_samples);
-		convert_process(&dir->conv, remap_src_datas, src_datas, n_samples);
-	} else {
-		if (dir->need_remap) {
-			for (i = 0; i < dir->conv.n_channels; i++) {
-				remap_src_datas[dir->remap[i]] = (void *)src_datas[i];
-				spa_log_trace_fp(this->log, "%p: input remap %d -> %d", this, dir->remap[i], i);
-			}
-			out_datas = (void **)remap_src_datas;
-		} else {
-			out_datas = (void **)src_datas;
-		}
-	}
-
-	if (!mix_passthrough) {
-		in_datas = (const void**)out_datas;
-		if (resample_passthrough && out_passthrough) {
-			out_datas = (void **)dst_remap;
-			n_samples = SPA_MIN(n_samples, n_out);
-		} else {
-			out_datas = (void **)this->tmp_datas[(tmp++) & 1];
-		}
-		spa_log_trace_fp(this->log, "%p: channelmix %d %d %d", this, n_samples,
-				resample_passthrough, out_passthrough);
-		if (ctrlport != NULL && ctrlport->ctrl != NULL) {
-			if (channelmix_process_apply_sequence(this, ctrlport->ctrl,
-						&ctrlport->ctrl_offset, out_datas, in_datas, n_samples) == 1) {
-				ctrlio->status = SPA_STATUS_OK;
-				ctrlport->ctrl = NULL;
-			}
-		} else if (this->vol_ramp_sequence) {
-			if (channelmix_process_apply_sequence(this, this->vol_ramp_sequence,
-					&this->vol_ramp_offset, out_datas, in_datas, n_samples) == 1) {
-				free(this->vol_ramp_sequence);
-				this->vol_ramp_sequence = NULL;
-			}
-		}
-		else {
-			channelmix_process(&this->mix, out_datas, in_datas, n_samples);
-		}
-	}
-	if (!resample_passthrough) {
-		uint32_t in_len, out_len;
-
-		in_datas = (const void**)out_datas;
-		if (out_passthrough)
-			out_datas = (void **)dst_remap;
-		else
-			out_datas = (void **)this->tmp_datas[(tmp++) & 1];
-
-		in_len = n_samples;
-		out_len = n_out;
-		resample_process(&this->resample, in_datas, &in_len, out_datas, &out_len);
-		spa_log_trace_fp(this->log, "%p: resample %d/%d -> %d/%d %d", this,
-				n_samples, in_len, n_out, out_len, out_passthrough);
-		this->in_offset += in_len;
-		n_samples = out_len;
-	} else {
-		n_samples = SPA_MIN(n_samples, n_out);
-		this->in_offset += n_samples;
-	}
-	this->out_offset += n_samples;
-
-	if (!out_passthrough) {
-		dir = &this->dir[SPA_DIRECTION_OUTPUT];
-		if (dir->need_remap) {
-			for (i = 0; i < dir->conv.n_channels; i++) {
-				remap_dst_datas[dir->remap[i]] = out_datas[i];
-				spa_log_trace_fp(this->log, "%p: output remap %d -> %d", this, i, dir->remap[i]);
-			}
-			in_datas = (const void**)remap_dst_datas;
-		} else {
-			in_datas = (const void**)out_datas;
-		}
-		spa_log_trace_fp(this->log, "%p: output convert %d", this, n_samples);
-		convert_process(&dir->conv, dst_datas, in_datas, n_samples);
-	}
-	if (this->direction == SPA_DIRECTION_OUTPUT)
-		handle_wav(this, (const void**)dst_datas, n_samples);
+	this->in_offset += ctx.in_samples;
+	this->out_offset += ctx.n_samples;
 
 	spa_log_trace_fp(this->log, "%d/%d  %d/%d %d->%d", this->in_offset, max_in,
 			this->out_offset, max_out, n_samples, n_out);
@@ -3391,6 +3985,8 @@ static int impl_clear(struct spa_handle *handle)
 
 	free_tmp(this);
 
+	clean_filter_handles(this, true);
+
 	if (this->resample.free)
 		resample_free(&this->resample);
 	if (this->wav_file != NULL)
@@ -3406,34 +4002,6 @@ impl_get_size(const struct spa_handle_factory *factory,
 	return sizeof(struct impl);
 }
 
-static uint32_t channel_from_name(const char *name)
-{
-	int i;
-	for (i = 0; spa_type_audio_channel[i].name; i++) {
-		if (spa_streq(name, spa_debug_type_short_name(spa_type_audio_channel[i].name)))
-			return spa_type_audio_channel[i].type;
-	}
-	return SPA_AUDIO_CHANNEL_UNKNOWN;
-}
-
-static inline uint32_t parse_position(uint32_t *pos, const char *val, size_t len)
-{
-	struct spa_json it[2];
-	char v[256];
-	uint32_t i = 0;
-
-	spa_json_init(&it[0], val, len);
-	if (spa_json_enter_array(&it[0], &it[1]) <= 0)
-		spa_json_init(&it[1], val, len);
-
-	while (spa_json_get_string(&it[1], v, sizeof(v)) > 0 &&
-			i < SPA_AUDIO_MAX_CHANNELS) {
-		pos[i++] = channel_from_name(v);
-	}
-	return i;
-}
-
-
 static int
 impl_init(const struct spa_handle_factory *factory,
 	  struct spa_handle *handle,
@@ -3443,6 +4011,8 @@ impl_init(const struct spa_handle_factory *factory,
 {
 	struct impl *this;
 	uint32_t i;
+	const char *str;
+	bool filter_graph_disabled;
 
 	spa_return_val_if_fail(factory != NULL, -EINVAL);
 	spa_return_val_if_fail(handle != NULL, -EINVAL);
@@ -3461,7 +4031,10 @@ impl_init(const struct spa_handle_factory *factory,
 		this->cpu_flags = spa_cpu_get_flags(this->cpu);
 		this->max_align = SPA_MIN(MAX_ALIGN, spa_cpu_get_max_align(this->cpu));
 	}
+	this->loader = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_PluginLoader);
+
 	props_reset(&this->props);
+	filter_graph_disabled = this->props.filter_graph_disabled;
 
 	this->rate_limit.interval = 2 * SPA_NSEC_PER_SEC;
 	this->rate_limit.burst = 1;
@@ -3474,19 +4047,27 @@ impl_init(const struct spa_handle_factory *factory,
 	this->mix.rear_delay = 0.0f;
 	this->mix.widen = 0.0f;
 
+	if (info && (str = spa_dict_lookup(info, "clock.quantum-limit")) != NULL)
+		spa_atou32(str, &this->quantum_limit, 0);
+
 	for (i = 0; info && i < info->n_items; i++) {
 		const char *k = info->items[i].key;
 		const char *s = info->items[i].value;
-		if (spa_streq(k, "clock.quantum-limit"))
-			spa_atou32(s, &this->quantum_limit, 0);
-		else if (spa_streq(k, "resample.peaks"))
+		if (spa_streq(k, "resample.peaks"))
 			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, "convert.direction")) {
+			if (spa_streq(s, "output"))
+				this->direction = SPA_DIRECTION_OUTPUT;
+			else
+				this->direction = SPA_DIRECTION_INPUT;
+		}
 		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));
+				spa_audio_parse_position(s, strlen(s), this->props.channel_map,
+						&this->props.n_channels);
 		}
 		else if (spa_streq(k, SPA_KEY_PORT_IGNORE_LATENCY))
 			this->port_ignore_latency = spa_atob(s);
@@ -3494,10 +4075,12 @@ impl_init(const struct spa_handle_factory *factory,
 			spa_scnprintf(this->group_name, sizeof(this->group_name), "%s", s);
 		else if (spa_streq(k, "monitor.passthrough"))
 			this->monitor_passthrough = spa_atob(s);
+		else if (spa_streq(k, "audioconvert.filter-graph.disable"))
+			filter_graph_disabled = spa_atob(s);
 		else
 			audioconvert_set_param(this, k, s);
 	}
-
+	this->props.filter_graph_disabled = filter_graph_disabled;
 	this->props.channel.n_volumes = this->props.n_channels;
 	this->props.soft.n_volumes = this->props.n_channels;
 	this->props.monitor.n_volumes = this->props.n_channels;
diff --git a/spa/plugins/audioconvert/benchmark-fmt-ops.c b/spa/plugins/audioconvert/benchmark-fmt-ops.c
index 556c8227..9ea43ec6 100644
--- a/spa/plugins/audioconvert/benchmark-fmt-ops.c
+++ b/spa/plugins/audioconvert/benchmark-fmt-ops.c
@@ -132,6 +132,13 @@ static void test_f32_s16(void)
 		run_testc("test_f32d_s16_2", "avx2", false, true, conv_f32d_to_s16_2_avx2, 2);
 		run_testc("test_f32d_s16_4", "avx2", false, true, conv_f32d_to_s16_4_avx2, 4);
 	}
+#endif
+#if defined (HAVE_RVV)
+	if (cpu_flags & SPA_CPU_FLAG_RISCV_V) {
+		run_test("test_f32_s16", "rvv", true, true, conv_f32_to_s16_rvv);
+		run_test("test_f32d_s16d", "rvv", false, false, conv_f32d_to_s16d_rvv);
+		run_test("test_f32d_s16", "rvv", false, true, conv_f32d_to_s16_rvv);
+	}
 #endif
 	run_test("test_f32_s16d", "c", true, false, conv_f32_to_s16d_c);
 	run_test("test_f32d_s16d", "c", false, false, conv_f32d_to_s16d_c);
@@ -153,6 +160,11 @@ static void test_s16_f32(void)
 		run_test("test_s16_f32d", "avx2", true, false, conv_s16_to_f32d_avx2);
 		run_testc("test_s16_f32d_2", "avx2", true, false, conv_s16_to_f32d_2_avx2, 2);
 	}
+#endif
+#if defined (HAVE_RVV)
+	if (cpu_flags & SPA_CPU_FLAG_RISCV_V) {
+		run_test("test_s16_f32d", "rvv", true, false, conv_s16_to_f32d_rvv);
+	}
 #endif
 	run_test("test_s16d_f32d", "c", false, false, conv_s16d_to_f32d_c);
 }
@@ -170,6 +182,11 @@ static void test_f32_s32(void)
 	if (cpu_flags & SPA_CPU_FLAG_AVX2) {
 		run_test("test_f32d_s32", "avx2", false, true, conv_f32d_to_s32_avx2);
 	}
+#endif
+#if defined (HAVE_RVV)
+	if (cpu_flags & SPA_CPU_FLAG_RISCV_V) {
+		run_test("test_f32d_s32", "rvv", false, true, conv_f32d_to_s32_rvv);
+	}
 #endif
 	run_test("test_f32_s32d", "c", true, false, conv_f32_to_s32d_c);
 	run_test("test_f32d_s32d", "c", false, false, conv_f32d_to_s32d_c);
@@ -188,6 +205,11 @@ static void test_s32_f32(void)
 	if (cpu_flags & SPA_CPU_FLAG_AVX2) {
 		run_test("test_s32_f32d", "avx2", true, false, conv_s32_to_f32d_avx2);
 	}
+#endif
+#if defined (HAVE_RVV)
+	if (cpu_flags & SPA_CPU_FLAG_RISCV_V) {
+		run_test("test_s32_f32d", "rvv", true, false, conv_s32_to_f32d_rvv);
+	}
 #endif
 	run_test("test_s32_f32d", "c", true, false, conv_s32_to_f32d_c);
 	run_test("test_s32d_f32d", "c", false, false, conv_s32d_to_f32d_c);
diff --git a/spa/plugins/audioconvert/biquad.c b/spa/plugins/audioconvert/biquad.c
index 9e8bae5f..d9bb7e3d 100644
--- a/spa/plugins/audioconvert/biquad.c
+++ b/spa/plugins/audioconvert/biquad.c
@@ -8,9 +8,6 @@
  * found in the LICENSE.WEBKIT file.
  */
 
-
-#include <spa/utils/defs.h>
-
 #include <math.h>
 #include "biquad.h"
 
@@ -18,9 +15,8 @@
 #define M_PI 3.14159265358979323846
 #endif
 
-#ifndef M_SQRT2
-#define M_SQRT2 1.41421356237309504880
-#endif
+/* Q = 1 / sqrt(2), also resulting Q value when S = 1 */
+#define BIQUAD_DEFAULT_Q 0.707106781186548
 
 static void set_coefficient(struct biquad *bq, double b0, double b1, double b2,
 			    double a0, double a1, double a2)
@@ -33,79 +29,346 @@ static void set_coefficient(struct biquad *bq, double b0, double b1, double b2,
 	bq->a2 = (float)(a2 * a0_inv);
 }
 
-static void biquad_lowpass(struct biquad *bq, double cutoff)
+static void biquad_lowpass(struct biquad *bq, double cutoff, double Q)
 {
 	/* Limit cutoff to 0 to 1. */
-	cutoff = SPA_CLAMP(cutoff, 0.0, 1.0);
+	cutoff = fmax(0.0, fmin(cutoff, 1.0));
 
-	if (cutoff >= 1.0) {
-		/* When cutoff is 1, the z-transform is 1. */
-		set_coefficient(bq, 1, 0, 0, 1, 0, 0);
-	} else if (cutoff > 0) {
-		/* Compute biquad coefficients for lowpass filter */
-		double theta = M_PI * cutoff;
-		double sn = 0.5 * M_SQRT2 * sin(theta);
-		double beta = 0.5 * (1 - sn) / (1 + sn);
-		double gamma_coeff = (0.5 + beta) * cos(theta);
-		double alpha = 0.25 * (0.5 + beta - gamma_coeff);
-
-		double b0 = 2 * alpha;
-		double b1 = 2 * 2 * alpha;
-		double b2 = 2 * alpha;
-		double a1 = 2 * -gamma_coeff;
-		double a2 = 2 * beta;
-
-		set_coefficient(bq, b0, b1, b2, 1, a1, a2);
-	} else {
-		/* When cutoff is zero, nothing gets through the filter, so set
+	if (cutoff == 1 || cutoff == 0) {
+		/* When cutoff is 1, the z-transform is 1.
+		 * When cutoff is zero, nothing gets through the filter, so set
 		 * coefficients up correctly.
 		 */
-		set_coefficient(bq, 0, 0, 0, 1, 0, 0);
+		set_coefficient(bq, cutoff, 0, 0, 1, 0, 0);
+		return;
 	}
+
+	/* Set Q to a sane default value if not set */
+	if (Q <= 0)
+		Q = BIQUAD_DEFAULT_Q;
+
+	/* Compute biquad coefficients for lowpass filter */
+	/* H(s) = 1 / (s^2 + s/Q + 1) */
+	double w0 = M_PI * cutoff;
+	double alpha = sin(w0) / (2 * Q);
+	double k = cos(w0);
+
+	double b0 = (1 - k) / 2;
+	double b1 = 1 - k;
+	double b2 = (1 - k) / 2;
+	double a0 = 1 + alpha;
+	double a1 = -2 * k;
+	double a2 = 1 - alpha;
+
+	set_coefficient(bq, b0, b1, b2, a0, a1, a2);
 }
 
-static void biquad_highpass(struct biquad *bq, double cutoff)
+static void biquad_highpass(struct biquad *bq, double cutoff, double Q)
 {
 	/* Limit cutoff to 0 to 1. */
-	cutoff = SPA_CLAMP(cutoff, 0.0, 1.0);
+	cutoff = fmax(0.0, fmin(cutoff, 1.0));
 
-	if (cutoff >= 1.0) {
-		/* The z-transform is 0. */
-		set_coefficient(bq, 0, 0, 0, 1, 0, 0);
-	} else if (cutoff > 0) {
-		/* Compute biquad coefficients for highpass filter */
-		double theta = M_PI * cutoff;
-		double sn = 0.5 * M_SQRT2 * sin(theta);
-		double beta = 0.5 * (1 - sn) / (1 + sn);
-		double gamma_coeff = (0.5 + beta) * cos(theta);
-		double alpha = 0.25 * (0.5 + beta + gamma_coeff);
-
-		double b0 = 2 * alpha;
-		double b1 = 2 * -2 * alpha;
-		double b2 = 2 * alpha;
-		double a1 = 2 * -gamma_coeff;
-		double a2 = 2 * beta;
-
-		set_coefficient(bq, b0, b1, b2, 1, a1, a2);
-	} else {
+	if (cutoff == 1 || cutoff == 0) {
+		/* When cutoff is one, the z-transform is 0. */
 		/* When cutoff is zero, we need to be careful because the above
 		 * gives a quadratic divided by the same quadratic, with poles
 		 * and zeros on the unit circle in the same place. When cutoff
 		 * is zero, the z-transform is 1.
 		 */
+		set_coefficient(bq, 1 - cutoff, 0, 0, 1, 0, 0);
+		return;
+	}
+
+	/* Set Q to a sane default value if not set */
+	if (Q <= 0)
+		Q = BIQUAD_DEFAULT_Q;
+
+	/* Compute biquad coefficients for highpass filter */
+	/* H(s) = s^2 / (s^2 + s/Q + 1) */
+	double w0 = M_PI * cutoff;
+	double alpha = sin(w0) / (2 * Q);
+	double k = cos(w0);
+
+	double b0 = (1 + k) / 2;
+	double b1 = -(1 + k);
+	double b2 = (1 + k) / 2;
+	double a0 = 1 + alpha;
+	double a1 = -2 * k;
+	double a2 = 1 - alpha;
+
+	set_coefficient(bq, b0, b1, b2, a0, a1, a2);
+}
+
+static void biquad_bandpass(struct biquad *bq, double frequency, double Q)
+{
+	/* No negative frequencies allowed. */
+	frequency = fmax(0.0, frequency);
+
+	/* Don't let Q go negative, which causes an unstable filter. */
+	Q = fmax(0.0, Q);
+
+	if (frequency <= 0 || frequency >= 1) {
+		/* When the cutoff is zero, the z-transform approaches 0, if Q
+		 * > 0. When both Q and cutoff are zero, the z-transform is
+		 * pretty much undefined. What should we do in this case?
+		 * For now, just make the filter 0. When the cutoff is 1, the
+		 * z-transform also approaches 0.
+		 */
+		set_coefficient(bq, 0, 0, 0, 1, 0, 0);
+		return;
+	}
+	if (Q <= 0) {
+		/* When Q = 0, the above formulas have problems. If we
+		 * look at the z-transform, we can see that the limit
+		 * as Q->0 is 1, so set the filter that way.
+		 */
+		set_coefficient(bq, 1, 0, 0, 1, 0, 0);
+		return;
+	}
+
+	double w0 = M_PI * frequency;
+	double alpha = sin(w0) / (2 * Q);
+	double k = cos(w0);
+
+	double b0 = alpha;
+	double b1 = 0;
+	double b2 = -alpha;
+	double a0 = 1 + alpha;
+	double a1 = -2 * k;
+	double a2 = 1 - alpha;
+
+	set_coefficient(bq, b0, b1, b2, a0, a1, a2);
+}
+
+static void biquad_lowshelf(struct biquad *bq, double frequency, double Q,
+			    double db_gain)
+{
+	/* Clip frequencies to between 0 and 1, inclusive. */
+	frequency = fmax(0.0, fmin(frequency, 1.0));
+
+	double A = pow(10.0, db_gain / 40);
+
+	if (frequency == 1) {
+		/* The z-transform is a constant gain. */
+		set_coefficient(bq, A * A, 0, 0, 1, 0, 0);
+		return;
+	}
+	if (frequency <= 0) {
+		/* When frequency is 0, the z-transform is 1. */
+		set_coefficient(bq, 1, 0, 0, 1, 0, 0);
+		return;
+	}
+
+	/* Set Q to an equivalent value to S = 1 if not specified */
+	if (Q <= 0)
+		Q = BIQUAD_DEFAULT_Q;
+
+	double w0 = M_PI * frequency;
+	double alpha = sin(w0) / (2 * Q);
+	double k = cos(w0);
+	double k2 = 2 * sqrt(A) * alpha;
+	double a_plus_one = A + 1;
+	double a_minus_one = A - 1;
+
+	double b0 = A * (a_plus_one - a_minus_one * k + k2);
+	double b1 = 2 * A * (a_minus_one - a_plus_one * k);
+	double b2 = A * (a_plus_one - a_minus_one * k - k2);
+	double a0 = a_plus_one + a_minus_one * k + k2;
+	double a1 = -2 * (a_minus_one + a_plus_one * k);
+	double a2 = a_plus_one + a_minus_one * k - k2;
+
+	set_coefficient(bq, b0, b1, b2, a0, a1, a2);
+}
+
+static void biquad_highshelf(struct biquad *bq, double frequency, double Q,
+			     double db_gain)
+{
+	/* Clip frequencies to between 0 and 1, inclusive. */
+	frequency = fmax(0.0, fmin(frequency, 1.0));
+
+	double A = pow(10.0, db_gain / 40);
+
+	if (frequency == 1) {
+		/* The z-transform is 1. */
+		set_coefficient(bq, 1, 0, 0, 1, 0, 0);
+		return;
+	}
+	if (frequency <= 0) {
+		/* When frequency = 0, the filter is just a gain, A^2. */
+		set_coefficient(bq, A * A, 0, 0, 1, 0, 0);
+		return;
+	}
+
+	/* Set Q to an equivalent value to S = 1 if not specified */
+	if (Q <= 0)
+		Q = BIQUAD_DEFAULT_Q;
+
+	double w0 = M_PI * frequency;
+	double alpha = sin(w0) / (2 * Q);
+	double k = cos(w0);
+	double k2 = 2 * sqrt(A) * alpha;
+	double a_plus_one = A + 1;
+	double a_minus_one = A - 1;
+
+	double b0 = A * (a_plus_one + a_minus_one * k + k2);
+	double b1 = -2 * A * (a_minus_one + a_plus_one * k);
+	double b2 = A * (a_plus_one + a_minus_one * k - k2);
+	double a0 = a_plus_one - a_minus_one * k + k2;
+	double a1 = 2 * (a_minus_one - a_plus_one * k);
+	double a2 = a_plus_one - a_minus_one * k - k2;
+
+	set_coefficient(bq, b0, b1, b2, a0, a1, a2);
+}
+
+static void biquad_peaking(struct biquad *bq, double frequency, double Q,
+			   double db_gain)
+{
+	/* Clip frequencies to between 0 and 1, inclusive. */
+	frequency = fmax(0.0, fmin(frequency, 1.0));
+
+	/* Don't let Q go negative, which causes an unstable filter. */
+	Q = fmax(0.0, Q);
+
+	double A = pow(10.0, db_gain / 40);
+
+	if (frequency <= 0 || frequency >= 1) {
+		/* When frequency is 0 or 1, the z-transform is 1. */
+		set_coefficient(bq, 1, 0, 0, 1, 0, 0);
+		return;
+	}
+	if (Q <= 0) {
+		/* When Q = 0, the above formulas have problems. If we
+		 * look at the z-transform, we can see that the limit
+		 * as Q->0 is A^2, so set the filter that way.
+		 */
+		set_coefficient(bq, A * A, 0, 0, 1, 0, 0);
+		return;
+	}
+
+	double w0 = M_PI * frequency;
+	double alpha = sin(w0) / (2 * Q);
+	double k = cos(w0);
+
+	double b0 = 1 + alpha * A;
+	double b1 = -2 * k;
+	double b2 = 1 - alpha * A;
+	double a0 = 1 + alpha / A;
+	double a1 = -2 * k;
+	double a2 = 1 - alpha / A;
+
+	set_coefficient(bq, b0, b1, b2, a0, a1, a2);
+}
+
+static void biquad_notch(struct biquad *bq, double frequency, double Q)
+{
+	/* Clip frequencies to between 0 and 1, inclusive. */
+	frequency = fmax(0.0, fmin(frequency, 1.0));
+
+	/* Don't let Q go negative, which causes an unstable filter. */
+	Q = fmax(0.0, Q);
+
+	if (frequency <= 0 || frequency >= 1) {
+		/* When frequency is 0 or 1, the z-transform is 1. */
 		set_coefficient(bq, 1, 0, 0, 1, 0, 0);
+		return;
+	}
+	if (Q <= 0) {
+		/* When Q = 0, the above formulas have problems. If we
+		 * look at the z-transform, we can see that the limit
+		 * as Q->0 is 0, so set the filter that way.
+		 */
+		set_coefficient(bq, 0, 0, 0, 1, 0, 0);
+		return;
 	}
+
+	double w0 = M_PI * frequency;
+	double alpha = sin(w0) / (2 * Q);
+	double k = cos(w0);
+
+	double b0 = 1;
+	double b1 = -2 * k;
+	double b2 = 1;
+	double a0 = 1 + alpha;
+	double a1 = -2 * k;
+	double a2 = 1 - alpha;
+
+	set_coefficient(bq, b0, b1, b2, a0, a1, a2);
+}
+
+static void biquad_allpass(struct biquad *bq, double frequency, double Q)
+{
+	/* Clip frequencies to between 0 and 1, inclusive. */
+	frequency = fmax(0.0, fmin(frequency, 1.0));
+
+	/* Don't let Q go negative, which causes an unstable filter. */
+	Q = fmax(0.0, Q);
+
+	if (frequency <= 0 || frequency >= 1) {
+		/* When frequency is 0 or 1, the z-transform is 1. */
+		set_coefficient(bq, 1, 0, 0, 1, 0, 0);
+		return;
+	}
+
+	if (Q <= 0) {
+		/* When Q = 0, the above formulas have problems. If we
+		 * look at the z-transform, we can see that the limit
+		 * as Q->0 is -1, so set the filter that way.
+		 */
+		set_coefficient(bq, -1, 0, 0, 1, 0, 0);
+		return;
+	}
+
+	double w0 = M_PI * frequency;
+	double alpha = sin(w0) / (2 * Q);
+	double k = cos(w0);
+
+	double b0 = 1 - alpha;
+	double b1 = -2 * k;
+	double b2 = 1 + alpha;
+	double a0 = 1 + alpha;
+	double a1 = -2 * k;
+	double a2 = 1 - alpha;
+
+	set_coefficient(bq, b0, b1, b2, a0, a1, a2);
 }
 
-void biquad_set(struct biquad *bq, enum biquad_type type, double freq)
+void biquad_set(struct biquad *bq, enum biquad_type type, double freq, double Q,
+		double gain)
 {
+	/* Clear history values. */
+	bq->type = type;
+	bq->x1 = 0;
+	bq->x2 = 0;
 
 	switch (type) {
 	case BQ_LOWPASS:
-		biquad_lowpass(bq, freq);
+		biquad_lowpass(bq, freq, Q);
 		break;
 	case BQ_HIGHPASS:
-		biquad_highpass(bq, freq);
+		biquad_highpass(bq, freq, Q);
+		break;
+	case BQ_BANDPASS:
+		biquad_bandpass(bq, freq, Q);
+		break;
+	case BQ_LOWSHELF:
+		biquad_lowshelf(bq, freq, Q, gain);
+		break;
+	case BQ_HIGHSHELF:
+		biquad_highshelf(bq, freq, Q, gain);
+		break;
+	case BQ_PEAKING:
+		biquad_peaking(bq, freq, Q, gain);
+		break;
+	case BQ_NOTCH:
+		biquad_notch(bq, freq, Q);
+		break;
+	case BQ_ALLPASS:
+		biquad_allpass(bq, freq, Q);
+		break;
+	case BQ_NONE:
+	case BQ_RAW:
+		/* Default is an identity filter. */
+		set_coefficient(bq, 1, 0, 0, 1, 0, 0);
 		break;
 	}
 }
diff --git a/spa/plugins/audioconvert/biquad.h b/spa/plugins/audioconvert/biquad.h
index 8b7eccca..3344598e 100644
--- a/spa/plugins/audioconvert/biquad.h
+++ b/spa/plugins/audioconvert/biquad.h
@@ -10,6 +10,20 @@
 extern "C" {
 #endif
 
+/* The type of the biquad filters */
+enum biquad_type {
+	BQ_NONE,
+	BQ_LOWPASS,
+	BQ_HIGHPASS,
+	BQ_BANDPASS,
+	BQ_LOWSHELF,
+	BQ_HIGHSHELF,
+	BQ_PEAKING,
+	BQ_NOTCH,
+	BQ_ALLPASS,
+	BQ_RAW,
+};
+
 /* The biquad filter parameters. The transfer function H(z) is (b0 + b1 * z^(-1)
  * + b2 * z^(-2)) / (1 + a1 * z^(-1) + a2 * z^(-2)).  The previous two inputs
  * are stored in x1 and x2, and the previous two outputs are stored in y1 and
@@ -19,14 +33,10 @@ extern "C" {
  * float is used during the actual filtering for faster computation.
  */
 struct biquad {
+	enum biquad_type type;
 	float b0, b1, b2;
 	float a1, a2;
-};
-
-/* The type of the biquad filters */
-enum biquad_type {
-	BQ_LOWPASS,
-	BQ_HIGHPASS,
+	float x1, x2;
 };
 
 /* Initialize a biquad filter parameters from its type and parameters.
@@ -35,8 +45,11 @@ enum biquad_type {
  *    type - The type of the biquad filter.
  *    frequency - The value should be in the range [0, 1]. It is relative to
  *        half of the sampling rate.
+ *    Q - Quality factor. See Web Audio API for details.
+ *    gain - The value is in dB. See Web Audio API for details.
  */
-void biquad_set(struct biquad *bq, enum biquad_type type, double freq);
+void biquad_set(struct biquad *bq, enum biquad_type type, double freq, double Q,
+		double gain);
 
 #ifdef __cplusplus
 } /* extern "C" */
diff --git a/spa/plugins/audioconvert/channelmix-ops-c.c b/spa/plugins/audioconvert/channelmix-ops-c.c
index 49613e35..2f03df84 100644
--- a/spa/plugins/audioconvert/channelmix-ops-c.c
+++ b/spa/plugins/audioconvert/channelmix-ops-c.c
@@ -2,6 +2,9 @@
 /* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */
 /* SPDX-License-Identifier: MIT */
 
+#include <float.h>
+#include <math.h>
+
 #include "channelmix-ops.h"
 
 static inline void clear_c(float *d, uint32_t n_samples)
@@ -11,7 +14,8 @@ static inline void clear_c(float *d, uint32_t n_samples)
 
 static inline void copy_c(float *d, const float *s, uint32_t n_samples)
 {
-	spa_memcpy(d, s, n_samples * sizeof(float));
+	if (d != s)
+		spa_memcpy(d, s, n_samples * sizeof(float));
 }
 
 static inline void vol_c(float *d, const float *s, float vol, uint32_t n_samples)
@@ -62,6 +66,73 @@ channelmix_copy_c(struct channelmix *mix, void * SPA_RESTRICT dst[],
 		vol_c(d[i], s[i], mix->matrix[i][i], n_samples);
 }
 
+static void lr4_process_c(struct lr4 *lr4, float *dst, const float *src, const float vol, int samples)
+{
+	float x1 = lr4->x1;
+	float x2 = lr4->x2;
+	float y1 = lr4->y1;
+	float y2 = lr4->y2;
+	float b0 = lr4->bq.b0;
+	float b1 = lr4->bq.b1;
+	float b2 = lr4->bq.b2;
+	float a1 = lr4->bq.a1;
+	float a2 = lr4->bq.a2;
+	float x, y, z;
+	int i;
+
+	if (vol == 0.0f || !lr4->active) {
+		vol_c(dst, src, vol, samples);
+		return;
+	}
+
+	for (i = 0; i < samples; i++) {
+		x  = src[i];
+		y  = b0 * x          + x1;
+		x1 = b1 * x - a1 * y + x2;
+		x2 = b2 * x - a2 * y;
+		z  = b0 * y          + y1;
+		y1 = b1 * y - a1 * z + y2;
+		y2 = b2 * y - a2 * z;
+		dst[i] = z * vol;
+	}
+#define F(x) (isnormal(x) ? (x) : 0.0f)
+	lr4->x1 = F(x1);
+	lr4->x2 = F(x2);
+	lr4->y1 = F(y1);
+	lr4->y2 = F(y2);
+#undef F
+}
+
+static inline void delay_convolve_run_c(float *buffer, uint32_t *pos,
+		uint32_t n_buffer, uint32_t delay,
+		const float *taps, uint32_t n_taps,
+		float *dst, const float *src, const float vol, uint32_t n_samples)
+{
+	uint32_t i, j;
+	uint32_t w = *pos;
+	uint32_t o = n_buffer - delay - n_taps-1;
+
+	if (n_taps == 1) {
+		for (i = 0; i < n_samples; i++) {
+			buffer[w] = buffer[w + n_buffer] = src[i];
+			dst[i] = buffer[w + o] * vol;
+			w = w + 1 >= n_buffer ? 0 : w + 1;
+		}
+	} else {
+		for (i = 0; i < n_samples; i++) {
+			float sum = 0.0f;
+
+			buffer[w] = buffer[w + n_buffer] = src[i];
+			for (j = 0; j < n_taps; j++)
+				sum += taps[j] * buffer[w+o+j];
+			dst[i] = sum * vol;
+
+			w = w + 1 >= n_buffer ? 0 : w + 1;
+		}
+	}
+	*pos = w;
+}
+
 #define _M(ch)		(1UL << SPA_AUDIO_CHANNEL_ ## ch)
 
 void
@@ -99,10 +170,10 @@ channelmix_f32_n_m_c(struct channelmix *mix, void * SPA_RESTRICT dst[],
 			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);
+				lr4_process_c(&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);
+				lr4_process_c(&mix->lr4[i], di, di, 1.0f, n_samples);
 			}
 		}
 	}
@@ -199,9 +270,9 @@ channelmix_f32_2_4_c(struct channelmix *mix, void * SPA_RESTRICT dst[],
 		} else {
 			sub_c(d[2], s[0], s[1], n_samples);
 
-			delay_convolve_run(mix->buffer[1], &mix->pos[1], BUFFER_SIZE, mix->delay,
+			delay_convolve_run_c(mix->buffer[1], &mix->pos[1], BUFFER_SIZE, mix->delay,
 					   mix->taps, mix->n_taps, d[3], d[2], -v3, n_samples);
-			delay_convolve_run(mix->buffer[0], &mix->pos[0], BUFFER_SIZE, mix->delay,
+			delay_convolve_run_c(mix->buffer[0], &mix->pos[0], BUFFER_SIZE, mix->delay,
 					   mix->taps, mix->n_taps, d[2], d[2], v2, n_samples);
 		}
 	}
@@ -238,8 +309,8 @@ channelmix_f32_2_3p1_c(struct channelmix *mix, void * SPA_RESTRICT dst[],
 				d[2][n] = c * 0.5f;
 			}
 		}
-		lr4_process(&mix->lr4[3], d[3], d[2], v3, n_samples);
-		lr4_process(&mix->lr4[2], d[2], d[2], v2, n_samples);
+		lr4_process_c(&mix->lr4[3], d[3], d[2], v3, n_samples);
+		lr4_process_c(&mix->lr4[2], d[2], d[2], v2, n_samples);
 	}
 }
 
@@ -267,9 +338,9 @@ channelmix_f32_2_5p1_c(struct channelmix *mix, void * SPA_RESTRICT dst[],
 		} else {
 			sub_c(d[4], s[0], s[1], n_samples);
 
-			delay_convolve_run(mix->buffer[1], &mix->pos[1], BUFFER_SIZE, mix->delay,
+			delay_convolve_run_c(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,
+			delay_convolve_run_c(mix->buffer[0], &mix->pos[0], BUFFER_SIZE, mix->delay,
 					mix->taps, mix->n_taps, d[4], d[4], v4, n_samples);
 		}
 	}
@@ -303,9 +374,9 @@ channelmix_f32_2_7p1_c(struct channelmix *mix, void * SPA_RESTRICT dst[],
 		} else {
 			sub_c(d[6], s[0], s[1], n_samples);
 
-			delay_convolve_run(mix->buffer[1], &mix->pos[1], BUFFER_SIZE, mix->delay,
+			delay_convolve_run_c(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,
+			delay_convolve_run_c(mix->buffer[0], &mix->pos[0], BUFFER_SIZE, mix->delay,
 					mix->taps, mix->n_taps, d[6], d[6], v6, n_samples);
 		}
 	}
diff --git a/spa/plugins/audioconvert/channelmix-ops-sse.c b/spa/plugins/audioconvert/channelmix-ops-sse.c
index 36840286..1058959c 100644
--- a/spa/plugins/audioconvert/channelmix-ops-sse.c
+++ b/spa/plugins/audioconvert/channelmix-ops-sse.c
@@ -5,6 +5,8 @@
 #include "channelmix-ops.h"
 
 #include <xmmintrin.h>
+#include <float.h>
+#include <math.h>
 
 static inline void clear_sse(float *d, uint32_t n_samples)
 {
@@ -149,6 +151,197 @@ void channelmix_copy_sse(struct channelmix *mix, void * SPA_RESTRICT dst[],
 		vol_sse(d[i], s[i], mix->matrix[i][i], n_samples);
 }
 
+static void lr4_process_sse(struct lr4 *lr4, float *dst, const float *src, const float vol, int samples)
+{
+	__m128 x, y, z;
+	__m128 b012;
+	__m128 a12;
+	__m128 x12, y12, v;
+	int i;
+
+	if (vol == 0.0f || !lr4->active) {
+		vol_sse(dst, src, vol, samples);
+		return;
+	}
+
+	b012 = _mm_setr_ps(lr4->bq.b0, lr4->bq.b1, lr4->bq.b2, 0.0f);	/* b0  b1  b2  0 */
+	a12 = _mm_setr_ps(0.0f, lr4->bq.a1, lr4->bq.a2, 0.0f);	  	/* 0   a1  a2  0 */
+	x12 = _mm_setr_ps(lr4->x1, lr4->x2, 0.0f, 0.0f);	  	/* x1  x2  0   0 */
+	y12 = _mm_setr_ps(lr4->y1, lr4->y2, 0.0f, 0.0f);	  	/* y1  y2  0   0 */
+	v = _mm_setr_ps(vol, vol, 0.0f, 0.0f);
+
+	for (i = 0; i < samples; i++) {
+		x = _mm_load1_ps(&src[i]);		/*  x         x         x      x */
+
+		z = _mm_mul_ps(x, b012);				/*  b0*x      b1*x      b2*x   0 */
+		z = _mm_add_ps(z, x12); 				/*  b0*x+x1   b1*x+x2   b2*x   0 */
+		y = _mm_shuffle_ps(z, z, _MM_SHUFFLE(0,0,0,0));	/*  b0*x+x1  b0*x+x1  b0*x+x1  b0*x+x1 = y*/
+		x = _mm_mul_ps(y, a12);			        /*  0        a1*y     a2*y     0 */
+		x = _mm_sub_ps(z, x);	 			/*  y        x1       x2       0 */
+		x12 = _mm_shuffle_ps(x, x, _MM_SHUFFLE(3,3,2,1));    /*  x1  x2  0  0*/
+
+		z = _mm_mul_ps(y, b012);
+		z = _mm_add_ps(z, y12);
+		x = _mm_shuffle_ps(z, z, _MM_SHUFFLE(0,0,0,0));
+		y = _mm_mul_ps(x, a12);
+		y = _mm_sub_ps(z, y);
+		y12 = _mm_shuffle_ps(y, y, _MM_SHUFFLE(3,3,2,1));
+
+		x = _mm_mul_ps(x, v);
+		_mm_store_ss(&dst[i], x);
+	}
+#define F(x) (isnormal(x) ? (x) : 0.0f)
+	lr4->x1 = F(x12[0]);
+	lr4->x2 = F(x12[1]);
+	lr4->y1 = F(y12[0]);
+	lr4->y2 = F(y12[1]);
+#undef F
+}
+
+static void lr4_process_2_sse(struct lr4 *lr40, struct lr4 *lr41, float *dst0, float *dst1,
+		const float *src0, const float *src1, const float vol0, const float vol1, uint32_t samples)
+{
+	__m128 x, y, z;
+	__m128 b0, b1, b2;
+	__m128 a1, a2;
+	__m128 x1, x2;
+	__m128 y1, y2, v;
+	uint32_t i;
+
+	b0 = _mm_setr_ps(lr40->bq.b0, lr41->bq.b0, 0.0f, 0.0f);
+	b1 = _mm_setr_ps(lr40->bq.b1, lr41->bq.b1, 0.0f, 0.0f);
+	b2 = _mm_setr_ps(lr40->bq.b2, lr41->bq.b2, 0.0f, 0.0f);
+	a1 = _mm_setr_ps(lr40->bq.a1, lr41->bq.a1, 0.0f, 0.0f);
+	a2 = _mm_setr_ps(lr40->bq.a2, lr41->bq.a2, 0.0f, 0.0f);
+	x1 = _mm_setr_ps(lr40->x1, lr41->x1, 0.0f, 0.0f);
+	x2 = _mm_setr_ps(lr40->x2, lr41->x2, 0.0f, 0.0f);
+	y1 = _mm_setr_ps(lr40->y1, lr41->y1, 0.0f, 0.0f);
+	y2 = _mm_setr_ps(lr40->y2, lr41->y2, 0.0f, 0.0f);
+	v = _mm_setr_ps(vol0, vol1, 0.0f, 0.0f);
+
+	for (i = 0; i < samples; i++) {
+		x = _mm_setr_ps(src0[i], src1[i], 0.0f, 0.0f);
+
+		y = _mm_mul_ps(x, b0);		/* y = x * b0 */
+		y = _mm_add_ps(y, x1);		/* y = x * b0 + x1*/
+		z = _mm_mul_ps(y, a1);		/* z = a1 * y */
+		x1 = _mm_mul_ps(x, b1);		/* x1 = x * b1 */
+		x1 = _mm_add_ps(x1, x2);	/* x1 = x * b1 + x2*/
+		x1 = _mm_sub_ps(x1, z);		/* x1 = x * b1 + x2 - a1 * y*/
+		z = _mm_mul_ps(y, a2);		/* z = a2 * y */
+		x2 = _mm_mul_ps(x, b2);		/* x2 = x * b2 */
+		x2 = _mm_sub_ps(x2, z);		/* x2 = x * b2 - a2 * y*/
+
+		x = _mm_mul_ps(y, b0);		/* y = x * b0 */
+		x = _mm_add_ps(x, y1);		/* y = x * b0 + x1*/
+		z = _mm_mul_ps(x, a1);		/* z = a1 * y */
+		y1 = _mm_mul_ps(y, b1);		/* x1 = x * b1 */
+		y1 = _mm_add_ps(y1, y2);	/* x1 = x * b1 + x2*/
+		y1 = _mm_sub_ps(y1, z);		/* x1 = x * b1 + x2 - a1 * y*/
+		z = _mm_mul_ps(x, a2);		/* z = a2 * y */
+		y2 = _mm_mul_ps(y, b2);		/* x2 = x * b2 */
+		y2 = _mm_sub_ps(y2, z);		/* x2 = x * b2 - a2 * y*/
+
+		x = _mm_mul_ps(x, v);
+		dst0[i] = x[0];
+		dst1[i] = x[1];
+	}
+#define F(x) (isnormal(x) ? (x) : 0.0f)
+	lr40->x1 = F(x1[0]);
+	lr40->x2 = F(x2[0]);
+	lr40->y1 = F(y1[0]);
+	lr40->y2 = F(y2[0]);
+	lr41->x1 = F(x1[1]);
+	lr41->x2 = F(x2[1]);
+	lr41->y1 = F(y1[1]);
+	lr41->y2 = F(y2[1]);
+#undef F
+}
+
+static inline void convolver_run(const float *src, float *dst,
+		const float *taps, uint32_t n_taps, const __m128 vol)
+{
+	__m128 t[1], sum[4];
+	uint32_t i;
+
+	sum[0] = _mm_setzero_ps();
+	for(i = 0; i < n_taps; i+=4) {
+		t[0] = _mm_loadu_ps(&src[i]);
+		sum[0] = _mm_add_ps(sum[0], _mm_mul_ps(_mm_load_ps(&taps[i]), t[0]));
+	}
+	sum[0] = _mm_add_ps(sum[0], _mm_movehl_ps(sum[0], sum[0]));
+	sum[0] = _mm_add_ss(sum[0], _mm_shuffle_ps(sum[0], sum[0], 0x55));
+	t[0] = _mm_mul_ss(sum[0], vol);
+	_mm_store_ss(dst, t[0]);
+}
+
+static inline void delay_convolve_run_sse(float *buffer, uint32_t *pos,
+		uint32_t n_buffer, uint32_t delay,
+		const float *taps, uint32_t n_taps,
+		float *dst, const float *src, const float vol, uint32_t n_samples)
+{
+	__m128 t[1];
+	const __m128 v = _mm_set1_ps(vol);
+	uint32_t i;
+	uint32_t w = *pos;
+	uint32_t o = n_buffer - delay - n_taps-1;
+	uint32_t n, unrolled;
+
+	if (SPA_IS_ALIGNED(src, 16) &&
+	    SPA_IS_ALIGNED(dst, 16))
+		unrolled = n_samples & ~3;
+	else
+		unrolled = 0;
+
+	if (n_taps == 1) {
+		for(n = 0; n < unrolled; n += 4) {
+			t[0] = _mm_load_ps(&src[n]);
+			_mm_storeu_ps(&buffer[w], t[0]);
+			_mm_storeu_ps(&buffer[w+n_buffer], t[0]);
+			t[0] = _mm_loadu_ps(&buffer[w+o]);
+			t[0] = _mm_mul_ps(t[0], v);
+			_mm_store_ps(&dst[n], t[0]);
+			w += 4;
+			if (w >= n_buffer) {
+				w -= n_buffer;
+				t[0] = _mm_load_ps(&buffer[n_buffer]);
+				_mm_store_ps(&buffer[0], t[0]);
+			}
+		}
+		for(; n < n_samples; n++) {
+			t[0] = _mm_load_ss(&src[n]);
+			_mm_store_ss(&buffer[w], t[0]);
+			_mm_store_ss(&buffer[w+n_buffer], t[0]);
+			t[0] = _mm_load_ss(&buffer[w+o]);
+			t[0] = _mm_mul_ss(t[0], v);
+			_mm_store_ss(&dst[n], t[0]);
+			w = w + 1 >= n_buffer ? 0 : w + 1;
+		}
+	} else {
+		for(n = 0; n < unrolled; n += 4) {
+			t[0] = _mm_load_ps(&src[n]);
+			_mm_storeu_ps(&buffer[w], t[0]);
+			_mm_storeu_ps(&buffer[w+n_buffer], t[0]);
+			for(i = 0; i < 4; i++)
+				convolver_run(&buffer[w+o+i], &dst[n+i], taps, n_taps, v);
+			w += 4;
+			if (w >= n_buffer) {
+				w -= n_buffer;
+				t[0] = _mm_load_ps(&buffer[n_buffer]);
+				_mm_store_ps(&buffer[0], t[0]);
+			}
+		}
+		for(; n < n_samples; n++) {
+			t[0] = _mm_load_ss(&src[n]);
+			_mm_store_ss(&buffer[w], t[0]);
+			_mm_store_ss(&buffer[w+n_buffer], t[0]);
+			convolver_run(&buffer[w+o], &dst[n], taps, n_taps, v);
+			w = w + 1 >= n_buffer ? 0 : w + 1;
+		}
+	}
+	*pos = w;
+}
+
 void
 channelmix_f32_n_m_sse(struct channelmix *mix, void * SPA_RESTRICT dst[],
 		   const void * SPA_RESTRICT src[], uint32_t n_samples)
@@ -172,13 +365,10 @@ channelmix_f32_n_m_sse(struct channelmix *mix, void * SPA_RESTRICT dst[],
 		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);
+			lr4_process_sse(&mix->lr4[i], 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);
+			lr4_process_sse(&mix->lr4[i], di, di, 1.0f, n_samples);
 		}
 	}
 }
@@ -239,8 +429,7 @@ channelmix_f32_2_3p1_sse(struct channelmix *mix, void * SPA_RESTRICT dst[],
 				_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);
+		lr4_process_2_sse(&mix->lr4[3], &mix->lr4[2], d[3], d[2], d[2], d[2], v3, v2, n_samples);
 	}
 }
 
@@ -267,9 +456,9 @@ channelmix_f32_2_5p1_sse(struct channelmix *mix, void * SPA_RESTRICT dst[],
 		} else {
 			sub_sse(d[4], s[0], s[1], n_samples);
 
-			delay_convolve_run(mix->buffer[1], &mix->pos[1], BUFFER_SIZE, mix->delay,
+			delay_convolve_run_sse(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,
+			delay_convolve_run_sse(mix->buffer[0], &mix->pos[0], BUFFER_SIZE, mix->delay,
 					mix->taps, mix->n_taps, d[4], d[4], v4, n_samples);
 		}
 	}
@@ -303,9 +492,9 @@ channelmix_f32_2_7p1_sse(struct channelmix *mix, void * SPA_RESTRICT dst[],
 		} else {
 			sub_sse(d[6], s[0], s[1], n_samples);
 
-			delay_convolve_run(mix->buffer[1], &mix->pos[1], BUFFER_SIZE, mix->delay,
+			delay_convolve_run_sse(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,
+			delay_convolve_run_sse(mix->buffer[0], &mix->pos[0], BUFFER_SIZE, mix->delay,
 					mix->taps, mix->n_taps, d[6], d[6], v6, n_samples);
 		}
 	}
diff --git a/spa/plugins/audioconvert/channelmix-ops.c b/spa/plugins/audioconvert/channelmix-ops.c
index 86cd049f..d774112b 100644
--- a/spa/plugins/audioconvert/channelmix-ops.c
+++ b/spa/plugins/audioconvert/channelmix-ops.c
@@ -673,7 +673,7 @@ done:
 			spa_log_info(mix->log, "channel %d is FC cutoff:%f", ic, mix->fc_cutoff);
 			lr4_set(&mix->lr4[ic], BQ_LOWPASS, mix->fc_cutoff / mix->freq);
 		} else {
-			mix->lr4[ic].active = false;
+			lr4_set(&mix->lr4[ic], BQ_NONE, mix->fc_cutoff / mix->freq);
 		}
 		ic++;
 	}
@@ -775,16 +775,25 @@ int channelmix_init(struct channelmix *mix)
 	mix->delay = (uint32_t)(mix->rear_delay * mix->freq / 1000.0f);
 	mix->func_name = info->name;
 
-	spa_log_debug(mix->log, "selected %s delay:%d options:%08x", info->name, mix->delay,
-			mix->options);
+	spa_zero(mix->taps_mem);
+	mix->taps = SPA_PTR_ALIGN(mix->taps_mem, CHANNELMIX_OPS_MAX_ALIGN, float);
+	mix->buffer[0] = SPA_PTR_ALIGN(&mix->buffer_mem[0], CHANNELMIX_OPS_MAX_ALIGN, float);
+	mix->buffer[1] = SPA_PTR_ALIGN(&mix->buffer_mem[2*BUFFER_SIZE], CHANNELMIX_OPS_MAX_ALIGN, float);
 
 	if (mix->hilbert_taps > 0) {
-		mix->n_taps = SPA_CLAMP(mix->hilbert_taps, 15u, 255u) | 1;
+		mix->n_taps = SPA_CLAMP(mix->hilbert_taps, 15u, MAX_TAPS) | 1;
 		blackman_window(mix->taps, mix->n_taps);
 		hilbert_generate(mix->taps, mix->n_taps);
+		reverse_taps(mix->taps, mix->n_taps);
 	} else {
 		mix->n_taps = 1;
 		mix->taps[0] = 1.0f;
 	}
+	if (mix->delay + mix->n_taps > BUFFER_SIZE)
+		mix->delay = BUFFER_SIZE - mix->n_taps;
+
+	spa_log_debug(mix->log, "selected %s delay:%d options:%08x", info->name, mix->delay,
+			mix->options);
+
 	return make_matrix(mix);
 }
diff --git a/spa/plugins/audioconvert/channelmix-ops.h b/spa/plugins/audioconvert/channelmix-ops.h
index 8fb2e005..26e2efc3 100644
--- a/spa/plugins/audioconvert/channelmix-ops.h
+++ b/spa/plugins/audioconvert/channelmix-ops.h
@@ -10,7 +10,6 @@
 #include <spa/param/audio/raw.h>
 
 #include "crossover.h"
-#include "delay.h"
 
 #define VOLUME_MIN 0.0f
 #define VOLUME_NORM 1.0f
@@ -24,7 +23,9 @@
 #define MASK_7_1	_M(FL)|_M(FR)|_M(FC)|_M(LFE)|_M(SL)|_M(SR)|_M(RL)|_M(RR)
 
 #define BUFFER_SIZE 4096
-#define MAX_TAPS 255
+#define MAX_TAPS 255u
+
+#define CHANNELMIX_OPS_MAX_ALIGN 16
 
 struct channelmix {
 	uint32_t src_chan;
@@ -60,10 +61,12 @@ struct channelmix {
 	uint32_t hilbert_taps;				/* to phase shift, 0 disabled */
 	struct lr4 lr4[SPA_AUDIO_MAX_CHANNELS];
 
-	float buffer[2][BUFFER_SIZE];
+	float buffer_mem[2 * BUFFER_SIZE*2 + CHANNELMIX_OPS_MAX_ALIGN/4];
+	float *buffer[2];
 	uint32_t pos[2];
 	uint32_t delay;
-	float taps[MAX_TAPS];
+	float taps_mem[MAX_TAPS + CHANNELMIX_OPS_MAX_ALIGN/4];
+	float *taps;
 	uint32_t n_taps;
 
 	void (*process) (struct channelmix *mix, void * SPA_RESTRICT dst[],
@@ -105,8 +108,6 @@ void channelmix_##name##_##arch(struct channelmix *mix,				\
 		void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],	\
 		uint32_t n_samples);
 
-#define CHANNELMIX_OPS_MAX_ALIGN 16
-
 DEFINE_FUNCTION(copy, c);
 DEFINE_FUNCTION(f32_n_m, c);
 DEFINE_FUNCTION(f32_1_2, c);
diff --git a/spa/plugins/audioconvert/crossover.c b/spa/plugins/audioconvert/crossover.c
index 177cf610..537d4937 100644
--- a/spa/plugins/audioconvert/crossover.c
+++ b/spa/plugins/audioconvert/crossover.c
@@ -10,55 +10,10 @@
 
 void lr4_set(struct lr4 *lr4, enum biquad_type type, float freq)
 {
-	biquad_set(&lr4->bq, type, freq);
+	biquad_set(&lr4->bq, type, freq, 0, 0);
 	lr4->x1 = 0;
 	lr4->x2 = 0;
 	lr4->y1 = 0;
 	lr4->y2 = 0;
-	lr4->z1 = 0;
-	lr4->z2 = 0;
-	lr4->active = true;
-}
-
-void lr4_process(struct lr4 *lr4, float *dst, const float *src, const float vol, int samples)
-{
-	float x1 = lr4->x1;
-	float x2 = lr4->x2;
-	float y1 = lr4->y1;
-	float y2 = lr4->y2;
-	float b0 = lr4->bq.b0;
-	float b1 = lr4->bq.b1;
-	float b2 = lr4->bq.b2;
-	float a1 = lr4->bq.a1;
-	float a2 = lr4->bq.a2;
-	float x, y, z;
-	int i;
-
-	if (vol == 0.0f) {
-		memset(dst, 0, samples * sizeof(float));
-		return;
-	} else if (!lr4->active) {
-		if (src != dst || vol != 1.0f) {
-			for (i = 0; i < samples; i++)
-				dst[i] = src[i] * vol;
-		}
-		return;
-	}
-
-	for (i = 0; i < samples; i++) {
-		x  = src[i];
-		y  = b0 * x          + x1;
-		x1 = b1 * x - a1 * y + x2;
-		x2 = b2 * x - a2 * y;
-		z  = b0 * y          + y1;
-		y1 = b1 * y - a1 * z + y2;
-		y2 = b2 * y - a2 * z;
-		dst[i] = z * vol;
-	}
-#define F(x) (-FLT_MIN < (x) && (x) < FLT_MIN ? 0.0f : (x))
-	lr4->x1 = F(x1);
-	lr4->x2 = F(x2);
-	lr4->y1 = F(y1);
-	lr4->y2 = F(y2);
-#undef F
+	lr4->active = type != BQ_NONE;
 }
diff --git a/spa/plugins/audioconvert/crossover.h b/spa/plugins/audioconvert/crossover.h
index b6f458ba..159d9590 100644
--- a/spa/plugins/audioconvert/crossover.h
+++ b/spa/plugins/audioconvert/crossover.h
@@ -20,12 +20,9 @@ struct lr4 {
 	struct biquad bq;
 	float x1, x2;
 	float y1, y2;
-	float z1, z2;
 	bool active;
 };
 
 void lr4_set(struct lr4 *lr4, enum biquad_type type, float freq);
 
-void lr4_process(struct lr4 *lr4, float *dst, const float *src, const float vol, int samples);
-
 #endif /* CROSSOVER_H_ */
diff --git a/spa/plugins/audioconvert/delay.h b/spa/plugins/audioconvert/delay.h
deleted file mode 100644
index 61b028bc..00000000
--- a/spa/plugins/audioconvert/delay.h
+++ /dev/null
@@ -1,52 +0,0 @@
-/* Spa */
-/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */
-/* SPDX-License-Identifier: MIT */
-
-#ifndef DELAY_H
-#define DELAY_H
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-static inline void delay_run(float *buffer, uint32_t *pos,
-		uint32_t n_buffer, uint32_t delay,
-		float *dst, const float *src, const float vol, uint32_t n_samples)
-{
-	uint32_t i;
-	uint32_t p = *pos;
-
-	for (i = 0; i < n_samples; i++) {
-		buffer[p] = src[i];
-		dst[i] = buffer[(p - delay) & (n_buffer-1)] * vol;
-		p = (p + 1) & (n_buffer-1);
-	}
-	*pos = p;
-}
-
-static inline void delay_convolve_run(float *buffer, uint32_t *pos,
-		uint32_t n_buffer, uint32_t delay,
-		const float *taps, uint32_t n_taps,
-		float *dst, const float *src, const float vol, uint32_t n_samples)
-{
-	uint32_t i, j;
-	uint32_t p = *pos;
-
-	for (i = 0; i < n_samples; i++) {
-		float sum = 0.0f;
-
-		buffer[p] = src[i];
-		for (j = 0; j < n_taps; j++)
-			sum += (taps[j] * buffer[((p - delay) - j) & (n_buffer-1)]);
-		dst[i] = sum * vol;
-
-		p = (p + 1) & (n_buffer-1);
-	}
-	*pos = p;
-}
-
-#ifdef __cplusplus
-}
-#endif
-
-#endif /* DELAY_H */
diff --git a/spa/plugins/audioconvert/fmt-ops-avx2.c b/spa/plugins/audioconvert/fmt-ops-avx2.c
index 69970d6e..a939da45 100644
--- a/spa/plugins/audioconvert/fmt-ops-avx2.c
+++ b/spa/plugins/audioconvert/fmt-ops-avx2.c
@@ -23,6 +23,13 @@
 #define _MM_CLAMP_SS(r,min,max)				\
 	_mm_min_ss(_mm_max_ss(r, min), max)
 
+#define _MM256_BSWAP_EPI16(x)						\
+({									\
+	_mm256_or_si256(						\
+		_mm256_slli_epi16(x, 8),				\
+		_mm256_srli_epi16(x, 8));				\
+})
+
 static void
 conv_s16_to_f32d_1s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src,
 		uint32_t n_channels, uint32_t n_samples)
@@ -74,6 +81,59 @@ conv_s16_to_f32d_avx2(struct convert *conv, void * SPA_RESTRICT dst[], const voi
 		conv_s16_to_f32d_1s_avx2(conv, &dst[i], &s[i], n_channels, n_samples);
 }
 
+
+static void
+conv_s16s_to_f32d_1s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src,
+		uint32_t n_channels, uint32_t n_samples)
+{
+	const uint16_t *s = src;
+	float *d0 = dst[0];
+	uint32_t n, unrolled;
+	__m256i in = _mm256_setzero_si256();
+	__m256 out, factor = _mm256_set1_ps(1.0f / S16_SCALE);
+
+	if (SPA_LIKELY(SPA_IS_ALIGNED(d0, 32)))
+		unrolled = n_samples & ~7;
+	else
+		unrolled = 0;
+
+	for(n = 0; n < unrolled; n += 8) {
+		in = _mm256_insert_epi16(in, s[0*n_channels],  1);
+		in = _mm256_insert_epi16(in, s[1*n_channels],  3);
+		in = _mm256_insert_epi16(in, s[2*n_channels],  5);
+		in = _mm256_insert_epi16(in, s[3*n_channels],  7);
+		in = _mm256_insert_epi16(in, s[4*n_channels],  9);
+		in = _mm256_insert_epi16(in, s[5*n_channels], 11);
+		in = _mm256_insert_epi16(in, s[6*n_channels], 13);
+		in = _mm256_insert_epi16(in, s[7*n_channels], 15);
+		in = _MM256_BSWAP_EPI16(in);
+
+		in = _mm256_srai_epi32(in, 16);
+		out = _mm256_cvtepi32_ps(in);
+		out = _mm256_mul_ps(out, factor);
+		_mm256_store_ps(&d0[n], out);
+		s += 8*n_channels;
+	}
+	for(; n < n_samples; n++) {
+		__m128 out, factor = _mm_set1_ps(1.0f / S16_SCALE);
+		out = _mm_cvtsi32_ss(factor, (int16_t)bswap_16(s[0]));
+		out = _mm_mul_ss(out, factor);
+		_mm_store_ss(&d0[n], out);
+		s += n_channels;
+	}
+}
+
+void
+conv_s16s_to_f32d_avx2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
+		uint32_t n_samples)
+{
+	const uint16_t *s = src[0];
+	uint32_t i = 0, n_channels = conv->n_channels;
+
+	for(; i < n_channels; i++)
+		conv_s16s_to_f32d_1s_avx2(conv, &dst[i], &s[i], n_channels, n_samples);
+}
+
 void
 conv_s16_to_f32d_2_avx2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
 		uint32_t n_samples)
@@ -132,6 +192,66 @@ conv_s16_to_f32d_2_avx2(struct convert *conv, void * SPA_RESTRICT dst[], const v
 	}
 }
 
+void
+conv_s16s_to_f32d_2_avx2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
+		uint32_t n_samples)
+{
+	const uint16_t *s = src[0];
+	float *d0 = dst[0], *d1 = dst[1];
+	uint32_t n, unrolled;
+	__m256i in[2], t[4];
+	__m256 out[4], factor = _mm256_set1_ps(1.0f / S16_SCALE);
+
+	if (SPA_IS_ALIGNED(s, 32) &&
+	    SPA_IS_ALIGNED(d0, 32) &&
+	    SPA_IS_ALIGNED(d1, 32))
+		unrolled = n_samples & ~15;
+	else
+		unrolled = 0;
+
+	for(n = 0; n < unrolled; n += 16) {
+		in[0] = _mm256_load_si256((__m256i*)(s + 0));
+		in[1] = _mm256_load_si256((__m256i*)(s + 16));
+		in[0] = _MM256_BSWAP_EPI16(in[0]);
+		in[1] = _MM256_BSWAP_EPI16(in[1]);
+
+		t[0] = _mm256_slli_epi32(in[0], 16);
+		t[0] = _mm256_srai_epi32(t[0], 16);
+		out[0] = _mm256_cvtepi32_ps(t[0]);
+		out[0] = _mm256_mul_ps(out[0], factor);
+
+		t[1] = _mm256_srai_epi32(in[0], 16);
+		out[1] = _mm256_cvtepi32_ps(t[1]);
+		out[1] = _mm256_mul_ps(out[1], factor);
+
+		t[2] = _mm256_slli_epi32(in[1], 16);
+		t[2] = _mm256_srai_epi32(t[2], 16);
+		out[2] = _mm256_cvtepi32_ps(t[2]);
+		out[2] = _mm256_mul_ps(out[2], factor);
+
+		t[3] = _mm256_srai_epi32(in[1], 16);
+		out[3] = _mm256_cvtepi32_ps(t[3]);
+		out[3] = _mm256_mul_ps(out[3], factor);
+
+		_mm256_store_ps(&d0[n + 0], out[0]);
+		_mm256_store_ps(&d1[n + 0], out[1]);
+		_mm256_store_ps(&d0[n + 8], out[2]);
+		_mm256_store_ps(&d1[n + 8], out[3]);
+
+		s += 32;
+	}
+	for(; n < n_samples; n++) {
+		__m128 out[4], factor = _mm_set1_ps(1.0f / S16_SCALE);
+		out[0] = _mm_cvtsi32_ss(factor, (int16_t)bswap_16(s[0]));
+		out[0] = _mm_mul_ss(out[0], factor);
+		out[1] = _mm_cvtsi32_ss(factor, (int16_t)bswap_16(s[1]));
+		out[1] = _mm_mul_ss(out[1], factor);
+		_mm_store_ss(&d0[n], out[0]);
+		_mm_store_ss(&d1[n], out[1]);
+		s += 2;
+	}
+}
+
 static void
 conv_s24_to_f32d_1s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src,
 		uint32_t n_channels, uint32_t n_samples)
@@ -316,7 +436,7 @@ conv_s32_to_f32d_4s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA
 	float *d0 = dst[0], *d1 = dst[1], *d2 = dst[2], *d3 = dst[3];
 	uint32_t n, unrolled;
 	__m256i in[4];
-	__m256 out[4], factor = _mm256_set1_ps(1.0f / S24_SCALE);
+	__m256 out[4], factor = _mm256_set1_ps(1.0f / S32_SCALE_I2F);
 	__m256i mask1 = _mm256_setr_epi32(0*n_channels, 1*n_channels, 2*n_channels, 3*n_channels,
 					  4*n_channels, 5*n_channels, 6*n_channels, 7*n_channels);
 
@@ -334,11 +454,6 @@ conv_s32_to_f32d_4s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA
 		in[2] = _mm256_i32gather_epi32((int*)&s[2], mask1, 4);
 		in[3] = _mm256_i32gather_epi32((int*)&s[3], mask1, 4);
 
-		in[0] = _mm256_srai_epi32(in[0], 8);
-		in[1] = _mm256_srai_epi32(in[1], 8);
-		in[2] = _mm256_srai_epi32(in[2], 8);
-		in[3] = _mm256_srai_epi32(in[3], 8);
-
 		out[0] = _mm256_cvtepi32_ps(in[0]);
 		out[1] = _mm256_cvtepi32_ps(in[1]);
 		out[2] = _mm256_cvtepi32_ps(in[2]);
@@ -357,11 +472,11 @@ conv_s32_to_f32d_4s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA
 		s += 8*n_channels;
 	}
 	for(; n < n_samples; n++) {
-		__m128 out[4], factor = _mm_set1_ps(1.0f / S24_SCALE);
-		out[0] = _mm_cvtsi32_ss(factor, s[0] >> 8);
-		out[1] = _mm_cvtsi32_ss(factor, s[1] >> 8);
-		out[2] = _mm_cvtsi32_ss(factor, s[2] >> 8);
-		out[3] = _mm_cvtsi32_ss(factor, s[3] >> 8);
+		__m128 out[4], factor = _mm_set1_ps(1.0f / S32_SCALE_I2F);
+		out[0] = _mm_cvtsi32_ss(factor, s[0]);
+		out[1] = _mm_cvtsi32_ss(factor, s[1]);
+		out[2] = _mm_cvtsi32_ss(factor, s[2]);
+		out[3] = _mm_cvtsi32_ss(factor, s[3]);
 		out[0] = _mm_mul_ss(out[0], factor);
 		out[1] = _mm_mul_ss(out[1], factor);
 		out[2] = _mm_mul_ss(out[2], factor);
@@ -382,7 +497,7 @@ conv_s32_to_f32d_2s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA
 	float *d0 = dst[0], *d1 = dst[1];
 	uint32_t n, unrolled;
 	__m256i in[4];
-	__m256 out[4], factor = _mm256_set1_ps(1.0f / S24_SCALE);
+	__m256 out[4], factor = _mm256_set1_ps(1.0f / S32_SCALE_I2F);
 	__m256i mask1 = _mm256_setr_epi32(0*n_channels, 1*n_channels, 2*n_channels, 3*n_channels,
 					  4*n_channels, 5*n_channels, 6*n_channels, 7*n_channels);
 
@@ -396,9 +511,6 @@ conv_s32_to_f32d_2s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA
 		in[0] = _mm256_i32gather_epi32((int*)&s[0], mask1, 4);
 		in[1] = _mm256_i32gather_epi32((int*)&s[1], mask1, 4);
 
-		in[0] = _mm256_srai_epi32(in[0], 8);
-		in[1] = _mm256_srai_epi32(in[1], 8);
-
 		out[0] = _mm256_cvtepi32_ps(in[0]);
 		out[1] = _mm256_cvtepi32_ps(in[1]);
 
@@ -411,9 +523,9 @@ conv_s32_to_f32d_2s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA
 		s += 8*n_channels;
 	}
 	for(; n < n_samples; n++) {
-		__m128 out[2], factor = _mm_set1_ps(1.0f / S24_SCALE);
-		out[0] = _mm_cvtsi32_ss(factor, s[0] >> 8);
-		out[1] = _mm_cvtsi32_ss(factor, s[1] >> 8);
+		__m128 out[2], factor = _mm_set1_ps(1.0f / S32_SCALE_I2F);
+		out[0] = _mm_cvtsi32_ss(factor, s[0]);
+		out[1] = _mm_cvtsi32_ss(factor, s[1]);
 		out[0] = _mm_mul_ss(out[0], factor);
 		out[1] = _mm_mul_ss(out[1], factor);
 		_mm_store_ss(&d0[n], out[0]);
@@ -430,7 +542,7 @@ conv_s32_to_f32d_1s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA
 	float *d0 = dst[0];
 	uint32_t n, unrolled;
 	__m256i in[2];
-	__m256 out[2], factor = _mm256_set1_ps(1.0f / S24_SCALE);
+	__m256 out[2], factor = _mm256_set1_ps(1.0f / S32_SCALE_I2F);
 	__m256i mask1 = _mm256_setr_epi32(0*n_channels, 1*n_channels, 2*n_channels, 3*n_channels,
 					  4*n_channels, 5*n_channels, 6*n_channels, 7*n_channels);
 
@@ -443,9 +555,6 @@ conv_s32_to_f32d_1s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA
 		in[0] = _mm256_i32gather_epi32(&s[0*n_channels], mask1, 4);
 		in[1] = _mm256_i32gather_epi32(&s[8*n_channels], mask1, 4);
 
-		in[0] = _mm256_srai_epi32(in[0], 8);
-		in[1] = _mm256_srai_epi32(in[1], 8);
-
 		out[0] = _mm256_cvtepi32_ps(in[0]);
 		out[1] = _mm256_cvtepi32_ps(in[1]);
 
@@ -458,8 +567,8 @@ conv_s32_to_f32d_1s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA
 		s += 16*n_channels;
 	}
 	for(; n < n_samples; n++) {
-		__m128 out, factor = _mm_set1_ps(1.0f / S24_SCALE);
-		out = _mm_cvtsi32_ss(factor, s[0] >> 8);
+		__m128 out, factor = _mm_set1_ps(1.0f / S32_SCALE_I2F);
+		out = _mm_cvtsi32_ss(factor, s[0]);
 		out = _mm_mul_ss(out, factor);
 		_mm_store_ss(&d0[n], out);
 		s += n_channels;
@@ -490,9 +599,9 @@ conv_f32d_to_s32_1s_avx2(void *data, void * SPA_RESTRICT dst, const void * SPA_R
 	uint32_t n, unrolled;
 	__m128 in[1];
 	__m128i out[4];
-	__m128 scale = _mm_set1_ps(S24_SCALE);
-	__m128 int_max = _mm_set1_ps(S24_MAX);
-	__m128 int_min = _mm_set1_ps(S24_MIN);
+	__m128 scale = _mm_set1_ps(S32_SCALE_F2I);
+	__m128 int_min = _mm_set1_ps(S32_MIN_F2I);
+	__m128 int_max = _mm_set1_ps(S32_MAX_F2I);
 
 	if (SPA_IS_ALIGNED(s0, 16))
 		unrolled = n_samples & ~3;
@@ -503,7 +612,6 @@ conv_f32d_to_s32_1s_avx2(void *data, void * SPA_RESTRICT dst, const void * SPA_R
 		in[0] = _mm_mul_ps(_mm_load_ps(&s0[n]), scale);
 		in[0] = _MM_CLAMP_PS(in[0], int_min, int_max);
 		out[0] = _mm_cvtps_epi32(in[0]);
-		out[0] = _mm_slli_epi32(out[0], 8);
 		out[1] = _mm_shuffle_epi32(out[0], _MM_SHUFFLE(0, 3, 2, 1));
 		out[2] = _mm_shuffle_epi32(out[0], _MM_SHUFFLE(1, 0, 3, 2));
 		out[3] = _mm_shuffle_epi32(out[0], _MM_SHUFFLE(2, 1, 0, 3));
@@ -518,7 +626,7 @@ conv_f32d_to_s32_1s_avx2(void *data, void * SPA_RESTRICT dst, const void * SPA_R
 		in[0] = _mm_load_ss(&s0[n]);
 		in[0] = _mm_mul_ss(in[0], scale);
 		in[0] = _MM_CLAMP_SS(in[0], int_min, int_max);
-		*d = _mm_cvtss_si32(in[0]) << 8;
+		*d = _mm_cvtss_si32(in[0]);
 		d += n_channels;
 	}
 }
@@ -538,12 +646,12 @@ conv_f32d_to_s32_2s_avx2(void *data, void * SPA_RESTRICT dst, const void * SPA_R
 	uint32_t n, unrolled;
 	__m256 in[2];
 	__m256i out[2], t[2];
-	__m256 scale = _mm256_set1_ps(S24_SCALE);
-	__m256 int_min = _mm256_set1_ps(S24_MIN);
-	__m256 int_max = _mm256_set1_ps(S24_MAX);
+	__m256 scale = _mm256_set1_ps(S32_SCALE_F2I);
+	__m256 int_min = _mm256_set1_ps(S32_MIN_F2I);
+	__m256 int_max = _mm256_set1_ps(S32_MAX_F2I);
 
 	if (SPA_IS_ALIGNED(s0, 32) &&
-	    SPA_IS_ALIGNED(s1, 32))
+		SPA_IS_ALIGNED(s1, 32))
 		unrolled = n_samples & ~7;
 	else
 		unrolled = 0;
@@ -557,8 +665,6 @@ conv_f32d_to_s32_2s_avx2(void *data, void * SPA_RESTRICT dst, const void * SPA_R
 
 		out[0] = _mm256_cvtps_epi32(in[0]);	/* a0 a1 a2 a3 a4 a5 a6 a7 */
 		out[1] = _mm256_cvtps_epi32(in[1]);	/* b0 b1 b2 b3 b4 b5 b6 b7 */
-		out[0] = _mm256_slli_epi32(out[0], 8);
-		out[1] = _mm256_slli_epi32(out[1], 8);
 
 		t[0] = _mm256_unpacklo_epi32(out[0], out[1]); /* a0 b0 a1 b1 a4 b4 a5 b5 */
 		t[1] = _mm256_unpackhi_epi32(out[0], out[1]); /* a2 b2 a3 b3 a6 b6 a7 b7 */
@@ -587,9 +693,9 @@ conv_f32d_to_s32_2s_avx2(void *data, void * SPA_RESTRICT dst, const void * SPA_R
 	for(; n < n_samples; n++) {
 		__m128 in[2];
 		__m128i out[2];
-		__m128 scale = _mm_set1_ps(S24_SCALE);
-		__m128 int_min = _mm_set1_ps(S24_MIN);
-		__m128 int_max = _mm_set1_ps(S24_MAX);
+		__m128 scale = _mm_set1_ps(S32_SCALE_F2I);
+		__m128 int_min = _mm_set1_ps(S32_MIN_F2I);
+		__m128 int_max = _mm_set1_ps(S32_MAX_F2I);
 
 		in[0] = _mm_load_ss(&s0[n]);
 		in[1] = _mm_load_ss(&s1[n]);
@@ -599,7 +705,6 @@ conv_f32d_to_s32_2s_avx2(void *data, void * SPA_RESTRICT dst, const void * SPA_R
 		in[0] = _mm_mul_ps(in[0], scale);
 		in[0] = _MM_CLAMP_PS(in[0], int_min, int_max);
 		out[0] = _mm_cvtps_epi32(in[0]);
-		out[0] = _mm_slli_epi32(out[0], 8);
 		_mm_storel_epi64((__m128i*)d, out[0]);
 		d += n_channels;
 	}
@@ -614,14 +719,14 @@ conv_f32d_to_s32_4s_avx2(void *data, void * SPA_RESTRICT dst, const void * SPA_R
 	uint32_t n, unrolled;
 	__m256 in[4];
 	__m256i out[4], t[4];
-	__m256 scale = _mm256_set1_ps(S24_SCALE);
-	__m256 int_min = _mm256_set1_ps(S24_MIN);
-	__m256 int_max = _mm256_set1_ps(S24_MAX);
+	__m256 scale = _mm256_set1_ps(S32_SCALE_F2I);
+	__m256 int_min = _mm256_set1_ps(S32_MIN_F2I);
+	__m256 int_max = _mm256_set1_ps(S32_MAX_F2I);
 
 	if (SPA_IS_ALIGNED(s0, 32) &&
-	    SPA_IS_ALIGNED(s1, 32) &&
-	    SPA_IS_ALIGNED(s2, 32) &&
-	    SPA_IS_ALIGNED(s3, 32))
+		SPA_IS_ALIGNED(s1, 32) &&
+		SPA_IS_ALIGNED(s2, 32) &&
+		SPA_IS_ALIGNED(s3, 32))
 		unrolled = n_samples & ~7;
 	else
 		unrolled = 0;
@@ -641,10 +746,6 @@ conv_f32d_to_s32_4s_avx2(void *data, void * SPA_RESTRICT dst, const void * SPA_R
 		out[1] = _mm256_cvtps_epi32(in[1]); /* b0 b1 b2 b3 b4 b5 b6 b7 */
 		out[2] = _mm256_cvtps_epi32(in[2]); /* c0 c1 c2 c3 c4 c5 c6 c7 */
 		out[3] = _mm256_cvtps_epi32(in[3]); /* d0 d1 d2 d3 d4 d5 d6 d7 */
-		out[0] = _mm256_slli_epi32(out[0], 8);
-		out[1] = _mm256_slli_epi32(out[1], 8);
-		out[2] = _mm256_slli_epi32(out[2], 8);
-		out[3] = _mm256_slli_epi32(out[3], 8);
 
 		t[0] = _mm256_unpacklo_epi32(out[0], out[1]); /* a0 b0 a1 b1 a4 b4 a5 b5 */
 		t[1] = _mm256_unpackhi_epi32(out[0], out[1]); /* a2 b2 a3 b3 a6 b6 a7 b7 */
@@ -669,9 +770,9 @@ conv_f32d_to_s32_4s_avx2(void *data, void * SPA_RESTRICT dst, const void * SPA_R
 	for(; n < n_samples; n++) {
 		__m128 in[4];
 		__m128i out[4];
-		__m128 scale = _mm_set1_ps(S24_SCALE);
-		__m128 int_min = _mm_set1_ps(S24_MIN);
-		__m128 int_max = _mm_set1_ps(S24_MAX);
+		__m128 scale = _mm_set1_ps(S32_SCALE_F2I);
+		__m128 int_min = _mm_set1_ps(S32_MIN_F2I);
+		__m128 int_max = _mm_set1_ps(S32_MAX_F2I);
 
 		in[0] = _mm_load_ss(&s0[n]);
 		in[1] = _mm_load_ss(&s1[n]);
@@ -685,7 +786,6 @@ conv_f32d_to_s32_4s_avx2(void *data, void * SPA_RESTRICT dst, const void * SPA_R
 		in[0] = _mm_mul_ps(in[0], scale);
 		in[0] = _MM_CLAMP_PS(in[0], int_min, int_max);
 		out[0] = _mm_cvtps_epi32(in[0]);
-		out[0] = _mm_slli_epi32(out[0], 8);
 		_mm_storeu_si128((__m128i*)d, out[0]);
 		d += n_channels;
 	}
@@ -1026,3 +1126,62 @@ conv_f32d_to_s16_2_avx2(struct convert *conv, void * SPA_RESTRICT dst[], const v
 		d += 2;
 	}
 }
+
+void
+conv_f32d_to_s16s_2_avx2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
+		uint32_t n_samples)
+{
+	const float *s0 = src[0], *s1 = src[1];
+	uint16_t *d = dst[0];
+	uint32_t n, unrolled;
+	__m256 in[4];
+	__m256i out[4], t[4];
+	__m256 int_scale = _mm256_set1_ps(S16_SCALE);
+
+	if (SPA_IS_ALIGNED(s0, 32) &&
+	    SPA_IS_ALIGNED(s1, 32))
+		unrolled = n_samples & ~15;
+	else
+		unrolled = 0;
+
+	for(n = 0; n < unrolled; n += 16) {
+		in[0] = _mm256_mul_ps(_mm256_load_ps(&s0[n+0]), int_scale);
+		in[1] = _mm256_mul_ps(_mm256_load_ps(&s1[n+0]), int_scale);
+		in[2] = _mm256_mul_ps(_mm256_load_ps(&s0[n+8]), int_scale);
+		in[3] = _mm256_mul_ps(_mm256_load_ps(&s1[n+8]), int_scale);
+
+		out[0] = _mm256_cvtps_epi32(in[0]); /* a0 a1 a2 a3 a4 a5 a6 a7 */
+		out[1] = _mm256_cvtps_epi32(in[1]); /* b0 b1 b2 b3 b4 b5 b6 b7 */
+		out[2] = _mm256_cvtps_epi32(in[2]); /* a0 a1 a2 a3 a4 a5 a6 a7 */
+		out[3] = _mm256_cvtps_epi32(in[3]); /* b0 b1 b2 b3 b4 b5 b6 b7 */
+
+		t[0] = _mm256_unpacklo_epi32(out[0], out[1]); /* a0 b0 a1 b1 a4 b4 a5 b5 */
+		t[1] = _mm256_unpackhi_epi32(out[0], out[1]); /* a2 b2 a3 b3 a6 b6 a7 b7 */
+		t[2] = _mm256_unpacklo_epi32(out[2], out[3]); /* a0 b0 a1 b1 a4 b4 a5 b5 */
+		t[3] = _mm256_unpackhi_epi32(out[2], out[3]); /* a2 b2 a3 b3 a6 b6 a7 b7 */
+
+		out[0] = _mm256_packs_epi32(t[0], t[1]); /* a0 b0 a1 b1 a2 b2 a3 b3 a4 b4 a5 b5 a6 b6 a7 b7 */
+		out[1] = _mm256_packs_epi32(t[2], t[3]); /* a0 b0 a1 b1 a2 b2 a3 b3 a4 b4 a5 b5 a6 b6 a7 b7 */
+		out[0] = _MM256_BSWAP_EPI16(out[0]);
+		out[1] = _MM256_BSWAP_EPI16(out[1]);
+
+		_mm256_store_si256((__m256i*)(d+0), out[0]);
+		_mm256_store_si256((__m256i*)(d+16), out[1]);
+
+		d += 32;
+	}
+	for(; n < n_samples; n++) {
+		__m128 in[4];
+		__m128 int_scale = _mm_set1_ps(S16_SCALE);
+		__m128 int_max = _mm_set1_ps(S16_MAX);
+		__m128 int_min = _mm_set1_ps(S16_MIN);
+
+		in[0] = _mm_mul_ss(_mm_load_ss(&s0[n]), int_scale);
+		in[1] = _mm_mul_ss(_mm_load_ss(&s1[n]), int_scale);
+		in[0] = _MM_CLAMP_SS(in[0], int_min, int_max);
+		in[1] = _MM_CLAMP_SS(in[1], int_min, int_max);
+		d[0] = bswap_16((uint16_t)_mm_cvtss_si32(in[0]));
+		d[1] = bswap_16((uint16_t)_mm_cvtss_si32(in[1]));
+		d += 2;
+	}
+}
diff --git a/spa/plugins/audioconvert/fmt-ops-rvv.c b/spa/plugins/audioconvert/fmt-ops-rvv.c
new file mode 100644
index 00000000..02b01720
--- /dev/null
+++ b/spa/plugins/audioconvert/fmt-ops-rvv.c
@@ -0,0 +1,259 @@
+/* Spa */
+/* SPDX-FileCopyrightText: Copyright (c) 2023 Institue of Software Chinese Academy of Sciences (ISCAS). */
+/* SPDX-License-Identifier: MIT */
+
+#include "fmt-ops.h"
+
+#if HAVE_RVV
+void
+f32_to_s16(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src,
+		uint32_t n_samples)
+{
+	asm __volatile__ (
+		".option       arch, +v                                 \n\t"
+		"li            t0, 1191182336                           \n\t"
+		"fmv.w.x       fa5, t0                                  \n\t"
+		"1:                                                     \n\t"
+		"vsetvli       t0, %[n_samples], e32, m8, ta, ma        \n\t"
+		"vle32.v       v8, (%[src])                             \n\t"
+		"sub           %[n_samples], %[n_samples], t0           \n\t"
+		"vfmul.vf      v8, v8, fa5                              \n\t"
+		"vsetvli       zero, zero, e16, m4, ta, ma              \n\t"
+		"vfncvt.x.f.w  v8, v8                                   \n\t"
+		"slli          t0, t0, 1                                \n\t"
+		"vse16.v       v8, (%[dst])                             \n\t"
+		"add           %[src], %[src], t0                       \n\t"
+		"add           %[dst], %[dst], t0                       \n\t"
+		"add           %[src], %[src], t0                       \n\t"
+		"bnez          %[n_samples], 1b                         \n\t"
+		: [n_samples] "+r" (n_samples),
+		  [src] "+r" (src),
+		  [dst] "+r" (dst)
+		:
+		: "cc", "memory"
+	);
+}
+
+void
+conv_f32_to_s16_rvv(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
+		uint32_t n_samples)
+{
+	if (n_samples * conv->n_channels <= 4) {
+		conv_f32_to_s16_c(conv, dst, src, n_samples);
+		return;
+	}
+
+	f32_to_s16(conv, *dst, *src, n_samples * conv -> n_channels);
+}
+
+void
+conv_f32d_to_s16d_rvv(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
+		uint32_t n_samples)
+{
+	if (n_samples <= 4) {
+		conv_f32d_to_s16d_c(conv, dst, src, n_samples);
+		return;
+	}
+
+	uint32_t i = 0, n_channels = conv->n_channels;
+	for(i = 0; i < n_channels; i++) {
+		f32_to_s16(conv, dst[i], src[i], n_samples);
+	}
+}
+
+static void
+f32d_to_s16(void *data, void * SPA_RESTRICT dst, const void * SPA_RESTRICT src[],
+		uint32_t n_channels, uint32_t n_samples)
+{
+	const float *s = src[0];
+	uint32_t stride = n_channels << 1;
+
+	asm __volatile__ (
+		".option       arch, +v                                 \n\t"
+		"li            t0, 1191182336                           \n\t"
+		"fmv.w.x       fa5, t0                                  \n\t"
+		"1:                                                     \n\t"
+		"vsetvli       t0, %[n_samples], e32, m8, ta, ma        \n\t"
+		"vle32.v       v8, (%[s])                               \n\t"
+		"sub           %[n_samples], %[n_samples], t0           \n\t"
+		"vfmul.vf      v8, v8, fa5                              \n\t"
+		"vsetvli       zero, zero, e16, m4, ta, ma              \n\t"
+		"vfncvt.x.f.w  v8, v8                                   \n\t"
+		"slli          t2, t0, 2                                \n\t"
+		"mul           t3, t0, %[stride]                        \n\t"
+		"vsse16.v      v8, (%[dst]), %[stride]                  \n\t"
+		"add           %[s], %[s], t2                           \n\t"
+		"add           %[dst], %[dst], t3                       \n\t"
+		"bnez          %[n_samples], 1b                         \n\t"
+		: [n_samples] "+r" (n_samples),
+		  [s] "+r" (s),
+		  [dst] "+r" (dst)
+		: [stride] "r" (stride)
+		: "cc", "memory"
+	);
+}
+
+void
+conv_f32d_to_s16_rvv(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
+		uint32_t n_samples)
+{
+	if (n_samples <= 4) {
+		conv_f32d_to_s16_c(conv, dst, src, n_samples);
+		return;
+	}
+
+	int16_t *d = dst[0];
+	uint32_t i = 0, n_channels = conv->n_channels;
+
+	for(i = 0; i < n_channels; i++)
+		f32d_to_s16(conv, &d[i], &src[i], n_channels, n_samples);
+}
+
+static void
+s16_to_f32d(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src,
+		uint32_t n_channels, uint32_t n_samples)
+{
+	float *d = dst[0];
+	uint32_t stride = n_channels << 1;
+
+	asm __volatile__ (
+		".option       arch, +v                                 \n\t"
+		"li            t0, 939524096                            \n\t"
+		"fmv.w.x       fa5, t0                                  \n\t"
+		"1:                                                     \n\t"
+		"vsetvli       t0, %[n_samples], e16, m4, ta, ma        \n\t"
+		"vlse16.v      v8, (%[src]), %[stride]                  \n\t"
+		"sub           %[n_samples], %[n_samples], t0           \n\t"
+		"vfwcvt.f.x.v  v16, v8                                  \n\t"
+		"vsetvli       zero, zero, e32, m8, ta, ma              \n\t"
+		"mul           t4, t0, %[stride]                        \n\t"
+		"vfmul.vf      v8, v16, fa5                             \n\t"
+		"slli          t3, t0, 2                                \n\t"
+		"vse32.v       v8, (%[d])                               \n\t"
+		"add           %[src], %[src], t4                       \n\t"
+		"add           %[d], %[d], t3                           \n\t"
+		"bnez          %[n_samples], 1b                         \n\t"
+		: [n_samples] "+r" (n_samples),
+		  [src] "+r" (src),
+		  [d] "+r" (d)
+		: [stride] "r" (stride)
+		: "cc", "memory"
+	);
+
+}
+
+void
+conv_s16_to_f32d_rvv(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
+		uint32_t n_samples)
+{
+	if (n_samples <= 4) {
+		conv_s16_to_f32d_c(conv, dst, src, n_samples);
+		return;
+	}
+
+	const int16_t *s = src[0];
+	uint32_t i = 0, n_channels = conv->n_channels;
+
+	for(i = 0; i < n_channels; i++)
+		s16_to_f32d(conv, &dst[i], &s[i], n_channels, n_samples);
+}
+
+static void
+s32_to_f32d(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src,
+		uint32_t n_channels, uint32_t n_samples)
+{
+	float *d = dst[0];
+	uint32_t stride = n_channels << 2;
+
+	asm __volatile__ (
+		".option       arch, +v                                 \n\t"
+		"li            t0, 805306368                            \n\t"
+		"fmv.w.x       fa5, t0                                  \n\t"
+		"1:                                                     \n\t"
+		"vsetvli       t0, %[n_samples], e32, m8, ta, ma        \n\t"
+		"vlse32.v      v8, (%[src]), %[stride]                  \n\t"
+		"sub           %[n_samples], %[n_samples], t0           \n\t"
+		"vfcvt.f.x.v   v8, v8                                   \n\t"
+		"mul           t4, t0, %[stride]                        \n\t"
+		"vfmul.vf      v8, v8, fa5                              \n\t"
+		"slli          t3, t0, 2                                \n\t"
+		"vse32.v       v8, (%[d])                               \n\t"
+		"add           %[src], %[src], t4                       \n\t"
+		"add           %[d], %[d], t3                           \n\t"
+		"bnez          %[n_samples], 1b                         \n\t"
+		: [n_samples] "+r" (n_samples),
+		  [src] "+r" (src),
+		  [d] "+r" (d)
+		: [stride] "r" (stride)
+		: "cc", "memory"
+	);
+
+}
+
+void
+conv_s32_to_f32d_rvv(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
+		uint32_t n_samples)
+{
+	if (n_samples <= 4) {
+		conv_s32_to_f32d_c(conv, dst, src, n_samples);
+		return;
+	}
+
+	const int32_t *s = src[0];
+	uint32_t i = 0, n_channels = conv->n_channels;
+
+	for(i = 0; i < n_channels; i++)
+		s32_to_f32d(conv, &dst[i], &s[i], n_channels, n_samples);
+	return;
+}
+
+static void
+f32d_to_s32(void *data, void * SPA_RESTRICT dst, const void * SPA_RESTRICT src[],
+		uint32_t n_channels, uint32_t n_samples)
+{
+	const float *s = src[0];
+	uint32_t stride = n_channels << 2;
+
+	asm __volatile__ (
+		".option       arch, +v                                 \n\t"
+		"li            t0, 1325400064                           \n\t"
+		"li            t2, 1325400063                           \n\t"
+		"fmv.w.x       fa5, t0                                  \n\t"
+		"fmv.w.x       fa4, t2                                  \n\t"
+		"1:                                                     \n\t"
+		"vsetvli       t0, %[n_samples], e32, m8, ta, ma        \n\t"
+		"vle32.v       v8, (%[s])                               \n\t"
+		"sub           %[n_samples], %[n_samples], t0           \n\t"
+		"vfmul.vf      v8, v8, fa5                              \n\t"
+		"vfmin.vf      v8, v8, fa4                              \n\t"
+		"vfcvt.x.f.v   v8, v8                                   \n\t"
+		"slli          t2, t0, 2                                \n\t"
+		"mul           t3, t0, %[stride]                        \n\t"
+		"vsse32.v      v8, (%[dst]), %[stride]                  \n\t"
+		"add           %[s], %[s], t2                           \n\t"
+		"add           %[dst], %[dst], t3                       \n\t"
+		"bnez          %[n_samples], 1b                         \n\t"
+		: [n_samples] "+r" (n_samples),
+		  [s] "+r" (s),
+		  [dst] "+r" (dst)
+		: [stride] "r" (stride)
+		: "cc", "memory"
+	);
+}
+
+void
+conv_f32d_to_s32_rvv(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
+		uint32_t n_samples)
+{
+	if (n_samples <= 4) {
+		conv_f32d_to_s32_c(conv, dst, src, n_samples);
+		return;
+	}
+
+	int32_t *d = dst[0];
+	uint32_t i = 0, n_channels = conv->n_channels;
+
+	for(i = 0; i < n_channels; i++)
+		f32d_to_s32(conv, &d[i], &src[i], n_channels, n_samples);
+}
+#endif
diff --git a/spa/plugins/audioconvert/fmt-ops-sse2.c b/spa/plugins/audioconvert/fmt-ops-sse2.c
index 4aca0e9c..fe6af66c 100644
--- a/spa/plugins/audioconvert/fmt-ops-sse2.c
+++ b/spa/plugins/audioconvert/fmt-ops-sse2.c
@@ -12,6 +12,20 @@
 #define _MM_CLAMP_SS(r,min,max)				\
 	_mm_min_ss(_mm_max_ss(r, min), max)
 
+#define _MM_BSWAP_EPI16(x)						\
+({									\
+	_mm_or_si128(							\
+		_mm_slli_epi16(x, 8),					\
+		_mm_srli_epi16(x, 8));					\
+})
+
+#define _MM_BSWAP_EPI32(x)						\
+({									\
+	__m128i a = _MM_BSWAP_EPI16(x);					\
+	a = _mm_shufflelo_epi16(a, _MM_SHUFFLE(2, 3, 0, 1));		\
+	a = _mm_shufflehi_epi16(a, _MM_SHUFFLE(2, 3, 0, 1));		\
+})
+
 static void
 conv_s16_to_f32d_1s_sse2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src,
 		uint32_t n_channels, uint32_t n_samples)
@@ -57,6 +71,52 @@ conv_s16_to_f32d_sse2(struct convert *conv, void * SPA_RESTRICT dst[], const voi
 		conv_s16_to_f32d_1s_sse2(conv, &dst[i], &s[i], n_channels, n_samples);
 }
 
+static void
+conv_s16s_to_f32d_1s_sse2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src,
+		uint32_t n_channels, uint32_t n_samples)
+{
+	const uint16_t *s = src;
+	float *d0 = dst[0];
+	uint32_t n, unrolled;
+	__m128i in = _mm_setzero_si128();
+	__m128 out, factor = _mm_set1_ps(1.0f / S16_SCALE);
+
+	if (SPA_LIKELY(SPA_IS_ALIGNED(d0, 16)))
+		unrolled = n_samples & ~3;
+	else
+		unrolled = 0;
+
+	for(n = 0; n < unrolled; n += 4) {
+		in = _mm_insert_epi16(in, s[0*n_channels], 1);
+		in = _mm_insert_epi16(in, s[1*n_channels], 3);
+		in = _mm_insert_epi16(in, s[2*n_channels], 5);
+		in = _mm_insert_epi16(in, s[3*n_channels], 7);
+		in = _MM_BSWAP_EPI16(in);
+		in = _mm_srai_epi32(in, 16);
+		out = _mm_cvtepi32_ps(in);
+		out = _mm_mul_ps(out, factor);
+		_mm_store_ps(&d0[n], out);
+		s += 4*n_channels;
+	}
+	for(; n < n_samples; n++) {
+		out = _mm_cvtsi32_ss(factor, (int16_t)bswap_16(s[0]));
+		out = _mm_mul_ss(out, factor);
+		_mm_store_ss(&d0[n], out);
+		s += n_channels;
+	}
+}
+
+void
+conv_s16s_to_f32d_sse2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
+		uint32_t n_samples)
+{
+	const uint16_t *s = src[0];
+	uint32_t i = 0, n_channels = conv->n_channels;
+
+	for(; i < n_channels; i++)
+		conv_s16s_to_f32d_1s_sse2(conv, &dst[i], &s[i], n_channels, n_samples);
+}
+
 void
 conv_s16_to_f32d_2_sse2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
 		uint32_t n_samples)
@@ -114,6 +174,65 @@ conv_s16_to_f32d_2_sse2(struct convert *conv, void * SPA_RESTRICT dst[], const v
 	}
 }
 
+void
+conv_s16s_to_f32d_2_sse2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
+		uint32_t n_samples)
+{
+	const uint16_t *s = src[0];
+	float *d0 = dst[0], *d1 = dst[1];
+	uint32_t n, unrolled;
+	__m128i in[2], t[4];
+	__m128 out[4], factor = _mm_set1_ps(1.0f / S16_SCALE);
+
+	if (SPA_IS_ALIGNED(s, 16) &&
+	    SPA_IS_ALIGNED(d0, 16) &&
+	    SPA_IS_ALIGNED(d1, 16))
+		unrolled = n_samples & ~7;
+	else
+		unrolled = 0;
+
+	for(n = 0; n < unrolled; n += 8) {
+		in[0] = _mm_load_si128((__m128i*)(s + 0));
+		in[1] = _mm_load_si128((__m128i*)(s + 8));
+		in[0] = _MM_BSWAP_EPI16(in[0]);
+		in[1] = _MM_BSWAP_EPI16(in[1]);
+
+		t[0] = _mm_slli_epi32(in[0], 16);
+		t[0] = _mm_srai_epi32(t[0], 16);
+		out[0] = _mm_cvtepi32_ps(t[0]);
+		out[0] = _mm_mul_ps(out[0], factor);
+
+		t[1] = _mm_srai_epi32(in[0], 16);
+		out[1] = _mm_cvtepi32_ps(t[1]);
+		out[1] = _mm_mul_ps(out[1], factor);
+
+		t[2] = _mm_slli_epi32(in[1], 16);
+		t[2] = _mm_srai_epi32(t[2], 16);
+		out[2] = _mm_cvtepi32_ps(t[2]);
+		out[2] = _mm_mul_ps(out[2], factor);
+
+		t[3] = _mm_srai_epi32(in[1], 16);
+		out[3] = _mm_cvtepi32_ps(t[3]);
+		out[3] = _mm_mul_ps(out[3], factor);
+
+		_mm_store_ps(&d0[n + 0], out[0]);
+		_mm_store_ps(&d1[n + 0], out[1]);
+		_mm_store_ps(&d0[n + 4], out[2]);
+		_mm_store_ps(&d1[n + 4], out[3]);
+
+		s += 16;
+	}
+	for(; n < n_samples; n++) {
+		out[0] = _mm_cvtsi32_ss(factor, (int16_t)bswap_16(s[0]));
+		out[0] = _mm_mul_ss(out[0], factor);
+		out[1] = _mm_cvtsi32_ss(factor, (int16_t)bswap_16(s[1]));
+		out[1] = _mm_mul_ss(out[1], factor);
+		_mm_store_ss(&d0[n], out[0]);
+		_mm_store_ss(&d1[n], out[1]);
+		s += 2;
+	}
+}
+
 #define spa_read_unaligned(ptr, type) \
 __extension__ ({ \
 	__typeof__(type) _val; \
@@ -335,7 +454,7 @@ conv_s32_to_f32d_1s_sse2(void *data, void * SPA_RESTRICT dst[], const void * SPA
 	float *d0 = dst[0];
 	uint32_t n, unrolled;
 	__m128i in;
-	__m128 out, factor = _mm_set1_ps(1.0f / S24_SCALE);
+	__m128 out, factor = _mm_set1_ps(1.0f / S32_SCALE_I2F);
 
 	if (SPA_IS_ALIGNED(d0, 16))
 		unrolled = n_samples & ~3;
@@ -347,14 +466,13 @@ conv_s32_to_f32d_1s_sse2(void *data, void * SPA_RESTRICT dst[], const void * SPA
 				    s[1*n_channels],
 				    s[2*n_channels],
 				    s[3*n_channels]);
-		in = _mm_srai_epi32(in, 8);
 		out = _mm_cvtepi32_ps(in);
 		out = _mm_mul_ps(out, factor);
 		_mm_store_ps(&d0[n], out);
 		s += 4*n_channels;
 	}
 	for(; n < n_samples; n++) {
-		out = _mm_cvtsi32_ss(factor, s[0]>>8);
+		out = _mm_cvtsi32_ss(factor, s[0]);
 		out = _mm_mul_ss(out, factor);
 		_mm_store_ss(&d0[n], out);
 		s += n_channels;
@@ -381,9 +499,9 @@ conv_f32d_to_s32_1s_sse2(void *data, void * SPA_RESTRICT dst, const void * SPA_R
 	uint32_t n, unrolled;
 	__m128 in[1];
 	__m128i out[4];
-	__m128 scale = _mm_set1_ps(S24_SCALE);
-	__m128 int_min = _mm_set1_ps(S24_MIN);
-	__m128 int_max = _mm_set1_ps(S24_MAX);
+	__m128 scale = _mm_set1_ps(S32_SCALE_F2I);
+	__m128 int_min = _mm_set1_ps(S32_MIN_F2I);
+	__m128 int_max = _mm_set1_ps(S32_MAX_F2I);
 
 	if (SPA_IS_ALIGNED(s0, 16))
 		unrolled = n_samples & ~3;
@@ -394,7 +512,6 @@ conv_f32d_to_s32_1s_sse2(void *data, void * SPA_RESTRICT dst, const void * SPA_R
 		in[0] = _mm_mul_ps(_mm_load_ps(&s0[n]), scale);
 		in[0] = _MM_CLAMP_PS(in[0], int_min, int_max);
 		out[0] = _mm_cvtps_epi32(in[0]);
-		out[0] = _mm_slli_epi32(out[0], 8);
 		out[1] = _mm_shuffle_epi32(out[0], _MM_SHUFFLE(0, 3, 2, 1));
 		out[2] = _mm_shuffle_epi32(out[0], _MM_SHUFFLE(1, 0, 3, 2));
 		out[3] = _mm_shuffle_epi32(out[0], _MM_SHUFFLE(2, 1, 0, 3));
@@ -409,7 +526,7 @@ conv_f32d_to_s32_1s_sse2(void *data, void * SPA_RESTRICT dst, const void * SPA_R
 		in[0] = _mm_load_ss(&s0[n]);
 		in[0] = _mm_mul_ss(in[0], scale);
 		in[0] = _MM_CLAMP_SS(in[0], int_min, int_max);
-		*d = _mm_cvtss_si32(in[0]) << 8;
+		*d = _mm_cvtss_si32(in[0]);
 		d += n_channels;
 	}
 }
@@ -423,12 +540,12 @@ conv_f32d_to_s32_2s_sse2(void *data, void * SPA_RESTRICT dst, const void * SPA_R
 	uint32_t n, unrolled;
 	__m128 in[2];
 	__m128i out[2], t[2];
-	__m128 scale = _mm_set1_ps(S24_SCALE);
-	__m128 int_min = _mm_set1_ps(S24_MIN);
-	__m128 int_max = _mm_set1_ps(S24_MAX);
+	__m128 scale = _mm_set1_ps(S32_SCALE_F2I);
+	__m128 int_min = _mm_set1_ps(S32_MIN_F2I);
+	__m128 int_max = _mm_set1_ps(S32_MAX_F2I);
 
 	if (SPA_IS_ALIGNED(s0, 16) &&
-	    SPA_IS_ALIGNED(s1, 16))
+		SPA_IS_ALIGNED(s1, 16))
 		unrolled = n_samples & ~3;
 	else
 		unrolled = 0;
@@ -442,8 +559,6 @@ conv_f32d_to_s32_2s_sse2(void *data, void * SPA_RESTRICT dst, const void * SPA_R
 
 		out[0] = _mm_cvtps_epi32(in[0]);
 		out[1] = _mm_cvtps_epi32(in[1]);
-		out[0] = _mm_slli_epi32(out[0], 8);
-		out[1] = _mm_slli_epi32(out[1], 8);
 
 		t[0] = _mm_unpacklo_epi32(out[0], out[1]);
 		t[1] = _mm_unpackhi_epi32(out[0], out[1]);
@@ -463,7 +578,6 @@ conv_f32d_to_s32_2s_sse2(void *data, void * SPA_RESTRICT dst, const void * SPA_R
 		in[0] = _mm_mul_ps(in[0], scale);
 		in[0] = _MM_CLAMP_PS(in[0], int_min, int_max);
 		out[0] = _mm_cvtps_epi32(in[0]);
-		out[0] = _mm_slli_epi32(out[0], 8);
 		_mm_storel_epi64((__m128i*)d, out[0]);
 		d += n_channels;
 	}
@@ -478,14 +592,14 @@ conv_f32d_to_s32_4s_sse2(void *data, void * SPA_RESTRICT dst, const void * SPA_R
 	uint32_t n, unrolled;
 	__m128 in[4];
 	__m128i out[4];
-	__m128 scale = _mm_set1_ps(S24_SCALE);
-	__m128 int_min = _mm_set1_ps(S24_MIN);
-	__m128 int_max = _mm_set1_ps(S24_MAX);
+	__m128 scale = _mm_set1_ps(S32_SCALE_F2I);
+	__m128 int_min = _mm_set1_ps(S32_MIN_F2I);
+	__m128 int_max = _mm_set1_ps(S32_MAX_F2I);
 
 	if (SPA_IS_ALIGNED(s0, 16) &&
-	    SPA_IS_ALIGNED(s1, 16) &&
-	    SPA_IS_ALIGNED(s2, 16) &&
-	    SPA_IS_ALIGNED(s3, 16))
+		SPA_IS_ALIGNED(s1, 16) &&
+		SPA_IS_ALIGNED(s2, 16) &&
+		SPA_IS_ALIGNED(s3, 16))
 		unrolled = n_samples & ~3;
 	else
 		unrolled = 0;
@@ -507,10 +621,6 @@ conv_f32d_to_s32_4s_sse2(void *data, void * SPA_RESTRICT dst, const void * SPA_R
 		out[1] = _mm_cvtps_epi32(in[1]);
 		out[2] = _mm_cvtps_epi32(in[2]);
 		out[3] = _mm_cvtps_epi32(in[3]);
-		out[0] = _mm_slli_epi32(out[0], 8);
-		out[1] = _mm_slli_epi32(out[1], 8);
-		out[2] = _mm_slli_epi32(out[2], 8);
-		out[3] = _mm_slli_epi32(out[3], 8);
 
 		_mm_storeu_si128((__m128i*)(d + 0*n_channels), out[0]);
 		_mm_storeu_si128((__m128i*)(d + 1*n_channels), out[1]);
@@ -531,7 +641,6 @@ conv_f32d_to_s32_4s_sse2(void *data, void * SPA_RESTRICT dst, const void * SPA_R
 		in[0] = _mm_mul_ps(in[0], scale);
 		in[0] = _MM_CLAMP_PS(in[0], int_min, int_max);
 		out[0] = _mm_cvtps_epi32(in[0]);
-		out[0] = _mm_slli_epi32(out[0], 8);
 		_mm_storeu_si128((__m128i*)d, out[0]);
 		d += n_channels;
 	}
@@ -620,6 +729,7 @@ void conv_noise_tri_hf_sse2(struct convert *conv, float *noise, uint32_t n_sampl
 	_mm_store_si128((__m128i*)p, old[0]);
 }
 
+// FIXME: this function is not covered with tests.
 static void
 conv_f32d_to_s32_1s_noise_sse2(struct convert *conv, void * SPA_RESTRICT dst, const void * SPA_RESTRICT src,
 		float *noise, uint32_t n_channels, uint32_t n_samples)
@@ -629,9 +739,9 @@ conv_f32d_to_s32_1s_noise_sse2(struct convert *conv, void * SPA_RESTRICT dst, co
 	uint32_t n, unrolled;
 	__m128 in[1];
 	__m128i out[4];
-	__m128 scale = _mm_set1_ps(S24_SCALE);
-	__m128 int_min = _mm_set1_ps(S24_MIN);
-	__m128 int_max = _mm_set1_ps(S24_MAX);
+	__m128 scale = _mm_set1_ps(S32_SCALE_F2I);
+	__m128 int_min = _mm_set1_ps(S32_MIN_F2I);
+	__m128 int_max = _mm_set1_ps(S32_MAX_F2I);
 
 	if (SPA_IS_ALIGNED(s, 16))
 		unrolled = n_samples & ~3;
@@ -643,7 +753,6 @@ conv_f32d_to_s32_1s_noise_sse2(struct convert *conv, void * SPA_RESTRICT dst, co
 		in[0] = _mm_add_ps(in[0], _mm_load_ps(&noise[n]));
 		in[0] = _MM_CLAMP_PS(in[0], int_min, int_max);
 		out[0] = _mm_cvtps_epi32(in[0]);
-		out[0] = _mm_slli_epi32(out[0], 8);
 		out[1] = _mm_shuffle_epi32(out[0], _MM_SHUFFLE(0, 3, 2, 1));
 		out[2] = _mm_shuffle_epi32(out[0], _MM_SHUFFLE(1, 0, 3, 2));
 		out[3] = _mm_shuffle_epi32(out[0], _MM_SHUFFLE(2, 1, 0, 3));
@@ -659,7 +768,7 @@ conv_f32d_to_s32_1s_noise_sse2(struct convert *conv, void * SPA_RESTRICT dst, co
 		in[0] = _mm_mul_ss(in[0], scale);
 		in[0] = _mm_add_ss(in[0], _mm_load_ss(&noise[n]));
 		in[0] = _MM_CLAMP_SS(in[0], int_min, int_max);
-		*d = _mm_cvtss_si32(in[0]) << 8;
+		*d = _mm_cvtss_si32(in[0]);
 		d += n_channels;
 	}
 }
@@ -766,15 +875,6 @@ conv_32d_to_32_sse2(struct convert *conv, void * SPA_RESTRICT dst[], const void
 		conv_interleave_32_1s_sse2(conv, &d[i], &src[i], n_channels, n_samples);
 }
 
-#define _MM_BSWAP_EPI32(x)						\
-({									\
-	__m128i a = _mm_or_si128(					\
-		_mm_slli_epi16(x, 8),					\
-		_mm_srli_epi16(x, 8));					\
-	a = _mm_shufflelo_epi16(a, _MM_SHUFFLE(2, 3, 0, 1));		\
-	a = _mm_shufflehi_epi16(a, _MM_SHUFFLE(2, 3, 0, 1));		\
-})
-
 static void
 conv_interleave_32s_1s_sse2(void *data, void * SPA_RESTRICT dst, const void * SPA_RESTRICT src[],
 		uint32_t n_channels, uint32_t n_samples)
@@ -1256,6 +1356,62 @@ conv_f32d_to_s16_sse2(struct convert *conv, void * SPA_RESTRICT dst[], const voi
 		conv_f32d_to_s16_1s_sse2(conv, &d[i], &src[i], n_channels, n_samples);
 }
 
+
+static void
+conv_f32d_to_s16s_1s_sse2(void *data, void * SPA_RESTRICT dst, const void * SPA_RESTRICT src[],
+		uint32_t n_channels, uint32_t n_samples)
+{
+	const float *s0 = src[0];
+	uint16_t *d = dst;
+	uint32_t n, unrolled;
+	__m128 in[2];
+	__m128i out[2];
+	__m128 int_scale = _mm_set1_ps(S16_SCALE);
+	__m128 int_max = _mm_set1_ps(S16_MAX);
+        __m128 int_min = _mm_set1_ps(S16_MIN);
+
+	if (SPA_IS_ALIGNED(s0, 16))
+		unrolled = n_samples & ~7;
+	else
+		unrolled = 0;
+
+	for(n = 0; n < unrolled; n += 8) {
+		in[0] = _mm_mul_ps(_mm_load_ps(&s0[n]), int_scale);
+		in[1] = _mm_mul_ps(_mm_load_ps(&s0[n+4]), int_scale);
+		out[0] = _mm_cvtps_epi32(in[0]);
+		out[1] = _mm_cvtps_epi32(in[1]);
+		out[0] = _mm_packs_epi32(out[0], out[1]);
+		out[0] = _MM_BSWAP_EPI16(out[0]);
+
+		d[0*n_channels] = _mm_extract_epi16(out[0], 0);
+		d[1*n_channels] = _mm_extract_epi16(out[0], 1);
+		d[2*n_channels] = _mm_extract_epi16(out[0], 2);
+		d[3*n_channels] = _mm_extract_epi16(out[0], 3);
+		d[4*n_channels] = _mm_extract_epi16(out[0], 4);
+		d[5*n_channels] = _mm_extract_epi16(out[0], 5);
+		d[6*n_channels] = _mm_extract_epi16(out[0], 6);
+		d[7*n_channels] = _mm_extract_epi16(out[0], 7);
+		d += 8*n_channels;
+	}
+	for(; n < n_samples; n++) {
+		in[0] = _mm_mul_ss(_mm_load_ss(&s0[n]), int_scale);
+		in[0] = _MM_CLAMP_SS(in[0], int_min, int_max);
+		*d = bswap_16((uint16_t)_mm_cvtss_si32(in[0]));
+		d += n_channels;
+	}
+}
+
+void
+conv_f32d_to_s16s_sse2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
+		uint32_t n_samples)
+{
+	uint16_t *d = dst[0];
+	uint32_t i = 0, n_channels = conv->n_channels;
+
+	for(; i < n_channels; i++)
+		conv_f32d_to_s16s_1s_sse2(conv, &d[i], &src[i], n_channels, n_samples);
+}
+
 static void
 conv_f32d_to_s16_1s_noise_sse2(struct convert *conv, void * SPA_RESTRICT dst, const void * SPA_RESTRICT src,
 		const float *noise, uint32_t n_channels, uint32_t n_samples)
@@ -1428,3 +1584,58 @@ conv_f32d_to_s16_2_sse2(struct convert *conv, void * SPA_RESTRICT dst[], const v
 		d += 2;
 	}
 }
+
+void
+conv_f32d_to_s16s_2_sse2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
+		uint32_t n_samples)
+{
+	const float *s0 = src[0], *s1 = src[1];
+	uint16_t *d = dst[0];
+	uint32_t n, unrolled;
+	__m128 in[4];
+	__m128i out[4];
+	__m128 int_scale = _mm_set1_ps(S16_SCALE);
+	__m128 int_max = _mm_set1_ps(S16_MAX);
+        __m128 int_min = _mm_set1_ps(S16_MIN);
+
+	if (SPA_IS_ALIGNED(s0, 16) &&
+	    SPA_IS_ALIGNED(s1, 16))
+		unrolled = n_samples & ~7;
+	else
+		unrolled = 0;
+
+	for(n = 0; n < unrolled; n += 8) {
+		in[0] = _mm_mul_ps(_mm_load_ps(&s0[n+0]), int_scale);
+		in[1] = _mm_mul_ps(_mm_load_ps(&s1[n+0]), int_scale);
+		in[2] = _mm_mul_ps(_mm_load_ps(&s0[n+4]), int_scale);
+		in[3] = _mm_mul_ps(_mm_load_ps(&s1[n+4]), int_scale);
+
+		out[0] = _mm_cvtps_epi32(in[0]);
+		out[1] = _mm_cvtps_epi32(in[1]);
+		out[2] = _mm_cvtps_epi32(in[2]);
+		out[3] = _mm_cvtps_epi32(in[3]);
+
+		out[0] = _mm_packs_epi32(out[0], out[2]);
+		out[1] = _mm_packs_epi32(out[1], out[3]);
+
+		out[2] = _mm_unpacklo_epi16(out[0], out[1]);
+		out[3] = _mm_unpackhi_epi16(out[0], out[1]);
+
+		out[2] = _MM_BSWAP_EPI16(out[2]);
+		out[3] = _MM_BSWAP_EPI16(out[3]);
+
+		_mm_storeu_si128((__m128i*)(d+0), out[2]);
+		_mm_storeu_si128((__m128i*)(d+8), out[3]);
+
+		d += 16;
+	}
+	for(; n < n_samples; n++) {
+		in[0] = _mm_mul_ss(_mm_load_ss(&s0[n]), int_scale);
+		in[1] = _mm_mul_ss(_mm_load_ss(&s1[n]), int_scale);
+		in[0] = _MM_CLAMP_SS(in[0], int_min, int_max);
+		in[1] = _MM_CLAMP_SS(in[1], int_min, int_max);
+		d[0] = bswap_16((uint16_t)_mm_cvtss_si32(in[0]));
+		d[1] = bswap_16((uint16_t)_mm_cvtss_si32(in[1]));
+		d += 2;
+	}
+}
diff --git a/spa/plugins/audioconvert/fmt-ops.c b/spa/plugins/audioconvert/fmt-ops.c
index 748c2231..4df31323 100644
--- a/spa/plugins/audioconvert/fmt-ops.c
+++ b/spa/plugins/audioconvert/fmt-ops.c
@@ -68,10 +68,21 @@ static struct conv_info conv_table[] =
 #if defined (HAVE_SSE2)
 	MAKE(S16, F32P, 2, conv_s16_to_f32d_2_sse2, SPA_CPU_FLAG_SSE2),
 	MAKE(S16, F32P, 0, conv_s16_to_f32d_sse2, SPA_CPU_FLAG_SSE2),
+#endif
+#if defined (HAVE_RVV)
+	MAKE(S16, F32P, 0, conv_s16_to_f32d_rvv, SPA_CPU_FLAG_RISCV_V),
 #endif
 	MAKE(S16, F32P, 0, conv_s16_to_f32d_c),
 	MAKE(S16P, F32, 0, conv_s16d_to_f32_c),
 
+#if defined (HAVE_AVX2)
+	MAKE(S16_OE, F32P, 2, conv_s16s_to_f32d_2_avx2, SPA_CPU_FLAG_AVX2),
+	MAKE(S16_OE, F32P, 0, conv_s16s_to_f32d_avx2, SPA_CPU_FLAG_AVX2),
+#endif
+#if defined (HAVE_SSE2)
+	MAKE(S16_OE, F32P, 2, conv_s16s_to_f32d_2_sse2, SPA_CPU_FLAG_SSE2),
+	MAKE(S16_OE, F32P, 0, conv_s16s_to_f32d_sse2, SPA_CPU_FLAG_SSE2),
+#endif
 	MAKE(S16_OE, F32P, 0, conv_s16s_to_f32d_c),
 
 	MAKE(F32, F32, 0, conv_copy32_c),
@@ -102,6 +113,9 @@ static struct conv_info conv_table[] =
 #endif
 #if defined (HAVE_SSE2)
 	MAKE(S32, F32P, 0, conv_s32_to_f32d_sse2, SPA_CPU_FLAG_SSE2),
+#endif
+#if defined (HAVE_RVV)
+	MAKE(S32, F32P, 0, conv_s32_to_f32d_rvv, SPA_CPU_FLAG_RISCV_V),
 #endif
 	MAKE(S32, F32, 0, conv_s32_to_f32_c),
 	MAKE(S32P, F32P, 0, conv_s32d_to_f32d_c),
@@ -176,6 +190,11 @@ static struct conv_info conv_table[] =
 
 #if defined (HAVE_SSE2)
 	MAKE(F32, S16, 0, conv_f32_to_s16_sse2, SPA_CPU_FLAG_SSE2),
+#endif
+#if defined (HAVE_RVV)
+	MAKE(F32, S16, 0, conv_f32_to_s16_rvv, SPA_CPU_FLAG_RISCV_V),
+	MAKE(F32P, S16P, 0, conv_f32d_to_s16d_rvv, SPA_CPU_FLAG_RISCV_V),
+	MAKE(F32P, S16, 0, conv_f32d_to_s16_rvv, SPA_CPU_FLAG_RISCV_V),
 #endif
 	MAKE(F32, S16, 0, conv_f32_to_s16_c),
 
@@ -212,6 +231,10 @@ static struct conv_info conv_table[] =
 
 	MAKE(F32P, S16_OE, 0, conv_f32d_to_s16s_shaped_c, 0, CONV_SHAPE),
 	MAKE(F32P, S16_OE, 0, conv_f32d_to_s16s_noise_c, 0, CONV_NOISE),
+#if defined (HAVE_SSE2)
+	MAKE(F32P, S16_OE, 2, conv_f32d_to_s16s_2_sse2, SPA_CPU_FLAG_SSE2),
+	MAKE(F32P, S16_OE, 0, conv_f32d_to_s16s_sse2, SPA_CPU_FLAG_SSE2),
+#endif
 	MAKE(F32P, S16_OE, 0, conv_f32d_to_s16s_c),
 
 	MAKE(F32, U32, 0, conv_f32_to_u32_c),
@@ -232,6 +255,9 @@ static struct conv_info conv_table[] =
 #endif
 #if defined (HAVE_SSE2)
 	MAKE(F32P, S32, 0, conv_f32d_to_s32_sse2, SPA_CPU_FLAG_SSE2),
+#endif
+#if defined (HAVE_RVV)
+	MAKE(F32P, S32, 0, conv_f32d_to_s32_rvv, SPA_CPU_FLAG_RISCV_V),
 #endif
 	MAKE(F32P, S32, 0, conv_f32d_to_s32_c),
 
diff --git a/spa/plugins/audioconvert/fmt-ops.h b/spa/plugins/audioconvert/fmt-ops.h
index c8d1d526..7aed0bc6 100644
--- a/spa/plugins/audioconvert/fmt-ops.h
+++ b/spa/plugins/audioconvert/fmt-ops.h
@@ -3,16 +3,9 @@
 /* SPDX-License-Identifier: MIT */
 
 #include <math.h>
-#if defined(__FreeBSD__) || defined(__MidnightBSD__)
-#include <sys/endian.h>
-#define bswap_16 bswap16
-#define bswap_32 bswap32
-#define bswap_64 bswap64
-#else
-#include <byteswap.h>
-#endif
 
 #include <spa/utils/defs.h>
+#include <spa/utils/endian.h>
 #include <spa/utils/string.h>
 
 #define f32_round(a)	lrintf(a)
@@ -22,7 +15,7 @@
 #define FTOI(type,v,scale,offs,noise,min,max) \
 	(type)f32_round(SPA_CLAMPF((v) * (scale) + (offs) + (noise), min, max))
 
-#define FMT_OPS_MAX_ALIGN	32
+#define FMT_OPS_MAX_ALIGN	32u
 
 #define U8_MIN			0u
 #define U8_MAX			255u
@@ -88,6 +81,18 @@
 
 #define U32_TO_U24_32(v)	(((uint32_t)(v)) >> 8)
 
+#define S25_MIN			-16777216
+#define S25_MAX			16777215
+#define S25_SCALE		16777216.0f
+#define S25_32_TO_F32(v)	ITOF(int32_t, v, S25_SCALE, 0.0f)
+#define S25_32S_TO_F32(v)	S25_32_TO_F32(bswap_32(v))
+#define F32_TO_S25_32_D(v,d)	FTOI(int32_t, v, S25_SCALE, 0.0f, d, S25_MIN, S25_MAX)
+#define F32_TO_S25_32(v)	F32_TO_S25_32_D(v, 0.0f)
+#define F32_TO_S25_32S(v)	bswap_32(F32_TO_S25_32(v))
+#define F32_TO_S25_32S_D(v,d)	bswap_32(F32_TO_S25_32_D(v,d))
+#define S25_32_TO_S32(v)	((int32_t)(((uint32_t)(v)) << 7))
+#define S32_TO_S25_32(v)	(((int32_t)(v)) >> 7)
+
 #define U32_MIN			0u
 #define U32_MAX			4294967295u
 #define U32_SCALE		2147483648.f
@@ -107,12 +112,17 @@
 
 #define S32_TO_S24_32(v)	(((int32_t)(v)) >> 8)
 
-#define S32_MIN			(S24_MIN * 256)
-#define S32_MAX			(S24_MAX * 256)
-#define S32_TO_F32(v)		ITOF(int32_t, S32_TO_S24_32(v), S24_SCALE, 0.0f)
+#define S32_MIN			-2147483648
+#define S32_MAX			2147483647
+#define S32_SCALE_I2F		2147483648.0f
+#define S32_TO_F32(v)		ITOF(int32_t, v, S32_SCALE_I2F, 0.0f)
 #define S32S_TO_F32(v)		S32_TO_F32(bswap_32(v))
-#define F32_TO_S32(v)		S24_32_TO_S32(F32_TO_S24_32(v))
-#define F32_TO_S32_D(v,d)	S24_32_TO_S32(F32_TO_S24_32_D(v,d))
+
+#define S32_MIN_F2I		((int32_t)(((uint32_t)(S25_MIN)) << 7))
+#define S32_MAX_F2I		((S25_MAX) << 7)
+#define S32_SCALE_F2I		(-((float)(S32_MIN_F2I)))
+#define F32_TO_S32_D(v,d)	FTOI(int32_t, v, S32_SCALE_F2I, 0.0f, d, S32_MIN_F2I, S32_MAX_F2I)
+#define F32_TO_S32(v)		F32_TO_S32_D(v, 0.0f)
 #define F32_TO_S32S(v)		bswap_32(F32_TO_S32(v))
 #define F32_TO_S32S_D(v,d)	bswap_32(F32_TO_S32_D(v,d))
 
@@ -430,9 +440,19 @@ DEFINE_FUNCTION(s16_to_f32d_2, neon);
 DEFINE_FUNCTION(s16_to_f32d, neon);
 DEFINE_FUNCTION(f32d_to_s16, neon);
 #endif
+#if defined(HAVE_RVV)
+DEFINE_FUNCTION(f32d_to_s32, rvv);
+DEFINE_FUNCTION(f32_to_s16, rvv);
+DEFINE_FUNCTION(f32d_to_s16d, rvv);
+DEFINE_FUNCTION(f32d_to_s16, rvv);
+DEFINE_FUNCTION(s16_to_f32d, rvv);
+DEFINE_FUNCTION(s32_to_f32d, rvv);
+#endif
 #if defined(HAVE_SSE2)
 DEFINE_FUNCTION(s16_to_f32d_2, sse2);
 DEFINE_FUNCTION(s16_to_f32d, sse2);
+DEFINE_FUNCTION(s16s_to_f32d, sse2);
+DEFINE_FUNCTION(s16s_to_f32d_2, sse2);
 DEFINE_FUNCTION(s24_to_f32d, sse2);
 DEFINE_FUNCTION(s32_to_f32d, sse2);
 DEFINE_FUNCTION(f32d_to_s32, sse2);
@@ -440,6 +460,8 @@ DEFINE_FUNCTION(f32d_to_s32_noise, sse2);
 DEFINE_FUNCTION(f32_to_s16, sse2);
 DEFINE_FUNCTION(f32d_to_s16_2, sse2);
 DEFINE_FUNCTION(f32d_to_s16, sse2);
+DEFINE_FUNCTION(f32d_to_s16s_2, sse2);
+DEFINE_FUNCTION(f32d_to_s16s, sse2);
 DEFINE_FUNCTION(f32d_to_s16_noise, sse2);
 DEFINE_FUNCTION(f32d_to_s16d, sse2);
 DEFINE_FUNCTION(f32d_to_s16d_noise, sse2);
@@ -457,6 +479,8 @@ DEFINE_FUNCTION(s24_to_f32d, sse41);
 #if defined(HAVE_AVX2)
 DEFINE_FUNCTION(s16_to_f32d_2, avx2);
 DEFINE_FUNCTION(s16_to_f32d, avx2);
+DEFINE_FUNCTION(s16s_to_f32d, avx2);
+DEFINE_FUNCTION(s16s_to_f32d_2, avx2);
 DEFINE_FUNCTION(s24_to_f32d, avx2);
 DEFINE_FUNCTION(s32_to_f32d, avx2);
 DEFINE_FUNCTION(f32d_to_s32, avx2);
diff --git a/spa/plugins/audioconvert/hilbert.h b/spa/plugins/audioconvert/hilbert.h
index 7abef708..aa00940b 100644
--- a/spa/plugins/audioconvert/hilbert.h
+++ b/spa/plugins/audioconvert/hilbert.h
@@ -43,6 +43,13 @@ static inline int hilbert_generate(float *taps, int n_taps)
 	return 0;
 }
 
+static inline void reverse_taps(float *taps, int n_taps)
+{
+	int i;
+	for (i = 0; i < n_taps/2; i++)
+		SPA_SWAP(taps[i], taps[n_taps-1-i]);
+}
+
 #ifdef __cplusplus
 } /* extern "C" */
 #endif
diff --git a/spa/plugins/audioconvert/meson.build b/spa/plugins/audioconvert/meson.build
index d18f64b5..394bc11e 100644
--- a/spa/plugins/audioconvert/meson.build
+++ b/spa/plugins/audioconvert/meson.build
@@ -105,10 +105,51 @@ if have_neon
   simd_dependencies += audioconvert_neon
 endif
 
+if have_rvv
+  audioconvert_rvv = static_library('audioconvert_rvv',
+    ['fmt-ops-rvv.c' ],
+    c_args : ['-O3', '-DHAVE_RVV'],
+    dependencies : [ spa_dep ],
+    install : false
+    )
+  simd_cargs += ['-DHAVE_RVV']
+  simd_dependencies += audioconvert_rvv
+endif
+
+sparesampledumpcoeffs_sources = [
+  'resample-native.c',
+  'resample-native-c.c',
+  'spa-resample-dump-coeffs.c',
+]
+
+sparesampledumpcoeffs = executable(
+  'spa-resample-dump-coeffs',
+  sparesampledumpcoeffs_sources,
+  c_args : [ cc_flags_native, '-DRESAMPLE_DISABLE_PRECOMP' ],
+  dependencies : [ spa_dep, mathlib_native ],
+  install : false,
+  native : true,
+)
+
+precomptuples = []
+foreach tuple : get_option('resampler-precomp-tuples')
+  precomptuples += '-t ' + tuple
+endforeach
+
+resample_native_precomp_h = custom_target(
+  'resample-native-precomp.h',
+  output : 'resample-native-precomp.h',
+  capture : true,
+  command : [
+    sparesampledumpcoeffs,
+  ] + precomptuples
+)
+
 audioconvert_lib = static_library('audioconvert',
   ['fmt-ops.c',
     'channelmix-ops.c',
     'peaks-ops.c',
+    resample_native_precomp_h,
     'resample-native.c',
     'resample-peaks.c',
     'wavfile.c',
@@ -144,6 +185,7 @@ test_apps = [
   'test-fmt-ops',
   'test-peaks',
   'test-resample',
+  'test-resample-delay',
   ]
 
 foreach a : test_apps
diff --git a/spa/plugins/audioconvert/resample-native.c b/spa/plugins/audioconvert/resample-native.c
index 0183d368..f393e3dc 100644
--- a/spa/plugins/audioconvert/resample-native.c
+++ b/spa/plugins/audioconvert/resample-native.c
@@ -7,6 +7,9 @@
 #include <spa/param/audio/format.h>
 
 #include "resample-native-impl.h"
+#ifndef RESAMPLE_DISABLE_PRECOMP
+#include "resample-native-precomp.h"
+#endif
 
 struct quality {
 	uint32_t n_taps;
@@ -302,14 +305,36 @@ static void impl_native_reset (struct resample *r)
 	if (r->options & RESAMPLE_OPTION_PREFILL)
 		d->hist = d->n_taps - 1;
 	else
-		d->hist = (d->n_taps / 2) - 1;
+		d->hist = d->n_taps / 2;
 	d->phase = 0;
 }
 
 static uint32_t impl_native_delay (struct resample *r)
 {
 	struct native_data *d = r->data;
-	return d->n_taps / 2;
+	return d->n_taps / 2 - 1;
+}
+
+static float impl_native_phase (struct resample *r)
+{
+	struct native_data *d = r->data;
+	float pho = 0;
+
+	if (d->func == d->info->process_full) {
+		pho = -(float)((int32_t)d->phase) / d->out_rate;
+
+		/* XXX: this is how it seems to behave, but not clear why */
+		if (d->hist >= d->n_taps - 1)
+			pho += 1.0f;
+	} else if (d->func == d->info->process_inter) {
+		pho = -d->phase / d->out_rate;
+
+		/* XXX: this is how it seems to behave, but not clear why */
+		if (d->hist >= d->n_taps - 1)
+			pho += 1.0f;
+	}
+
+	return pho;
 }
 
 int resample_native_init(struct resample *r)
@@ -328,6 +353,7 @@ int resample_native_init(struct resample *r)
 	r->process = impl_native_process;
 	r->reset = impl_native_reset;
 	r->delay = impl_native_delay;
+	r->phase = impl_native_phase;
 
 	q = &window_qualities[r->quality];
 
@@ -375,7 +401,25 @@ int resample_native_init(struct resample *r)
 	for (c = 0; c < r->channels; c++)
 		d->history[c] = SPA_PTROFF(d->hist_mem, c * history_stride, float);
 
-	build_filter(d->filter, d->filter_stride, n_taps, n_phases, scale);
+#ifndef RESAMPLE_DISABLE_PRECOMP
+	/* See if we have precomputed coefficients */
+	for (c = 0; precomp_coeffs[c].filter; c++) {
+		if (precomp_coeffs[c].in_rate == r->i_rate &&
+				precomp_coeffs[c].out_rate == r->o_rate &&
+				precomp_coeffs[c].quality == r->quality)
+			break;
+	}
+
+	if (precomp_coeffs[c].filter) {
+		spa_log_debug(r->log, "using precomputed filter for %u->%u(%u)",
+				r->i_rate, r->o_rate, r->quality);
+		spa_memcpy(d->filter, precomp_coeffs[c].filter, filter_size);
+	} else {
+#endif
+		build_filter(d->filter, d->filter_stride, n_taps, n_phases, scale);
+#ifndef RESAMPLE_DISABLE_PRECOMP
+	}
+#endif
 
 	d->info = find_resample_info(SPA_AUDIO_FORMAT_F32, r->cpu_flags);
 	if (SPA_UNLIKELY(d->info == NULL)) {
diff --git a/spa/plugins/audioconvert/resample-peaks.c b/spa/plugins/audioconvert/resample-peaks.c
index 51c28e7b..aade7711 100644
--- a/spa/plugins/audioconvert/resample-peaks.c
+++ b/spa/plugins/audioconvert/resample-peaks.c
@@ -100,6 +100,11 @@ static void impl_peaks_reset (struct resample *r)
 	d->i_count = d->o_count = 0;
 }
 
+static float impl_peaks_phase (struct resample *r)
+{
+	return 0;
+}
+
 int resample_peaks_init(struct resample *r)
 {
 	struct peaks_data *d;
@@ -125,6 +130,7 @@ int resample_peaks_init(struct resample *r)
 	r->delay = impl_peaks_delay;
 	r->in_len = impl_peaks_in_len;
 	r->out_len = impl_peaks_out_len;
+	r->phase = impl_peaks_phase;
 
 	spa_log_debug(r->log, "peaks %p: in:%d out:%d features:%08x:%08x", r,
 			r->i_rate, r->o_rate, r->cpu_flags, d->peaks.cpu_flags);
diff --git a/spa/plugins/audioconvert/resample.h b/spa/plugins/audioconvert/resample.h
index a07b559c..5308fa82 100644
--- a/spa/plugins/audioconvert/resample.h
+++ b/spa/plugins/audioconvert/resample.h
@@ -32,6 +32,10 @@ struct resample {
 				 void * SPA_RESTRICT dst[], uint32_t *out_len);
 	void (*reset)		(struct resample *r);
 	uint32_t (*delay)	(struct resample *r);
+
+	/** Fractional part of delay (in input samples) */
+	float (*phase)		(struct resample *r);
+
 	void *data;
 };
 
@@ -42,6 +46,7 @@ struct resample {
 #define resample_process(r,...)		(r)->process(r,__VA_ARGS__)
 #define resample_reset(r)		(r)->reset(r)
 #define resample_delay(r)		(r)->delay(r)
+#define resample_phase(r)		(r)->phase(r)
 
 int resample_native_init(struct resample *r);
 int resample_peaks_init(struct resample *r);
diff --git a/spa/plugins/audioconvert/spa-resample-dump-coeffs.c b/spa/plugins/audioconvert/spa-resample-dump-coeffs.c
new file mode 100644
index 00000000..154792fc
--- /dev/null
+++ b/spa/plugins/audioconvert/spa-resample-dump-coeffs.c
@@ -0,0 +1,200 @@
+/* Spa */
+/* SPDX-FileCopyrightText: Copyright © 2024 Arun Raghavan <arun@asymptotic.io> */
+/* SPDX-License-Identifier: MIT */
+
+#include <stdio.h>
+#include <string.h>
+#include <errno.h>
+#include <getopt.h>
+
+#include <spa/support/log-impl.h>
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+
+SPA_LOG_IMPL(logger);
+
+#include "resample.h"
+#include "resample-native-impl.h"
+
+#define OPTIONS		"ht:"
+static const struct option long_options[] = {
+	{ "help",	no_argument,		NULL, 'h'},
+
+	{ "tuple",	required_argument,	NULL, 't' },
+
+        { NULL, 0, NULL, 0 }
+};
+
+static void show_usage(const char *name, bool is_error)
+{
+	FILE *fp;
+
+	fp = is_error ? stderr : stdout;
+
+	fprintf(fp, "%s [options]\n", name);
+	fprintf(fp,
+		"  -h, --help                            Show this help\n"
+		"\n"
+		"  -t  --tuple                            Sample rate tuple (as \"in_rate,out_rate[,quality]\")\n"
+		"\n");
+}
+
+static void parse_tuple(const char *arg, int *in, int *out, int *quality)
+{
+	char tuple[256];
+	char *token;
+
+	strncpy(tuple, arg, sizeof(tuple) - 1);
+	*in = 0;
+	*out = 0;
+
+	token = strtok(tuple, ",");
+	if (!token || !spa_atoi32(token, in, 10))
+		return;
+
+	token = strtok(NULL, ",");
+	if (!token || !spa_atoi32(token, out, 10))
+		return;
+
+	token = strtok(NULL, ",");
+	if (!token) {
+		*quality = RESAMPLE_DEFAULT_QUALITY;
+	} else if (!spa_atoi32(token, quality, 10)) {
+		*quality = -1;
+		return;
+	}
+
+	/* first, second now contain zeroes on error, or the numbers on success,
+	 * third contains a quality or -1 on error, default value if unspecified */
+}
+
+#define PREFIX "__precomp_coeff"
+
+static void dump_header(void)
+{
+	printf("/* This is a generated file, see spa-resample-dump-coeffs.c */");
+	printf("\n#include <stdint.h>\n");
+	printf("\n#include <stdlib.h>\n");
+	printf("\n");
+	printf("struct resample_coeffs {\n");
+	printf("\tuint32_t in_rate;\n");
+	printf("\tuint32_t out_rate;\n");
+	printf("\tint quality;\n");
+	printf("\tconst float *filter;\n");
+	printf("};\n");
+}
+
+static void dump_footer(const uint32_t *ins, const uint32_t *outs, const int *qualities)
+{
+	printf("\n");
+	printf("static const struct resample_coeffs precomp_coeffs[] = {\n");
+	while (*ins && *outs) {
+		printf("\t{ .in_rate = %u, .out_rate = %u, .quality = %u, "
+				".filter = %s_%u_%u_%u },\n",
+				*ins, *outs, *qualities, PREFIX, *ins, *outs, *qualities);
+		ins++;
+		outs++;
+		qualities++;
+	}
+	printf("\t{ .in_rate = 0, .out_rate = 0, .quality = 0, .filter = NULL },\n");
+	printf("};\n");
+}
+
+static void dump_coeffs(unsigned int in_rate, unsigned int out_rate, int quality)
+{
+	struct resample r = { 0, };
+	struct native_data *d;
+	unsigned int i, filter_size;
+	int ret;
+
+	r.log = &logger.log;
+	r.i_rate = in_rate;
+	r.o_rate = out_rate;
+	r.quality = quality;
+	r.channels = 1; /* irrelevant for generated taps */
+
+	if ((ret = resample_native_init(&r)) < 0) {
+		fprintf(stderr, "can't init converter: %s\n", spa_strerror(ret));
+		return;
+	}
+
+	d = r.data;
+	filter_size = d->filter_stride * (d->n_phases + 1);
+
+	printf("\n");
+	printf("static const float %s_%u_%u_%u[] = {", PREFIX, in_rate, out_rate, quality);
+	for (i = 0; i < filter_size; i++) {
+		printf("%a", d->filter[i]);
+		if (i != filter_size - 1)
+			printf(",");
+	}
+	printf("};\n");
+
+	if (r.free)
+		r.free(&r);
+}
+
+int main(int argc, char* argv[])
+{
+	unsigned int ins[256] = { 0, }, outs[256] = { 0, };
+	int qualities[256] = { 0, };
+	int in_rate = 0, out_rate = 0, quality = 0;
+	int c, longopt_index = 0, i = 0;
+
+	while ((c = getopt_long(argc, argv, OPTIONS, long_options, &longopt_index)) != -1) {
+		switch (c) {
+		case 'h':
+                        show_usage(argv[0], false);
+                        return EXIT_SUCCESS;
+		case 't':
+			parse_tuple(optarg, &in_rate, &out_rate, &quality);
+			if (in_rate <= 0) {
+				fprintf(stderr, "error: bad input rate %d\n", in_rate);
+                                goto error;
+			}
+			if (out_rate <= 0) {
+				fprintf(stderr, "error: bad output rate %d\n", out_rate);
+                                goto error;
+			}
+			if (quality < 0 || quality > 14) {
+				fprintf(stderr, "error: bad quality value %s\n", optarg);
+                                goto error;
+			}
+			ins[i] = in_rate;
+			outs[i] = out_rate;
+			qualities[i] = quality;
+			i++;
+			break;
+		default:
+			fprintf(stderr, "error: unknown option\n");
+			goto error_usage;
+		}
+	}
+
+	if (optind != argc) {
+                fprintf(stderr, "error: got %d extra argument(s))\n",
+				optind - argc);
+		goto error_usage;
+	}
+	if (in_rate == 0) {
+                fprintf(stderr, "error: input rate must be specified\n");
+		goto error;
+	}
+	if (out_rate == 0) {
+                fprintf(stderr, "error: input rate must be specified\n");
+		goto error;
+	}
+
+	dump_header();
+	while (i--) {
+		dump_coeffs(ins[i], outs[i], qualities[i]);
+	}
+	dump_footer(ins, outs, qualities);
+
+	return EXIT_SUCCESS;
+
+error_usage:
+	show_usage(argv[0], true);
+error:
+	return EXIT_FAILURE;
+}
diff --git a/spa/plugins/audioconvert/spa-resample.c b/spa/plugins/audioconvert/spa-resample.c
index 1c90a713..ec752c65 100644
--- a/spa/plugins/audioconvert/spa-resample.c
+++ b/spa/plugins/audioconvert/spa-resample.c
@@ -119,6 +119,8 @@ sf_str_to_fmt(const char *str)
 
 static int open_files(struct data *d)
 {
+        int i, count = 0, format = -1;
+
 	d->ifile = sf_open(d->iname, SFM_READ, &d->iinfo);
         if (d->ifile == NULL) {
 		fprintf(stderr, "error: failed to open input file \"%s\": %s\n",
@@ -128,8 +130,32 @@ static int open_files(struct data *d)
 
 	d->oinfo.channels = d->iinfo.channels;
 	d->oinfo.samplerate = d->rate > 0 ? d->rate : d->iinfo.samplerate;
-	d->oinfo.format = d->format > 0 ? d->format : d->iinfo.format;
-	d->oinfo.format |= SF_FORMAT_WAV;
+	d->oinfo.format = d->format > 0 ? d->format : (d->iinfo.format & SF_FORMAT_SUBMASK);
+
+	/* try to guess the format from the extension */
+	if (sf_command(NULL, SFC_GET_FORMAT_MAJOR_COUNT, &count, sizeof(int)) != 0)
+		count = 0;
+
+	for (i = 0; i < count; i++) {
+		SF_FORMAT_INFO fi;
+
+		spa_zero(fi);
+		fi.format = i;
+		if (sf_command(NULL, SFC_GET_FORMAT_MAJOR, &fi, sizeof(fi)) != 0)
+			continue;
+
+		if (spa_strendswith(d->oname, fi.extension)) {
+			format = fi.format;
+			break;
+		}
+	}
+	if (format == -1)
+		/* use the same format as the input file otherwise */
+		format = d->iinfo.format & ~SF_FORMAT_SUBMASK;
+	if (format == SF_FORMAT_WAV && d->oinfo.channels > 2)
+		format = SF_FORMAT_WAVEX;
+
+	d->oinfo.format |= format;
 
 	d->ofile = sf_open(d->oname, SFM_WRITE, &d->oinfo);
         if (d->ofile == NULL) {
diff --git a/spa/plugins/audioconvert/test-fmt-ops.c b/spa/plugins/audioconvert/test-fmt-ops.c
index b14da3a5..17a26a35 100644
--- a/spa/plugins/audioconvert/test-fmt-ops.c
+++ b/spa/plugins/audioconvert/test-fmt-ops.c
@@ -228,6 +228,16 @@ static void test_f32_s16(void)
 			false, true, conv_f32d_to_s16_neon);
 	}
 #endif
+#if defined(HAVE_RVV)
+	if (cpu_flags & SPA_CPU_FLAG_RISCV_V) {
+		run_test("test_f32_s16_rvv", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+			true, true, conv_f32_to_s16_rvv);
+		run_test("test_f32d_s16d_rvv", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+			false, false, conv_f32d_to_s16d_rvv);
+		run_test("test_f32d_s16_rvv", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+			false, true, conv_f32d_to_s16_rvv);
+	}
+#endif
 }
 
 static void test_s16_f32(void)
@@ -261,6 +271,12 @@ static void test_s16_f32(void)
 			true, false, conv_s16_to_f32d_neon);
 	}
 #endif
+#if defined(HAVE_RVV)
+	if (cpu_flags & SPA_CPU_FLAG_RISCV_V) {
+		run_test("test_s16_f32d_rvv", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+			true, false, conv_s16_to_f32d_rvv);
+	}
+#endif
 }
 
 static void test_f32_u32(void)
@@ -291,10 +307,21 @@ static void test_u32_f32(void)
 static void test_f32_s32(void)
 {
 	static const float in[] = { 0.0f, 1.0f, -1.0f, 0.5f, -0.5f, 1.1f, -1.1f,
-		1.0f/0xa00000, 1.0f/0x1000000, -1.0f/0xa00000, -1.0f/0x1000000 };
-	static const int32_t out[] = { 0, 0x7fffff00, 0x80000000, 0x40000000, 0xc0000000,
-					0x7fffff00, 0x80000000,
-		0x00000100, 0x00000000, 0xffffff00, 0x00000000 };
+		1.0f/0xa00000, -1.0f/0xa00000, 1.0f/0x800000, -1.0f/0x800000,
+		1.0f/0x1000000, -1.0f/0x1000000, 1.0f/0x2000000, -1.0f/0x2000000,
+		1.0f/0x4000000, -1.0f/0x4000000, 1.0f/0x8000000, -1.0f/0x8000000,
+		1.0f/0x10000000, -1.0f/0x10000000, 1.0f/0x20000000, -1.0f/0x20000000,
+		1.0f/0x40000000, -1.0f/0x40000000, 1.0f/0x80000000, -1.0f/0x80000000,
+		1.0f/0x100000000, -1.0f/0x100000000, 1.0f/0x200000000, -1.0f/0x200000000,
+	};
+	static const int32_t out[] = { 0x00000000, 0x7fffff80, 0x80000000,
+		0x40000000, 0xc0000000, 0x7fffff80, 0x80000000, 0x000000cd,
+		0xffffff33, 0x00000100, 0xffffff00, 0x00000080, 0xffffff80,
+		0x00000040, 0xffffffc0, 0x00000020, 0xffffffe0, 0x00000010,
+		0xfffffff0, 0x00000008, 0xfffffff8, 0x00000004, 0xfffffffc,
+		0x00000002, 0xfffffffe, 0x00000001, 0xffffffff, 0x00000000,
+		0x00000000, 0x00000000, 0x00000000,
+	};
 
 	run_test("test_f32_s32", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
 			true, true, conv_f32_to_s32_c);
@@ -316,12 +343,27 @@ static void test_f32_s32(void)
 			false, true, conv_f32d_to_s32_avx2);
 	}
 #endif
+#if defined(HAVE_RVV)
+	if (cpu_flags & SPA_CPU_FLAG_RISCV_V) {
+		run_test("test_f32d_s32_rvv", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+			false, true, conv_f32d_to_s32_rvv);
+	}
+#endif
 }
 
 static void test_s32_f32(void)
 {
-	static const int32_t in[] = { 0, 0x7fffff00, 0x80000000, 0x40000000, 0xc0000000 };
-	static const float out[] = { 0.0f, 0.999999880791f, -1.0f, 0.5, -0.5, };
+	static const int32_t in[] = { 0, 0x7FFFFFFF, 0x80000000, 0x7fffff00,
+		0x80000100, 0x40000000, 0xc0000000, 0x0080, 0xFFFFFF80, 0x0100,
+		0xFFFFFF00, 0x0200, 0xFFFFFE00
+	};
+
+	static const float out[] = { 0.e+00f, 1.e+00f, -1.e+00f,
+		9.9999988079071044921875e-01f, -9.9999988079071044921875e-01f, 5.e-01f,
+		-5.e-01f, 5.9604644775390625e-08f, -5.9604644775390625e-08f,
+		1.1920928955078125e-07f, -1.1920928955078125e-07f,
+		2.384185791015625e-07f, -2.384185791015625e-07f
+	};
 
 	run_test("test_s32_f32d", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
 			true, false, conv_s32_to_f32d_c);
@@ -343,6 +385,12 @@ static void test_s32_f32(void)
 			true, false, conv_s32_to_f32d_avx2);
 	}
 #endif
+#if defined(HAVE_RVV)
+	if (cpu_flags & SPA_CPU_FLAG_RISCV_V) {
+		run_test("test_s32_f32d_rvv", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+			true, false, conv_s32_to_f32d_rvv);
+	}
+#endif
 }
 
 static void test_f32_u24(void)
@@ -600,15 +648,81 @@ static void test_lossless_u24(void)
 	}
 }
 
-static void test_lossless_s32(void)
+static void test_lossless_s25_32_to_f32_to_s25_32(void)
+{
+	int32_t i;
+
+	fprintf(stderr, "test %s:\n", __func__);
+	for (i = S25_MIN; i <= S25_MAX; i+=11) {
+		float v = S25_32_TO_F32(i);
+		int32_t t = F32_TO_S25_32(v);
+		spa_assert_se(i == t);
+	}
+}
+
+static void test_lossless_s25_32_to_s32_to_f32_to_s25_32(void)
 {
 	int32_t i;
 
 	fprintf(stderr, "test %s:\n", __func__);
-	for (i = S32_MIN; i < S32_MAX; i+=255) {
+	for (i = S25_MIN; i <= S25_MAX; i+=13) {
+		float v = S32_TO_F32(S25_32_TO_S32(i));
+		int32_t t = F32_TO_S25_32(v);
+		spa_assert_se(i == t);
+	}
+}
+
+static void test_lossless_s25_32_to_s32_to_f32_to_s32_to_s25_32(void)
+{
+	int32_t i;
+
+	fprintf(stderr, "test %s:\n", __func__);
+	for (i = S25_MIN; i <= S25_MAX; i+=11) {
+		float v = S32_TO_F32(S25_32_TO_S32(i));
+		int32_t t = S32_TO_S25_32(F32_TO_S32(v));
+		spa_assert_se(i == t);
+	}
+}
+
+static void test_lossless_s25_32_to_f32_to_s32_to_s25_32(void)
+{
+	int32_t i;
+
+	fprintf(stderr, "test %s:\n", __func__);
+	for (i = S25_MIN; i <= S25_MAX; i+=11) {
+		float v = S25_32_TO_F32(i);
+		int32_t t = S32_TO_S25_32(F32_TO_S32(v));
+		spa_assert_se(i == t);
+	}
+}
+
+static void test_lossless_s32(void)
+{
+	int64_t i;
+
+	fprintf(stderr, "test %s:\n", __func__);
+	for (i = S32_MIN; i < S32_MAX; i += 63) {
 		float v = S32_TO_F32(i);
 		int32_t t = F32_TO_S32(v);
-		spa_assert_se(SPA_ABS(i - t) <= 256);
+		spa_assert_se(SPA_ABS(i - t) <= 126);
+		// NOTE: 126 is the maximal absolute error given step=1,
+		// for wider steps it may (errneously) be lower,
+		// because we may not check some integer that would bump it.
+	}
+}
+
+static void test_lossless_s32_lossless_subset(void)
+{
+	int32_t i, j;
+
+	fprintf(stderr, "test %s:\n", __func__);
+	for (i = S25_MIN; i <= S25_MAX; i+=11) {
+		for(j = 0; j < 8; ++j) {
+			int32_t s = i * (1<<j);
+			float v = S32_TO_F32(s);
+			int32_t t = F32_TO_S32(v);
+			spa_assert_se(s == t);
+		}
 	}
 }
 
@@ -775,7 +889,12 @@ int main(int argc, char *argv[])
 	test_lossless_u16();
 	test_lossless_s24();
 	test_lossless_u24();
+	test_lossless_s25_32_to_f32_to_s25_32();
+	test_lossless_s25_32_to_s32_to_f32_to_s25_32();
+	test_lossless_s25_32_to_s32_to_f32_to_s32_to_s25_32();
+	test_lossless_s25_32_to_f32_to_s32_to_s25_32();
 	test_lossless_s32();
+	test_lossless_s32_lossless_subset();
 	test_lossless_u32();
 
 	test_swaps();
diff --git a/spa/plugins/audioconvert/test-resample-delay.c b/spa/plugins/audioconvert/test-resample-delay.c
new file mode 100644
index 00000000..91a04a5a
--- /dev/null
+++ b/spa/plugins/audioconvert/test-resample-delay.c
@@ -0,0 +1,456 @@
+/* Spa */
+/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */
+/* SPDX-License-Identifier: MIT */
+
+#include <string.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <getopt.h>
+#include <errno.h>
+#include <time.h>
+#include <math.h>
+
+#include <spa/support/log-impl.h>
+#include <spa/debug/mem.h>
+
+SPA_LOG_IMPL(logger);
+
+#include "resample.h"
+
+
+static float samp_in[65536];
+static float samp_out[65536];
+static bool force_print;
+
+static void assert_test(bool check)
+{
+	if (!check)
+		fprintf(stderr, "FAIL\n\n");
+#if 1
+	spa_assert_se(check);
+#endif
+}
+
+static double difference(double delay, const float *a, size_t len_a, const float *b, size_t len_b, double b_rate)
+{
+	size_t i;
+	float c = 0, wa = 0, wb = 0;
+
+	/* Difference measure: sum((a-b)^2) / sqrt(sum a^2 sum b^2); restricted to overlap */
+	for (i = 0; i < len_a; ++i) {
+		double jf = (i + delay) * b_rate;
+		int j;
+		double x;
+		float bv;
+
+		j = (int)floor(jf);
+		if (j < 0 || j + 1 >= (int)len_b)
+			continue;
+
+		x = jf - j;
+		bv = (float)((1 - x) * b[j] + x * b[j + 1]);
+
+		c += (a[i] - bv) * (a[i] - bv);
+		wa += a[i]*a[i];
+		wb = bv*bv;
+	}
+
+	if (wa == 0 || wb == 0)
+		return 1e30;
+
+	return c / sqrt(wa * wb);
+}
+
+struct find_delay_data {
+	const float *a;
+	const float *b;
+	size_t len_a;
+	size_t len_b;
+	double b_rate;
+};
+
+static double find_delay_func(double x, void *user_data)
+{
+	const struct find_delay_data *data = user_data;
+
+	return difference((float)x, data->a, data->len_a, data->b, data->len_b, data->b_rate);
+}
+
+static double minimum(double x1, double x4, double (*func)(double, void *), void *user_data, double tol)
+{
+	/* Find minimum with golden section search */
+	const double phi = (1 + sqrt(5)) / 2;
+	double x2, x3;
+	double f2, f3;
+
+	spa_assert(x4 >= x1);
+
+	x2 = x4 - (x4 - x1) / phi;
+	x3 = x1 + (x4 - x1) / phi;
+
+	f2 = func(x2, user_data);
+	f3 = func(x3, user_data);
+
+	while (x4 - x1 > tol) {
+		if (f2 > f3) {
+			x1 = x2;
+			x2 = x3;
+			x3 = x1 + (x4 - x1) / phi;
+			f2 = f3;
+			f3 = func(x3, user_data);
+		} else {
+			x4 = x3;
+			x3 = x2;
+			x2 = x4 - (x4 - x1) / phi;
+			f3 = f2;
+			f2 = func(x2, user_data);
+		}
+	}
+
+	return (f2 < f3) ? x2 : x3;
+}
+
+static double find_delay(const float *a, size_t len_a, const float *b, size_t len_b, double b_rate, int maxdelay, double tol)
+{
+	struct find_delay_data data = { .a = a, .len_a = len_a, .b = b, .len_b = len_b, .b_rate = b_rate };
+	double best_x, best_f;
+	int i;
+
+	best_x = 0;
+	best_f = find_delay_func(best_x, &data);
+
+	for (i = -maxdelay; i <= maxdelay; ++i) {
+		double f = find_delay_func(i, &data);
+
+		if (f < best_f) {
+			best_x = i;
+			best_f = f;
+		}
+	}
+
+	return minimum(best_x - 2, best_x + 2, find_delay_func, &data, tol);
+}
+
+static void test_find_delay(void)
+{
+	float v1[1024];
+	float v2[1024];
+	const double tol = 0.001;
+	double delay, expect;
+	int i;
+
+	fprintf(stderr, "\n\n-- test_find_delay\n\n");
+
+	for (i = 0; i < 1024; ++i) {
+		v1[i] = sinf(0.1f * i);
+		v2[i] = sinf(0.1f * (i - 3.1234f));
+	}
+
+	delay = find_delay(v1, SPA_N_ELEMENTS(v1), v2, SPA_N_ELEMENTS(v2), 1, 50, tol);
+	expect = 3.1234f;
+	fprintf(stderr, "find_delay = %g (exact %g)\n", delay, expect);
+	assert_test(expect - 2*tol < delay && delay < expect + 2*tol);
+
+	for (i = 0; i < 1024; ++i) {
+		v1[i] = sinf(0.1f * i);
+		v2[i] = sinf(0.1f * (i*3.0f/4 - 3.1234f));
+	}
+
+	delay = find_delay(v1, SPA_N_ELEMENTS(v1), v2, SPA_N_ELEMENTS(v2), 4.0/3.0, 50, tol);
+	expect = 3.1234f;
+	fprintf(stderr, "find_delay = %g (exact %g)\n", delay, expect);
+	assert_test(expect - 2*tol < delay && delay < expect + 2*tol);
+}
+
+static uint32_t feed_sine(struct resample *r, uint32_t in, uint32_t *inp, uint32_t *phase, bool print)
+{
+	uint32_t i, out;
+	const void *src[1];
+	void *dst[1];
+
+	for (i = 0; i < in; ++i)
+		samp_in[i] = sinf(0.01f * (*phase + i)) * expf(-0.001f * (*phase + i));
+
+	src[0] = samp_in;
+	dst[0] = samp_out;
+	out = SPA_N_ELEMENTS(samp_out);
+
+	*inp = in;
+	resample_process(r, src, inp, dst, &out);
+	spa_assert_se(*inp == in);
+
+	if (print || force_print) {
+		fprintf(stderr, "inp(%u) = ", in);
+		for (uint32_t i = 0; i < in; ++i)
+			fprintf(stderr, "%g, ", samp_in[i]);
+		fprintf(stderr, "\n\n");
+
+		fprintf(stderr, "out(%u) = ", out);
+		for (uint32_t i = 0; i < out; ++i)
+			fprintf(stderr, "%g, ", samp_out[i]);
+		fprintf(stderr, "\n\n");
+	} else {
+		fprintf(stderr, "inp(%u) = ...\n", in);
+		fprintf(stderr, "out(%u) = ...\n", out);
+	}
+
+	*phase += in;
+	*inp = in;
+	return out;
+}
+
+static void check_delay(double rate, uint32_t out_rate, uint32_t options)
+{
+	const double tol = 0.001;
+	struct resample r;
+	uint32_t in_phase = 0;
+	uint32_t in, out;
+	double expect, got;
+
+	spa_zero(r);
+	r.log = &logger.log;
+	r.channels = 1;
+	r.i_rate = 48000;
+	r.o_rate = out_rate;
+	r.quality = RESAMPLE_DEFAULT_QUALITY;
+	r.options = options;
+	resample_native_init(&r);
+
+	resample_update_rate(&r, rate);
+
+	feed_sine(&r, 512, &in, &in_phase, false);
+
+	/* Test delay */
+	expect = resample_delay(&r) + (double)resample_phase(&r);
+	out = feed_sine(&r, 256, &in, &in_phase, true);
+	got = find_delay(samp_in, in, samp_out, out, out_rate / 48000.0, 100, tol);
+
+	fprintf(stderr, "delay: expect = %g, got = %g\n", expect, got);
+	assert_test(expect - 4*tol < got && got < expect + 4*tol);
+
+	resample_free(&r);
+}
+
+static void test_delay_copy(void)
+{
+	fprintf(stderr, "\n\n-- test_delay_copy (no prefill)\n\n");
+	check_delay(1, 48000, 0);
+
+	fprintf(stderr, "\n\n-- test_delay_copy (prefill)\n\n");
+	check_delay(1, 48000, RESAMPLE_OPTION_PREFILL);
+}
+
+static void test_delay_full(void)
+{
+	const uint32_t rates[] = { 16000, 32000, 44100, 48000, 88200, 96000, 144000, 192000 };
+	unsigned int i;
+
+	for (i = 0; i < SPA_N_ELEMENTS(rates); ++i) {
+		fprintf(stderr, "\n\n-- test_delay_full(%u, no prefill)\n\n", rates[i]);
+		check_delay(1, rates[i], 0);
+		fprintf(stderr, "\n\n-- test_delay_full(%u, prefill)\n\n", rates[i]);
+		check_delay(1, rates[i], RESAMPLE_OPTION_PREFILL);
+	}
+}
+
+static void test_delay_interp(void)
+{
+	fprintf(stderr, "\n\n-- test_delay_interp(no prefill)\n\n");
+	check_delay(1 + 1e-12, 48000, 0);
+
+	fprintf(stderr, "\n\n-- test_delay_interp(prefill)\n\n");
+	check_delay(1 + 1e-12, 48000, RESAMPLE_OPTION_PREFILL);
+}
+
+static void check_delay_vary_rate(double rate, double end_rate, uint32_t out_rate, uint32_t options)
+{
+	const double tol = 0.001;
+	struct resample r;
+	uint32_t in_phase = 0;
+	uint32_t in, out;
+	double expect, got;
+
+	fprintf(stderr, "\n\n-- check_delay_vary_rate(%g, %.14g, %u, %s)\n\n", rate, end_rate, out_rate,
+			(options & RESAMPLE_OPTION_PREFILL) ? "prefill" : "no prefill");
+
+	spa_zero(r);
+	r.log = &logger.log;
+	r.channels = 1;
+	r.i_rate = 48000;
+	r.o_rate = out_rate;
+	r.quality = RESAMPLE_DEFAULT_QUALITY;
+	r.options = options;
+	resample_native_init(&r);
+
+	/* Cause nonzero resampler phase */
+	resample_update_rate(&r, rate);
+	feed_sine(&r, 128, &in, &in_phase, false);
+
+	resample_update_rate(&r, 1.7);
+	feed_sine(&r, 128, &in, &in_phase, false);
+
+	resample_update_rate(&r, end_rate);
+	feed_sine(&r, 128, &in, &in_phase, false);
+	feed_sine(&r, 255, &in, &in_phase, false);
+
+	/* Test delay */
+	expect = (double)resample_delay(&r) + (double)resample_phase(&r);
+	out = feed_sine(&r, 256, &in, &in_phase, true);
+	got = find_delay(samp_in, in, samp_out, out, out_rate/48000.0, 100, tol);
+
+	fprintf(stderr, "delay: expect = %g, got = %g\n", expect, got);
+	assert_test(expect - 4*tol < got && got < expect + 4*tol);
+
+	resample_free(&r);
+}
+
+
+static void test_delay_interp_vary_rate(void)
+{
+	const uint32_t rates[] = { 32000, 44100, 48000, 88200, 96000 };
+	const double factors[] = { 1.0123456789, 1.123456789, 1.203883, 1.23456789, 1.3456789 };
+	unsigned int i, j;
+
+	for (i = 0; i < SPA_N_ELEMENTS(rates); ++i) {
+		for (j = 0; j < SPA_N_ELEMENTS(factors); ++j) {
+			/* Interp at end */
+			check_delay_vary_rate(factors[j], 1 + 1e-12, rates[i], 0);
+
+			/* Copy/full at end */
+			check_delay_vary_rate(factors[j], 1, rates[i], 0);
+
+			/* Interp at end */
+			check_delay_vary_rate(factors[j], 1 + 1e-12, rates[i], RESAMPLE_OPTION_PREFILL);
+
+			/* Copy/full at end */
+			check_delay_vary_rate(factors[j], 1, rates[i], RESAMPLE_OPTION_PREFILL);
+		}
+	}
+}
+
+static void run(uint32_t in_rate, uint32_t out_rate, double end_rate, double mid_rate, uint32_t options)
+{
+	const double tol = 0.001;
+	struct resample r;
+	uint32_t in_phase = 0;
+	uint32_t in, out;
+	double expect, got;
+
+	spa_zero(r);
+	r.log = &logger.log;
+	r.channels = 1;
+	r.i_rate = in_rate;
+	r.o_rate = out_rate;
+	r.quality = RESAMPLE_DEFAULT_QUALITY;
+	r.options = options;
+	resample_native_init(&r);
+
+	/* Cause nonzero resampler phase */
+	if (mid_rate != 0.0) {
+		resample_update_rate(&r, mid_rate);
+		feed_sine(&r, 128, &in, &in_phase, true);
+
+		resample_update_rate(&r, 1.7);
+		feed_sine(&r, 128, &in, &in_phase, true);
+	}
+
+	resample_update_rate(&r, end_rate);
+	feed_sine(&r, 128, &in, &in_phase, true);
+	feed_sine(&r, 255, &in, &in_phase, true);
+
+	/* Test delay */
+	expect = (double)resample_delay(&r) + (double)resample_phase(&r);
+	out = feed_sine(&r, 256, &in, &in_phase, true);
+	got = find_delay(samp_in, in, samp_out, out, ((double)out_rate)/in_rate, 100, tol);
+
+	fprintf(stderr, "delay: expect = %g, got = %g\n", expect, got);
+	if (!(expect - 4*tol < got && got < expect + 4*tol))
+		fprintf(stderr, "FAIL\n\n");
+
+	resample_free(&r);
+}
+
+int main(int argc, char *argv[])
+{
+        static const struct option long_options[] = {
+		{ "in-rate", required_argument, NULL, 'i' },
+		{ "out-rate", required_argument, NULL, 'o' },
+		{ "end-full", no_argument, NULL, 'f' },
+		{ "end-interp", no_argument, NULL, 'p' },
+		{ "mid-rate", required_argument, NULL, 'm' },
+		{ "prefill", no_argument, NULL, 'r' },
+		{ "print", no_argument, NULL, 'P' },
+		{ "help", no_argument, NULL, 'h' },
+		{ NULL, 0, NULL, 0}
+        };
+	const char *help = "%s [options]\n"
+		"\n"
+		"Check resampler delay. If no arguments, run tests.\n"
+		"\n"
+		"-i | --in-rate INRATE      input rate\n"
+		"-o | --out-rate OUTRATE    output rate\n"
+		"-f | --end-full            force full (or copy) resampler\n"
+		"-p | --end-interp          force interp resampler\n"
+		"-m | --mid-rate RELRATE    force rate adjustment in the middle\n"
+		"-r | --prefill             enable prefill\n"
+		"-P | --print               force printing\n"
+		"\n";
+	uint32_t in_rate = 0, out_rate = 0;
+	double end_rate = 1, mid_rate = 0;
+	uint32_t options = 0;
+	int c;
+
+	logger.log.level = SPA_LOG_LEVEL_TRACE;
+
+	while ((c = getopt_long(argc, argv, "i:o:fpm:rPh", long_options, NULL)) != -1) {
+		switch (c) {
+		case 'h':
+			fprintf(stderr, help, argv[0]);
+			return 0;
+		case 'i':
+			if (!spa_atou32(optarg, &in_rate, 0))
+				goto error_arg;
+			break;
+		case 'o':
+			if (!spa_atou32(optarg, &out_rate, 0))
+				goto error_arg;
+			break;
+		case 'f':
+			end_rate = 1;
+			break;
+		case 'p':
+			end_rate = 1 + 1e-12;
+			break;
+		case 'm':
+			if (!spa_atod(optarg, &mid_rate))
+				goto error_arg;
+			break;
+		case 'r':
+			options = RESAMPLE_OPTION_PREFILL;
+			break;
+		case 'P':
+			force_print = true;
+			break;
+		default:
+			goto error_arg;
+		}
+	}
+
+	if (in_rate && out_rate) {
+		run(in_rate, out_rate, end_rate, mid_rate, options);
+		return 0;
+	}
+
+	test_find_delay();
+	test_delay_copy();
+	test_delay_full();
+	test_delay_interp();
+	test_delay_interp_vary_rate();
+
+	return 0;
+
+error_arg:
+	fprintf(stderr, "Invalid arguments\n");
+	return 1;
+}
diff --git a/spa/plugins/audiomixer/mix-ops.h b/spa/plugins/audiomixer/mix-ops.h
index 22147404..0d955963 100644
--- a/spa/plugins/audiomixer/mix-ops.h
+++ b/spa/plugins/audiomixer/mix-ops.h
@@ -123,7 +123,7 @@ void mix_##name##_##arch(struct mix_ops *ops, void * SPA_RESTRICT dst,	\
 		const void * SPA_RESTRICT src[], uint32_t n_src,		\
 		uint32_t n_samples)						\
 
-#define MIX_OPS_MAX_ALIGN	32
+#define MIX_OPS_MAX_ALIGN	32u
 
 DEFINE_FUNCTION(s8, c);
 DEFINE_FUNCTION(u8, c);
diff --git a/spa/plugins/avb/avb-pcm.c b/spa/plugins/avb/avb-pcm.c
index ffe1c748..3f608d7f 100644
--- a/spa/plugins/avb/avb-pcm.c
+++ b/spa/plugins/avb/avb-pcm.c
@@ -37,10 +37,11 @@ static int avb_set_param(struct state *state, const char *k, const char *s)
 		state->default_rate = atoi(s);
 		fmt_change++;
 	} else if (spa_streq(k, SPA_KEY_AUDIO_FORMAT)) {
-		state->default_format = spa_avb_format_from_name(s, strlen(s));
+		state->default_format = spa_type_audio_format_from_short_name(s);
 		fmt_change++;
 	} else if (spa_streq(k, SPA_KEY_AUDIO_POSITION)) {
-		spa_avb_parse_position(&state->default_pos, s, strlen(s));
+		spa_audio_parse_position(s, strlen(s), state->default_pos.pos,
+				&state->default_pos.channels);
 		fmt_change++;
 	} else if (spa_streq(k, SPA_KEY_AUDIO_ALLOWED_RATES)) {
 		state->n_allowed_rates = spa_avb_parse_rates(state->allowed_rates,
diff --git a/spa/plugins/avb/avb-pcm.h b/spa/plugins/avb/avb-pcm.h
index fb2591ff..d4dfa03f 100644
--- a/spa/plugins/avb/avb-pcm.h
+++ b/spa/plugins/avb/avb-pcm.h
@@ -33,6 +33,7 @@ extern "C" {
 #include <spa/param/param.h>
 #include <spa/param/latency-utils.h>
 #include <spa/param/audio/format-utils.h>
+#include <spa/param/audio/raw-json.h>
 
 #include "avb.h"
 
@@ -264,54 +265,17 @@ int spa_avb_skip(struct state *state);
 
 void spa_avb_recycle_buffer(struct state *state, struct port *port, uint32_t buffer_id);
 
-static inline uint32_t spa_avb_format_from_name(const char *name, size_t len)
-{
-	int i;
-	for (i = 0; spa_type_audio_format[i].name; i++) {
-		if (strncmp(name, spa_debug_type_short_name(spa_type_audio_format[i].name), len) == 0)
-			return spa_type_audio_format[i].type;
-	}
-	return SPA_AUDIO_FORMAT_UNKNOWN;
-}
-
-static inline uint32_t spa_avb_channel_from_name(const char *name)
-{
-	int i;
-	for (i = 0; spa_type_audio_channel[i].name; i++) {
-		if (strcmp(name, spa_debug_type_short_name(spa_type_audio_channel[i].name)) == 0)
-			return spa_type_audio_channel[i].type;
-	}
-	return SPA_AUDIO_CHANNEL_UNKNOWN;
-}
-
-static inline void spa_avb_parse_position(struct channel_map *map, const char *val, size_t len)
-{
-	struct spa_json it[2];
-	char v[256];
-
-	spa_json_init(&it[0], val, len);
-        if (spa_json_enter_array(&it[0], &it[1]) <= 0)
-                spa_json_init(&it[1], val, len);
-
-	map->channels = 0;
-	while (spa_json_get_string(&it[1], v, sizeof(v)) > 0 &&
-	    map->channels < SPA_AUDIO_MAX_CHANNELS) {
-		map->pos[map->channels++] = spa_avb_channel_from_name(v);
-	}
-}
-
 static inline uint32_t spa_avb_parse_rates(uint32_t *rates, uint32_t max, const char *val, size_t len)
 {
-	struct spa_json it[2];
+	struct spa_json it[1];
 	char v[256];
 	uint32_t count;
 
-	spa_json_init(&it[0], val, len);
-        if (spa_json_enter_array(&it[0], &it[1]) <= 0)
-                spa_json_init(&it[1], val, len);
+        if (spa_json_begin_array_relax(&it[0], val, len) <= 0)
+		return 0;
 
 	count = 0;
-	while (spa_json_get_string(&it[1], v, sizeof(v)) > 0 && count < max)
+	while (spa_json_get_string(&it[0], v, sizeof(v)) > 0 && count < max)
 		rates[count++] = atoi(v);
 	return count;
 }
diff --git a/spa/plugins/bluez5/README-Telephony.md b/spa/plugins/bluez5/README-Telephony.md
new file mode 100644
index 00000000..93910b05
--- /dev/null
+++ b/spa/plugins/bluez5/README-Telephony.md
@@ -0,0 +1,345 @@
+# PipeWire Bluetooth Telephony service
+
+The Telephony service is a D-Bus service that allows applications to communicate
+with the HFP native backend in order to control phone calls. Phone call features
+are a core part of the HFP specification and are available when a mobile phone
+is paired (therefore, PipeWire acts as the Hands-Free and the phone is the Audio
+Gateway).
+
+The service is exposed on the user session bus by default, but there is an
+option to make it available on the system bus instead.
+
+The service implements its own interfaces alongside the standard DBus
+Introspectable, ObjectManager and Properties interfaces, where needed.
+These interfaces are mostly compatible with the ofono Manager, VoiceCallManager
+and VoiceCall interfaces. For compatibility, the `org.ofono.Manager`,
+`org.ofono.VoiceCallManager` and `org.ofono.VoiceCall` are also implemented
+with any additional compatibility methods & signals are necessary to allow
+ofono-based applications to be able to work just by modifying the service name,
+the manager object path and the operating bus (session vs system).
+
+In addition, to the compatibility interfaces, there is a runtime option to
+also register the service as `org.ofono` on the system bus, making it a drop-in
+replacement for ofono. Note, however, that this service is not a full replacement,
+but only for the Bluetooth-based voice calls.
+
+## Manager Object
+
+```
+Service         org.pipewire.Telephony
+            or  org.ofono
+Object path     /org/pipewire/Telephony
+            or  /
+Implements      org.ofono.Manager
+                org.freedesktop.DBus.Introspectable
+                org.freedesktop.DBus.ObjectManager
+```
+
+The manager object is always available and allows applications to get access to
+the connected audio gateways.
+
+The object path is set to `/` when ofono service compatibility is enabled,
+in which case the service name `org.ofono` is also registered instead of
+`org.pipewire.Telephony`.
+
+The methods and signals below are made available on the `org.ofono.Manager`
+interface, for compatibility. AudioGateway objects are normally announced via
+the standard DBus ObjectManager interface.
+
+### Methods
+
+`array{object,dict} GetModems()`
+
+Get an array of AudioGateway objects and properties that represents the
+currently connected audio gateways.
+
+### Signals
+
+`ModemAdded(object path, dict properties)`
+
+Signal that is sent when a new audio gateway is added. It contains the object
+path of the new audio gateway and also its properties.
+
+`ModemRemoved(object path)`
+
+Signal that is sent when an audio gateway has been removed. The object path is
+no longer accessible after this signal and only emitted for reference.
+
+## AudioGateway Object
+
+```
+Service         org.pipewire.Telephony
+            or  org.ofono
+Object path     /org/pipewire/Telephony/{ag0,ag1,...}
+Implements      org.pipewire.Telephony.AudioGateway1
+                org.ofono.VoiceCallManager
+                org.freedesktop.DBus.Introspectable
+                org.freedesktop.DBus.ObjectManager
+```
+
+Audio gateway objects represent the currently connected AG devices (typically
+mobile phones).
+
+The methods, signals and properties listed below are made available on both
+`org.pipewire.Telephony.AudioGateway1` and
+`org.ofono.VoiceCallManager` interfaces, unless explicitly documented otherwise.
+
+Call objects are announced via both the standard DBus ObjectManager interface
+and via the `org.ofono.VoiceCallManager` interface, for compatibility.
+
+### Methods
+
+`array{object,dict} GetCalls()`
+
+Get an array of call object paths and properties that represents the currently
+present calls.
+
+This method call should only be used once when an application starts up.
+Further call additions and removal shall be monitored via CallAdded and
+CallRemoved signals.
+
+NOTE: This method is implemented only on the `org.ofono.VoiceCallManager`
+interface, for compatibility. Call announcements are normally made available via
+the standard `org.freedesktop.DBus.ObjectManager` interface.
+
+`object Dial(string number)`
+
+Initiates a new outgoing call. Returns the object path to the newly created
+call.
+
+The number must be a string containing the following characters:
+`[0-9+*#,ABCD]{1,80}` In other words, it must be a non-empty string consisting
+of 1 to 80 characters. The character set can contain numbers, `+`, `*`, `#`, `,`
+and the letters `A` to `D`. Besides this sanity checking no further number
+validation is performed. It is assumed that the gateway and/or the network will
+perform further validation.
+
+NOTE: If an active call (single or multiparty) exists, then it is automatically
+put on hold if the dial procedure is successful.
+
+Possible Errors:
+ * org.pipewire.Telephony.Error.InvalidState
+ * org.freedesktop.DBus.Error.InvalidArgs
+ * org.freedesktop.DBus.Error.Failed
+
+`void SwapCalls()`
+
+Swaps Active and Held calls. The effect of this is that all calls (0 or more
+including calls in a multi-party conversation) that were Active are now Held,
+and all calls (0 or more) that were Held are now Active.
+
+GSM specification does not allow calls to be swapped in the case where Held,
+Active and Waiting calls exist. Some modems implement this anyway, thus it is
+manufacturer specific whether this method will succeed in the case of Held,
+Active and Waiting calls.
+
+Possible Errors:
+ * org.pipewire.Telephony.Error.InvalidState
+ * org.freedesktop.DBus.Error.Failed
+
+`void ReleaseAndAnswer()`
+
+Releases currently active call (0 or more) and answers the currently waiting
+call. Please note that if the current call is a multiparty call, then all
+parties in the multi-party call will be released.
+
+Possible Errors:
+ * org.pipewire.Telephony.Error.InvalidState
+ * org.freedesktop.DBus.Error.Failed
+
+`void ReleaseAndSwap()`
+
+Releases currently active call (0 or more) and activates any currently held
+calls. Please note that if the current call is a multiparty call, then all
+parties in the multi-party call will be released.
+
+Possible Errors:
+ * org.pipewire.Telephony.Error.InvalidState
+ * org.freedesktop.DBus.Error.Failed
+
+`void HoldAndAnswer()`
+
+Puts the current call (including multi-party calls) on hold and answers the
+currently waiting call. Calling this function when a user already has a both
+Active and Held calls is invalid, since in GSM a user can have only a single
+Held call at a time.
+
+Possible Errors:
+ * org.pipewire.Telephony.Error.InvalidState
+ * org.freedesktop.DBus.Error.Failed
+
+`void HangupAll()`
+
+Releases all calls except waiting calls. This includes multiparty calls.
+
+Possible Errors:
+ * org.pipewire.Telephony.Error.InvalidState
+ * org.freedesktop.DBus.Error.Failed
+
+`array{object} CreateMultiparty()`
+
+Joins active and held calls together into a multi-party call. If one of the
+calls is already a multi-party call, then the other call is added to the
+multiparty conversation. Returns the new list of calls participating in the
+multiparty call.
+
+There can only be one subscriber controlled multi-party call according to the
+GSM specification.
+
+Possible Errors:
+ * org.pipewire.Telephony.Error.InvalidState
+ * org.freedesktop.DBus.Error.Failed
+
+`void SendTones(string tones)`
+
+Sends the DTMF tones to the network. The tones have a fixed duration. Tones
+can be one of: '0' - '9', '*', '#', 'A', 'B', 'C', 'D'. The last four are
+typically not used in normal circumstances.
+
+Possible Errors:
+ * org.pipewire.Telephony.Error.InvalidState
+ * org.freedesktop.DBus.Error.InvalidArgs
+ * org.freedesktop.DBus.Error.Failed
+
+### Signals
+
+`CallAdded(object path, dict properties)`
+
+Signal that is sent when a new call is added.  It contains the object path of
+the new voice call and also its properties.
+
+NOTE: This method is implemented only on the `org.ofono.VoiceCallManager`
+interface, for compatibility. Call announcements are normally made available via
+the standard `org.freedesktop.DBus.ObjectManager` interface.
+
+`CallRemoved(object path)`
+
+Signal that is sent when a voice call has been released. The object path is no
+longer accessible after this signal and only emitted for reference.
+
+NOTE: This method is implemented only on the `org.ofono.VoiceCallManager`
+interface, for compatibility. Call announcements are normally made available via
+the standard `org.freedesktop.DBus.ObjectManager` interface.
+
+## Call Object
+
+```
+Service         org.pipewire.Telephony
+            or  org.ofono
+Object path     /org/pipewire/Telephony/{ag0,ag1,...}/{call0,call1,...}
+Implements      org.pipewire.Telephony.Call1
+                org.ofono.VoiceCall
+                org.freedesktop.DBus.Introspectable
+                org.freedesktop.DBus.Properties
+```
+
+Call objects represent active calls and allow managing them.
+
+The methods, signals and properties listed below are made available on both
+`org.pipewire.Telephony.Call1` and `org.ofono.VoiceCall`
+interfaces, unless explicitly documented otherwise.
+
+### Methods
+
+`dict GetProperties()`
+
+Returns all properties for this object. See the properties section for available
+properties.
+
+NOTE: This method is implemented only on the `org.ofono.VoiceCall` interface,
+for compatibility. Properties are normally made available via the standard
+`org.freedesktop.DBus.Properties` interface.
+
+`void Answer()`
+
+Answers an incoming call. Only valid if the state of the call is "incoming".
+
+Possible Errors:
+ * org.pipewire.Telephony.Error.InvalidState
+ * org.freedesktop.DBus.Error.Failed
+
+`void Hangup()`
+
+Hangs up the call.
+
+For an incoming call, the call is hung up using ATH or equivalent. For a
+waiting call, the remote party is notified by using the User Determined User
+Busy (UDUB) condition. This is generally implemented using CHLD=0.
+
+Please note that the GSM specification does not allow the release of a held call
+when a waiting call exists. This is because 27.007 allows CHLD=1X to operate
+only on active calls. Hence a held call cannot be hung up without affecting the
+state of the incoming call (e.g. using other CHLD alternatives). Most
+manufacturers provide vendor extensions that do allow the state of the held call
+to be modified using CHLD=1X or equivalent. It should be noted that Bluetooth
+HFP specifies the classic 27.007 behavior and does not allow CHLD=1X to modify
+the state of held calls.
+
+Based on the discussion above, it should also be noted that releasing a
+particular party of a held multiparty call might not be possible on some
+implementations. It is recommended for the applications to structure their UI
+accordingly.
+
+NOTE: Releasing active calls does not produce side-effects. That is the state
+of held or waiting calls is not affected. As an exception, in the case where a
+single active call and a waiting call are present, releasing the active call
+will result in the waiting call transitioning to the 'incoming' state.
+
+Possible Errors:
+ * org.pipewire.Telephony.Error.InvalidState
+ * org.freedesktop.DBus.Error.Failed
+
+### Signals
+
+`PropertyChanged(string property, variant value)`
+
+Signal is emitted whenever a property has changed. The new value is passed as
+the signal argument.
+
+NOTE: This method is implemented only on the `org.ofono.VoiceCall` interface,
+for compatibility. Properties are normally made available via the standard
+`org.freedesktop.DBus.Properties` interface.
+
+### Properties
+
+`string LineIdentification [readonly]`
+
+Contains the Line Identification information returned by the network, if
+present. For incoming calls this is effectively the CLIP. For outgoing calls
+this attribute will hold the dialed number, or the COLP if received by the
+audio gateway.
+
+Please note that COLP may be different from the dialed number. A special
+"withheld" value means the remote party refused to provide caller ID and the
+"override category" option was not provisioned for the current subscriber.
+
+`string IncomingLine [readonly, optional]`
+
+Contains the Called Line Identification information returned by the network.
+This is only available for incoming calls and indicates the local subscriber
+number which was dialed by the remote party. This is useful for subscribers
+which have a multiple line service with their network provider and would like to
+know what line the call is coming in on.
+
+`string Name [readonly]`
+
+Contains the Name Identification information returned by the network, if
+present.
+
+`boolean Multiparty [readonly]`
+
+Contains the indication if the call is part of a multiparty call or not.
+
+Notifications if a call becomes part or leaves a multiparty call are sent.
+
+`string State [readonly]`
+
+Contains the state of the current call. The state can be one of:
+  - "active" - The call is active
+  - "held" - The call is on hold
+  - "dialing" - The call is being dialed
+  - "alerting" - The remote party is being alerted
+  - "incoming" - Incoming call in progress
+  - "waiting" - Call is waiting
+  - "disconnected" - No further use of this object is allowed, it will be
+       destroyed shortly
diff --git a/spa/plugins/bluez5/a2dp-codec-aac.c b/spa/plugins/bluez5/a2dp-codec-aac.c
index 5420e914..bf0b735f 100644
--- a/spa/plugins/bluez5/a2dp-codec-aac.c
+++ b/spa/plugins/bluez5/a2dp-codec-aac.c
@@ -40,6 +40,9 @@ struct impl {
 	uint32_t rate;
 	uint32_t channels;
 	int samplesize;
+
+	uint32_t enc_delay;
+	uint32_t dec_delay;
 };
 
 static bool eld_supported(void)
@@ -68,7 +71,7 @@ done:
 }
 
 static int codec_fill_caps(const struct media_codec *codec, uint32_t flags,
-		uint8_t caps[A2DP_MAX_CAPS_SIZE])
+		const struct spa_dict *settings, uint8_t caps[A2DP_MAX_CAPS_SIZE])
 {
 	const a2dp_aac_t a2dp_aac = {
 		.object_type =
@@ -364,7 +367,15 @@ static void *codec_init(const struct media_codec *codec, uint32_t flags,
 	if (res != AACENC_OK)
 		goto error;
 
-	if (conf->object_type & AAC_OBJECT_TYPE_MPEG4_AAC_ELD) {
+	/* If object type has multiple bits set (invalid per spec, see above),
+	 * assume the device usually means AAC-LC.
+	 */
+	if (conf->object_type & (AAC_OBJECT_TYPE_MPEG2_AAC_LC |
+						AAC_OBJECT_TYPE_MPEG4_AAC_LC)) {
+		res = aacEncoder_SetParam(this->aacenc, AACENC_AOT, AOT_AAC_LC);
+		if (res != AACENC_OK)
+			goto error;
+	} else if (conf->object_type & AAC_OBJECT_TYPE_MPEG4_AAC_ELD) {
 		res = aacEncoder_SetParam(this->aacenc, AACENC_AOT, AOT_ER_AAC_ELD);
 		if (res != AACENC_OK)
 			goto error;
@@ -372,11 +383,6 @@ static void *codec_init(const struct media_codec *codec, uint32_t flags,
 		res = aacEncoder_SetParam(this->aacenc,  AACENC_SBR_MODE, 1);
 		if (res != AACENC_OK)
 			goto error;
-	} else if (conf->object_type & (AAC_OBJECT_TYPE_MPEG2_AAC_LC |
-						AAC_OBJECT_TYPE_MPEG4_AAC_LC)) {
-		res = aacEncoder_SetParam(this->aacenc, AACENC_AOT, AOT_AAC_LC);
-		if (res != AACENC_OK)
-			goto error;
 	} else {		
 		res = -EINVAL;
 		goto error;
@@ -440,6 +446,8 @@ static void *codec_init(const struct media_codec *codec, uint32_t flags,
 	if (res != AACENC_OK)
 		goto error;
 
+	this->enc_delay = enc_info.nDelay;
+
 	this->codesize = enc_info.frameLength * this->channels * this->samplesize;
 
 	this->aacdec = aacDecoder_Open(TT_MP4_LATM_MCP1, 1);
@@ -468,6 +476,8 @@ static void *codec_init(const struct media_codec *codec, uint32_t flags,
 	}
 #endif
 
+	this->dec_delay = 0;
+
 	return this;
 
 error:
@@ -647,6 +657,21 @@ static int codec_increase_bitpool(void *data)
 	return codec_change_bitrate(this, (this->cur_bitrate * 4) / 3);
 }
 
+static void codec_get_delay(void *data, uint32_t *encoder, uint32_t *decoder)
+{
+	struct impl *this = data;
+
+	if (encoder)
+		*encoder = this->enc_delay;
+
+	if (decoder) {
+		CStreamInfo *info = aacDecoder_GetStreamInfo(this->aacdec);
+		if (info)
+			this->dec_delay = info->outputDelay;
+		*decoder = this->dec_delay;
+	}
+}
+
 static void codec_set_log(struct spa_log *global_log)
 {
 	log = global_log;
@@ -675,6 +700,7 @@ const struct media_codec a2dp_codec_aac = {
 	.reduce_bitpool = codec_reduce_bitpool,
 	.increase_bitpool = codec_increase_bitpool,
 	.set_log = codec_set_log,
+	.get_delay = codec_get_delay,
 };
 
 const struct media_codec a2dp_codec_aac_eld = {
@@ -700,6 +726,7 @@ const struct media_codec a2dp_codec_aac_eld = {
 	.reduce_bitpool = codec_reduce_bitpool,
 	.increase_bitpool = codec_increase_bitpool,
 	.set_log = codec_set_log,
+	.get_delay = codec_get_delay,
 };
 
 MEDIA_CODEC_EXPORT_DEF(
diff --git a/spa/plugins/bluez5/a2dp-codec-aptx.c b/spa/plugins/bluez5/a2dp-codec-aptx.c
index 2682d9d1..e10691e7 100644
--- a/spa/plugins/bluez5/a2dp-codec-aptx.c
+++ b/spa/plugins/bluez5/a2dp-codec-aptx.c
@@ -74,7 +74,7 @@ static inline size_t codec_get_caps_size(const struct media_codec *codec)
 }
 
 static int codec_fill_caps(const struct media_codec *codec, uint32_t flags,
-		uint8_t caps[A2DP_MAX_CAPS_SIZE])
+		const struct spa_dict *settings, uint8_t caps[A2DP_MAX_CAPS_SIZE])
 {
 	size_t actual_conf_size = codec_get_caps_size(codec);
 	const a2dp_aptx_t a2dp_aptx = {
@@ -449,6 +449,14 @@ static int codec_decode(void *data,
 	return res;
 }
 
+static void codec_get_delay(void *data, uint32_t *encoder, uint32_t *decoder)
+{
+	if (encoder)
+		*encoder = 90;
+	if (decoder)
+		*decoder = 0;
+}
+
 /*
  * mSBC duplex codec
  *
@@ -627,6 +635,7 @@ const struct media_codec a2dp_codec_aptx = {
 	.decode = codec_decode,
 	.reduce_bitpool = codec_reduce_bitpool,
 	.increase_bitpool = codec_increase_bitpool,
+	.get_delay = codec_get_delay,
 };
 
 
@@ -650,6 +659,7 @@ const struct media_codec a2dp_codec_aptx_hd = {
 	.decode = codec_decode,
 	.reduce_bitpool = codec_reduce_bitpool,
 	.increase_bitpool = codec_increase_bitpool,
+	.get_delay = codec_get_delay,
 };
 
 #define APTX_LL_COMMON_DEFS				\
@@ -665,7 +675,8 @@ const struct media_codec a2dp_codec_aptx_hd = {
 	.start_encode = codec_start_encode,		\
 	.encode = codec_encode,				\
 	.reduce_bitpool = codec_reduce_bitpool,		\
-	.increase_bitpool = codec_increase_bitpool
+	.increase_bitpool = codec_increase_bitpool,	\
+	.get_delay = codec_get_delay
 
 
 const struct media_codec a2dp_codec_aptx_ll_0 = {
diff --git a/spa/plugins/bluez5/a2dp-codec-caps.h b/spa/plugins/bluez5/a2dp-codec-caps.h
index 2bb883ec..d775ce10 100644
--- a/spa/plugins/bluez5/a2dp-codec-caps.h
+++ b/spa/plugins/bluez5/a2dp-codec-caps.h
@@ -486,4 +486,6 @@ typedef struct {
 	uint8_t data;
 } __attribute__ ((packed)) a2dp_opus_g_t;
 
+#define ASHA_CODEC_G722	0x63
+
 #endif
diff --git a/spa/plugins/bluez5/a2dp-codec-faststream.c b/spa/plugins/bluez5/a2dp-codec-faststream.c
index ce70e43f..bbf9e96a 100644
--- a/spa/plugins/bluez5/a2dp-codec-faststream.c
+++ b/spa/plugins/bluez5/a2dp-codec-faststream.c
@@ -7,12 +7,10 @@
 #include <stddef.h>
 #include <errno.h>
 #include <arpa/inet.h>
-#if __BYTE_ORDER != __LITTLE_ENDIAN
-#include <byteswap.h>
-#endif
 
 #include <spa/param/audio/format.h>
 #include <spa/param/audio/format-utils.h>
+#include <spa/utils/endian.h>
 
 #include <sbc/sbc.h>
 
@@ -32,7 +30,7 @@ struct duplex_impl {
 };
 
 static int codec_fill_caps(const struct media_codec *codec, uint32_t flags,
-		uint8_t caps[A2DP_MAX_CAPS_SIZE])
+		const struct spa_dict *settings, uint8_t caps[A2DP_MAX_CAPS_SIZE])
 {
 	const a2dp_faststream_t a2dp_faststream = {
 		.info = codec->vendor,
@@ -556,6 +554,14 @@ static int duplex_decode(void *data,
 	return res;
 }
 
+static void codec_get_delay(void *data, uint32_t *encoder, uint32_t *decoder)
+{
+	if (encoder)
+		*encoder = 73;
+	if (decoder)
+		*decoder = 0;
+}
+
 /* Voice channel SBC, not a real A2DP codec */
 static const struct media_codec duplex_codec = {
 	.codec_id = A2DP_CODEC_VENDOR,
@@ -592,7 +598,8 @@ static const struct media_codec duplex_codec = {
 	.start_encode = codec_start_encode,		\
 	.encode = codec_encode,				\
 	.reduce_bitpool = codec_reduce_bitpool,		\
-	.increase_bitpool = codec_increase_bitpool
+	.increase_bitpool = codec_increase_bitpool,	\
+	.get_delay = codec_get_delay
 
 const struct media_codec a2dp_codec_faststream = {
 	FASTSTREAM_COMMON_DEFS,
diff --git a/spa/plugins/bluez5/a2dp-codec-lc3plus.c b/spa/plugins/bluez5/a2dp-codec-lc3plus.c
index 98868899..05a3aa20 100644
--- a/spa/plugins/bluez5/a2dp-codec-lc3plus.c
+++ b/spa/plugins/bluez5/a2dp-codec-lc3plus.c
@@ -7,12 +7,10 @@
 #include <stddef.h>
 #include <errno.h>
 #include <arpa/inet.h>
-#if __BYTE_ORDER != __LITTLE_ENDIAN
-#include <byteswap.h>
-#endif
 
 #include <spa/param/audio/format.h>
 #include <spa/param/audio/format-utils.h>
+#include <spa/utils/endian.h>
 
 #ifdef HAVE_LC3PLUS_H
 #include <lc3plus.h>
@@ -67,7 +65,7 @@ struct impl {
 };
 
 static int codec_fill_caps(const struct media_codec *codec, uint32_t flags,
-		uint8_t caps[A2DP_MAX_CAPS_SIZE])
+		const struct spa_dict *settings, uint8_t caps[A2DP_MAX_CAPS_SIZE])
 {
 	const a2dp_lc3plus_hr_t a2dp_lc3plus_hr = {
 		.info = codec->vendor,
diff --git a/spa/plugins/bluez5/a2dp-codec-ldac.c b/spa/plugins/bluez5/a2dp-codec-ldac.c
index 3ec20b72..f5d0b155 100644
--- a/spa/plugins/bluez5/a2dp-codec-ldac.c
+++ b/spa/plugins/bluez5/a2dp-codec-ldac.c
@@ -59,7 +59,8 @@ struct impl {
 	int frame_count;
 };
 
-static int codec_fill_caps(const struct media_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,
+	const struct spa_dict *settings, uint8_t caps[A2DP_MAX_CAPS_SIZE])
 {
 	static const a2dp_ldac_t a2dp_ldac = {
 		.info.vendor_id = LDAC_VENDOR_ID,
@@ -551,6 +552,25 @@ static int codec_encode(void *data,
 	return src_used;
 }
 
+static void codec_get_delay(void *data, uint32_t *encoder, uint32_t *decoder)
+{
+	struct impl *this = data;
+
+	if (encoder) {
+		switch (this->frequency) {
+		case 96000:
+		case 88200:
+			*encoder = 256;
+			break;
+		default:
+			*encoder = 128;
+			break;
+		}
+	}
+	if (decoder)
+		*decoder = 0;
+}
+
 const struct media_codec a2dp_codec_ldac = {
 	.id = SPA_BLUETOOTH_AUDIO_CODEC_LDAC,
 	.codec_id = A2DP_CODEC_VENDOR,
@@ -577,6 +597,7 @@ const struct media_codec a2dp_codec_ldac = {
 	.encode = codec_encode,
 	.reduce_bitpool = codec_reduce_bitpool,
 	.increase_bitpool = codec_increase_bitpool,
+	.get_delay = codec_get_delay,
 };
 
 MEDIA_CODEC_EXPORT_DEF(
diff --git a/spa/plugins/bluez5/a2dp-codec-opus-g.c b/spa/plugins/bluez5/a2dp-codec-opus-g.c
index df1ad776..85a36bf8 100644
--- a/spa/plugins/bluez5/a2dp-codec-opus-g.c
+++ b/spa/plugins/bluez5/a2dp-codec-opus-g.c
@@ -8,15 +8,13 @@
 #include <stddef.h>
 #include <errno.h>
 #include <arpa/inet.h>
-#if __BYTE_ORDER != __LITTLE_ENDIAN
-#include <byteswap.h>
-#endif
 
 #include <spa/debug/types.h>
 #include <spa/param/audio/type-info.h>
 #include <spa/param/audio/raw.h>
 #include <spa/utils/string.h>
 #include <spa/utils/dict.h>
+#include <spa/utils/endian.h>
 #include <spa/param/audio/format.h>
 #include <spa/param/audio/format-utils.h>
 
@@ -28,6 +26,7 @@
 static struct spa_log *log;
 
 struct dec_data {
+	int32_t delay;
 };
 
 struct enc_data {
@@ -39,6 +38,8 @@ struct enc_data {
 	int frame_dms;
 	int bitrate;
 	int packet_size;
+
+	int32_t delay;
 };
 
 struct impl {
@@ -55,7 +56,7 @@ struct impl {
 };
 
 static int codec_fill_caps(const struct media_codec *codec, uint32_t flags,
-		uint8_t caps[A2DP_MAX_CAPS_SIZE])
+		const struct spa_dict *settings, uint8_t caps[A2DP_MAX_CAPS_SIZE])
 {
 	a2dp_opus_g_t conf = {
 		.info = codec->vendor,
@@ -336,6 +337,8 @@ static void *codec_init(const struct media_codec *codec, uint32_t flags,
 
 	opus_encoder_ctl(this->enc, OPUS_SET_BITRATE(this->e.bitrate));
 
+	opus_encoder_ctl(this->enc, OPUS_GET_LOOKAHEAD(&this->e.delay));
+
 	/*
 	 * Setup decoder
 	 */
@@ -345,6 +348,8 @@ static void *codec_init(const struct media_codec *codec, uint32_t flags,
 		goto error;
 	}
 
+	opus_decoder_ctl(this->dec, OPUS_GET_LOOKAHEAD(&this->d.delay));
+
 	return this;
 
 error_errno:
@@ -489,6 +494,16 @@ static int codec_increase_bitpool(void *data)
 	return 0;
 }
 
+static void codec_get_delay(void *data, uint32_t *encoder, uint32_t *decoder)
+{
+	struct impl *this = data;
+
+	if (encoder)
+		*encoder = this->e.delay;
+	if (decoder)
+		*decoder = this->d.delay;
+}
+
 static void codec_set_log(struct spa_log *global_log)
 {
 	log = global_log;
@@ -518,6 +533,7 @@ const struct media_codec a2dp_codec_opus_g = {
 	.name = "opus_g",
 	.description = "Opus",
 	.fill_caps = codec_fill_caps,
+	.get_delay = codec_get_delay,
 };
 
 MEDIA_CODEC_EXPORT_DEF(
diff --git a/spa/plugins/bluez5/a2dp-codec-opus.c b/spa/plugins/bluez5/a2dp-codec-opus.c
index 651d82d5..e65b62fa 100644
--- a/spa/plugins/bluez5/a2dp-codec-opus.c
+++ b/spa/plugins/bluez5/a2dp-codec-opus.c
@@ -8,13 +8,11 @@
 #include <stddef.h>
 #include <errno.h>
 #include <arpa/inet.h>
-#if __BYTE_ORDER != __LITTLE_ENDIAN
-#include <byteswap.h>
-#endif
 
 #include <spa/debug/types.h>
 #include <spa/param/audio/type-info.h>
 #include <spa/param/audio/raw.h>
+#include <spa/utils/endian.h>
 #include <spa/utils/string.h>
 #include <spa/utils/dict.h>
 #include <spa/param/audio/format.h>
@@ -82,6 +80,8 @@ struct dec_data {
 	int fragment_size;
 	int fragment_count;
 	uint8_t fragment[OPUS_05_MAX_BYTES];
+
+	int32_t delay;
 };
 
 struct abr {
@@ -121,6 +121,8 @@ struct enc_data {
 
 	int frame_dms;
 	int application;
+
+	int32_t delay;
 };
 
 struct impl {
@@ -554,7 +556,7 @@ static int get_mapping(const struct media_codec *codec, const a2dp_opus_05_direc
 }
 
 static int codec_fill_caps(const struct media_codec *codec, uint32_t flags,
-		uint8_t caps[A2DP_MAX_CAPS_SIZE])
+		const struct spa_dict *settings, uint8_t caps[A2DP_MAX_CAPS_SIZE])
 {
 	a2dp_opus_05_t a2dp_opus_05 = {
 		.info = codec->vendor,
@@ -1007,6 +1009,7 @@ static void *codec_init(const struct media_codec *codec, uint32_t flags,
 	this->e.samples = this->e.frame_dms * this->samplerate / 10000;
 	this->e.codesize = this->e.samples * (int)this->channels * sizeof(float);
 
+	opus_multistream_encoder_ctl(this->enc, OPUS_GET_LOOKAHEAD(&this->e.delay));
 
 	/*
 	 * Setup decoder
@@ -1022,6 +1025,8 @@ static void *codec_init(const struct media_codec *codec, uint32_t flags,
 		goto error;
 	}
 
+	opus_multistream_decoder_ctl(this->dec, OPUS_GET_LOOKAHEAD(&this->d.delay));
+
 	return this;
 
 error_errno:
@@ -1327,6 +1332,16 @@ static int codec_increase_bitpool(void *data)
 	return 0;
 }
 
+static void codec_get_delay(void *data, uint32_t *encoder, uint32_t *decoder)
+{
+	struct impl *this = data;
+
+	if (encoder)
+		*encoder = this->e.delay;
+	if (decoder)
+		*decoder = this->d.delay;
+}
+
 static void codec_set_log(struct spa_log *global_log)
 {
 	log = global_log;
@@ -1349,7 +1364,8 @@ static void codec_set_log(struct spa_log *global_log)
 	.encode = codec_encode,					\
 	.reduce_bitpool = codec_reduce_bitpool,			\
 	.increase_bitpool = codec_increase_bitpool,		\
-	.set_log = codec_set_log
+	.set_log = codec_set_log,				\
+	.get_delay = codec_get_delay
 
 #define OPUS_05_COMMON_FULL_DEFS				\
 	OPUS_05_COMMON_DEFS,					\
diff --git a/spa/plugins/bluez5/a2dp-codec-sbc.c b/spa/plugins/bluez5/a2dp-codec-sbc.c
index 3397a8f5..fc55a031 100644
--- a/spa/plugins/bluez5/a2dp-codec-sbc.c
+++ b/spa/plugins/bluez5/a2dp-codec-sbc.c
@@ -29,10 +29,12 @@ struct impl {
 
 	int min_bitpool;
 	int max_bitpool;
+
+	uint32_t enc_delay;
 };
 
 static int codec_fill_caps(const struct media_codec *codec, uint32_t flags,
-		uint8_t caps[A2DP_MAX_CAPS_SIZE])
+		const struct spa_dict *settings, uint8_t caps[A2DP_MAX_CAPS_SIZE])
 {
 	static const a2dp_sbc_t a2dp_sbc = {
 		.frequency =
@@ -497,9 +499,11 @@ static void *codec_init(const struct media_codec *codec, uint32_t flags,
 	switch (conf->subbands) {
 	case SBC_SUBBANDS_4:
 		this->sbc.subbands = SBC_SB_4;
+		this->enc_delay = 37;
 		break;
 	case SBC_SUBBANDS_8:
 		this->sbc.subbands = SBC_SB_8;
+		this->enc_delay = 73;
 		break;
 	default:
 		res = -EINVAL;
@@ -618,6 +622,16 @@ static int codec_decode(void *data,
 	return res;
 }
 
+static void codec_get_delay(void *data, uint32_t *encoder, uint32_t *decoder)
+{
+	struct impl *this = data;
+
+	if (encoder)
+		*encoder = this->enc_delay;
+	if (decoder)
+		*decoder = 0;
+}
+
 const struct media_codec a2dp_codec_sbc = {
 	.id = SPA_BLUETOOTH_AUDIO_CODEC_SBC,
 	.codec_id = A2DP_CODEC_SBC,
@@ -638,6 +652,7 @@ const struct media_codec a2dp_codec_sbc = {
 	.decode = codec_decode,
 	.reduce_bitpool = codec_reduce_bitpool,
 	.increase_bitpool = codec_increase_bitpool,
+	.get_delay = codec_get_delay,
 };
 
 const struct media_codec a2dp_codec_sbc_xq = {
@@ -661,6 +676,7 @@ const struct media_codec a2dp_codec_sbc_xq = {
 	.decode = codec_decode,
 	.reduce_bitpool = codec_reduce_bitpool,
 	.increase_bitpool = codec_increase_bitpool,
+	.get_delay = codec_get_delay,
 };
 
 MEDIA_CODEC_EXPORT_DEF(
diff --git a/spa/plugins/bluez5/asha-codec-g722.c b/spa/plugins/bluez5/asha-codec-g722.c
new file mode 100644
index 00000000..1043b4fe
--- /dev/null
+++ b/spa/plugins/bluez5/asha-codec-g722.c
@@ -0,0 +1,176 @@
+/* Spa ASHA G722 codec */
+/* SPDX-FileCopyrightText: Copyright © 2024 Wim Taymans */
+/* SPDX-License-Identifier: MIT */
+
+#include <spa/param/audio/format.h>
+#include <spa/utils/dict.h>
+#include <spa/debug/log.h>
+
+#include "rtp.h"
+#include "media-codecs.h"
+#include "g722/g722_enc_dec.h"
+
+#define ASHA_HEADER_SZ       1 /* 1 byte sequence number */
+#define ASHA_ENCODED_PKT_SZ  160
+
+static struct spa_log *spalog;
+
+struct impl {
+	g722_encode_state_t encode;
+	unsigned int codesize;
+};
+
+static int codec_reduce_bitpool(void *data)
+{
+	return -ENOTSUP;
+}
+
+static int codec_increase_bitpool(void *data)
+{
+	return -ENOTSUP;
+}
+
+static int codec_abr_process (void *data, size_t unsent)
+{
+	return -ENOTSUP;
+}
+
+static int codec_get_block_size(void *data)
+{
+	struct impl *this = data;
+	return this->codesize;
+}
+
+static int codec_start_encode (void *data,
+		void *dst, size_t dst_size, uint16_t seqnum, uint32_t timestamp)
+{
+	/* Payload for ASHA must be preceded by 1-byte sequence number */
+	*(uint8_t *)dst = seqnum % 256;
+
+	return 1;
+}
+
+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)
+{
+	struct spa_pod_frame f[1];
+	uint32_t position[1];
+
+	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_S16),
+			0);
+	spa_pod_builder_add(b,
+		SPA_FORMAT_AUDIO_rate, SPA_POD_Int(16000),
+		0);
+
+	spa_pod_builder_add(b,
+			SPA_FORMAT_AUDIO_channels, SPA_POD_Int(1),
+			0);
+	position[0] = SPA_AUDIO_CHANNEL_MONO;
+	spa_pod_builder_add(b,
+			SPA_FORMAT_AUDIO_channels, SPA_POD_Int(1),
+			SPA_FORMAT_AUDIO_position, SPA_POD_Array(sizeof(uint32_t),
+				SPA_TYPE_Id, 1, position),
+			0);
+
+	*param = spa_pod_builder_pop(b, &f[0]);
+
+	return *param == NULL ? -EIO : 1;
+}
+
+static void codec_deinit(void *data)
+{
+	return;
+}
+
+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)
+{
+	struct impl *this;
+
+	if ((this = calloc(1, sizeof(struct impl))) == NULL)
+		return NULL;
+
+	g722_encode_init(&this->encode, 64000, G722_PACKED);
+
+	/*
+	 * G722 has a compression ratio of 4. Considering 160 bytes of encoded
+	 * payload, we need 640 bytes for generating an encoded frame.
+	 */
+	this->codesize = ASHA_ENCODED_PKT_SZ * 4;
+
+	spa_log_debug(spalog, "Codec initialized");
+
+	return this;
+}
+
+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;
+	size_t src_sz;
+	int ret;
+
+	if (src_size < this->codesize) {
+		spa_log_trace(spalog, "Insufficient bytes for encoding, %zd", src_size);
+		return 0;
+	}
+
+	if (dst_size < (ASHA_HEADER_SZ + ASHA_ENCODED_PKT_SZ)) {
+		spa_log_trace(spalog, "No space for encoded output, %zd", dst_size);
+		return 0;
+	}
+
+	src_sz = (src_size > this->codesize) ? this->codesize : src_size;
+
+	ret = g722_encode(&this->encode, dst, src, src_sz / 2 /* S16LE */);
+	if (ret < 0) {
+		spa_log_error(spalog, "encode error: %d", ret);
+		return -EIO;
+	}
+
+	*dst_out = ret;
+	*need_flush = NEED_FLUSH_ALL;
+
+	return src_sz;
+}
+
+static void codec_set_log(struct spa_log *global_log)
+{
+	spalog = global_log;
+	spa_log_topic_init(spalog, &codec_plugin_log_topic);
+}
+
+const struct media_codec asha_codec_g722 = {
+	.id = SPA_BLUETOOTH_AUDIO_CODEC_G722,
+	.codec_id = ASHA_CODEC_G722,
+	.name = "g722",
+	.asha = true,
+	.description = "G722",
+	.fill_caps = NULL,
+	.enum_config = codec_enum_config,
+	.init = codec_init,
+	.deinit = codec_deinit,
+	.get_block_size = codec_get_block_size,
+	.start_encode = codec_start_encode,
+	.encode = codec_encode,
+	.abr_process = codec_abr_process,
+	.reduce_bitpool = codec_reduce_bitpool,
+	.increase_bitpool = codec_increase_bitpool,
+	.set_log = codec_set_log,
+};
+
+MEDIA_CODEC_EXPORT_DEF(
+	"g722",
+	&asha_codec_g722
+);
diff --git a/spa/plugins/bluez5/backend-native.c b/spa/plugins/bluez5/backend-native.c
index 33deda7c..35969690 100644
--- a/spa/plugins/bluez5/backend-native.c
+++ b/spa/plugins/bluez5/backend-native.c
@@ -4,6 +4,9 @@
 /* SPDX-License-Identifier: MIT */
 
 #include <errno.h>
+#include <stdbool.h>
+#include <string.h>
+#include <sys/poll.h>
 #include <unistd.h>
 #include <stdarg.h>
 #include <sys/types.h>
@@ -36,6 +39,7 @@
 
 #include "modemmanager.h"
 #include "upower.h"
+#include "telephony.h"
 
 SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.bluez5.native");
 #undef SPA_LOG_TOPIC_DEFAULT
@@ -43,6 +47,7 @@ SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.bluez5.native");
 
 #define PROP_KEY_ROLES "bluez5.roles"
 #define PROP_KEY_HEADSET_ROLES "bluez5.headset-roles"
+#define PROP_KEY_HFP_DISABLE_NREC "bluez5.hfp-hf.disable-nrec"
 
 #define HFP_CODEC_SWITCH_INITIAL_TIMEOUT_MSEC 5000
 #define HFP_CODEC_SWITCH_TIMEOUT_MSEC 20000
@@ -94,6 +99,7 @@ struct impl {
 
 #define DEFAULT_ENABLED_PROFILES (SPA_BT_PROFILE_HFP_HF | SPA_BT_PROFILE_HFP_AG)
 	enum spa_bt_profile enabled_profiles;
+	bool hfp_disable_nrec;
 
 	struct spa_source sco;
 
@@ -108,6 +114,7 @@ struct impl {
 	void *modemmanager;
 	struct spa_source *ring_timer;
 	void *upower;
+	struct spa_bt_telephony *telephony;
 };
 
 struct transport_data {
@@ -123,6 +130,11 @@ enum hfp_hf_state {
 	hfp_hf_cind1,
 	hfp_hf_cind2,
 	hfp_hf_cmer,
+	hfp_hf_chld,
+	hfp_hf_clip,
+	hfp_hf_ccwa,
+	hfp_hf_cmee,
+	hfp_hf_nrec,
 	hfp_hf_slc1,
 	hfp_hf_slc2,
 	hfp_hf_vgs,
@@ -142,6 +154,16 @@ struct rfcomm_volume {
 	int hw_volume;
 };
 
+struct rfcomm_call_data {
+	struct rfcomm *rfcomm;
+	struct spa_bt_telephony_call *call;
+};
+
+struct rfcomm_cmd {
+	struct spa_list link;
+	char* cmd;
+};
+
 struct rfcomm {
 	struct spa_list link;
 	struct spa_source source;
@@ -168,11 +190,20 @@ struct rfcomm {
 	unsigned int cind_call_notify:1;
 	unsigned int extended_error_reporting:1;
 	unsigned int clip_notify:1;
+	unsigned int hfp_hf_3way:1;
+	unsigned int hfp_hf_nrec:1;
+	unsigned int hfp_hf_clcc:1;
+	unsigned int hfp_hf_cme:1;
+	unsigned int hfp_hf_cmd_in_progress:1;
+	unsigned int hfp_hf_in_progress:1;
+	unsigned int chld_supported: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];
+	struct spa_bt_telephony_ag *telephony_ag;
+	struct spa_list hfp_hf_commands;
 #endif
 };
 
@@ -193,14 +224,25 @@ static void transport_destroy(void *data)
 	rfcomm->transport = NULL;
 }
 
+static void transport_state_changed (void *data, enum spa_bt_transport_state old,
+			enum spa_bt_transport_state state)
+{
+	struct rfcomm *rfcomm = data;
+	if (rfcomm->telephony_ag) {
+		rfcomm->telephony_ag->transport.state = state;
+		telephony_ag_transport_notify_updated_props(rfcomm->telephony_ag);
+	}
+}
+
 static const struct spa_bt_transport_events transport_events = {
 	SPA_VERSION_BT_TRANSPORT_EVENTS,
 	.destroy = transport_destroy,
+	.state_changed = transport_state_changed,
 };
 
 static const struct spa_bt_transport_implementation sco_transport_impl;
 
-static int rfcomm_new_transport(struct rfcomm *rfcomm)
+static int rfcomm_new_transport(struct rfcomm *rfcomm, int codec)
 {
 	struct impl *backend = rfcomm->backend;
 	struct spa_bt_transport *t = NULL;
@@ -229,7 +271,7 @@ static int rfcomm_new_transport(struct rfcomm *rfcomm)
 	t->backend = &backend->this;
 	t->n_channels = 1;
 	t->channels[0] = SPA_AUDIO_CHANNEL_MONO;
-	t->codec = HFP_AUDIO_CODEC_CVSD;
+	t->codec = codec;
 
 	td = t->user_data;
 	td->rfcomm = rfcomm;
@@ -252,6 +294,12 @@ static int rfcomm_new_transport(struct rfcomm *rfcomm)
 
 	spa_bt_transport_add_listener(t, &rfcomm->transport_listener, &transport_events, rfcomm);
 
+	if (rfcomm->telephony_ag) {
+		rfcomm->telephony_ag->transport.codec = codec;
+		rfcomm->telephony_ag->transport.state = SPA_BT_TRANSPORT_STATE_IDLE;
+		telephony_ag_transport_notify_updated_props(rfcomm->telephony_ag);
+	}
+
 	rfcomm->transport = t;
 	return 0;
 
@@ -267,6 +315,10 @@ static void volume_sync_stop_timer(struct rfcomm *rfcomm);
 static void rfcomm_free(struct rfcomm *rfcomm)
 {
 	codec_switch_stop_timer(rfcomm);
+	if (rfcomm->telephony_ag) {
+		telephony_ag_destroy(rfcomm->telephony_ag);
+		rfcomm->telephony_ag = NULL;
+	}
 	for (int i = 0; i < MAX_HF_INDICATORS; i++) {
 		if (rfcomm->hf_indicators[i]) {
 			free(rfcomm->hf_indicators[i]);
@@ -300,7 +352,7 @@ static void rfcomm_free(struct rfcomm *rfcomm)
 
 /* from HF/HS to AG */
 SPA_PRINTF_FUNC(2, 3)
-static ssize_t rfcomm_send_cmd(const struct rfcomm *rfcomm, const char *format, ...)
+static ssize_t rfcomm_send_cmd(struct rfcomm *rfcomm, const char *format, ...)
 {
 	struct impl *backend = rfcomm->backend;
 	char message[RFCOMM_MESSAGE_MAX_LENGTH + 1];
@@ -317,6 +369,14 @@ static ssize_t rfcomm_send_cmd(const struct rfcomm *rfcomm, const char *format,
 	if (len > RFCOMM_MESSAGE_MAX_LENGTH)
 		return -E2BIG;
 
+	if (rfcomm->hfp_hf_cmd_in_progress) {
+		spa_log_debug(backend->log, "Command in progress, postponing: %s", message);
+		struct rfcomm_cmd *cmd = calloc(1, sizeof(struct rfcomm_cmd));
+		cmd->cmd = strndup(message, len);
+		spa_list_append(&rfcomm->hfp_hf_commands, &cmd->link);
+		return 0;
+	}
+
 	spa_log_debug(backend->log, "RFCOMM >> %s", message);
 
 	/*
@@ -337,6 +397,8 @@ static ssize_t rfcomm_send_cmd(const struct rfcomm *rfcomm, const char *format,
 		spa_log_error(backend->log, "RFCOMM write error: %s", strerror(errno));
 	}
 
+	rfcomm->hfp_hf_cmd_in_progress = true;
+
 	return len;
 }
 
@@ -851,7 +913,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;
+		ag_features |= SPA_BT_HFP_AG_FEATURE_HF_INDICATORS | SPA_BT_HFP_AG_FEATURE_ESCO_S4;
 		rfcomm_send_reply(rfcomm, "+BRSF: %u", ag_features);
 		rfcomm_send_reply(rfcomm, "OK");
 	} else if (spa_strstartswith(buf, "AT+BAC=")) {
@@ -916,10 +978,9 @@ static bool rfcomm_hfp_ag(struct rfcomm *rfcomm, char* buf)
 				rfcomm_send_reply(rfcomm, "+BCS: 2");
 			codec_switch_start_timer(rfcomm, HFP_CODEC_SWITCH_INITIAL_TIMEOUT_MSEC);
 		} else {
-			if (rfcomm_new_transport(rfcomm) < 0) {
+			if (rfcomm_new_transport(rfcomm, HFP_AUDIO_CODEC_CVSD) < 0) {
 				// TODO: We should manage the missing transport
 			} else {
-				rfcomm->transport->codec = HFP_AUDIO_CODEC_CVSD;
 				spa_bt_device_connect_profile(rfcomm->device, rfcomm->profile);
 				rfcomm_emit_volume_changed(rfcomm, -1, SPA_BT_VOLUME_INVALID);
 			}
@@ -958,14 +1019,13 @@ static bool rfcomm_hfp_ag(struct rfcomm *rfcomm, char* buf)
 		spa_log_debug(backend->log, "RFCOMM selected_codec = %i", selected_codec);
 
 		/* Recreate transport, since previous connection may now be invalid */
-		if (rfcomm_new_transport(rfcomm) < 0) {
+		if (rfcomm_new_transport(rfcomm, selected_codec) < 0) {
 			// TODO: We should manage the missing transport
 			rfcomm_send_error(rfcomm, CMEE_AG_FAILURE);
 			if (was_switching_codec)
 				spa_bt_device_emit_codec_switched(rfcomm->device, -ENOMEM);
 			return true;
 		}
-		rfcomm->transport->codec = selected_codec;
 		spa_bt_device_connect_profile(rfcomm->device, rfcomm->profile);
 		rfcomm_emit_volume_changed(rfcomm, -1, SPA_BT_VOLUME_INVALID);
 
@@ -1221,15 +1281,606 @@ next_indicator:
 	return true;
 }
 
+static bool rfcomm_hfp_hf(struct rfcomm *rfcomm, char* token);
+
+static bool hfp_hf_wait_for_reply(struct rfcomm *rfcomm, char *buf, size_t len)
+{
+	struct impl *backend = rfcomm->backend;
+	struct pollfd fds[1];
+	bool reply_found = false;
+
+	fds[0].fd = rfcomm->source.fd;
+	fds[0].events = POLLIN;
+	while (!reply_found) {
+		int ret;
+		char tmp_buf[512];
+		ssize_t tmp_len;
+		char *ptr, *token;
+
+		ret = poll(fds, 1, 2000);
+		if (ret < 0) {
+			spa_log_error(backend->log, "RFCOMM poll error: %s", strerror(errno));
+			goto done;
+		} else if (ret == 0) {
+			spa_log_error(backend->log, "RFCOMM poll timeout");
+			goto done;
+		}
+
+		if (fds[0].revents & (POLLHUP | POLLERR)) {
+			spa_log_info(backend->log, "lost RFCOMM connection.");
+			rfcomm_free(rfcomm);
+			return false;
+		}
+
+		if (fds[0].revents & POLLIN) {
+			tmp_len = read(rfcomm->source.fd, tmp_buf, sizeof(tmp_buf) - 1);
+			if (tmp_len < 0) {
+				spa_log_error(backend->log, "RFCOMM read error: %s", strerror(errno));
+				goto done;
+			}
+			tmp_buf[tmp_len] = '\0';
+
+			/* Relaxed parsing of \r\n<REPLY>\r\n */
+			ptr = tmp_buf;
+			while ((token = strsep(&ptr, "\r"))) {
+				size_t ptr_len;
+
+				/* Skip leading and trailing \n */
+				while (*token == '\n')
+					++token;
+				for (ptr_len = strlen(token); ptr_len > 0 && token[ptr_len - 1] == '\n'; --ptr_len)
+					token[ptr_len - 1] = '\0';
+
+				/* Skip empty */
+				if (*token == '\0' /*&& buf == NULL*/)
+					continue;
+
+				spa_log_debug(backend->log, "RFCOMM event: %s", token);
+				if (spa_strstartswith(token, "OK") || spa_strstartswith(token, "ERROR") ||
+						spa_strstartswith(token, "+CME ERROR:")) {
+					spa_log_debug(backend->log, "RFCOMM reply found: %s", token);
+					reply_found = true;
+					strncpy(buf, token, len);
+					buf[len-1] = '\0';
+				} else if (!rfcomm_hfp_hf(rfcomm, token)) {
+					spa_log_debug(backend->log, "RFCOMM received unsupported event: %s", token);
+				}
+			}
+		}
+	}
+
+done:
+	rfcomm->hfp_hf_cmd_in_progress = false;
+	if (!spa_list_is_empty(&rfcomm->hfp_hf_commands)) {
+		struct rfcomm_cmd *cmd;
+		cmd = spa_list_first(&rfcomm->hfp_hf_commands, struct rfcomm_cmd, link);
+		spa_list_remove(&cmd->link);
+		spa_log_debug(backend->log, "Sending postponed command: %s", cmd->cmd);
+		rfcomm_send_cmd(rfcomm, "%s", cmd->cmd);
+		free(cmd->cmd);
+		free(cmd);
+	}
+
+	return reply_found;
+}
+
+static void hfp_hf_get_error_from_reply(char *reply, enum spa_bt_telephony_error *err, uint8_t *cme_error)
+{
+	if (spa_strstartswith(reply, "+CME ERROR:")) {
+		*cme_error = atoi(reply + strlen("+CME ERROR:"));
+		*err = BT_TELEPHONY_ERROR_CME;
+	} else {
+		*err = BT_TELEPHONY_ERROR_FAILED;
+	}
+}
+
+static void hfp_hf_answer(void *data, enum spa_bt_telephony_error *err, uint8_t *cme_error)
+{
+	struct rfcomm_call_data *call_data = data;
+	struct rfcomm *rfcomm = call_data->rfcomm;
+	struct impl *backend = rfcomm->backend;
+	char reply[20];
+	bool res;
+
+	if (call_data->call->state != CALL_STATE_INCOMING) {
+		*err = BT_TELEPHONY_ERROR_INVALID_STATE;
+		return;
+	}
+
+	rfcomm_send_cmd(rfcomm, "ATA");
+	res = hfp_hf_wait_for_reply(rfcomm, reply, sizeof(reply));
+	if (!res || !spa_strstartswith(reply, "OK")) {
+		spa_log_info(backend->log, "Failed to answer call");
+		if (res)
+			hfp_hf_get_error_from_reply(reply, err, cme_error);
+		else
+			*err = BT_TELEPHONY_ERROR_FAILED;
+		return;
+	}
+
+	*err = BT_TELEPHONY_ERROR_NONE;
+}
+
+static void hfp_hf_hangup(void *data, enum spa_bt_telephony_error *err, uint8_t *cme_error)
+{
+	struct rfcomm_call_data *call_data = data;
+	struct rfcomm *rfcomm = call_data->rfcomm;
+	struct impl *backend = rfcomm->backend;
+	char reply[20];
+	bool res;
+
+	switch (call_data->call->state) {
+	case CALL_STATE_ACTIVE:
+	case CALL_STATE_DIALING:
+	case CALL_STATE_ALERTING:
+	case CALL_STATE_INCOMING:
+		rfcomm_send_cmd(rfcomm, "AT+CHUP");
+		break;
+	case CALL_STATE_WAITING:
+		rfcomm_send_cmd(rfcomm, "AT+CHLD=0");
+		break;
+	default:
+		spa_log_info(backend->log, "Call not incoming, waiting or active: skip hangup");
+		*err = BT_TELEPHONY_ERROR_INVALID_STATE;
+		return;
+	}
+
+	res = hfp_hf_wait_for_reply(rfcomm, reply, sizeof(reply));
+	if (!res || !spa_strstartswith(reply, "OK")) {
+		spa_log_info(backend->log, "Failed to hangup call");
+		if (res)
+			hfp_hf_get_error_from_reply(reply, err, cme_error);
+		else
+			*err = BT_TELEPHONY_ERROR_FAILED;
+		return;
+	}
+
+	*err = BT_TELEPHONY_ERROR_NONE;
+}
+
+static const struct spa_bt_telephony_call_callbacks telephony_call_callbacks = {
+	SPA_VERSION_BT_TELEPHONY_AG_CALLBACKS,
+	.answer = hfp_hf_answer,
+	.hangup = hfp_hf_hangup,
+};
+
+static struct spa_bt_telephony_call *hfp_hf_add_call(struct rfcomm *rfcomm, struct spa_bt_telephony_ag *ag, enum spa_bt_telephony_call_state state,
+                                                     const char *number)
+{
+	struct spa_bt_telephony_call *call;
+	struct rfcomm_call_data *data;
+
+	call = telephony_call_new(ag, sizeof(*data));
+	if (!call)
+		return NULL;
+	call->state = state;
+	if (number)
+		call->line_identification = strdup(number);
+	data = telephony_call_get_user_data(call);
+	data->rfcomm = rfcomm;
+	data->call = call;
+	telephony_call_set_callbacks(call, &telephony_call_callbacks, data);
+	telephony_call_register(call);
+
+	return call;
+}
+
+static void hfp_hf_dial(void *data, const char *number, enum spa_bt_telephony_error *err, uint8_t *cme_error)
+{
+	struct rfcomm *rfcomm = data;
+	struct impl *backend = rfcomm->backend;
+	char reply[20];
+	bool res;
+
+	spa_log_info(backend->log, "Dialing: \"%s\"", number);
+	rfcomm_send_cmd(rfcomm, "ATD%s;", number);
+	res = hfp_hf_wait_for_reply(rfcomm, reply, sizeof(reply));
+	if (res && spa_strstartswith(reply, "OK")) {
+		struct spa_bt_telephony_call *call;
+		call = hfp_hf_add_call(rfcomm, rfcomm->telephony_ag, CALL_STATE_DIALING, number);
+		*err = call ? BT_TELEPHONY_ERROR_NONE : BT_TELEPHONY_ERROR_FAILED;
+	} else {
+		spa_log_info(backend->log, "Failed to dial: \"%s\"", number);
+		if (res)
+			hfp_hf_get_error_from_reply(reply, err, cme_error);
+		else
+			*err = BT_TELEPHONY_ERROR_FAILED;
+	}
+}
+
+static void hfp_hf_swap_calls(void *data, enum spa_bt_telephony_error *err, uint8_t *cme_error)
+{
+	struct rfcomm *rfcomm = data;
+	struct impl *backend = rfcomm->backend;
+	struct spa_bt_telephony_call *call;
+	bool found_active = false;
+	bool found_held = false;
+	char reply[20];
+	bool res;
+
+	if (!rfcomm->chld_supported) {
+		*err = BT_TELEPHONY_ERROR_NOT_SUPPORTED;
+		return;
+	} else if (rfcomm->hfp_hf_in_progress) {
+		*err = BT_TELEPHONY_ERROR_IN_PROGRESS;
+		return;
+	}
+
+	spa_list_for_each(call, &rfcomm->telephony_ag->call_list, link) {
+		if (call->state == CALL_STATE_WAITING) {
+			spa_log_debug(backend->log, "call waiting before swapping");
+			*err = BT_TELEPHONY_ERROR_INVALID_STATE;
+			return;
+		} else if (call->state == CALL_STATE_ACTIVE)
+			found_active = true;
+		else if (call->state == CALL_STATE_HELD)
+			found_held = true;
+	}
+
+	if (!found_active || !found_held) {
+		spa_log_debug(backend->log, "no active and held calls");
+		*err = BT_TELEPHONY_ERROR_INVALID_STATE;
+		return;
+	}
+
+	rfcomm_send_cmd(rfcomm, "AT+CHLD=2");
+	res = hfp_hf_wait_for_reply(rfcomm, reply, sizeof(reply));
+	if (!res || !spa_strstartswith(reply, "OK")) {
+		spa_log_info(backend->log, "Failed to swap calls");
+		if (res)
+			hfp_hf_get_error_from_reply(reply, err, cme_error);
+		else
+			*err = BT_TELEPHONY_ERROR_FAILED;
+		return;
+	}
+
+	rfcomm->hfp_hf_in_progress = true;
+	*err = BT_TELEPHONY_ERROR_NONE;
+}
+
+static void hfp_hf_release_and_answer(void *data, enum spa_bt_telephony_error *err, uint8_t *cme_error)
+{
+	struct rfcomm *rfcomm = data;
+	struct impl *backend = rfcomm->backend;
+	struct spa_bt_telephony_call *call;
+	bool found_active = false;
+	bool found_waiting = false;
+	char reply[20];
+	bool res;
+
+	if (!rfcomm->chld_supported) {
+		*err = BT_TELEPHONY_ERROR_NOT_SUPPORTED;
+		return;
+	} else if (rfcomm->hfp_hf_in_progress) {
+		*err = BT_TELEPHONY_ERROR_IN_PROGRESS;
+		return;
+	}
+
+	spa_list_for_each(call, &rfcomm->telephony_ag->call_list, link) {
+		if (call->state == CALL_STATE_ACTIVE)
+			found_active = true;
+		else if (call->state == CALL_STATE_WAITING)
+			found_waiting = true;
+	}
+
+	if (!found_active || !found_waiting) {
+		spa_log_debug(backend->log, "no active and waiting calls");
+		*err = BT_TELEPHONY_ERROR_INVALID_STATE;
+		return;
+	}
+
+	rfcomm_send_cmd(rfcomm, "AT+CHLD=1");
+	res = hfp_hf_wait_for_reply(rfcomm, reply, sizeof(reply));
+	if (!res || !spa_strstartswith(reply, "OK")) {
+		spa_log_info(backend->log, "Failed to release and answer calls");
+		if (res)
+			hfp_hf_get_error_from_reply(reply, err, cme_error);
+		else
+			*err = BT_TELEPHONY_ERROR_FAILED;
+		return;
+	}
+
+	rfcomm->hfp_hf_in_progress = true;
+	*err = BT_TELEPHONY_ERROR_NONE;
+}
+
+static void hfp_hf_release_and_swap(void *data, enum spa_bt_telephony_error *err, uint8_t *cme_error)
+{
+	struct rfcomm *rfcomm = data;
+	struct impl *backend = rfcomm->backend;
+	struct spa_bt_telephony_call *call;
+	bool found_active = false;
+	bool found_held = false;
+	char reply[20];
+	bool res;
+
+	if (!rfcomm->chld_supported) {
+		*err = BT_TELEPHONY_ERROR_NOT_SUPPORTED;
+		return;
+	} else if (rfcomm->hfp_hf_in_progress) {
+		*err = BT_TELEPHONY_ERROR_IN_PROGRESS;
+		return;
+	}
+
+	spa_list_for_each(call, &rfcomm->telephony_ag->call_list, link) {
+		if (call->state == CALL_STATE_WAITING) {
+			spa_log_debug(backend->log, "call waiting before release and swap");
+			*err = BT_TELEPHONY_ERROR_INVALID_STATE;
+			return;
+		} else if (call->state == CALL_STATE_ACTIVE)
+			found_active = true;
+		else if (call->state == CALL_STATE_HELD)
+			found_held = true;
+	}
+
+	if (!found_active || !found_held) {
+		spa_log_debug(backend->log, "no active and held calls");
+		*err = BT_TELEPHONY_ERROR_INVALID_STATE;
+		return;
+	}
+
+	rfcomm_send_cmd(rfcomm, "AT+CHLD=1");
+	res = hfp_hf_wait_for_reply(rfcomm, reply, sizeof(reply));
+	if (!res || !spa_strstartswith(reply, "OK")) {
+		spa_log_info(backend->log, "Failed to release and swap calls");
+		if (res)
+			hfp_hf_get_error_from_reply(reply, err, cme_error);
+		else
+			*err = BT_TELEPHONY_ERROR_FAILED;
+		return;
+	}
+
+	rfcomm->hfp_hf_in_progress = true;
+	*err = BT_TELEPHONY_ERROR_NONE;
+}
+
+static void hfp_hf_hold_and_answer(void *data, enum spa_bt_telephony_error *err, uint8_t *cme_error)
+{
+	struct rfcomm *rfcomm = data;
+	struct impl *backend = rfcomm->backend;
+	struct spa_bt_telephony_call *call;
+	bool found_active = false;
+	bool found_waiting = false;
+	char reply[20];
+	bool res;
+
+	if (!rfcomm->chld_supported) {
+		*err = BT_TELEPHONY_ERROR_NOT_SUPPORTED;
+		return;
+	} else if (rfcomm->hfp_hf_in_progress) {
+		*err = BT_TELEPHONY_ERROR_IN_PROGRESS;
+		return;
+	}
+
+	spa_list_for_each(call, &rfcomm->telephony_ag->call_list, link) {
+		if (call->state == CALL_STATE_ACTIVE)
+			found_active = true;
+		else if (call->state == CALL_STATE_WAITING)
+			found_waiting = true;
+	}
+
+	if (!found_active || !found_waiting) {
+		spa_log_debug(backend->log, "no active and waiting calls");
+		*err = BT_TELEPHONY_ERROR_INVALID_STATE;
+		return;
+	}
+
+	rfcomm_send_cmd(rfcomm, "AT+CHLD=2");
+	res = hfp_hf_wait_for_reply(rfcomm, reply, sizeof(reply));
+	if (!res || !spa_strstartswith(reply, "OK")) {
+		spa_log_info(backend->log, "Failed to hold and answer calls");
+		if (res)
+			hfp_hf_get_error_from_reply(reply, err, cme_error);
+		else
+			*err = BT_TELEPHONY_ERROR_FAILED;
+		return;
+	}
+
+	rfcomm->hfp_hf_in_progress = true;
+	*err = BT_TELEPHONY_ERROR_NONE;
+}
+
+static void hfp_hf_hangup_all(void *data, enum spa_bt_telephony_error *err, uint8_t *cme_error)
+{
+	struct rfcomm *rfcomm = data;
+	struct impl *backend = rfcomm->backend;
+	struct spa_bt_telephony_call *call;
+	bool found_active = false;
+	bool found_held = false;
+	char reply[20];
+	bool res;
+
+	spa_list_for_each(call, &rfcomm->telephony_ag->call_list, link) {
+		switch (call->state) {
+		case CALL_STATE_ACTIVE:
+		case CALL_STATE_DIALING:
+		case CALL_STATE_ALERTING:
+		case CALL_STATE_INCOMING:
+			found_active = true;
+			break;
+		case CALL_STATE_HELD:
+		case CALL_STATE_WAITING:
+			found_held = true;
+			break;
+		default:
+			break;
+		}
+	}
+
+	*err = BT_TELEPHONY_ERROR_NONE;
+
+	/* Hangup held calls */
+	if (found_held) {
+		rfcomm_send_cmd(rfcomm, "AT+CHLD=0");
+		res = hfp_hf_wait_for_reply(rfcomm, reply, sizeof(reply));
+		if (!res || !spa_strstartswith(reply, "OK")) {
+			spa_log_info(backend->log, "Failed to hangup held calls");
+			if (res)
+				hfp_hf_get_error_from_reply(reply, err, cme_error);
+			else
+				*err = BT_TELEPHONY_ERROR_FAILED;
+		}
+	}
+
+	/* Hangup active calls */
+	if (found_active) {
+		rfcomm_send_cmd(rfcomm, "AT+CHUP");
+		res = hfp_hf_wait_for_reply(rfcomm, reply, sizeof(reply));
+		if (!res || !spa_strstartswith(reply, "OK")) {
+			spa_log_info(backend->log, "Failed to hangup active calls");
+			if (res)
+				hfp_hf_get_error_from_reply(reply, err, cme_error);
+			else
+				*err = BT_TELEPHONY_ERROR_FAILED;
+		}
+	}
+}
+
+static void hfp_hf_create_multiparty(void *data, enum spa_bt_telephony_error *err, uint8_t *cme_error)
+{
+	struct rfcomm *rfcomm = data;
+	struct impl *backend = rfcomm->backend;
+	struct spa_bt_telephony_call *call;
+	bool found_active = false;
+	bool found_held = false;
+	char reply[20];
+	bool res;
+
+	if (!rfcomm->chld_supported) {
+		*err = BT_TELEPHONY_ERROR_NOT_SUPPORTED;
+		return;
+	} else if (rfcomm->hfp_hf_in_progress) {
+		*err = BT_TELEPHONY_ERROR_IN_PROGRESS;
+		return;
+	}
+
+	spa_list_for_each(call, &rfcomm->telephony_ag->call_list, link) {
+		if (call->state == CALL_STATE_WAITING) {
+			spa_log_debug(backend->log, "call waiting before creating multiparty");
+			*err = BT_TELEPHONY_ERROR_INVALID_STATE;
+			return;
+		} else if (call->state == CALL_STATE_ACTIVE)
+			found_active = true;
+		else if (call->state == CALL_STATE_HELD)
+			found_held = true;
+	}
+
+	if (!found_active || !found_held) {
+		spa_log_debug(backend->log, "no active and held calls");
+		*err = BT_TELEPHONY_ERROR_INVALID_STATE;
+		return;
+	}
+
+	rfcomm_send_cmd(rfcomm, "AT+CHLD=3");
+	res = hfp_hf_wait_for_reply(rfcomm, reply, sizeof(reply));
+	if (!res || !spa_strstartswith(reply, "OK")) {
+		spa_log_info(backend->log, "Failed to create multiparty");
+		if (res)
+			hfp_hf_get_error_from_reply(reply, err, cme_error);
+		else
+			*err = BT_TELEPHONY_ERROR_FAILED;
+		return;
+	}
+
+	rfcomm->hfp_hf_in_progress = true;
+	*err = BT_TELEPHONY_ERROR_NONE;
+}
+
+static void hfp_hf_send_tones(void *data, const char *tones, enum spa_bt_telephony_error *err, uint8_t *cme_error)
+{
+	struct rfcomm *rfcomm = data;
+	struct impl *backend = rfcomm->backend;
+	struct spa_bt_telephony_call *call;
+	bool found = false;
+	char reply[20];
+	bool res;
+
+	spa_list_for_each(call, &rfcomm->telephony_ag->call_list, link) {
+		if (call->state == CALL_STATE_ACTIVE) {
+			found = true;
+			break;
+		}
+	}
+
+	if (!found) {
+		spa_log_debug(backend->log, "no active call");
+		*err = BT_TELEPHONY_ERROR_INVALID_STATE;
+		return;
+	}
+
+	rfcomm_send_cmd(rfcomm, "AT+VTS=%s", tones);
+	res = hfp_hf_wait_for_reply(rfcomm, reply, sizeof(reply));
+	if (!res || !spa_strstartswith(reply, "OK")) {
+		spa_log_info(backend->log, "Failed to send tones: %s", tones);
+		if (res)
+			hfp_hf_get_error_from_reply(reply, err, cme_error);
+		else
+			*err = BT_TELEPHONY_ERROR_FAILED;
+		return;
+	}
+
+	*err = BT_TELEPHONY_ERROR_NONE;
+}
+
+static void hfp_hf_transport_activate(void *data, enum spa_bt_telephony_error *err, uint8_t *cme_error)
+{
+	struct rfcomm *rfcomm = data;
+	struct impl *backend = rfcomm->backend;
+	char reply[20];
+	bool res;
+
+	if (spa_list_is_empty(&rfcomm->telephony_ag->call_list)) {
+		spa_log_debug(backend->log, "no ongoing call");
+		*err = BT_TELEPHONY_ERROR_INVALID_STATE;
+		return;
+	}
+	if (rfcomm->transport->fd > 0) {
+		spa_log_debug(backend->log, "transport is already active; SCO socket exists");
+		*err = BT_TELEPHONY_ERROR_INVALID_STATE;
+		return;
+	}
+
+	rfcomm_send_cmd(rfcomm, "AT+BCC");
+	res = hfp_hf_wait_for_reply(rfcomm, reply, sizeof(reply));
+	if (!res || !spa_strstartswith(reply, "OK")) {
+		spa_log_info(backend->log, "Failed to send AT+BCC");
+		if (res)
+			hfp_hf_get_error_from_reply(reply, err, cme_error);
+		else
+			*err = BT_TELEPHONY_ERROR_FAILED;
+		return;
+	}
+
+	*err = BT_TELEPHONY_ERROR_NONE;
+}
+
+static const struct spa_bt_telephony_ag_callbacks telephony_ag_callbacks = {
+	SPA_VERSION_BT_TELEPHONY_AG_CALLBACKS,
+	.dial = hfp_hf_dial,
+	.swap_calls = hfp_hf_swap_calls,
+	.release_and_answer = hfp_hf_release_and_answer,
+	.release_and_swap = hfp_hf_release_and_swap,
+	.hold_and_answer = hfp_hf_hold_and_answer,
+	.hangup_all = hfp_hf_hangup_all,
+	.create_multiparty = hfp_hf_create_multiparty,
+	.send_tones = hfp_hf_send_tones,
+	.transport_activate = hfp_hf_transport_activate,
+};
+
 static bool rfcomm_hfp_hf(struct rfcomm *rfcomm, char* token)
 {
 	struct impl *backend = rfcomm->backend;
-	unsigned int features, gain, selected_codec, indicator, value;
+	unsigned int features, gain, selected_codec, indicator, value, type;
+	char number[17];
 
 	if (sscanf(token, "+BRSF:%u", &features) == 1) {
 		if (((features & (SPA_BT_HFP_AG_FEATURE_CODEC_NEGOTIATION)) != 0) &&
 				(rfcomm->msbc_supported_by_hfp || rfcomm->lc3_supported_by_hfp))
 			rfcomm->codec_negotiation_supported = true;
+		rfcomm->hfp_hf_3way = (features & SPA_BT_HFP_AG_FEATURE_3WAY) != 0;
+		rfcomm->hfp_hf_nrec = (features & SPA_BT_HFP_AG_FEATURE_ECNR) != 0;
+		rfcomm->hfp_hf_clcc = (features & SPA_BT_HFP_AG_FEATURE_ENHANCED_CALL_STATUS) != 0;
+		rfcomm->hfp_hf_cme = (features & SPA_BT_HFP_AG_FEATURE_EXTENDED_RES_CODE) != 0;
 	} 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 &&
 				selected_codec != HFP_AUDIO_CODEC_LC3_SWB) {
@@ -1243,10 +1894,9 @@ static bool rfcomm_hfp_hf(struct rfcomm *rfcomm, char* token)
 			rfcomm->hf_state = hfp_hf_bcs;
 
 			if (!rfcomm->transport || (rfcomm->transport->codec != selected_codec) ) {
-				if (rfcomm_new_transport(rfcomm) < 0) {
+				if (rfcomm_new_transport(rfcomm, selected_codec) < 0) {
 					// TODO: We should manage the missing transport
 				} else {
-					rfcomm->transport->codec = selected_codec;
 					spa_bt_device_connect_profile(rfcomm->device, rfcomm->profile);
 				}
 			}
@@ -1294,6 +1944,26 @@ static bool rfcomm_hfp_hf(struct rfcomm *rfcomm, char* token)
 			token += strcspn(token, "\0") + 1;
 			i++;
 		}
+	} else if (spa_strstartswith(token, "+CHLD: (")) {
+		int chlds = 0;
+		token[strcspn(token, "\r")] = 0;
+		token[strcspn(token, "\n")] = 0;
+		token[strcspn(token, ")")] = 0;
+		token += strlen("+CHLD: (");
+		while (strlen(token)) {
+			token[strcspn(token, ",")] = 0;
+			if (spa_streq(token, "0"))
+				chlds |= 1 << 0;
+			else if (spa_streq(token, "1"))
+				chlds |= 1 << 1;
+			else if (spa_streq(token, "2"))
+				chlds |= 1 << 2;
+			else if (spa_streq(token, "3"))
+				chlds |= 1 << 3;
+			token += strcspn(token, "\0") + 1;
+		}
+		rfcomm->chld_supported = (chlds == 0x0F);
+		spa_log_debug(backend->log, "AT+CHLD supported: %d (0x%X)", rfcomm->chld_supported, chlds);
 	} 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);
@@ -1302,67 +1972,388 @@ static bool rfcomm_hfp_hf(struct rfcomm *rfcomm, char* token)
 
 			if (spa_streq(rfcomm->hf_indicators[indicator], "battchg")) {
 				spa_bt_device_report_battery_level(rfcomm->device, value * 100 / 5);
+			} else if (spa_streq(rfcomm->hf_indicators[indicator], "callsetup")) {
+				if (value == CIND_CALLSETUP_NONE) {
+					struct spa_bt_telephony_call *call, *tcall;
+					spa_list_for_each_safe(call, tcall, &rfcomm->telephony_ag->call_list, link) {
+						if (call->state == CALL_STATE_DIALING || call->state == CALL_STATE_ALERTING ||
+						    call->state == CALL_STATE_INCOMING) {
+							call->state = CALL_STATE_DISCONNECTED;
+							telephony_call_notify_updated_props(call);
+							telephony_call_destroy(call);
+						}
+					}
+				} else if (value == CIND_CALLSETUP_INCOMING) {
+					struct spa_bt_telephony_call *call;
+					bool found = false;
+
+					spa_list_for_each(call, &rfcomm->telephony_ag->call_list, link) {
+						if (call->state == CALL_STATE_INCOMING || call->state == CALL_STATE_WAITING) {
+							spa_log_info(backend->log, "incoming call already in progress (%d)", call->state);
+							found = true;
+							break;
+						}
+					}
+
+					if (!found && !rfcomm->hfp_hf_clcc) {
+						spa_log_info(backend->log, "Incoming call");
+						if (hfp_hf_add_call(rfcomm, rfcomm->telephony_ag, CALL_STATE_INCOMING, NULL) == NULL)
+							spa_log_warn(backend->log, "failed to create incoming call");
+					}
+				} else if (value == CIND_CALLSETUP_DIALING) {
+					struct spa_bt_telephony_call *call;
+					bool found = false;
+
+					spa_list_for_each(call, &rfcomm->telephony_ag->call_list, link) {
+						if (call->state == CALL_STATE_DIALING || call->state == CALL_STATE_ALERTING) {
+							spa_log_info(backend->log, "dialing call already in progress (%d)", call->state);
+							found = true;
+							break;
+						}
+					}
+
+					if (!found && !rfcomm->hfp_hf_clcc) {
+						spa_log_info(backend->log, "Dialing call");
+						if (hfp_hf_add_call(rfcomm, rfcomm->telephony_ag, CALL_STATE_DIALING, NULL) == NULL)
+							spa_log_warn(backend->log, "failed to create dialing call");
+					}
+				} else if (value == CIND_CALLSETUP_ALERTING) {
+					struct spa_bt_telephony_call *call;
+					spa_list_for_each(call, &rfcomm->telephony_ag->call_list, link) {
+						if (call->state == CALL_STATE_DIALING) {
+							call->state = CALL_STATE_ALERTING;
+							telephony_call_notify_updated_props(call);
+						}
+					}
+				}
+
+				if (rfcomm->hfp_hf_clcc)
+					rfcomm_send_cmd(rfcomm, "AT+CLCC");
+				else
+					rfcomm->hfp_hf_in_progress = false;
+			} else if (spa_streq(rfcomm->hf_indicators[indicator], "call")) {
+				if (value == 0) {
+					struct spa_bt_telephony_call *call, *tcall;
+					spa_list_for_each_safe(call, tcall, &rfcomm->telephony_ag->call_list, link) {
+						if (call->state == CALL_STATE_ACTIVE) {
+							call->state = CALL_STATE_DISCONNECTED;
+							telephony_call_notify_updated_props(call);
+							telephony_call_destroy(call);
+						}
+					}
+				} else if (value == 1) {
+					struct spa_bt_telephony_call *call;
+					spa_list_for_each(call, &rfcomm->telephony_ag->call_list, link) {
+						if (call->state == CALL_STATE_DIALING || call->state == CALL_STATE_ALERTING ||
+						    call->state == CALL_STATE_INCOMING) {
+							call->state = CALL_STATE_ACTIVE;
+							telephony_call_notify_updated_props(call);
+						}
+					}
+				}
+
+				if (rfcomm->hfp_hf_clcc)
+					rfcomm_send_cmd(rfcomm, "AT+CLCC");
+				else
+					rfcomm->hfp_hf_in_progress = false;
+			} else if (spa_streq(rfcomm->hf_indicators[indicator], "callheld")) {
+				if (value == 0) {	/* Reject waiting call or no held calls */
+					struct spa_bt_telephony_call *call, *tcall;
+					bool found_waiting = false;
+					spa_list_for_each_safe(call, tcall, &rfcomm->telephony_ag->call_list, link) {
+						if (call->state == CALL_STATE_WAITING) {
+							call->state = CALL_STATE_DISCONNECTED;
+							telephony_call_notify_updated_props(call);
+							telephony_call_destroy(call);
+							found_waiting = true;
+							break;
+						}
+					}
+					if (!found_waiting) {
+						spa_list_for_each_safe(call, tcall, &rfcomm->telephony_ag->call_list, link) {
+							if (call->state == CALL_STATE_HELD) {
+								call->state = CALL_STATE_DISCONNECTED;
+								telephony_call_notify_updated_props(call);
+								telephony_call_destroy(call);
+							}
+						}
+					}
+				} else if (value == 1) {	/* Swap calls */
+					struct spa_bt_telephony_call *call;
+					spa_list_for_each(call, &rfcomm->telephony_ag->call_list, link) {
+						bool changed = false;
+						if (call->state == CALL_STATE_ACTIVE) {
+							call->state = CALL_STATE_HELD;
+							changed = true;
+						} else if (call->state == CALL_STATE_HELD) {
+							call->state = CALL_STATE_ACTIVE;
+							changed = true;
+						}
+
+						if (changed)
+							telephony_call_notify_updated_props(call);
+					}
+				} else if (value == 2) {	/* No active calls, place waiting on hold */
+					struct spa_bt_telephony_call *call;
+					spa_list_for_each(call, &rfcomm->telephony_ag->call_list, link) {
+						bool changed = false;
+						if (call->state == CALL_STATE_ACTIVE || call->state == CALL_STATE_WAITING) {
+							call->state = CALL_STATE_HELD;
+							changed = true;
+						}
+
+						if (changed)
+							telephony_call_notify_updated_props(call);
+					}
+				}
+
+				if (rfcomm->hfp_hf_clcc)
+					rfcomm_send_cmd(rfcomm, "AT+CLCC");
+				else
+					rfcomm->hfp_hf_in_progress = false;
 			}
 		}
-	} else if (spa_strstartswith(token, "OK")) {
-		switch(rfcomm->hf_state) {
-		case hfp_hf_brsf:
-			if (rfcomm->codec_negotiation_supported) {
-				char buf[64];
-				struct spa_strbuf str;
-
-				spa_strbuf_init(&str, buf, sizeof(buf));
-				spa_strbuf_append(&str, "1");
-				if (rfcomm->msbc_supported_by_hfp)
-					spa_strbuf_append(&str, ",2");
-				if (rfcomm->lc3_supported_by_hfp)
-					spa_strbuf_append(&str, ",3");
-
-				rfcomm_send_cmd(rfcomm, "AT+BAC=%s", buf);
-				rfcomm->hf_state = hfp_hf_bac;
-			} else {
+	} else if (sscanf(token, "+CLIP: \"%16[^\"]\",%u", number, &type) == 2) {
+		struct spa_bt_telephony_call *call;
+		spa_list_for_each(call, &rfcomm->telephony_ag->call_list, link) {
+			if (call->state == CALL_STATE_INCOMING && !spa_streq(number, call->line_identification)) {
+				if (call->line_identification)
+					free(call->line_identification);
+				call->line_identification = strdup(number);
+				telephony_call_notify_updated_props(call);
+				break;
+			}
+		}
+	} else if (sscanf(token, "+CCWA: \"%16[^\"]\",%u", number, &type) == 2) {
+		struct spa_bt_telephony_call *call;
+		bool found = false;
+
+		spa_log_info(backend->log, "Waiting call");
+		spa_list_for_each(call, &rfcomm->telephony_ag->call_list, link) {
+			if (call->state == CALL_STATE_WAITING) {
+				spa_log_info(backend->log, "waiting call already in progress (id: %d)", call->id);
+				found = true;
+				break;
+			}
+		}
+		if (!found) {
+			call = hfp_hf_add_call(rfcomm, rfcomm->telephony_ag, CALL_STATE_WAITING, number);
+			if (call == NULL)
+				spa_log_warn(backend->log, "failed to create waiting call");
+		}
+	} else if (spa_strstartswith(token, "+CLCC:")) {
+		struct spa_bt_telephony_call *call;
+		size_t pos;
+		char *token_end;
+		int idx;
+		unsigned int status, mpty;
+		bool parsed = false, found = false;
+
+		number[0] = '\0';
+
+		token[strcspn(token, "\r")] = 0;
+		token[strcspn(token, "\n")] = 0;
+		token_end = token + strlen(token);
+		token += strlen("+CLCC:");
+
+		if (token < token_end) {
+			pos = strcspn(token, ",");
+			token[pos] = '\0';
+			idx = atoi(token);
+			token += pos + 1;
+		}
+		if (token < token_end) {
+			// Skip direction
+			pos = strcspn(token, ",");
+			token += pos + 1;
+		}
+		if (token < token_end) {
+			pos = strcspn(token, ",");
+			token[pos] = '\0';
+			status = atoi(token);
+			token += pos + 1;
+		}
+		if (token < token_end) {
+			// Skip mode
+			pos = strcspn(token, ",");
+			token += pos + 1;
+		}
+		if (token < token_end) {
+			pos = strcspn(token, ",");
+			token[pos] = '\0';
+			mpty = atoi(token);
+			token += pos + 1;
+			parsed = true;
+		}
+		if (token < token_end) {
+			if (sscanf(token, "\"%16[^\"]\",%u", number, &type) != 2) {
+				spa_log_warn(backend->log, "Failed to parse number: %s", token);
+				number[0] = '\0';
+			}
+		}
+
+		if (SPA_LIKELY (parsed)) {
+			spa_list_for_each(call, &rfcomm->telephony_ag->call_list, link) {
+				if (call->id == idx) {
+					bool changed = false;
+
+					found = true;
+
+					if (call->state != status) {
+						call->state =status;
+						changed = true;
+					}
+					if (call->multiparty != mpty) {
+						call->multiparty = mpty;
+						changed = true;
+					}
+					if (strlen(number) && !spa_streq(number, call->line_identification)) {
+						if (call->line_identification)
+							free(call->line_identification);
+						call->line_identification = strdup(number);
+						changed = true;
+					}
+
+					if (changed)
+						telephony_call_notify_updated_props(call);
+				}
+			}
+
+			if (!found) {
+				spa_log_info(backend->log, "New call, initial state: %u", status);
+				call = hfp_hf_add_call(rfcomm, rfcomm->telephony_ag, status, strlen(number) ? number : NULL);
+				if (call == NULL)
+					spa_log_warn(backend->log, "failed to create call");
+				else if (call->id != idx)
+					spa_log_warn(backend->log, "wrong call index: %d, expected: %d", call->id, idx);
+			}
+		} else {
+			spa_log_warn(backend->log, "malformed +CLCC command received from AG");
+		}
+
+		rfcomm->hfp_hf_in_progress = false;
+	} else if (spa_strstartswith(token, "OK") || spa_strstartswith(token, "ERROR") ||
+				spa_strstartswith(token, "+CME ERROR:")) {
+		rfcomm->hfp_hf_cmd_in_progress = false;
+		if (!spa_list_is_empty(&rfcomm->hfp_hf_commands)) {
+			struct rfcomm_cmd *cmd;
+			cmd = spa_list_first(&rfcomm->hfp_hf_commands, struct rfcomm_cmd, link);
+			spa_list_remove(&cmd->link);
+			spa_log_debug(backend->log, "Sending postponed command: %s", cmd->cmd);
+			rfcomm_send_cmd(rfcomm, "%s", cmd->cmd);
+			free(cmd->cmd);
+			free(cmd);
+		}
+
+		if (spa_strstartswith(token, "OK")) {
+			switch(rfcomm->hf_state) {
+			case hfp_hf_brsf:
+				if (rfcomm->codec_negotiation_supported) {
+					char buf[64];
+					struct spa_strbuf str;
+
+					spa_strbuf_init(&str, buf, sizeof(buf));
+					spa_strbuf_append(&str, "1");
+					if (rfcomm->msbc_supported_by_hfp)
+						spa_strbuf_append(&str, ",2");
+					if (rfcomm->lc3_supported_by_hfp)
+						spa_strbuf_append(&str, ",3");
+
+					rfcomm_send_cmd(rfcomm, "AT+BAC=%s", buf);
+					rfcomm->hf_state = hfp_hf_bac;
+				} else {
+					rfcomm_send_cmd(rfcomm, "AT+CIND=?");
+					rfcomm->hf_state = hfp_hf_cind1;
+				}
+				break;
+			case hfp_hf_bac:
 				rfcomm_send_cmd(rfcomm, "AT+CIND=?");
 				rfcomm->hf_state = hfp_hf_cind1;
-			}
-			break;
-		case hfp_hf_bac:
-			rfcomm_send_cmd(rfcomm, "AT+CIND=?");
-			rfcomm->hf_state = hfp_hf_cind1;
-			break;
-		case hfp_hf_cind1:
-			rfcomm_send_cmd(rfcomm, "AT+CIND?");
-			rfcomm->hf_state = hfp_hf_cind2;
-			break;
-		case hfp_hf_cind2:
-			rfcomm_send_cmd(rfcomm, "AT+CMER=3,0,0,1");
-			rfcomm->hf_state = hfp_hf_cmer;
-			break;
-		case hfp_hf_cmer:
-			rfcomm->hf_state = hfp_hf_slc1;
-			rfcomm->slc_configured = true;
-			if (!rfcomm->codec_negotiation_supported) {
-				if (rfcomm_new_transport(rfcomm) < 0) {
-					// TODO: We should manage the missing transport
+				break;
+			case hfp_hf_cind1:
+				rfcomm_send_cmd(rfcomm, "AT+CIND?");
+				rfcomm->hf_state = hfp_hf_cind2;
+				break;
+			case hfp_hf_cind2:
+				rfcomm_send_cmd(rfcomm, "AT+CMER=3,0,0,1");
+				rfcomm->hf_state = hfp_hf_cmer;
+				break;
+			case hfp_hf_cmer:
+				if (rfcomm->hfp_hf_3way) {
+					rfcomm_send_cmd(rfcomm, "AT+CHLD=?");
+					rfcomm->hf_state = hfp_hf_chld;
+					break;
+				}
+				SPA_FALLTHROUGH;
+			case hfp_hf_chld:
+				rfcomm_send_cmd(rfcomm, "AT+CLIP=1");
+				rfcomm->hf_state = hfp_hf_clip;
+				break;
+			case hfp_hf_clip:
+				if (rfcomm->chld_supported) {
+					rfcomm_send_cmd(rfcomm, "AT+CCWA=1");
+					rfcomm->hf_state = hfp_hf_ccwa;
+					break;
+				}
+				SPA_FALLTHROUGH;
+			case hfp_hf_ccwa:
+				if (rfcomm->hfp_hf_cme) {
+					rfcomm_send_cmd(rfcomm, "AT+CMEE=1");
+					rfcomm->hf_state = hfp_hf_cmee;
+					break;
+				}
+				SPA_FALLTHROUGH;
+			case hfp_hf_cmee:
+				if (backend->hfp_disable_nrec && rfcomm->hfp_hf_nrec) {
+					rfcomm_send_cmd(rfcomm, "AT+NREC=0");
+					rfcomm->hf_state = hfp_hf_nrec;
+					break;
+				}
+				SPA_FALLTHROUGH;
+			case hfp_hf_nrec:
+				rfcomm->hf_state = hfp_hf_slc1;
+				rfcomm->slc_configured = true;
+
+				if (!rfcomm->codec_negotiation_supported) {
+					if (rfcomm_new_transport(rfcomm, HFP_AUDIO_CODEC_CVSD) < 0) {
+						// TODO: We should manage the missing transport
+					} else {
+						spa_bt_device_connect_profile(rfcomm->device, rfcomm->profile);
+					}
+				}
+
+				rfcomm->telephony_ag = telephony_ag_new(backend->telephony, 0);
+				rfcomm->telephony_ag->address = strdup(rfcomm->device->address);
+				telephony_ag_set_callbacks(rfcomm->telephony_ag,
+							&telephony_ag_callbacks, rfcomm);
+				if (rfcomm->transport) {
+					rfcomm->telephony_ag->transport.codec = rfcomm->transport->codec;
+					rfcomm->telephony_ag->transport.state = rfcomm->transport->state;
+				}
+				telephony_ag_register(rfcomm->telephony_ag);
+
+				if (rfcomm->hfp_hf_clcc) {
+					rfcomm_send_cmd(rfcomm, "AT+CLCC");
+					rfcomm->hf_state = hfp_hf_slc2;
+					break;
 				} else {
-					rfcomm->transport->codec = HFP_AUDIO_CODEC_CVSD;
-					spa_bt_device_connect_profile(rfcomm->device, rfcomm->profile);
+					// TODO: Create calls if CIND reports one during SLC setup
 				}
+
+				/* Report volume on SLC establishment */
+				SPA_FALLTHROUGH;
+			case hfp_hf_slc2:
+				if (rfcomm_send_volume_cmd(rfcomm, SPA_BT_VOLUME_ID_RX))
+					rfcomm->hf_state = hfp_hf_vgs;
+				break;
+			case hfp_hf_vgs:
+				rfcomm->hf_state = hfp_hf_slc1;
+				if (rfcomm_send_volume_cmd(rfcomm, SPA_BT_VOLUME_ID_TX))
+					rfcomm->hf_state = hfp_hf_vgm;
+				break;
+			default:
+				break;
 			}
-			/* Report volume on SLC establishment */
-			if (rfcomm_send_volume_cmd(rfcomm, SPA_BT_VOLUME_ID_RX))
-				rfcomm->hf_state = hfp_hf_vgs;
-			break;
-		case hfp_hf_slc2:
-			if (rfcomm_send_volume_cmd(rfcomm, SPA_BT_VOLUME_ID_RX))
-				rfcomm->hf_state = hfp_hf_vgs;
-			break;
-		case hfp_hf_vgs:
-			rfcomm->hf_state = hfp_hf_slc1;
-			if (rfcomm_send_volume_cmd(rfcomm, SPA_BT_VOLUME_ID_TX))
-				rfcomm->hf_state = hfp_hf_vgm;
-			break;
-		default:
-			break;
 		}
 	}
 
@@ -1835,6 +2826,11 @@ static void sco_listen_event(struct spa_source *source)
 
 	spa_assert(t->profile & SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY);
 
+	if (rfcomm->telephony_ag && rfcomm->telephony_ag->transport.rejectSCO) {
+		spa_log_info(backend->log, "rejecting SCO, AudioGatewayTransport1.RejectSCO=true");
+		return;
+	}
+
 	if (t->fd >= 0) {
 		spa_log_debug(backend->log, "transport %p: Rejecting, audio already connected", t);
 		return;
@@ -2130,8 +3126,7 @@ static void codec_switch_timer_event(struct spa_source *source)
 		/* Failure, try falling back to CVSD. */
 		rfcomm->hfp_ag_initial_codec_setup = HFP_AG_INITIAL_CODEC_SETUP_NONE;
 		if (rfcomm->transport == NULL) {
-			if (rfcomm_new_transport(rfcomm) == 0) {
-				rfcomm->transport->codec = HFP_AUDIO_CODEC_CVSD;
+			if (rfcomm_new_transport(rfcomm, HFP_AUDIO_CODEC_CVSD) == 0) {
 				spa_bt_device_connect_profile(rfcomm->device, rfcomm->profile);
 			}
 		}
@@ -2291,6 +3286,7 @@ static DBusHandlerResult profile_new_connection(DBusConnection *conn, DBusMessag
 	rfcomm->source.fd = spa_steal_fd(fd);
 	rfcomm->source.mask = SPA_IO_IN;
 	rfcomm->source.rmask = 0;
+	spa_list_init(&rfcomm->hfp_hf_commands);
 	/* By default all indicators are enabled */
 	rfcomm->cind_enabled_indicators = 0xFFFFFFFF;
 	memset(rfcomm->hf_indicators, 0, sizeof rfcomm->hf_indicators);
@@ -2306,10 +3302,9 @@ static DBusHandlerResult profile_new_connection(DBusConnection *conn, DBusMessag
 	spa_list_append(&backend->rfcomm_list, &rfcomm->link);
 
 	if (profile == SPA_BT_PROFILE_HSP_HS || profile == SPA_BT_PROFILE_HSP_AG) {
-		if (rfcomm_new_transport(rfcomm) < 0)
+		if (rfcomm_new_transport(rfcomm, HFP_AUDIO_CODEC_CVSD) < 0)
 			goto fail_need_memory;
 
-		rfcomm->transport->codec = HFP_AUDIO_CODEC_CVSD;
 		rfcomm->has_volume = rfcomm_volume_enabled(rfcomm);
 
 		if (profile == SPA_BT_PROFILE_HSP_AG) {
@@ -2322,7 +3317,9 @@ static DBusHandlerResult profile_new_connection(DBusConnection *conn, DBusMessag
 				rfcomm->transport->path, handler);
 	} else if (profile == SPA_BT_PROFILE_HFP_AG) {
 		/* Start SLC connection */
-		unsigned int hf_features = SPA_BT_HFP_HF_FEATURE_NONE;
+		unsigned int hf_features = SPA_BT_HFP_HF_FEATURE_CLIP | SPA_BT_HFP_HF_FEATURE_3WAY |
+									SPA_BT_HFP_HF_FEATURE_ENHANCED_CALL_STATUS |
+									SPA_BT_HFP_HF_FEATURE_ESCO_S4;
 		bool has_msbc = device_supports_codec(backend, rfcomm->device, HFP_AUDIO_CODEC_MSBC);
 		bool has_lc3 = device_supports_codec(backend, rfcomm->device, HFP_AUDIO_CODEC_LC3_SWB);
 
@@ -2538,6 +3535,9 @@ static int register_profile(struct impl *backend, const char *profile, const cha
 
 		/* We announce wideband speech support anyway */
 		features = SPA_BT_HFP_SDP_AG_FEATURE_WIDEBAND_SPEECH;
+#ifdef HAVE_LC3
+		features |= SPA_BT_HFP_SDP_AG_FEATURE_SUPER_WIDEBAND_SPEECH;
+#endif
 		dbus_message_iter_open_container(&it[1], DBUS_TYPE_DICT_ENTRY, NULL, &it[2]);
 		dbus_message_iter_append_basic(&it[2], DBUS_TYPE_STRING, &str);
 		dbus_message_iter_open_container(&it[2], DBUS_TYPE_VARIANT, "q", &it[3]);
@@ -2545,9 +3545,9 @@ static int register_profile(struct impl *backend, const char *profile, const cha
 		dbus_message_iter_close_container(&it[2], &it[3]);
 		dbus_message_iter_close_container(&it[1], &it[2]);
 
-		/* HFP version 1.7 */
+		/* HFP version 1.9 */
 		str = "Version";
-		version = 0x0107;
+		version = 0x0109;
 		dbus_message_iter_open_container(&it[1], DBUS_TYPE_DICT_ENTRY, NULL, &it[2]);
 		dbus_message_iter_append_basic(&it[2], DBUS_TYPE_STRING, &str);
 		dbus_message_iter_open_container(&it[2], DBUS_TYPE_VARIANT, "q", &it[3]);
@@ -2559,6 +3559,9 @@ static int register_profile(struct impl *backend, const char *profile, const cha
 
 		/* We announce wideband speech support anyway */
 		features = SPA_BT_HFP_SDP_HF_FEATURE_WIDEBAND_SPEECH;
+#ifdef HAVE_LC3
+		features |= SPA_BT_HFP_SDP_HF_FEATURE_SUPER_WIDEBAND_SPEECH;
+#endif
 		dbus_message_iter_open_container(&it[1], DBUS_TYPE_DICT_ENTRY, NULL, &it[2]);
 		dbus_message_iter_append_basic(&it[2], DBUS_TYPE_STRING, &str);
 		dbus_message_iter_open_container(&it[2], DBUS_TYPE_VARIANT, "q", &it[3]);
@@ -2566,9 +3569,9 @@ static int register_profile(struct impl *backend, const char *profile, const cha
 		dbus_message_iter_close_container(&it[2], &it[3]);
 		dbus_message_iter_close_container(&it[1], &it[2]);
 
-		/* HFP version 1.7 */
+		/* HFP version 1.9 */
 		str = "Version";
-		version = 0x0107;
+		version = 0x0109;
 		dbus_message_iter_open_container(&it[1], DBUS_TYPE_DICT_ENTRY, NULL, &it[2]);
 		dbus_message_iter_append_basic(&it[2], DBUS_TYPE_STRING, &str);
 		dbus_message_iter_open_container(&it[2], DBUS_TYPE_VARIANT, "q", &it[3]);
@@ -2837,6 +3840,8 @@ static int backend_native_free(void *data)
 		backend->upower = NULL;
 	}
 
+	spa_clear_ptr(backend->telephony, telephony_free);
+
 	if (backend->ring_timer)
 		spa_loop_utils_destroy_source(backend->loop_utils, backend->ring_timer);
 
@@ -2883,6 +3888,16 @@ fallback:
 	return 0;
 }
 
+static void parse_hfp_disable_nrec(struct impl *backend, const struct spa_dict *info)
+{
+	const char *str;
+
+	if ((str = spa_dict_lookup(info, PROP_KEY_HFP_DISABLE_NREC)) != NULL)
+		backend->hfp_disable_nrec = spa_atob(str);
+	else
+		backend->hfp_disable_nrec = false;
+}
+
 static const struct spa_bt_backend_implementation backend_impl = {
 	SPA_VERSION_BT_BACKEND_IMPLEMENTATION,
 	.free = backend_native_free,
@@ -2940,6 +3955,8 @@ struct spa_bt_backend *backend_native_new(struct spa_bt_monitor *monitor,
 	if (parse_headset_roles(backend, info) < 0)
 		goto fail;
 
+	parse_hfp_disable_nrec(backend, info);
+
 #ifdef HAVE_BLUEZ_5_BACKEND_HSP_NATIVE
 	if (!dbus_connection_register_object_path(backend->conn,
 						  PROFILE_HSP_AG,
@@ -2970,6 +3987,7 @@ struct spa_bt_backend *backend_native_new(struct spa_bt_monitor *monitor,
 
 	backend->modemmanager = mm_register(backend->log, backend->conn, info, &mm_ops, backend);
 	backend->upower = upower_register(backend->log, backend->conn, set_battery_level, backend);
+	backend->telephony = telephony_new(backend->log, backend->dbus, info);
 
 	return &backend->this;
 
diff --git a/spa/plugins/bluez5/bap-codec-caps.h b/spa/plugins/bluez5/bap-codec-caps.h
index 981b0015..9d1da42b 100644
--- a/spa/plugins/bluez5/bap-codec-caps.h
+++ b/spa/plugins/bluez5/bap-codec-caps.h
@@ -25,15 +25,42 @@
                                  LC3_FREQ_44KHZ | \
                                  LC3_FREQ_48KHZ)
 
+#define LC3_VAL_FREQ_8KHZ       8000
+#define LC3_VAL_FREQ_11KHZ      11025
+#define LC3_VAL_FREQ_16KHZ      16000
+#define LC3_VAL_FREQ_22KHZ      22050
+#define LC3_VAL_FREQ_24KHZ      24000
+#define LC3_VAL_FREQ_32KHZ      32000
+#define LC3_VAL_FREQ_44KHZ      44100
+#define LC3_VAL_FREQ_48KHZ      48000
+
 #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_VAL_DUR_7_5         7.5
+#define LC3_VAL_DUR_10          10
+
 #define LC3_TYPE_CHAN           0x03
 #define LC3_CHAN_1              (1 << 0)
 #define LC3_CHAN_2              (1 << 1)
+#define LC3_CHAN_3              (1 << 2)
+#define LC3_CHAN_4              (1 << 3)
+#define LC3_CHAN_5              (1 << 4)
+#define LC3_CHAN_6              (1 << 5)
+#define LC3_CHAN_7              (1 << 6)
+#define LC3_CHAN_8              (1 << 7)
+
+#define LC3_VAL_CHAN_1          1
+#define LC3_VAL_CHAN_2          2
+#define LC3_VAL_CHAN_3          3
+#define LC3_VAL_CHAN_4          4
+#define LC3_VAL_CHAN_5          5
+#define LC3_VAL_CHAN_6          6
+#define LC3_VAL_CHAN_7          7
+#define LC3_VAL_CHAN_8          8
 
 #define LC3_TYPE_FRAMELEN       0x04
 #define LC3_TYPE_BLKS           0x05
diff --git a/spa/plugins/bluez5/bap-codec-lc3.c b/spa/plugins/bluez5/bap-codec-lc3.c
index f628bd48..e9b77232 100644
--- a/spa/plugins/bluez5/bap-codec-lc3.c
+++ b/spa/plugins/bluez5/bap-codec-lc3.c
@@ -14,6 +14,7 @@
 #include <spa/param/audio/format.h>
 #include <spa/param/audio/format-utils.h>
 #include <spa/utils/string.h>
+#include <spa/utils/json.h>
 #include <spa/debug/log.h>
 
 #include <lc3.h>
@@ -23,7 +24,7 @@
 
 #define MAX_PACS	64
 
-static struct spa_log *log;
+static struct spa_log *log_;
 
 struct impl {
 	lc3_encoder_t enc[LC3_MAX_CHANNELS];
@@ -238,20 +239,153 @@ static int write_ltv_uint32(uint8_t *dest, uint8_t type, uint32_t value)
 	return write_ltv(dest, type, &value, sizeof(value));
 }
 
+static uint16_t parse_rates(const char *str)
+{
+	struct spa_json it;
+	uint16_t rate_mask = 0;
+	int value;
+
+	if (spa_json_begin_array_relax(&it, str, strlen(str)) <= 0)
+		return rate_mask;
+
+	while (spa_json_get_int(&it, &value) > 0) {
+		switch (value) {
+		case LC3_VAL_FREQ_8KHZ:
+			rate_mask |= LC3_FREQ_8KHZ;
+			break;
+		case LC3_VAL_FREQ_16KHZ:
+			rate_mask |= LC3_FREQ_16KHZ;
+			break;
+		case LC3_VAL_FREQ_24KHZ:
+			rate_mask |=  LC3_FREQ_24KHZ;
+			break;
+		case LC3_VAL_FREQ_32KHZ:
+			rate_mask |=  LC3_FREQ_32KHZ;
+			break;
+		case LC3_VAL_FREQ_44KHZ:
+			rate_mask |=  LC3_FREQ_44KHZ;
+			break;
+		case LC3_VAL_FREQ_48KHZ:
+			rate_mask |=  LC3_FREQ_48KHZ;
+			break;
+		default:
+			break;
+		}
+	}
+
+	return rate_mask;
+}
+
+static uint8_t parse_durations(const char *str)
+{
+	struct spa_json it;
+	uint8_t duration_mask = 0;
+	float value;
+
+	if (spa_json_begin_array_relax(&it, str, strlen(str)) <= 0)
+		return duration_mask;
+
+	while (spa_json_get_float(&it, &value) > 0) {
+		if (value == (float)LC3_VAL_DUR_7_5)
+			duration_mask |= LC3_DUR_7_5;
+		else if (value == (float)LC3_VAL_DUR_10)
+			duration_mask |= LC3_DUR_10;
+	}
+
+	return duration_mask;
+}
+
+static uint8_t parse_channel_counts(const char *str)
+{
+	struct spa_json it;
+	uint8_t channel_counts = 0;
+	int value;
+
+	if (spa_json_begin_array_relax(&it, str, strlen(str)) <= 0)
+		return channel_counts;
+
+	while (spa_json_get_int(&it, &value) > 0) {
+		switch (value) {
+		case LC3_VAL_CHAN_1:
+			channel_counts |= LC3_CHAN_1;
+			break;
+		case LC3_VAL_CHAN_2:
+			channel_counts |= LC3_CHAN_2;
+			break;
+		case LC3_VAL_CHAN_3:
+			channel_counts |= LC3_CHAN_3;
+			break;
+		case LC3_VAL_CHAN_4:
+			channel_counts |= LC3_CHAN_4;
+			break;
+		case LC3_VAL_CHAN_5:
+			channel_counts |= LC3_CHAN_5;
+			break;
+		case LC3_VAL_CHAN_6:
+			channel_counts |= LC3_CHAN_6;
+			break;
+		case LC3_VAL_CHAN_7:
+			channel_counts |= LC3_CHAN_7;
+			break;
+		case LC3_VAL_CHAN_8:
+			channel_counts |= LC3_CHAN_8;
+			break;
+		default:
+			break;
+		}
+	}
+
+	return channel_counts;
+}
+
 static int codec_fill_caps(const struct media_codec *codec, uint32_t flags,
-		uint8_t caps[A2DP_MAX_CAPS_SIZE])
+		const struct spa_dict *settings, 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_32KHZ | \
-					 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);
+	const char *str;
+	uint16_t framelen[2];
+	uint16_t rate_mask = LC3_FREQ_48KHZ | LC3_FREQ_32KHZ | \
+				LC3_FREQ_24KHZ | LC3_FREQ_16KHZ | LC3_FREQ_8KHZ;
+	uint8_t duration_mask = LC3_DUR_ANY;
+	uint8_t channel_counts = LC3_CHAN_1 | LC3_CHAN_2;
+	uint16_t framelen_min = LC3_MIN_FRAME_BYTES;
+	uint16_t framelen_max = LC3_MAX_FRAME_BYTES;
+	uint8_t max_frames = 2;
+	uint32_t value;
+
+	if (settings && (str = spa_dict_lookup(settings, "bluez5.bap-server-capabilities.rates")))
+		rate_mask = parse_rates(str);
+
+	if (settings && (str = spa_dict_lookup(settings, "bluez5.bap-server-capabilities.durations")))
+		duration_mask = parse_durations(str);
+
+	if (settings && (str = spa_dict_lookup(settings, "bluez5.bap-server-capabilities.channels")))
+		channel_counts = parse_channel_counts(str);
+
+	if (settings && (str = spa_dict_lookup(settings, "bluez5.bap-server-capabilities.framelen_min")))
+		if (spa_atou32(str, &value, 0))
+			framelen_min = value;
+
+	if (settings && (str = spa_dict_lookup(settings, "bluez5.bap-server-capabilities.framelen_max")))
+		if (spa_atou32(str, &value, 0))
+			framelen_max = value;
+
+	if (settings && (str = spa_dict_lookup(settings, "bluez5.bap-server-capabilities.max_frames")))
+		if (spa_atou32(str, &value, 0))
+			max_frames = value;
+
+	framelen[0] = htobs(framelen_min);
+	framelen[1] = htobs(framelen_max);
+
+	data += write_ltv_uint16(data, LC3_TYPE_FREQ, htobs(rate_mask));
+	data += write_ltv_uint8(data, LC3_TYPE_DUR, duration_mask);
+	data += write_ltv_uint8(data, LC3_TYPE_CHAN, channel_counts);
 	data += write_ltv(data, LC3_TYPE_FRAMELEN, framelen, sizeof(framelen));
 	/* XXX: we support only one frame block -> max 2 frames per SDU */
-	data += write_ltv_uint8(data, LC3_TYPE_BLKS, 2);
+	if (max_frames > 2)
+		max_frames = 2;
+
+	data += write_ltv_uint8(data, LC3_TYPE_BLKS, max_frames);
 
 	return data - caps;
 }
@@ -633,7 +767,7 @@ static int pac_cmp(const void *p1, const void *p2)
 {
 	const struct pac_data *pac1 = p1;
 	const struct pac_data *pac2 = p2;
-	struct spa_debug_log_ctx debug_ctx = SPA_LOG_DEBUG_INIT(log, SPA_LOG_LEVEL_TRACE);
+	struct spa_debug_log_ctx debug_ctx = SPA_LOG_DEBUG_INIT(log_, SPA_LOG_LEVEL_TRACE);
 	bap_lc3_t conf1, conf2;
 	int res1, res2;
 
@@ -655,7 +789,7 @@ static int codec_select_config(const struct media_codec *codec, uint32_t flags,
 	uint32_t locations = 0;
 	uint32_t channel_allocation = 0;
 	bool sink = false, duplex = false;
-	struct spa_debug_log_ctx debug_ctx = SPA_LOG_DEBUG_INIT(log, SPA_LOG_LEVEL_TRACE);
+	struct spa_debug_log_ctx debug_ctx = SPA_LOG_DEBUG_INIT(log_, SPA_LOG_LEVEL_TRACE);
 	int i;
 
 	if (caps == NULL)
@@ -670,7 +804,7 @@ static int codec_select_config(const struct media_codec *codec, uint32_t flags,
 		}
 
 		if (spa_atob(spa_dict_lookup(settings, "bluez5.bap.debug")))
-			debug_ctx = SPA_LOG_DEBUG_INIT(log, SPA_LOG_LEVEL_DEBUG);
+			debug_ctx = SPA_LOG_DEBUG_INIT(log_, SPA_LOG_LEVEL_DEBUG);
 
 		/* Is remote endpoint sink or source */
 		sink = spa_atob(spa_dict_lookup(settings, "bluez5.bap.sink"));
@@ -897,7 +1031,7 @@ static int codec_get_qos(const struct media_codec *codec,
 			conf.framelen, conf.framelen);
 	if (!bap_qos) {
 		/* shouldn't happen: select_config should pick existing one */
-		spa_log_error(log, "no QoS settings found");
+		spa_log_error(log_, "no QoS settings found");
 		return -EINVAL;
 	}
 
@@ -955,7 +1089,7 @@ static void *codec_init(const struct media_codec *codec, uint32_t flags,
 		goto error;
 
 	if (!parse_conf(&conf, config, config_len)) {
-		spa_log_error(log, "invalid LC3 config");
+		spa_log_error(log_, "invalid LC3 config");
 		res = -ENOTSUP;
 		goto error;
 	}
@@ -976,12 +1110,12 @@ static void *codec_init(const struct media_codec *codec, uint32_t flags,
 		goto error;
 	}
 
-	spa_log_info(log, "LC3 rate:%d frame_duration:%d channels:%d framelen:%d nblks:%d",
+	spa_log_info(log_, "LC3 rate:%d frame_duration:%d channels:%d framelen:%d nblks:%d",
 			this->samplerate, this->frame_dus, this->channels, this->framelen, conf.n_blks);
 
 	res = lc3_frame_samples(this->frame_dus, this->samplerate);
 	if (res < 0) {
-		spa_log_error(log, "invalid LC3 frame samples");
+		spa_log_error(log_, "invalid LC3 frame samples");
 		res = -EINVAL;
 		goto error;
 	}
@@ -1146,8 +1280,8 @@ static int codec_increase_bitpool(void *data)
 
 static void codec_set_log(struct spa_log *global_log)
 {
-	log = global_log;
-	spa_log_topic_init(log, &codec_plugin_log_topic);
+	log_ = global_log;
+	spa_log_topic_init(log_, &codec_plugin_log_topic);
 }
 
 static int codec_get_bis_config(const struct media_codec *codec, uint8_t *caps,
diff --git a/spa/plugins/bluez5/bluez5-dbus.c b/spa/plugins/bluez5/bluez5-dbus.c
index 19d365c4..e4495f0e 100644
--- a/spa/plugins/bluez5/bluez5-dbus.c
+++ b/spa/plugins/bluez5/bluez5-dbus.c
@@ -138,6 +138,7 @@ struct spa_bt_remote_endpoint {
 	struct spa_list device_link;
 	struct spa_bt_monitor *monitor;
 	char *path;
+	char *transport_path;
 
 	char *uuid;
 	unsigned int codec;
@@ -180,6 +181,7 @@ struct spa_bt_big {
 	int presentation_delay;
 	struct spa_list bis_list;
 	int big_id;
+	int sync_factor;
 };
 
 /*
@@ -218,7 +220,8 @@ struct spa_bt_media_codec_switch {
 
 #define DEFAULT_RECONNECT_PROFILES SPA_BT_PROFILE_NULL
 #define DEFAULT_HW_VOLUME_PROFILES (SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY | SPA_BT_PROFILE_HEADSET_HEAD_UNIT | \
-					SPA_BT_PROFILE_A2DP_SOURCE | SPA_BT_PROFILE_A2DP_SINK)
+					SPA_BT_PROFILE_A2DP_SOURCE | SPA_BT_PROFILE_A2DP_SINK | \
+					SPA_BT_PROFILE_BAP_AUDIO)
 
 #define BT_DEVICE_DISCONNECTED	0
 #define BT_DEVICE_CONNECTED	1
@@ -555,6 +558,8 @@ static enum spa_bt_profile get_codec_profile(const struct media_codec *codec,
 	case SPA_BT_MEDIA_SOURCE:
 		return codec->bap ? SPA_BT_PROFILE_BAP_SOURCE : SPA_BT_PROFILE_A2DP_SOURCE;
 	case SPA_BT_MEDIA_SINK:
+		if (codec->asha)
+			return SPA_BT_PROFILE_ASHA_SINK;
 		return codec->bap ? SPA_BT_PROFILE_BAP_SINK : SPA_BT_PROFILE_A2DP_SINK;
 	case SPA_BT_MEDIA_SOURCE_BROADCAST:
 		return SPA_BT_PROFILE_BAP_BROADCAST_SOURCE;
@@ -2019,10 +2024,11 @@ int spa_bt_device_check_profiles(struct spa_bt_device *device, bool force)
 	uint32_t connected_profiles = device->connected_profiles;
 	uint32_t connectable_profiles =
 		device->adapter ? adapter_connectable_profiles(device->adapter) : 0;
-	uint32_t direction_masks[3] = {
+	uint32_t direction_masks[4] = {
 		SPA_BT_PROFILE_MEDIA_SINK | SPA_BT_PROFILE_HEADSET_HEAD_UNIT,
 		SPA_BT_PROFILE_MEDIA_SOURCE,
 		SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY,
+		SPA_BT_PROFILE_ASHA_SINK,
 	};
 	bool direction_connected = false;
 	bool set_connected = true;
@@ -2075,9 +2081,10 @@ static void device_set_connected(struct spa_bt_device *device, int connected)
 	if (device->connected && !connected)
 		device->connected_profiles = 0;
 
-	if (connected)
+	if (connected) {
+		spa_bt_quirks_log_features(monitor->quirks, device->adapter, device);
 		spa_bt_device_check_profiles(device, false);
-	else {
+	} else {
 		/* Stop codec switch on disconnect */
 		struct spa_bt_media_codec_switch *sw;
 		spa_list_consume(sw, &device->codec_switch_list, device_link)
@@ -2093,13 +2100,11 @@ static void device_update_set_status(struct spa_bt_device *device, bool force, c
 
 int spa_bt_device_connect_profile(struct spa_bt_device *device, enum spa_bt_profile profile)
 {
-	uint32_t prev_connected = device->connected_profiles;
 	device->connected_profiles |= profile;
-	if ((prev_connected ^ device->connected_profiles) & SPA_BT_PROFILE_BAP_DUPLEX)
+	if (profile & SPA_BT_PROFILE_BAP_DUPLEX)
 		device_update_set_status(device, true, NULL);
 	spa_bt_device_check_profiles(device, false);
-	if (device->connected_profiles != prev_connected)
-		spa_bt_device_emit_profiles_changed(device, device->profiles, prev_connected);
+	spa_bt_device_emit_profiles_changed(device, profile);
 	return 0;
 }
 
@@ -2457,8 +2462,7 @@ static int device_update_props(struct spa_bt_device *device,
 			}
 
 			if (device->profiles != prev_profiles)
-				spa_bt_device_emit_profiles_changed(
-					device, prev_profiles, device->connected_profiles);
+				spa_bt_device_emit_profiles_changed(device, 0);
 		}
 		else if (spa_streq(key, "Sets")) {
 			device_update_device_sets_prop(device, &it[1]);
@@ -2494,12 +2498,13 @@ bool spa_bt_device_supports_media_codec(struct spa_bt_device *device, const stru
 		{ SPA_BLUETOOTH_AUDIO_CODEC_APTX_LL_DUPLEX, SPA_BT_FEATURE_A2DP_DUPLEX },
 		{ SPA_BLUETOOTH_AUDIO_CODEC_FASTSTREAM_DUPLEX, SPA_BT_FEATURE_A2DP_DUPLEX },
 	};
+	bool is_a2dp = !codec->bap && !codec->asha;
 	size_t i;
 
 	if (!is_media_codec_enabled(device->monitor, codec))
 		return false;
 
-	if (!device->adapter->a2dp_application_registered && !codec->bap) {
+	if (!device->adapter->a2dp_application_registered && is_a2dp) {
 		/* Codec switching not supported: only plain SBC allowed */
 		return (codec->codec_id == A2DP_CODEC_SBC && spa_streq(codec->name, "sbc") &&
 				device->adapter->legacy_endpoints_registered);
@@ -2646,6 +2651,8 @@ static struct spa_bt_device *create_bcast_device(struct spa_bt_monitor *monitor,
 	return d;
 }
 
+static int setup_asha_transport(struct spa_bt_remote_endpoint *remote_endpoint, struct spa_bt_monitor *monitor);
+
 static int remote_endpoint_update_props(struct spa_bt_remote_endpoint *remote_endpoint,
 				DBusMessageIter *props_iter,
 				DBusMessageIter *invalidated_iter)
@@ -2699,6 +2706,11 @@ static int remote_endpoint_update_props(struct spa_bt_remote_endpoint *remote_en
 						spa_list_append(&device->remote_endpoint_list, &remote_endpoint->device_link);
 				}
 			}
+			/* For ASHA */
+			else if (spa_streq(key, "Transport")) {
+				free(remote_endpoint->transport_path);
+				remote_endpoint->transport_path = strdup(value);
+			}
 		}
 		else if (type == DBUS_TYPE_BOOLEAN) {
 			int value;
@@ -2722,6 +2734,16 @@ static int remote_endpoint_update_props(struct spa_bt_remote_endpoint *remote_en
 				remote_endpoint->codec = value;
 			}
 		}
+		/* Codecs property is present for ASHA */
+		else if (type == DBUS_TYPE_UINT16) {
+			uint16_t value;
+
+			dbus_message_iter_get_basic(&it[1], &value);
+
+			if (spa_streq(key, "Codecs")) {
+				spa_log_debug(monitor->log, "remote_endpoint %p: %s=%02x", remote_endpoint, key, value);
+			}
+		}
 		else if (spa_streq(key, "Capabilities")) {
 			DBusMessageIter iter;
 			uint8_t *value;
@@ -2745,6 +2767,23 @@ static int remote_endpoint_update_props(struct spa_bt_remote_endpoint *remote_en
 				remote_endpoint->capabilities_len = len;
 			}
 		}
+		/* HiSyncId property is present for ASHA */
+		else if (spa_streq(key, "HiSyncId")) {
+			/*
+			 * TODO: Required for Stereo support in ASHA, for now just log.
+			 */
+			DBusMessageIter iter;
+			uint8_t *value;
+			int len;
+
+			if (!check_iter_signature(&it[1], "ay"))
+				goto next;
+
+			dbus_message_iter_recurse(&it[1], &iter);
+			dbus_message_iter_get_fixed_array(&iter, &value, &len);
+
+			spa_log_debug(monitor->log, "remote_endpoint %p: %s=%d", remote_endpoint, key, len);
+		}
 		else
 			spa_log_debug(monitor->log, "remote_endpoint %p: unhandled key %s", remote_endpoint, key);
 
@@ -2762,6 +2801,11 @@ next:
 		profile = spa_bt_profile_from_uuid(remote_endpoint->uuid);
 		if (profile & SPA_BT_PROFILE_BAP_AUDIO)
 			spa_bt_device_add_profile(remote_endpoint->device, profile);
+
+		if (spa_streq(remote_endpoint->uuid, SPA_BT_UUID_ASHA_SINK)) {
+			if (profile & SPA_BT_PROFILE_ASHA_SINK)
+				setup_asha_transport(remote_endpoint, monitor);
+		}
 	}
 
 	return 0;
@@ -2795,6 +2839,7 @@ static void remote_endpoint_free(struct spa_bt_remote_endpoint *remote_endpoint)
 
 	spa_list_remove(&remote_endpoint->link);
 	free(remote_endpoint->path);
+	free(remote_endpoint->transport_path);
 	free(remote_endpoint->uuid);
 	free(remote_endpoint->capabilities);
 	free(remote_endpoint);
@@ -2904,7 +2949,6 @@ void spa_bt_transport_free(struct spa_bt_transport *transport)
 {
 	struct spa_bt_monitor *monitor = transport->monitor;
 	struct spa_bt_device *device = transport->device;
-	uint32_t prev_connected = 0;
 
 	spa_log_debug(monitor->log, "transport %p: free %s", transport, transport->path);
 
@@ -2931,7 +2975,8 @@ void spa_bt_transport_free(struct spa_bt_transport *transport)
 	cancel_and_unref(&transport->volume_call);
 
 	if (transport->fd >= 0) {
-		spa_bt_player_set_state(transport->device->adapter->dummy_player, SPA_BT_PLAYER_STOPPED);
+		if (device)
+			spa_bt_player_set_state(device->adapter->dummy_player, SPA_BT_PLAYER_STOPPED);
 
 		shutdown(transport->fd, SHUT_RDWR);
 		close(transport->fd);
@@ -2939,16 +2984,20 @@ void spa_bt_transport_free(struct spa_bt_transport *transport)
 	}
 
 	spa_list_remove(&transport->link);
-	if (transport->device) {
-		prev_connected = transport->device->connected_profiles;
-		transport->device->connected_profiles &= ~transport->profile;
+	if (device) {
+		struct spa_bt_transport *t;
+		uint32_t disconnected = transport->profile;
+
 		spa_list_remove(&transport->device_link);
-	}
 
-	if (device && device->connected_profiles != prev_connected) {
-		if ((prev_connected ^ device->connected_profiles) & SPA_BT_PROFILE_BAP_DUPLEX)
+		spa_list_for_each(t, &device->transport_list, device_link)
+			disconnected &= ~t->profile;
+		device->connected_profiles &= ~disconnected;
+
+		if (transport->profile & SPA_BT_PROFILE_BAP_DUPLEX)
 			device_update_set_status(device, true, NULL);
-		spa_bt_device_emit_profiles_changed(device, device->profiles, prev_connected);
+
+		spa_bt_device_emit_profiles_changed(device, transport->profile);
 	}
 
 	spa_list_remove(&transport->bap_transport_linked);
@@ -3165,6 +3214,8 @@ static void spa_bt_transport_volume_changed(struct spa_bt_transport *transport)
 		volume_id = SPA_BT_VOLUME_ID_TX;
 	else if (transport->profile & SPA_BT_PROFILE_A2DP_SOURCE)
 		volume_id = SPA_BT_VOLUME_ID_RX;
+	else if (transport->profile & SPA_BT_PROFILE_ASHA_SINK)
+		volume_id = SPA_BT_VOLUME_ID_TX;
 	else
 		return;
 
@@ -3406,6 +3457,12 @@ static int transport_update_props(struct spa_bt_transport *transport,
 				t_volume = &transport->volumes[SPA_BT_VOLUME_ID_TX];
 			else if (transport->profile & SPA_BT_PROFILE_A2DP_SOURCE)
 				t_volume = &transport->volumes[SPA_BT_VOLUME_ID_RX];
+			else if (transport->profile & SPA_BT_PROFILE_ASHA_SINK)
+				t_volume = &transport->volumes[SPA_BT_VOLUME_ID_TX];
+			else if (transport->profile & SPA_BT_PROFILE_BAP_SINK)
+				t_volume = &transport->volumes[SPA_BT_VOLUME_ID_TX];
+			else if (transport->profile & SPA_BT_PROFILE_BAP_SOURCE)
+				t_volume = &transport->volumes[SPA_BT_VOLUME_ID_RX];
 			else
 				goto next;
 
@@ -3455,6 +3512,9 @@ static int transport_update_props(struct spa_bt_transport *transport,
 			if (!check_iter_signature(&it[1], "ao"))
 				goto next;
 
+			spa_list_remove(&transport->bap_transport_linked);
+			spa_list_init(&transport->bap_transport_linked);
+
 			dbus_message_iter_recurse(&it[1], &iter);
 			while (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_INVALID) {
 				const char *transport_path;
@@ -3554,7 +3614,7 @@ static int transport_set_volume(void *data, int id, float volume)
 	if (!t_volume->active || !spa_bt_transport_volume_enabled(transport))
 		return -ENOTSUP;
 
-	value = spa_bt_volume_linear_to_hw(volume, 127);
+	value = spa_bt_volume_linear_to_hw(volume, t_volume->hw_volume_max);
 	t_volume->volume = volume;
 
 	/* AVRCP volume would not applied on remote sink device
@@ -3684,21 +3744,32 @@ static void transport_acquire_reply(DBusPendingCall *pending, void *user_data)
 	transport_sync_volume(transport);
 
 finish:
-	if (ret < 0)
+	if (ret < 0) {
 		spa_bt_transport_set_state(transport, SPA_BT_TRANSPORT_STATE_ERROR);
-	else {
+
+		/* For broadcast, skip handling links. Each link acquire
+		 * is handled separately.
+		 */
+		if ((transport->profile == SPA_BT_PROFILE_BAP_BROADCAST_SINK) ||
+			(transport->profile == SPA_BT_PROFILE_BAP_BROADCAST_SOURCE))
+			return;
+	} else {
 		if (transport_create_iso_io(transport) < 0)
 			spa_log_error(monitor->log, "transport %p: transport_create_iso_io failed",
 					transport);
-		/* For broadcast the initiator moves the transport state to SPA_BT_TRANSPORT_STATE_ACTIVE */
+		/* For broadcast, each transport has a different fd, so it needs to be
+		 * acquired independently from others. Each transport moves to
+		 * SPA_BT_TRANSPORT_STATE_ACTIVE after acquire is completed.
+		 */
 		/* TODO: handling multiple BIGs support */
 		if ((transport->profile == SPA_BT_PROFILE_BAP_BROADCAST_SINK) ||
 			(transport->profile == SPA_BT_PROFILE_BAP_BROADCAST_SOURCE))	{
 			spa_bt_transport_set_state(transport, SPA_BT_TRANSPORT_STATE_ACTIVE);
-		} else {
-			if (!transport->bap_initiator)
-				spa_bt_transport_set_state(transport, SPA_BT_TRANSPORT_STATE_ACTIVE);
+			return;
 		}
+
+		if (!transport->bap_initiator)
+			spa_bt_transport_set_state(transport, SPA_BT_TRANSPORT_STATE_ACTIVE);
 	}
 
 	/* For LE Audio, multiple transport from the same device may share the same
@@ -3758,6 +3829,13 @@ static int do_transport_acquire(struct spa_bt_transport *transport)
 	spa_autoptr(DBusMessage) m = NULL;
 	struct spa_bt_transport *t_linked;
 
+	if ((transport->profile == SPA_BT_PROFILE_BAP_BROADCAST_SINK) ||
+		(transport->profile == SPA_BT_PROFILE_BAP_BROADCAST_SOURCE))
+		/* For Broadcast, all linked transports need to be
+		 * acquired independently, since they have different fds.
+		 */
+		goto acquire;
+
 	spa_list_for_each(t_linked, &transport->bap_transport_linked, bap_transport_linked) {
 		/* If a linked transport has been acquired, it will do all the work */
 		if (t_linked->acquire_call || t_linked->acquired) {
@@ -3768,6 +3846,7 @@ static int do_transport_acquire(struct spa_bt_transport *transport)
 		}
 	}
 
+acquire:
 	if (transport->acquire_call)
 		return -EBUSY;
 
@@ -3862,10 +3941,17 @@ static int do_transport_release(struct spa_bt_transport *transport)
 		transport->iso_io = NULL;
 	}
 
-	/* For LE Audio, multiple transport stream (CIS) can be linked together (CIG).
+	/* For Unicast LE Audio, multiple transport stream (CIS) can be linked together (CIG).
 	 * If they are part of the same device they reuse the same fd, and call to
 	 * release should be done for the last one only.
+	 *
+	 * For Broadcast LE Audio, since linked transports have different fds, they
+	 * should be released independently.
 	 */
+	if ((transport->profile == SPA_BT_PROFILE_BAP_BROADCAST_SINK) ||
+		(transport->profile == SPA_BT_PROFILE_BAP_BROADCAST_SOURCE))
+		goto release;
+
 	spa_list_for_each(t_linked, &transport->bap_transport_linked, bap_transport_linked) {
 		if (t_linked->acquire_call || t_linked->acquired) {
 			linked = true;
@@ -3878,6 +3964,7 @@ static int do_transport_release(struct spa_bt_transport *transport)
 		return 0;
 	}
 
+release:
 	if (transport->fd >= 0) {
 		close(transport->fd);
 		transport->fd = -1;
@@ -3951,14 +4038,117 @@ static int transport_release(void *data)
 	return do_transport_release(data);
 }
 
+static int transport_set_delay(void *data, int64_t delay_nsec)
+{
+	struct spa_bt_transport *transport = data;
+	struct spa_bt_monitor *monitor = transport->monitor;
+	DBusMessageIter it[2];
+	spa_autoptr(DBusMessage) m = NULL;
+	uint16_t value;
+	const char *property = "Delay", *interface = BLUEZ_MEDIA_TRANSPORT_INTERFACE;
+
+	if (!(transport->profile & SPA_BT_PROFILE_A2DP_DUPLEX))
+		return -ENOTSUP;
+
+	value = SPA_CLAMP(delay_nsec / (100 * SPA_NSEC_PER_USEC), 0, 10 * UINT16_MAX);
+
+	if (transport->delay_us == 100 * value)
+		return 0;
+	transport->delay_us = 100 * value;
+
+	m = dbus_message_new_method_call(BLUEZ_SERVICE, transport->path, DBUS_INTERFACE_PROPERTIES, "Set");
+	if (m == NULL)
+		return -ENOMEM;
+
+	dbus_message_iter_init_append(m, &it[0]);
+	dbus_message_iter_append_basic(&it[0], DBUS_TYPE_STRING, &interface);
+	dbus_message_iter_append_basic(&it[0], DBUS_TYPE_STRING, &property);
+	dbus_message_iter_open_container(&it[0], DBUS_TYPE_VARIANT, DBUS_TYPE_UINT16_AS_STRING, &it[1]);
+	dbus_message_iter_append_basic(&it[1], DBUS_TYPE_UINT16, &value);
+	dbus_message_iter_close_container(&it[0], &it[1]);
+
+	if (!dbus_connection_send(monitor->conn, m, NULL))
+		return -EIO;
+
+	spa_log_debug(monitor->log, "transport %p: set delay %d us", transport, 100 * value);
+	return 0;
+}
 
 static const struct spa_bt_transport_implementation transport_impl = {
 	SPA_VERSION_BT_TRANSPORT_IMPLEMENTATION,
 	.acquire = transport_acquire,
 	.release = transport_release,
 	.set_volume = transport_set_volume,
+	.set_delay = transport_set_delay,
 };
 
+static int setup_asha_transport(struct spa_bt_remote_endpoint *remote_endpoint, struct spa_bt_monitor *monitor)
+{
+	const struct media_codec * const * const media_codecs = monitor->media_codecs;
+	const struct media_codec *codec = NULL;
+	struct spa_bt_transport *transport;
+	char *tpath;
+
+	if (!remote_endpoint->transport_path) {
+		spa_log_error(monitor->log, "Missing ASHA transport path");
+		return -EINVAL;
+	}
+
+	transport = spa_bt_transport_find(monitor, remote_endpoint->transport_path);
+	if (transport != NULL) {
+		spa_log_debug(monitor->log, "transport %p: free %s",
+			transport, transport->path);
+		spa_bt_transport_free(transport);
+	}
+
+	tpath = strdup(remote_endpoint->transport_path);
+	transport = spa_bt_transport_create(monitor, tpath, 0);
+	if (transport == NULL) {
+		spa_log_error(monitor->log, "Failed to create transport for %s",
+				remote_endpoint->transport_path);
+		free(tpath);
+		return -EINVAL;
+	}
+
+	spa_bt_transport_set_implementation(transport, &transport_impl, transport);
+
+	spa_log_debug(monitor->log, "Created ASHA transport for %s", remote_endpoint->transport_path);
+
+	for (int i = 0; media_codecs[i]; i++) {
+		const struct media_codec *mcodec = media_codecs[i];
+		if (!mcodec->asha)
+			continue;
+		if (!spa_streq(mcodec->name, "g722"))
+			continue;
+		codec = mcodec;
+		spa_log_debug(monitor->log, "Setting ASHA codec: %s", mcodec->name);
+	}
+
+	free(transport->endpoint_path);
+	transport->endpoint_path = strdup(remote_endpoint->path);
+	transport->profile = SPA_BT_PROFILE_ASHA_SINK;
+	transport->media_codec = codec;
+	transport->device = remote_endpoint->device;
+
+	spa_list_append(&remote_endpoint->device->transport_list, &transport->device_link);
+
+	spa_bt_device_update_last_bluez_action_time(transport->device);
+
+	transport->volumes[SPA_BT_VOLUME_ID_TX].active = true;
+	transport->volumes[SPA_BT_VOLUME_ID_TX].volume = DEFAULT_TX_VOLUME;
+	transport->n_channels = 1;
+	transport->channels[0] = SPA_AUDIO_CHANNEL_MONO;
+
+	spa_bt_device_add_profile(transport->device, transport->profile);
+	spa_bt_device_connect_profile(transport->device, transport->profile);
+
+	transport_sync_volume(transport);
+
+	spa_log_debug(monitor->log, "ASHA transport setup complete");
+
+	return 0;
+}
+
 static void media_codec_switch_reply(DBusPendingCall *pending, void *userdata);
 
 static int media_codec_switch_cmp(const void *a, const void *b);
@@ -4506,7 +4696,10 @@ static DBusHandlerResult endpoint_set_configuration(DBusConnection *conn,
 
 	for (int i = 0; i < SPA_BT_VOLUME_ID_TERM; ++i) {
 		transport->volumes[i].hw_volume = SPA_BT_VOLUME_INVALID;
-		transport->volumes[i].hw_volume_max = SPA_BT_VOLUME_A2DP_MAX;
+		if (profile & SPA_BT_PROFILE_BAP_AUDIO)
+			transport->volumes[i].hw_volume_max = SPA_BT_VOLUME_BAP_MAX;
+		else
+			transport->volumes[i].hw_volume_max = SPA_BT_VOLUME_A2DP_MAX;
 	}
 
 	free(transport->endpoint_path);
@@ -4722,7 +4915,7 @@ static int bluez_register_endpoint_legacy(struct spa_bt_adapter *adapter,
 	if (ret < 0)
 		return ret;
 
-	ret = caps_size = codec->fill_caps(codec, sink ? MEDIA_CODEC_FLAG_SINK : 0, caps);
+	ret = caps_size = codec->fill_caps(codec, sink ? MEDIA_CODEC_FLAG_SINK : 0, &monitor->global_settings, caps);
 	if (ret < 0)
 		return ret;
 
@@ -4828,7 +5021,7 @@ static void append_media_object(DBusMessageIter *iter, const char *endpoint,
 	append_basic_variant_dict_entry(&dict, "Codec", DBUS_TYPE_BYTE, "y", &codec_id);
 	append_basic_array_variant_dict_entry(&dict, "Capabilities", "ay", "y", DBUS_TYPE_BYTE, caps, caps_size);
 
-	if (spa_bt_profile_from_uuid(uuid) & SPA_BT_PROFILE_A2DP_SOURCE) {
+	if (spa_bt_profile_from_uuid(uuid) & (SPA_BT_PROFILE_A2DP_SINK | SPA_BT_PROFILE_A2DP_SOURCE)) {
 		dbus_bool_t delay_reporting = TRUE;
 
 		append_basic_variant_dict_entry(&dict, "DelayReporting", DBUS_TYPE_BOOLEAN, "b", &delay_reporting);
@@ -4914,12 +5107,14 @@ static DBusHandlerResult object_manager_handler(DBusConnection *c, DBusMessage *
 
 			if (codec->bap != is_bap)
 				continue;
+			if (codec->asha)
+				continue;
 
 			if (!is_media_codec_enabled(monitor, codec))
 				continue;
 
 			if (endpoint_should_be_registered(monitor, codec, SPA_BT_MEDIA_SINK)) {
-				caps_size = codec->fill_caps(codec, MEDIA_CODEC_FLAG_SINK, caps);
+				caps_size = codec->fill_caps(codec, MEDIA_CODEC_FLAG_SINK, &monitor->global_settings, caps);
 				if (caps_size < 0)
 					continue;
 
@@ -4934,7 +5129,7 @@ static DBusHandlerResult object_manager_handler(DBusConnection *c, DBusMessage *
 			}
 
 			if (endpoint_should_be_registered(monitor, codec, SPA_BT_MEDIA_SOURCE)) {
-				caps_size = codec->fill_caps(codec, 0, caps);
+				caps_size = codec->fill_caps(codec, 0, &monitor->global_settings, caps);
 				if (caps_size < 0)
 					continue;
 
@@ -4950,7 +5145,7 @@ static DBusHandlerResult object_manager_handler(DBusConnection *c, DBusMessage *
 
 			if (codec->bap && register_bcast) {
 				if (endpoint_should_be_registered(monitor, codec, SPA_BT_MEDIA_SOURCE_BROADCAST)) {
-					caps_size = codec->fill_caps(codec, 0, caps);
+					caps_size = codec->fill_caps(codec, 0, &monitor->global_settings, caps);
 					if (caps_size < 0)
 						continue;
 
@@ -4965,7 +5160,7 @@ static DBusHandlerResult object_manager_handler(DBusConnection *c, DBusMessage *
 				}
 
 				if (endpoint_should_be_registered(monitor, codec, SPA_BT_MEDIA_SINK_BROADCAST)) {
-					caps_size = codec->fill_caps(codec, MEDIA_CODEC_FLAG_SINK, caps);
+					caps_size = codec->fill_caps(codec, MEDIA_CODEC_FLAG_SINK, &monitor->global_settings, caps);
 					if (caps_size < 0)
 						continue;
 
@@ -5310,8 +5505,6 @@ static void configure_bis(struct spa_bt_monitor *monitor,
 	int options = 0;
 	int skip = 0;
 	int sync_cte_type = 0;
-	/* sync_factor should be >=2 to avoid invalid extended advertising interval value */
-	int sync_factor = 2;
 	int sync_timeout = 2000;
 	int timeout = 2000;
 
@@ -5364,7 +5557,12 @@ static void configure_bis(struct spa_bt_monitor *monitor,
 
 	append_basic_variant_dict_entry(&qos_dict, "BIG", DBUS_TYPE_BYTE, "y", &big->big_id);
 	append_basic_variant_dict_entry(&qos_dict, "BIS", DBUS_TYPE_BYTE, "y", &bis_id);
-	append_basic_variant_dict_entry(&qos_dict, "SyncFactor", DBUS_TYPE_BYTE, "y", &sync_factor);
+
+	/* sync_factor should be >=2 to avoid invalid extended advertising interval value */
+	if (big->sync_factor < 2)
+		big->sync_factor = 2;
+
+	append_basic_variant_dict_entry(&qos_dict, "SyncFactor", DBUS_TYPE_BYTE, "y", &big->sync_factor);
 	append_basic_variant_dict_entry(&qos_dict, "Options", DBUS_TYPE_BYTE, "y", &options);
 	append_basic_variant_dict_entry(&qos_dict, "Skip", DBUS_TYPE_UINT16, "q", &skip);
 	append_basic_variant_dict_entry(&qos_dict, "SyncTimeout", DBUS_TYPE_UINT16, "q", &sync_timeout);
@@ -5462,6 +5660,8 @@ static void interface_added(struct spa_bt_monitor *monitor,
 						object_path);
 				return;
 			}
+			spa_log_info(monitor->log, "Created Bluetooth device %s",
+					object_path);
 		}
 
 		device_update_props(d, props_iter, NULL);
@@ -5496,7 +5696,7 @@ static void interface_added(struct spa_bt_monitor *monitor,
 
 		d = ep->device;
 		if (d)
-			spa_bt_device_emit_profiles_changed(d, d->profiles, d->connected_profiles);
+			spa_bt_device_emit_profiles_changed(d, 0);
 
 		if (spa_streq(ep->uuid, SPA_BT_UUID_BAP_BROADCAST_SINK)) {
 			int ret, i;
@@ -5590,7 +5790,7 @@ static void interfaces_removed(struct spa_bt_monitor *monitor, DBusMessageIter *
 				struct spa_bt_device *d = ep->device;
 				remote_endpoint_free(ep);
 				if (d)
-					spa_bt_device_emit_profiles_changed(d, d->profiles, d->connected_profiles);
+					spa_bt_device_emit_profiles_changed(d, 0);
 			}
 		} else if (spa_streq(interface_name, BLUEZ_MEDIA_TRANSPORT_INTERFACE)) {
 			struct spa_bt_transport *transport;
@@ -5600,7 +5800,7 @@ static void interfaces_removed(struct spa_bt_monitor *monitor, DBusMessageIter *
 					struct spa_bt_device *d = transport->device;
 					if (d != NULL){
 						device_free(d);
-					}		
+					}
 				} else if (transport->profile == SPA_BT_PROFILE_BAP_BROADCAST_SOURCE) {
 					/*
 					 * For each transport that has a broadcast source profile,
@@ -5610,7 +5810,7 @@ static void interfaces_removed(struct spa_bt_monitor *monitor, DBusMessageIter *
 					 * for this case will have the scanned device to the transport
 					 * "/fd0" and create new devices for the other transports from this device
 					 * that appear only in case of multiple BISes per BIG.
-					 * 
+					 *
 					 * Here we delete the created devices.
 					 */
 					char *pos = strstr(transport->path, "/fd0");
@@ -5858,7 +6058,7 @@ static DBusHandlerResult filter_cb(DBusConnection *bus, DBusMessage *m, void *us
 
 			d = ep->device;
 			if (d)
-				spa_bt_device_emit_profiles_changed(d, d->profiles, d->connected_profiles);
+				spa_bt_device_emit_profiles_changed(d, 0);
 		}
 		else if (spa_streq(iface, BLUEZ_MEDIA_TRANSPORT_INTERFACE)) {
 			struct spa_bt_transport *transport;
@@ -6075,13 +6275,11 @@ impl_get_size(const struct spa_handle_factory *factory,
 
 int spa_bt_profiles_from_json_array(const char *str)
 {
-	struct spa_json it, it_array;
+	struct spa_json it_array;
 	char role_name[256];
 	enum spa_bt_profile profiles = SPA_BT_PROFILE_NULL;
 
-	spa_json_init(&it, str, strlen(str));
-
-	if (spa_json_enter_array(&it, &it_array) <= 0)
+	if (spa_json_begin_array(&it_array, str, strlen(str)) <= 0)
 		return -EINVAL;
 
 	while (spa_json_get_string(&it_array, role_name, sizeof(role_name)) > 0) {
@@ -6105,6 +6303,8 @@ int spa_bt_profiles_from_json_array(const char *str)
 			profiles |= SPA_BT_PROFILE_BAP_BROADCAST_SOURCE;
 		} else if (spa_streq(role_name, "bap_bcast_sink")) {
 			profiles |= SPA_BT_PROFILE_BAP_BROADCAST_SINK;
+		} else if (spa_streq(role_name, "asha_sink")) {
+			profiles |= SPA_BT_PROFILE_ASHA_SINK;
 		}
 	}
 
@@ -6115,7 +6315,7 @@ static int parse_roles(struct spa_bt_monitor *monitor, const struct spa_dict *in
 {
 	const char *str;
 	int res = 0;
-	int profiles = SPA_BT_PROFILE_MEDIA_SINK | SPA_BT_PROFILE_MEDIA_SOURCE;
+	int profiles = SPA_BT_PROFILE_MEDIA_SINK | SPA_BT_PROFILE_MEDIA_SOURCE | SPA_BT_PROFILE_ASHA_SINK;
 
 	/* HSP/HFP backends parse this property separately */
 	if (info && (str = spa_dict_lookup(info, "bluez5.roles"))) {
@@ -6144,7 +6344,7 @@ static void parse_broadcast_source_config(struct spa_bt_monitor *monitor, const
 	char bcode[BROADCAST_CODE_LEN + 3];
 	int cursor;
 	int big_id = 0;
-	struct spa_json it[4], it_array[4];
+	struct spa_json it[3], it_array[4];
 	struct spa_list big_list = SPA_LIST_INIT(&big_list);
 	struct spa_error_location loc;
 	struct spa_bt_big *big;
@@ -6153,14 +6353,12 @@ static void parse_broadcast_source_config(struct spa_bt_monitor *monitor, const
 	if (!(info && (str = spa_dict_lookup(info, "bluez5.bcast_source.config"))))
 		return;
 
-	spa_json_init(&it[0], str, strlen(str));
-
 	/* Verify is an array of BIGS */
-	if (spa_json_enter_array(&it[0], &it_array[0]) <= 0)
+	if (spa_json_begin_array(&it_array[0], str, strlen(str)) <= 0)
 		goto parse_failed;
 
 	/* Iterate on all BIG objects */
-	while (spa_json_enter_object(&it_array[0], &it[1]) > 0) {
+	while (spa_json_enter_object(&it_array[0], &it[0]) > 0) {
 		struct spa_bt_big *big_entry = calloc(1, sizeof(struct spa_bt_big));
 
 		if (!big_entry)
@@ -6171,22 +6369,26 @@ static void parse_broadcast_source_config(struct spa_bt_monitor *monitor, const
 		spa_list_append(&big_list, &big_entry->link);
 
 		/* Iterate on all BIG values */
-		while (spa_json_get_string(&it[1], key, sizeof(key)) > 0) {
+		while (spa_json_get_string(&it[0], key, sizeof(key)) > 0) {
 			if (spa_streq(key, "broadcast_code")) {
-				if (spa_json_get_string(&it[1], bcode, sizeof(bcode)) <= 0)
+				if (spa_json_get_string(&it[0], bcode, sizeof(bcode)) <= 0)
 						goto parse_failed;
 				if (strlen(bcode) > BROADCAST_CODE_LEN)
 					goto parse_failed;
 				memcpy(big_entry->broadcast_code, bcode, strlen(bcode));
 				spa_log_debug(monitor->log, "big_entry->broadcast_code %s", big_entry->broadcast_code);
 			} else if (spa_streq(key, "encryption")) {
-				if (spa_json_get_bool(&it[1], &big_entry->encryption) <= 0)
+				if (spa_json_get_bool(&it[0], &big_entry->encryption) <= 0)
 					goto parse_failed;
 				spa_log_debug(monitor->log, "big_entry->encryption %d", big_entry->encryption);
+			} else if (spa_streq(key, "sync_factor")) {
+				if (spa_json_get_int(&it[0], &big_entry->sync_factor) <= 0)
+					goto parse_failed;
+				spa_log_debug(monitor->log, "big_entry->sync_factor %d", big_entry->sync_factor);
 			} else if (spa_streq(key, "bis")) {
-				if (spa_json_enter_array(&it[1], &it_array[1]) <= 0)
+				if (spa_json_enter_array(&it[0], &it_array[1]) <= 0)
 					goto parse_failed;
-				while (spa_json_enter_object(&it_array[1], &it[2]) > 0) {
+				while (spa_json_enter_object(&it_array[1], &it[1]) > 0) {
 					/* Iterate on all BIS values */
 					struct spa_bt_bis *bis_entry = calloc(1, sizeof(struct spa_bt_bis));
 
@@ -6196,19 +6398,19 @@ static void parse_broadcast_source_config(struct spa_bt_monitor *monitor, const
 					spa_list_init(&bis_entry->metadata_list);
 					spa_list_append(&big_entry->bis_list, &bis_entry->link);
 
-					while (spa_json_get_string(&it[2], bis_key, sizeof(bis_key)) > 0) {
+					while (spa_json_get_string(&it[1], bis_key, sizeof(bis_key)) > 0) {
 						if (spa_streq(bis_key, "qos_preset")) {
-							if (spa_json_get_string(&it[2], bis_entry->qos_preset, sizeof(bis_entry->qos_preset)) <= 0)
+							if (spa_json_get_string(&it[1], bis_entry->qos_preset, sizeof(bis_entry->qos_preset)) <= 0)
 								goto parse_failed;
 							spa_log_debug(monitor->log, "bis_entry->qos_preset %s", bis_entry->qos_preset);
 						} else if (spa_streq(bis_key, "audio_channel_allocation")) {
-							if (spa_json_get_int(&it[2], &bis_entry->channel_allocation) <= 0)
+							if (spa_json_get_int(&it[1], &bis_entry->channel_allocation) <= 0)
 								goto parse_failed;
 							spa_log_debug(monitor->log, "bis_entry->channel_allocation %d", bis_entry->channel_allocation);
 						} else if (spa_streq(bis_key, "metadata")) {
-							if (spa_json_enter_array(&it[2], &it_array[2]) <= 0)
+							if (spa_json_enter_array(&it[1], &it_array[2]) <= 0)
 								goto parse_failed;
-							while (spa_json_enter_object(&it_array[2], &it[3]) > 0) {
+							while (spa_json_enter_object(&it_array[2], &it[2]) > 0) {
 								struct spa_bt_metadata *metadata_entry = calloc(1, sizeof(struct spa_bt_metadata));
 
 								if (!metadata_entry)
@@ -6216,13 +6418,13 @@ static void parse_broadcast_source_config(struct spa_bt_monitor *monitor, const
 
 								spa_list_append(&bis_entry->metadata_list, &metadata_entry->link);
 
-								while (spa_json_get_string(&it[3], qos_key, sizeof(qos_key)) > 0) {
+								while (spa_json_get_string(&it[2], qos_key, sizeof(qos_key)) > 0) {
 									if (spa_streq(qos_key, "type")) {
-										if (spa_json_get_int(&it[3], &metadata_entry->type) <= 0)
+										if (spa_json_get_int(&it[2], &metadata_entry->type) <= 0)
 											goto parse_failed;
 										spa_log_debug(monitor->log, "metadata_entry->type %d", metadata_entry->type);
 									} else if (spa_streq(qos_key, "value")) {
-										if (spa_json_enter_array(&it[3], &it_array[3]) <= 0)
+										if (spa_json_enter_array(&it[2], &it_array[3]) <= 0)
 											goto parse_failed;
 										for (cursor = 0; cursor < METADATA_MAX_LEN - 1; cursor++) {
 											int temp_val = 0;
@@ -6254,7 +6456,7 @@ errno_failed:
 
 parse_failed:
 	str = spa_dict_lookup(info, "bluez5.bcast_source.config");
-	if (spa_json_get_error(&it[0], str, &loc)) {
+	if (spa_json_get_error(&it_array[0], str, &loc)) {
 		spa_debug_log_error_location(monitor->log, SPA_LOG_LEVEL_WARN,
 			&loc, "malformed bluez5.bcast_source.config: %s", loc.reason);
 	} else {
@@ -6272,7 +6474,7 @@ static int parse_codec_array(struct spa_bt_monitor *this, const struct spa_dict
 	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;
+	struct spa_json it_array;
 	char codec_name[256];
 	size_t num_codecs;
 	int i;
@@ -6290,9 +6492,7 @@ static int parse_codec_array(struct spa_bt_monitor *this, const struct spa_dict
 	if (info == NULL || (str = spa_dict_lookup(info, "bluez5.codecs")) == NULL)
 		goto fallback;
 
-	spa_json_init(&it, str, strlen(str));
-
-	if (spa_json_enter_array(&it, &it_array) <= 0) {
+	if (spa_json_begin_array(&it_array, str, strlen(str)) <= 0) {
 		spa_log_error(this->log, "property bluez5.codecs '%s' is not an array", str);
 		goto fallback;
 	}
diff --git a/spa/plugins/bluez5/bluez5-device.c b/spa/plugins/bluez5/bluez5-device.c
index 009d1586..2095ba82 100644
--- a/spa/plugins/bluez5/bluez5-device.c
+++ b/spa/plugins/bluez5/bluez5-device.c
@@ -59,9 +59,19 @@ enum {
 	DEVICE_PROFILE_A2DP = 2,
 	DEVICE_PROFILE_HSP_HFP = 3,
 	DEVICE_PROFILE_BAP = 4,
+	DEVICE_PROFILE_ASHA = 5,
 	DEVICE_PROFILE_LAST,
 };
 
+enum {
+	ROUTE_INPUT = 0,
+	ROUTE_OUTPUT,
+	ROUTE_HF_OUTPUT,
+	ROUTE_SET_INPUT,
+	ROUTE_SET_OUTPUT,
+	ROUTE_LAST,
+};
+
 struct props {
 	enum spa_bluetooth_audio_codec codec;
 	bool offload_active;
@@ -112,6 +122,8 @@ struct device_set_member {
 struct device_set {
 	struct impl *impl;
 	char *path;
+	bool sink_enabled;
+	bool source_enabled;
 	bool leader;
 	uint32_t sinks;
 	uint32_t sources;
@@ -502,12 +514,7 @@ static void get_channels(struct spa_bt_transport *t, bool a2dp_duplex, uint32_t
 
 static const char *get_channel_name(uint32_t channel)
 {
-        int i;
-        for (i = 0; spa_type_audio_channel[i].name; i++) {
-		if (spa_type_audio_channel[i].type == channel)
-			return spa_debug_type_short_name(spa_type_audio_channel[i].name);
-        }
-        return NULL;
+	return spa_type_to_short_name(channel, spa_type_audio_channel, NULL);
 }
 
 static int channel_position_cmp(const void *pa, const void *pb)
@@ -521,7 +528,7 @@ static void emit_device_set_node(struct impl *this, uint32_t id)
 	struct spa_bt_device *device = this->bt_dev;
 	struct node *node = &this->nodes[id];
 	struct spa_device_object_info info;
-	struct spa_dict_item items[7];
+	struct spa_dict_item items[8];
 	char str_id[32], members_json[8192], channels_json[512];
 	struct device_set_member *members;
 	uint32_t n_members;
@@ -534,6 +541,7 @@ static void emit_device_set_node(struct impl *this, uint32_t id)
 	items[n_items++] = SPA_DICT_ITEM_INIT("api.bluez5.set.leader", "true");
 	snprintf(str_id, sizeof(str_id), "%d", id);
 	items[n_items++] = SPA_DICT_ITEM_INIT("card.profile.device", str_id);
+	items[n_items++] = SPA_DICT_ITEM_INIT("device.routes", "1");
 
 	if (id == DEVICE_ID_SOURCE_SET) {
 		items[n_items++] = SPA_DICT_ITEM_INIT("media.class", "Audio/Source");
@@ -646,9 +654,9 @@ static void emit_node(struct impl *this, struct spa_bt_transport *t,
 	spa_log_debug(this->log, "%p: node, transport:%p id:%08x factory:%s", this, t, id, factory_name);
 
 	if (id & SINK_ID_FLAG)
-		in_device_set = this->device_set.path && (this->device_set.sinks > 1);
+		in_device_set = this->device_set.sink_enabled;
 	else
-		in_device_set = this->device_set.path && (this->device_set.sources > 1);
+		in_device_set = this->device_set.source_enabled;
 
 	snprintf(transport, sizeof(transport), "pointer:%p", t);
 	items[0] = SPA_DICT_ITEM_INIT(SPA_KEY_API_BLUEZ5_TRANSPORT, transport);
@@ -694,7 +702,7 @@ static void emit_node(struct impl *this, struct spa_bt_transport *t,
 
 	spa_device_emit_object_info(&this->hooks, id, &info);
 
-	if (this->device_set.path) {
+	if (in_device_set) {
 		/* Device set member nodes don't have their own routes */
 		this->nodes[id].impl = this;
 		this->nodes[id].active = false;
@@ -917,9 +925,8 @@ static void remove_dynamic_node(struct dynamic_node *this)
 	this->factory_name = NULL;
 }
 
-static void device_set_clear(struct impl *impl)
+static void device_set_clear(struct impl *impl, struct device_set *set)
 {
-	struct device_set *set = &impl->device_set;
 	unsigned int i;
 
 	for (i = 0; i < SPA_N_ELEMENTS(set->sink); ++i)
@@ -953,10 +960,9 @@ static const struct spa_bt_transport_events device_set_transport_events = {
 	.destroy = device_set_transport_destroy,
 };
 
-static void device_set_update(struct impl *this)
+static void device_set_update(struct impl *this, struct device_set *dset)
 {
 	struct spa_bt_device *device = this->bt_dev;
-	struct device_set *dset = &this->device_set;
 	struct spa_bt_set_membership *set;
 	struct spa_bt_set_membership tmp_set = {
 		.device = device,
@@ -981,7 +987,7 @@ static void device_set_update(struct impl *this)
 		struct spa_bt_set_membership *s;
 		int num_devices = 0;
 
-		device_set_clear(this);
+		device_set_clear(this, dset);
 
 		spa_bt_for_each_set_member(s, set) {
 			struct spa_bt_transport *t;
@@ -1049,6 +1055,26 @@ static void device_set_update(struct impl *this)
 		if (num_devices > 1)
 			break;
 	}
+
+	dset->sink_enabled = dset->path && (dset->sinks > 1);
+	dset->source_enabled = dset->path && (dset->sources > 1);
+}
+
+static bool device_set_equal(struct device_set *a, struct device_set *b)
+{
+	unsigned int i;
+
+	if (!spa_streq(a->path, b->path) || a->sink_enabled != b->sink_enabled ||
+			a->source_enabled != b->source_enabled || a->leader != b->leader ||
+			a->sinks != b->sinks || a->sources != b->sources)
+		return false;
+	for (i = 0; i < a->sinks; ++i)
+		if (a->sink[i].transport != b->sink[i].transport)
+			return false;
+	for (i = 0; i < a->sources; ++i)
+		if (a->source[i].transport != b->source[i].transport)
+			return false;
+	return true;
 }
 
 static int emit_nodes(struct impl *this)
@@ -1057,7 +1083,7 @@ static int emit_nodes(struct impl *this)
 
 	this->props.codec = 0;
 
-	device_set_update(this);
+	device_set_update(this, &this->device_set);
 
 	switch (this->profile) {
 	case DEVICE_PROFILE_OFF:
@@ -1084,6 +1110,17 @@ static int emit_nodes(struct impl *this)
 			}
 		}
 		break;
+	case DEVICE_PROFILE_ASHA:
+		if (this->bt_dev->connected_profiles & SPA_BT_PROFILE_ASHA_SINK) {
+			t = find_transport(this, SPA_BT_PROFILE_ASHA_SINK);
+			if (t) {
+				this->props.codec = t->media_codec->id;
+				emit_node(this, t, DEVICE_ID_SINK, SPA_NAME_API_BLUEZ5_MEDIA_SINK, false);
+			} else {
+				spa_log_warn(this->log, "Unable to find transport for ASHA");
+			}
+		}
+		break;
 	case DEVICE_PROFILE_A2DP:
 		if (this->bt_dev->connected_profiles & SPA_BT_PROFILE_A2DP_SOURCE) {
 			t = find_transport(this, SPA_BT_PROFILE_A2DP_SOURCE);
@@ -1139,7 +1176,7 @@ static int emit_nodes(struct impl *this)
 				emit_dynamic_node(this, t, id, SPA_NAME_API_BLUEZ5_MEDIA_SOURCE, false);
 		}
 
-		if (set->path && set->leader && set->sources > 1)
+		if (set->source_enabled && set->leader)
 			emit_device_set_node(this, DEVICE_ID_SOURCE_SET);
 
 		for (i = 0; i < set->sinks; ++i) {
@@ -1158,7 +1195,7 @@ static int emit_nodes(struct impl *this)
 				emit_dynamic_node(this, t, id, SPA_NAME_API_BLUEZ5_MEDIA_SINK, false);
 		}
 
-		if (set->path && set->leader && set->sinks > 1)
+		if (set->sink_enabled && set->leader)
 			emit_device_set_node(this, DEVICE_ID_SINK_SET);
 
 		if (this->bt_dev->connected_profiles & (SPA_BT_PROFILE_BAP_BROADCAST_SINK)) {
@@ -1167,9 +1204,6 @@ static int emit_nodes(struct impl *this)
 				this->props.codec = t->media_codec->id;
 				emit_node(this, t, DEVICE_ID_SINK, SPA_NAME_API_BLUEZ5_MEDIA_SINK, false);
 			}
-
-			if (this->device_set.leader && this->device_set.sinks > 0)
-				emit_device_set_node(this, DEVICE_ID_SINK_SET);
 		}
 
 		if (this->bt_dev->connected_profiles & (SPA_BT_PROFILE_BAP_BROADCAST_SOURCE)) {
@@ -1263,8 +1297,9 @@ static int set_profile(struct impl *this, uint32_t profile, enum spa_bluetooth_a
 	this->save_profile = save;
 
 	if (this->profile == profile &&
+	    (this->profile != DEVICE_PROFILE_ASHA || codec == this->props.codec) &&
 	    (this->profile != DEVICE_PROFILE_A2DP || codec == this->props.codec) &&
-	    (this->profile != DEVICE_PROFILE_BAP || codec == this->props.codec || this->device_set.path) &&
+	    (this->profile != DEVICE_PROFILE_BAP || codec == this->props.codec) &&
 	    (this->profile != DEVICE_PROFILE_HSP_HFP || codec == this->props.codec))
 		return 0;
 
@@ -1355,20 +1390,29 @@ static void codec_switched(void *userdata, int status)
 	emit_info(this, false);
 }
 
-static void profiles_changed(void *userdata, uint32_t prev_profiles, uint32_t prev_connected_profiles)
+static bool device_set_needs_update(struct impl *this)
+{
+	struct device_set dset = { .impl = this };
+	bool changed;
+
+	if (this->profile != DEVICE_PROFILE_BAP)
+		return false;
+
+	device_set_update(this, &dset);
+	changed = !device_set_equal(&dset, &this->device_set);
+	device_set_clear(this, &dset);
+	return changed;
+}
+
+static void profiles_changed(void *userdata, uint32_t connected_change)
 {
 	struct impl *this = userdata;
-	uint32_t connected_change;
 	bool nodes_changed = false;
 
-	connected_change = (this->bt_dev->connected_profiles ^ prev_connected_profiles);
-
 	/* Profiles changed. We have to re-emit device information. */
-	spa_log_info(this->log, "profiles changed to  %08x %08x (prev %08x %08x, change %08x)"
-		     " switching_codec:%d",
-		     this->bt_dev->profiles, this->bt_dev->connected_profiles,
-		     prev_profiles, prev_connected_profiles, connected_change,
-		     this->switching_codec);
+	spa_log_info(this->log, "profiles changed to %08x %08x (change %08x) switching_codec:%d",
+			this->bt_dev->profiles, this->bt_dev->connected_profiles,
+			connected_change, this->switching_codec);
 
 	if (this->switching_codec)
 		return;
@@ -1384,15 +1428,26 @@ 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_MEDIA_SOURCE));
+						     SPA_BT_PROFILE_A2DP_SOURCE));
 		spa_log_debug(this->log, "profiles changed: AG nodes changed: %d",
 			      nodes_changed);
 		break;
+	case DEVICE_PROFILE_ASHA:
+		nodes_changed = (connected_change & SPA_BT_PROFILE_ASHA_SINK);
+		spa_log_debug(this->log, "profiles changed: ASHA nodes changed: %d",
+			      nodes_changed);
+		break;
 	case DEVICE_PROFILE_A2DP:
+		nodes_changed = (connected_change & SPA_BT_PROFILE_A2DP_DUPLEX);
+		spa_log_debug(this->log, "profiles changed: A2DP nodes changed: %d",
+			      nodes_changed);
+		break;
 	case DEVICE_PROFILE_BAP:
-		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 = ((connected_change & SPA_BT_PROFILE_BAP_DUPLEX)
+					&& device_set_needs_update(this))
+				|| (connected_change & (SPA_BT_PROFILE_BAP_BROADCAST_SINK |
+							SPA_BT_PROFILE_BAP_BROADCAST_SOURCE));
+		spa_log_debug(this->log, "profiles changed: BAP nodes changed: %d",
 			      nodes_changed);
 		break;
 	case DEVICE_PROFILE_HSP_HFP:
@@ -1424,6 +1479,11 @@ static void device_set_changed(void *userdata)
 	if (this->profile != DEVICE_PROFILE_BAP)
 		return;
 
+	if (!device_set_needs_update(this)) {
+		spa_log_debug(this->log, "%p: device set not changed", this);
+		return;
+	}
+
 	spa_log_debug(this->log, "%p: device set changed", this);
 
 	emit_remove_nodes(this);
@@ -1545,6 +1605,10 @@ static uint32_t profile_direction_mask(struct impl *this, uint32_t index, enum s
 		if (device->connected_profiles & SPA_BT_PROFILE_HEADSET_HEAD_UNIT)
 			have_output = have_input = true;
 		break;
+	case DEVICE_PROFILE_ASHA:
+		if (device->connected_profiles & SPA_BT_PROFILE_ASHA_SINK)
+			have_input = true;
+		break;
 	default:
 		break;
 	}
@@ -1559,33 +1623,37 @@ static uint32_t profile_direction_mask(struct impl *this, uint32_t index, enum s
 
 static uint32_t get_profile_from_index(struct impl *this, uint32_t index, uint32_t *next, enum spa_bluetooth_audio_codec *codec)
 {
-	if (index < DEVICE_PROFILE_LAST) {
-		*codec = 0;
-		*next = index + 1;
-		return index;
-	} else if (index != SPA_ID_INVALID) {
-		const struct spa_type_info *info;
-		uint32_t profile;
+	uint32_t profile = (index >> 16);
+	const struct spa_type_info *info;
 
-		*codec = index - DEVICE_PROFILE_LAST;
-		*next = SPA_ID_INVALID;
+	switch (profile) {
+	case DEVICE_PROFILE_OFF:
+	case DEVICE_PROFILE_AG:
+		*codec = 0;
+		*next = (profile + 1) << 16;
+		return profile;
+	case DEVICE_PROFILE_ASHA:
+		*codec = SPA_BLUETOOTH_AUDIO_CODEC_G722;
+		*next = (profile + 1) << 16;
+		return profile;
+	case DEVICE_PROFILE_A2DP:
+	case DEVICE_PROFILE_HSP_HFP:
+	case DEVICE_PROFILE_BAP:
+		*codec = (index & 0xffff);
+		*next = (profile + 1) << 16;
 
 		for (info = spa_type_bluetooth_audio_codec; info->type; ++info)
 			if (info->type > *codec)
-				*next = SPA_MIN(info->type + DEVICE_PROFILE_LAST, *next);
-
-		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;
-
+				*next = SPA_MIN(*next, (profile << 16) | (info->type & 0xffff));
 		return profile;
+	default:
+		*codec = 0;
+		*next = SPA_ID_INVALID;
+		profile = SPA_ID_INVALID;
+		break;
 	}
 
-	*next = SPA_ID_INVALID;
-	return SPA_ID_INVALID;
+	return profile;
 }
 
 static uint32_t get_index_from_profile(struct impl *this, uint32_t profile, enum spa_bluetooth_audio_codec codec)
@@ -1593,19 +1661,41 @@ static uint32_t get_index_from_profile(struct impl *this, uint32_t profile, enum
 	switch (profile) {
 	case DEVICE_PROFILE_OFF:
 	case DEVICE_PROFILE_AG:
-		return profile;
+		return (profile << 16);
+
+	case DEVICE_PROFILE_ASHA:
+		return (profile << 16) | (SPA_BLUETOOTH_AUDIO_CODEC_G722 & 0xffff);
 
 	case DEVICE_PROFILE_A2DP:
 	case DEVICE_PROFILE_BAP:
 	case DEVICE_PROFILE_HSP_HFP:
 		if (!codec)
 			return SPA_ID_INVALID;
-		return codec + DEVICE_PROFILE_LAST;
+		return (profile << 16) | (codec & 0xffff);
 	}
 
 	return SPA_ID_INVALID;
 }
 
+static bool set_initial_asha_profile(struct impl *this)
+{
+	struct spa_bt_transport *t;
+	if (!(this->bt_dev->connected_profiles & SPA_BT_PROFILE_ASHA_SINK))
+		return false;
+
+	t = find_transport(this, SPA_BT_PROFILE_ASHA_SINK);
+	if (t) {
+		this->profile = DEVICE_PROFILE_ASHA;
+		this->props.codec = SPA_BLUETOOTH_AUDIO_CODEC_G722;
+
+		spa_log_debug(this->log, "initial ASHA profile:%d codec:%d",
+				this->profile, this->props.codec);
+		return true;
+	}
+
+	return false;
+}
+
 static bool set_initial_hsp_hfp_profile(struct impl *this)
 {
 	struct spa_bt_transport *t;
@@ -1647,13 +1737,16 @@ static void set_initial_profile(struct impl *this)
 	/* If default profile is set to HSP/HFP, first try those and exit if found. */
 	if (this->bt_dev->settings != NULL) {
 		const char *str = spa_dict_lookup(this->bt_dev->settings, "bluez5.profile");
+
+		if (spa_streq(str, "asha-sink") && set_initial_asha_profile(this))
+			return;
 		if (spa_streq(str, "off"))
 			goto off;
 		if (spa_streq(str, "headset-head-unit") && set_initial_hsp_hfp_profile(this))
 			return;
 	}
 
-	for (i = SPA_BT_PROFILE_BAP_SINK; i <= SPA_BT_PROFILE_A2DP_SOURCE; i <<= 1) {
+	for (i = SPA_BT_PROFILE_BAP_SINK; i <= SPA_BT_PROFILE_ASHA_SINK; i <<= 1) {
 		if (!(this->bt_dev->connected_profiles & i))
 			continue;
 
@@ -1663,6 +1756,8 @@ static void set_initial_profile(struct impl *this)
 				this->profile =  DEVICE_PROFILE_AG;
 			else if (i == SPA_BT_PROFILE_BAP_SINK)
 				this->profile =  DEVICE_PROFILE_BAP;
+			else if (i == SPA_BT_PROFILE_ASHA_SINK)
+				this->profile =  DEVICE_PROFILE_ASHA;
 			else
 				this->profile =  DEVICE_PROFILE_A2DP;
 			this->props.codec = t->media_codec->id;
@@ -1724,6 +1819,26 @@ static struct spa_pod *build_profile(struct impl *this, struct spa_pod_builder *
 			priority = 256;
 		break;
 	}
+	case DEVICE_PROFILE_ASHA:
+	{
+		uint32_t profile = device->connected_profiles & SPA_BT_PROFILE_ASHA_SINK;
+
+		if (codec == 0)
+			return NULL;
+		if (profile == 0)
+			return NULL;
+		if (!(profile & SPA_BT_PROFILE_ASHA_SINK)) {
+			return NULL;
+		}
+
+		name = spa_bt_profile_name(profile);
+		desc = _("Audio Streaming for Hearing Aids (ASHA Sink)");
+
+		n_sink++;
+		priority = 1;
+
+		break;
+	}
 	case DEVICE_PROFILE_A2DP:
 	{
 		/* make this device profile visible only if there is an A2DP sink */
@@ -1850,13 +1965,10 @@ static struct spa_pod *build_profile(struct impl *this, struct spa_pod_builder *
 			priority = 128;
 		}
 
-		if (this->device_set.leader) {
-			n_sink = this->device_set.sinks ? 1 : 0;
-			n_source = this->device_set.sinks ? 1 : 0;
-		} else if (this->device_set.path) {
-			n_sink = 0;
-			n_source = 0;
-		}
+		if (this->device_set.sink_enabled)
+			n_sink = this->device_set.leader ? 1 : 0;
+		if (this->device_set.source_enabled)
+			n_source = this->device_set.leader ? 1 : 0;
 		break;
 	}
 	case DEVICE_PROFILE_HSP_HFP:
@@ -1955,8 +2067,47 @@ static bool validate_profile(struct impl *this, uint32_t profile,
 	return (build_profile(this, &b, 0, 0, profile, codec, false) != NULL);
 }
 
+static bool profile_has_route(uint32_t profile, uint32_t route)
+{
+	switch (profile) {
+	case DEVICE_PROFILE_OFF:
+	case DEVICE_PROFILE_AG:
+		break;
+	case DEVICE_PROFILE_A2DP:
+		switch (route) {
+		case ROUTE_INPUT:
+		case ROUTE_OUTPUT:
+			return true;
+		}
+		break;
+	case DEVICE_PROFILE_HSP_HFP:
+		switch (route) {
+		case ROUTE_INPUT:
+		case ROUTE_HF_OUTPUT:
+			return true;
+		}
+		break;
+	case DEVICE_PROFILE_BAP:
+		switch (route) {
+		case ROUTE_INPUT:
+		case ROUTE_OUTPUT:
+		case ROUTE_SET_INPUT:
+		case ROUTE_SET_OUTPUT:
+			return true;
+		}
+		break;
+	case DEVICE_PROFILE_ASHA:
+		switch (route) {
+		case ROUTE_OUTPUT:
+			return true;
+		}
+		break;
+	}
+	return false;
+}
+
 static struct spa_pod *build_route(struct impl *this, struct spa_pod_builder *b,
-		uint32_t id, uint32_t port, uint32_t profile)
+		uint32_t id, uint32_t route, uint32_t profile)
 {
 	struct spa_bt_device *device = this->bt_dev;
 	struct spa_pod_frame f[2];
@@ -1967,7 +2118,7 @@ static struct spa_pod *build_route(struct impl *this, struct spa_pod_builder *b,
 	enum spa_param_availability available;
 	char name[128];
 	uint32_t i, j, mask, next;
-	uint32_t dev = SPA_ID_INVALID, enum_dev;
+	uint32_t dev;
 
 	ff = spa_bt_form_factor_from_class(device->bluetooth_class);
 
@@ -2035,86 +2186,60 @@ static struct spa_pod *build_route(struct impl *this, struct spa_pod_builder *b,
 		break;
 	}
 
-	switch (port) {
-	case 0:
+	switch (route) {
+	case ROUTE_INPUT:
 		direction = SPA_DIRECTION_INPUT;
 		snprintf(name, sizeof(name), "%s-input", name_prefix);
-		enum_dev = DEVICE_ID_SOURCE;
+		dev = DEVICE_ID_SOURCE;
+		available = this->device_set.source_enabled ?
+			SPA_PARAM_AVAILABILITY_no : SPA_PARAM_AVAILABILITY_yes;
 
 		if ((this->bt_dev->connected_profiles & SPA_BT_PROFILE_A2DP_SINK) &&
 				!(this->bt_dev->connected_profiles & SPA_BT_PROFILE_A2DP_SOURCE) &&
 				!(this->bt_dev->connected_profiles & SPA_BT_PROFILE_BAP_AUDIO) &&
 				(this->bt_dev->connected_profiles & SPA_BT_PROFILE_HEADSET_HEAD_UNIT))
 			description = hfp_description;
-
-		if (profile == DEVICE_PROFILE_A2DP || profile == DEVICE_PROFILE_BAP ||
-				profile == DEVICE_PROFILE_HSP_HFP)
-			dev = enum_dev;
-		else if (profile != SPA_ID_INVALID)
-			enum_dev = SPA_ID_INVALID;
 		break;
-	case 1:
+	case ROUTE_OUTPUT:
 		direction = SPA_DIRECTION_OUTPUT;
 		snprintf(name, sizeof(name), "%s-output", name_prefix);
-		enum_dev = DEVICE_ID_SINK;
-		if (profile == DEVICE_PROFILE_A2DP || profile == DEVICE_PROFILE_BAP)
-			dev = enum_dev;
-		else if (profile != SPA_ID_INVALID)
-			enum_dev = SPA_ID_INVALID;
+		dev = DEVICE_ID_SINK;
+		available = this->device_set.sink_enabled ?
+			SPA_PARAM_AVAILABILITY_no : SPA_PARAM_AVAILABILITY_yes;
 		break;
-	case 2:
+	case ROUTE_HF_OUTPUT:
 		direction = SPA_DIRECTION_OUTPUT;
 		snprintf(name, sizeof(name), "%s-hf-output", name_prefix);
 		description = hfp_description;
-		enum_dev = DEVICE_ID_SINK;
-		if (profile == DEVICE_PROFILE_HSP_HFP)
-			dev = enum_dev;
-		else if (profile != SPA_ID_INVALID)
-			enum_dev = SPA_ID_INVALID;
+		dev = DEVICE_ID_SINK;
+		available = SPA_PARAM_AVAILABILITY_yes;
 		break;
-	case 3:
-		if (!this->device_set.leader) {
-			errno = EINVAL;
+	case ROUTE_SET_INPUT:
+		if (!(this->device_set.source_enabled && this->device_set.leader))
 			return NULL;
-		}
 		direction = SPA_DIRECTION_INPUT;
 		snprintf(name, sizeof(name), "%s-set-input", name_prefix);
-		enum_dev = DEVICE_ID_SOURCE_SET;
-		if (profile == DEVICE_PROFILE_BAP)
-			dev = enum_dev;
-		else if (profile != SPA_ID_INVALID)
-			enum_dev = SPA_ID_INVALID;
+		dev = DEVICE_ID_SOURCE_SET;
+		available = SPA_PARAM_AVAILABILITY_yes;
 		break;
-	case 4:
-		if (!this->device_set.leader) {
-			errno = EINVAL;
+	case ROUTE_SET_OUTPUT:
+		if (!(this->device_set.sink_enabled && this->device_set.leader))
 			return NULL;
-		}
 		direction = SPA_DIRECTION_OUTPUT;
 		snprintf(name, sizeof(name), "%s-set-output", name_prefix);
-		enum_dev = DEVICE_ID_SINK_SET;
-		if (profile == DEVICE_PROFILE_BAP)
-			dev = enum_dev;
-		else if (profile != SPA_ID_INVALID)
-			enum_dev = SPA_ID_INVALID;
+		dev = DEVICE_ID_SINK_SET;
+		available = SPA_PARAM_AVAILABILITY_yes;
 		break;
 	default:
-		errno = EINVAL;
 		return NULL;
 	}
 
-	if (enum_dev == SPA_ID_INVALID) {
-		errno = EINVAL;
+	if (profile != SPA_ID_INVALID && !profile_has_route(profile, route))
 		return NULL;
-	}
-
-	available = SPA_PARAM_AVAILABILITY_yes;
-	if (this->device_set.path && !(port == 4 || port == 5))
-		available = SPA_PARAM_AVAILABILITY_no;
 
 	spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_ParamRoute, id);
 	spa_pod_builder_add(b,
-		SPA_PARAM_ROUTE_index, SPA_POD_Int(port),
+		SPA_PARAM_ROUTE_index, SPA_POD_Int(route),
 		SPA_PARAM_ROUTE_direction,  SPA_POD_Id(direction),
 		SPA_PARAM_ROUTE_name,  SPA_POD_String(name),
 		SPA_PARAM_ROUTE_description,  SPA_POD_String(description),
@@ -2133,14 +2258,10 @@ static struct spa_pod *build_route(struct impl *this, struct spa_pod_builder *b,
 	spa_pod_builder_push_array(b, &f[1]);
 
 	mask = 0;
-	for (i = 1; (j = get_profile_from_index(this, i, &next, &codec)) != SPA_ID_INVALID; i = next) {
+	for (i = 0; (j = get_profile_from_index(this, i, &next, &codec)) != SPA_ID_INVALID; i = next) {
 		uint32_t profile_mask;
 
-		if (j == DEVICE_PROFILE_A2DP && !(port == 0 || port == 1))
-			continue;
-		if (j == DEVICE_PROFILE_BAP && !(port == 0 || port == 1 || port == 3 || port == 4))
-			continue;
-		if (j == DEVICE_PROFILE_HSP_HFP && !(port == 0 || port == 2))
+		if (!profile_has_route(j, route))
 			continue;
 
 		profile_mask = profile_direction_mask(this, j, codec, false);
@@ -2161,7 +2282,7 @@ static struct spa_pod *build_route(struct impl *this, struct spa_pod_builder *b,
 		return NULL;
 	}
 
-	if (dev != SPA_ID_INVALID) {
+	if (profile != SPA_ID_INVALID) {
 		struct node *node = &this->nodes[dev];
 		struct spa_bt_transport_volume *t_volume;
 
@@ -2206,17 +2327,16 @@ static struct spa_pod *build_route(struct impl *this, struct spa_pod_builder *b,
 
 		spa_pod_builder_prop(b, SPA_PARAM_ROUTE_save, 0);
 		spa_pod_builder_bool(b, node->save);
+
+		spa_pod_builder_prop(b, SPA_PARAM_ROUTE_profile, 0);
+		spa_pod_builder_int(b, profile);
 	}
 
 	spa_pod_builder_prop(b, SPA_PARAM_ROUTE_devices, 0);
 	spa_pod_builder_push_array(b, &f[1]);
-	spa_pod_builder_int(b, enum_dev);
+	spa_pod_builder_int(b, dev);
 	spa_pod_builder_pop(b, &f[1]);
 
-	if (profile != SPA_ID_INVALID) {
-		spa_pod_builder_prop(b, SPA_PARAM_ROUTE_profile, 0);
-		spa_pod_builder_int(b, profile);
-	}
 	return spa_pod_builder_pop(b, &f[0]);
 }
 
@@ -2288,7 +2408,7 @@ static struct spa_pod *build_prop_info_codec(struct impl *this, struct spa_pod_b
 	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 || this->profile == DEVICE_PROFILE_BAP) {
+	if (this->profile == DEVICE_PROFILE_A2DP || this->profile == DEVICE_PROFILE_BAP || this->profile == DEVICE_PROFILE_ASHA) {
 		FOR_EACH_MEDIA_CODEC(j, codec) {
 			spa_pod_builder_int(b, codec->id);
 			spa_pod_builder_string(b, codec->description);
@@ -2344,20 +2464,12 @@ static int impl_enum_params(void *object, int seq,
 		enum spa_bluetooth_audio_codec codec;
 
 		profile = get_profile_from_index(this, result.index, &result.next, &codec);
-
-		switch (profile) {
-		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)
-				goto next;
-			break;
-		default:
+		if (profile == SPA_ID_INVALID)
 			return 0;
-		}
+
+		param = build_profile(this, &b, id, result.index, profile, codec, false);
+		if (param == NULL)
+			goto next;
 		break;
 	}
 	case SPA_PARAM_Profile:
@@ -2378,26 +2490,23 @@ static int impl_enum_params(void *object, int seq,
 	}
 	case SPA_PARAM_EnumRoute:
 	{
-		switch (result.index) {
-		case 0: case 1: case 2: case 3: case 4:
+		if (result.index < ROUTE_LAST) {
 			param = build_route(this, &b, id, result.index, SPA_ID_INVALID);
 			if (param == NULL)
 				goto next;
-			break;
-		default:
+		} else {
 			return 0;
 		}
 		break;
 	}
 	case SPA_PARAM_Route:
 	{
-		switch (result.index) {
-		case 0: case 1: case 2: case 3: case 4: case 5:
+		if (result.index < ROUTE_LAST) {
 			param = build_route(this, &b, id, result.index, this->profile);
 			if (param == NULL)
 				goto next;
 			break;
-		default:
+		} else {
 			return 0;
 		}
 		break;
@@ -2447,6 +2556,41 @@ static int impl_enum_params(void *object, int seq,
 	return 0;
 }
 
+static void device_set_update_volumes(struct node *node)
+{
+	struct impl *impl = node->impl;
+	struct device_set *dset = &impl->device_set;
+	float hw_volume = node_get_hw_volume(node);
+	bool sink = (node->id == DEVICE_ID_SINK_SET);
+	struct device_set_member *members = sink ? dset->sink : dset->source;
+	uint32_t n_members = sink ? dset->sinks : dset->sources;
+	uint32_t i;
+
+	/* Check if all sub-devices have HW volume */
+	if ((sink && !dset->sink_enabled) || (!sink && !dset->source_enabled))
+		goto soft_volume;
+
+	for (i = 0; i < n_members; ++i) {
+		struct spa_bt_transport *t = members[i].transport;
+		struct spa_bt_transport_volume *t_volume = t ? &t->volumes[members[i].id] : NULL;
+
+		if (!t_volume || !t_volume->active)
+			goto soft_volume;
+	}
+
+	node_update_soft_volumes(node, hw_volume);
+	for (i = 0; i < n_members; ++i)
+		spa_bt_transport_set_volume(members[i].transport, members[i].id, hw_volume);
+	return;
+
+soft_volume:
+	/* Soft volume fallback */
+	for (i = 0; i < n_members; ++i)
+		spa_bt_transport_set_volume(members[i].transport, members[i].id, 1.0f);
+	node_update_soft_volumes(node, 1.0f);
+	return;
+}
+
 static int node_set_volume(struct impl *this, struct node *node, float volumes[], uint32_t n_volumes)
 {
 	uint32_t i;
@@ -2474,6 +2618,8 @@ static int node_set_volume(struct impl *this, struct node *node, float volumes[]
 
 		node_update_soft_volumes(node, hw_volume);
 		spa_bt_transport_set_volume(node->transport, node->id, hw_volume);
+	} else if (node->id == DEVICE_ID_SOURCE_SET || node->id == DEVICE_ID_SINK_SET) {
+		device_set_update_volumes(node);
 	} else {
 		float boost = get_soft_volume_boost(node);
 		for (uint32_t i = 0; i < node->n_channels; ++i)
@@ -2718,7 +2864,7 @@ static int impl_set_param(void *object,
 		if (codec_id == SPA_ID_INVALID)
 			return 0;
 
-		if (this->profile == DEVICE_PROFILE_A2DP || this->profile == DEVICE_PROFILE_BAP) {
+		if (this->profile == DEVICE_PROFILE_A2DP || this->profile == DEVICE_PROFILE_BAP || this->profile == DEVICE_PROFILE_ASHA) {
 			size_t j;
 			for (j = 0; j < this->supported_codec_count; ++j) {
 				if (this->supported_codecs[j]->id == codec_id) {
@@ -2790,7 +2936,7 @@ static int impl_clear(struct spa_handle *handle)
 			free((void *)it->value);
 	}
 
-	device_set_clear(this);
+	device_set_clear(this, &this->device_set);
 	return 0;
 }
 
diff --git a/spa/plugins/bluez5/codec-loader.c b/spa/plugins/bluez5/codec-loader.c
index 3b908779..6fd1d043 100644
--- a/spa/plugins/bluez5/codec-loader.c
+++ b/spa/plugins/bluez5/codec-loader.c
@@ -51,6 +51,7 @@ static int codec_order(const struct media_codec *c)
 		SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_DUPLEX,
 		SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_PRO,
 		SPA_BLUETOOTH_AUDIO_CODEC_AAC_ELD,
+		SPA_BLUETOOTH_AUDIO_CODEC_G722,
 	};
 	size_t i;
 	for (i = 0; i < SPA_N_ELEMENTS(order); ++i)
@@ -172,7 +173,8 @@ const struct media_codec * const *load_media_codecs(struct spa_plugin_loader *lo
 		MEDIA_CODEC_FACTORY_LIB("lc3plus"),
 		MEDIA_CODEC_FACTORY_LIB("opus"),
 		MEDIA_CODEC_FACTORY_LIB("opus-g"),
-		MEDIA_CODEC_FACTORY_LIB("lc3")
+		MEDIA_CODEC_FACTORY_LIB("lc3"),
+		MEDIA_CODEC_FACTORY_LIB("g722")
 #undef MEDIA_CODEC_FACTORY_LIB
 	};
 
diff --git a/spa/plugins/bluez5/decode-buffer.h b/spa/plugins/bluez5/decode-buffer.h
index 2eab4645..0441f041 100644
--- a/spa/plugins/bluez5/decode-buffer.h
+++ b/spa/plugins/bluez5/decode-buffer.h
@@ -21,12 +21,8 @@
  * The regular timer cycle cannot be aligned with this, so process()
  * may occur at any time.
  *
- * The buffer level is the difference between the number of samples in
- * buffer immediately after receiving a packet, and the samples consumed
- * before receiving the next packet.
- *
- * The buffer level indicates how much any packet can be delayed without
- * underrun. If it is positive, there are no underruns.
+ * The buffer level is the position of last received sample, relative to the current
+ * playback position. If it is larger than duration, there is no underrun.
  *
  * The rate correction aims to maintain the average level at a safety margin.
  */
@@ -45,16 +41,6 @@
 #define BUFFERING_RATE_DIFF_MAX		0.005
 
 
-/**
- * Safety margin.
- *
- * The spike is the long-window maximum difference
- * between minimum and average buffer level.
- */
-#define BUFFERING_TARGET(spike,packet_size,max_buf)			\
-	SPA_CLAMP((spike)*3/2, (packet_size), (max_buf) - 2*(packet_size))
-
-
 struct spa_bt_decode_buffer
 {
 	struct spa_log *log;
@@ -74,20 +60,20 @@ struct spa_bt_decode_buffer
 	struct spa_bt_rate_control ctl;
 	double corr;
 
-	uint32_t prev_consumed;
-	uint32_t prev_avail;
-	uint32_t prev_duration;
-	uint32_t underrun;
+	uint32_t duration;
 	uint32_t pos;
 
 	int32_t target;		/**< target buffer (0: automatic) */
-	int32_t max_target;
+	int32_t max_extra;
+
+	int32_t level;
+	uint64_t next_nsec;
+	double rate_diff;
 
-	uint8_t received:1;
 	uint8_t buffering:1;
 };
 
-static int spa_bt_decode_buffer_init(struct spa_bt_decode_buffer *this, struct spa_log *log,
+static inline int spa_bt_decode_buffer_init(struct spa_bt_decode_buffer *this, struct spa_log *log,
 		uint32_t frame_size, uint32_t rate, uint32_t quantum_limit, uint32_t reserve)
 {
 	spa_zero(*this);
@@ -100,7 +86,7 @@ static int spa_bt_decode_buffer_init(struct spa_bt_decode_buffer *this, struct s
 	this->corr = 1.0;
 	this->target = 0;
 	this->buffering = true;
-	this->max_target = INT32_MAX;
+	this->max_extra = INT32_MAX;
 
 	spa_bt_rate_control_init(&this->ctl, 0);
 
@@ -114,13 +100,13 @@ static int spa_bt_decode_buffer_init(struct spa_bt_decode_buffer *this, struct s
 	return 0;
 }
 
-static void spa_bt_decode_buffer_clear(struct spa_bt_decode_buffer *this)
+static inline void spa_bt_decode_buffer_clear(struct spa_bt_decode_buffer *this)
 {
 	free(this->buffer_decoded);
 	spa_zero(*this);
 }
 
-static void spa_bt_decode_buffer_compact(struct spa_bt_decode_buffer *this)
+static inline void spa_bt_decode_buffer_compact(struct spa_bt_decode_buffer *this)
 {
 	uint32_t avail;
 
@@ -153,7 +139,23 @@ done:
 	spa_assert(this->buffer_size - this->write_index >= this->buffer_reserve);
 }
 
-static void *spa_bt_decode_buffer_get_write(struct spa_bt_decode_buffer *this, uint32_t *avail)
+static inline void *spa_bt_decode_buffer_get_read(struct spa_bt_decode_buffer *this, uint32_t *avail)
+{
+	spa_assert(this->write_index >= this->read_index);
+	if (!this->buffering)
+		*avail = this->write_index - this->read_index;
+	else
+		*avail = 0;
+	return SPA_PTROFF(this->buffer_decoded, this->read_index, void);
+}
+
+static inline void spa_bt_decode_buffer_read(struct spa_bt_decode_buffer *this, uint32_t size)
+{
+	spa_assert(size % this->frame_size == 0);
+	this->read_index += size;
+}
+
+static inline void *spa_bt_decode_buffer_get_write(struct spa_bt_decode_buffer *this, uint32_t *avail)
 {
 	spa_bt_decode_buffer_compact(this);
 	spa_assert(this->buffer_size >= this->write_index);
@@ -161,69 +163,85 @@ static void *spa_bt_decode_buffer_get_write(struct spa_bt_decode_buffer *this, u
 	return SPA_PTROFF(this->buffer_decoded, this->write_index, void);
 }
 
-static void spa_bt_decode_buffer_write_packet(struct spa_bt_decode_buffer *this, uint32_t size)
+static inline void spa_bt_decode_buffer_write_packet(struct spa_bt_decode_buffer *this, uint32_t size, uint64_t nsec)
 {
+	int32_t remain;
+	uint32_t avail;
+
 	spa_assert(size % this->frame_size == 0);
 	this->write_index += size;
-	this->received = true;
 	spa_bt_ptp_update(&this->packet_size, size / this->frame_size, size / this->frame_size);
-}
 
-static void *spa_bt_decode_buffer_get_read(struct spa_bt_decode_buffer *this, uint32_t *avail)
-{
-	spa_assert(this->write_index >= this->read_index);
-	if (!this->buffering)
-		*avail = this->write_index - this->read_index;
-	else
-		*avail = 0;
-	return SPA_PTROFF(this->buffer_decoded, this->read_index, void);
-}
+	if (nsec && this->next_nsec && this->rate_diff != 0.0) {
+		int64_t dt = (this->next_nsec >= nsec) ?
+			(int64_t)(this->next_nsec - nsec) : -(int64_t)(nsec - this->next_nsec);
+		remain = (int32_t)SPA_CLAMP(dt * this->rate_diff * this->rate / SPA_NSEC_PER_SEC,
+				-(int32_t)this->duration, this->duration);
+	} else {
+		remain = 0;
+	}
 
-static void spa_bt_decode_buffer_read(struct spa_bt_decode_buffer *this, uint32_t size)
-{
-	spa_assert(size % this->frame_size == 0);
-	this->read_index += size;
+	spa_bt_decode_buffer_get_read(this, &avail);
+	this->level = avail / this->frame_size + remain;
 }
 
-static void spa_bt_decode_buffer_recover(struct spa_bt_decode_buffer *this)
+static inline void spa_bt_decode_buffer_recover(struct spa_bt_decode_buffer *this)
 {
 	int32_t size = (this->write_index - this->read_index) / this->frame_size;
-	int32_t level;
-
-	this->prev_avail = size * this->frame_size;
-	this->prev_consumed = this->prev_duration;
 
-	level = (int32_t)this->prev_avail/this->frame_size
-		- (int32_t)this->prev_duration;
+	this->level = size;
 	this->corr = 1.0;
-
-	spa_bt_rate_control_init(&this->ctl, level);
+	spa_bt_rate_control_init(&this->ctl, size);
 }
 
-static SPA_UNUSED
-void spa_bt_decode_buffer_set_target_latency(struct spa_bt_decode_buffer *this, int32_t samples)
+static inline void spa_bt_decode_buffer_set_target_latency(struct spa_bt_decode_buffer *this, int32_t samples)
 {
 	this->target = samples;
 }
 
-static SPA_UNUSED
-void spa_bt_decode_buffer_set_max_latency(struct spa_bt_decode_buffer *this, int32_t samples)
+static inline void spa_bt_decode_buffer_set_max_extra_latency(struct spa_bt_decode_buffer *this, int32_t samples)
 {
-	this->max_target = samples;
+	this->max_extra = samples;
 }
 
-static void spa_bt_decode_buffer_process(struct spa_bt_decode_buffer *this, uint32_t samples, uint32_t duration)
+static inline int32_t spa_bt_decode_buffer_get_target_latency(struct spa_bt_decode_buffer *this)
+{
+	const int32_t duration = this->duration;
+	const int32_t packet_size = SPA_CLAMP(this->packet_size.max, 0, INT32_MAX/8);
+	const int32_t max_buf = (this->buffer_size - this->buffer_reserve) / this->frame_size;
+	const int32_t spike = SPA_CLAMP(this->spike.max, 0, max_buf);
+	int32_t target;
+
+	if (this->target)
+		target = this->target;
+	else
+		target = SPA_CLAMP(SPA_ROUND_UP(SPA_MAX(spike * 3/2, duration), 
+						SPA_CLAMP((int)this->rate / 50, 1, INT32_MAX)),
+				duration, max_buf - 2*packet_size);
+
+	return SPA_MIN(target, duration + SPA_CLAMP(this->max_extra, 0, INT32_MAX - duration));
+}
+
+static inline void spa_bt_decode_buffer_process(struct spa_bt_decode_buffer *this, uint32_t samples, uint32_t duration,
+		double rate_diff, uint64_t next_nsec)
 {
 	const uint32_t data_size = samples * this->frame_size;
 	const int32_t packet_size = SPA_CLAMP(this->packet_size.max, 0, INT32_MAX/8);
 	const int32_t max_level = SPA_MAX(8 * packet_size, (int32_t)duration);
+	const uint32_t avg_period = (uint64_t)this->rate * BUFFERING_SHORT_MSEC / 1000;
+	int32_t target;
 	uint32_t avail;
 
-	if (SPA_UNLIKELY(duration != this->prev_duration)) {
-		this->prev_duration = duration;
+	this->rate_diff = rate_diff;
+	this->next_nsec = next_nsec;
+
+	if (SPA_UNLIKELY(duration != this->duration)) {
+		this->duration = duration;
 		spa_bt_decode_buffer_recover(this);
 	}
 
+	target = spa_bt_decode_buffer_get_target_latency(this);
+
 	if (SPA_UNLIKELY(this->buffering)) {
 		int32_t size = (this->write_index - this->read_index) / this->frame_size;
 
@@ -231,89 +249,63 @@ static void spa_bt_decode_buffer_process(struct spa_bt_decode_buffer *this, uint
 
 		spa_log_trace(this->log, "%p buffering size:%d", this, (int)size);
 
-		if (this->received &&
-				packet_size > 0 &&
-				size >= SPA_MAX(3*packet_size, (int32_t)duration))
+		if (size >= SPA_MAX((int)duration, target))
 			this->buffering = false;
 		else
 			return;
 
+		spa_bt_ptp_update(&this->spike, packet_size, duration);
 		spa_bt_decode_buffer_recover(this);
 	}
 
 	spa_bt_decode_buffer_get_read(this, &avail);
 
-	if (this->received) {
-		const uint32_t avg_period = (uint64_t)this->rate * BUFFERING_SHORT_MSEC / 1000;
-		const int32_t max_buf = (this->buffer_size - this->buffer_reserve) / this->frame_size;
-		int32_t level, target;
+	/* Track buffer level */
+	this->level = SPA_MAX(this->level, -max_level);
 
-		/* Track buffer level */
-		level = (int32_t)(this->prev_avail/this->frame_size) - (int32_t)this->prev_consumed;
-		level = SPA_MAX(level, -max_level);
-		this->prev_consumed = SPA_MIN(this->prev_consumed, avg_period);
+	spa_bt_ptp_update(&this->spike, (int32_t)this->ctl.avg - this->level, duration);
 
-		spa_bt_ptp_update(&this->spike, (int32_t)(this->ctl.avg - level), this->prev_consumed);
+	if (this->level > SPA_MAX(4 * target, 3*(int32_t)duration) &&
+			avail > data_size) {
+		/* Lagging too much: drop data */
+		uint32_t size = SPA_MIN(avail - data_size,
+				(this->level - target) * this->frame_size);
 
-		/* Update target level */
-		if (this->target)
-			target = this->target;
-		else
-			target = BUFFERING_TARGET(this->spike.max, packet_size, max_buf);
-
-		target = SPA_MIN(target, this->max_target);
-
-		if (level > SPA_MAX(4 * target, 2*(int32_t)duration) &&
-				avail > data_size) {
-			/* Lagging too much: drop data */
-			uint32_t size = SPA_MIN(avail - data_size,
-					(level - target) * this->frame_size);
-
-			spa_bt_decode_buffer_read(this, size);
-			spa_log_trace(this->log, "%p overrun samples:%d level:%d target:%d",
-					this, (int)size/this->frame_size,
-					(int)level, (int)target);
-
-			spa_bt_decode_buffer_recover(this);
-		}
-
-		this->pos += this->prev_consumed;
-		if (this->pos > this->rate) {
-			spa_log_debug(this->log,
-					"%p avg:%d target:%d level:%d buffer:%d spike:%d corr:%f",
-					this,
-					(int)this->ctl.avg,
-					(int)target,
-					(int)level,
-					(int)(avail / this->frame_size),
-					(int)this->spike.max,
-					(double)this->corr);
-			this->pos = 0;
-		}
-
-		this->corr = spa_bt_rate_control_update(&this->ctl,
-				level, target, this->prev_consumed, avg_period,
-				BUFFERING_RATE_DIFF_MAX);
-
-		spa_bt_decode_buffer_get_read(this, &avail);
-
-		this->prev_consumed = 0;
-		this->prev_avail = avail;
-		this->underrun = 0;
-		this->received = false;
+		spa_bt_decode_buffer_read(this, size);
+		spa_log_trace(this->log, "%p overrun samples:%d level:%d target:%d",
+				this, (int)size/this->frame_size,
+				(int)this->level, (int)target);
+
+		spa_bt_decode_buffer_recover(this);
+	}
+
+	this->pos += duration;
+	if (this->pos > this->rate) {
+		spa_log_debug(this->log,
+				"%p avg:%d target:%d level:%d buffer:%d spike:%d corr:%f",
+				this,
+				(int)this->ctl.avg,
+				(int)target,
+				(int)this->level,
+				(int)(avail / this->frame_size),
+				(int)this->spike.max,
+				(double)this->corr);
+		this->pos = 0;
 	}
 
+	this->corr = spa_bt_rate_control_update(&this->ctl,
+			this->level, target, duration, avg_period,
+			BUFFERING_RATE_DIFF_MAX);
+
+	this->level -= duration;
+
+	spa_bt_decode_buffer_get_read(this, &avail);
 	if (avail < data_size) {
 		spa_log_trace(this->log, "%p underrun samples:%d", this,
 				(data_size - avail) / this->frame_size);
-		this->underrun += samples;
-		if (this->underrun >= SPA_MIN((uint32_t)max_level, this->buffer_size / this->frame_size)) {
-			this->buffering = true;
-			spa_log_debug(this->log, "%p underrun too much: start buffering", this);
-		}
+		this->buffering = true;
+		spa_bt_ptp_update(&this->spike, (int32_t)this->ctl.avg - this->level, duration);
 	}
-
-	this->prev_consumed += samples;
 }
 
 #endif
diff --git a/spa/plugins/bluez5/defs.h b/spa/plugins/bluez5/defs.h
index ba9e54ac..23094580 100644
--- a/spa/plugins/bluez5/defs.h
+++ b/spa/plugins/bluez5/defs.h
@@ -127,6 +127,7 @@ extern "C" {
 #define SPA_BT_UUID_BAP_SOURCE  "00002bcb-0000-1000-8000-00805f9b34fb"
 #define SPA_BT_UUID_BAP_BROADCAST_SOURCE  "00001852-0000-1000-8000-00805f9b34fb"
 #define SPA_BT_UUID_BAP_BROADCAST_SINK    "00001851-0000-1000-8000-00805f9b34fb"
+#define SPA_BT_UUID_ASHA_SINK   "0000FDF0-0000-1000-8000-00805f9b34fb"
 
 #define PROFILE_HSP_AG	"/Profile/HSPAG"
 #define PROFILE_HSP_HS	"/Profile/HSPHS"
@@ -181,12 +182,13 @@ enum spa_bt_profile {
 	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_BAP_BROADCAST_SOURCE =	(1 << 8),
-	SPA_BT_PROFILE_BAP_BROADCAST_SINK   =	(1 << 9),
+	SPA_BT_PROFILE_ASHA_SINK =      (1 << 4),
+	SPA_BT_PROFILE_HSP_HS =		(1 << 5),
+	SPA_BT_PROFILE_HSP_AG =		(1 << 6),
+	SPA_BT_PROFILE_HFP_HF =		(1 << 7),
+	SPA_BT_PROFILE_HFP_AG =		(1 << 8),
+	SPA_BT_PROFILE_BAP_BROADCAST_SOURCE =	(1 << 9),
+	SPA_BT_PROFILE_BAP_BROADCAST_SINK   =	(1 << 10),
 
 	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),
@@ -226,6 +228,8 @@ static inline enum spa_bt_profile spa_bt_profile_from_uuid(const char *uuid)
 		return SPA_BT_PROFILE_BAP_BROADCAST_SOURCE;
 	else if (strcasecmp(uuid, SPA_BT_UUID_BAP_BROADCAST_SINK) == 0)
 		return SPA_BT_PROFILE_BAP_BROADCAST_SINK;
+	else if (strcasecmp(uuid, SPA_BT_UUID_ASHA_SINK) == 0)
+		return SPA_BT_PROFILE_ASHA_SINK;
 	else
 		return 0;
 }
@@ -252,13 +256,16 @@ enum spa_bt_hfp_ag_feature {
 };
 
 enum spa_bt_hfp_sdp_ag_features {
-	SPA_BT_HFP_SDP_AG_FEATURE_NONE =		(0),
-	SPA_BT_HFP_SDP_AG_FEATURE_3WAY =		(1 << 0),
-	SPA_BT_HFP_SDP_AG_FEATURE_ECNR =		(1 << 1),
-	SPA_BT_HFP_SDP_AG_FEATURE_VOICE_RECOG =		(1 << 2),
-	SPA_BT_HFP_SDP_AG_FEATURE_IN_BAND_RING_TONE =	(1 << 3),
-	SPA_BT_HFP_SDP_AG_FEATURE_ATTACH_VOICE_TAG =	(1 << 4),
-	SPA_BT_HFP_SDP_AG_FEATURE_WIDEBAND_SPEECH =	(1 << 5),
+	SPA_BT_HFP_SDP_AG_FEATURE_NONE =			(0),
+	SPA_BT_HFP_SDP_AG_FEATURE_3WAY =			(1 << 0),
+	SPA_BT_HFP_SDP_AG_FEATURE_ECNR =			(1 << 1),
+	SPA_BT_HFP_SDP_AG_FEATURE_VOICE_RECOG =			(1 << 2),
+	SPA_BT_HFP_SDP_AG_FEATURE_IN_BAND_RING_TONE =		(1 << 3),
+	SPA_BT_HFP_SDP_AG_FEATURE_ATTACH_VOICE_TAG =		(1 << 4),
+	SPA_BT_HFP_SDP_AG_FEATURE_WIDEBAND_SPEECH =		(1 << 5),
+	SPA_BT_HFP_SDP_AG_FEATURE_ENH_VOICE_RECOG_STATUS =	(1 << 6),
+	SPA_BT_HFP_SDP_AG_FEATURE_VOICE_RECOG_TEXT =		(1 << 7),
+	SPA_BT_HFP_SDP_AG_FEATURE_SUPER_WIDEBAND_SPEECH =	(1 << 8),
 };
 
 enum spa_bt_hfp_hf_feature {
@@ -299,17 +306,22 @@ enum spa_bt_hfp_hf_xapl_features {
 };
 
 enum spa_bt_hfp_sdp_hf_features {
-	SPA_BT_HFP_SDP_HF_FEATURE_NONE =		(0),
-	SPA_BT_HFP_SDP_HF_FEATURE_ECNR =		(1 << 0),
-	SPA_BT_HFP_SDP_HF_FEATURE_3WAY =		(1 << 1),
-	SPA_BT_HFP_SDP_HF_FEATURE_CLIP =		(1 << 2),
-	SPA_BT_HFP_SDP_HF_FEATURE_VOICE_RECOGNITION =	(1 << 3),
+	SPA_BT_HFP_SDP_HF_FEATURE_NONE =			(0),
+	SPA_BT_HFP_SDP_HF_FEATURE_ECNR =			(1 << 0),
+	SPA_BT_HFP_SDP_HF_FEATURE_3WAY =			(1 << 1),
+	SPA_BT_HFP_SDP_HF_FEATURE_CLIP =			(1 << 2),
+	SPA_BT_HFP_SDP_HF_FEATURE_VOICE_RECOGNITION =		(1 << 3),
 	SPA_BT_HFP_SDP_HF_FEATURE_REMOTE_VOLUME_CONTROL =	(1 << 4),
-	SPA_BT_HFP_SDP_HF_FEATURE_WIDEBAND_SPEECH =	(1 << 5),
+	SPA_BT_HFP_SDP_HF_FEATURE_WIDEBAND_SPEECH =		(1 << 5),
+	SPA_BT_HFP_SDP_HF_FEATURE_ENH_VOICE_RECOG_STATUS =	(1 << 6),
+	SPA_BT_HFP_SDP_HF_FEATURE_VOICE_RECOG_TEXT =		(1 << 7),
+	SPA_BT_HFP_SDP_HF_FEATURE_SUPER_WIDEBAND_SPEECH =	(1 << 8),
 };
 
 static inline const char *spa_bt_profile_name (enum spa_bt_profile profile) {
       switch (profile) {
+      case SPA_BT_PROFILE_ASHA_SINK:
+        return "asha-sink";
       case SPA_BT_PROFILE_A2DP_SOURCE:
         return "a2dp-source";
       case SPA_BT_PROFILE_A2DP_SINK:
@@ -462,7 +474,7 @@ struct spa_bt_device_events {
 	void (*codec_switched) (void *data, int status);
 
 	/** Profile configuration changed */
-	void (*profiles_changed) (void *data, uint32_t prev_profiles, uint32_t prev_connected);
+	void (*profiles_changed) (void *data, uint32_t connected_change);
 
 	/** Device set configuration changed */
 	void (*device_set_changed) (void *data);
@@ -584,6 +596,7 @@ int spa_bt_sco_io_write(struct spa_bt_sco_io *io, uint8_t *data, int size);
 #define SPA_BT_VOLUME_INVALID	-1
 #define SPA_BT_VOLUME_HS_MAX	15
 #define SPA_BT_VOLUME_A2DP_MAX	127
+#define SPA_BT_VOLUME_BAP_MAX	255
 
 enum spa_bt_transport_state {
         SPA_BT_TRANSPORT_STATE_ERROR = -1,
@@ -610,6 +623,7 @@ struct spa_bt_transport_implementation {
 	int (*acquire) (void *data, bool optional);
 	int (*release) (void *data);
 	int (*set_volume) (void *data, int id, float volume);
+	int (*set_delay) (void *data, int64_t delay_nsec);
 	int (*destroy) (void *data);
 };
 
@@ -715,12 +729,13 @@ int spa_bt_transport_ensure_sco_io(struct spa_bt_transport *t, struct spa_loop *
 
 #define spa_bt_transport_destroy(t)		spa_bt_transport_impl(t, destroy, 0)
 #define spa_bt_transport_set_volume(t,...)	spa_bt_transport_impl(t, set_volume, 0, __VA_ARGS__)
+#define spa_bt_transport_set_delay(t,...)	spa_bt_transport_impl(t, set_delay, 0, __VA_ARGS__)
 
 static inline enum spa_bt_transport_state spa_bt_transport_state_from_string(const char *value)
 {
 	if (strcasecmp("idle", value) == 0)
 		return SPA_BT_TRANSPORT_STATE_IDLE;
-	else if (strcasecmp("pending", value) == 0)
+	else if ((strcasecmp("pending", value) == 0) || (strcasecmp("broadcasting", value) == 0))
 		return SPA_BT_TRANSPORT_STATE_PENDING;
 	else if (strcasecmp("active", value) == 0)
 		return SPA_BT_TRANSPORT_STATE_ACTIVE;
@@ -772,6 +787,9 @@ int spa_bt_quirks_get_features(const struct spa_bt_quirks *quirks,
 		const struct spa_bt_adapter *adapter,
 		const struct spa_bt_device *device,
 		uint32_t *features);
+void spa_bt_quirks_log_features(const struct spa_bt_quirks *this,
+		const struct spa_bt_adapter *adapter,
+		const struct spa_bt_device *device);
 void spa_bt_quirks_destroy(struct spa_bt_quirks *quirks);
 
 int spa_bt_adapter_has_msbc(struct spa_bt_adapter *adapter);
diff --git a/spa/plugins/bluez5/g722/g722_enc_dec.h b/spa/plugins/bluez5/g722/g722_enc_dec.h
new file mode 100644
index 00000000..0da6060d
--- /dev/null
+++ b/spa/plugins/bluez5/g722/g722_enc_dec.h
@@ -0,0 +1,148 @@
+/*
+ * SpanDSP - a series of DSP components for telephony
+ *
+ * g722.h - The ITU G.722 codec.
+ *
+ * Written by Steve Underwood <steveu@coppice.org>
+ *
+ * Copyright (C) 2005 Steve Underwood
+ *
+ *  Despite my general liking of the GPL, I place my own contributions
+ *  to this code in the public domain for the benefit of all mankind -
+ *  even the slimy ones who might try to proprietize my work and use it
+ *  to my detriment.
+ *
+ * Based on a single channel G.722 codec which is:
+ *
+ *****    Copyright (c) CMU    1993      *****
+ * Computer Science, Speech Group
+ * Chengxiang Lu and Alex Hauptmann
+ *
+ * $Id: g722.h 48959 2006-12-25 06:42:15Z rizzo $
+ */
+
+/*! \file */
+
+#if !defined(_G722_H_)
+#define _G722_H_
+
+#include <stdint.h>
+
+/*! \page g722_page G.722 encoding and decoding
+\section g722_page_sec_1 What does it do?
+The G.722 module is a bit exact implementation of the ITU G.722 specification for all three
+specified bit rates - 64000bps, 56000bps and 48000bps. It passes the ITU tests.
+
+To allow fast and flexible interworking with narrow band telephony, the encoder and decoder
+support an option for the linear audio to be an 8k samples/second stream. In this mode the
+codec is considerably faster, and still fully compatible with wideband terminals using G.722.
+
+\section g722_page_sec_2 How does it work?
+???.
+*/
+
+/* Format DAC12 is added to decode directly into samples suitable for
+   a 12-bit DAC using offset binary representation. */
+
+enum {
+  G722_SAMPLE_RATE_8000 = 0x0001,
+  G722_PACKED = 0x0002,
+  G722_FORMAT_DAC12 = 0x0004,
+};
+
+#ifdef BUILD_FEATURE_DAC
+#define NLDECOMPRESS_APPLY_GAIN(s, g) (((s) * (int32_t)(g)) >> 16)
+// Equivalent to shift 16, add 0x8000, shift 4
+#define NLDECOMPRESS_APPLY_GAIN_CONVERTED_DAC(s, g) \
+  (uint16_t)((uint16_t)(((s) * (int32_t)(g)) >> 20) + 0x800)
+#else
+#define NLDECOMPRESS_APPLY_GAIN(s, g) (((int32_t)(s) * (int32_t)(g)) >> 16)
+#endif
+
+#ifdef BUILD_FEATURE_DAC
+#define NLDECOMPRESS_PREPROCESS_PCM_SAMPLE_WITH_GAIN(s, g) \
+  NLDECOMPRESS_APPLY_GAIN_CONVERTED_DAC((s), (g))
+#define NLDECOMPRESS_PREPROCESS_SAMPLE_WITH_GAIN(s, g) ((int16_t)NLDECOMPRESS_APPLY_GAIN((s), (g)))
+#else
+#define NLDECOMPRESS_PREPROCESS_PCM_SAMPLE_WITH_GAIN NLDECOMPRESS_PREPROCESS_SAMPLE_WITH_GAIN
+#define NLDECOMPRESS_PREPROCESS_SAMPLE_WITH_GAIN(s, g) \
+  ((int16_t)(NLDECOMPRESS_APPLY_GAIN((s), (g))))
+#endif
+
+typedef struct {
+  int s;
+  int sp;
+  int sz;
+  int r[3];
+  int a[3];
+  int ap[3];
+  int p[3];
+  int d[7];
+  int b[7];
+  int bp[7];
+  int nb;
+  int det;
+} g722_band_t;
+
+typedef struct {
+  /*! TRUE if the operating in the special ITU test mode, with the band split filters disabled. */
+  int itu_test_mode;
+  /*! TRUE if the G.722 data is packed */
+  int packed;
+  /*! TRUE if encode from 8k samples/second */
+  int eight_k;
+  /*! 6 for 48000kbps, 7 for 56000kbps, or 8 for 64000kbps. */
+  int bits_per_sample;
+
+  /*! Signal history for the QMF */
+  int x[24];
+
+  g722_band_t band[2];
+
+  unsigned int in_buffer;
+  int in_bits;
+  unsigned int out_buffer;
+  int out_bits;
+} g722_encode_state_t;
+
+typedef struct {
+  /*! TRUE if the operating in the special ITU test mode, with the band split filters disabled. */
+  int itu_test_mode;
+  /*! TRUE if the G.722 data is packed */
+  int packed;
+  /*! TRUE if decode to 8k samples/second */
+  int eight_k;
+  /*! 6 for 48000kbps, 7 for 56000kbps, or 8 for 64000kbps. */
+  int bits_per_sample;
+  /*! TRUE if offset binary for a 12-bit DAC */
+  int dac_pcm;
+
+  /*! Signal history for the QMF */
+  int x[24];
+
+  g722_band_t band[2];
+
+  unsigned int in_buffer;
+  int in_bits;
+  unsigned int out_buffer;
+  int out_bits;
+} g722_decode_state_t;
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+g722_encode_state_t *g722_encode_init(g722_encode_state_t *s, unsigned int rate, int options);
+int g722_encode_release(g722_encode_state_t *s);
+int g722_encode(g722_encode_state_t *s, uint8_t g722_data[], const int16_t amp[], int len);
+
+g722_decode_state_t *g722_decode_init(g722_decode_state_t *s, unsigned int rate, int options);
+int g722_decode_release(g722_decode_state_t *s);
+uint32_t g722_decode(g722_decode_state_t *s, int16_t amp[], const uint8_t g722_data[], int len,
+                     uint16_t aGain);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/spa/plugins/bluez5/g722/g722_encode.c b/spa/plugins/bluez5/g722/g722_encode.c
new file mode 100644
index 00000000..a185e259
--- /dev/null
+++ b/spa/plugins/bluez5/g722/g722_encode.c
@@ -0,0 +1,387 @@
+/*
+ * SpanDSP - a series of DSP components for telephony
+ *
+ * g722_encode.c - The ITU G.722 codec, encode part.
+ *
+ * Written by Steve Underwood <steveu@coppice.org>
+ *
+ * Copyright (C) 2005 Steve Underwood
+ *
+ * All rights reserved.
+ *
+ *  Despite my general liking of the GPL, I place my own contributions
+ *  to this code in the public domain for the benefit of all mankind -
+ *  even the slimy ones who might try to proprietize my work and use it
+ *  to my detriment.
+ *
+ * Based on a single channel 64kbps only G.722 codec which is:
+ *
+ *****    Copyright (c) CMU    1993      *****
+ * Computer Science, Speech Group
+ * Chengxiang Lu and Alex Hauptmann
+ *
+ * $Id: g722_encode.c,v 1.14 2006/07/07 16:37:49 steveu Exp $
+ */
+
+/*! \file */
+
+#include <stdlib.h>
+#include <string.h>
+
+#include "g722_enc_dec.h"
+
+#if !defined(FALSE)
+#define FALSE 0
+#endif
+#if !defined(TRUE)
+#define TRUE (!FALSE)
+#endif
+
+#define PACKED_OUTPUT (0)
+#define BITS_PER_SAMPLE (8)
+
+#ifndef BUILD_FEATURE_G722_USE_INTRINSIC_SAT
+static __inline int16_t saturate(int32_t amp) {
+  int16_t amp16;
+
+  /* Hopefully this is optimised for the common case - not clipping */
+  amp16 = (int16_t)amp;
+  if (amp == amp16) {
+    return amp16;
+  }
+  if (amp > 0x7FFF) {
+    return 0x7FFF;
+  }
+  return 0x8000;
+}
+#else
+static __inline int16_t saturate(int32_t val) {
+  register int32_t res;
+  __asm volatile("SSAT %0, #16, %1\n\t" : "=r"(res) : "r"(val) :);
+  return (int16_t)res;
+}
+#endif
+/*- End of function --------------------------------------------------------*/
+
+static void block4(g722_band_t *band, int d) {
+  int wd1;
+  int wd2;
+  int wd3;
+  int i;
+  int sg[7];
+  int ap1, ap2;
+  int sg0, sgi;
+  int sz;
+
+  /* Block 4, RECONS */
+  band->d[0] = d;
+  band->r[0] = saturate(band->s + d);
+
+  /* Block 4, PARREC */
+  band->p[0] = saturate(band->sz + d);
+
+  /* Block 4, UPPOL2 */
+  for (i = 0; i < 3; i++) {
+    sg[i] = band->p[i] >> 15;
+  }
+  wd1 = saturate(band->a[1] << 2);
+
+  wd2 = (sg[0] == sg[1]) ? -wd1 : wd1;
+  if (wd2 > 32767) {
+    wd2 = 32767;
+  }
+
+  ap2 = (wd2 >> 7) + ((sg[0] == sg[2]) ? 128 : -128);
+  ap2 += (band->a[2] * 32512) >> 15;
+  if (ap2 > 12288) {
+    ap2 = 12288;
+  } else if (ap2 < -12288) {
+    ap2 = -12288;
+  }
+  band->ap[2] = ap2;
+
+  /* Block 4, UPPOL1 */
+  sg[0] = band->p[0] >> 15;
+  sg[1] = band->p[1] >> 15;
+  wd1 = (sg[0] == sg[1]) ? 192 : -192;
+  wd2 = (band->a[1] * 32640) >> 15;
+
+  ap1 = saturate(wd1 + wd2);
+  wd3 = saturate(15360 - band->ap[2]);
+  if (ap1 > wd3) {
+    ap1 = wd3;
+  } else if (ap1 < -wd3) {
+    ap1 = -wd3;
+  }
+  band->ap[1] = ap1;
+
+  /* Block 4, UPZERO */
+  /* Block 4, FILTEZ */
+  wd1 = (d == 0) ? 0 : 128;
+
+  sg0 = sg[0] = d >> 15;
+  for (i = 1; i < 7; i++) {
+    sgi = band->d[i] >> 15;
+    wd2 = (sgi == sg0) ? wd1 : -wd1;
+    wd3 = (band->b[i] * 32640) >> 15;
+    band->bp[i] = saturate(wd2 + wd3);
+  }
+
+  /* Block 4, DELAYA */
+  sz = 0;
+  for (i = 6; i > 0; i--) {
+    int bi;
+
+    band->d[i] = band->d[i - 1];
+    bi = band->b[i] = band->bp[i];
+    wd1 = saturate(band->d[i] + band->d[i]);
+    sz += (bi * wd1) >> 15;
+  }
+  band->sz = sz;
+
+  for (i = 2; i > 0; i--) {
+    band->r[i] = band->r[i - 1];
+    band->p[i] = band->p[i - 1];
+    band->a[i] = band->ap[i];
+  }
+
+  /* Block 4, FILTEP */
+  wd1 = saturate(band->r[1] + band->r[1]);
+  wd1 = (band->a[1] * wd1) >> 15;
+  wd2 = saturate(band->r[2] + band->r[2]);
+  wd2 = (band->a[2] * wd2) >> 15;
+  band->sp = saturate(wd1 + wd2);
+
+  /* Block 4, PREDIC */
+  band->s = saturate(band->sp + band->sz);
+}
+/*- End of function --------------------------------------------------------*/
+
+g722_encode_state_t *g722_encode_init(g722_encode_state_t *s, unsigned int rate, int options) {
+  if (s == NULL) {
+#ifdef G722_SUPPORT_MALLOC
+    if ((s = (g722_encode_state_t *)malloc(sizeof(*s))) == NULL)
+#endif
+      return NULL;
+  }
+  memset(s, 0, sizeof(*s));
+  if (rate == 48000) {
+    s->bits_per_sample = 6;
+  } else if (rate == 56000) {
+    s->bits_per_sample = 7;
+  } else {
+    s->bits_per_sample = 8;
+  }
+  s->band[0].det = 32;
+  s->band[1].det = 8;
+  return s;
+}
+/*- End of function --------------------------------------------------------*/
+
+int g722_encode_release(g722_encode_state_t *s) {
+  free(s);
+  return 0;
+}
+/*- End of function --------------------------------------------------------*/
+
+/* WebRtc, tlegrand:
+ * Only define the following if bit-exactness with reference implementation
+ * is needed. Will only have any effect if input signal is saturated.
+ */
+// #define RUN_LIKE_REFERENCE_G722
+#ifdef RUN_LIKE_REFERENCE_G722
+int16_t limitValues(int16_t rl) {
+  int16_t yl;
+
+  yl = (rl > 16383) ? 16383 : ((rl < -16384) ? -16384 : rl);
+
+  return yl;
+}
+/*- End of function --------------------------------------------------------*/
+#endif
+
+static int16_t q6[32] = {0,    35,   72,   110,  150,  190,  233,  276,  323,  370,  422,
+                         473,  530,  587,  650,  714,  786,  858,  940,  1023, 1121, 1219,
+                         1339, 1458, 1612, 1765, 1980, 2195, 2557, 2919, 0,    0};
+static int16_t iln[32] = {0,  63, 62, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19,
+                          18, 17, 16, 15, 14, 13, 12, 11, 10, 9,  8,  7,  6,  5,  4,  0};
+static int16_t ilp[32] = {0,  61, 60, 59, 58, 57, 56, 55, 54, 53, 52, 51, 50, 49, 48, 47,
+                          46, 45, 44, 43, 42, 41, 40, 39, 38, 37, 36, 35, 34, 33, 32, 0};
+static int16_t wl[8] = {-60, -30, 58, 172, 334, 538, 1198, 3042};
+static int16_t rl42[16] = {0, 7, 6, 5, 4, 3, 2, 1, 7, 6, 5, 4, 3, 2, 1, 0};
+static int16_t ilb[32] = {2048, 2093, 2139, 2186, 2233, 2282, 2332, 2383, 2435, 2489, 2543,
+                          2599, 2656, 2714, 2774, 2834, 2896, 2960, 3025, 3091, 3158, 3228,
+                          3298, 3371, 3444, 3520, 3597, 3676, 3756, 3838, 3922, 4008};
+static int16_t qm4[16] = {0,     -20456, -12896, -8968, -6288, -4240, -2584, -1200,
+                          20456, 12896,  8968,   6288,  4240,  2584,  1200,  0};
+static int16_t qm2[4] = {-7408, -1616, 7408, 1616};
+static int16_t qmf_coeffs[12] = {
+        3, -11, 12, 32, -210, 951, 3876, -805, 362, -156, 53, -11,
+};
+static int16_t ihn[3] = {0, 1, 0};
+static int16_t ihp[3] = {0, 3, 2};
+static int16_t wh[3] = {0, -214, 798};
+static int16_t rh2[4] = {2, 1, 2, 1};
+
+int g722_encode(g722_encode_state_t *s, uint8_t g722_data[], const int16_t amp[], int len) {
+  int dlow;
+  int dhigh;
+  int el;
+  int wd;
+  int wd1;
+  int ril;
+  int wd2;
+  int il4;
+  int ih2;
+  int wd3;
+  int eh;
+  int mih;
+  int i;
+  int j;
+  /* Low and high band PCM from the QMF */
+  int xlow;
+  int xhigh;
+  int g722_bytes;
+  /* Even and odd tap accumulators */
+  int sumeven;
+  int sumodd;
+  int ihigh;
+  int ilow;
+  int code;
+
+  g722_bytes = 0;
+  xhigh = 0;
+  for (j = 0; j < len;) {
+    if (s->itu_test_mode) {
+      xlow = xhigh = amp[j++] >> 1;
+    } else {
+      {
+        /* Apply the transmit QMF */
+        /* Shuffle the buffer down */
+        for (i = 0; i < 22; i++) {
+          s->x[i] = s->x[i + 2];
+        }
+        // TODO: if len is odd, then this can be a buffer overrun
+        s->x[22] = amp[j++];
+        s->x[23] = amp[j++];
+
+        /* Discard every other QMF output */
+        sumeven = 0;
+        sumodd = 0;
+        for (i = 0; i < 12; i++) {
+          sumodd += s->x[2 * i] * qmf_coeffs[i];
+          sumeven += s->x[2 * i + 1] * qmf_coeffs[11 - i];
+        }
+        /* We shift by 12 to allow for the QMF filters (DC gain = 4096), plus 1
+           to allow for us summing two filters, plus 1 to allow for the 15 bit
+           input to the G.722 algorithm. */
+        xlow = (sumeven + sumodd) >> 14;
+        xhigh = (sumeven - sumodd) >> 14;
+
+#ifdef RUN_LIKE_REFERENCE_G722
+        /* The following lines are only used to verify bit-exactness
+         * with reference implementation of G.722. Higher precision
+         * is achieved without limiting the values.
+         */
+        xlow = limitValues(xlow);
+        xhigh = limitValues(xhigh);
+#endif
+      }
+    }
+    /* Block 1L, SUBTRA */
+    el = saturate(xlow - s->band[0].s);
+
+    /* Block 1L, QUANTL */
+    wd = (el >= 0) ? el : -(el + 1);
+
+    for (i = 1; i < 30; i++) {
+      wd1 = (q6[i] * s->band[0].det) >> 12;
+      if (wd < wd1) {
+        break;
+      }
+    }
+    ilow = (el < 0) ? iln[i] : ilp[i];
+
+    /* Block 2L, INVQAL */
+    ril = ilow >> 2;
+    wd2 = qm4[ril];
+    dlow = (s->band[0].det * wd2) >> 15;
+
+    /* Block 3L, LOGSCL */
+    il4 = rl42[ril];
+    wd = (s->band[0].nb * 127) >> 7;
+    s->band[0].nb = wd + wl[il4];
+    if (s->band[0].nb < 0) {
+      s->band[0].nb = 0;
+    } else if (s->band[0].nb > 18432) {
+      s->band[0].nb = 18432;
+    }
+
+    /* Block 3L, SCALEL */
+    wd1 = (s->band[0].nb >> 6) & 31;
+    wd2 = 8 - (s->band[0].nb >> 11);
+    wd3 = (wd2 < 0) ? (ilb[wd1] << -wd2) : (ilb[wd1] >> wd2);
+    s->band[0].det = wd3 << 2;
+
+    block4(&s->band[0], dlow);
+    {
+      int nb;
+
+      /* Block 1H, SUBTRA */
+      eh = saturate(xhigh - s->band[1].s);
+
+      /* Block 1H, QUANTH */
+      wd = (eh >= 0) ? eh : -(eh + 1);
+      wd1 = (564 * s->band[1].det) >> 12;
+      mih = (wd >= wd1) ? 2 : 1;
+      ihigh = (eh < 0) ? ihn[mih] : ihp[mih];
+
+      /* Block 2H, INVQAH */
+      wd2 = qm2[ihigh];
+      dhigh = (s->band[1].det * wd2) >> 15;
+
+      /* Block 3H, LOGSCH */
+      ih2 = rh2[ihigh];
+      wd = (s->band[1].nb * 127) >> 7;
+
+      nb = wd + wh[ih2];
+      if (nb < 0) {
+        nb = 0;
+      } else if (nb > 22528) {
+        nb = 22528;
+      }
+      s->band[1].nb = nb;
+
+      /* Block 3H, SCALEH */
+      wd1 = (s->band[1].nb >> 6) & 31;
+      wd2 = 10 - (s->band[1].nb >> 11);
+      wd3 = (wd2 < 0) ? (ilb[wd1] << -wd2) : (ilb[wd1] >> wd2);
+      s->band[1].det = wd3 << 2;
+
+      block4(&s->band[1], dhigh);
+#if BITS_PER_SAMPLE == 8
+      code = ((ihigh << 6) | ilow);
+#elif BITS_PER_SAMPLE == 7
+      code = ((ihigh << 6) | ilow) >> 1;
+#elif BITS_PER_SAMPLE == 6
+      code = ((ihigh << 6) | ilow) >> 2;
+#endif
+    }
+
+#if PACKED_OUTPUT == 1
+    /* Pack the code bits */
+    s->out_buffer |= (code << s->out_bits);
+    s->out_bits += s->bits_per_sample;
+    if (s->out_bits >= 8) {
+      g722_data[g722_bytes++] = (uint8_t)(s->out_buffer & 0xFF);
+      s->out_bits -= 8;
+      s->out_buffer >>= 8;
+    }
+#else
+    g722_data[g722_bytes++] = (uint8_t)code;
+#endif
+  }
+  return g722_bytes;
+}
+/*- End of function --------------------------------------------------------*/
+/*- End of file ------------------------------------------------------------*/
diff --git a/spa/plugins/bluez5/media-codecs.h b/spa/plugins/bluez5/media-codecs.h
index a1b0a9f2..c9a00f7b 100644
--- a/spa/plugins/bluez5/media-codecs.h
+++ b/spa/plugins/bluez5/media-codecs.h
@@ -26,7 +26,7 @@
 
 #define SPA_TYPE_INTERFACE_Bluez5CodecMedia	SPA_TYPE_INFO_INTERFACE_BASE "Bluez5:Codec:Media:Private"
 
-#define SPA_VERSION_BLUEZ5_CODEC_MEDIA		9
+#define SPA_VERSION_BLUEZ5_CODEC_MEDIA		12
 
 struct spa_bluez5_codec_a2dp {
 	struct spa_interface iface;
@@ -71,6 +71,7 @@ struct media_codec {
 	a2dp_vendor_codec_t vendor;
 
 	bool bap;
+	bool asha;
 
 	const char *name;
 	const char *description;
@@ -87,7 +88,7 @@ struct media_codec {
 
 	/** 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]);
+			const struct spa_dict *settings, uint8_t caps[A2DP_MAX_CAPS_SIZE]);
 
 	int (*select_config) (const struct media_codec *codec, uint32_t flags,
 			const void *caps, size_t caps_size,
@@ -199,6 +200,17 @@ struct media_codec {
 	int (*increase_bitpool) (void *data);
 
 	void (*set_log) (struct spa_log *global_log);
+
+	/**
+	 * Get codec internal delays, in samples at input/output rates.
+	 *
+	 * The delay does not include the duration of the PCM input/output
+	 * audio data, but is that internal to the codec.
+	 *
+	 * \param[out] encoder    Encoder delay in samples, or NULL
+	 * \param[out] decoder    Decoder delay in samples, or NULL
+	 */
+	void (*get_delay) (void *data, uint32_t *encoder, uint32_t *decoder);
 };
 
 struct media_codec_config {
diff --git a/spa/plugins/bluez5/media-sink.c b/spa/plugins/bluez5/media-sink.c
index 1e822f77..49dc5de7 100644
--- a/spa/plugins/bluez5/media-sink.c
+++ b/spa/plugins/bluez5/media-sink.c
@@ -165,6 +165,8 @@ struct impl {
 	uint64_t packet_delay_ns;
 	struct spa_source *update_delay_event;
 
+	uint32_t encoder_delay;
+
 	const struct media_codec *codec;
 	bool codec_props_changed;
 	void *codec_props;
@@ -380,7 +382,7 @@ static void set_latency(struct impl *this, bool emit_latency)
 
 	/* in main loop */
 
-	if (this->transport == NULL)
+	if (this->transport == NULL || !port->have_format)
 		return;
 
 	/*
@@ -388,12 +390,13 @@ static void set_latency(struct impl *this, bool emit_latency)
 	 *
 	 * (packet delay) + (codec internal delay) + (transport delay) + (latency offset)
 	 *
-	 * and doesn't depend on the quantum. The codec internal delay is neglected.
-	 * Kernel knows the latency due to socket/controller queue, but doesn't
-	 * tell us, so not included but hopefully in < 20 ms range.
+	 * and doesn't depend on the quantum. Kernel knows the latency due to
+	 * socket/controller queue, but doesn't tell us, so not included but
+	 * hopefully in < 10 ms range.
 	 */
 
 	delay = __atomic_load_n(&this->packet_delay_ns, __ATOMIC_RELAXED);
+	delay += (int64_t)this->encoder_delay * SPA_NSEC_PER_SEC / port->current_format.info.raw.rate;
 	delay += spa_bt_transport_get_delay_nsec(this->transport);
 	delay += SPA_CLAMP(this->props.latency_offset, -delay, INT64_MAX / 2);
 	delay = SPA_MAX(delay, 0);
@@ -539,9 +542,8 @@ static uint64_t get_reference_time(struct impl *this, uint64_t *duration_ns_ret)
 	/* Account for resampling delay */
 	resampling = (port->current_format.info.raw.rate != this->process_rate) || this->following;
 	if (port->rate_match && this->position && resampling) {
-		t -= (uint64_t)port->rate_match->delay * SPA_NSEC_PER_SEC
-			/ this->position->clock.rate.denom;
-		t += SPA_NSEC_PER_SEC / port->current_format.info.raw.rate;
+		t -= (port->rate_match->delay * SPA_NSEC_PER_SEC + port->rate_match->delay_frac)
+			/ port->current_format.info.raw.rate;
 	}
 
 	return t;
@@ -716,8 +718,8 @@ static int encode_fragment(struct impl *this)
 
 static int flush_buffer(struct impl *this)
 {
-	spa_log_trace(this->log, "%p: used:%d block_size:%d", this,
-			this->buffer_used, this->block_size);
+	spa_log_trace(this->log, "%p: used:%d block_size:%d need_flush:%d", this,
+			this->buffer_used, this->block_size, this->need_flush);
 
 	if (this->need_flush)
 		return send_buffer(this);
@@ -1069,7 +1071,7 @@ static void media_iso_pull(struct spa_bt_iso_io *iso_io)
 	} else {
 		spa_bt_rate_control_update(&port->ratectl, err, 0,
 				iso_io->duration, period, RATE_CTL_DIFF_MAX);
-		spa_log_trace(this->log, "%p: ISO sync err:%+.3f value:%.3f target:%.3f (ms) corr:%g",
+		spa_log_trace(this->log, "%p: ISO sync err:%+.3g value:%.6f target:%.6f (ms) corr:%g",
 				this,
 				port->ratectl.avg / SPA_NSEC_PER_MSEC,
 				value / SPA_NSEC_PER_MSEC,
@@ -1250,9 +1252,15 @@ static int transport_start(struct impl *this)
 		this->codec_props_changed = true;
 	}
 
-	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->encoder_delay = 0;
+	if (this->codec->get_delay)
+		this->codec->get_delay(this->codec_data, &this->encoder_delay, NULL);
+
+	const char *codec_profile = this->codec->asha ? "ASHA" : (this->codec->bap ? "BAP" : "A2DP");
+	spa_log_info(this->log, "%p: using %s codec %s, delay:%.2f ms, codec-delay:%.2f ms", this,
+			codec_profile, this->codec->description,
+			(double)spa_bt_transport_get_delay_nsec(this->transport) / SPA_NSEC_PER_MSEC,
+			(double)this->encoder_delay * SPA_MSEC_PER_SEC / port->current_format.info.raw.rate);
 
 	this->seqnum = UINT16_MAX;
 
@@ -1490,12 +1498,13 @@ static void emit_node_info(struct impl *this, bool full)
 		node_group = node_group_buf;
 	}
 
+	const char *codec_profile = this->codec->asha ? "ASHA" : (this->codec->bap ? "BAP" : "A2DP");
 	struct spa_dict_item node_info_items[] = {
 		{ SPA_KEY_DEVICE_API, "bluez5" },
 		{ SPA_KEY_MEDIA_CLASS, this->is_internal ? "Audio/Sink/Internal" :
 		  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" ) },
+					this->transport->device->name : codec_profile ) },
 		{ SPA_KEY_NODE_DRIVER, this->is_output ? "true" : "false" },
 		{ "node.group", node_group },
 	};
diff --git a/spa/plugins/bluez5/media-source.c b/spa/plugins/bluez5/media-source.c
index 488cf497..00559749 100644
--- a/spa/plugins/bluez5/media-source.c
+++ b/spa/plugins/bluez5/media-source.c
@@ -68,7 +68,7 @@ struct port {
 	struct spa_port_info info;
 	struct spa_io_buffers *io;
 	struct spa_io_rate_match *rate_match;
-	struct spa_latency_info latency;
+	struct spa_latency_info latency[2];
 #define IDX_EnumFormat	0
 #define IDX_Meta	1
 #define IDX_IO		2
@@ -87,6 +87,18 @@ struct port {
 	struct spa_bt_decode_buffer buffer;
 };
 
+struct delay_info {
+	union {
+		struct {
+			int32_t buffer;
+			uint32_t duration;
+		};
+		uint64_t v;
+	};
+};
+
+SPA_STATIC_ASSERT(sizeof(struct delay_info) == sizeof(uint64_t));
+
 struct impl {
 	struct spa_handle handle;
 	struct spa_node node;
@@ -94,6 +106,7 @@ struct impl {
 	struct spa_log *log;
 	struct spa_loop *data_loop;
 	struct spa_system *data_system;
+	struct spa_loop_utils *loop_utils;
 
 	struct spa_hook_list hooks;
 	struct spa_callbacks callbacks;
@@ -150,6 +163,10 @@ struct impl {
 	uint64_t sample_count;
 
 	uint32_t errqueue_count;
+
+	struct delay_info delay;
+	int64_t delay_sink;
+	struct spa_source *update_delay_event;
 };
 
 #define CHECK_PORT(this,d,p)    ((d) == SPA_DIRECTION_OUTPUT && (p) == 0)
@@ -307,8 +324,10 @@ static void set_latency(struct impl *this, bool emit_latency)
 {
 	if (this->codec->bap && !this->is_input && this->transport &&
 			this->transport->delay_us != SPA_BT_UNKNOWN_DELAY) {
+		struct port *port = &this->port;
 		unsigned int node_latency = 2048;
-		unsigned int target = this->transport->delay_us*48000ll/SPA_USEC_PER_SEC * 1/2;
+		uint64_t rate = port->current_format.info.raw.rate;
+		unsigned int target = this->transport->delay_us*rate/SPA_USEC_PER_SEC * 1/2;
 
 		/* Adjust requested node latency to be somewhat (~1/2) smaller
 		 * than presentation delay. The difference functions as room
@@ -323,8 +342,9 @@ static void set_latency(struct impl *this, bool emit_latency)
 				emit_node_info(this, false);
 		}
 
-		spa_log_info(this->log, "BAP presentation delay %d us, node latency %u/48000",
-				(int)this->transport->delay_us, node_latency);
+		spa_log_info(this->log, "BAP presentation delay %d us, node latency %u/%u",
+				(int)this->transport->delay_us, node_latency,
+				(unsigned int)rate);
 	}
 }
 
@@ -504,6 +524,9 @@ static void media_on_ready_read(struct spa_source *source)
 
 	spa_log_trace(this->log, "socket poll");
 
+	/* update the current pts */
+	spa_system_clock_gettime(this->data_system, CLOCK_MONOTONIC, &now);
+
 	/* read */
 	size_read = read_data (this);
 	if (size_read == 0)
@@ -513,9 +536,6 @@ static void media_on_ready_read(struct spa_source *source)
 		goto stop;
 	}
 
-	/* update the current pts */
-	spa_system_clock_gettime(this->data_system, CLOCK_MONOTONIC, &now);
-
 	if (this->codec_props_changed && this->codec_props
 			&& this->codec->update_props) {
 		this->codec->update_props(this->codec_data, this->codec_props);
@@ -539,7 +559,7 @@ static void media_on_ready_read(struct spa_source *source)
 	if (!this->started)
 		return;
 
-	spa_bt_decode_buffer_write_packet(&port->buffer, decoded);
+	spa_bt_decode_buffer_write_packet(&port->buffer, decoded, SPA_TIMESPEC_TO_NSEC(&now));
 
 	dt = SPA_TIMESPEC_TO_NSEC(&this->now);
 	this->now = now;
@@ -648,6 +668,52 @@ static void media_iso_pull(struct spa_bt_iso_io *iso_io)
 	 * iso-io whether this source is running or not. */
 }
 
+static void emit_port_info(struct impl *this, struct port *port, bool full);
+
+static void update_transport_delay(struct impl *this)
+{
+	struct port *port = &this->port;
+	struct delay_info info;
+	float latency;
+	int64_t latency_nsec;
+	int64_t delay_sink;
+
+	if (!this->transport || !port->have_format)
+		return;
+
+	info.v = __atomic_load_n(&this->delay.v, __ATOMIC_RELAXED);
+
+	/* Latency to sink */
+	latency = info.buffer
+		+ port->latency[SPA_DIRECTION_INPUT].min_rate
+		+ port->latency[SPA_DIRECTION_INPUT].min_quantum * info.duration;
+
+	latency_nsec = port->latency[SPA_DIRECTION_INPUT].min_ns
+		+ (int64_t)(latency * SPA_NSEC_PER_SEC / port->current_format.info.raw.rate);
+
+	spa_bt_transport_set_delay(this->transport, latency_nsec);
+
+	delay_sink =
+		port->latency[SPA_DIRECTION_INPUT].min_ns
+		+ (int64_t)((port->latency[SPA_DIRECTION_INPUT].min_rate
+						+ port->latency[SPA_DIRECTION_INPUT].min_quantum * info.duration)
+				* SPA_NSEC_PER_SEC / port->current_format.info.raw.rate);
+	__atomic_store_n(&this->delay_sink, delay_sink, __ATOMIC_RELAXED);
+
+	/* Latency from source */
+	port->latency[SPA_DIRECTION_OUTPUT] = SPA_LATENCY_INFO(SPA_DIRECTION_OUTPUT,
+			.min_rate = info.buffer, .max_rate = info.buffer);
+	port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+	port->params[IDX_Latency].user++;
+	emit_port_info(this, port, false);
+}
+
+static void update_delay_event(void *data, uint64_t count)
+{
+	/* in main loop */
+	update_transport_delay(data);
+}
+
 static int do_start_iso_io(struct spa_loop *loop, bool async, uint32_t seq,
 		const void *data, size_t size, void *user_data)
 {
@@ -709,11 +775,15 @@ static int transport_start(struct impl *this)
 		return res;
 
 	if (this->is_duplex) {
-		/* 80 ms max buffer */
-		spa_bt_decode_buffer_set_max_latency(&port->buffer,
+		/* 80 ms max extra buffer */
+		spa_bt_decode_buffer_set_max_extra_latency(&port->buffer,
 				port->current_format.info.raw.rate * 80 / 1000);
 	}
 
+	this->delay.buffer = -1;
+	this->delay.duration = 0;
+	this->update_delay_event = spa_loop_utils_add_event(this->loop_utils, update_delay_event, this);
+
 	this->sample_count = 0;
 	this->errqueue_count = 0;
 
@@ -790,6 +860,11 @@ static int do_remove_source(struct spa_loop *loop,
 		spa_bt_iso_io_set_cb(this->transport->iso_io, NULL, NULL);
 	set_timeout(this, 0);
 
+	if (this->update_delay_event) {
+		spa_loop_utils_destroy_source(this->loop_utils, this->update_delay_event);
+		this->update_delay_event = NULL;
+	}
+
 	return 0;
 }
 
@@ -898,7 +973,9 @@ static void emit_node_info(struct impl *this, bool full)
 {
 	uint64_t old = full ? this->info.change_mask : 0;
 	char latency[64];
+	char rate[64];
 	char media_name[256];
+	struct port *port = &this->port;
 
 	spa_scnprintf(
 		media_name,
@@ -915,10 +992,12 @@ static void emit_node_info(struct impl *this, bool full)
 		  this->is_input ? "Audio/Source" : "Stream/Output/Audio" },
 		{ SPA_KEY_NODE_LATENCY, this->is_input ? "" : latency },
 		{ "media.name", media_name },
+		{ "node.rate", this->is_input ? "" : rate },
 		{ SPA_KEY_NODE_DRIVER, this->is_input ? "true" : "false" },
 	};
 
-	spa_scnprintf(latency, sizeof(latency), "%d/48000", this->node_latency);
+	spa_scnprintf(latency, sizeof(latency), "%u/%u", this->node_latency, port->current_format.info.raw.rate);
+	spa_scnprintf(rate, sizeof(rate), "1/%u", port->current_format.info.raw.rate);
 
 	if (full)
 		this->info.change_mask = this->info_all;
@@ -1104,8 +1183,8 @@ impl_node_port_enum_params(void *object, int seq,
 
 	case SPA_PARAM_Latency:
 		switch (result.index) {
-		case 0:
-			param = spa_latency_build(&b, id, &port->latency);
+		case 0: case 1:
+			param = spa_latency_build(&b, id, &port->latency[result.index]);
 			break;
 		default:
 			return 0;
@@ -1186,6 +1265,8 @@ static int port_set_format(struct impl *this, struct port *port,
 
 		port->current_format = info;
 		port->have_format = true;
+
+		set_latency(this, false);
 	}
 
 	port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
@@ -1201,6 +1282,9 @@ static int port_set_format(struct impl *this, struct port *port,
 	}
 	emit_port_info(this, port, false);
 
+	this->info.change_mask |= SPA_NODE_CHANGE_MASK_PROPS;
+	emit_node_info(this, false);
+
 	return 0;
 }
 
@@ -1224,8 +1308,28 @@ impl_node_port_set_param(void *object,
 		res = port_set_format(this, port, flags, param);
 		break;
 	case SPA_PARAM_Latency:
+	{
+		enum spa_direction other = SPA_DIRECTION_REVERSE(direction);
+		struct spa_latency_info info;
+
+		if (param == NULL)
+			info = SPA_LATENCY_INFO(other);
+		else if ((res = spa_latency_parse(param, &info)) < 0)
+			return res;
+		if (info.direction != other)
+			return -EINVAL;
+		if (memcmp(&port->latency[info.direction], &info, sizeof(info)) == 0)
+			return 0;
+
+		port->latency[info.direction] = info;
+		this->port.info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+		this->port.params[IDX_Latency].user++;
+
+		update_transport_delay(this);
+		emit_port_info(this, port, false);
 		res = 0;
 		break;
+	}
 	default:
 		res = -ENOENT;
 		break;
@@ -1354,7 +1458,8 @@ static uint32_t get_samples(struct impl *this, uint32_t *result_duration)
 static void update_target_latency(struct impl *this)
 {
 	struct port *port = &this->port;
-	uint32_t samples, duration;
+	uint32_t samples, duration, latency;
+	int64_t delay_sink;
 
 	if (this->transport == NULL || !port->have_format)
 		return;
@@ -1380,8 +1485,11 @@ static void update_target_latency(struct impl *this)
 	samples = (uint64_t)this->transport->delay_us *
 		port->current_format.info.raw.rate / SPA_USEC_PER_SEC;
 
-	if (samples > duration)
-		samples -= duration;
+	delay_sink = __atomic_load_n(&this->delay_sink, __ATOMIC_RELAXED);
+	latency = delay_sink * port->current_format.info.raw.rate / SPA_NSEC_PER_SEC;
+
+	if (samples > latency)
+		samples -= latency;
 	else
 		samples = 1;
 
@@ -1406,7 +1514,9 @@ static void process_buffering(struct impl *this)
 
 	update_target_latency(this);
 
-	spa_bt_decode_buffer_process(&port->buffer, samples, duration);
+	spa_bt_decode_buffer_process(&port->buffer, samples, duration,
+			this->position ? this->position->clock.rate_diff : 1.0,
+			this->position ? this->position->clock.next_nsec : 0);
 
 	setup_matching(this);
 
@@ -1459,6 +1569,23 @@ static void process_buffering(struct impl *this)
 		spa_log_trace(this->log, "queue %d frames:%d", buffer->id, (int)samples);
 		spa_list_append(&port->ready, &buffer->link);
 	}
+
+	if (this->update_delay_event) {
+		int32_t target = spa_bt_decode_buffer_get_target_latency(&port->buffer);
+		uint32_t decoder_delay = 0;
+
+		if (this->codec->get_delay)
+			this->codec->get_delay(this->codec_data, NULL, &decoder_delay);
+
+		target += decoder_delay;
+
+		if (target != this->delay.buffer || duration != this->delay.duration) {
+			struct delay_info info = { .buffer = target, .duration = duration };
+
+			__atomic_store_n(&this->delay.v, info.v, __ATOMIC_RELAXED);
+			spa_loop_utils_signal_event(this->loop_utils, this->update_delay_event);
+		}
+	}
 }
 
 static int produce_buffer(struct impl *this)
@@ -1679,6 +1806,7 @@ impl_init(const struct spa_handle_factory *factory,
 	this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
 	this->data_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataLoop);
 	this->data_system = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataSystem);
+	this->loop_utils = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_LoopUtils);
 
 	spa_log_topic_init(this->log, &log_topic);
 
@@ -1690,6 +1818,10 @@ impl_init(const struct spa_handle_factory *factory,
 		spa_log_error(this->log, "a data system is needed");
 		return -EINVAL;
 	}
+	if (this->loop_utils == NULL) {
+		spa_log_error(this->log, "loop utils are needed");
+		return -EINVAL;
+	}
 
 	this->node.iface = SPA_INTERFACE_INIT(
 			SPA_TYPE_INTERFACE_Node,
@@ -1731,9 +1863,8 @@ impl_init(const struct spa_handle_factory *factory,
 	port->info.params = port->params;
 	port->info.n_params = N_PORT_PARAMS;
 
-	port->latency = SPA_LATENCY_INFO(SPA_DIRECTION_OUTPUT);
-	port->latency.min_quantum = 1.0f;
-	port->latency.max_quantum = 1.0f;
+	port->latency[SPA_DIRECTION_INPUT] = SPA_LATENCY_INFO(SPA_DIRECTION_INPUT);
+	port->latency[SPA_DIRECTION_OUTPUT] = SPA_LATENCY_INFO(SPA_DIRECTION_OUTPUT);
 
 	/* Init the buffer lists */
 	spa_list_init(&port->ready);
diff --git a/spa/plugins/bluez5/meson.build b/spa/plugins/bluez5/meson.build
index 035e4b7a..b0990eaf 100644
--- a/spa/plugins/bluez5/meson.build
+++ b/spa/plugins/bluez5/meson.build
@@ -56,7 +56,7 @@ if get_option('bluez5-backend-hsp-native').allowed() or get_option('bluez5-backe
     bluez5_deps += mm_dep
     bluez5_sources += ['modemmanager.c']
   endif
-  bluez5_sources += ['backend-native.c', 'upower.c']
+  bluez5_sources += ['backend-native.c', 'upower.c', 'telephony.c']
 endif
 
 if get_option('bluez5-backend-ofono').allowed()
@@ -173,6 +173,16 @@ if get_option('bluez5-codec-lc3').allowed() and lc3_dep.found()
     install_dir : spa_plugindir / 'bluez5')
 endif
 
+if get_option('bluez5-codec-g722').allowed()
+  bluez_codec_g722 = shared_library('spa-codec-bluez5-g722',
+    [ 'g722/g722_encode.c', 'asha-codec-g722.c', 'media-codecs.c' ],
+    include_directories : [ configinc ],
+    c_args : codec_args,
+    dependencies : [ spa_dep ],
+    install : true,
+    install_dir : spa_plugindir / 'bluez5')
+endif
+
 test_apps = [
   'test-midi',
 ]
diff --git a/spa/plugins/bluez5/midi-node.c b/spa/plugins/bluez5/midi-node.c
index af8c9aa6..b5fd179e 100644
--- a/spa/plugins/bluez5/midi-node.c
+++ b/spa/plugins/bluez5/midi-node.c
@@ -23,6 +23,7 @@
 #include <spa/utils/ringbuffer.h>
 #include <spa/monitor/device.h>
 #include <spa/control/control.h>
+#include <spa/control/ump-utils.h>
 
 #include <spa/node/node.h>
 #include <spa/node/utils.h>
@@ -449,7 +450,7 @@ static void midi_event_recv(void *user_data, uint16_t timestamp, uint8_t *data,
 	struct impl *this = user_data;
 	struct port *port = &this->ports[PORT_OUT];
 	struct time_sync *sync = &port->sync;
-	uint64_t time;
+	uint64_t time, state = 0;
 	int res;
 
 	spa_assert(size > 0);
@@ -459,11 +460,19 @@ static void midi_event_recv(void *user_data, uint16_t timestamp, uint8_t *data,
 	spa_log_trace(this->log, "%p: event:0x%x size:%d timestamp:%d time:%"PRIu64"",
 			this, (int)data[0], (int)size, (int)timestamp, (uint64_t)time);
 
-	res = midi_event_ringbuffer_push(&this->event_rbuf, time, data, size);
-	if (res < 0) {
-		midi_event_ringbuffer_init(&this->event_rbuf);
-		spa_log_warn(this->log, "%p: MIDI receive buffer overflow: %s",
-				this, spa_strerror(res));
+	while (size > 0) {
+		uint32_t ump[4];
+		int ump_size = spa_ump_from_midi(&data, &size,
+					ump, sizeof(ump), 0, &state);
+		if (ump_size <= 0)
+			break;
+
+		res = midi_event_ringbuffer_push(&this->event_rbuf, time, (uint8_t*)ump, ump_size);
+		if (res < 0) {
+			midi_event_ringbuffer_init(&this->event_rbuf);
+			spa_log_warn(this->log, "%p: MIDI receive buffer overflow: %s",
+					this, spa_strerror(res));
+		}
 	}
 }
 
@@ -704,7 +713,7 @@ static int process_output(struct impl *this)
 			offset = time * this->rate / SPA_NSEC_PER_SEC;
 			offset = SPA_CLAMP(offset, 0u, this->duration - 1);
 
-			spa_pod_builder_control(&port->builder, offset, SPA_CONTROL_Midi);
+			spa_pod_builder_control(&port->builder, offset, SPA_CONTROL_UMP);
 			buf = spa_pod_builder_reserve_bytes(&port->builder, size);
 			if (buf) {
 				midi_event_ringbuffer_pop(&this->event_rbuf, buf, size);
@@ -773,15 +782,18 @@ static int write_data(struct impl *this, struct spa_data *d)
 	time = 0;
 
 	SPA_POD_SEQUENCE_FOREACH(pod, c) {
-		uint8_t *event;
-		size_t size;
+		int size;
+		uint8_t event[32];
 
-		if (c->type != SPA_CONTROL_Midi)
+		if (c->type != SPA_CONTROL_UMP)
 			continue;
 
 		time = SPA_MAX(time, this->current_time + c->offset * SPA_NSEC_PER_SEC / this->rate);
-		event = SPA_POD_BODY(&c->value);
-		size = SPA_POD_BODY_SIZE(&c->value);
+
+		size = spa_ump_to_midi(SPA_POD_BODY(&c->value),
+				SPA_POD_BODY_SIZE(&c->value), event, sizeof(event));
+		if (size <= 0)
+			continue;
 
 		spa_log_trace(this->log, "%p: output event:0x%x time:%"PRIu64, this,
 				(size > 0) ? event[0] : 0, time);
@@ -1555,7 +1567,8 @@ next:
 		param = spa_pod_builder_add_object(&b,
 				SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat,
 				SPA_FORMAT_mediaType,	   SPA_POD_Id(SPA_MEDIA_TYPE_application),
-				SPA_FORMAT_mediaSubtype,   SPA_POD_Id(SPA_MEDIA_SUBTYPE_control));
+				SPA_FORMAT_mediaSubtype,   SPA_POD_Id(SPA_MEDIA_SUBTYPE_control),
+				SPA_FORMAT_CONTROL_types,  SPA_POD_CHOICE_FLAGS_Int(1u<<SPA_CONTROL_UMP));
 		break;
 
 	case SPA_PARAM_Format:
@@ -1567,7 +1580,8 @@ next:
 		param = spa_pod_builder_add_object(&b,
 				SPA_TYPE_OBJECT_Format, SPA_PARAM_Format,
 				SPA_FORMAT_mediaType,	   SPA_POD_Id(SPA_MEDIA_TYPE_application),
-				SPA_FORMAT_mediaSubtype,   SPA_POD_Id(SPA_MEDIA_SUBTYPE_control));
+				SPA_FORMAT_mediaSubtype,   SPA_POD_Id(SPA_MEDIA_SUBTYPE_control),
+				SPA_FORMAT_CONTROL_types,  SPA_POD_Int(1u<<SPA_CONTROL_UMP));
 		break;
 
 	case SPA_PARAM_Buffers:
@@ -2010,13 +2024,13 @@ impl_init(const struct spa_handle_factory *factory,
 	for (i = 0; i < N_PORTS; ++i) {
 		struct port *port = &this->ports[i];
 		static const struct spa_dict_item in_port_items[] = {
-			SPA_DICT_ITEM_INIT(SPA_KEY_FORMAT_DSP, "8 bit raw midi"),
+			SPA_DICT_ITEM_INIT(SPA_KEY_FORMAT_DSP, "32 bit raw UMP"),
 			SPA_DICT_ITEM_INIT(SPA_KEY_PORT_NAME, "in"),
 			SPA_DICT_ITEM_INIT(SPA_KEY_PORT_ALIAS, "in"),
 			SPA_DICT_ITEM_INIT(SPA_KEY_PORT_GROUP, "group.0"),
 		};
 		static const struct spa_dict_item out_port_items[] = {
-			SPA_DICT_ITEM_INIT(SPA_KEY_FORMAT_DSP, "8 bit raw midi"),
+			SPA_DICT_ITEM_INIT(SPA_KEY_FORMAT_DSP, "32 bit raw UMP"),
 			SPA_DICT_ITEM_INIT(SPA_KEY_PORT_NAME, "out"),
 			SPA_DICT_ITEM_INIT(SPA_KEY_PORT_ALIAS, "out"),
 			SPA_DICT_ITEM_INIT(SPA_KEY_PORT_GROUP, "group.0"),
diff --git a/spa/plugins/bluez5/quirks.c b/spa/plugins/bluez5/quirks.c
index e204822c..c4b293e6 100644
--- a/spa/plugins/bluez5/quirks.c
+++ b/spa/plugins/bluez5/quirks.c
@@ -87,26 +87,24 @@ static int do_match(const char *rules, struct spa_dict *dict, uint32_t *no_featu
 
 	while (spa_json_enter_object(&rules_arr, &it[0]) > 0) {
 		char key[256];
-		int match = true;
+		int match = true, len;
 		uint32_t no_features_cur = 0;
+		const char *value;
 
-		while (spa_json_get_string(&it[0], key, sizeof(key)) > 0) {
+		while ((len = spa_json_object_next(&it[0], key, sizeof(key), &value)) > 0) {
 			char val[4096];
-			const char *str, *value;
-			int len;
+			const char *str;
 			bool success = false;
 
 			if (spa_streq(key, "no-features")) {
-				if (spa_json_enter_array(&it[0], &it[1]) > 0) {
+				if (spa_json_is_array(value, len) > 0) {
+					spa_json_enter(&it[0], &it[1]);
 					while (spa_json_get_string(&it[1], val, sizeof(val)) > 0)
 						no_features_cur |= parse_feature(val);
 				}
 				continue;
 			}
 
-			if ((len = spa_json_next(&it[0], &value)) <= 0)
-				break;
-
 			if (spa_json_is_null(value, len)) {
 				value = NULL;
 			} else {
@@ -161,17 +159,13 @@ static void load_quirks(struct spa_bt_quirks *this, const char *str, size_t len)
 	struct spa_json rules;
 	char key[1024];
 	struct spa_error_location loc;
+	int sz;
+	const char *value;
 
 	if (spa_json_enter_object(&data, &rules) <= 0)
 		spa_json_init(&rules, str, len);
 
-	while (spa_json_get_string(&rules, key, sizeof(key)) > 0) {
-		int sz;
-		const char *value;
-
-		if ((sz = spa_json_next(&rules, &value)) <= 0)
-			break;
-
+	while ((sz = spa_json_object_next(&rules, key, sizeof(key), &value)) > 0) {
 		if (!spa_json_is_container(value, sz))
 			continue;
 
@@ -284,10 +278,11 @@ static void strtolower(char *src, char *dst, int maxsize)
 		*dst = '\0';
 }
 
-int spa_bt_quirks_get_features(const struct spa_bt_quirks *this,
+static int get_features(const struct spa_bt_quirks *this,
 		const struct spa_bt_adapter *adapter,
 		const struct spa_bt_device *device,
-		uint32_t *features)
+		uint32_t *features,
+		bool debug)
 {
 	struct spa_dict props;
 	struct spa_dict_item items[5];
@@ -300,15 +295,18 @@ int spa_bt_quirks_get_features(const struct spa_bt_quirks *this,
 		uint32_t no_features = 0;
 		int nitems = 0;
 		struct utsname name;
+
 		if ((res = uname(&name)) < 0)
 			return res;
 		items[nitems++] = SPA_DICT_ITEM_INIT("sysname", name.sysname);
 		items[nitems++] = SPA_DICT_ITEM_INIT("release", name.release);
 		items[nitems++] = SPA_DICT_ITEM_INIT("version", name.version);
 		props = SPA_DICT_INIT(items, nitems);
-		log_props(this->log, &props);
+		if (debug)
+			log_props(this->log, &props);
 		do_match(this->kernel_rules, &props, &no_features);
-		spa_log_debug(this->log, "kernel quirks:%08x", no_features);
+		if (debug)
+			spa_log_debug(this->log, "kernel quirks:%08x", no_features);
 		*features &= ~no_features;
 	}
 
@@ -331,9 +329,11 @@ int spa_bt_quirks_get_features(const struct spa_bt_quirks *this,
 			items[nitems++] = SPA_DICT_ITEM_INIT("address", address);
 		}
 		props = SPA_DICT_INIT(items, nitems);
-		log_props(this->log, &props);
+		if (debug)
+			log_props(this->log, &props);
 		do_match(this->adapter_rules, &props, &no_features);
-		spa_log_debug(this->log, "adapter quirks:%08x", no_features);
+		if (debug)
+			spa_log_debug(this->log, "adapter quirks:%08x", no_features);
 		*features &= ~no_features;
 	}
 
@@ -358,9 +358,11 @@ int spa_bt_quirks_get_features(const struct spa_bt_quirks *this,
 			items[nitems++] = SPA_DICT_ITEM_INIT("address", address);
 		}
 		props = SPA_DICT_INIT(items, nitems);
-		log_props(this->log, &props);
+		if (debug)
+			log_props(this->log, &props);
 		do_match(this->device_rules, &props, &no_features);
-		spa_log_debug(this->log, "device quirks:%08x", no_features);
+		if (debug)
+			spa_log_debug(this->log, "device quirks:%08x", no_features);
 		*features &= ~no_features;
 	}
 
@@ -385,3 +387,21 @@ int spa_bt_quirks_get_features(const struct spa_bt_quirks *this,
 
 	return 0;
 }
+
+int spa_bt_quirks_get_features(const struct spa_bt_quirks *this,
+		const struct spa_bt_adapter *adapter,
+		const struct spa_bt_device *device,
+		uint32_t *features)
+{
+	return get_features(this, adapter, device, features, false);
+}
+
+void spa_bt_quirks_log_features(const struct spa_bt_quirks *this,
+		const struct spa_bt_adapter *adapter,
+		const struct spa_bt_device *device)
+{
+	uint32_t features = 0;
+
+	get_features(this, adapter, device, &features, true);
+	spa_log_debug(this->log, "features:%08x", features);
+}
diff --git a/spa/plugins/bluez5/sco-source.c b/spa/plugins/bluez5/sco-source.c
index 71725e49..dc2e1f07 100644
--- a/spa/plugins/bluez5/sco-source.c
+++ b/spa/plugins/bluez5/sco-source.c
@@ -458,7 +458,7 @@ static int lc3_decode_frame(struct impl *this, const void *src, size_t src_size,
 #endif
 }
 
-static uint32_t preprocess_and_decode_codec_data(void *userdata, uint8_t *read_data, int size_read)
+static uint32_t preprocess_and_decode_codec_data(void *userdata, uint8_t *read_data, int size_read, uint64_t now)
 {
 	struct impl *this = userdata;
 	struct port *port = &this->port;
@@ -531,7 +531,7 @@ static uint32_t preprocess_and_decode_codec_data(void *userdata, uint8_t *read_d
 			continue;
 		}
 
-		spa_bt_decode_buffer_write_packet(&port->buffer, written);
+		spa_bt_decode_buffer_write_packet(&port->buffer, written, now);
 		decoded += written;
 	}
 
@@ -566,7 +566,7 @@ static int sco_source_cb(void *userdata, uint8_t *read_data, int size_read)
 
 	if (this->transport->codec == HFP_AUDIO_CODEC_MSBC ||
 			this->transport->codec == HFP_AUDIO_CODEC_LC3_SWB) {
-		decoded = preprocess_and_decode_codec_data(userdata, read_data, size_read);
+		decoded = preprocess_and_decode_codec_data(userdata, read_data, size_read, SPA_TIMESPEC_TO_NSEC(&this->now));
 	} else {
 		uint32_t avail;
 		uint8_t *packet;
@@ -591,7 +591,7 @@ static int sco_source_cb(void *userdata, uint8_t *read_data, int size_read)
 		packet = spa_bt_decode_buffer_get_write(&port->buffer, &avail);
 		avail = SPA_MIN(avail, (uint32_t)size_read);
 		spa_memmove(packet, read_data, avail);
-		spa_bt_decode_buffer_write_packet(&port->buffer, avail);
+		spa_bt_decode_buffer_write_packet(&port->buffer, avail, SPA_TIMESPEC_TO_NSEC(&this->now));
 
 		decoded = avail;
 	}
@@ -728,8 +728,8 @@ static int transport_start(struct impl *this)
 			this->quantum_limit, this->quantum_limit)) < 0)
 		return res;
 
-	/* 40 ms max buffer */
-	spa_bt_decode_buffer_set_max_latency(&port->buffer,
+	/* 40 ms max buffer (on top of duration) */
+	spa_bt_decode_buffer_set_max_extra_latency(&port->buffer,
 			port->current_format.info.raw.rate * 40 / 1000);
 
 	/* Init mSBC/LC3 if needed */
@@ -1402,7 +1402,9 @@ static void process_buffering(struct impl *this)
 	void *buf;
 	uint32_t avail;
 
-	spa_bt_decode_buffer_process(&port->buffer, samples, duration);
+	spa_bt_decode_buffer_process(&port->buffer, samples, duration,
+			this->position ? this->position->clock.rate_diff : 1.0,
+			this->position ? this->position->clock.next_nsec : 0);
 
 	setup_matching(this);
 
diff --git a/spa/plugins/bluez5/telephony.c b/spa/plugins/bluez5/telephony.c
new file mode 100644
index 00000000..c2f9d674
--- /dev/null
+++ b/spa/plugins/bluez5/telephony.c
@@ -0,0 +1,1870 @@
+/* Spa Bluez5 Telephony D-Bus service */
+/* SPDX-FileCopyrightText: Copyright © 2024 Collabora Ltd. */
+/* SPDX-License-Identifier: MIT */
+
+#include "telephony.h"
+
+#include <errno.h>
+#include <stdbool.h>
+#include <string.h>
+#include <dbus/dbus.h>
+#include <spa-private/dbus-helpers.h>
+
+#include <spa/utils/list.h>
+#include <spa/utils/string.h>
+
+#define PW_TELEPHONY_SERVICE "org.pipewire.Telephony"
+
+#define PW_TELEPHONY_OBJECT_PATH "/org/pipewire/Telephony"
+
+#define PW_TELEPHONY_AG_IFACE "org.pipewire.Telephony.AudioGateway1"
+#define PW_TELEPHONY_AG_TRANSPORT_IFACE "org.pipewire.Telephony.AudioGatewayTransport1"
+#define PW_TELEPHONY_CALL_IFACE "org.pipewire.Telephony.Call1"
+
+#define OFONO_MANAGER_IFACE "org.ofono.Manager"
+#define OFONO_VOICE_CALL_MANAGER_IFACE "org.ofono.VoiceCallManager"
+#define OFONO_VOICE_CALL_IFACE "org.ofono.VoiceCall"
+
+#define DBUS_OBJECT_MANAGER_IFACE_INTROSPECT_XML				\
+	" <interface name='" DBUS_INTERFACE_OBJECT_MANAGER "'>"			\
+	"  <method name='GetManagedObjects'>"		 			\
+	"   <arg name='objects' direction='out' type='a{oa{sa{sv}}}'/>"		\
+	"  </method>"								\
+	"  <signal name='InterfacesAdded'>"					\
+	"   <arg name='object' type='o'/>"					\
+	"   <arg name='interfaces' type='a{sa{sv}}'/>"				\
+	"  </signal>"								\
+	"  <signal name='InterfacesRemoved'>"					\
+	"   <arg name='object' type='o'/>"					\
+	"   <arg name='interfaces' type='as'/>"	 				\
+	"  </signal>"								\
+	" </interface>"
+
+#define DBUS_PROPERTIES_IFACE_INTROSPECT_XML					\
+	" <interface name='" DBUS_INTERFACE_PROPERTIES "'>"			\
+	"  <method name='Get'>"							\
+	"   <arg name='interface' type='s' direction='in' />"			\
+	"   <arg name='name' type='s' direction='in' />"			\
+	"   <arg name='value' type='v' direction='out' />"			\
+	"  </method>"								\
+	"  <method name='Set'>"							\
+	"   <arg name='interface' type='s' direction='in' />"			\
+	"   <arg name='name' type='s' direction='in' />"			\
+	"   <arg name='value' type='v' direction='in' />"			\
+	"  </method>"								\
+	"  <method name='GetAll'>"						\
+	"   <arg name='interface' type='s' direction='in' />"			\
+	"   <arg name='properties' type='a{sv}' direction='out' />"		\
+	"  </method>"								\
+	"  <signal name='PropertiesChanged'>"					\
+	"   <arg name='interface' type='s' />"					\
+	"   <arg name='changed_properties' type='a{sv}' />"			\
+	"   <arg name='invalidated_properties' type='as' />"			\
+	"  </signal>"								\
+	" </interface>"
+
+#define DBUS_INTROSPECTABLE_IFACE_INTROSPECT_XML				\
+	" <interface name='" DBUS_INTERFACE_INTROSPECTABLE "'>"			\
+	"  <method name='Introspect'>"						\
+	"   <arg name='xml' type='s' direction='out'/>"				\
+	"  </method>"								\
+	" </interface>"
+
+#define PW_TELEPHONY_MANAGER_INTROSPECT_XML \
+	DBUS_INTROSPECT_1_0_XML_DOCTYPE_DECL_NODE				\
+	"<node>"		 						\
+	" <interface name='" OFONO_MANAGER_IFACE "'>"				\
+	"  <method name='GetModems'>"		 				\
+	"   <arg name='objects' direction='out' type='a{oa{sv}}'/>"		\
+	"  </method>"								\
+	"  <signal name='ModemAdded'>"						\
+	"   <arg name='path' type='o'/>"					\
+	"   <arg name='properties' type='a{sv}'/>"				\
+	"  </signal>"								\
+	"  <signal name='ModemRemoved'>"					\
+	"   <arg name='path' type='o'/>"					\
+	"  </signal>"								\
+	" </interface>"								\
+	DBUS_OBJECT_MANAGER_IFACE_INTROSPECT_XML				\
+	DBUS_INTROSPECTABLE_IFACE_INTROSPECT_XML				\
+	"</node>"
+
+#define PW_TELEPHONY_AG_COMMON_INTROSPECT_XML					\
+	"  <method name='Dial'>"						\
+	"   <arg name='number' direction='in' type='s'/>"			\
+	"  </method>"								\
+	"  <method name='SwapCalls'>"						\
+	"  </method>"								\
+	"  <method name='ReleaseAndAnswer'>"					\
+	"  </method>"								\
+	"  <method name='ReleaseAndSwap'>"					\
+	"  </method>"								\
+	"  <method name='HoldAndAnswer'>"					\
+	"  </method>"								\
+	"  <method name='HangupAll'>"						\
+	"  </method>"								\
+	"  <method name='CreateMultiparty'>"					\
+	"  </method>"								\
+	"  <method name='SendTones'>"						\
+	"   <arg name='tones' direction='in' type='s'/>"			\
+	"  </method>"
+
+#define PW_TELEPHONY_AG_INTROSPECT_XML \
+	DBUS_INTROSPECT_1_0_XML_DOCTYPE_DECL_NODE				\
+	"<node>"		 						\
+	" <interface name='" PW_TELEPHONY_AG_IFACE "'>"				\
+	PW_TELEPHONY_AG_COMMON_INTROSPECT_XML					\
+	"  <property name='Address' type='s' access='read'>"			\
+	"    <annotation name='org.freedesktop.DBus.Property.EmitsChangedSignal' value='const'/>" \
+	"  </property>"								\
+	" </interface>"								\
+	" <interface name='" PW_TELEPHONY_AG_TRANSPORT_IFACE "'>"		\
+	"  <property name='State' type='s' access='read'/>"			\
+	"  <property name='Codec' type='y' access='read'/>"			\
+	"  <property name='RejectSCO' type='b' access='readwrite'/>"		\
+	"  <method name='Activate'/>"						\
+	" </interface>"								\
+	" <interface name='" OFONO_VOICE_CALL_MANAGER_IFACE "'>"		\
+	PW_TELEPHONY_AG_COMMON_INTROSPECT_XML					\
+	"  <method name='GetCalls'>"		 				\
+	"   <arg name='objects' direction='out' type='a{oa{sv}}'/>"		\
+	"  </method>"								\
+	"  <signal name='CallAdded'>"						\
+	"   <arg name='path' type='o'/>"					\
+	"   <arg name='properties' type='a{sv}'/>"				\
+	"  </signal>"								\
+	"  <signal name='CallRemoved'>"						\
+	"   <arg name='path' type='o'/>"					\
+	"  </signal>"								\
+	" </interface>"								\
+	DBUS_OBJECT_MANAGER_IFACE_INTROSPECT_XML				\
+	DBUS_PROPERTIES_IFACE_INTROSPECT_XML					\
+	DBUS_INTROSPECTABLE_IFACE_INTROSPECT_XML				\
+	"</node>"
+
+#define PW_TELEPHONY_CALL_COMMON_INTROSPECT_XML					\
+	"  <method name='Answer'>"						\
+	"  </method>"								\
+	"  <method name='Hangup'>"						\
+	"  </method>"
+
+#define PW_TELEPHONY_CALL_INTROSPECT_XML \
+	DBUS_INTROSPECT_1_0_XML_DOCTYPE_DECL_NODE				\
+	"<node>"								\
+	" <interface name='" PW_TELEPHONY_CALL_IFACE "'>"			\
+	PW_TELEPHONY_CALL_COMMON_INTROSPECT_XML					\
+	"  <property name='LineIdentification' type='s' access='read'/>"	\
+	"  <property name='IncomingLine' type='s' access='read'/>"		\
+	"  <property name='Name' type='s' access='read'/>"			\
+	"  <property name='Multiparty' type='b' access='read'/>"		\
+	"  <property name='State' type='s' access='read'/>"			\
+	" </interface>"								\
+	" <interface name='" OFONO_VOICE_CALL_IFACE "'>"			\
+	PW_TELEPHONY_CALL_COMMON_INTROSPECT_XML					\
+	"  <method name='GetProperties'>"					\
+	"   <arg name='properties' type='a{sv}' direction='out' />"		\
+	"  </method>"								\
+	"  <signal name='PropertyChanged'>"					\
+	"   <arg name='property' type='s' />"					\
+	"   <arg name='value' type='v' />"					\
+	"  </signal>"								\
+	" </interface>"								\
+	DBUS_PROPERTIES_IFACE_INTROSPECT_XML					\
+	DBUS_INTROSPECTABLE_IFACE_INTROSPECT_XML				\
+	"</node>"
+
+SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.bluez5.telephony");
+#undef SPA_LOG_TOPIC_DEFAULT
+#define SPA_LOG_TOPIC_DEFAULT &log_topic
+
+struct callimpl;
+
+struct impl {
+	struct spa_bt_telephony this;
+
+	struct spa_log *log;
+	struct spa_dbus *dbus;
+
+	struct spa_dbus_connection *dbus_connection;
+	DBusConnection *conn;
+
+	const char *path;
+	struct spa_list ag_list;
+
+	bool default_reject_sco;
+};
+
+struct agimpl {
+	struct spa_bt_telephony_ag this;
+	struct spa_list link;
+	char *path;
+	struct spa_callbacks callbacks;
+	void *user_data;
+
+	bool dial_in_progress;
+	struct callimpl *dial_return;
+
+	struct {
+		struct spa_bt_telephony_ag_transport transport;
+	} prev;
+};
+
+struct callimpl {
+	struct spa_bt_telephony_call this;
+	char *path;
+	struct spa_callbacks callbacks;
+	void *user_data;
+
+	/* previous values of properties */
+	struct {
+		char *line_identification;
+		char *incoming_line;
+		char *name;
+		bool multiparty;
+		enum spa_bt_telephony_call_state state;
+	} prev;
+};
+
+#define ag_emit(ag,m,v,...) 				spa_callbacks_call(&ag->callbacks, struct spa_bt_telephony_ag_callbacks, m, v, ##__VA_ARGS__)
+#define ag_emit_dial(s,n,e,cme)				ag_emit(s,dial,0,n,e,cme)
+#define ag_emit_swap_calls(s,e,cme)			ag_emit(s,swap_calls,0,e,cme)
+#define ag_emit_release_and_answer(s,e,cme)	ag_emit(s,release_and_answer,0,e,cme)
+#define ag_emit_release_and_swap(s,e,cme)	ag_emit(s,release_and_swap,0,e,cme)
+#define ag_emit_hold_and_answer(s,e,cme)	ag_emit(s,hold_and_answer,0,e,cme)
+#define ag_emit_hangup_all(s,e,cme)			ag_emit(s,hangup_all,0,e,cme)
+#define ag_emit_create_multiparty(s,e,cme)	ag_emit(s,create_multiparty,0,e,cme)
+#define ag_emit_send_tones(s,t,e,cme)		ag_emit(s,send_tones,0,t,e,cme)
+#define ag_emit_transport_activate(s,e,cme) ag_emit(s,transport_activate,0,e,cme)
+
+#define call_emit(c,m,v,...) 	spa_callbacks_call(&c->callbacks, struct spa_bt_telephony_call_callbacks, m, v, ##__VA_ARGS__)
+#define call_emit_answer(s,e,cme)	call_emit(s,answer,0,e,cme)
+#define call_emit_hangup(s,e,cme)	call_emit(s,hangup,0,e,cme)
+
+static void dbus_iter_append_ag_interfaces(DBusMessageIter *i, struct spa_bt_telephony_ag *ag);
+static void dbus_iter_append_call_properties(DBusMessageIter *i, struct spa_bt_telephony_call *call, bool all);
+
+#define PW_TELEPHONY_ERROR_FAILED "org.pipewire.Telephony.Error.Failed"
+#define PW_TELEPHONY_ERROR_NOT_SUPPORTED "org.pipewire.Telephony.Error.NotSupported"
+#define PW_TELEPHONY_ERROR_INVALID_FORMAT "org.pipewire.Telephony.Error.InvalidFormat"
+#define PW_TELEPHONY_ERROR_INVALID_STATE "org.pipewire.Telephony.Error.InvalidState"
+#define PW_TELEPHONY_ERROR_IN_PROGRESS "org.pipewire.Telephony.Error.InProgress"
+#define PW_TELEPHONY_ERROR_CME "org.pipewire.Telephony.Error.CME"
+
+static const char *telephony_error_to_dbus (enum spa_bt_telephony_error err)
+{
+	switch (err) {
+	case BT_TELEPHONY_ERROR_FAILED:
+		return PW_TELEPHONY_ERROR_FAILED;
+	case BT_TELEPHONY_ERROR_NOT_SUPPORTED:
+		return PW_TELEPHONY_ERROR_NOT_SUPPORTED;
+	case BT_TELEPHONY_ERROR_INVALID_FORMAT:
+		return PW_TELEPHONY_ERROR_INVALID_FORMAT;
+	case BT_TELEPHONY_ERROR_INVALID_STATE:
+		return PW_TELEPHONY_ERROR_INVALID_STATE;
+	case BT_TELEPHONY_ERROR_IN_PROGRESS:
+		return PW_TELEPHONY_ERROR_IN_PROGRESS;
+	case BT_TELEPHONY_ERROR_CME:
+		return PW_TELEPHONY_ERROR_CME;
+	default:
+		return "";
+	}
+}
+
+static const char *telephony_error_to_description (enum spa_bt_telephony_error err, uint8_t cme_error)
+{
+	switch (err) {
+	case BT_TELEPHONY_ERROR_FAILED:
+		return "Method call failed";
+	case BT_TELEPHONY_ERROR_NOT_SUPPORTED:
+		return "Method is not supported on this Audio Gateway";
+	case BT_TELEPHONY_ERROR_INVALID_FORMAT:
+		return "Invalid phone number or tones";
+	case BT_TELEPHONY_ERROR_INVALID_STATE:
+		return "The current state does not allow this method call";
+	case BT_TELEPHONY_ERROR_IN_PROGRESS:
+		return "Command already in progress";
+	case BT_TELEPHONY_ERROR_CME:
+		switch (cme_error) {
+		case 0: return "AG failure";
+		case 1: return "no connection to phone";
+		case 3: return "operation not allowed";
+		case 4: return "operation not supported";
+		case 5: return "PH-SIM PIN required";
+		case 10: return "SIM not inserted";
+		case 11: return "SIM PIN required";
+		case 12: return "SIM PUK required";
+		case 13: return "SIM failure";
+		case 14: return "SIM busy";
+		case 16: return "incorrect password";
+		case 17: return "SIM PIN2 required";
+		case 18: return "SIM PUK2 required";
+		case 20: return "memory full";
+		case 21: return "invalid index";
+		case 23: return "memory failure";
+		case 24: return "text string too long";
+		case 25: return "invalid characters in text string";
+		case 26: return "dial string too long";
+		case 27: return "invalid characters in dial string";
+		case 30: return "no network service";
+		case 31: return "network Timeout";
+		case 32: return "network not allowed - Emergency calls only";
+		default: return "Unknown CME error";
+		}
+	default:
+		return "";
+	}
+}
+
+#define find_free_object_id(list, obj_type, link)	\
+({						\
+	int id = 1;				\
+	obj_type *object;			\
+	spa_list_for_each(object, list, link) {	\
+		if (object->this.id <= id)		\
+			id = object->this.id + 1;	\
+	}					\
+	id;					\
+})
+
+static DBusMessage *manager_introspect(struct impl *impl, DBusMessage *m)
+{
+	const char *xml = PW_TELEPHONY_MANAGER_INTROSPECT_XML;
+	spa_autoptr(DBusMessage) r = NULL;
+	if ((r = dbus_message_new_method_return(m)) == NULL)
+		return NULL;
+	if (!dbus_message_append_args(r, DBUS_TYPE_STRING, &xml, DBUS_TYPE_INVALID))
+		return NULL;
+	return spa_steal_ptr(r);
+}
+
+static DBusMessage *manager_get_managed_objects(struct impl *impl, DBusMessage *m, bool ofono_compat)
+{
+	struct agimpl *agimpl;
+	spa_autoptr(DBusMessage) r = NULL;
+	DBusMessageIter iter, array1, entry1, props_dict;
+
+	if ((r = dbus_message_new_method_return(m)) == NULL)
+		return NULL;
+
+	dbus_message_iter_init_append(r, &iter);
+	dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY,
+		ofono_compat ? "{oa{sv}}" : "{oa{sa{sv}}}", &array1);
+
+	spa_list_for_each (agimpl, &impl->ag_list, link) {
+		if (agimpl->path) {
+			dbus_message_iter_open_container(&array1, DBUS_TYPE_DICT_ENTRY, NULL, &entry1);
+			if (ofono_compat) {
+				dbus_message_iter_append_basic(&entry1, DBUS_TYPE_OBJECT_PATH, &agimpl->path);
+				dbus_message_iter_open_container(&entry1, DBUS_TYPE_ARRAY, "{sv}", &props_dict);
+				dbus_message_iter_close_container(&entry1, &props_dict);
+			} else {
+				dbus_iter_append_ag_interfaces(&entry1, &agimpl->this);
+			}
+			dbus_message_iter_close_container(&array1, &entry1);
+		}
+	}
+	dbus_message_iter_close_container(&iter, &array1);
+
+	return spa_steal_ptr(r);
+}
+
+static DBusHandlerResult manager_handler(DBusConnection *c, DBusMessage *m, void *userdata)
+{
+	struct impl *impl = userdata;
+
+	spa_autoptr(DBusMessage) r = NULL;
+	const char *path, *interface, *member;
+
+	path = dbus_message_get_path(m);
+	interface = dbus_message_get_interface(m);
+	member = dbus_message_get_member(m);
+
+	spa_log_debug(impl->log, "dbus: path=%s, interface=%s, member=%s", path, interface, member);
+
+	if (dbus_message_is_method_call(m, DBUS_INTERFACE_INTROSPECTABLE, "Introspect")) {
+		r = manager_introspect(impl, m);
+	} else if (dbus_message_is_method_call(m, DBUS_INTERFACE_OBJECT_MANAGER, "GetManagedObjects")) {
+		r = manager_get_managed_objects(impl, m, false);
+	} else if (dbus_message_is_method_call(m, OFONO_MANAGER_IFACE, "GetModems")) {
+		r = manager_get_managed_objects(impl, m, true);
+	} else {
+		return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+	}
+
+	if (r == NULL)
+		return DBUS_HANDLER_RESULT_NEED_MEMORY;
+	if (!dbus_connection_send(impl->conn, r, NULL))
+		return DBUS_HANDLER_RESULT_NEED_MEMORY;
+	return DBUS_HANDLER_RESULT_HANDLED;
+}
+
+struct spa_bt_telephony *
+telephony_new(struct spa_log *log, struct spa_dbus *dbus, const struct spa_dict *info)
+{
+	struct impl *impl = NULL;
+	spa_auto(DBusError) err = DBUS_ERROR_INIT;
+	bool service_enabled = true;
+	bool ofono_service_compat = false;
+	enum spa_dbus_type bus_type = SPA_DBUS_TYPE_SESSION;
+	int res;
+
+	static const DBusObjectPathVTable vtable_manager = {
+		.message_function = manager_handler,
+	};
+
+	spa_assert(log);
+	spa_assert(dbus);
+
+	spa_log_topic_init(log, &log_topic);
+
+	if (info) {
+		const char *str;
+		if ((str = spa_dict_lookup(info, "bluez5.telephony-dbus-service")) != NULL) {
+			service_enabled = spa_atob(str);
+		}
+		if ((str = spa_dict_lookup(info, "bluez5.telephony.use-system-bus")) != NULL) {
+			bus_type = spa_atob(str) ? SPA_DBUS_TYPE_SYSTEM : SPA_DBUS_TYPE_SESSION;
+		}
+		if ((str = spa_dict_lookup(info, "bluez5.telephony.provide-ofono")) != NULL) {
+			ofono_service_compat = spa_atob(str);
+			bus_type = SPA_DBUS_TYPE_SYSTEM;
+		}
+	}
+
+	if (!service_enabled) {
+		spa_log_info(log, "Bluetooth Telephony service disabled by configuration");
+		return NULL;
+	}
+
+	impl = calloc(1, sizeof(*impl));
+	if (impl == NULL)
+		return NULL;
+
+	impl->log = log;
+	impl->dbus = dbus;
+	impl->ag_list = SPA_LIST_INIT(&impl->ag_list);
+
+	impl->dbus_connection = spa_dbus_get_connection(impl->dbus, bus_type);
+	if (impl->dbus_connection == NULL) {
+		spa_log_warn(impl->log, "no session dbus connection");
+		goto fail;
+	}
+	impl->conn = spa_dbus_connection_get(impl->dbus_connection);
+	if (impl->conn == NULL) {
+		spa_log_warn(impl->log, "failed to get session dbus connection");
+		goto fail;
+	}
+
+	impl->default_reject_sco = false;
+	if (info) {
+		const char *str;
+		if ((str = spa_dict_lookup(info, "bluez5.telephony.default-reject-sco")) != NULL) {
+			impl->default_reject_sco = spa_atob(str);
+		}
+	}
+
+	/* XXX: We should handle spa_dbus reconnecting, but we don't, so ref
+	 * XXX: the handle so that we can keep it if spa_dbus unrefs it.
+	 */
+	dbus_connection_ref(impl->conn);
+
+	res = dbus_bus_request_name(impl->conn,
+				    ofono_service_compat ? OFONO_SERVICE : PW_TELEPHONY_SERVICE,
+				    DBUS_NAME_FLAG_DO_NOT_QUEUE, &err);
+	if (res < 0) {
+		spa_log_warn(impl->log, "D-Bus RequestName() error: %s", err.message);
+		goto fail;
+	}
+	if (res == DBUS_REQUEST_NAME_REPLY_EXISTS) {
+		spa_log_warn(impl->log, "Bluetooth Telephony service is already registered by another connection");
+		goto fail;
+	}
+
+	impl->path = ofono_service_compat ? "/" : PW_TELEPHONY_OBJECT_PATH;
+
+	if (!dbus_connection_register_object_path(impl->conn, impl->path,
+						  &vtable_manager, impl)) {
+		goto fail;
+	}
+
+	return &impl->this;
+
+fail:
+	spa_log_info(impl->log, "Bluetooth Telephony service disabled due to failure");
+	if (impl->conn)
+		dbus_connection_unref(impl->conn);
+	if (impl->dbus_connection)
+		spa_dbus_connection_destroy(impl->dbus_connection);
+	free(impl);
+	return NULL;
+}
+
+void telephony_free(struct spa_bt_telephony *telephony)
+{
+	struct impl *impl = SPA_CONTAINER_OF(telephony, struct impl, this);
+	struct agimpl *agimpl;
+
+	spa_list_consume (agimpl, &impl->ag_list, link) {
+		telephony_ag_destroy(&agimpl->this);
+	}
+
+	dbus_connection_unref(impl->conn);
+	spa_dbus_connection_destroy(impl->dbus_connection);
+	impl->dbus_connection = NULL;
+	impl->conn = NULL;
+
+	free(impl);
+}
+
+static void telephony_ag_transport_commit_properties(struct spa_bt_telephony_ag *ag)
+{
+	struct agimpl *agimpl = SPA_CONTAINER_OF(ag, struct agimpl, this);
+	agimpl->prev.transport = ag->transport;
+}
+
+static const char * const * transport_state_to_string(int state)
+{
+	static const char * const state_str[] = {
+		"error",
+		"idle",
+		"pending",
+		"active",
+	};
+	if (state < -1 || state > 2)
+		state = -1;
+	return &state_str[state + 1];
+}
+
+static bool
+dbus_iter_append_ag_properties(DBusMessageIter *i, struct spa_bt_telephony_ag *ag, bool all)
+{
+	DBusMessageIter dict, entry, variant;
+	bool changed = false;
+
+	dbus_message_iter_open_container(i, DBUS_TYPE_ARRAY, "{sv}", &dict);
+
+	/* Address must be set before registering and never changes,
+	   so there is no need to check for changes here */
+	if (all) {
+		dbus_message_iter_open_container(&dict, DBUS_TYPE_DICT_ENTRY, NULL, &entry);
+		const char *name = "Address";
+		dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &name);
+		dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT,
+						DBUS_TYPE_STRING_AS_STRING,
+						&variant);
+		dbus_message_iter_append_basic(&variant, DBUS_TYPE_STRING, &ag->address);
+		dbus_message_iter_close_container(&entry, &variant);
+		dbus_message_iter_close_container(&dict, &entry);
+		changed = true;
+	}
+
+	dbus_message_iter_close_container(i, &dict);
+	return changed;
+}
+
+static bool
+dbus_iter_append_ag_transport_properties(DBusMessageIter *i, struct spa_bt_telephony_ag *ag, bool all)
+{
+	struct agimpl *agimpl = SPA_CONTAINER_OF(ag, struct agimpl, this);
+	DBusMessageIter dict, entry, variant;
+	bool changed = false;
+
+	dbus_message_iter_open_container(i, DBUS_TYPE_ARRAY, "{sv}", &dict);
+
+	if (all || ag->transport.codec != agimpl->prev.transport.codec) {
+		dbus_message_iter_open_container(&dict, DBUS_TYPE_DICT_ENTRY, NULL, &entry);
+		const char *name = "Codec";
+		dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &name);
+		dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT,
+						DBUS_TYPE_BYTE_AS_STRING,
+						&variant);
+		dbus_message_iter_append_basic(&variant, DBUS_TYPE_BYTE, &ag->transport.codec);
+		dbus_message_iter_close_container(&entry, &variant);
+		dbus_message_iter_close_container(&dict, &entry);
+		changed = true;
+	}
+
+	if (all || ag->transport.state != agimpl->prev.transport.state) {
+		dbus_message_iter_open_container(&dict, DBUS_TYPE_DICT_ENTRY, NULL, &entry);
+		const char *name = "State";
+		dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &name);
+		dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT,
+						DBUS_TYPE_STRING_AS_STRING,
+						&variant);
+		dbus_message_iter_append_basic(&variant, DBUS_TYPE_STRING,
+						transport_state_to_string(ag->transport.state));
+		dbus_message_iter_close_container(&entry, &variant);
+		dbus_message_iter_close_container(&dict, &entry);
+		changed = true;
+	}
+
+	if (all || ag->transport.rejectSCO != agimpl->prev.transport.rejectSCO) {
+		dbus_message_iter_open_container(&dict, DBUS_TYPE_DICT_ENTRY, NULL, &entry);
+		const char *name = "RejectSCO";
+		dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &name);
+		dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT,
+						DBUS_TYPE_BOOLEAN_AS_STRING,
+						&variant);
+		dbus_message_iter_append_basic(&variant, DBUS_TYPE_BOOLEAN,
+						&ag->transport.rejectSCO);
+		dbus_message_iter_close_container(&entry, &variant);
+		dbus_message_iter_close_container(&dict, &entry);
+		changed = true;
+	}
+
+	dbus_message_iter_close_container(i, &dict);
+	return changed;
+}
+
+static void
+dbus_iter_append_ag_interfaces(DBusMessageIter *i, struct spa_bt_telephony_ag *ag)
+{
+	struct agimpl *agimpl = SPA_CONTAINER_OF(ag, struct agimpl, this);
+	DBusMessageIter entry, dict;
+
+	dbus_message_iter_append_basic(i, DBUS_TYPE_OBJECT_PATH, &agimpl->path);
+	dbus_message_iter_open_container(i, DBUS_TYPE_ARRAY, "{sa{sv}}", &dict);
+
+	const char *interface = PW_TELEPHONY_AG_IFACE;
+	dbus_message_iter_open_container(&dict, DBUS_TYPE_DICT_ENTRY, NULL, &entry);
+	dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &interface);
+	dbus_iter_append_ag_properties(&entry, ag, true);
+	dbus_message_iter_close_container(&dict, &entry);
+
+	const char *interface2 = PW_TELEPHONY_AG_TRANSPORT_IFACE;
+	dbus_message_iter_open_container(&dict, DBUS_TYPE_DICT_ENTRY, NULL, &entry);
+	dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &interface2);
+	dbus_iter_append_ag_transport_properties(&entry, ag, true);
+	dbus_message_iter_close_container(&dict, &entry);
+
+	dbus_message_iter_close_container(i, &dict);
+}
+
+static DBusMessage *ag_introspect(struct agimpl *agimpl, DBusMessage *m)
+{
+	const char *xml = PW_TELEPHONY_AG_INTROSPECT_XML;
+	spa_autoptr(DBusMessage) r = NULL;
+	if ((r = dbus_message_new_method_return(m)) == NULL)
+		return NULL;
+	if (!dbus_message_append_args(r, DBUS_TYPE_STRING, &xml, DBUS_TYPE_INVALID))
+		return NULL;
+	return spa_steal_ptr(r);
+}
+
+static DBusMessage *ag_get_managed_objects(struct agimpl *agimpl, DBusMessage *m, bool ofono_compat)
+{
+	struct callimpl *callimpl;
+	spa_autoptr(DBusMessage) r = NULL;
+	DBusMessageIter iter, array1, entry1, array2, entry2;
+	const char *interface = PW_TELEPHONY_CALL_IFACE;
+
+	if ((r = dbus_message_new_method_return(m)) == NULL)
+		return NULL;
+
+	dbus_message_iter_init_append(r, &iter);
+	dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY,
+		ofono_compat ? "{oa{sv}}" : "{oa{sa{sv}}}", &array1);
+
+	spa_list_for_each (callimpl, &agimpl->this.call_list, this.link) {
+		dbus_message_iter_open_container(&array1, DBUS_TYPE_DICT_ENTRY, NULL, &entry1);
+		dbus_message_iter_append_basic(&entry1, DBUS_TYPE_OBJECT_PATH, &callimpl->path);
+		if (ofono_compat) {
+			dbus_iter_append_call_properties(&entry1, &callimpl->this, true);
+		} else {
+			dbus_message_iter_open_container(&entry1, DBUS_TYPE_ARRAY, "{sa{sv}}", &array2);
+			dbus_message_iter_open_container(&array2, DBUS_TYPE_DICT_ENTRY, NULL, &entry2);
+			dbus_message_iter_append_basic(&entry2, DBUS_TYPE_STRING, &interface);
+			dbus_iter_append_call_properties(&entry2, &callimpl->this, true);
+			dbus_message_iter_close_container(&array2, &entry2);
+			dbus_message_iter_close_container(&entry1, &array2);
+		}
+		dbus_message_iter_close_container(&array1, &entry1);
+	}
+	dbus_message_iter_close_container(&iter, &array1);
+
+	return spa_steal_ptr(r);
+}
+
+static DBusMessage *ag_properties_get(struct agimpl *agimpl, DBusMessage *m)
+{
+	const char *iface, *name;
+	DBusMessage *r;
+	DBusMessageIter i, v;
+
+	if (!dbus_message_get_args(m, NULL,
+				DBUS_TYPE_STRING, &iface,
+				DBUS_TYPE_STRING, &name,
+				DBUS_TYPE_INVALID))
+		return NULL;
+
+	if (spa_streq(iface, PW_TELEPHONY_AG_IFACE)) {
+		if (spa_streq(name, "Address")) {
+			r = dbus_message_new_method_return(m);
+			if (r == NULL)
+				return NULL;
+			dbus_message_iter_init_append(r, &i);
+			dbus_message_iter_open_container(&i, DBUS_TYPE_VARIANT,
+					DBUS_TYPE_STRING_AS_STRING, &v);
+			dbus_message_iter_append_basic(&v, DBUS_TYPE_STRING,
+					&agimpl->this.address);
+			dbus_message_iter_close_container(&i, &v);
+			return r;
+		}
+	} else if (spa_streq(iface, PW_TELEPHONY_AG_TRANSPORT_IFACE)) {
+		if (spa_streq(name, "Codec")) {
+			r = dbus_message_new_method_return(m);
+			if (r == NULL)
+				return NULL;
+			dbus_message_iter_init_append(r, &i);
+			dbus_message_iter_open_container(&i, DBUS_TYPE_VARIANT,
+					DBUS_TYPE_BYTE_AS_STRING, &v);
+			dbus_message_iter_append_basic(&v, DBUS_TYPE_BYTE,
+					&agimpl->this.transport.codec);
+			dbus_message_iter_close_container(&i, &v);
+			return r;
+		} else if (spa_streq(name, "State")) {
+			r = dbus_message_new_method_return(m);
+			if (r == NULL)
+				return NULL;
+			dbus_message_iter_init_append(r, &i);
+			dbus_message_iter_open_container(&i, DBUS_TYPE_VARIANT,
+					DBUS_TYPE_STRING_AS_STRING, &v);
+			dbus_message_iter_append_basic(&v, DBUS_TYPE_STRING,
+					transport_state_to_string(agimpl->this.transport.state));
+			dbus_message_iter_close_container(&i, &v);
+			return r;
+		} else if (spa_streq(name, "RejectSCO")) {
+			r = dbus_message_new_method_return(m);
+			if (r == NULL)
+				return NULL;
+			dbus_message_iter_init_append(r, &i);
+			dbus_message_iter_open_container(&i, DBUS_TYPE_VARIANT,
+					DBUS_TYPE_BOOLEAN_AS_STRING, &v);
+			dbus_message_iter_append_basic(&v, DBUS_TYPE_BOOLEAN,
+					&agimpl->this.transport.rejectSCO);
+			dbus_message_iter_close_container(&i, &v);
+			return r;
+		}
+	} else {
+		return dbus_message_new_error(m, DBUS_ERROR_UNKNOWN_INTERFACE,
+				"No such interface");
+	}
+
+	return dbus_message_new_error(m, DBUS_ERROR_UNKNOWN_PROPERTY,
+			"No such property");
+}
+
+static DBusMessage *ag_properties_get_all(struct agimpl *agimpl, DBusMessage *m)
+{
+	DBusMessage *r;
+	DBusMessageIter i;
+	const char *iface;
+
+	if (!dbus_message_get_args(m, NULL,
+				DBUS_TYPE_STRING, &iface,
+				DBUS_TYPE_INVALID))
+		return NULL;
+
+	if (spa_streq(iface, PW_TELEPHONY_AG_IFACE)) {
+		r = dbus_message_new_method_return(m);
+		if (r == NULL)
+			return NULL;
+		dbus_message_iter_init_append(r, &i);
+		dbus_iter_append_ag_properties(&i, &agimpl->this, true);
+		return r;
+	} else if (spa_streq(iface, PW_TELEPHONY_AG_TRANSPORT_IFACE)) {
+		r = dbus_message_new_method_return(m);
+		if (r == NULL)
+			return NULL;
+		dbus_message_iter_init_append(r, &i);
+		dbus_iter_append_ag_transport_properties(&i, &agimpl->this, true);
+		return r;
+	} else {
+		return dbus_message_new_error(m, DBUS_ERROR_UNKNOWN_INTERFACE,
+				"No such interface");
+	}
+}
+
+static DBusMessage *ag_properties_set(struct agimpl *agimpl, DBusMessage *m)
+{
+	const char *iface, *name;
+	DBusMessageIter i, variant;
+
+	if (!dbus_message_get_args(m, NULL,
+				DBUS_TYPE_STRING, &iface,
+				DBUS_TYPE_STRING, &name,
+				DBUS_TYPE_INVALID))
+		return NULL;
+
+	if (spa_streq(iface, PW_TELEPHONY_AG_TRANSPORT_IFACE)) {
+		if (spa_streq(name, "RejectSCO")) {
+			dbus_message_iter_init(m, &i);
+			dbus_message_iter_next(&i); /* skip iface */
+			dbus_message_iter_next(&i); /* skip name */
+			dbus_message_iter_recurse(&i, &variant); /* value */
+			dbus_message_iter_get_basic(&variant, &agimpl->this.transport.rejectSCO);
+			return dbus_message_new_method_return(m);
+		}
+	}
+
+	return dbus_message_new_error(m, DBUS_ERROR_PROPERTY_READ_ONLY,
+			"Property not writable");
+}
+
+static bool validate_phone_number(const char *number)
+{
+	const char *c;
+	int count = 0;
+
+	if (!number)
+		return false;
+	for (c = number; *c != '\0'; c++) {
+		if (!(*c >= '0' && *c <= '9') && !(*c >= 'A' && *c <= 'D') &&
+			*c != '#' && *c != '*' && *c != '+' && *c != ',' )
+			return false;
+		count++;
+	}
+	if (count < 1 || count > 80)
+		return false;
+	return true;
+}
+
+static bool validate_tones(const char *tones)
+{
+	const char *c;
+	if (!tones)
+		return false;
+	for (c = tones; *c != '\0'; c++) {
+		if (!(*c >= '0' && *c <= '9') && !(*c >= 'A' && *c <= 'D') &&
+				*c != '#' && *c != '*')
+			return false;
+	}
+	return true;
+}
+
+static DBusMessage *ag_dial(struct agimpl *agimpl, DBusMessage *m)
+{
+	const char *number = NULL;
+	enum spa_bt_telephony_error err = BT_TELEPHONY_ERROR_FAILED;
+	uint8_t cme_error;
+	spa_autoptr(DBusMessage) r = NULL;
+
+	if (!dbus_message_get_args(m, NULL,
+				DBUS_TYPE_STRING, &number,
+				DBUS_TYPE_INVALID))
+		return NULL;
+
+	if (!validate_phone_number(number)) {
+		err = BT_TELEPHONY_ERROR_INVALID_FORMAT;
+		goto failed;
+	}
+
+	agimpl->dial_in_progress = true;
+	if (!ag_emit_dial(agimpl, number, &err, &cme_error)) {
+		agimpl->dial_in_progress = false;
+		goto failed;
+	}
+	agimpl->dial_in_progress = false;
+
+	if (!agimpl->dial_return || !agimpl->dial_return->path)
+		err = BT_TELEPHONY_ERROR_FAILED;
+
+	if (err != BT_TELEPHONY_ERROR_NONE)
+		goto failed;
+
+	if ((r = dbus_message_new_method_return(m)) == NULL)
+		return NULL;
+	if (!dbus_message_append_args(r, DBUS_TYPE_OBJECT_PATH,
+			&agimpl->dial_return->path, DBUS_TYPE_INVALID))
+		return NULL;
+
+	agimpl->dial_return = NULL;
+
+	return spa_steal_ptr(r);
+
+failed:
+	return dbus_message_new_error(m, telephony_error_to_dbus (err),
+		telephony_error_to_description (err, cme_error));
+}
+
+static DBusMessage *ag_swap_calls(struct agimpl *agimpl, DBusMessage *m)
+{
+	enum spa_bt_telephony_error err = BT_TELEPHONY_ERROR_FAILED;
+	uint8_t cme_error;
+
+	if (ag_emit_swap_calls(agimpl, &err, &cme_error) && err == BT_TELEPHONY_ERROR_NONE)
+		return dbus_message_new_method_return(m);
+
+	return dbus_message_new_error(m, telephony_error_to_dbus (err),
+		telephony_error_to_description (err, cme_error));
+}
+
+static DBusMessage *ag_release_and_answer(struct agimpl *agimpl, DBusMessage *m)
+{
+	enum spa_bt_telephony_error err = BT_TELEPHONY_ERROR_FAILED;
+	uint8_t cme_error;
+
+	if (ag_emit_release_and_answer(agimpl, &err, &cme_error) && err == BT_TELEPHONY_ERROR_NONE)
+		return dbus_message_new_method_return(m);
+
+	return dbus_message_new_error(m, telephony_error_to_dbus (err),
+		telephony_error_to_description (err, cme_error));
+}
+
+static DBusMessage *ag_release_and_swap(struct agimpl *agimpl, DBusMessage *m)
+{
+	enum spa_bt_telephony_error err = BT_TELEPHONY_ERROR_FAILED;
+	uint8_t cme_error;
+
+	if (ag_emit_release_and_swap(agimpl, &err, &cme_error) && err == BT_TELEPHONY_ERROR_NONE)
+		return dbus_message_new_method_return(m);
+
+	return dbus_message_new_error(m, telephony_error_to_dbus (err),
+		telephony_error_to_description (err, cme_error));
+}
+
+static DBusMessage *ag_hold_and_answer(struct agimpl *agimpl, DBusMessage *m)
+{
+	enum spa_bt_telephony_error err = BT_TELEPHONY_ERROR_FAILED;
+	uint8_t cme_error;
+
+	if (ag_emit_hold_and_answer(agimpl, &err, &cme_error) && err == BT_TELEPHONY_ERROR_NONE)
+		return dbus_message_new_method_return(m);
+
+	return dbus_message_new_error(m, telephony_error_to_dbus (err),
+		telephony_error_to_description (err, cme_error));
+}
+
+static DBusMessage *ag_hangup_all(struct agimpl *agimpl, DBusMessage *m)
+{
+	enum spa_bt_telephony_error err = BT_TELEPHONY_ERROR_FAILED;
+	uint8_t cme_error;
+
+	if (ag_emit_hangup_all(agimpl, &err, &cme_error) && err == BT_TELEPHONY_ERROR_NONE)
+		return dbus_message_new_method_return(m);
+
+	return dbus_message_new_error(m, telephony_error_to_dbus (err),
+		telephony_error_to_description (err, cme_error));
+}
+
+static DBusMessage *ag_create_multiparty(struct agimpl *agimpl, DBusMessage *m)
+{
+	enum spa_bt_telephony_error err = BT_TELEPHONY_ERROR_FAILED;
+	uint8_t cme_error;
+
+	if (ag_emit_create_multiparty(agimpl, &err, &cme_error) && err == BT_TELEPHONY_ERROR_NONE)
+		return dbus_message_new_method_return(m);
+
+	return dbus_message_new_error(m, telephony_error_to_dbus (err),
+		telephony_error_to_description (err, cme_error));
+}
+
+static DBusMessage *ag_send_tones(struct agimpl *agimpl, DBusMessage *m)
+{
+	const char *tones = NULL;
+	enum spa_bt_telephony_error err = BT_TELEPHONY_ERROR_FAILED;
+	uint8_t cme_error;
+
+	if (!dbus_message_get_args(m, NULL,
+				DBUS_TYPE_STRING, &tones,
+				DBUS_TYPE_INVALID))
+		return NULL;
+
+	if (!validate_tones(tones)) {
+		err = BT_TELEPHONY_ERROR_INVALID_FORMAT;
+		goto failed;
+	}
+
+	if (ag_emit_send_tones(agimpl, tones, &err, &cme_error) && err == BT_TELEPHONY_ERROR_NONE)
+		return dbus_message_new_method_return(m);
+
+failed:
+	return dbus_message_new_error(m, telephony_error_to_dbus (err),
+		telephony_error_to_description (err, cme_error));
+}
+
+static DBusMessage *ag_transport_activate(struct agimpl *agimpl, DBusMessage *m)
+{
+	enum spa_bt_telephony_error err = BT_TELEPHONY_ERROR_FAILED;
+	uint8_t cme_error;
+
+	if (ag_emit_transport_activate(agimpl, &err, &cme_error) && err == BT_TELEPHONY_ERROR_NONE)
+		return dbus_message_new_method_return(m);
+
+	return dbus_message_new_error(m, telephony_error_to_dbus (err),
+		telephony_error_to_description (err, cme_error));
+}
+
+static DBusHandlerResult ag_handler(DBusConnection *c, DBusMessage *m, void *userdata)
+{
+	struct agimpl *agimpl = userdata;
+	struct impl *impl = SPA_CONTAINER_OF(agimpl->this.telephony, struct impl, this);
+
+	spa_autoptr(DBusMessage) r = NULL;
+	const char *path, *interface, *member;
+
+	path = dbus_message_get_path(m);
+	interface = dbus_message_get_interface(m);
+	member = dbus_message_get_member(m);
+
+	spa_log_debug(impl->log, "dbus: path=%s, interface=%s, member=%s", path, interface, member);
+
+	if (dbus_message_is_method_call(m, DBUS_INTERFACE_INTROSPECTABLE, "Introspect")) {
+		r = ag_introspect(agimpl, m);
+	} else if (dbus_message_is_method_call(m, DBUS_INTERFACE_OBJECT_MANAGER, "GetManagedObjects")) {
+		r = ag_get_managed_objects(agimpl, m, false);
+	} else if (dbus_message_is_method_call(m, DBUS_INTERFACE_PROPERTIES, "Get")) {
+		r = ag_properties_get(agimpl, m);
+	} else if (dbus_message_is_method_call(m, DBUS_INTERFACE_PROPERTIES, "GetAll")) {
+		r = ag_properties_get_all(agimpl, m);
+	} else if (dbus_message_is_method_call(m, DBUS_INTERFACE_PROPERTIES, "Set")) {
+		r = ag_properties_set(agimpl, m);
+	} else if (dbus_message_is_method_call(m, PW_TELEPHONY_AG_IFACE, "Dial") ||
+		   dbus_message_is_method_call(m, OFONO_VOICE_CALL_MANAGER_IFACE, "Dial")) {
+		r = ag_dial(agimpl, m);
+	} else if (dbus_message_is_method_call(m, PW_TELEPHONY_AG_IFACE, "SwapCalls") ||
+		   dbus_message_is_method_call(m, OFONO_VOICE_CALL_MANAGER_IFACE, "SwapCalls")) {
+		r = ag_swap_calls(agimpl, m);
+	} else if (dbus_message_is_method_call(m, PW_TELEPHONY_AG_IFACE, "ReleaseAndAnswer") ||
+		   dbus_message_is_method_call(m, OFONO_VOICE_CALL_MANAGER_IFACE, "ReleaseAndAnswer")) {
+		r = ag_release_and_answer(agimpl, m);
+	} else if (dbus_message_is_method_call(m, PW_TELEPHONY_AG_IFACE, "ReleaseAndSwap") ||
+		   dbus_message_is_method_call(m, OFONO_VOICE_CALL_MANAGER_IFACE, "ReleaseAndSwap")) {
+		r = ag_release_and_swap(agimpl, m);
+	} else if (dbus_message_is_method_call(m, PW_TELEPHONY_AG_IFACE, "HoldAndAnswer") ||
+		   dbus_message_is_method_call(m, OFONO_VOICE_CALL_MANAGER_IFACE, "HoldAndAnswer")) {
+		r = ag_hold_and_answer(agimpl, m);
+	} else if (dbus_message_is_method_call(m, PW_TELEPHONY_AG_IFACE, "HangupAll") ||
+		   dbus_message_is_method_call(m, OFONO_VOICE_CALL_MANAGER_IFACE, "HangupAll")) {
+		r = ag_hangup_all(agimpl, m);
+	} else if (dbus_message_is_method_call(m, PW_TELEPHONY_AG_IFACE, "CreateMultiparty") ||
+		   dbus_message_is_method_call(m, OFONO_VOICE_CALL_MANAGER_IFACE, "CreateMultiparty")) {
+		r = ag_create_multiparty(agimpl, m);
+	} else if (dbus_message_is_method_call(m, PW_TELEPHONY_AG_IFACE, "SendTones") ||
+		   dbus_message_is_method_call(m, OFONO_VOICE_CALL_MANAGER_IFACE, "SendTones")) {
+		r = ag_send_tones(agimpl, m);
+	} else if (dbus_message_is_method_call(m, OFONO_VOICE_CALL_MANAGER_IFACE, "GetCalls")) {
+		r = ag_get_managed_objects(agimpl, m, true);
+	} else if (dbus_message_is_method_call(m, PW_TELEPHONY_AG_TRANSPORT_IFACE, "Activate")) {
+		r = ag_transport_activate(agimpl, m);
+	} else {
+		return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+	}
+
+	if (r == NULL)
+		return DBUS_HANDLER_RESULT_NEED_MEMORY;
+	if (!dbus_connection_send(impl->conn, r, NULL))
+		return DBUS_HANDLER_RESULT_NEED_MEMORY;
+	return DBUS_HANDLER_RESULT_HANDLED;
+}
+
+struct spa_bt_telephony_ag *
+telephony_ag_new(struct spa_bt_telephony *telephony, size_t user_data_size)
+{
+	struct impl *impl = SPA_CONTAINER_OF(telephony, struct impl, this);
+	struct agimpl *agimpl;
+
+	spa_assert(user_data_size < SIZE_MAX - sizeof(*agimpl));
+
+	agimpl = calloc(1, sizeof(*agimpl) + user_data_size);
+	if (agimpl == NULL)
+		return NULL;
+
+	agimpl->this.telephony = telephony;
+	agimpl->this.id = find_free_object_id(&impl->ag_list, struct agimpl, link);
+	spa_list_init(&agimpl->this.call_list);
+
+	spa_list_append(&impl->ag_list, &agimpl->link);
+
+	if (user_data_size > 0)
+		agimpl->user_data = SPA_PTROFF(agimpl, sizeof(struct agimpl), void);
+
+	agimpl->this.transport.rejectSCO = impl->default_reject_sco;
+
+	return &agimpl->this;
+}
+
+void telephony_ag_destroy(struct spa_bt_telephony_ag *ag)
+{
+	struct agimpl *agimpl = SPA_CONTAINER_OF(ag, struct agimpl, this);
+	struct callimpl *callimpl;
+
+	spa_list_consume (callimpl, &agimpl->this.call_list, this.link) {
+		telephony_call_destroy(&callimpl->this);
+	}
+
+	telephony_ag_unregister(ag);
+	spa_list_remove(&agimpl->link);
+
+	free(ag->address);
+
+	free(agimpl);
+}
+
+void *telephony_ag_get_user_data(struct spa_bt_telephony_ag *ag)
+{
+	struct agimpl *agimpl = SPA_CONTAINER_OF(ag, struct agimpl, this);
+	return agimpl->user_data;
+}
+
+int telephony_ag_register(struct spa_bt_telephony_ag *ag)
+{
+	struct agimpl *agimpl = SPA_CONTAINER_OF(ag, struct agimpl, this);
+	struct impl *impl = SPA_CONTAINER_OF(agimpl->this.telephony, struct impl, this);
+	char *path;
+
+	const DBusObjectPathVTable vtable = {
+		.message_function = ag_handler,
+	};
+
+	path = spa_aprintf (PW_TELEPHONY_OBJECT_PATH "/ag%d", agimpl->this.id);
+
+	/* register object */
+	if (!dbus_connection_register_object_path(impl->conn, path, &vtable, agimpl)) {
+		spa_log_error(impl->log, "failed to register %s", path);
+		return -EIO;
+	}
+	agimpl->path = strdup(path);
+
+	/* notify on ObjectManager of the Manager object */
+	{
+		spa_autoptr(DBusMessage) msg = NULL;
+		DBusMessageIter iter;
+
+		msg = dbus_message_new_signal(impl->path, DBUS_INTERFACE_OBJECT_MANAGER,
+						"InterfacesAdded");
+		dbus_message_iter_init_append(msg, &iter);
+		dbus_iter_append_ag_interfaces(&iter, ag);
+
+		if (!dbus_connection_send(impl->conn, msg, NULL)) {
+			spa_log_error(impl->log, "failed to send InterfacesAdded for %s", path);
+			telephony_ag_unregister(ag);
+			return -EIO;
+		}
+	}
+
+	/* emit ModemAdded on the Manager object */
+	{
+		spa_autoptr(DBusMessage) msg = NULL;
+		DBusMessageIter iter, props_dict;
+
+		msg = dbus_message_new_signal(impl->path, OFONO_MANAGER_IFACE,
+						"ModemAdded");
+		dbus_message_iter_init_append(msg, &iter);
+		dbus_message_iter_append_basic(&iter, DBUS_TYPE_OBJECT_PATH, &path);
+		dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{sv}", &props_dict);
+		dbus_message_iter_close_container(&iter, &props_dict);
+
+		if (!dbus_connection_send(impl->conn, msg, NULL)) {
+			spa_log_error(impl->log, "failed to send ModemAdded for %s", path);
+			telephony_ag_unregister(ag);
+			return -EIO;
+		}
+	}
+
+	spa_log_debug(impl->log, "registered AudioGateway: %s", path);
+
+	return 0;
+}
+
+void telephony_ag_unregister(struct spa_bt_telephony_ag *ag)
+{
+	struct agimpl *agimpl = SPA_CONTAINER_OF(ag, struct agimpl, this);
+	struct impl *impl = SPA_CONTAINER_OF(agimpl->this.telephony, struct impl, this);
+
+	if (!agimpl->path)
+		return;
+
+	spa_log_debug(impl->log, "removing AudioGateway: %s", agimpl->path);
+
+	{
+		spa_autoptr(DBusMessage) msg = NULL;
+		DBusMessageIter iter, entry;
+		const char *interface = PW_TELEPHONY_AG_IFACE;
+		const char *interface2 = PW_TELEPHONY_AG_TRANSPORT_IFACE;
+
+		msg = dbus_message_new_signal(impl->path, DBUS_INTERFACE_OBJECT_MANAGER,
+						"InterfacesRemoved");
+		dbus_message_iter_init_append(msg, &iter);
+		dbus_message_iter_append_basic(&iter, DBUS_TYPE_OBJECT_PATH, &agimpl->path);
+		dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY,
+						DBUS_TYPE_STRING_AS_STRING, &entry);
+		dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &interface);
+		dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &interface2);
+		dbus_message_iter_close_container(&iter, &entry);
+
+		if (!dbus_connection_send(impl->conn, msg, NULL)) {
+			spa_log_warn(impl->log, "sending InterfacesRemoved failed");
+		}
+	}
+	{
+		spa_autoptr(DBusMessage) msg = NULL;
+		DBusMessageIter iter;
+
+		msg = dbus_message_new_signal(impl->path, OFONO_MANAGER_IFACE,
+						"ModemRemoved");
+		dbus_message_iter_init_append(msg, &iter);
+		dbus_message_iter_append_basic(&iter, DBUS_TYPE_OBJECT_PATH, &agimpl->path);
+
+		if (!dbus_connection_send(impl->conn, msg, NULL)) {
+			spa_log_warn(impl->log, "sending ModemRemoved failed");
+		}
+	}
+
+	if (!dbus_connection_unregister_object_path(impl->conn, agimpl->path)) {
+		spa_log_warn(impl->log, "failed to unregister %s", agimpl->path);
+	}
+
+	free(agimpl->path);
+	agimpl->path = NULL;
+}
+
+/* send message to notify about property changes */
+void telephony_ag_transport_notify_updated_props(struct spa_bt_telephony_ag *ag)
+{
+	struct agimpl *agimpl = SPA_CONTAINER_OF(ag, struct agimpl, this);
+	struct impl *impl = SPA_CONTAINER_OF(agimpl->this.telephony, struct impl, this);
+
+	spa_autoptr(DBusMessage) msg = NULL;
+	const char *interface = PW_TELEPHONY_AG_TRANSPORT_IFACE;
+	DBusMessageIter i, a;
+
+	msg = dbus_message_new_signal(agimpl->path,
+				DBUS_INTERFACE_PROPERTIES,
+				"PropertiesChanged");
+
+	dbus_message_iter_init_append(msg, &i);
+	dbus_message_iter_append_basic(&i, DBUS_TYPE_STRING, &interface);
+
+	if (!dbus_iter_append_ag_transport_properties(&i, ag, false))
+		return;
+
+	dbus_message_iter_open_container(&i, DBUS_TYPE_ARRAY,
+			DBUS_TYPE_STRING_AS_STRING, &a);
+	dbus_message_iter_close_container(&i, &a);
+
+	if (!dbus_connection_send(impl->conn, msg, NULL)){
+		spa_log_warn(impl->log, "sending PropertiesChanged failed");
+	}
+
+	telephony_ag_transport_commit_properties(ag);
+}
+
+void telephony_ag_set_callbacks(struct spa_bt_telephony_ag *ag,
+	const struct spa_bt_telephony_ag_callbacks *cbs,
+	void *data)
+{
+	struct agimpl *agimpl = SPA_CONTAINER_OF(ag, struct agimpl, this);
+	agimpl->callbacks.funcs = cbs;
+	agimpl->callbacks.data = data;
+}
+
+struct spa_bt_telephony_call *
+telephony_call_new(struct spa_bt_telephony_ag *ag, size_t user_data_size)
+{
+	struct agimpl *agimpl = SPA_CONTAINER_OF(ag, struct agimpl, this);
+	struct callimpl *callimpl;
+
+	spa_assert(user_data_size < SIZE_MAX - sizeof(*callimpl));
+
+	callimpl = calloc(1, sizeof(*callimpl) + user_data_size);
+	if (callimpl == NULL)
+		return NULL;
+
+	callimpl->this.ag = ag;
+	callimpl->this.id = find_free_object_id(&ag->call_list, struct callimpl, this.link);
+
+	spa_list_append(&ag->call_list, &callimpl->this.link);
+
+	if (user_data_size > 0)
+		callimpl->user_data = SPA_PTROFF(callimpl, sizeof(struct callimpl), void);
+
+	/* mark this object as the return value of the Dial method */
+	if (agimpl->dial_in_progress)
+		agimpl->dial_return = callimpl;
+
+	return &callimpl->this;
+}
+
+void telephony_call_destroy(struct spa_bt_telephony_call *call)
+{
+	struct callimpl *callimpl = SPA_CONTAINER_OF(call, struct callimpl, this);
+
+	telephony_call_unregister(call);
+	spa_list_remove(&call->link);
+
+	free(callimpl->prev.line_identification);
+	free(callimpl->prev.incoming_line);
+	free(callimpl->prev.name);
+
+	free(call->line_identification);
+	free(call->incoming_line);
+	free(call->name);
+
+	free(callimpl);
+}
+
+void *telephony_call_get_user_data(struct spa_bt_telephony_call *call)
+{
+	struct callimpl *callimpl = SPA_CONTAINER_OF(call, struct callimpl, this);
+	return callimpl->user_data;
+}
+
+static void telephony_call_commit_properties(struct spa_bt_telephony_call *call)
+{
+	struct callimpl *callimpl = SPA_CONTAINER_OF(call, struct callimpl, this);
+
+	if (!spa_streq (call->line_identification, callimpl->prev.line_identification)) {
+		free(callimpl->prev.line_identification);
+		callimpl->prev.line_identification = call->line_identification ?
+			strdup (call->line_identification) : NULL;
+	}
+	if (!spa_streq (call->incoming_line, callimpl->prev.incoming_line)) {
+		free(callimpl->prev.incoming_line);
+		callimpl->prev.incoming_line = call->incoming_line ?
+			strdup (call->incoming_line) : NULL;
+	}
+	if (!spa_streq (call->name, callimpl->prev.name)) {
+		free(callimpl->prev.name);
+		callimpl->prev.name = call->name ? strdup (call->name) : NULL;
+	}
+	callimpl->prev.multiparty = call->multiparty;
+	callimpl->prev.state = call->state;
+}
+
+static const char * const call_state_to_string[] = {
+	"active",
+	"held",
+	"dialing",
+	"alerting",
+	"incoming",
+	"waiting",
+	"disconnected",
+};
+
+static inline const void *safe_string(char **str)
+{
+	static const char *empty_string = "";
+	return *str ? (const char **) str : &empty_string;
+}
+
+static void
+dbus_iter_append_call_properties(DBusMessageIter *i, struct spa_bt_telephony_call *call, bool all)
+{
+	struct callimpl *callimpl = SPA_CONTAINER_OF(call, struct callimpl, this);
+	DBusMessageIter dict, entry, variant;
+
+	dbus_message_iter_open_container(i, DBUS_TYPE_ARRAY, "{sv}", &dict);
+
+	if (all || !spa_streq (call->line_identification, callimpl->prev.line_identification)) {
+		dbus_message_iter_open_container(&dict, DBUS_TYPE_DICT_ENTRY, NULL,
+						&entry);
+		const char *line_identification = "LineIdentification";
+		dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &line_identification);
+		dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT,
+						DBUS_TYPE_STRING_AS_STRING, &variant);
+		dbus_message_iter_append_basic(&variant, DBUS_TYPE_STRING,
+						safe_string (&call->line_identification));
+		dbus_message_iter_close_container(&entry, &variant);
+		dbus_message_iter_close_container(&dict, &entry);
+	}
+
+	if (all || !spa_streq (call->incoming_line, callimpl->prev.incoming_line)) {
+		dbus_message_iter_open_container(&dict, DBUS_TYPE_DICT_ENTRY, NULL, &entry);
+		const char *incoming_line = "IncomingLine";
+		dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &incoming_line);
+		dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT,
+						DBUS_TYPE_STRING_AS_STRING,
+						&variant);
+		dbus_message_iter_append_basic(&variant, DBUS_TYPE_STRING,
+						safe_string (&call->incoming_line));
+		dbus_message_iter_close_container(&entry, &variant);
+		dbus_message_iter_close_container(&dict, &entry);
+	}
+
+	if (all || !spa_streq (call->name, callimpl->prev.name)) {
+		dbus_message_iter_open_container(&dict, DBUS_TYPE_DICT_ENTRY, NULL, &entry);
+		const char *name = "Name";
+		dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &name);
+		dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT,
+						DBUS_TYPE_STRING_AS_STRING,
+						&variant);
+		dbus_message_iter_append_basic(&variant, DBUS_TYPE_STRING,
+						safe_string (&call->name));
+		dbus_message_iter_close_container(&entry, &variant);
+		dbus_message_iter_close_container(&dict, &entry);
+	}
+
+	if (all || call->multiparty != callimpl->prev.multiparty) {
+		dbus_message_iter_open_container(&dict, DBUS_TYPE_DICT_ENTRY, NULL, &entry);
+		const char *multiparty = "Multiparty";
+		dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &multiparty);
+		dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT,
+						DBUS_TYPE_BOOLEAN_AS_STRING,
+						&variant);
+		dbus_message_iter_append_basic(&variant, DBUS_TYPE_BOOLEAN, &call->multiparty);
+		dbus_message_iter_close_container(&entry, &variant);
+		dbus_message_iter_close_container(&dict, &entry);
+	}
+
+	if (all || call->state != callimpl->prev.state) {
+		dbus_message_iter_open_container(&dict, DBUS_TYPE_DICT_ENTRY, NULL, &entry);
+		const char *state = "State";
+		dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &state);
+		dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT,
+						DBUS_TYPE_STRING_AS_STRING,
+						&variant);
+		dbus_message_iter_append_basic(&variant, DBUS_TYPE_STRING,
+						&call_state_to_string[call->state]);
+		dbus_message_iter_close_container(&entry, &variant);
+		dbus_message_iter_close_container(&dict, &entry);
+	}
+
+	dbus_message_iter_close_container(i, &dict);
+}
+
+static DBusMessage *call_introspect(struct callimpl *callimpl, DBusMessage *m)
+{
+	const char *xml = PW_TELEPHONY_CALL_INTROSPECT_XML;
+	spa_autoptr(DBusMessage) r = NULL;
+	if ((r = dbus_message_new_method_return(m)) == NULL)
+		return NULL;
+	if (!dbus_message_append_args(r, DBUS_TYPE_STRING, &xml, DBUS_TYPE_INVALID))
+		return NULL;
+	return spa_steal_ptr(r);
+}
+
+static DBusMessage *call_properties_get(struct callimpl *callimpl, DBusMessage *m)
+{
+	const char *iface, *name;
+	DBusMessage *r;
+	DBusMessageIter i, v;
+
+	if (!dbus_message_get_args(m, NULL,
+				DBUS_TYPE_STRING, &iface,
+				DBUS_TYPE_STRING, &name,
+				DBUS_TYPE_INVALID))
+		return NULL;
+
+	if (spa_streq(iface, PW_TELEPHONY_CALL_IFACE))
+		return dbus_message_new_error(m, DBUS_ERROR_INVALID_ARGS,
+				"No such interface");
+
+	if (spa_streq(name, "Multiparty")) {
+		r = dbus_message_new_method_return(m);
+		if (r == NULL)
+			return NULL;
+		dbus_message_iter_init_append(r, &i);
+		dbus_message_iter_open_container(&i, DBUS_TYPE_VARIANT,
+				DBUS_TYPE_BOOLEAN_AS_STRING, &v);
+		dbus_message_iter_append_basic(&v, DBUS_TYPE_BOOLEAN,
+				&callimpl->this.multiparty);
+		dbus_message_iter_close_container(&i, &v);
+		return r;
+	} else {
+		const char * const *property = NULL;
+		if (spa_streq(name, "LineIdentification")) {
+			property = (const char * const *) &callimpl->this.line_identification;
+		} else if (spa_streq(name, "IncomingLine")) {
+			property = (const char * const *) &callimpl->this.incoming_line;
+		} else if (spa_streq(name, "Name")) {
+			property = (const char * const *) &callimpl->this.name;
+		} else if (spa_streq(name, "State")) {
+			property = &call_state_to_string[callimpl->this.state];
+		}
+
+		if (property) {
+			r = dbus_message_new_method_return(m);
+			if (r == NULL)
+				return NULL;
+			dbus_message_iter_init_append(r, &i);
+			dbus_message_iter_open_container(&i, DBUS_TYPE_VARIANT,
+					DBUS_TYPE_STRING_AS_STRING, &v);
+			dbus_message_iter_append_basic(&v, DBUS_TYPE_STRING,
+					property);
+			dbus_message_iter_close_container(&i, &v);
+			return r;
+		}
+	}
+
+	return dbus_message_new_error(m, DBUS_ERROR_INVALID_ARGS,
+			"No such property");
+}
+
+static DBusMessage *call_properties_get_all(struct callimpl *callimpl, DBusMessage *m, bool ofono_compat)
+{
+	DBusMessage *r;
+	DBusMessageIter i;
+
+	if (!ofono_compat) {
+		const char *iface;
+
+		if (!dbus_message_get_args(m, NULL,
+					DBUS_TYPE_STRING, &iface,
+					DBUS_TYPE_INVALID))
+			return NULL;
+
+		if (!spa_streq(iface, PW_TELEPHONY_CALL_IFACE))
+			return dbus_message_new_error(m, DBUS_ERROR_UNKNOWN_INTERFACE,
+					"No such interface");
+	}
+
+	r = dbus_message_new_method_return(m);
+	if (r == NULL)
+		return NULL;
+
+	dbus_message_iter_init_append(r, &i);
+	dbus_iter_append_call_properties(&i, &callimpl->this, true);
+	return r;
+}
+
+static DBusMessage *call_properties_set(struct callimpl *callimpl, DBusMessage *m)
+{
+	return dbus_message_new_error(m, DBUS_ERROR_PROPERTY_READ_ONLY,
+			"Property not writable");
+}
+
+static DBusMessage *call_answer(struct callimpl *callimpl, DBusMessage *m)
+{
+	enum spa_bt_telephony_error err = BT_TELEPHONY_ERROR_FAILED;
+	uint8_t cme_error;
+
+	if (call_emit_answer(callimpl, &err, &cme_error) && err == BT_TELEPHONY_ERROR_NONE)
+		return dbus_message_new_method_return(m);
+
+	return dbus_message_new_error(m, telephony_error_to_dbus (err),
+		telephony_error_to_description (err, cme_error));
+}
+
+static DBusMessage *call_hangup(struct callimpl *callimpl, DBusMessage *m)
+{
+	enum spa_bt_telephony_error err = BT_TELEPHONY_ERROR_FAILED;
+	uint8_t cme_error;
+
+	if (call_emit_hangup(callimpl, &err, &cme_error) && err == BT_TELEPHONY_ERROR_NONE)
+		return dbus_message_new_method_return(m);
+
+	return dbus_message_new_error(m, telephony_error_to_dbus (err),
+		telephony_error_to_description (err, cme_error));
+}
+
+static DBusHandlerResult call_handler(DBusConnection *c, DBusMessage *m, void *userdata)
+{
+	struct callimpl *callimpl = userdata;
+	struct agimpl *agimpl = SPA_CONTAINER_OF(callimpl->this.ag, struct agimpl, this);
+	struct impl *impl = SPA_CONTAINER_OF(agimpl->this.telephony, struct impl, this);
+
+	spa_autoptr(DBusMessage) r = NULL;
+	const char *path, *interface, *member;
+
+	path = dbus_message_get_path(m);
+	interface = dbus_message_get_interface(m);
+	member = dbus_message_get_member(m);
+
+	spa_log_debug(impl->log, "dbus: path=%s, interface=%s, member=%s", path, interface, member);
+
+	if (dbus_message_is_method_call(m, DBUS_INTERFACE_INTROSPECTABLE, "Introspect")) {
+		r = call_introspect(callimpl, m);
+	} else if (dbus_message_is_method_call(m, DBUS_INTERFACE_PROPERTIES, "Get")) {
+		r = call_properties_get(callimpl, m);
+	} else if (dbus_message_is_method_call(m, DBUS_INTERFACE_PROPERTIES, "GetAll")) {
+		r = call_properties_get_all(callimpl, m, false);
+	} else if (dbus_message_is_method_call(m, DBUS_INTERFACE_PROPERTIES, "Set")) {
+		r = call_properties_set(callimpl, m);
+	} else if (dbus_message_is_method_call(m, PW_TELEPHONY_CALL_IFACE, "Answer") ||
+		   dbus_message_is_method_call(m, OFONO_VOICE_CALL_IFACE, "Answer")) {
+		r = call_answer(callimpl, m);
+	} else if (dbus_message_is_method_call(m, PW_TELEPHONY_CALL_IFACE, "Hangup") ||
+		   dbus_message_is_method_call(m, OFONO_VOICE_CALL_IFACE, "Hangup")) {
+		r = call_hangup(callimpl, m);
+	} else if (dbus_message_is_method_call(m, OFONO_VOICE_CALL_IFACE, "GetProperties")) {
+		r = call_properties_get_all(callimpl, m, true);
+	} else {
+		return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+	}
+
+	if (r == NULL)
+		return DBUS_HANDLER_RESULT_NEED_MEMORY;
+	if (!dbus_connection_send(impl->conn, r, NULL))
+		return DBUS_HANDLER_RESULT_NEED_MEMORY;
+	return DBUS_HANDLER_RESULT_HANDLED;
+}
+
+int telephony_call_register(struct spa_bt_telephony_call *call)
+{
+	struct callimpl *callimpl = SPA_CONTAINER_OF(call, struct callimpl, this);
+	struct agimpl *agimpl = SPA_CONTAINER_OF(callimpl->this.ag, struct agimpl, this);
+	struct impl *impl = SPA_CONTAINER_OF(agimpl->this.telephony, struct impl, this);
+	char *path;
+
+	const DBusObjectPathVTable vtable = {
+		.message_function = call_handler,
+	};
+
+	path = spa_aprintf ("%s/call%d", agimpl->path, callimpl->this.id);
+
+	/* register object */
+	if (!dbus_connection_register_object_path(impl->conn, path, &vtable, callimpl)) {
+		spa_log_error(impl->log, "failed to register %s", path);
+		return -EIO;
+	}
+	callimpl->path = strdup(path);
+
+	/* notify on ObjectManager of the AudioGateway object */
+	{
+		spa_autoptr(DBusMessage) msg = NULL;
+		DBusMessageIter iter, entry, dict;
+		const char *interface = PW_TELEPHONY_CALL_IFACE;
+
+		msg = dbus_message_new_signal(agimpl->path,
+					DBUS_INTERFACE_OBJECT_MANAGER,
+					"InterfacesAdded");
+		dbus_message_iter_init_append(msg, &iter);
+		dbus_message_iter_append_basic(&iter, DBUS_TYPE_OBJECT_PATH, &path);
+		dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{sa{sv}}", &dict);
+		dbus_message_iter_open_container(&dict, DBUS_TYPE_DICT_ENTRY, NULL, &entry);
+		dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &interface);
+		dbus_iter_append_call_properties(&entry, call, true);
+		dbus_message_iter_close_container(&dict, &entry);
+		dbus_message_iter_close_container(&iter, &dict);
+
+		if (!dbus_connection_send(impl->conn, msg, NULL)) {
+			spa_log_error(impl->log, "failed to send InterfacesAdded for %s", path);
+			telephony_call_unregister(call);
+			return -EIO;
+		}
+	}
+
+	/* emit CallAdded on the AudioGateway object */
+	{
+		spa_autoptr(DBusMessage) msg = NULL;
+		DBusMessageIter iter;
+
+		msg = dbus_message_new_signal(agimpl->path,
+					OFONO_VOICE_CALL_MANAGER_IFACE,
+					"CallAdded");
+		dbus_message_iter_init_append(msg, &iter);
+		dbus_message_iter_append_basic(&iter, DBUS_TYPE_OBJECT_PATH, &path);
+		dbus_iter_append_call_properties(&iter, call, true);
+
+		if (!dbus_connection_send(impl->conn, msg, NULL)) {
+			spa_log_error(impl->log, "failed to send CallAdded for %s", path);
+			telephony_call_unregister(call);
+			return -EIO;
+		}
+	}
+
+	telephony_call_commit_properties(call);
+
+	spa_log_debug(impl->log, "registered Call: %s", path);
+
+	return 0;
+}
+
+void telephony_call_unregister(struct spa_bt_telephony_call *call)
+{
+	struct callimpl *callimpl = SPA_CONTAINER_OF(call, struct callimpl, this);
+	struct agimpl *agimpl = SPA_CONTAINER_OF(callimpl->this.ag, struct agimpl, this);
+	struct impl *impl = SPA_CONTAINER_OF(agimpl->this.telephony, struct impl, this);
+
+	if (!callimpl->path)
+		return;
+
+	spa_log_debug(impl->log, "removing Call: %s", callimpl->path);
+
+	{
+		spa_autoptr(DBusMessage) msg = NULL;
+		DBusMessageIter iter, entry;
+		const char *interface = PW_TELEPHONY_CALL_IFACE;
+
+		msg = dbus_message_new_signal(agimpl->path,
+					DBUS_INTERFACE_OBJECT_MANAGER,
+					"InterfacesRemoved");
+		dbus_message_iter_init_append(msg, &iter);
+		dbus_message_iter_append_basic(&iter, DBUS_TYPE_OBJECT_PATH, &callimpl->path);
+		dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY,
+						DBUS_TYPE_STRING_AS_STRING, &entry);
+		dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &interface);
+		dbus_message_iter_close_container(&iter, &entry);
+
+		if (!dbus_connection_send(impl->conn, msg, NULL)) {
+			spa_log_warn(impl->log, "sending InterfacesRemoved failed");
+		}
+	}
+	{
+		spa_autoptr(DBusMessage) msg = NULL;
+		DBusMessageIter iter;
+
+		msg = dbus_message_new_signal(agimpl->path,
+					OFONO_VOICE_CALL_MANAGER_IFACE,
+					"CallRemoved");
+		dbus_message_iter_init_append(msg, &iter);
+		dbus_message_iter_append_basic(&iter, DBUS_TYPE_OBJECT_PATH, &callimpl->path);
+
+		if (!dbus_connection_send(impl->conn, msg, NULL)) {
+			spa_log_warn(impl->log, "sending CallRemoved failed");
+		}
+	}
+
+	if (!dbus_connection_unregister_object_path(impl->conn, callimpl->path)) {
+		spa_log_warn(impl->log, "failed to unregister %s", callimpl->path);
+	}
+
+	free(callimpl->path);
+	callimpl->path = NULL;
+}
+
+/* send message to notify about property changes */
+void telephony_call_notify_updated_props(struct spa_bt_telephony_call *call)
+{
+	struct callimpl *callimpl = SPA_CONTAINER_OF(call, struct callimpl, this);
+	struct agimpl *agimpl = SPA_CONTAINER_OF(callimpl->this.ag, struct agimpl, this);
+	struct impl *impl = SPA_CONTAINER_OF(agimpl->this.telephony, struct impl, this);
+
+	{
+		spa_autoptr(DBusMessage) msg = NULL;
+		const char *interface = PW_TELEPHONY_CALL_IFACE;
+		DBusMessageIter i, a;
+
+		msg = dbus_message_new_signal(callimpl->path,
+					DBUS_INTERFACE_PROPERTIES,
+					"PropertiesChanged");
+
+		dbus_message_iter_init_append(msg, &i);
+		dbus_message_iter_append_basic(&i, DBUS_TYPE_STRING, &interface);
+
+		dbus_iter_append_call_properties(&i, call, false);
+
+		dbus_message_iter_open_container(&i, DBUS_TYPE_ARRAY,
+				DBUS_TYPE_STRING_AS_STRING, &a);
+		dbus_message_iter_close_container(&i, &a);
+
+		if (!dbus_connection_send(impl->conn, msg, NULL)){
+			spa_log_warn(impl->log, "sending PropertiesChanged failed");
+		}
+	}
+
+	if (!spa_streq (call->line_identification, callimpl->prev.line_identification)) {
+		spa_autoptr(DBusMessage) msg = NULL;
+		DBusMessageIter entry, variant;
+
+		msg = dbus_message_new_signal(callimpl->path,
+					OFONO_VOICE_CALL_IFACE,
+					"PropertyChanged");
+
+		const char *line_identification = "LineIdentification";
+		dbus_message_iter_init_append(msg, &entry);
+		dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &line_identification);
+		dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT,
+						DBUS_TYPE_STRING_AS_STRING, &variant);
+		dbus_message_iter_append_basic(&variant, DBUS_TYPE_STRING,
+						safe_string (&call->line_identification));
+		dbus_message_iter_close_container(&entry, &variant);
+
+		if (!dbus_connection_send(impl->conn, msg, NULL)){
+			spa_log_warn(impl->log, "sending PropertyChanged failed");
+		}
+	}
+
+	if (!spa_streq (call->incoming_line, callimpl->prev.incoming_line)) {
+		spa_autoptr(DBusMessage) msg = NULL;
+		DBusMessageIter entry, variant;
+
+		msg = dbus_message_new_signal(callimpl->path,
+					OFONO_VOICE_CALL_IFACE,
+					"PropertyChanged");
+
+		const char *incoming_line = "IncomingLine";
+		dbus_message_iter_init_append(msg, &entry);
+		dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &incoming_line);
+		dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT,
+						DBUS_TYPE_STRING_AS_STRING,
+						&variant);
+		dbus_message_iter_append_basic(&variant, DBUS_TYPE_STRING,
+						safe_string (&call->incoming_line));
+		dbus_message_iter_close_container(&entry, &variant);
+
+		if (!dbus_connection_send(impl->conn, msg, NULL)){
+			spa_log_warn(impl->log, "sending PropertyChanged failed");
+		}
+	}
+
+	if (!spa_streq (call->name, callimpl->prev.name)) {
+		spa_autoptr(DBusMessage) msg = NULL;
+		DBusMessageIter entry, variant;
+
+		msg = dbus_message_new_signal(callimpl->path,
+					OFONO_VOICE_CALL_IFACE,
+					"PropertyChanged");
+
+		const char *name = "Name";
+		dbus_message_iter_init_append(msg, &entry);
+		dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &name);
+		dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT,
+						DBUS_TYPE_STRING_AS_STRING,
+						&variant);
+		dbus_message_iter_append_basic(&variant, DBUS_TYPE_STRING,
+						safe_string (&call->name));
+		dbus_message_iter_close_container(&entry, &variant);
+
+		if (!dbus_connection_send(impl->conn, msg, NULL)){
+			spa_log_warn(impl->log, "sending PropertyChanged failed");
+		}
+	}
+
+	if (call->multiparty != callimpl->prev.multiparty) {
+		spa_autoptr(DBusMessage) msg = NULL;
+		DBusMessageIter entry, variant;
+
+		msg = dbus_message_new_signal(callimpl->path,
+					OFONO_VOICE_CALL_IFACE,
+					"PropertyChanged");
+
+		const char *multiparty = "Multiparty";
+		dbus_message_iter_init_append(msg, &entry);
+		dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &multiparty);
+		dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT,
+						DBUS_TYPE_BOOLEAN_AS_STRING,
+						&variant);
+		dbus_message_iter_append_basic(&variant, DBUS_TYPE_BOOLEAN, &call->multiparty);
+		dbus_message_iter_close_container(&entry, &variant);
+
+		if (!dbus_connection_send(impl->conn, msg, NULL)){
+			spa_log_warn(impl->log, "sending PropertyChanged failed");
+		}
+	}
+
+	if (call->state != callimpl->prev.state) {
+		spa_autoptr(DBusMessage) msg = NULL;
+		DBusMessageIter entry, variant;
+
+		msg = dbus_message_new_signal(callimpl->path,
+					OFONO_VOICE_CALL_IFACE,
+					"PropertyChanged");
+
+		const char *state = "State";
+		dbus_message_iter_init_append(msg, &entry);
+		dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &state);
+		dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT,
+						DBUS_TYPE_STRING_AS_STRING,
+						&variant);
+		dbus_message_iter_append_basic(&variant, DBUS_TYPE_STRING,
+						&call_state_to_string[call->state]);
+		dbus_message_iter_close_container(&entry, &variant);
+
+		if (!dbus_connection_send(impl->conn, msg, NULL)){
+			spa_log_warn(impl->log, "sending PropertyChanged failed");
+		}
+	}
+
+	telephony_call_commit_properties(call);
+}
+
+void telephony_call_set_callbacks(struct spa_bt_telephony_call *call,
+	const struct spa_bt_telephony_call_callbacks *cbs,
+	void *data)
+{
+	struct callimpl *callimpl = SPA_CONTAINER_OF(call, struct callimpl, this);
+	callimpl->callbacks.funcs = cbs;
+	callimpl->callbacks.data = data;
+}
diff --git a/spa/plugins/bluez5/telephony.h b/spa/plugins/bluez5/telephony.h
new file mode 100644
index 00000000..8ba85ec4
--- /dev/null
+++ b/spa/plugins/bluez5/telephony.h
@@ -0,0 +1,132 @@
+/* Spa Bluez5 Telephony D-Bus service */
+/* SPDX-FileCopyrightText: Copyright © 2024 Collabora Ltd. */
+/* SPDX-License-Identifier: MIT */
+
+#ifndef SPA_BLUEZ5_TELEPHONY_H
+#define SPA_BLUEZ5_TELEPHONY_H
+
+#include "defs.h"
+
+enum spa_bt_telephony_error {
+	BT_TELEPHONY_ERROR_NONE = 0,
+	BT_TELEPHONY_ERROR_FAILED,
+	BT_TELEPHONY_ERROR_NOT_SUPPORTED,
+	BT_TELEPHONY_ERROR_INVALID_FORMAT,
+	BT_TELEPHONY_ERROR_INVALID_STATE,
+	BT_TELEPHONY_ERROR_IN_PROGRESS,
+	BT_TELEPHONY_ERROR_CME,
+};
+
+enum spa_bt_telephony_call_state {
+	CALL_STATE_ACTIVE,
+	CALL_STATE_HELD,
+	CALL_STATE_DIALING,
+	CALL_STATE_ALERTING,
+	CALL_STATE_INCOMING,
+	CALL_STATE_WAITING,
+	CALL_STATE_DISCONNECTED,
+};
+
+struct spa_bt_telephony {
+
+};
+
+struct spa_bt_telephony_ag_transport {
+	int8_t codec;
+	enum spa_bt_transport_state state;
+	dbus_bool_t rejectSCO;
+};
+
+struct spa_bt_telephony_ag {
+	struct spa_bt_telephony *telephony;
+	struct spa_list call_list;
+
+	int id;
+
+	/* D-Bus properties */
+	char *address;
+	struct spa_bt_telephony_ag_transport transport;
+};
+
+struct spa_bt_telephony_call {
+	struct spa_bt_telephony_ag *ag;
+	struct spa_list link;	/* link in ag->call_list */
+
+	int id;
+
+	/* D-Bus properties */
+	char *line_identification;
+	char *incoming_line;
+	char *name;
+	bool multiparty;
+	enum spa_bt_telephony_call_state state;
+};
+
+struct spa_bt_telephony_ag_callbacks {
+#define SPA_VERSION_BT_TELEPHONY_AG_CALLBACKS	0
+	uint32_t version;
+
+	void (*dial)(void *data, const char *number, enum spa_bt_telephony_error *err, uint8_t *cme_error);
+	void (*swap_calls)(void *data, enum spa_bt_telephony_error *err, uint8_t *cme_error);
+	void (*release_and_answer)(void *data, enum spa_bt_telephony_error *err, uint8_t *cme_error);
+	void (*release_and_swap)(void *data, enum spa_bt_telephony_error *err, uint8_t *cme_error);
+	void (*hold_and_answer)(void *data, enum spa_bt_telephony_error *err, uint8_t *cme_error);
+	void (*hangup_all)(void *data, enum spa_bt_telephony_error *err, uint8_t *cme_error);
+	void (*create_multiparty)(void *data, enum spa_bt_telephony_error *err, uint8_t *cme_error);
+	void (*send_tones)(void *data, const char *tones, enum spa_bt_telephony_error *err, uint8_t *cme_error);
+
+	void (*transport_activate)(void *data, enum spa_bt_telephony_error *err, uint8_t *cme_error);
+};
+
+struct spa_bt_telephony_call_callbacks {
+#define SPA_VERSION_BT_TELEPHONY_CALL_CALLBACKS	0
+	uint32_t version;
+
+	void (*answer)(void *data, enum spa_bt_telephony_error *err, uint8_t *cme_error);
+	void (*hangup)(void *data, enum spa_bt_telephony_error *err, uint8_t *cme_error);
+};
+
+struct spa_bt_telephony *telephony_new(struct spa_log *log, struct spa_dbus *dbus,
+					const struct spa_dict *info);
+void telephony_free(struct spa_bt_telephony *telephony);
+
+
+/* create/destroy the ag object */
+struct spa_bt_telephony_ag * telephony_ag_new(struct spa_bt_telephony *telephony,
+					      size_t user_data_size);
+void telephony_ag_destroy(struct spa_bt_telephony_ag *ag);
+
+/* get the user data structure; struct size is set when creating the AG */
+void *telephony_ag_get_user_data(struct spa_bt_telephony_ag *ag);
+
+void telephony_ag_set_callbacks(struct spa_bt_telephony_ag *ag,
+			       const struct spa_bt_telephony_ag_callbacks *cbs,
+			       void *data);
+
+void telephony_ag_transport_notify_updated_props(struct spa_bt_telephony_ag *ag);
+
+/* register/unregister AudioGateway object on the bus */
+int telephony_ag_register(struct spa_bt_telephony_ag *ag);
+void telephony_ag_unregister(struct spa_bt_telephony_ag *ag);
+
+
+/* create/destroy the call object */
+struct spa_bt_telephony_call * telephony_call_new(struct spa_bt_telephony_ag *ag,
+						  size_t user_data_size);
+void telephony_call_destroy(struct spa_bt_telephony_call *call);
+
+/* get the user data structure; struct size is set when creating the Call */
+void *telephony_call_get_user_data(struct spa_bt_telephony_call *call);
+
+/* register/unregister Call object on the bus */
+int telephony_call_register(struct spa_bt_telephony_call *call);
+void telephony_call_unregister(struct spa_bt_telephony_call *call);
+
+/* send message to notify about property changes */
+void telephony_call_notify_updated_props(struct spa_bt_telephony_call *call);
+
+void telephony_call_set_callbacks(struct spa_bt_telephony_call *call,
+				 const struct spa_bt_telephony_call_callbacks *cbs,
+				 void *data);
+
+#endif
diff --git a/spa/plugins/control/mixer.c b/spa/plugins/control/mixer.c
index 390a4dab..439bea76 100644
--- a/spa/plugins/control/mixer.c
+++ b/spa/plugins/control/mixer.c
@@ -19,6 +19,7 @@
 #include <spa/param/audio/format-utils.h>
 #include <spa/param/param.h>
 #include <spa/control/control.h>
+#include <spa/control/ump-utils.h>
 #include <spa/pod/filter.h>
 
 #undef SPA_LOG_TOPIC_DEFAULT
@@ -50,6 +51,8 @@ struct port {
 	unsigned int valid:1;
 	unsigned int have_format:1;
 
+	uint32_t types;
+
 	struct buffer buffers[MAX_BUFFERS];
 	uint32_t n_buffers;
 
@@ -449,7 +452,7 @@ static int port_set_format(void *object,
 			clear_buffers(this, port);
 		}
 	} else {
-		uint32_t media_type, media_subtype;
+		uint32_t media_type, media_subtype, types = 0;
 		if ((res = spa_format_parse(format, &media_type, &media_subtype)) < 0)
 			return res;
 
@@ -457,11 +460,17 @@ static int port_set_format(void *object,
 		    media_subtype != SPA_MEDIA_SUBTYPE_control)
 			return -EINVAL;
 
+		if ((res = spa_pod_parse_object(format,
+				SPA_TYPE_OBJECT_Format, NULL,
+				SPA_FORMAT_CONTROL_types,  SPA_POD_OPT_Int(&types))) < 0)
+			return res;
+
 		this->have_format = true;
 
 		if (!port->have_format) {
 			this->n_formats++;
 			port->have_format = true;
+			port->types = types;
 			spa_log_debug(this->log, "%p: set format on port %d:%d",
 					this, direction, port_id);
 		}
@@ -620,6 +629,17 @@ static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t
 	return queue_buffer(this, port, &port->buffers[buffer_id]);
 }
 
+static inline int event_compare(uint8_t s1, uint8_t s2)
+{
+	/* 11 (controller) > 12 (program change) >
+	 * 8 (note off) > 9 (note on) > 10 (aftertouch) >
+	 * 13 (channel pressure) > 14 (pitch bend) */
+	static int priotab[] = { 5,4,3,7,6,2,1,0 };
+	if ((s1 & 0xf) != (s2 & 0xf))
+		return 0;
+	return priotab[(s2>>4) & 7] - priotab[(s1>>4) & 7];
+}
+
 static inline int event_sort(struct spa_pod_control *a, struct spa_pod_control *b)
 {
 	if (a->offset < b->offset)
@@ -631,21 +651,20 @@ static inline int event_sort(struct spa_pod_control *a, struct spa_pod_control *
 	switch(a->type) {
 	case SPA_CONTROL_Midi:
 	{
-		/* 11 (controller) > 12 (program change) >
-		 * 8 (note off) > 9 (note on) > 10 (aftertouch) >
-		 * 13 (channel pressure) > 14 (pitch bend) */
-		static int priotab[] = { 5,4,3,7,6,2,1,0 };
-		uint8_t *da, *db;
-
-		if (SPA_POD_BODY_SIZE(&a->value) < 1 ||
-		    SPA_POD_BODY_SIZE(&b->value) < 1)
+		uint8_t *da = SPA_POD_BODY(&a->value), *db = SPA_POD_BODY(&b->value);
+		if (SPA_POD_BODY_SIZE(&a->value) < 1 || SPA_POD_BODY_SIZE(&b->value) < 1)
 			return 0;
-
-		da = SPA_POD_BODY(&a->value);
-		db = SPA_POD_BODY(&b->value);
-		if ((da[0] & 0xf) != (db[0] & 0xf))
+		return event_compare(da[0], db[0]);
+	}
+	case SPA_CONTROL_UMP:
+	{
+		uint32_t *da = SPA_POD_BODY(&a->value), *db = SPA_POD_BODY(&b->value);
+		if (SPA_POD_BODY_SIZE(&a->value) < 4 || SPA_POD_BODY_SIZE(&b->value) < 4)
+			return 0;
+		if ((da[0] >> 28) != 2 || (da[0] >> 28) != 4 ||
+		    (db[0] >> 28) != 2 || (db[0] >> 28) != 4)
 			return 0;
-		return priotab[(db[0]>>4) & 7] - priotab[(da[0]>>4) & 7];
+		return event_compare(da[0] >> 16, db[0] >> 16);
 	}
 	default:
 		return 0;
@@ -763,8 +782,39 @@ static int impl_node_process(void *object)
 		if (next == NULL)
 			break;
 
-		spa_pod_builder_control(&builder, next->offset, next->type);
-		spa_pod_builder_primitive(&builder, &next->value);
+		if (outport->types && (outport->types & (1u << next->type)) == 0) {
+			uint8_t *data = SPA_POD_BODY(&next->value);
+			size_t size = SPA_POD_BODY_SIZE(&next->value);
+
+			switch (next->type) {
+			case SPA_CONTROL_Midi:
+			{
+				uint32_t ump[4];
+				uint64_t state = 0;
+				while (size > 0) {
+					int ump_size = spa_ump_from_midi(&data, &size, ump, sizeof(ump), 0, &state);
+					if (ump_size <= 0)
+						break;
+					spa_pod_builder_control(&builder, next->offset, SPA_CONTROL_UMP);
+					spa_pod_builder_bytes(&builder, ump, ump_size);
+				}
+				break;
+			}
+			case SPA_CONTROL_UMP:
+			{
+				uint8_t ev[8];
+				int ev_size = spa_ump_to_midi((uint32_t*)data, size, ev, sizeof(ev));
+				if (ev_size <= 0)
+					break;
+				spa_pod_builder_control(&builder, next->offset, SPA_CONTROL_Midi);
+				spa_pod_builder_bytes(&builder, ev, ev_size);
+				break;
+			}
+			}
+		} else {
+			spa_pod_builder_control(&builder, next->offset, next->type);
+			spa_pod_builder_primitive(&builder, &next->value);
+		}
 
 		ctrl[next_index] = spa_pod_control_next(ctrl[next_index]);
 	}
diff --git a/spa/plugins/filter-graph/audio-dsp-avx.c b/spa/plugins/filter-graph/audio-dsp-avx.c
new file mode 100644
index 00000000..1509284c
--- /dev/null
+++ b/spa/plugins/filter-graph/audio-dsp-avx.c
@@ -0,0 +1,326 @@
+/* Spa */
+/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */
+/* SPDX-License-Identifier: MIT */
+
+#include <string.h>
+#include <stdio.h>
+#include <math.h>
+
+#include <spa/utils/defs.h>
+
+#include "config.h"
+#ifndef HAVE_FFTW
+#include "pffft.h"
+#endif
+#include "audio-dsp-impl.h"
+
+#include <immintrin.h>
+
+static void dsp_add_avx(void *obj, float *dst, const float * SPA_RESTRICT src[],
+		uint32_t n_src, uint32_t n_samples)
+{
+	uint32_t n, i, unrolled;
+	__m256 in[4];
+	const float **s = (const float **)src;
+	float *d = dst;
+
+	if (SPA_LIKELY(SPA_IS_ALIGNED(dst, 32))) {
+		unrolled = n_samples & ~31;
+		for (i = 0; i < n_src; i++) {
+			if (SPA_UNLIKELY(!SPA_IS_ALIGNED(src[i], 32))) {
+				unrolled = 0;
+				break;
+			}
+		}
+	} else
+		unrolled = 0;
+
+	for (n = 0; n < unrolled; n += 32) {
+		in[0] = _mm256_load_ps(&s[0][n+ 0]);
+		in[1] = _mm256_load_ps(&s[0][n+ 8]);
+		in[2] = _mm256_load_ps(&s[0][n+16]);
+		in[3] = _mm256_load_ps(&s[0][n+24]);
+
+		for (i = 1; i < n_src; i++) {
+			in[0] = _mm256_add_ps(in[0], _mm256_load_ps(&s[i][n+ 0]));
+			in[1] = _mm256_add_ps(in[1], _mm256_load_ps(&s[i][n+ 8]));
+			in[2] = _mm256_add_ps(in[2], _mm256_load_ps(&s[i][n+16]));
+			in[3] = _mm256_add_ps(in[3], _mm256_load_ps(&s[i][n+24]));
+		}
+		_mm256_store_ps(&d[n+ 0], in[0]);
+		_mm256_store_ps(&d[n+ 8], in[1]);
+		_mm256_store_ps(&d[n+16], in[2]);
+		_mm256_store_ps(&d[n+24], in[3]);
+	}
+	for (; n < n_samples; n++) {
+		__m128 in[1];
+		in[0] = _mm_load_ss(&s[0][n]);
+		for (i = 1; i < n_src; i++)
+			in[0] = _mm_add_ss(in[0], _mm_load_ss(&s[i][n]));
+		_mm_store_ss(&d[n], in[0]);
+	}
+}
+
+static void dsp_add_1_gain_avx(void *obj, float *dst, const float * SPA_RESTRICT src[],
+		uint32_t n_src, float gain, uint32_t n_samples)
+{
+	uint32_t n, i, unrolled;
+	__m256 in[4], g;
+	const float **s = (const float **)src;
+	float *d = dst;
+	__m128 g1;
+
+	if (SPA_LIKELY(SPA_IS_ALIGNED(dst, 32))) {
+		unrolled = n_samples & ~31;
+		for (i = 0; i < n_src; i++) {
+			if (SPA_UNLIKELY(!SPA_IS_ALIGNED(src[i], 32))) {
+				unrolled = 0;
+				break;
+			}
+		}
+	} else
+		unrolled = 0;
+
+	g = _mm256_set1_ps(gain);
+	g1 = _mm_set_ss(gain);
+
+	for (n = 0; n < unrolled; n += 32) {
+		in[0] = _mm256_load_ps(&s[0][n+ 0]);
+		in[1] = _mm256_load_ps(&s[0][n+ 8]);
+		in[2] = _mm256_load_ps(&s[0][n+16]);
+		in[3] = _mm256_load_ps(&s[0][n+24]);
+
+		for (i = 1; i < n_src; i++) {
+			in[0] = _mm256_add_ps(in[0], _mm256_load_ps(&s[i][n+ 0]));
+			in[1] = _mm256_add_ps(in[1], _mm256_load_ps(&s[i][n+ 8]));
+			in[2] = _mm256_add_ps(in[2], _mm256_load_ps(&s[i][n+16]));
+			in[3] = _mm256_add_ps(in[3], _mm256_load_ps(&s[i][n+24]));
+		}
+		_mm256_store_ps(&d[n+ 0], _mm256_mul_ps(g, in[0]));
+		_mm256_store_ps(&d[n+ 8], _mm256_mul_ps(g, in[1]));
+		_mm256_store_ps(&d[n+16], _mm256_mul_ps(g, in[2]));
+		_mm256_store_ps(&d[n+24], _mm256_mul_ps(g, in[3]));
+	}
+	for (; n < n_samples; n++) {
+		__m128 in[1];
+		in[0] = _mm_load_ss(&s[0][n]);
+		for (i = 1; i < n_src; i++)
+			in[0] = _mm_add_ss(in[0], _mm_load_ss(&s[i][n]));
+		_mm_store_ss(&d[n], _mm_mul_ss(g1, in[0]));
+	}
+}
+
+static void dsp_add_n_gain_avx(void *obj, float *dst,
+		const float * SPA_RESTRICT src[], uint32_t n_src,
+		float gain[], uint32_t n_gain, uint32_t n_samples)
+{
+	uint32_t n, i, unrolled;
+	__m256 in[4], g;
+	const float **s = (const float **)src;
+	float *d = dst;
+
+	if (SPA_LIKELY(SPA_IS_ALIGNED(dst, 32))) {
+		unrolled = n_samples & ~31;
+		for (i = 0; i < n_src; i++) {
+			if (SPA_UNLIKELY(!SPA_IS_ALIGNED(src[i], 32))) {
+				unrolled = 0;
+				break;
+			}
+		}
+	} else
+		unrolled = 0;
+
+	for (n = 0; n < unrolled; n += 32) {
+		g = _mm256_set1_ps(gain[0]);
+		in[0] = _mm256_mul_ps(g, _mm256_load_ps(&s[0][n+ 0]));
+		in[1] = _mm256_mul_ps(g, _mm256_load_ps(&s[0][n+ 8]));
+		in[2] = _mm256_mul_ps(g, _mm256_load_ps(&s[0][n+16]));
+		in[3] = _mm256_mul_ps(g, _mm256_load_ps(&s[0][n+24]));
+
+		for (i = 1; i < n_src; i++) {
+			g = _mm256_set1_ps(gain[i]);
+			in[0] = _mm256_add_ps(in[0], _mm256_mul_ps(g, _mm256_load_ps(&s[i][n+ 0])));
+			in[1] = _mm256_add_ps(in[1], _mm256_mul_ps(g, _mm256_load_ps(&s[i][n+ 8])));
+			in[2] = _mm256_add_ps(in[2], _mm256_mul_ps(g, _mm256_load_ps(&s[i][n+16])));
+			in[3] = _mm256_add_ps(in[3], _mm256_mul_ps(g, _mm256_load_ps(&s[i][n+24])));
+		}
+		_mm256_store_ps(&d[n+ 0], in[0]);
+		_mm256_store_ps(&d[n+ 8], in[1]);
+		_mm256_store_ps(&d[n+16], in[2]);
+		_mm256_store_ps(&d[n+24], in[3]);
+	}
+	for (; n < n_samples; n++) {
+		__m128 in[1], g;
+		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]);
+	}
+}
+
+
+void dsp_mix_gain_avx(void *obj,
+		float * SPA_RESTRICT dst,
+		const float * SPA_RESTRICT src[], uint32_t n_src,
+		float gain[], uint32_t n_gain, uint32_t n_samples)
+{
+	if (n_src == 0) {
+		memset(dst, 0, n_samples * sizeof(float));
+	} else if (n_src == 1 && gain[0] == 1.0f) {
+		if (dst != src[0])
+			spa_memcpy(dst, src[0], n_samples * sizeof(float));
+	} else {
+		if (n_gain == 0)
+			dsp_add_avx(obj, dst, src, n_src, n_samples);
+		else if (n_gain < n_src)
+			dsp_add_1_gain_avx(obj, dst, src, n_src, gain[0], n_samples);
+		else
+			dsp_add_n_gain_avx(obj, dst, src, n_src, gain, n_gain, n_samples);
+	}
+}
+
+void dsp_sum_avx(void *obj, float *r, const float *a, const float *b, uint32_t n_samples)
+{
+	uint32_t n, unrolled;
+	__m256 in[4];
+
+	unrolled = n_samples & ~31;
+
+	if (SPA_LIKELY(SPA_IS_ALIGNED(r, 32)) &&
+	    SPA_LIKELY(SPA_IS_ALIGNED(a, 32)) &&
+	    SPA_LIKELY(SPA_IS_ALIGNED(b, 32))) {
+		for (n = 0; n < unrolled; n += 32) {
+			in[0] = _mm256_load_ps(&a[n+ 0]);
+			in[1] = _mm256_load_ps(&a[n+ 8]);
+			in[2] = _mm256_load_ps(&a[n+16]);
+			in[3] = _mm256_load_ps(&a[n+24]);
+
+			in[0] = _mm256_add_ps(in[0], _mm256_load_ps(&b[n+ 0]));
+			in[1] = _mm256_add_ps(in[1], _mm256_load_ps(&b[n+ 8]));
+			in[2] = _mm256_add_ps(in[2], _mm256_load_ps(&b[n+16]));
+			in[3] = _mm256_add_ps(in[3], _mm256_load_ps(&b[n+24]));
+
+			_mm256_store_ps(&r[n+ 0], in[0]);
+			_mm256_store_ps(&r[n+ 8], in[1]);
+			_mm256_store_ps(&r[n+16], in[2]);
+			_mm256_store_ps(&r[n+24], in[3]);
+		}
+	} else {
+		for (n = 0; n < unrolled; n += 32) {
+			in[0] = _mm256_loadu_ps(&a[n+ 0]);
+			in[1] = _mm256_loadu_ps(&a[n+ 8]);
+			in[2] = _mm256_loadu_ps(&a[n+16]);
+			in[3] = _mm256_loadu_ps(&a[n+24]);
+
+			in[0] = _mm256_add_ps(in[0], _mm256_loadu_ps(&b[n+ 0]));
+			in[1] = _mm256_add_ps(in[1], _mm256_loadu_ps(&b[n+ 8]));
+			in[2] = _mm256_add_ps(in[2], _mm256_loadu_ps(&b[n+16]));
+			in[3] = _mm256_add_ps(in[3], _mm256_loadu_ps(&b[n+24]));
+
+			_mm256_storeu_ps(&r[n+ 0], in[0]);
+			_mm256_storeu_ps(&r[n+ 8], in[1]);
+			_mm256_storeu_ps(&r[n+16], in[2]);
+			_mm256_storeu_ps(&r[n+24], in[3]);
+		}
+	}
+	for (; n < n_samples; n++) {
+		__m128 in[1];
+		in[0] = _mm_load_ss(&a[n]);
+		in[0] = _mm_add_ss(in[0], _mm_load_ss(&b[n]));
+		_mm_store_ss(&r[n], in[0]);
+	}
+}
+
+inline static __m256 _mm256_mul_pz(__m256 ab, __m256 cd)
+{
+	__m256 aa, bb, dc, x0, x1;
+	aa = _mm256_moveldup_ps(ab);
+	bb = _mm256_movehdup_ps(ab);
+	x0 = _mm256_mul_ps(aa, cd);
+	dc = _mm256_shuffle_ps(cd, cd, _MM_SHUFFLE(2,3,0,1));
+	x1 = _mm256_mul_ps(bb, dc);
+	return _mm256_addsub_ps(x0, x1);
+}
+
+void dsp_fft_cmul_avx(void *obj, void *fft,
+	float * SPA_RESTRICT dst, const float * SPA_RESTRICT a,
+	const float * SPA_RESTRICT b, uint32_t len, const float scale)
+{
+#ifdef HAVE_FFTW
+	__m256 s = _mm256_set1_ps(scale);
+	__m256 aa[2], bb[2], dd[2];
+	uint32_t i, unrolled;
+
+	if (SPA_IS_ALIGNED(a, 32) &&
+	    SPA_IS_ALIGNED(b, 32) &&
+	    SPA_IS_ALIGNED(dst, 32))
+		unrolled = len & ~7;
+	else
+		unrolled = 0;
+
+	for (i = 0; i < unrolled; i+=8) {
+		aa[0] = _mm256_load_ps(&a[2*i]);	/* ar0 ai0 ar1 ai1 */
+		aa[1] = _mm256_load_ps(&a[2*i+8]);	/* ar1 ai1 ar2 ai2 */
+		bb[0] = _mm256_load_ps(&b[2*i]);	/* br0 bi0 br1 bi1 */
+		bb[1] = _mm256_load_ps(&b[2*i+8]);	/* br2 bi2 br3 bi3 */
+		dd[0] = _mm256_mul_pz(aa[0], bb[0]);
+		dd[1] = _mm256_mul_pz(aa[1], bb[1]);
+		dd[0] = _mm256_mul_ps(dd[0], s);
+		dd[1] = _mm256_mul_ps(dd[1], s);
+		_mm256_store_ps(&dst[2*i], dd[0]);
+		_mm256_store_ps(&dst[2*i+8], dd[1]);
+	}
+	for (; i < len; i++) {
+		dst[2*i  ] = (a[2*i] * b[2*i  ] - a[2*i+1] * b[2*i+1]) * scale;
+		dst[2*i+1] = (a[2*i] * b[2*i+1] + a[2*i+1] * b[2*i  ]) * scale;
+	}
+#else
+	pffft_zconvolve(fft, a, b, dst, scale);
+#endif
+}
+
+void dsp_fft_cmuladd_avx(void *obj, void *fft,
+	float * SPA_RESTRICT dst, const float * SPA_RESTRICT src,
+	const float * SPA_RESTRICT a, const float * SPA_RESTRICT b,
+	uint32_t len, const float scale)
+{
+#ifdef HAVE_FFTW
+	__m256 s = _mm256_set1_ps(scale);
+	__m256 aa[2], bb[2], dd[2], t[2];
+	uint32_t i, unrolled;
+
+	if (SPA_IS_ALIGNED(a, 32) &&
+	    SPA_IS_ALIGNED(b, 32) &&
+	    SPA_IS_ALIGNED(src, 32) &&
+	    SPA_IS_ALIGNED(dst, 32))
+		unrolled = len & ~7;
+	else
+		unrolled = 0;
+
+	for (i = 0; i < unrolled; i+=8) {
+		aa[0] = _mm256_load_ps(&a[2*i]);	/* ar0 ai0 ar1 ai1 */
+		aa[1] = _mm256_load_ps(&a[2*i+8]);	/* ar1 ai1 ar2 ai2 */
+		bb[0] = _mm256_load_ps(&b[2*i]);	/* br0 bi0 br1 bi1 */
+		bb[1] = _mm256_load_ps(&b[2*i+8]);	/* br2 bi2 br3 bi3 */
+		dd[0] = _mm256_mul_pz(aa[0], bb[0]);
+		dd[1] = _mm256_mul_pz(aa[1], bb[1]);
+		dd[0] = _mm256_mul_ps(dd[0], s);
+		dd[1] = _mm256_mul_ps(dd[1], s);
+		t[0] = _mm256_load_ps(&src[2*i]);
+		t[1] = _mm256_load_ps(&src[2*i+8]);
+		t[0] = _mm256_add_ps(t[0], dd[0]);
+		t[1] = _mm256_add_ps(t[1], dd[1]);
+		_mm256_store_ps(&dst[2*i], t[0]);
+		_mm256_store_ps(&dst[2*i+8], t[1]);
+	}
+	for (; i < len; i++) {
+		dst[2*i  ] = src[2*i  ] + (a[2*i] * b[2*i  ] - a[2*i+1] * b[2*i+1]) * scale;
+		dst[2*i+1] = src[2*i+1] + (a[2*i] * b[2*i+1] + a[2*i+1] * b[2*i  ]) * scale;
+	}
+#else
+	pffft_zconvolve_accumulate(fft, a, b, src, dst, scale);
+#endif
+}
diff --git a/spa/plugins/filter-graph/audio-dsp-c.c b/spa/plugins/filter-graph/audio-dsp-c.c
new file mode 100644
index 00000000..f2a509a7
--- /dev/null
+++ b/spa/plugins/filter-graph/audio-dsp-c.c
@@ -0,0 +1,345 @@
+/* Spa */
+/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */
+/* SPDX-License-Identifier: MIT */
+
+#include <string.h>
+#include <stdio.h>
+#include <math.h>
+#include <float.h>
+#include <errno.h>
+
+#include <spa/utils/defs.h>
+
+#include "config.h"
+#ifdef HAVE_FFTW
+#include <fftw3.h>
+#else
+#include "pffft.h"
+#endif
+#include "audio-dsp-impl.h"
+
+void dsp_clear_c(void *obj, float * SPA_RESTRICT dst, uint32_t n_samples)
+{
+	memset(dst, 0, sizeof(float) * n_samples);
+}
+
+void dsp_copy_c(void *obj, float * SPA_RESTRICT dst,
+			const float * SPA_RESTRICT src, uint32_t n_samples)
+{
+	if (dst != src)
+		spa_memcpy(dst, src, sizeof(float) * n_samples);
+}
+
+static inline void dsp_add_c(void *obj, float * SPA_RESTRICT dst,
+			const float * 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(void *obj, float * dst,
+			const float * src, float gain, uint32_t n_samples)
+{
+	uint32_t i;
+	const float *s = src;
+	float *d = dst;
+	if (gain == 0.0f)
+		dsp_clear_c(obj, dst, n_samples);
+	else if (gain == 1.0f)
+		dsp_copy_c(obj, dst, src, n_samples);
+	else  {
+		for (i = 0; i < n_samples; i++)
+			d[i] = s[i] * gain;
+	}
+}
+
+static inline void dsp_gain_add_c(void *obj, float * SPA_RESTRICT dst,
+			const float * SPA_RESTRICT src, float gain, uint32_t n_samples)
+{
+	uint32_t i;
+	const float *s = src;
+	float *d = dst;
+
+	if (gain == 0.0f)
+		return;
+	else if (gain == 1.0f)
+		dsp_add_c(obj, dst, src, n_samples);
+	else {
+		for (i = 0; i < n_samples; i++)
+			d[i] += s[i] * gain;
+	}
+}
+
+
+void dsp_mix_gain_c(void *obj,
+		float * SPA_RESTRICT dst,
+		const float * SPA_RESTRICT src[], uint32_t n_src,
+		float gain[], uint32_t n_gain, uint32_t n_samples)
+{
+	uint32_t i;
+	if (n_src == 0) {
+		dsp_clear_c(obj, dst, n_samples);
+	} else {
+		if (n_gain < n_src) {
+			dsp_copy_c(obj, dst, src[0], n_samples);
+			for (i = 1; i < n_src; i++)
+				dsp_add_c(obj, dst, src[i], n_samples);
+			if (n_gain > 0)
+				dsp_gain_c(obj, dst, dst, gain[0], n_samples);
+		} else {
+			dsp_gain_c(obj, dst, src[0], gain[0], n_samples);
+			for (i = 1; i < n_src; i++)
+				dsp_gain_add_c(obj, dst, src[i], gain[i], n_samples);
+		}
+	}
+}
+
+static inline void dsp_mult1_c(void *obj, float * SPA_RESTRICT dst,
+			const float * 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];
+}
+
+void dsp_mult_c(void *obj,
+		float * SPA_RESTRICT dst,
+		const float * SPA_RESTRICT src[],
+		uint32_t n_src, uint32_t n_samples)
+{
+	uint32_t i;
+	if (n_src == 0) {
+		dsp_clear_c(obj, dst, n_samples);
+	} else {
+		dsp_copy_c(obj, dst, src[0], n_samples);
+		for (i = 1; i < n_src; i++)
+			dsp_mult1_c(obj, dst, src[i], n_samples);
+	}
+}
+
+static void biquad_run_c(void *obj, struct biquad *bq,
+		float *out, const float *in, uint32_t n_samples)
+{
+	float x, y, x1, x2;
+	float b0, b1, b2, a1, a2;
+	uint32_t i;
+
+	if (bq->type == BQ_NONE) {
+		dsp_copy_c(obj, out, in, n_samples);
+		return;
+	}
+
+	x1 = bq->x1;
+	x2 = bq->x2;
+	b0 = bq->b0;
+	b1 = bq->b1;
+	b2 = bq->b2;
+	a1 = bq->a1;
+	a2 = bq->a2;
+	for (i = 0; i < n_samples; i++) {
+		x  = in[i];
+		y  = b0 * x          + x1;
+		x1 = b1 * x - a1 * y + x2;
+		x2 = b2 * x - a2 * y;
+		out[i] = y;
+	}
+#define F(x) (isnormal(x) ? (x) : 0.0f)
+	bq->x1 = F(x1);
+	bq->x2 = F(x2);
+#undef F
+}
+
+void dsp_biquad_run_c(void *obj, struct biquad *bq, uint32_t n_bq, uint32_t bq_stride,
+		float * SPA_RESTRICT out[], const float * SPA_RESTRICT in[],
+		uint32_t n_src, uint32_t n_samples)
+{
+	uint32_t i, j;
+	const float *s;
+	float *d;
+	for (i = 0; i < n_src; i++, bq+=bq_stride) {
+		s = in[i];
+		d = out[i];
+		if (s == NULL || d == NULL)
+			continue;
+		if (n_bq > 0)
+			biquad_run_c(obj, &bq[0], d, s, n_samples);
+		for (j = 1; j < n_bq; j++)
+			biquad_run_c(obj, &bq[j], d, d, n_samples);
+	}
+}
+
+void dsp_sum_c(void *obj, float * dst,
+		const float * SPA_RESTRICT a, const float * SPA_RESTRICT b, uint32_t n_samples)
+{
+	uint32_t i;
+	for (i = 0; i < n_samples; i++)
+		dst[i] = a[i] + b[i];
+}
+
+void dsp_linear_c(void *obj, float * dst,
+		const float * SPA_RESTRICT src, const float mult,
+		const float add, uint32_t n_samples)
+{
+	uint32_t i;
+	if (add == 0.0f) {
+		dsp_gain_c(obj, dst, src, mult, n_samples);
+	} else {
+		if (mult == 0.0f) {
+			for (i = 0; i < n_samples; i++)
+				dst[i] = add;
+		} else if (mult == 1.0f) {
+			for (i = 0; i < n_samples; i++)
+				dst[i] = src[i] + add;
+		} else {
+			for (i = 0; i < n_samples; i++)
+				dst[i] = mult * src[i] + add;
+		}
+	}
+}
+
+
+void dsp_delay_c(void *obj, float *buffer, uint32_t *pos, uint32_t n_buffer,
+		uint32_t delay, float *dst, const float *src, uint32_t n_samples)
+{
+	if (delay == 0) {
+		dsp_copy_c(obj, dst, src, n_samples);
+	} else {
+		uint32_t w, o, i;
+
+		w = *pos;
+		o = n_buffer - delay;
+
+		for (i = 0; i < n_samples; i++) {
+			buffer[w] = buffer[w + n_buffer] = src[i];
+			dst[i] = buffer[w + o];
+			w = w + 1 > n_buffer ? 0 : w + 1;
+		}
+		*pos = w;
+	}
+}
+
+#ifdef HAVE_FFTW
+struct fft_info {
+	fftwf_plan plan_r2c;
+	fftwf_plan plan_c2r;
+};
+#endif
+
+void *dsp_fft_new_c(void *obj, uint32_t size, bool real)
+{
+#ifdef HAVE_FFTW
+	struct fft_info *info = calloc(1, sizeof(struct fft_info));
+	float *rdata;
+	fftwf_complex *cdata;
+
+	if (info == NULL)
+		return NULL;
+
+	rdata = fftwf_alloc_real(size * 2);
+	cdata = fftwf_alloc_complex(size + 1);
+
+	info->plan_r2c = fftwf_plan_dft_r2c_1d(size, rdata, cdata, FFTW_ESTIMATE);
+	info->plan_c2r = fftwf_plan_dft_c2r_1d(size, cdata, rdata, FFTW_ESTIMATE);
+
+	fftwf_free(rdata);
+	fftwf_free(cdata);
+
+	return info;
+#else
+	return pffft_new_setup(size, real ? PFFFT_REAL : PFFFT_COMPLEX);
+#endif
+}
+
+void dsp_fft_free_c(void *obj, void *fft)
+{
+#ifdef HAVE_FFTW
+	struct fft_info *info = fft;
+	fftwf_destroy_plan(info->plan_r2c);
+	fftwf_destroy_plan(info->plan_c2r);
+	free(info);
+#else
+	pffft_destroy_setup(fft);
+#endif
+}
+
+void *dsp_fft_memalloc_c(void *obj, uint32_t size, bool real)
+{
+#ifdef HAVE_FFTW
+	if (real)
+		return fftwf_alloc_real(size);
+	else
+		return fftwf_alloc_complex(size);
+#else
+	if (real)
+		return pffft_aligned_malloc(size * sizeof(float));
+	else
+		return pffft_aligned_malloc(size * 2 * sizeof(float));
+#endif
+}
+
+void dsp_fft_memfree_c(void *obj, void *data)
+{
+#ifdef HAVE_FFTW
+	fftwf_free(data);
+#else
+	pffft_aligned_free(data);
+#endif
+}
+
+void dsp_fft_memclear_c(void *obj, void *data, uint32_t size, bool real)
+{
+#ifdef HAVE_FFTW
+	spa_fga_dsp_clear(obj, data, real ? size : size * 2);
+#else
+	spa_fga_dsp_clear(obj, data, real ? size : size * 2);
+#endif
+}
+
+void dsp_fft_run_c(void *obj, void *fft, int direction,
+	const float * SPA_RESTRICT src, float * SPA_RESTRICT dst)
+{
+#ifdef HAVE_FFTW
+	struct fft_info *info = fft;
+	if (direction > 0)
+		fftwf_execute_dft_r2c (info->plan_r2c, (float*)src, (fftwf_complex*)dst);
+	else
+		fftwf_execute_dft_c2r (info->plan_c2r, (fftwf_complex*)src, dst);
+#else
+	pffft_transform(fft, src, dst, NULL, direction < 0 ? PFFFT_BACKWARD : PFFFT_FORWARD);
+#endif
+}
+
+void dsp_fft_cmul_c(void *obj, void *fft,
+	float * SPA_RESTRICT dst, const float * SPA_RESTRICT a,
+	const float * SPA_RESTRICT b, uint32_t len, const float scale)
+{
+#ifdef HAVE_FFTW
+	for (uint32_t i = 0; i < len; i++) {
+		dst[2*i  ] = (a[2*i] * b[2*i  ] - a[2*i+1] * b[2*i+1]) * scale;
+		dst[2*i+1] = (a[2*i] * b[2*i+1] + a[2*i+1] * b[2*i  ]) * scale;
+	}
+#else
+	pffft_zconvolve(fft, a, b, dst, scale);
+#endif
+}
+
+void dsp_fft_cmuladd_c(void *obj, void *fft,
+	float * SPA_RESTRICT dst, const float * SPA_RESTRICT src,
+	const float * SPA_RESTRICT a, const float * SPA_RESTRICT b,
+	uint32_t len, const float scale)
+{
+#ifdef HAVE_FFTW
+	for (uint32_t i = 0; i < len; i++) {
+		dst[2*i  ] = src[2*i  ] + (a[2*i] * b[2*i  ] - a[2*i+1] * b[2*i+1]) * scale;
+		dst[2*i+1] = src[2*i+1] + (a[2*i] * b[2*i+1] + a[2*i+1] * b[2*i  ]) * scale;
+	}
+#else
+	pffft_zconvolve_accumulate(fft, a, b, src, dst, scale);
+#endif
+}
+
diff --git a/spa/plugins/filter-graph/audio-dsp-impl.h b/spa/plugins/filter-graph/audio-dsp-impl.h
new file mode 100644
index 00000000..388a7453
--- /dev/null
+++ b/spa/plugins/filter-graph/audio-dsp-impl.h
@@ -0,0 +1,94 @@
+/* Spa */
+/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */
+/* SPDX-License-Identifier: MIT */
+
+#ifndef DSP_IMPL_H
+#define DSP_IMPL_H
+
+#include "audio-dsp.h"
+
+struct spa_fga_dsp * spa_fga_dsp_new(uint32_t cpu_flags);
+void spa_fga_dsp_free(struct spa_fga_dsp *dsp);
+
+#define MAKE_CLEAR_FUNC(arch) \
+void dsp_clear_##arch(void *obj, float * SPA_RESTRICT dst, uint32_t n_samples)
+#define MAKE_COPY_FUNC(arch) \
+void dsp_copy_##arch(void *obj, float * SPA_RESTRICT dst, \
+	const float * SPA_RESTRICT src, uint32_t n_samples)
+#define MAKE_MIX_GAIN_FUNC(arch) \
+void dsp_mix_gain_##arch(void *obj, float * SPA_RESTRICT dst,	\
+	const float * SPA_RESTRICT src[], uint32_t n_src, float gain[], uint32_t n_gain, uint32_t n_samples)
+#define MAKE_SUM_FUNC(arch) \
+void dsp_sum_##arch (void *obj, float * SPA_RESTRICT dst, \
+	const float * SPA_RESTRICT a, const float * SPA_RESTRICT b, uint32_t n_samples)
+#define MAKE_LINEAR_FUNC(arch) \
+void dsp_linear_##arch (void *obj, float * SPA_RESTRICT dst, \
+	const float * SPA_RESTRICT src, const float mult, const float add, uint32_t n_samples)
+#define MAKE_MULT_FUNC(arch) \
+void dsp_mult_##arch(void *obj, float * SPA_RESTRICT dst,	\
+	const float * SPA_RESTRICT src[], uint32_t n_src, uint32_t n_samples)
+#define MAKE_BIQUAD_RUN_FUNC(arch) \
+void dsp_biquad_run_##arch (void *obj, struct biquad *bq, uint32_t n_bq, uint32_t bq_stride, \
+	float * SPA_RESTRICT out[], const float * SPA_RESTRICT in[], uint32_t n_src, uint32_t n_samples)
+#define MAKE_DELAY_FUNC(arch) \
+void dsp_delay_##arch (void *obj, float *buffer, uint32_t *pos, uint32_t n_buffer, \
+		uint32_t delay, float *dst, const float *src, uint32_t n_samples)
+
+#define MAKE_FFT_NEW_FUNC(arch) \
+void *dsp_fft_new_##arch(void *obj, uint32_t size, bool real)
+#define MAKE_FFT_FREE_FUNC(arch) \
+void dsp_fft_free_##arch(void *obj, void *fft)
+#define MAKE_FFT_MEMALLOC_FUNC(arch) \
+void *dsp_fft_memalloc_##arch(void *obj, uint32_t size, bool real)
+#define MAKE_FFT_MEMFREE_FUNC(arch) \
+void dsp_fft_memfree_##arch(void *obj, void *mem)
+#define MAKE_FFT_MEMCLEAR_FUNC(arch) \
+void dsp_fft_memclear_##arch(void *obj, void *mem, uint32_t size, bool real)
+#define MAKE_FFT_RUN_FUNC(arch) \
+void dsp_fft_run_##arch(void *obj, void *fft, int direction, \
+	const float * SPA_RESTRICT src, float * SPA_RESTRICT dst)
+#define MAKE_FFT_CMUL_FUNC(arch) \
+void dsp_fft_cmul_##arch(void *obj, void *fft, \
+	float * SPA_RESTRICT dst, const float * SPA_RESTRICT a, \
+	const float * SPA_RESTRICT b, uint32_t len, const float scale)
+#define MAKE_FFT_CMULADD_FUNC(arch) \
+void dsp_fft_cmuladd_##arch(void *obj, void *fft,		\
+	float * dst, const float * src,					\
+	const float * SPA_RESTRICT a, const float * SPA_RESTRICT b,	\
+	uint32_t len, const float scale)
+
+
+MAKE_CLEAR_FUNC(c);
+MAKE_COPY_FUNC(c);
+MAKE_MIX_GAIN_FUNC(c);
+MAKE_SUM_FUNC(c);
+MAKE_LINEAR_FUNC(c);
+MAKE_MULT_FUNC(c);
+MAKE_BIQUAD_RUN_FUNC(c);
+MAKE_DELAY_FUNC(c);
+
+MAKE_FFT_NEW_FUNC(c);
+MAKE_FFT_FREE_FUNC(c);
+MAKE_FFT_MEMALLOC_FUNC(c);
+MAKE_FFT_MEMFREE_FUNC(c);
+MAKE_FFT_MEMCLEAR_FUNC(c);
+MAKE_FFT_RUN_FUNC(c);
+MAKE_FFT_CMUL_FUNC(c);
+MAKE_FFT_CMULADD_FUNC(c);
+
+#if defined (HAVE_SSE)
+MAKE_MIX_GAIN_FUNC(sse);
+MAKE_SUM_FUNC(sse);
+MAKE_BIQUAD_RUN_FUNC(sse);
+MAKE_DELAY_FUNC(sse);
+MAKE_FFT_CMUL_FUNC(sse);
+MAKE_FFT_CMULADD_FUNC(sse);
+#endif
+#if defined (HAVE_AVX)
+MAKE_MIX_GAIN_FUNC(avx);
+MAKE_SUM_FUNC(avx);
+MAKE_FFT_CMUL_FUNC(avx);
+MAKE_FFT_CMULADD_FUNC(avx);
+#endif
+
+#endif /* DSP_OPS_IMPL_H */
diff --git a/spa/plugins/filter-graph/audio-dsp-sse.c b/spa/plugins/filter-graph/audio-dsp-sse.c
new file mode 100644
index 00000000..8c2ffa8e
--- /dev/null
+++ b/spa/plugins/filter-graph/audio-dsp-sse.c
@@ -0,0 +1,744 @@
+/* Spa */
+/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */
+/* SPDX-License-Identifier: MIT */
+
+#include <string.h>
+#include <stdio.h>
+#include <math.h>
+#include <float.h>
+#include <complex.h>
+
+#include <spa/utils/defs.h>
+
+#include "config.h"
+#ifndef HAVE_FFTW
+#include "pffft.h"
+#endif
+
+#include "audio-dsp-impl.h"
+
+#include <xmmintrin.h>
+
+static void dsp_add_sse(void *obj, float *dst, const float * SPA_RESTRICT src[],
+		uint32_t n_src, uint32_t n_samples)
+{
+	uint32_t n, i, unrolled;
+	__m128 in[4];
+	const float **s = (const float **)src;
+	float *d = dst;
+
+	if (SPA_LIKELY(SPA_IS_ALIGNED(dst, 16))) {
+		unrolled = n_samples & ~15;
+		for (i = 0; i < n_src; i++) {
+			if (SPA_UNLIKELY(!SPA_IS_ALIGNED(src[i], 16))) {
+				unrolled = 0;
+				break;
+			}
+		}
+	} else
+		unrolled = 0;
+
+	for (n = 0; n < unrolled; n += 16) {
+		in[0] = _mm_load_ps(&s[0][n+ 0]);
+		in[1] = _mm_load_ps(&s[0][n+ 4]);
+		in[2] = _mm_load_ps(&s[0][n+ 8]);
+		in[3] = _mm_load_ps(&s[0][n+12]);
+
+		for (i = 1; i < n_src; i++) {
+			in[0] = _mm_add_ps(in[0], _mm_load_ps(&s[i][n+ 0]));
+			in[1] = _mm_add_ps(in[1], _mm_load_ps(&s[i][n+ 4]));
+			in[2] = _mm_add_ps(in[2], _mm_load_ps(&s[i][n+ 8]));
+			in[3] = _mm_add_ps(in[3], _mm_load_ps(&s[i][n+12]));
+		}
+		_mm_store_ps(&d[n+ 0], in[0]);
+		_mm_store_ps(&d[n+ 4], in[1]);
+		_mm_store_ps(&d[n+ 8], in[2]);
+		_mm_store_ps(&d[n+12], in[3]);
+	}
+	for (; n < n_samples; n++) {
+		in[0] = _mm_load_ss(&s[0][n]);
+		for (i = 1; i < n_src; i++)
+			in[0] = _mm_add_ss(in[0], _mm_load_ss(&s[i][n]));
+		_mm_store_ss(&d[n], in[0]);
+	}
+}
+
+static void dsp_add_1_gain_sse(void *obj,
+		float * SPA_RESTRICT dst,
+		const float * SPA_RESTRICT src[], uint32_t n_src,
+		float gain, uint32_t n_samples)
+{
+	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;
+
+	g = _mm_set1_ps(gain);
+
+	for (n = 0; n < unrolled; n += 16) {
+		in[0] = _mm_load_ps(&s[0][n+ 0]);
+		in[1] = _mm_load_ps(&s[0][n+ 4]);
+		in[2] = _mm_load_ps(&s[0][n+ 8]);
+		in[3] = _mm_load_ps(&s[0][n+12]);
+
+		for (i = 1; i < n_src; i++) {
+			in[0] = _mm_add_ps(in[0], _mm_load_ps(&s[i][n+ 0]));
+			in[1] = _mm_add_ps(in[1], _mm_load_ps(&s[i][n+ 4]));
+			in[2] = _mm_add_ps(in[2], _mm_load_ps(&s[i][n+ 8]));
+			in[3] = _mm_add_ps(in[3], _mm_load_ps(&s[i][n+12]));
+		}
+		_mm_store_ps(&d[n+ 0], _mm_mul_ps(in[0], g));
+		_mm_store_ps(&d[n+ 4], _mm_mul_ps(in[1], g));
+		_mm_store_ps(&d[n+ 8], _mm_mul_ps(in[2], g));
+		_mm_store_ps(&d[n+12], _mm_mul_ps(in[3], g));
+	}
+	for (; n < n_samples; n++) {
+		in[0] = _mm_load_ss(&s[0][n]);
+		for (i = 1; i < n_src; i++)
+			in[0] = _mm_add_ss(in[0], _mm_load_ss(&s[i][n]));
+		_mm_store_ss(&d[n], _mm_mul_ss(in[0], g));
+	}
+}
+
+static void dsp_add_n_gain_sse(void *obj,
+		float * SPA_RESTRICT dst,
+		const float * SPA_RESTRICT src[], uint32_t n_src,
+		float gain[], uint32_t n_gain, uint32_t n_samples)
+{
+	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]);
+	}
+}
+
+void dsp_mix_gain_sse(void *obj,
+		float * SPA_RESTRICT dst,
+		const float * SPA_RESTRICT src[], uint32_t n_src,
+		float gain[], uint32_t n_gain, uint32_t n_samples)
+{
+	if (n_src == 0) {
+		memset(dst, 0, n_samples * sizeof(float));
+	} else if (n_src == 1 && gain[0] == 1.0f) {
+		if (dst != src[0])
+			spa_memcpy(dst, src[0], n_samples * sizeof(float));
+	} else {
+		if (n_gain == 0)
+			dsp_add_sse(obj, dst, src, n_src, n_samples);
+		else if (n_gain < n_src)
+			dsp_add_1_gain_sse(obj, dst, src, n_src, gain[0], n_samples);
+		else
+			dsp_add_n_gain_sse(obj, dst, src, n_src, gain, n_gain, n_samples);
+	}
+}
+
+void dsp_sum_sse(void *obj, float *r, const float *a, const float *b, uint32_t n_samples)
+{
+	uint32_t n, unrolled;
+	__m128 in[4];
+
+	unrolled = n_samples & ~15;
+
+	if (SPA_LIKELY(SPA_IS_ALIGNED(r, 16)) &&
+	    SPA_LIKELY(SPA_IS_ALIGNED(a, 16)) &&
+	    SPA_LIKELY(SPA_IS_ALIGNED(b, 16))) {
+		for (n = 0; n < unrolled; n += 16) {
+			in[0] = _mm_load_ps(&a[n+ 0]);
+			in[1] = _mm_load_ps(&a[n+ 4]);
+			in[2] = _mm_load_ps(&a[n+ 8]);
+			in[3] = _mm_load_ps(&a[n+12]);
+
+			in[0] = _mm_add_ps(in[0], _mm_load_ps(&b[n+ 0]));
+			in[1] = _mm_add_ps(in[1], _mm_load_ps(&b[n+ 4]));
+			in[2] = _mm_add_ps(in[2], _mm_load_ps(&b[n+ 8]));
+			in[3] = _mm_add_ps(in[3], _mm_load_ps(&b[n+12]));
+
+			_mm_store_ps(&r[n+ 0], in[0]);
+			_mm_store_ps(&r[n+ 4], in[1]);
+			_mm_store_ps(&r[n+ 8], in[2]);
+			_mm_store_ps(&r[n+12], in[3]);
+		}
+	} else {
+		for (n = 0; n < unrolled; n += 16) {
+			in[0] = _mm_loadu_ps(&a[n+ 0]);
+			in[1] = _mm_loadu_ps(&a[n+ 4]);
+			in[2] = _mm_loadu_ps(&a[n+ 8]);
+			in[3] = _mm_loadu_ps(&a[n+12]);
+
+			in[0] = _mm_add_ps(in[0], _mm_loadu_ps(&b[n+ 0]));
+			in[1] = _mm_add_ps(in[1], _mm_loadu_ps(&b[n+ 4]));
+			in[2] = _mm_add_ps(in[2], _mm_loadu_ps(&b[n+ 8]));
+			in[3] = _mm_add_ps(in[3], _mm_loadu_ps(&b[n+12]));
+
+			_mm_storeu_ps(&r[n+ 0], in[0]);
+			_mm_storeu_ps(&r[n+ 4], in[1]);
+			_mm_storeu_ps(&r[n+ 8], in[2]);
+			_mm_storeu_ps(&r[n+12], in[3]);
+		}
+	}
+	for (; n < n_samples; n++) {
+		in[0] = _mm_load_ss(&a[n]);
+		in[0] = _mm_add_ss(in[0], _mm_load_ss(&b[n]));
+		_mm_store_ss(&r[n], in[0]);
+	}
+}
+
+static void dsp_biquad_run1_sse(void *obj, struct biquad *bq,
+		float *out, const float *in, uint32_t n_samples)
+{
+	__m128 x, y, z;
+	__m128 b012;
+	__m128 a12;
+	__m128 x12;
+	uint32_t i;
+
+	b012 = _mm_setr_ps(bq->b0, bq->b1, bq->b2, 0.0f);  /* b0  b1  b2  0 */
+	a12 = _mm_setr_ps(0.0f, bq->a1, bq->a2, 0.0f);	  /* 0   a1  a2  0 */
+	x12 = _mm_setr_ps(bq->x1, bq->x2, 0.0f, 0.0f);	  /* x1  x2  0   0 */
+
+	for (i = 0; i < n_samples; i++) {
+		x = _mm_load1_ps(&in[i]);		/*  x         x         x      x */
+		z = _mm_mul_ps(x, b012);		/*  b0*x      b1*x      b2*x   0 */
+		z = _mm_add_ps(z, x12); 		/*  b0*x+x1   b1*x+x2   b2*x   0 */
+		_mm_store_ss(&out[i], z);		/*  out[i] = b0*x+x1 */
+		y = _mm_shuffle_ps(z, z, _MM_SHUFFLE(0,0,0,0));	/*  b0*x+x1  b0*x+x1  b0*x+x1  b0*x+x1 = y*/
+		y = _mm_mul_ps(y, a12);		        /*  0        a1*y     a2*y     0 */
+		y = _mm_sub_ps(z, y);	 		/*  y        x1       x2       0 */
+		x12 = _mm_shuffle_ps(y, y, _MM_SHUFFLE(3,3,2,1));    /*  x1  x2  0  0*/
+	}
+#define F(x) (isnormal(x) ? (x) : 0.0f)
+	bq->x1 = F(x12[0]);
+	bq->x2 = F(x12[1]);
+#undef F
+}
+
+static void dsp_biquad2_run_sse(void *obj, struct biquad *bq,
+		float *out, const float *in, uint32_t n_samples)
+{
+	__m128 x, y, z;
+	__m128 b0, b1;
+	__m128 a0, a1;
+	__m128 x0, x1;
+	uint32_t i;
+
+	b0 = _mm_setr_ps(bq[0].b0, bq[0].b1, bq[0].b2, 0.0f);  /* b0  b1  b2  0 */
+	a0 = _mm_setr_ps(0.0f, bq[0].a1, bq[0].a2, 0.0f);	    /* 0   a1  a2  0 */
+	x0 = _mm_setr_ps(bq[0].x1, bq[0].x2, 0.0f, 0.0f);	    /* x1  x2  0   0 */
+
+	b1 = _mm_setr_ps(bq[1].b0, bq[1].b1, bq[1].b2, 0.0f);  /* b0  b1  b2  0 */
+	a1 = _mm_setr_ps(0.0f, bq[1].a1, bq[1].a2, 0.0f);	    /* 0   a1  a2  0 */
+	x1 = _mm_setr_ps(bq[1].x1, bq[1].x2, 0.0f, 0.0f);	    /* x1  x2  0   0 */
+
+	for (i = 0; i < n_samples; i++) {
+		x = _mm_load1_ps(&in[i]);			/*  x         x         x      x */
+
+		z = _mm_mul_ps(x, b0);				/*  b0*x      b1*x      b2*x   0 */
+		z = _mm_add_ps(z, x0); 				/*  b0*x+x1   b1*x+x2   b2*x   0 */
+		y = _mm_shuffle_ps(z, z, _MM_SHUFFLE(0,0,0,0));	/*  b0*x+x1  b0*x+x1  b0*x+x1  b0*x+x1 = y*/
+		x = _mm_mul_ps(y, a0);			        /*  0        a1*y     a2*y     0 */
+		x = _mm_sub_ps(z, x);	 			/*  y        x1       x2       0 */
+		x0 = _mm_shuffle_ps(x, x, _MM_SHUFFLE(3,3,2,1));    /*  x1  x2  0  0*/
+
+		z = _mm_mul_ps(y, b1);				/*  b0*x      b1*x      b2*x   0 */
+		z = _mm_add_ps(z, x1); 				/*  b0*x+x1   b1*x+x2   b2*x   0 */
+		x = _mm_shuffle_ps(z, z, _MM_SHUFFLE(0,0,0,0));	/*  b0*x+x1  b0*x+x1  b0*x+x1  b0*x+x1 = y*/
+		y = _mm_mul_ps(x, a1);			        /*  0        a1*y     a2*y     0 */
+		y = _mm_sub_ps(z, y);	 			/*  y        x1       x2       0 */
+		x1 = _mm_shuffle_ps(y, y, _MM_SHUFFLE(3,3,2,1));    /*  x1  x2  0  0*/
+
+		_mm_store_ss(&out[i], x);			/*  out[i] = b0*x+x1 */
+	}
+#define F(x) (isnormal(x) ? (x) : 0.0f)
+	bq[0].x1 = F(x0[0]);
+	bq[0].x2 = F(x0[1]);
+	bq[1].x1 = F(x1[0]);
+	bq[1].x2 = F(x1[1]);
+#undef F
+}
+
+static void dsp_biquad_run2_sse(void *obj, struct biquad *bq, uint32_t bq_stride,
+		float **out, const float **in, uint32_t n_samples)
+{
+	__m128 x, y, z;
+	__m128 b0, b1, b2;
+	__m128 a1, a2;
+	__m128 x1, x2;
+	uint32_t i;
+
+	b0 = _mm_setr_ps(bq[0].b0, bq[bq_stride].b0, 0.0f, 0.0f);  /* b00  b10  0  0 */
+	b1 = _mm_setr_ps(bq[0].b1, bq[bq_stride].b1, 0.0f, 0.0f);  /* b01  b11  0  0 */
+	b2 = _mm_setr_ps(bq[0].b2, bq[bq_stride].b2, 0.0f, 0.0f);  /* b02  b12  0  0 */
+	a1 = _mm_setr_ps(bq[0].a1, bq[bq_stride].a1, 0.0f, 0.0f);  /* b00  b10  0  0 */
+	a2 = _mm_setr_ps(bq[0].a2, bq[bq_stride].a2, 0.0f, 0.0f);  /* b01  b11  0  0 */
+	x1 = _mm_setr_ps(bq[0].x1, bq[bq_stride].x1, 0.0f, 0.0f);  /* b00  b10  0  0 */
+	x2 = _mm_setr_ps(bq[0].x2, bq[bq_stride].x2, 0.0f, 0.0f);  /* b01  b11  0  0 */
+
+	for (i = 0; i < n_samples; i++) {
+		x = _mm_setr_ps(in[0][i], in[1][i], 0.0f, 0.0f);
+
+		y = _mm_mul_ps(x, b0);		/* y = x * b0 */
+		y = _mm_add_ps(y, x1);		/* y = x * b0 + x1*/
+		z = _mm_mul_ps(y, a1);		/* z = a1 * y */
+		x1 = _mm_mul_ps(x, b1);		/* x1 = x * b1 */
+		x1 = _mm_add_ps(x1, x2);	/* x1 = x * b1 + x2*/
+		x1 = _mm_sub_ps(x1, z);		/* x1 = x * b1 + x2 - a1 * y*/
+		z = _mm_mul_ps(y, a2);		/* z = a2 * y */
+		x2 = _mm_mul_ps(x, b2);		/* x2 = x * b2 */
+		x2 = _mm_sub_ps(x2, z);		/* x2 = x * b2 - a2 * y*/
+
+		out[0][i] = y[0];
+		out[1][i] = y[1];
+	}
+#define F(x) (isnormal(x) ? (x) : 0.0f)
+	bq[0*bq_stride].x1 = F(x1[0]);
+	bq[0*bq_stride].x2 = F(x2[0]);
+	bq[1*bq_stride].x1 = F(x1[1]);
+	bq[1*bq_stride].x2 = F(x2[1]);
+#undef F
+}
+
+
+static void dsp_biquad2_run2_sse(void *obj, struct biquad *bq, uint32_t bq_stride,
+		float **out, const float **in, uint32_t n_samples)
+{
+	__m128 x, y, z;
+	__m128 b00, b01, b02, b10, b11, b12;
+	__m128 a01, a02, a11, a12;
+	__m128 x01, x02, x11, x12;
+	uint32_t i;
+
+	b00 = _mm_setr_ps(bq[0].b0, bq[bq_stride].b0, 0.0f, 0.0f);  /* b00  b10  0  0 */
+	b01 = _mm_setr_ps(bq[0].b1, bq[bq_stride].b1, 0.0f, 0.0f);  /* b01  b11  0  0 */
+	b02 = _mm_setr_ps(bq[0].b2, bq[bq_stride].b2, 0.0f, 0.0f);  /* b02  b12  0  0 */
+	a01 = _mm_setr_ps(bq[0].a1, bq[bq_stride].a1, 0.0f, 0.0f);  /* b00  b10  0  0 */
+	a02 = _mm_setr_ps(bq[0].a2, bq[bq_stride].a2, 0.0f, 0.0f);  /* b01  b11  0  0 */
+	x01 = _mm_setr_ps(bq[0].x1, bq[bq_stride].x1, 0.0f, 0.0f);  /* b00  b10  0  0 */
+	x02 = _mm_setr_ps(bq[0].x2, bq[bq_stride].x2, 0.0f, 0.0f);  /* b01  b11  0  0 */
+
+	b10 = _mm_setr_ps(bq[1].b0, bq[bq_stride+1].b0, 0.0f, 0.0f);  /* b00  b10  0  0 */
+	b11 = _mm_setr_ps(bq[1].b1, bq[bq_stride+1].b1, 0.0f, 0.0f);  /* b01  b11  0  0 */
+	b12 = _mm_setr_ps(bq[1].b2, bq[bq_stride+1].b2, 0.0f, 0.0f);  /* b02  b12  0  0 */
+	a11 = _mm_setr_ps(bq[1].a1, bq[bq_stride+1].a1, 0.0f, 0.0f);  /* b00  b10  0  0 */
+	a12 = _mm_setr_ps(bq[1].a2, bq[bq_stride+1].a2, 0.0f, 0.0f);  /* b01  b11  0  0 */
+	x11 = _mm_setr_ps(bq[1].x1, bq[bq_stride+1].x1, 0.0f, 0.0f);  /* b00  b10  0  0 */
+	x12 = _mm_setr_ps(bq[1].x2, bq[bq_stride+1].x2, 0.0f, 0.0f);  /* b01  b11  0  0 */
+
+	for (i = 0; i < n_samples; i++) {
+		x = _mm_setr_ps(in[0][i], in[1][i], 0.0f, 0.0f);
+
+		y = _mm_mul_ps(x, b00);		/* y = x * b0 */
+		y = _mm_add_ps(y, x01);		/* y = x * b0 + x1*/
+		z = _mm_mul_ps(y, a01);		/* z = a1 * y */
+		x01 = _mm_mul_ps(x, b01);	/* x1 = x * b1 */
+		x01 = _mm_add_ps(x01, x02);	/* x1 = x * b1 + x2*/
+		x01 = _mm_sub_ps(x01, z);	/* x1 = x * b1 + x2 - a1 * y*/
+		z = _mm_mul_ps(y, a02);		/* z = a2 * y */
+		x02 = _mm_mul_ps(x, b02);	/* x2 = x * b2 */
+		x02 = _mm_sub_ps(x02, z);	/* x2 = x * b2 - a2 * y*/
+
+		x = y;
+
+		y = _mm_mul_ps(x, b10);		/* y = x * b0 */
+		y = _mm_add_ps(y, x11);		/* y = x * b0 + x1*/
+		z = _mm_mul_ps(y, a11);		/* z = a1 * y */
+		x11 = _mm_mul_ps(x, b11);	/* x1 = x * b1 */
+		x11 = _mm_add_ps(x11, x12);	/* x1 = x * b1 + x2*/
+		x11 = _mm_sub_ps(x11, z);	/* x1 = x * b1 + x2 - a1 * y*/
+		z = _mm_mul_ps(y, a12);		/* z = a2 * y*/
+		x12 = _mm_mul_ps(x, b12);	/* x2 = x * b2 */
+		x12 = _mm_sub_ps(x12, z);	/* x2 = x * b2 - a2 * y*/
+
+		out[0][i] = y[0];
+		out[1][i] = y[1];
+	}
+#define F(x) (isnormal(x) ? (x) : 0.0f)
+	bq[0*bq_stride+0].x1 = F(x01[0]);
+	bq[0*bq_stride+0].x2 = F(x02[0]);
+	bq[1*bq_stride+0].x1 = F(x01[1]);
+	bq[1*bq_stride+0].x2 = F(x02[1]);
+
+	bq[0*bq_stride+1].x1 = F(x11[0]);
+	bq[0*bq_stride+1].x2 = F(x12[0]);
+	bq[1*bq_stride+1].x1 = F(x11[1]);
+	bq[1*bq_stride+1].x2 = F(x12[1]);
+#undef F
+}
+
+static void dsp_biquad_run4_sse(void *obj, struct biquad *bq, uint32_t bq_stride,
+		float **out, const float **in, uint32_t n_samples)
+{
+	__m128 x, y, z;
+	__m128 b0, b1, b2;
+	__m128 a1, a2;
+	__m128 x1, x2;
+	uint32_t i;
+
+	b0 = _mm_setr_ps(bq[0].b0, bq[bq_stride].b0, bq[2*bq_stride].b0, bq[3*bq_stride].b0);
+	b1 = _mm_setr_ps(bq[0].b1, bq[bq_stride].b1, bq[2*bq_stride].b1, bq[3*bq_stride].b1);
+	b2 = _mm_setr_ps(bq[0].b2, bq[bq_stride].b2, bq[2*bq_stride].b2, bq[3*bq_stride].b2);
+	a1 = _mm_setr_ps(bq[0].a1, bq[bq_stride].a1, bq[2*bq_stride].a1, bq[3*bq_stride].a1);
+	a2 = _mm_setr_ps(bq[0].a2, bq[bq_stride].a2, bq[2*bq_stride].a2, bq[3*bq_stride].a2);
+	x1 = _mm_setr_ps(bq[0].x1, bq[bq_stride].x1, bq[2*bq_stride].x1, bq[3*bq_stride].x1);
+	x2 = _mm_setr_ps(bq[0].x2, bq[bq_stride].x2, bq[2*bq_stride].x2, bq[3*bq_stride].x2);
+
+	for (i = 0; i < n_samples; i++) {
+		x = _mm_setr_ps(in[0][i], in[1][i], in[2][i], in[3][i]);
+
+		y = _mm_mul_ps(x, b0);		/* y = x * b0 */
+		y = _mm_add_ps(y, x1);		/* y = x * b0 + x1*/
+		z = _mm_mul_ps(y, a1);		/* z = a1 * y */
+		x1 = _mm_mul_ps(x, b1);		/* x1 = x * b1 */
+		x1 = _mm_add_ps(x1, x2);	/* x1 = x * b1 + x2*/
+		x1 = _mm_sub_ps(x1, z);		/* x1 = x * b1 + x2 - a1 * y*/
+		z = _mm_mul_ps(y, a2);		/* z = a2 * y */
+		x2 = _mm_mul_ps(x, b2);		/* x2 = x * b2 */
+		x2 = _mm_sub_ps(x2, z);		/* x2 = x * b2 - a2 * y*/
+
+		out[0][i] = y[0];
+		out[1][i] = y[1];
+		out[2][i] = y[2];
+		out[3][i] = y[3];
+	}
+#define F(x) (isnormal(x) ? (x) : 0.0f)
+	bq[0*bq_stride].x1 = F(x1[0]);
+	bq[0*bq_stride].x2 = F(x2[0]);
+	bq[1*bq_stride].x1 = F(x1[1]);
+	bq[1*bq_stride].x2 = F(x2[1]);
+	bq[2*bq_stride].x1 = F(x1[2]);
+	bq[2*bq_stride].x2 = F(x2[2]);
+	bq[3*bq_stride].x1 = F(x1[3]);
+	bq[3*bq_stride].x2 = F(x2[3]);
+#undef F
+}
+
+static void dsp_biquad2_run4_sse(void *obj, struct biquad *bq, uint32_t bq_stride,
+		float **out, const float **in, uint32_t n_samples)
+{
+	__m128 x, y, z;
+	__m128 b00, b01, b02, b10, b11, b12;
+	__m128 a01, a02, a11, a12;
+	__m128 x01, x02, x11, x12;
+	uint32_t i;
+
+	b00 = _mm_setr_ps(bq[0].b0, bq[bq_stride].b0, bq[2*bq_stride].b0, bq[3*bq_stride].b0);
+	b01 = _mm_setr_ps(bq[0].b1, bq[bq_stride].b1, bq[2*bq_stride].b1, bq[3*bq_stride].b1);
+	b02 = _mm_setr_ps(bq[0].b2, bq[bq_stride].b2, bq[2*bq_stride].b2, bq[3*bq_stride].b2);
+	a01 = _mm_setr_ps(bq[0].a1, bq[bq_stride].a1, bq[2*bq_stride].a1, bq[3*bq_stride].a1);
+	a02 = _mm_setr_ps(bq[0].a2, bq[bq_stride].a2, bq[2*bq_stride].a2, bq[3*bq_stride].a2);
+	x01 = _mm_setr_ps(bq[0].x1, bq[bq_stride].x1, bq[2*bq_stride].x1, bq[3*bq_stride].x1);
+	x02 = _mm_setr_ps(bq[0].x2, bq[bq_stride].x2, bq[2*bq_stride].x2, bq[3*bq_stride].x2);
+
+	b10 = _mm_setr_ps(bq[1].b0, bq[bq_stride+1].b0, bq[2*bq_stride+1].b0, bq[3*bq_stride+1].b0);
+	b11 = _mm_setr_ps(bq[1].b1, bq[bq_stride+1].b1, bq[2*bq_stride+1].b1, bq[3*bq_stride+1].b1);
+	b12 = _mm_setr_ps(bq[1].b2, bq[bq_stride+1].b2, bq[2*bq_stride+1].b2, bq[3*bq_stride+1].b2);
+	a11 = _mm_setr_ps(bq[1].a1, bq[bq_stride+1].a1, bq[2*bq_stride+1].a1, bq[3*bq_stride+1].a1);
+	a12 = _mm_setr_ps(bq[1].a2, bq[bq_stride+1].a2, bq[2*bq_stride+1].a2, bq[3*bq_stride+1].a2);
+	x11 = _mm_setr_ps(bq[1].x1, bq[bq_stride+1].x1, bq[2*bq_stride+1].x1, bq[3*bq_stride+1].x1);
+	x12 = _mm_setr_ps(bq[1].x2, bq[bq_stride+1].x2, bq[2*bq_stride+1].x2, bq[3*bq_stride+1].x2);
+
+	for (i = 0; i < n_samples; i++) {
+		x = _mm_setr_ps(in[0][i], in[1][i], in[2][i], in[3][i]);
+
+		y = _mm_mul_ps(x, b00);		/* y = x * b0 */
+		y = _mm_add_ps(y, x01);		/* y = x * b0 + x1*/
+		z = _mm_mul_ps(y, a01);		/* z = a1 * y */
+		x01 = _mm_mul_ps(x, b01);	/* x1 = x * b1 */
+		x01 = _mm_add_ps(x01, x02);	/* x1 = x * b1 + x2*/
+		x01 = _mm_sub_ps(x01, z);	/* x1 = x * b1 + x2 - a1 * y*/
+		z = _mm_mul_ps(y, a02);		/* z = a2 * y */
+		x02 = _mm_mul_ps(x, b02);	/* x2 = x * b2 */
+		x02 = _mm_sub_ps(x02, z);	/* x2 = x * b2 - a2 * y*/
+
+		x = y;
+
+		y = _mm_mul_ps(x, b10);		/* y = x * b0 */
+		y = _mm_add_ps(y, x11);		/* y = x * b0 + x1*/
+		z = _mm_mul_ps(y, a11);		/* z = a1 * y */
+		x11 = _mm_mul_ps(x, b11);	/* x1 = x * b1 */
+		x11 = _mm_add_ps(x11, x12);	/* x1 = x * b1 + x2*/
+		x11 = _mm_sub_ps(x11, z);	/* x1 = x * b1 + x2 - a1 * y*/
+		z = _mm_mul_ps(y, a12);		/* z = a2 * y*/
+		x12 = _mm_mul_ps(x, b12);	/* x2 = x * b2 */
+		x12 = _mm_sub_ps(x12, z);	/* x2 = x * b2 - a2 * y*/
+
+		out[0][i] = y[0];
+		out[1][i] = y[1];
+		out[2][i] = y[2];
+		out[3][i] = y[3];
+	}
+#define F(x) (isnormal(x) ? (x) : 0.0f)
+	bq[0*bq_stride+0].x1 = F(x01[0]);
+	bq[0*bq_stride+0].x2 = F(x02[0]);
+	bq[1*bq_stride+0].x1 = F(x01[1]);
+	bq[1*bq_stride+0].x2 = F(x02[1]);
+	bq[2*bq_stride+0].x1 = F(x01[2]);
+	bq[2*bq_stride+0].x2 = F(x02[2]);
+	bq[3*bq_stride+0].x1 = F(x01[3]);
+	bq[3*bq_stride+0].x2 = F(x02[3]);
+
+	bq[0*bq_stride+1].x1 = F(x11[0]);
+	bq[0*bq_stride+1].x2 = F(x12[0]);
+	bq[1*bq_stride+1].x1 = F(x11[1]);
+	bq[1*bq_stride+1].x2 = F(x12[1]);
+	bq[2*bq_stride+1].x1 = F(x11[2]);
+	bq[2*bq_stride+1].x2 = F(x12[2]);
+	bq[3*bq_stride+1].x1 = F(x11[3]);
+	bq[3*bq_stride+1].x2 = F(x12[3]);
+#undef F
+}
+
+void dsp_biquad_run_sse(void *obj, struct biquad *bq, uint32_t n_bq, uint32_t bq_stride,
+		float * SPA_RESTRICT out[], const float * SPA_RESTRICT in[],
+		uint32_t n_src, uint32_t n_samples)
+{
+	uint32_t i, j, bqs2 = bq_stride*2, bqs4 = bqs2*2;
+	uint32_t iunrolled4 = n_src & ~3;
+	uint32_t iunrolled2 = n_src & ~1;
+	uint32_t junrolled2 = n_bq & ~1;
+
+	for (i = 0; i < iunrolled4; i+=4, bq+=bqs4) {
+		const float *s[4] = { in[i], in[i+1], in[i+2], in[i+3] };
+		float *d[4] = { out[i], out[i+1], out[i+2], out[i+3] };
+
+		if (s[0] == NULL || s[1] == NULL || s[2] == NULL || s[3] == NULL ||
+		    d[0] == NULL || d[1] == NULL || d[2] == NULL || d[3] == NULL)
+			break;
+
+		j = 0;
+		if (j < junrolled2) {
+			dsp_biquad2_run4_sse(obj, &bq[j], bq_stride, d, s, n_samples);
+			s[0] = d[0];
+			s[1] = d[1];
+			s[2] = d[2];
+			s[3] = d[3];
+			j+=2;
+		}
+		for (; j < junrolled2; j+=2) {
+			dsp_biquad2_run4_sse(obj, &bq[j], bq_stride, d, s, n_samples);
+		}
+		if (j < n_bq) {
+			dsp_biquad_run4_sse(obj, &bq[j], bq_stride, d, s, n_samples);
+		}
+	}
+	for (; i < iunrolled2; i+=2, bq+=bqs2) {
+		const float *s[2] = { in[i], in[i+1] };
+		float *d[2] = { out[i], out[i+1] };
+
+		if (s[0] == NULL || s[1] == NULL || d[0] == NULL || d[1] == NULL)
+			break;
+
+		j = 0;
+		if (j < junrolled2) {
+			dsp_biquad2_run2_sse(obj, &bq[j], bq_stride, d, s, n_samples);
+			s[0] = d[0];
+			s[1] = d[1];
+			j+=2;
+		}
+		for (; j < junrolled2; j+=2) {
+			dsp_biquad2_run2_sse(obj, &bq[j], bq_stride, d, s, n_samples);
+		}
+		if (j < n_bq) {
+			dsp_biquad_run2_sse(obj, &bq[j], bq_stride, d, s, n_samples);
+		}
+	}
+	for (; i < n_src; i++, bq+=bq_stride) {
+		const float *s = in[i];
+		float *d = out[i];
+		if (s == NULL || d == NULL)
+			continue;
+
+		j = 0;
+		if (j < junrolled2) {
+			dsp_biquad2_run_sse(obj, &bq[j], d, s, n_samples);
+			s = d;
+			j+=2;
+		}
+		for (; j < junrolled2; j+=2) {
+			dsp_biquad2_run_sse(obj, &bq[j], d, s, n_samples);
+		}
+		if (j < n_bq) {
+			dsp_biquad_run1_sse(obj, &bq[j], d, s, n_samples);
+		}
+	}
+}
+
+void dsp_delay_sse(void *obj, float *buffer, uint32_t *pos, uint32_t n_buffer, uint32_t delay,
+		float *dst, const float *src, uint32_t n_samples)
+{
+	__m128 t[1];
+	uint32_t w = *pos;
+	uint32_t o = n_buffer - delay;
+	uint32_t n, unrolled;
+
+	if (SPA_IS_ALIGNED(src, 16) &&
+	    SPA_IS_ALIGNED(dst, 16))
+		unrolled = n_samples & ~3;
+	else
+		unrolled = 0;
+
+	for(n = 0; n < unrolled; n += 4) {
+		t[0] = _mm_load_ps(&src[n]);
+		_mm_storeu_ps(&buffer[w], t[0]);
+		_mm_storeu_ps(&buffer[w+n_buffer], t[0]);
+		t[0] = _mm_loadu_ps(&buffer[w+o]);
+		_mm_store_ps(&dst[n], t[0]);
+		w = w + 4 >= n_buffer ? 0 : w + 4;
+	}
+	for(; n < n_samples; n++) {
+		t[0] = _mm_load_ss(&src[n]);
+		_mm_store_ss(&buffer[w], t[0]);
+		_mm_store_ss(&buffer[w+n_buffer], t[0]);
+		t[0] = _mm_load_ss(&buffer[w+o]);
+		_mm_store_ss(&dst[n], t[0]);
+		w = w + 1 >= n_buffer ? 0 : w + 1;
+	}
+	*pos = w;
+}
+
+inline static void _mm_mul_pz(__m128 *a, __m128 *b, __m128 *d)
+{
+    __m128 ar, ai, br, bi, arbr, arbi, aibi, aibr, dr, di;
+    ar = _mm_shuffle_ps(a[0], a[1], _MM_SHUFFLE(2,0,2,0));	/* ar0 ar1 ar2 ar3 */
+    ai = _mm_shuffle_ps(a[0], a[1], _MM_SHUFFLE(3,1,3,1));	/* ai0 ai1 ai2 ai3 */
+    br = _mm_shuffle_ps(b[0], b[1], _MM_SHUFFLE(2,0,2,0));	/* br0 br1 br2 br3 */
+    bi = _mm_shuffle_ps(b[0], b[1], _MM_SHUFFLE(3,1,3,1))	/* bi0 bi1 bi2 bi3 */;
+
+    arbr = _mm_mul_ps(ar, br);	/* ar * br */
+    arbi = _mm_mul_ps(ar, bi);	/* ar * bi */
+
+    aibi = _mm_mul_ps(ai, bi);	/* ai * bi */
+    aibr = _mm_mul_ps(ai, br);	/* ai * br */
+
+    dr = _mm_sub_ps(arbr, aibi);	/* ar * br  - ai * bi */
+    di = _mm_add_ps(arbi, aibr);	/* ar * bi  + ai * br */
+    d[0] = _mm_unpacklo_ps(dr, di);
+    d[1] = _mm_unpackhi_ps(dr, di);
+}
+
+void dsp_fft_cmul_sse(void *obj, void *fft,
+	float * SPA_RESTRICT dst, const float * SPA_RESTRICT a,
+	const float * SPA_RESTRICT b, uint32_t len, const float scale)
+{
+#ifdef HAVE_FFTW
+	__m128 s = _mm_set1_ps(scale);
+	__m128 aa[2], bb[2], dd[2];
+	uint32_t i, unrolled;
+
+	if (SPA_IS_ALIGNED(a, 16) &&
+	    SPA_IS_ALIGNED(b, 16) &&
+	    SPA_IS_ALIGNED(dst, 16))
+		unrolled = len & ~3;
+	else
+		unrolled = 0;
+
+	for (i = 0; i < unrolled; i+=4) {
+		aa[0] = _mm_load_ps(&a[2*i]);	/* ar0 ai0 ar1 ai1 */
+		aa[1] = _mm_load_ps(&a[2*i+4]);	/* ar1 ai1 ar2 ai2 */
+		bb[0] = _mm_load_ps(&b[2*i]);	/* br0 bi0 br1 bi1 */
+		bb[1] = _mm_load_ps(&b[2*i+4]);	/* br2 bi2 br3 bi3 */
+		_mm_mul_pz(aa, bb, dd);
+		dd[0] = _mm_mul_ps(dd[0], s);
+		dd[1] = _mm_mul_ps(dd[1], s);
+		_mm_store_ps(&dst[2*i], dd[0]);
+		_mm_store_ps(&dst[2*i+4], dd[1]);
+	}
+	for (; i < len; i++) {
+		dst[2*i  ] = (a[2*i] * b[2*i  ] - a[2*i+1] * b[2*i+1]) * scale;
+		dst[2*i+1] = (a[2*i] * b[2*i+1] + a[2*i+1] * b[2*i  ]) * scale;
+	}
+#else
+	pffft_zconvolve(fft, a, b, dst, scale);
+#endif
+}
+
+void dsp_fft_cmuladd_sse(void *obj, void *fft,
+	float * SPA_RESTRICT dst, const float * SPA_RESTRICT src,
+	const float * SPA_RESTRICT a, const float * SPA_RESTRICT b,
+	uint32_t len, const float scale)
+{
+#ifdef HAVE_FFTW
+	__m128 s = _mm_set1_ps(scale);
+	__m128 aa[2], bb[2], dd[2], t[2];
+	uint32_t i, unrolled;
+
+	if (SPA_IS_ALIGNED(a, 16) &&
+	    SPA_IS_ALIGNED(b, 16) &&
+	    SPA_IS_ALIGNED(src, 16) &&
+	    SPA_IS_ALIGNED(dst, 16))
+		unrolled = len & ~3;
+	else
+		unrolled = 0;
+
+	for (i = 0; i < unrolled; i+=4) {
+		aa[0] = _mm_load_ps(&a[2*i]);	/* ar0 ai0 ar1 ai1 */
+		aa[1] = _mm_load_ps(&a[2*i+4]);	/* ar1 ai1 ar2 ai2 */
+		bb[0] = _mm_load_ps(&b[2*i]);	/* br0 bi0 br1 bi1 */
+		bb[1] = _mm_load_ps(&b[2*i+4]);	/* br2 bi2 br3 bi3 */
+		_mm_mul_pz(aa, bb, dd);
+		dd[0] = _mm_mul_ps(dd[0], s);
+		dd[1] = _mm_mul_ps(dd[1], s);
+		t[0] = _mm_load_ps(&src[2*i]);
+		t[1] = _mm_load_ps(&src[2*i+4]);
+		t[0] = _mm_add_ps(t[0], dd[0]);
+		t[1] = _mm_add_ps(t[1], dd[1]);
+		_mm_store_ps(&dst[2*i], t[0]);
+		_mm_store_ps(&dst[2*i+4], t[1]);
+	}
+	for (; i < len; i++) {
+		dst[2*i  ] = src[2*i  ] + (a[2*i] * b[2*i  ] - a[2*i+1] * b[2*i+1]) * scale;
+		dst[2*i+1] = src[2*i+1] + (a[2*i] * b[2*i+1] + a[2*i+1] * b[2*i  ]) * scale;
+	}
+#else
+	pffft_zconvolve_accumulate(fft, a, b, src, dst, scale);
+#endif
+}
diff --git a/src/modules/module-filter-chain/dsp-ops.c b/spa/plugins/filter-graph/audio-dsp.c
similarity index 55%
rename from src/modules/module-filter-chain/dsp-ops.c
rename to spa/plugins/filter-graph/audio-dsp.c
index 7a21177b..fadd8534 100644
--- a/src/modules/module-filter-chain/dsp-ops.c
+++ b/spa/plugins/filter-graph/audio-dsp.c
@@ -5,35 +5,42 @@
 #include <string.h>
 #include <stdio.h>
 #include <math.h>
+#include <time.h>
 
 #include <spa/support/cpu.h>
 #include <spa/utils/defs.h>
 #include <spa/param/audio/format-utils.h>
 
-#include "dsp-ops.h"
+#include "pffft.h"
+
+#include "audio-dsp-impl.h"
 
 struct dsp_info {
 	uint32_t cpu_flags;
 
-	struct dsp_ops_funcs funcs;
+	struct spa_fga_dsp_methods funcs;
 };
 
-static struct dsp_info dsp_table[] =
+static const struct dsp_info dsp_table[] =
 {
 #if defined (HAVE_AVX)
 	{ SPA_CPU_FLAG_AVX,
 		.funcs.clear = dsp_clear_c,
 		.funcs.copy = dsp_copy_c,
-		.funcs.mix_gain = dsp_mix_gain_sse,
-		.funcs.biquad_run = dsp_biquad_run_c,
+		.funcs.mix_gain = dsp_mix_gain_avx,
+		.funcs.biquad_run = dsp_biquad_run_sse,
 		.funcs.sum = dsp_sum_avx,
 		.funcs.linear = dsp_linear_c,
 		.funcs.mult = dsp_mult_c,
 		.funcs.fft_new = dsp_fft_new_c,
 		.funcs.fft_free = dsp_fft_free_c,
+		.funcs.fft_memalloc = dsp_fft_memalloc_c,
+		.funcs.fft_memfree = dsp_fft_memfree_c,
+		.funcs.fft_memclear = dsp_fft_memclear_c,
 		.funcs.fft_run = dsp_fft_run_c,
-		.funcs.fft_cmul = dsp_fft_cmul_c,
-		.funcs.fft_cmuladd = dsp_fft_cmuladd_c,
+		.funcs.fft_cmul = dsp_fft_cmul_avx,
+		.funcs.fft_cmuladd = dsp_fft_cmuladd_avx,
+		.funcs.delay = dsp_delay_sse,
 	},
 #endif
 #if defined (HAVE_SSE)
@@ -41,15 +48,19 @@ static struct dsp_info dsp_table[] =
 		.funcs.clear = dsp_clear_c,
 		.funcs.copy = dsp_copy_c,
 		.funcs.mix_gain = dsp_mix_gain_sse,
-		.funcs.biquad_run = dsp_biquad_run_c,
+		.funcs.biquad_run = dsp_biquad_run_sse,
 		.funcs.sum = dsp_sum_sse,
 		.funcs.linear = dsp_linear_c,
 		.funcs.mult = dsp_mult_c,
 		.funcs.fft_new = dsp_fft_new_c,
 		.funcs.fft_free = dsp_fft_free_c,
+		.funcs.fft_memalloc = dsp_fft_memalloc_c,
+		.funcs.fft_memfree = dsp_fft_memfree_c,
+		.funcs.fft_memclear = dsp_fft_memclear_c,
 		.funcs.fft_run = dsp_fft_run_c,
-		.funcs.fft_cmul = dsp_fft_cmul_c,
-		.funcs.fft_cmuladd = dsp_fft_cmuladd_c,
+		.funcs.fft_cmul = dsp_fft_cmul_sse,
+		.funcs.fft_cmuladd = dsp_fft_cmuladd_sse,
+		.funcs.delay = dsp_delay_sse,
 	},
 #endif
 	{ 0,
@@ -62,9 +73,13 @@ static struct dsp_info dsp_table[] =
 		.funcs.mult = dsp_mult_c,
 		.funcs.fft_new = dsp_fft_new_c,
 		.funcs.fft_free = dsp_fft_free_c,
+		.funcs.fft_memalloc = dsp_fft_memalloc_c,
+		.funcs.fft_memfree = dsp_fft_memfree_c,
+		.funcs.fft_memclear = dsp_fft_memclear_c,
 		.funcs.fft_run = dsp_fft_run_c,
 		.funcs.fft_cmul = dsp_fft_cmul_c,
 		.funcs.fft_cmuladd = dsp_fft_cmuladd_c,
+		.funcs.delay = dsp_delay_c,
 	},
 };
 
@@ -79,23 +94,31 @@ static const struct dsp_info *find_dsp_info(uint32_t cpu_flags)
 	return NULL;
 }
 
-static void impl_dsp_ops_free(struct dsp_ops *ops)
+void spa_fga_dsp_free(struct spa_fga_dsp *dsp)
 {
-	spa_zero(*ops);
+	free(dsp);
 }
 
-int dsp_ops_init(struct dsp_ops *ops, uint32_t cpu_flags)
+struct spa_fga_dsp * spa_fga_dsp_new(uint32_t cpu_flags)
 {
 	const struct dsp_info *info;
+	struct spa_fga_dsp *dsp;
 
 	info = find_dsp_info(cpu_flags);
-	if (info == NULL)
-		return -ENOTSUP;
+	if (info == NULL) {
+		errno = ENOTSUP;
+		return NULL;
+	}
+	dsp = calloc(1, sizeof(*dsp));
+	if (dsp == NULL)
+		return NULL;
 
-	ops->cpu_flags = cpu_flags;
-	ops->priv = info;
-	ops->free = impl_dsp_ops_free;
-	ops->funcs = info->funcs;
+	pffft_select_cpu(cpu_flags);
+	dsp->cpu_flags = cpu_flags;
+	dsp->iface = SPA_INTERFACE_INIT(
+			SPA_TYPE_INTERFACE_FILTER_GRAPH_AudioDSP,
+			SPA_VERSION_FGA_DSP,
+			&info->funcs, dsp);
 
-	return 0;
+	return dsp;
 }
diff --git a/spa/plugins/filter-graph/audio-dsp.h b/spa/plugins/filter-graph/audio-dsp.h
new file mode 100644
index 00000000..4fc06eb7
--- /dev/null
+++ b/spa/plugins/filter-graph/audio-dsp.h
@@ -0,0 +1,168 @@
+/* Spa */
+/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */
+/* SPDX-License-Identifier: MIT */
+
+#ifndef SPA_FGA_DSP_H
+#define SPA_FGA_DSP_H
+
+#include <spa/utils/defs.h>
+#include <spa/utils/hook.h>
+
+#include "biquad.h"
+
+#define SPA_TYPE_INTERFACE_FILTER_GRAPH_AudioDSP	SPA_TYPE_INFO_INTERFACE_BASE "FilterGraph:AudioDSP"
+
+#define SPA_VERSION_FGA_DSP	0
+struct spa_fga_dsp {
+	struct spa_interface iface;
+	uint32_t cpu_flags;
+};
+
+struct spa_fga_dsp_methods {
+#define SPA_VERSION_FGA_DSP_METHODS		0
+	uint32_t version;
+
+	void (*clear) (void *obj, float * SPA_RESTRICT dst, uint32_t n_samples);
+	void (*copy) (void *obj,
+			float * SPA_RESTRICT dst,
+			const float * SPA_RESTRICT src, uint32_t n_samples);
+	void (*mix_gain) (void *obj,
+			float * SPA_RESTRICT dst,
+			const float * SPA_RESTRICT src[], uint32_t n_src,
+			float gain[], uint32_t n_gain, uint32_t n_samples);
+	void (*sum) (void *obj,
+			float * dst, const float * SPA_RESTRICT a,
+			const float * SPA_RESTRICT b, uint32_t n_samples);
+
+	void *(*fft_new) (void *obj, uint32_t size, bool real);
+	void (*fft_free) (void *obj, void *fft);
+	void *(*fft_memalloc) (void *obj, uint32_t size, bool real);
+	void (*fft_memfree) (void *obj, void *mem);
+	void (*fft_memclear) (void *obj, void *mem, uint32_t size, bool real);
+	void (*fft_run) (void *obj, void *fft, int direction,
+			const float * SPA_RESTRICT src, float * SPA_RESTRICT dst);
+	void (*fft_cmul) (void *obj, void *fft,
+			float * SPA_RESTRICT dst, const float * SPA_RESTRICT a,
+			const float * SPA_RESTRICT b, uint32_t len, const float scale);
+	void (*fft_cmuladd) (void *obj, void *fft,
+			float * dst, const float * src,
+			const float * SPA_RESTRICT a, const float * SPA_RESTRICT b,
+			uint32_t len, const float scale);
+	void (*linear) (void *obj,
+			float * dst, const float * SPA_RESTRICT src,
+			const float mult, const float add, uint32_t n_samples);
+	void (*mult) (void *obj,
+			float * SPA_RESTRICT dst,
+			const float * SPA_RESTRICT src[], uint32_t n_src, uint32_t n_samples);
+	void (*biquad_run) (void *obj, struct biquad *bq, uint32_t n_bq, uint32_t bq_stride,
+			float * SPA_RESTRICT out[], const float * SPA_RESTRICT in[],
+			uint32_t n_src, uint32_t n_samples);
+	void (*delay) (void *obj, float *buffer, uint32_t *pos, uint32_t n_buffer, uint32_t delay,
+			float *dst, const float *src, uint32_t n_samples);
+};
+
+static inline void spa_fga_dsp_clear(struct spa_fga_dsp *obj, float * SPA_RESTRICT dst, uint32_t n_samples)
+{
+	spa_api_method_v(spa_fga_dsp, &obj->iface, clear, 0,
+			dst, n_samples);
+}
+static inline void spa_fga_dsp_copy(struct spa_fga_dsp *obj,
+		float * SPA_RESTRICT dst,
+		const float * SPA_RESTRICT src, uint32_t n_samples)
+{
+	spa_api_method_v(spa_fga_dsp, &obj->iface, copy, 0,
+			dst, src, n_samples);
+}
+static inline void spa_fga_dsp_mix_gain(struct spa_fga_dsp *obj,
+		float * SPA_RESTRICT dst,
+		const float * SPA_RESTRICT src[], uint32_t n_src,
+		float gain[], uint32_t n_gain, uint32_t n_samples)
+{
+	spa_api_method_v(spa_fga_dsp, &obj->iface, mix_gain, 0,
+			dst, src, n_src, gain, n_gain, n_samples);
+}
+static inline void spa_fga_dsp_sum(struct spa_fga_dsp *obj,
+		float * dst, const float * SPA_RESTRICT a,
+		const float * SPA_RESTRICT b, uint32_t n_samples)
+{
+	spa_api_method_v(spa_fga_dsp, &obj->iface, sum, 0,
+			dst, a, b, n_samples);
+}
+
+static inline void *spa_fga_dsp_fft_new(struct spa_fga_dsp *obj, uint32_t size, bool real)
+{
+	return spa_api_method_r(void *, NULL, spa_fga_dsp, &obj->iface, fft_new, 0,
+			size, real);
+}
+static inline void spa_fga_dsp_fft_free(struct spa_fga_dsp *obj, void *fft)
+{
+	spa_api_method_v(spa_fga_dsp, &obj->iface, fft_free, 0,
+			fft);
+}
+static inline void *spa_fga_dsp_fft_memalloc(struct spa_fga_dsp *obj, uint32_t size, bool real)
+{
+	return spa_api_method_r(void *, NULL, spa_fga_dsp, &obj->iface, fft_memalloc, 0,
+			size, real);
+}
+static inline void spa_fga_dsp_fft_memfree(struct spa_fga_dsp *obj, void *mem)
+{
+	spa_api_method_v(spa_fga_dsp, &obj->iface, fft_memfree, 0,
+			mem);
+}
+static inline void spa_fga_dsp_fft_memclear(struct spa_fga_dsp *obj, void *mem, uint32_t size, bool real)
+{
+	spa_api_method_v(spa_fga_dsp, &obj->iface, fft_memclear, 0,
+			mem, size, real);
+}
+static inline void spa_fga_dsp_fft_run(struct spa_fga_dsp *obj, void *fft, int direction,
+		const float * SPA_RESTRICT src, float * SPA_RESTRICT dst)
+{
+	spa_api_method_v(spa_fga_dsp, &obj->iface, fft_run, 0,
+			fft, direction, src, dst);
+}
+static inline void spa_fga_dsp_fft_cmul(struct spa_fga_dsp *obj, void *fft,
+		float * SPA_RESTRICT dst, const float * SPA_RESTRICT a,
+		const float * SPA_RESTRICT b, uint32_t len, const float scale)
+{
+	spa_api_method_v(spa_fga_dsp, &obj->iface, fft_cmul, 0,
+			fft, dst, a, b, len, scale);
+}
+static inline void spa_fga_dsp_fft_cmuladd(struct spa_fga_dsp *obj, void *fft,
+		float * dst, const float * src,
+		const float * SPA_RESTRICT a, const float * SPA_RESTRICT b,
+		uint32_t len, const float scale)
+{
+	spa_api_method_v(spa_fga_dsp, &obj->iface, fft_cmuladd, 0,
+			fft, dst, src, a, b, len, scale);
+}
+static inline void spa_fga_dsp_linear(struct spa_fga_dsp *obj,
+		float * dst, const float * SPA_RESTRICT src,
+		const float mult, const float add, uint32_t n_samples)
+{
+	spa_api_method_v(spa_fga_dsp, &obj->iface, linear, 0,
+			dst, src, mult, add, n_samples);
+}
+static inline void spa_fga_dsp_mult(struct spa_fga_dsp *obj,
+		float * SPA_RESTRICT dst,
+		const float * SPA_RESTRICT src[], uint32_t n_src, uint32_t n_samples)
+{
+	spa_api_method_v(spa_fga_dsp, &obj->iface, mult, 0,
+			dst, src, n_src, n_samples);
+}
+static inline void spa_fga_dsp_biquad_run(struct spa_fga_dsp *obj,
+		struct biquad *bq, uint32_t n_bq, uint32_t bq_stride,
+		float * SPA_RESTRICT out[], const float * SPA_RESTRICT in[],
+		uint32_t n_src, uint32_t n_samples)
+{
+	spa_api_method_v(spa_fga_dsp, &obj->iface, biquad_run, 0,
+			bq, n_bq, bq_stride, out, in, n_src, n_samples);
+}
+static inline void spa_fga_dsp_delay(struct spa_fga_dsp *obj,
+		float *buffer, uint32_t *pos, uint32_t n_buffer, uint32_t delay,
+		float *dst, const float *src, uint32_t n_samples)
+{
+	spa_api_method_v(spa_fga_dsp, &obj->iface, delay, 0,
+			buffer, pos, n_buffer, delay, dst, src, n_samples);
+}
+
+#endif /* SPA_FGA_DSP_H */
diff --git a/spa/plugins/filter-graph/audio-plugin.h b/spa/plugins/filter-graph/audio-plugin.h
new file mode 100644
index 00000000..e05d7a80
--- /dev/null
+++ b/spa/plugins/filter-graph/audio-plugin.h
@@ -0,0 +1,95 @@
+/* PipeWire */
+/* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */
+/* SPDX-License-Identifier: MIT */
+
+#ifndef SPA_FGA_PLUGIN_H
+#define SPA_FGA_PLUGIN_H
+
+#include <stdint.h>
+#include <stddef.h>
+
+#include <spa/utils/defs.h>
+#include <spa/utils/hook.h>
+#include <spa/support/plugin.h>
+
+#define SPA_TYPE_INTERFACE_FILTER_GRAPH_AudioPlugin	SPA_TYPE_INFO_INTERFACE_BASE "FilterGraph:AudioPlugin"
+
+#define SPA_VERSION_FGA_PLUGIN	0
+struct spa_fga_plugin { struct spa_interface iface; };
+
+struct spa_fga_plugin_methods {
+#define SPA_VERSION_FGA_PLUGIN_METHODS		0
+	uint32_t version;
+
+	const struct spa_fga_descriptor *(*make_desc) (void *plugin, const char *name);
+};
+
+struct spa_fga_port {
+	uint32_t index;
+	const char *name;
+#define SPA_FGA_PORT_INPUT	(1ULL << 0)
+#define SPA_FGA_PORT_OUTPUT	(1ULL << 1)
+#define SPA_FGA_PORT_CONTROL	(1ULL << 2)
+#define SPA_FGA_PORT_AUDIO	(1ULL << 3)
+	uint64_t flags;
+
+#define SPA_FGA_HINT_BOOLEAN		(1ULL << 2)
+#define SPA_FGA_HINT_SAMPLE_RATE	(1ULL << 3)
+#define SPA_FGA_HINT_INTEGER		(1ULL << 5)
+	uint64_t hint;
+	float def;
+	float min;
+	float max;
+};
+
+#define SPA_FGA_IS_PORT_INPUT(x)	((x) & SPA_FGA_PORT_INPUT)
+#define SPA_FGA_IS_PORT_OUTPUT(x)	((x) & SPA_FGA_PORT_OUTPUT)
+#define SPA_FGA_IS_PORT_CONTROL(x)	((x) & SPA_FGA_PORT_CONTROL)
+#define SPA_FGA_IS_PORT_AUDIO(x)	((x) & SPA_FGA_PORT_AUDIO)
+
+struct spa_fga_descriptor {
+	const char *name;
+#define SPA_FGA_DESCRIPTOR_SUPPORTS_NULL_DATA	(1ULL << 0)
+#define SPA_FGA_DESCRIPTOR_COPY			(1ULL << 1)
+	uint64_t flags;
+
+	void (*free) (const struct spa_fga_descriptor *desc);
+
+	uint32_t n_ports;
+	struct spa_fga_port *ports;
+
+	void *(*instantiate) (const struct spa_fga_plugin *plugin, const struct spa_fga_descriptor *desc,
+			unsigned long SampleRate, int index, const char *config);
+
+	void (*cleanup) (void *instance);
+
+	void (*connect_port) (void *instance, unsigned long port, float *data);
+	void (*control_changed) (void *instance);
+
+	void (*activate) (void *instance);
+	void (*deactivate) (void *instance);
+
+	void (*run) (void *instance, unsigned long SampleCount);
+};
+
+static inline void spa_fga_descriptor_free(const struct spa_fga_descriptor *desc)
+{
+	if (desc->free)
+		desc->free(desc);
+}
+
+static inline const struct spa_fga_descriptor *
+spa_fga_plugin_make_desc(struct spa_fga_plugin *plugin, const char *name)
+{
+	return spa_api_method_r(const struct spa_fga_descriptor *, NULL,
+			spa_fga_plugin, &plugin->iface, make_desc, 0, name);
+}
+
+typedef struct spa_fga_plugin *(spa_filter_graph_audio_plugin_load_func_t)(const struct spa_support *support,
+		uint32_t n_support, const char *path, const struct spa_dict *info);
+
+#define SPA_FILTER_GRAPH_AUDIO_PLUGIN_LOAD_FUNC_NAME "spa_filter_graph_audio_plugin_load"
+
+
+
+#endif /* PLUGIN_H */
diff --git a/src/modules/module-filter-chain/biquad.h b/spa/plugins/filter-graph/biquad.h
similarity index 96%
rename from src/modules/module-filter-chain/biquad.h
rename to spa/plugins/filter-graph/biquad.h
index 650b2639..3344598e 100644
--- a/src/modules/module-filter-chain/biquad.h
+++ b/spa/plugins/filter-graph/biquad.h
@@ -10,6 +10,20 @@
 extern "C" {
 #endif
 
+/* The type of the biquad filters */
+enum biquad_type {
+	BQ_NONE,
+	BQ_LOWPASS,
+	BQ_HIGHPASS,
+	BQ_BANDPASS,
+	BQ_LOWSHELF,
+	BQ_HIGHSHELF,
+	BQ_PEAKING,
+	BQ_NOTCH,
+	BQ_ALLPASS,
+	BQ_RAW,
+};
+
 /* The biquad filter parameters. The transfer function H(z) is (b0 + b1 * z^(-1)
  * + b2 * z^(-2)) / (1 + a1 * z^(-1) + a2 * z^(-2)).  The previous two inputs
  * are stored in x1 and x2, and the previous two outputs are stored in y1 and
@@ -19,23 +33,10 @@ extern "C" {
  * float is used during the actual filtering for faster computation.
  */
 struct biquad {
+	enum biquad_type type;
 	float b0, b1, b2;
 	float a1, a2;
 	float x1, x2;
-	float y1, y2;
-};
-
-/* The type of the biquad filters */
-enum biquad_type {
-	BQ_NONE,
-	BQ_LOWPASS,
-	BQ_HIGHPASS,
-	BQ_BANDPASS,
-	BQ_LOWSHELF,
-	BQ_HIGHSHELF,
-	BQ_PEAKING,
-	BQ_NOTCH,
-	BQ_ALLPASS
 };
 
 /* Initialize a biquad filter parameters from its type and parameters.
diff --git a/spa/plugins/filter-graph/builtin_plugin.c b/spa/plugins/filter-graph/builtin_plugin.c
new file mode 100644
index 00000000..f50ffd77
--- /dev/null
+++ b/spa/plugins/filter-graph/builtin_plugin.c
@@ -0,0 +1,2647 @@
+/* PipeWire */
+/* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */
+/* SPDX-License-Identifier: MIT */
+
+#include "config.h"
+
+#include <float.h>
+#include <math.h>
+#ifdef HAVE_SNDFILE
+#include <sndfile.h>
+#endif
+#include <unistd.h>
+#include <limits.h>
+
+#include <spa/utils/json.h>
+#include <spa/utils/result.h>
+#include <spa/support/cpu.h>
+#include <spa/support/log.h>
+#include <spa/plugins/audioconvert/resample.h>
+
+#include "audio-plugin.h"
+
+#include "biquad.h"
+#include "convolver.h"
+#include "audio-dsp.h"
+
+#define MAX_RATES	32u
+
+struct plugin {
+	struct spa_handle handle;
+	struct spa_fga_plugin plugin;
+
+	struct spa_fga_dsp *dsp;
+	struct spa_log *log;
+};
+
+struct builtin {
+	struct plugin *plugin;
+
+	struct spa_fga_dsp *dsp;
+	struct spa_log *log;
+
+	unsigned long rate;
+	float *port[64];
+
+	int type;
+	struct biquad bq;
+	float freq;
+	float Q;
+	float gain;
+	float b0, b1, b2;
+	float a0, a1, a2;
+	float accum;
+};
+
+static void *builtin_instantiate(const struct spa_fga_plugin *plugin, const struct spa_fga_descriptor * Descriptor,
+		unsigned long SampleRate, int index, const char *config)
+{
+	struct plugin *pl = SPA_CONTAINER_OF(plugin, struct plugin, plugin);
+	struct builtin *impl;
+
+	impl = calloc(1, sizeof(*impl));
+	if (impl == NULL)
+		return NULL;
+
+	impl->plugin = pl;
+	impl->rate = SampleRate;
+	impl->dsp = impl->plugin->dsp;
+	impl->log = impl->plugin->log;
+
+	return impl;
+}
+
+static void builtin_connect_port(void *Instance, unsigned long Port, float * DataLocation)
+{
+	struct builtin *impl = Instance;
+	impl->port[Port] = DataLocation;
+}
+
+static void builtin_cleanup(void * Instance)
+{
+	struct builtin *impl = Instance;
+	free(impl);
+}
+
+/** copy */
+static void copy_run(void * Instance, unsigned long SampleCount)
+{
+	struct builtin *impl = Instance;
+	float *in = impl->port[1], *out = impl->port[0];
+	spa_fga_dsp_copy(impl->dsp, out, in, SampleCount);
+}
+
+static struct spa_fga_port copy_ports[] = {
+	{ .index = 0,
+	  .name = "Out",
+	  .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = 1,
+	  .name = "In",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO,
+	}
+};
+
+static const struct spa_fga_descriptor copy_desc = {
+	.name = "copy",
+	.flags = SPA_FGA_DESCRIPTOR_COPY,
+
+	.n_ports = 2,
+	.ports = copy_ports,
+
+	.instantiate = builtin_instantiate,
+	.connect_port = builtin_connect_port,
+	.run = copy_run,
+	.cleanup = builtin_cleanup,
+};
+
+/** mixer */
+static void mixer_run(void * Instance, unsigned long SampleCount)
+{
+	struct builtin *impl = Instance;
+	int i, n_src = 0;
+	float *out = impl->port[0];
+	const float *src[8];
+	float gains[8];
+	bool eq_gain = true;
+
+	if (out == NULL)
+		return;
+
+	for (i = 0; i < 8; i++) {
+		float *in = impl->port[1+i];
+		float gain = impl->port[9+i][0];
+
+		if (in == NULL || gain == 0.0f)
+			continue;
+
+		src[n_src] = in;
+		gains[n_src++] = gain;
+		if (gain != gains[0])
+			eq_gain = false;
+	}
+	if (eq_gain)
+		spa_fga_dsp_mix_gain(impl->dsp, out, src, n_src, gains, 1, SampleCount);
+	else
+		spa_fga_dsp_mix_gain(impl->dsp, out, src, n_src, gains, n_src, SampleCount);
+}
+
+static struct spa_fga_port mixer_ports[] = {
+	{ .index = 0,
+	  .name = "Out",
+	  .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO,
+	},
+
+	{ .index = 1,
+	  .name = "In 1",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = 2,
+	  .name = "In 2",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = 3,
+	  .name = "In 3",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = 4,
+	  .name = "In 4",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = 5,
+	  .name = "In 5",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = 6,
+	  .name = "In 6",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = 7,
+	  .name = "In 7",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = 8,
+	  .name = "In 8",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO,
+	},
+
+	{ .index = 9,
+	  .name = "Gain 1",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL,
+	  .def = 1.0f, .min = 0.0f, .max = 10.0f
+	},
+	{ .index = 10,
+	  .name = "Gain 2",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL,
+	  .def = 1.0f, .min = 0.0f, .max = 10.0f
+	},
+	{ .index = 11,
+	  .name = "Gain 3",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL,
+	  .def = 1.0f, .min = 0.0f, .max = 10.0f
+	},
+	{ .index = 12,
+	  .name = "Gain 4",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL,
+	  .def = 1.0f, .min = 0.0f, .max = 10.0f
+	},
+	{ .index = 13,
+	  .name = "Gain 5",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL,
+	  .def = 1.0f, .min = 0.0f, .max = 10.0f
+	},
+	{ .index = 14,
+	  .name = "Gain 6",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL,
+	  .def = 1.0f, .min = 0.0f, .max = 10.0f
+	},
+	{ .index = 15,
+	  .name = "Gain 7",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL,
+	  .def = 1.0f, .min = 0.0f, .max = 10.0f
+	},
+	{ .index = 16,
+	  .name = "Gain 8",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL,
+	  .def = 1.0f, .min = 0.0f, .max = 10.0f
+	},
+};
+
+static const struct spa_fga_descriptor mixer_desc = {
+	.name = "mixer",
+	.flags = SPA_FGA_DESCRIPTOR_SUPPORTS_NULL_DATA,
+
+	.n_ports = 17,
+	.ports = mixer_ports,
+
+	.instantiate = builtin_instantiate,
+	.connect_port = builtin_connect_port,
+	.run = mixer_run,
+	.cleanup = builtin_cleanup,
+};
+
+/** biquads */
+static int bq_type_from_name(const char *name)
+{
+	if (spa_streq(name, "bq_lowpass"))
+		return BQ_LOWPASS;
+	if (spa_streq(name, "bq_highpass"))
+		return BQ_HIGHPASS;
+	if (spa_streq(name, "bq_bandpass"))
+		return BQ_BANDPASS;
+	if (spa_streq(name, "bq_lowshelf"))
+		return BQ_LOWSHELF;
+	if (spa_streq(name, "bq_highshelf"))
+		return BQ_HIGHSHELF;
+	if (spa_streq(name, "bq_peaking"))
+		return BQ_PEAKING;
+	if (spa_streq(name, "bq_notch"))
+		return BQ_NOTCH;
+	if (spa_streq(name, "bq_allpass"))
+		return BQ_ALLPASS;
+	if (spa_streq(name, "bq_raw"))
+		return BQ_NONE;
+	return BQ_NONE;
+}
+
+static const char *bq_name_from_type(int type)
+{
+	switch (type) {
+	case BQ_LOWPASS:
+		return "lowpass";
+	case BQ_HIGHPASS:
+		return "highpass";
+	case BQ_BANDPASS:
+		return "bandpass";
+	case BQ_LOWSHELF:
+		return "lowshelf";
+	case BQ_HIGHSHELF:
+		return "highshelf";
+	case BQ_PEAKING:
+		return "peaking";
+	case BQ_NOTCH:
+		return "notch";
+	case BQ_ALLPASS:
+		return "allpass";
+	case BQ_NONE:
+		return "raw";
+	}
+	return "unknown";
+}
+
+static void bq_raw_update(struct builtin *impl, float b0, float b1, float b2,
+		float a0, float a1, float a2)
+{
+	struct biquad *bq = &impl->bq;
+	impl->b0 = b0;
+	impl->b1 = b1;
+	impl->b2 = b2;
+	impl->a0 = a0;
+	impl->a1 = a1;
+	impl->a2 = a2;
+	if (a0 != 0.0f)
+		a0 = 1.0f / a0;
+	bq->b0 = impl->b0 * a0;
+	bq->b1 = impl->b1 * a0;
+	bq->b2 = impl->b2 * a0;
+	bq->a1 = impl->a1 * a0;
+	bq->a2 = impl->a2 * a0;
+	bq->x1 = bq->x2 = 0.0f;
+	bq->type = BQ_RAW;
+}
+
+/*
+ * config = {
+ *     coefficients = [
+ *         { rate =  44100, b0=.., b1=.., b2=.., a0=.., a1=.., a2=.. },
+ *         { rate =  48000, b0=.., b1=.., b2=.., a0=.., a1=.., a2=.. },
+ *         { rate = 192000, b0=.., b1=.., b2=.., a0=.., a1=.., a2=.. }
+ *     ]
+ * }
+ */
+static void *bq_instantiate(const struct spa_fga_plugin *plugin, const struct spa_fga_descriptor * Descriptor,
+		unsigned long SampleRate, int index, const char *config)
+{
+	struct plugin *pl = SPA_CONTAINER_OF(plugin, struct plugin, plugin);
+	struct builtin *impl;
+	struct spa_json it[3];
+	const char *val;
+	char key[256];
+	uint32_t best_rate = 0;
+	int len;
+
+	impl = calloc(1, sizeof(*impl));
+	if (impl == NULL)
+		return NULL;
+
+	impl->plugin = pl;
+	impl->log = impl->plugin->log;
+	impl->dsp = impl->plugin->dsp;
+	impl->rate = SampleRate;
+	impl->b0 = impl->a0 = 1.0f;
+	impl->type = bq_type_from_name(Descriptor->name);
+	if (impl->type != BQ_NONE)
+		return impl;
+
+	if (config == NULL) {
+		spa_log_error(impl->log, "biquads:bq_raw requires a config section");
+		goto error;
+	}
+
+	if (spa_json_begin_object(&it[0], config, strlen(config)) <= 0) {
+		spa_log_error(impl->log, "biquads:config section must be an object");
+		goto error;
+	}
+
+	while ((len = spa_json_object_next(&it[0], key, sizeof(key), &val)) > 0) {
+		if (spa_streq(key, "coefficients")) {
+			if (!spa_json_is_array(val, len)) {
+				spa_log_error(impl->log, "biquads:coefficients require an array");
+				goto error;
+			}
+			spa_json_enter(&it[0], &it[1]);
+			while (spa_json_enter_object(&it[1], &it[2]) > 0) {
+				int32_t rate = 0;
+				float b0 = 1.0f, b1 = 0.0f, b2 = 0.0f;
+				float a0 = 1.0f, a1 = 0.0f, a2 = 0.0f;
+
+				while ((len = spa_json_object_next(&it[2], key, sizeof(key), &val)) > 0) {
+					if (spa_streq(key, "rate")) {
+						if (spa_json_parse_int(val, len, &rate) <= 0) {
+							spa_log_error(impl->log, "biquads:rate requires a number");
+							goto error;
+						}
+					}
+					else if (spa_streq(key, "b0")) {
+						if (spa_json_parse_float(val, len, &b0) <= 0) {
+							spa_log_error(impl->log, "biquads:b0 requires a float");
+							goto error;
+						}
+					}
+					else if (spa_streq(key, "b1")) {
+						if (spa_json_parse_float(val, len, &b1) <= 0) {
+							spa_log_error(impl->log, "biquads:b1 requires a float");
+							goto error;
+						}
+					}
+					else if (spa_streq(key, "b2")) {
+						if (spa_json_parse_float(val, len, &b2) <= 0) {
+							spa_log_error(impl->log, "biquads:b2 requires a float");
+							goto error;
+						}
+					}
+					else if (spa_streq(key, "a0")) {
+						if (spa_json_parse_float(val, len, &a0) <= 0) {
+							spa_log_error(impl->log, "biquads:a0 requires a float");
+							goto error;
+						}
+					}
+					else if (spa_streq(key, "a1")) {
+						if (spa_json_parse_float(val, len, &a1) <= 0) {
+							spa_log_error(impl->log, "biquads:a1 requires a float");
+							goto error;
+						}
+					}
+					else if (spa_streq(key, "a2")) {
+						if (spa_json_parse_float(val, len, &a2) <= 0) {
+							spa_log_error(impl->log, "biquads:a0 requires a float");
+							goto error;
+						}
+					}
+					else {
+						spa_log_warn(impl->log, "biquads: ignoring coefficients key: '%s'", key);
+					}
+				}
+				if (labs((long)rate - (long)SampleRate) <
+				    labs((long)best_rate - (long)SampleRate)) {
+					best_rate = rate;
+					bq_raw_update(impl, b0, b1, b2, a0, a1, a2);
+				}
+			}
+		}
+		else {
+			spa_log_warn(impl->log, "biquads: ignoring config key: '%s'", key);
+		}
+	}
+
+	return impl;
+error:
+	free(impl);
+	errno = EINVAL;
+	return NULL;
+}
+
+#define BQ_NUM_PORTS		11
+static struct spa_fga_port bq_ports[] = {
+	{ .index = 0,
+	  .name = "Out",
+	  .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = 1,
+	  .name = "In",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = 2,
+	  .name = "Freq",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL,
+	  .hint = SPA_FGA_HINT_SAMPLE_RATE,
+	  .def = 0.0f, .min = 0.0f, .max = 1.0f,
+	},
+	{ .index = 3,
+	  .name = "Q",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL,
+	  .def = 0.0f, .min = 0.0f, .max = 10.0f,
+	},
+	{ .index = 4,
+	  .name = "Gain",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL,
+	  .def = 0.0f, .min = -120.0f, .max = 20.0f,
+	},
+	{ .index = 5,
+	  .name = "b0",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL,
+	  .def = 1.0f, .min = -10.0f, .max = 10.0f,
+	},
+	{ .index = 6,
+	  .name = "b1",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL,
+	  .def = 0.0f, .min = -10.0f, .max = 10.0f,
+	},
+	{ .index = 7,
+	  .name = "b2",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL,
+	  .def = 0.0f, .min = -10.0f, .max = 10.0f,
+	},
+	{ .index = 8,
+	  .name = "a0",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL,
+	  .def = 1.0f, .min = -10.0f, .max = 10.0f,
+	},
+	{ .index = 9,
+	  .name = "a1",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL,
+	  .def = 0.0f, .min = -10.0f, .max = 10.0f,
+	},
+	{ .index = 10,
+	  .name = "a2",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL,
+	  .def = 0.0f, .min = -10.0f, .max = 10.0f,
+	},
+
+};
+
+static void bq_freq_update(struct builtin *impl, int type, float freq, float Q, float gain)
+{
+	struct biquad *bq = &impl->bq;
+	impl->freq = freq;
+	impl->Q = Q;
+	impl->gain = gain;
+	biquad_set(bq, type, freq * 2 / impl->rate, Q, gain);
+	impl->port[5][0] = impl->b0 = bq->b0;
+	impl->port[6][0] = impl->b1 = bq->b1;
+	impl->port[7][0] = impl->b2 = bq->b2;
+	impl->port[8][0] = impl->a0 = 1.0f;
+	impl->port[9][0] = impl->a1 = bq->a1;
+	impl->port[10][0] = impl->a2 = bq->a2;
+}
+
+static void bq_activate(void * Instance)
+{
+	struct builtin *impl = Instance;
+	if (impl->type == BQ_NONE) {
+		impl->port[5][0] = impl->b0;
+		impl->port[6][0] = impl->b1;
+		impl->port[7][0] = impl->b2;
+		impl->port[8][0] = impl->a0;
+		impl->port[9][0] = impl->a1;
+		impl->port[10][0] = impl->a2;
+	} else {
+		float freq = impl->port[2][0];
+		float Q = impl->port[3][0];
+		float gain = impl->port[4][0];
+		bq_freq_update(impl, impl->type, freq, Q, gain);
+	}
+}
+
+static void bq_run(void *Instance, unsigned long samples)
+{
+	struct builtin *impl = Instance;
+	struct biquad *bq = &impl->bq;
+	float *out = impl->port[0];
+	float *in = impl->port[1];
+
+	if (impl->type == BQ_NONE) {
+		float b0, b1, b2, a0, a1, a2;
+		b0 = impl->port[5][0];
+		b1 = impl->port[6][0];
+		b2 = impl->port[7][0];
+		a0 = impl->port[8][0];
+		a1 = impl->port[9][0];
+		a2 = impl->port[10][0];
+		if (impl->b0 != b0 || impl->b1 != b1 || impl->b2 != b2 ||
+		    impl->a0 != a0 || impl->a1 != a1 || impl->a2 != a2) {
+			bq_raw_update(impl, b0, b1, b2, a0, a1, a2);
+		}
+	} else {
+		float freq = impl->port[2][0];
+		float Q = impl->port[3][0];
+		float gain = impl->port[4][0];
+		if (impl->freq != freq || impl->Q != Q || impl->gain != gain)
+			bq_freq_update(impl, impl->type, freq, Q, gain);
+	}
+	spa_fga_dsp_biquad_run(impl->dsp, bq, 1, 0, &out, (const float **)&in, 1, samples);
+}
+
+/** bq_lowpass */
+static const struct spa_fga_descriptor bq_lowpass_desc = {
+	.name = "bq_lowpass",
+
+	.n_ports = BQ_NUM_PORTS,
+	.ports = bq_ports,
+
+	.instantiate = bq_instantiate,
+	.connect_port = builtin_connect_port,
+	.activate = bq_activate,
+	.run = bq_run,
+	.cleanup = builtin_cleanup,
+};
+
+/** bq_highpass */
+static const struct spa_fga_descriptor bq_highpass_desc = {
+	.name = "bq_highpass",
+
+	.n_ports = BQ_NUM_PORTS,
+	.ports = bq_ports,
+
+	.instantiate = bq_instantiate,
+	.connect_port = builtin_connect_port,
+	.activate = bq_activate,
+	.run = bq_run,
+	.cleanup = builtin_cleanup,
+};
+
+/** bq_bandpass */
+static const struct spa_fga_descriptor bq_bandpass_desc = {
+	.name = "bq_bandpass",
+
+	.n_ports = BQ_NUM_PORTS,
+	.ports = bq_ports,
+
+	.instantiate = bq_instantiate,
+	.connect_port = builtin_connect_port,
+	.activate = bq_activate,
+	.run = bq_run,
+	.cleanup = builtin_cleanup,
+};
+
+/** bq_lowshelf */
+static const struct spa_fga_descriptor bq_lowshelf_desc = {
+	.name = "bq_lowshelf",
+
+	.n_ports = BQ_NUM_PORTS,
+	.ports = bq_ports,
+
+	.instantiate = bq_instantiate,
+	.connect_port = builtin_connect_port,
+	.activate = bq_activate,
+	.run = bq_run,
+	.cleanup = builtin_cleanup,
+};
+
+/** bq_highshelf */
+static const struct spa_fga_descriptor bq_highshelf_desc = {
+	.name = "bq_highshelf",
+
+	.n_ports = BQ_NUM_PORTS,
+	.ports = bq_ports,
+
+	.instantiate = bq_instantiate,
+	.connect_port = builtin_connect_port,
+	.activate = bq_activate,
+	.run = bq_run,
+	.cleanup = builtin_cleanup,
+};
+
+/** bq_peaking */
+static const struct spa_fga_descriptor bq_peaking_desc = {
+	.name = "bq_peaking",
+
+	.n_ports = BQ_NUM_PORTS,
+	.ports = bq_ports,
+
+	.instantiate = bq_instantiate,
+	.connect_port = builtin_connect_port,
+	.activate = bq_activate,
+	.run = bq_run,
+	.cleanup = builtin_cleanup,
+};
+
+/** bq_notch */
+static const struct spa_fga_descriptor bq_notch_desc = {
+	.name = "bq_notch",
+
+	.n_ports = BQ_NUM_PORTS,
+	.ports = bq_ports,
+
+	.instantiate = bq_instantiate,
+	.connect_port = builtin_connect_port,
+	.activate = bq_activate,
+	.run = bq_run,
+	.cleanup = builtin_cleanup,
+};
+
+
+/** bq_allpass */
+static const struct spa_fga_descriptor bq_allpass_desc = {
+	.name = "bq_allpass",
+
+	.n_ports = BQ_NUM_PORTS,
+	.ports = bq_ports,
+
+	.instantiate = bq_instantiate,
+	.connect_port = builtin_connect_port,
+	.activate = bq_activate,
+	.run = bq_run,
+	.cleanup = builtin_cleanup,
+};
+
+/* bq_raw */
+static const struct spa_fga_descriptor bq_raw_desc = {
+	.name = "bq_raw",
+
+	.n_ports = BQ_NUM_PORTS,
+	.ports = bq_ports,
+
+	.instantiate = bq_instantiate,
+	.connect_port = builtin_connect_port,
+	.activate = bq_activate,
+	.run = bq_run,
+	.cleanup = builtin_cleanup,
+};
+
+/** convolve */
+struct convolver_impl {
+	struct plugin *plugin;
+
+	struct spa_log *log;
+	struct spa_fga_dsp *dsp;
+	unsigned long rate;
+	float *port[2];
+
+	struct convolver *conv;
+};
+
+#ifdef HAVE_SNDFILE
+static float *read_samples_from_sf(SNDFILE *f, const SF_INFO *info, float gain, int delay,
+		int offset, int length, int channel, long unsigned *rate, int *n_samples) {
+	float *samples;
+	int i, n;
+
+	if (length <= 0)
+		length = info->frames;
+	else
+		length = SPA_MIN(length, info->frames);
+
+	length -= SPA_MIN(offset, length);
+
+	n = delay + length;
+	if (n == 0)
+		return NULL;
+
+	samples = calloc(n * info->channels, sizeof(float));
+	if (samples == NULL)
+		return NULL;
+
+	if (offset > 0)
+		sf_seek(f, offset, SEEK_SET);
+	sf_readf_float(f, samples + (delay * info->channels), length);
+
+	channel = channel % info->channels;
+
+	for (i = 0; i < n; i++)
+		samples[i] = samples[info->channels * i + channel] * gain;
+
+	*n_samples = n;
+	*rate = info->samplerate;
+	return samples;
+}
+#endif
+
+static float *read_closest(struct plugin *pl, char **filenames, float gain, float delay_sec, int offset,
+		int length, int channel, long unsigned *rate, int *n_samples)
+{
+#ifdef HAVE_SNDFILE
+	SF_INFO infos[MAX_RATES];
+	SNDFILE *fs[MAX_RATES];
+
+	spa_zero(infos);
+	spa_zero(fs);
+
+	int diff = INT_MAX;
+	uint32_t best = 0, i;
+	float *samples = NULL;
+
+	for (i = 0; i < MAX_RATES && filenames[i] && filenames[i][0]; i++) {
+		fs[i] = sf_open(filenames[i], SFM_READ, &infos[i]);
+		if (fs[i] == NULL)
+			continue;
+
+		if (labs((long)infos[i].samplerate - (long)*rate) < diff) {
+			best = i;
+			diff = labs((long)infos[i].samplerate - (long)*rate);
+			spa_log_debug(pl->log, "new closest match: %d", infos[i].samplerate);
+		}
+	}
+	if (fs[best] != NULL) {
+		spa_log_info(pl->log, "loading best rate:%u %s", infos[best].samplerate, filenames[best]);
+		samples = read_samples_from_sf(fs[best], &infos[best], gain,
+				(int) (delay_sec * infos[best].samplerate), offset, length,
+				channel, rate, n_samples);
+	} else {
+		char buf[PATH_MAX];
+		spa_log_error(pl->log, "Can't open any sample file (CWD %s):",
+				getcwd(buf, sizeof(buf)));
+		for (i = 0; i < MAX_RATES && filenames[i] && filenames[i][0]; i++) {
+			fs[i] = sf_open(filenames[i], SFM_READ, &infos[i]);
+			if (fs[i] == NULL)
+				spa_log_error(pl->log, " failed file %s: %s", filenames[i], sf_strerror(fs[i]));
+			else
+				spa_log_warn(pl->log, " unexpectedly opened file %s", filenames[i]);
+		}
+	}
+	for (i = 0; i < MAX_RATES; i++)
+		if (fs[i] != NULL)
+			sf_close(fs[i]);
+
+	return samples;
+#else
+	spa_log_error(pl->log, "compiled without sndfile support, can't load samples: "
+			"using dirac impulse");
+	float *samples = calloc(1, sizeof(float));
+	samples[0] = gain;
+	*n_samples = 1;
+	return samples;
+#endif
+}
+
+static float *create_hilbert(struct plugin *pl, const char *filename, float gain, int rate, float delay_sec, int offset,
+		int length, int *n_samples)
+{
+	float *samples, v;
+	int i, n, h;
+	int delay = (int) (delay_sec * rate);
+
+	if (length <= 0)
+		length = 1024;
+
+	length -= SPA_MIN(offset, length);
+
+	n = delay + length;
+	if (n == 0)
+		return NULL;
+
+	samples = calloc(n, sizeof(float));
+        if (samples == NULL)
+		return NULL;
+
+	gain *= 2 / (float)M_PI;
+	h = length / 2;
+	for (i = 1; i < h; i += 2) {
+		v = (gain / i) * (0.43f + 0.57f * cosf(i * (float)M_PI / h));
+		samples[delay + h + i] = -v;
+		samples[delay + h - i] =  v;
+	}
+	*n_samples = n;
+	return samples;
+}
+
+static float *create_dirac(struct plugin *pl, const char *filename, float gain, int rate, float delay_sec, int offset,
+		int length, int *n_samples)
+{
+	float *samples;
+	int delay = (int) (delay_sec * rate);
+	int n;
+
+	n = delay + 1;
+
+	samples = calloc(n, sizeof(float));
+        if (samples == NULL)
+		return NULL;
+
+	samples[delay] = gain;
+
+	*n_samples = n;
+	return samples;
+}
+
+static float *resample_buffer(struct plugin *pl, float *samples, int *n_samples,
+		unsigned long in_rate, unsigned long out_rate, uint32_t quality)
+{
+#ifdef HAVE_SPA_PLUGINS
+	uint32_t in_len, out_len, total_out = 0;
+	int out_n_samples;
+	float *out_samples, *out_buf, *in_buf;
+	struct resample r;
+	int res;
+
+	spa_zero(r);
+	r.channels = 1;
+	r.i_rate = in_rate;
+	r.o_rate = out_rate;
+	r.cpu_flags = pl->dsp->cpu_flags;
+	r.quality = quality;
+	if ((res = resample_native_init(&r)) < 0) {
+		spa_log_error(pl->log, "resampling failed: %s", spa_strerror(res));
+		errno = -res;
+		return NULL;
+	}
+
+	out_n_samples = SPA_ROUND_UP(*n_samples * out_rate, in_rate) / in_rate;
+	out_samples = calloc(out_n_samples, sizeof(float));
+	if (out_samples == NULL)
+		goto error;
+
+	in_len = *n_samples;
+	in_buf = samples;
+	out_len = out_n_samples;
+	out_buf = out_samples;
+
+	spa_log_info(pl->log, "Resampling filter: rate: %lu => %lu, n_samples: %u => %u, q:%u",
+		    in_rate, out_rate, in_len, out_len, quality);
+
+	resample_process(&r, (void*)&in_buf, &in_len, (void*)&out_buf, &out_len);
+	spa_log_debug(pl->log, "resampled: %u -> %u samples", in_len, out_len);
+	total_out += out_len;
+
+	in_len = resample_delay(&r);
+	in_buf = calloc(in_len, sizeof(float));
+	if (in_buf == NULL)
+		goto error;
+
+	out_buf = out_samples + total_out;
+	out_len = out_n_samples - total_out;
+
+	spa_log_debug(pl->log, "flushing resampler: %u in %u out", in_len, out_len);
+	resample_process(&r, (void*)&in_buf, &in_len, (void*)&out_buf, &out_len);
+	spa_log_debug(pl->log, "flushed: %u -> %u samples", in_len, out_len);
+	total_out += out_len;
+
+	free(in_buf);
+	free(samples);
+	resample_free(&r);
+
+	*n_samples = total_out;
+
+	float gain = (float)in_rate / (float)out_rate;
+	for (uint32_t i = 0; i < total_out; i++)
+		out_samples[i] = out_samples[i] * gain;
+
+	return out_samples;
+
+error:
+	resample_free(&r);
+	free(samples);
+	free(out_samples);
+	return NULL;
+#else
+	spa_log_error(impl->log, "compiled without spa-plugins support, can't resample");
+	float *out_samples = calloc(*n_samples, sizeof(float));
+	memcpy(out_samples, samples, *n_samples * sizeof(float));
+	return out_samples;
+#endif
+}
+
+static void * convolver_instantiate(const struct spa_fga_plugin *plugin, const struct spa_fga_descriptor * Descriptor,
+		unsigned long SampleRate, int index, const char *config)
+{
+	struct plugin *pl = SPA_CONTAINER_OF(plugin, struct plugin, plugin);
+	struct convolver_impl *impl;
+	float *samples;
+	int offset = 0, length = 0, channel = index, n_samples = 0, len;
+	uint32_t i = 0;
+	struct spa_json it[2];
+	const char *val;
+	char key[256], v[256];
+	char *filenames[MAX_RATES] = { 0 };
+	int blocksize = 0, tailsize = 0;
+	int resample_quality = RESAMPLE_DEFAULT_QUALITY;
+	float gain = 1.0f, delay = 0.0f;
+	unsigned long rate;
+
+	errno = EINVAL;
+	if (config == NULL) {
+		spa_log_error(pl->log, "convolver: requires a config section");
+		return NULL;
+	}
+
+	if (spa_json_begin_object(&it[0], config, strlen(config)) <= 0) {
+		spa_log_error(pl->log, "convolver:config must be an object");
+		return NULL;
+	}
+
+	while ((len = spa_json_object_next(&it[0], key, sizeof(key), &val)) > 0) {
+		if (spa_streq(key, "blocksize")) {
+			if (spa_json_parse_int(val, len, &blocksize) <= 0) {
+				spa_log_error(pl->log, "convolver:blocksize requires a number");
+				return NULL;
+			}
+		}
+		else if (spa_streq(key, "tailsize")) {
+			if (spa_json_parse_int(val, len, &tailsize) <= 0) {
+				spa_log_error(pl->log, "convolver:tailsize requires a number");
+				return NULL;
+			}
+		}
+		else if (spa_streq(key, "gain")) {
+			if (spa_json_parse_float(val, len, &gain) <= 0) {
+				spa_log_error(pl->log, "convolver:gain requires a number");
+				return NULL;
+			}
+		}
+		else if (spa_streq(key, "delay")) {
+			int delay_i;
+			if (spa_json_parse_int(val, len, &delay_i) > 0) {
+				delay = delay_i / (float)SampleRate;
+			} else if (spa_json_parse_float(val, len, &delay) <= 0) {
+				spa_log_error(pl->log, "convolver:delay requires a number");
+				return NULL;
+			}
+		}
+		else if (spa_streq(key, "filename")) {
+			if (spa_json_is_array(val, len)) {
+				spa_json_enter(&it[0], &it[1]);
+				while (spa_json_get_string(&it[1], v, sizeof(v)) > 0 &&
+					i < SPA_N_ELEMENTS(filenames)) {
+						filenames[i] = strdup(v);
+						i++;
+				}
+			}
+			else if (spa_json_parse_stringn(val, len, v, sizeof(v)) <= 0) {
+				spa_log_error(pl->log, "convolver:filename requires a string or an array");
+				return NULL;
+			} else {
+				filenames[0] = strdup(v);
+			}
+		}
+		else if (spa_streq(key, "offset")) {
+			if (spa_json_parse_int(val, len, &offset) <= 0) {
+				spa_log_error(pl->log, "convolver:offset requires a number");
+				return NULL;
+			}
+		}
+		else if (spa_streq(key, "length")) {
+			if (spa_json_parse_int(val, len, &length) <= 0) {
+				spa_log_error(pl->log, "convolver:length requires a number");
+				return NULL;
+			}
+		}
+		else if (spa_streq(key, "channel")) {
+			if (spa_json_parse_int(val, len, &channel) <= 0) {
+				spa_log_error(pl->log, "convolver:channel requires a number");
+				return NULL;
+			}
+		}
+		else if (spa_streq(key, "resample_quality")) {
+			if (spa_json_parse_int(val, len, &resample_quality) <= 0) {
+				spa_log_error(pl->log, "convolver:resample_quality requires a number");
+				return NULL;
+			}
+		}
+		else {
+			spa_log_warn(pl->log, "convolver: ignoring config key: '%s'", key);
+		}
+	}
+	if (filenames[0] == NULL) {
+		spa_log_error(pl->log, "convolver:filename was not given");
+		return NULL;
+	}
+
+	if (delay < 0.0f)
+		delay = 0.0f;
+	if (offset < 0)
+		offset = 0;
+
+	if (spa_streq(filenames[0], "/hilbert")) {
+		samples = create_hilbert(pl, filenames[0], gain, SampleRate, delay, offset,
+				length, &n_samples);
+	} else if (spa_streq(filenames[0], "/dirac")) {
+		samples = create_dirac(pl, filenames[0], gain, SampleRate, delay, offset,
+				length, &n_samples);
+	} else {
+		rate = SampleRate;
+		samples = read_closest(pl, filenames, gain, delay, offset,
+				length, channel, &rate, &n_samples);
+		if (samples != NULL && rate != SampleRate) {
+			samples = resample_buffer(pl, samples, &n_samples,
+					rate, SampleRate, resample_quality);
+		}
+	}
+
+	for (i = 0; i < MAX_RATES; i++)
+		if (filenames[i])
+			free(filenames[i]);
+
+	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, 32768);
+
+	spa_log_info(pl->log, "using n_samples:%u %d:%d blocksize delay:%f", n_samples,
+			blocksize, tailsize, delay);
+
+	impl = calloc(1, sizeof(*impl));
+	if (impl == NULL)
+		goto error;
+
+	impl->plugin = pl;
+	impl->log = pl->log;
+	impl->dsp = pl->dsp;
+	impl->rate = SampleRate;
+
+	impl->conv = convolver_new(impl->dsp, blocksize, tailsize, samples, n_samples);
+	if (impl->conv == NULL)
+		goto error;
+
+	free(samples);
+
+	return impl;
+error:
+	free(samples);
+	free(impl);
+	return NULL;
+}
+
+static void convolver_connect_port(void * Instance, unsigned long Port,
+                        float * DataLocation)
+{
+	struct convolver_impl *impl = Instance;
+	impl->port[Port] = DataLocation;
+}
+
+static void convolver_cleanup(void * Instance)
+{
+	struct convolver_impl *impl = Instance;
+	if (impl->conv)
+		convolver_free(impl->conv);
+	free(impl);
+}
+
+static struct spa_fga_port convolve_ports[] = {
+	{ .index = 0,
+	  .name = "Out",
+	  .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = 1,
+	  .name = "In",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO,
+	},
+};
+
+static void convolver_deactivate(void * Instance)
+{
+	struct convolver_impl *impl = Instance;
+	convolver_reset(impl->conv);
+}
+
+static void convolve_run(void * Instance, unsigned long SampleCount)
+{
+	struct convolver_impl *impl = Instance;
+	if (impl->port[1] != NULL && impl->port[0] != NULL)
+		convolver_run(impl->conv, impl->port[1], impl->port[0], SampleCount);
+}
+
+static const struct spa_fga_descriptor convolve_desc = {
+	.name = "convolver",
+	.flags = SPA_FGA_DESCRIPTOR_SUPPORTS_NULL_DATA,
+
+	.n_ports = 2,
+	.ports = convolve_ports,
+
+	.instantiate = convolver_instantiate,
+	.connect_port = convolver_connect_port,
+	.deactivate = convolver_deactivate,
+	.run = convolve_run,
+	.cleanup = convolver_cleanup,
+};
+
+/** delay */
+struct delay_impl {
+	struct plugin *plugin;
+
+	struct spa_fga_dsp *dsp;
+	struct spa_log *log;
+
+	unsigned long rate;
+	float *port[4];
+
+	float delay;
+	uint32_t delay_samples;
+	uint32_t buffer_samples;
+	float *buffer;
+	uint32_t ptr;
+};
+
+static void delay_cleanup(void * Instance)
+{
+	struct delay_impl *impl = Instance;
+	free(impl->buffer);
+	free(impl);
+}
+
+static void *delay_instantiate(const struct spa_fga_plugin *plugin, const struct spa_fga_descriptor * Descriptor,
+		unsigned long SampleRate, int index, const char *config)
+{
+	struct plugin *pl = SPA_CONTAINER_OF(plugin, struct plugin, plugin);
+	struct delay_impl *impl;
+	struct spa_json it[1];
+	const char *val;
+	char key[256];
+	float max_delay = 1.0f;
+	int len;
+
+	if (config == NULL) {
+		spa_log_error(pl->log, "delay: requires a config section");
+		errno = EINVAL;
+		return NULL;
+	}
+
+	if (spa_json_begin_object(&it[0], config, strlen(config)) <= 0) {
+		spa_log_error(pl->log, "delay:config must be an object");
+		return NULL;
+	}
+
+	while ((len = spa_json_object_next(&it[0], key, sizeof(key), &val)) > 0) {
+		if (spa_streq(key, "max-delay")) {
+			if (spa_json_parse_float(val, len, &max_delay) <= 0) {
+				spa_log_error(pl->log, "delay:max-delay requires a number");
+				return NULL;
+			}
+		} else {
+			spa_log_warn(pl->log, "delay: ignoring config key: '%s'", key);
+		}
+	}
+	if (max_delay <= 0.0f)
+		max_delay = 1.0f;
+
+	impl = calloc(1, sizeof(*impl));
+	if (impl == NULL)
+		return NULL;
+
+	impl->plugin = pl;
+	impl->dsp = pl->dsp;
+	impl->log = pl->log;
+	impl->rate = SampleRate;
+	impl->buffer_samples = SPA_ROUND_UP_N((uint32_t)(max_delay * impl->rate), 64);
+	spa_log_info(impl->log, "max-delay:%f seconds rate:%lu samples:%d", max_delay, impl->rate, impl->buffer_samples);
+
+	impl->buffer = calloc(impl->buffer_samples * 2 + 64, sizeof(float));
+	if (impl->buffer == NULL) {
+		delay_cleanup(impl);
+		return NULL;
+	}
+	return impl;
+}
+
+static void delay_connect_port(void * Instance, unsigned long Port,
+                        float * DataLocation)
+{
+	struct delay_impl *impl = Instance;
+	if (Port > 2)
+		return;
+	impl->port[Port] = DataLocation;
+}
+
+static void delay_run(void * Instance, unsigned long SampleCount)
+{
+	struct delay_impl *impl = Instance;
+	float *in = impl->port[1], *out = impl->port[0];
+	float delay = impl->port[2][0];
+
+	if (in == NULL || out == NULL)
+		return;
+
+	if (delay != impl->delay) {
+		impl->delay_samples = SPA_CLAMP((uint32_t)(delay * impl->rate), 0u, impl->buffer_samples-1);
+		impl->delay = delay;
+	}
+	spa_fga_dsp_delay(impl->dsp, impl->buffer, &impl->ptr, impl->buffer_samples,
+			impl->delay_samples, out, in, SampleCount);
+}
+
+static struct spa_fga_port delay_ports[] = {
+	{ .index = 0,
+	  .name = "Out",
+	  .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = 1,
+	  .name = "In",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = 2,
+	  .name = "Delay (s)",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL,
+	  .def = 0.0f, .min = 0.0f, .max = 100.0f
+	},
+};
+
+static const struct spa_fga_descriptor delay_desc = {
+	.name = "delay",
+	.flags = SPA_FGA_DESCRIPTOR_SUPPORTS_NULL_DATA,
+
+	.n_ports = 3,
+	.ports = delay_ports,
+
+	.instantiate = delay_instantiate,
+	.connect_port = delay_connect_port,
+	.run = delay_run,
+	.cleanup = delay_cleanup,
+};
+
+/* invert */
+static void invert_run(void * Instance, unsigned long SampleCount)
+{
+	struct builtin *impl = Instance;
+	float *in = impl->port[1], *out = impl->port[0];
+	unsigned long n;
+	for (n = 0; n < SampleCount; n++)
+		out[n] = -in[n];
+}
+
+static struct spa_fga_port invert_ports[] = {
+	{ .index = 0,
+	  .name = "Out",
+	  .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = 1,
+	  .name = "In",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO,
+	},
+};
+
+static const struct spa_fga_descriptor invert_desc = {
+	.name = "invert",
+
+	.n_ports = 2,
+	.ports = invert_ports,
+
+	.instantiate = builtin_instantiate,
+	.connect_port = builtin_connect_port,
+	.run = invert_run,
+	.cleanup = builtin_cleanup,
+};
+
+/* clamp */
+static void clamp_run(void * Instance, unsigned long SampleCount)
+{
+	struct builtin *impl = Instance;
+	float min = impl->port[4][0], max = impl->port[5][0];
+	float *in = impl->port[1], *out = impl->port[0];
+	float *ctrl = impl->port[3], *notify = impl->port[2];
+
+	if (in != NULL && out != NULL) {
+		unsigned long n;
+		for (n = 0; n < SampleCount; n++)
+			out[n] = SPA_CLAMPF(in[n], min, max);
+	}
+	if (ctrl != NULL && notify != NULL)
+		notify[0] = SPA_CLAMPF(ctrl[0], min, max);
+}
+
+static struct spa_fga_port clamp_ports[] = {
+	{ .index = 0,
+	  .name = "Out",
+	  .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = 1,
+	  .name = "In",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = 2,
+	  .name = "Notify",
+	  .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_CONTROL,
+	},
+	{ .index = 3,
+	  .name = "Control",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL,
+	},
+	{ .index = 4,
+	  .name = "Min",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL,
+	  .def = 0.0f, .min = -100.0f, .max = 100.0f
+	},
+	{ .index = 5,
+	  .name = "Max",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL,
+	  .def = 1.0f, .min = -100.0f, .max = 100.0f
+	},
+};
+
+static const struct spa_fga_descriptor clamp_desc = {
+	.name = "clamp",
+	.flags = SPA_FGA_DESCRIPTOR_SUPPORTS_NULL_DATA,
+
+	.n_ports = SPA_N_ELEMENTS(clamp_ports),
+	.ports = clamp_ports,
+
+	.instantiate = builtin_instantiate,
+	.connect_port = builtin_connect_port,
+	.run = clamp_run,
+	.cleanup = builtin_cleanup,
+};
+
+/* linear */
+static void linear_run(void * Instance, unsigned long SampleCount)
+{
+	struct builtin *impl = Instance;
+	float mult = impl->port[4][0], add = impl->port[5][0];
+	float *in = impl->port[1], *out = impl->port[0];
+	float *ctrl = impl->port[3], *notify = impl->port[2];
+
+	if (in != NULL && out != NULL)
+		spa_fga_dsp_linear(impl->dsp, out, in, mult, add, SampleCount);
+
+	if (ctrl != NULL && notify != NULL)
+		notify[0] = ctrl[0] * mult + add;
+}
+
+static struct spa_fga_port linear_ports[] = {
+	{ .index = 0,
+	  .name = "Out",
+	  .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = 1,
+	  .name = "In",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = 2,
+	  .name = "Notify",
+	  .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_CONTROL,
+	},
+	{ .index = 3,
+	  .name = "Control",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL,
+	},
+	{ .index = 4,
+	  .name = "Mult",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL,
+	  .def = 1.0f, .min = -10.0f, .max = 10.0f
+	},
+	{ .index = 5,
+	  .name = "Add",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL,
+	  .def = 0.0f, .min = -10.0f, .max = 10.0f
+	},
+};
+
+static const struct spa_fga_descriptor linear_desc = {
+	.name = "linear",
+	.flags = SPA_FGA_DESCRIPTOR_SUPPORTS_NULL_DATA,
+
+	.n_ports = SPA_N_ELEMENTS(linear_ports),
+	.ports = linear_ports,
+
+	.instantiate = builtin_instantiate,
+	.connect_port = builtin_connect_port,
+	.run = linear_run,
+	.cleanup = builtin_cleanup,
+};
+
+
+/* reciprocal */
+static void recip_run(void * Instance, unsigned long SampleCount)
+{
+	struct builtin *impl = Instance;
+	float *in = impl->port[1], *out = impl->port[0];
+	float *ctrl = impl->port[3], *notify = impl->port[2];
+
+	if (in != NULL && out != NULL) {
+		unsigned long n;
+		for (n = 0; n < SampleCount; n++) {
+			if (in[0] == 0.0f)
+				out[n] = 0.0f;
+			else
+				out[n] = 1.0f / in[n];
+		}
+	}
+	if (ctrl != NULL && notify != NULL) {
+		if (ctrl[0] == 0.0f)
+			notify[0] = 0.0f;
+		else
+			notify[0] = 1.0f / ctrl[0];
+	}
+}
+
+static struct spa_fga_port recip_ports[] = {
+	{ .index = 0,
+	  .name = "Out",
+	  .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = 1,
+	  .name = "In",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = 2,
+	  .name = "Notify",
+	  .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_CONTROL,
+	},
+	{ .index = 3,
+	  .name = "Control",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL,
+	},
+};
+
+static const struct spa_fga_descriptor recip_desc = {
+	.name = "recip",
+	.flags = SPA_FGA_DESCRIPTOR_SUPPORTS_NULL_DATA,
+
+	.n_ports = SPA_N_ELEMENTS(recip_ports),
+	.ports = recip_ports,
+
+	.instantiate = builtin_instantiate,
+	.connect_port = builtin_connect_port,
+	.run = recip_run,
+	.cleanup = builtin_cleanup,
+};
+
+/* exp */
+static void exp_run(void * Instance, unsigned long SampleCount)
+{
+	struct builtin *impl = Instance;
+	float base = impl->port[4][0];
+	float *in = impl->port[1], *out = impl->port[0];
+	float *ctrl = impl->port[3], *notify = impl->port[2];
+
+	if (in != NULL && out != NULL) {
+		unsigned long n;
+		for (n = 0; n < SampleCount; n++)
+			out[n] = powf(base, in[n]);
+	}
+	if (ctrl != NULL && notify != NULL)
+		notify[0] = powf(base, ctrl[0]);
+}
+
+static struct spa_fga_port exp_ports[] = {
+	{ .index = 0,
+	  .name = "Out",
+	  .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = 1,
+	  .name = "In",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = 2,
+	  .name = "Notify",
+	  .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_CONTROL,
+	},
+	{ .index = 3,
+	  .name = "Control",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL,
+	},
+	{ .index = 4,
+	  .name = "Base",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL,
+	  .def = (float)M_E, .min = -10.0f, .max = 10.0f
+	},
+};
+
+static const struct spa_fga_descriptor exp_desc = {
+	.name = "exp",
+	.flags = SPA_FGA_DESCRIPTOR_SUPPORTS_NULL_DATA,
+
+	.n_ports = SPA_N_ELEMENTS(exp_ports),
+	.ports = exp_ports,
+
+	.instantiate = builtin_instantiate,
+	.connect_port = builtin_connect_port,
+	.run = exp_run,
+	.cleanup = builtin_cleanup,
+};
+
+/* log */
+static void log_run(void * Instance, unsigned long SampleCount)
+{
+	struct builtin *impl = Instance;
+	float base = impl->port[4][0];
+	float m1 = impl->port[5][0];
+	float m2 = impl->port[6][0];
+	float *in = impl->port[1], *out = impl->port[0];
+	float *ctrl = impl->port[3], *notify = impl->port[2];
+	float lb = log2f(base);
+
+	if (in != NULL && out != NULL) {
+		unsigned long n;
+		for (n = 0; n < SampleCount; n++)
+			out[n] = m2 * log2f(fabsf(in[n] * m1)) / lb;
+	}
+	if (ctrl != NULL && notify != NULL)
+		notify[0] = m2 * log2f(fabsf(ctrl[0] * m1)) / lb;
+}
+
+static struct spa_fga_port log_ports[] = {
+	{ .index = 0,
+	  .name = "Out",
+	  .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = 1,
+	  .name = "In",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = 2,
+	  .name = "Notify",
+	  .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_CONTROL,
+	},
+	{ .index = 3,
+	  .name = "Control",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL,
+	},
+	{ .index = 4,
+	  .name = "Base",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL,
+	  .def = (float)M_E, .min = 2.0f, .max = 100.0f
+	},
+	{ .index = 5,
+	  .name = "M1",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL,
+	  .def = 1.0f, .min = -10.0f, .max = 10.0f
+	},
+	{ .index = 6,
+	  .name = "M2",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL,
+	  .def = 1.0f, .min = -10.0f, .max = 10.0f
+	},
+};
+
+static const struct spa_fga_descriptor log_desc = {
+	.name = "log",
+	.flags = SPA_FGA_DESCRIPTOR_SUPPORTS_NULL_DATA,
+
+	.n_ports = SPA_N_ELEMENTS(log_ports),
+	.ports = log_ports,
+
+	.instantiate = builtin_instantiate,
+	.connect_port = builtin_connect_port,
+	.run = log_run,
+	.cleanup = builtin_cleanup,
+};
+
+/* mult */
+static void mult_run(void * Instance, unsigned long SampleCount)
+{
+	struct builtin *impl = Instance;
+	int i, n_src = 0;
+	float *out = impl->port[0];
+	const float *src[8];
+
+	if (out == NULL)
+		return;
+
+	for (i = 0; i < 8; i++) {
+		float *in = impl->port[1+i];
+
+		if (in == NULL)
+			continue;
+
+		src[n_src++] = in;
+	}
+	spa_fga_dsp_mult(impl->dsp, out, src, n_src, SampleCount);
+}
+
+static struct spa_fga_port mult_ports[] = {
+	{ .index = 0,
+	  .name = "Out",
+	  .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = 1,
+	  .name = "In 1",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = 2,
+	  .name = "In 2",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = 3,
+	  .name = "In 3",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = 4,
+	  .name = "In 4",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = 5,
+	  .name = "In 5",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = 6,
+	  .name = "In 6",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = 7,
+	  .name = "In 7",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = 8,
+	  .name = "In 8",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO,
+	},
+};
+
+static const struct spa_fga_descriptor mult_desc = {
+	.name = "mult",
+	.flags = SPA_FGA_DESCRIPTOR_SUPPORTS_NULL_DATA,
+
+	.n_ports = SPA_N_ELEMENTS(mult_ports),
+	.ports = mult_ports,
+
+	.instantiate = builtin_instantiate,
+	.connect_port = builtin_connect_port,
+	.run = mult_run,
+	.cleanup = builtin_cleanup,
+};
+
+#define M_PI_M2f (float)(M_PI+M_PI)
+
+/* sine */
+static void sine_run(void * Instance, unsigned long SampleCount)
+{
+	struct builtin *impl = Instance;
+	float *out = impl->port[0];
+	float *notify = impl->port[1];
+	float freq = impl->port[2][0];
+	float ampl = impl->port[3][0];
+	float offs = impl->port[5][0];
+	unsigned long n;
+
+	for (n = 0; n < SampleCount; n++) {
+		if (out != NULL)
+			out[n] = sinf(impl->accum) * ampl + offs;
+		if (notify != NULL && n == 0)
+			notify[0] = sinf(impl->accum) * ampl + offs;
+
+		impl->accum += M_PI_M2f * freq / impl->rate;
+		if (impl->accum >= M_PI_M2f)
+			impl->accum -= M_PI_M2f;
+	}
+}
+
+static struct spa_fga_port sine_ports[] = {
+	{ .index = 0,
+	  .name = "Out",
+	  .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = 1,
+	  .name = "Notify",
+	  .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_CONTROL,
+	},
+	{ .index = 2,
+	  .name = "Freq",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL,
+	  .def = 440.0f, .min = 0.0f, .max = 1000000.0f
+	},
+	{ .index = 3,
+	  .name = "Ampl",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL,
+	  .def = 1.0, .min = 0.0f, .max = 10.0f
+	},
+	{ .index = 4,
+	  .name = "Phase",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL,
+	  .def = 0.0f, .min = (float)-M_PI, .max = (float)M_PI
+	},
+	{ .index = 5,
+	  .name = "Offset",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL,
+	  .def = 0.0f, .min = -10.0f, .max = 10.0f
+	},
+};
+
+static const struct spa_fga_descriptor sine_desc = {
+	.name = "sine",
+	.flags = SPA_FGA_DESCRIPTOR_SUPPORTS_NULL_DATA,
+
+	.n_ports = SPA_N_ELEMENTS(sine_ports),
+	.ports = sine_ports,
+
+	.instantiate = builtin_instantiate,
+	.connect_port = builtin_connect_port,
+	.run = sine_run,
+	.cleanup = builtin_cleanup,
+};
+
+#define PARAM_EQ_MAX		64
+struct param_eq_impl {
+	struct plugin *plugin;
+
+	struct spa_fga_dsp *dsp;
+	struct spa_log *log;
+
+	unsigned long rate;
+	float *port[8*2];
+
+	uint32_t n_bq;
+	struct biquad bq[PARAM_EQ_MAX * 8];
+};
+
+static int load_eq_bands(struct plugin *pl, const char *filename, int rate,
+		struct biquad *bq, uint32_t max_bq, uint32_t *n_bq)
+{
+	FILE *f = NULL;
+	char *line = NULL;
+	ssize_t nread;
+	size_t linelen;
+	uint32_t n = 0;
+	char filter_type[4];
+	char filter[4];
+	char freq[9], q[7], gain[7];
+	float vf, vg, vq;
+	int res = 0;
+
+	if ((f = fopen(filename, "r")) == NULL) {
+		res = -errno;
+		spa_log_error(pl->log, "failed to open param_eq file '%s': %m", filename);
+		goto exit;
+	}
+	/*
+	 * Read the Preamp gain line.
+	 * Example: Preamp: -6.8 dB
+	 *
+	 * When a pre-amp gain is required, which is usually the case when
+	 * applying EQ, we need to modify the first EQ band to apply a
+	 * bq_highshelf filter at frequency 0 Hz with the provided negative
+	 * gain.
+	 *
+	 * Pre-amp gain is always negative to offset the effect of possible
+	 * clipping introduced by the amplification resulting from EQ.
+	 */
+	nread = getline(&line, &linelen, f);
+	if (nread != -1 && sscanf(line, "%*s %6s %*s", gain) == 1) {
+		if (spa_json_parse_float(gain, strlen(gain), &vg)) {
+			spa_log_info(pl->log, "%d %s freq:0 q:1.0 gain:%f", n,
+					bq_name_from_type(BQ_HIGHSHELF), vg);
+			biquad_set(&bq[n++], BQ_HIGHSHELF, 0.0f, 1.0f, vg);
+		}
+	}
+	/* Read the filter bands */
+	while ((nread = getline(&line, &linelen, f)) != -1) {
+		if (n == PARAM_EQ_MAX) {
+			res = -ENOSPC;
+			goto exit;
+		}
+		/*
+		 * On field widths:
+		 * - filter can be ON or OFF
+		 * - filter type can be PK, LSC, HSC
+		 * - freq can be at most 5 decimal digits
+		 * - gain can be -xy.z
+		 * - Q can be x.y00
+		 *
+		 * Use a field width of 6 for gain and Q to account for any
+		 * possible zeros.
+		 */
+		if (sscanf(line, "%*s %*d: %3s %3s %*s %8s %*s %*s %6s %*s %*c %6s",
+					filter, filter_type, freq, gain, q) == 5) {
+			if (strcmp(filter, "ON") == 0) {
+				int type;
+
+				if (spa_streq(filter_type, "PK"))
+					type = BQ_PEAKING;
+				else if (spa_streq(filter_type, "LSC"))
+					type = BQ_LOWSHELF;
+				else if (spa_streq(filter_type, "HSC"))
+					type = BQ_HIGHSHELF;
+				else
+					continue;
+
+				if (spa_json_parse_float(freq, strlen(freq), &vf) &&
+				    spa_json_parse_float(gain, strlen(gain), &vg) &&
+				    spa_json_parse_float(q, strlen(q), &vq)) {
+					spa_log_info(pl->log, "%d %s freq:%f q:%f gain:%f", n,
+							bq_name_from_type(type), vf, vq, vg);
+					biquad_set(&bq[n++], type, vf * 2.0f / rate, vq, vg);
+				}
+			}
+		}
+	}
+	*n_bq = n;
+exit:
+	if (f)
+		fclose(f);
+	return res;
+}
+
+
+
+/*
+ * [
+ *   { type=bq_peaking freq=21 gain=6.7 q=1.100 }
+ *   { type=bq_peaking freq=85 gain=6.9 q=3.000 }
+ *   { type=bq_peaking freq=110 gain=-2.6 q=2.700 }
+ *   { type=bq_peaking freq=210 gain=5.9 q=2.100 }
+ *   { type=bq_peaking freq=710 gain=-1.0 q=0.600 }
+ *   { type=bq_peaking freq=1600 gain=2.3 q=2.700 }
+ * ]
+ */
+static int parse_filters(struct plugin *pl, struct spa_json *iter, int rate,
+		struct biquad *bq, uint32_t max_bq, uint32_t *n_bq)
+{
+	struct spa_json it[1];
+	const char *val;
+	char key[256];
+	char type_str[17];
+	int len;
+	uint32_t n = 0;
+
+	while (spa_json_enter_object(iter, &it[0]) > 0) {
+		float freq = 0.0f, gain = 0.0f, q = 1.0f;
+		int type = BQ_NONE;
+
+		while ((len = spa_json_object_next(&it[0], key, sizeof(key), &val)) > 0) {
+			if (spa_streq(key, "type")) {
+				if (spa_json_parse_stringn(val, len, type_str, sizeof(type_str)) <= 0) {
+					spa_log_error(pl->log, "param_eq:type requires a string");
+					return -EINVAL;
+				}
+				type = bq_type_from_name(type_str);
+			}
+			else if (spa_streq(key, "freq")) {
+				if (spa_json_parse_float(val, len, &freq) <= 0) {
+					spa_log_error(pl->log, "param_eq:rate requires a number");
+					return -EINVAL;
+				}
+			}
+			else if (spa_streq(key, "q")) {
+				if (spa_json_parse_float(val, len, &q) <= 0) {
+					spa_log_error(pl->log, "param_eq:q requires a float");
+					return -EINVAL;
+				}
+			}
+			else if (spa_streq(key, "gain")) {
+				if (spa_json_parse_float(val, len, &gain) <= 0) {
+					spa_log_error(pl->log, "param_eq:gain requires a float");
+					return -EINVAL;
+				}
+			}
+			else {
+				spa_log_warn(pl->log, "param_eq: ignoring filter key: '%s'", key);
+			}
+		}
+		if (n == max_bq)
+			return -ENOSPC;
+
+		spa_log_info(pl->log, "%d %s freq:%f q:%f gain:%f", n,
+					bq_name_from_type(type), freq, q, gain);
+		biquad_set(&bq[n++], type, freq * 2 / rate, q, gain);
+	}
+	*n_bq = n;
+	return 0;
+}
+
+/*
+ * {
+ *   filename = "...",
+ *   filenameX = "...", # to load channel X
+ *   filters = [ ... ]
+ *   filtersX = [ ... ] # to load channel X
+ * }
+ */
+static void *param_eq_instantiate(const struct spa_fga_plugin *plugin, const struct spa_fga_descriptor * Descriptor,
+		unsigned long SampleRate, int index, const char *config)
+{
+	struct plugin *pl = SPA_CONTAINER_OF(plugin, struct plugin, plugin);
+	struct spa_json it[3];
+	const char *val;
+	char key[256], filename[PATH_MAX];
+	int len, res;
+	struct param_eq_impl *impl;
+	uint32_t i, n_bq = 0;
+
+	if (config == NULL) {
+		spa_log_error(pl->log, "param_eq: requires a config section");
+		errno = EINVAL;
+		return NULL;
+	}
+
+	if (spa_json_begin_object(&it[0], config, strlen(config)) <= 0) {
+		spa_log_error(pl->log, "param_eq: config must be an object");
+		return NULL;
+	}
+
+	impl = calloc(1, sizeof(*impl));
+	if (impl == NULL)
+		return NULL;
+
+	impl->plugin = pl;
+	impl->dsp = pl->dsp;
+	impl->log = pl->log;
+	impl->rate = SampleRate;
+	for (i = 0; i < SPA_N_ELEMENTS(impl->bq); i++)
+		biquad_set(&impl->bq[i], BQ_NONE, 0.0f, 0.0f, 0.0f);
+
+	while ((len = spa_json_object_next(&it[0], key, sizeof(key), &val)) > 0) {
+		int32_t idx = 0;
+		struct biquad *bq = impl->bq;
+
+		if (spa_strstartswith(key, "filename")) {
+			if (spa_json_parse_stringn(val, len, filename, sizeof(filename)) <= 0) {
+				spa_log_error(impl->log, "param_eq: filename requires a string");
+				goto error;
+			}
+			if (spa_atoi32(key+8, &idx, 0))
+				bq = &impl->bq[(SPA_CLAMP(idx, 1, 8) - 1) * PARAM_EQ_MAX];
+
+			res = load_eq_bands(pl, filename, impl->rate, bq, PARAM_EQ_MAX, &n_bq);
+			if (res < 0) {
+				spa_log_error(impl->log, "param_eq: failed to parse configuration from '%s'", filename);
+				goto error;
+			}
+			spa_log_info(impl->log, "loaded %d biquads for channel %d from %s", n_bq, idx, filename);
+			impl->n_bq = SPA_MAX(impl->n_bq, n_bq);
+		}
+		else if (spa_strstartswith(key, "filters")) {
+			if (!spa_json_is_array(val, len)) {
+				spa_log_error(impl->log, "param_eq:filters require an array");
+				goto error;
+			}
+			spa_json_enter(&it[0], &it[1]);
+
+			if (spa_atoi32(key+7, &idx, 0))
+				bq = &impl->bq[(SPA_CLAMP(idx, 1, 8) - 1) * PARAM_EQ_MAX];
+
+			res = parse_filters(pl, &it[1], impl->rate, bq, PARAM_EQ_MAX, &n_bq);
+			if (res < 0) {
+				spa_log_error(impl->log, "param_eq: failed to parse configuration");
+				goto error;
+			}
+			spa_log_info(impl->log, "parsed %d biquads for channel %d", n_bq, idx);
+			impl->n_bq = SPA_MAX(impl->n_bq, n_bq);
+		} else {
+			spa_log_warn(impl->log, "param_eq: ignoring config key: '%s'", key);
+		}
+		if (idx == 0) {
+			for (i = 1; i < 8; i++)
+				memcpy(&impl->bq[i*PARAM_EQ_MAX], impl->bq,
+						sizeof(struct biquad) * PARAM_EQ_MAX);
+		}
+	}
+	return impl;
+error:
+	free(impl);
+	return NULL;
+}
+
+static void param_eq_connect_port(void * Instance, unsigned long Port,
+                        float * DataLocation)
+{
+	struct param_eq_impl *impl = Instance;
+	impl->port[Port] = DataLocation;
+}
+
+static void param_eq_run(void * Instance, unsigned long SampleCount)
+{
+	struct param_eq_impl *impl = Instance;
+	spa_fga_dsp_biquad_run(impl->dsp, impl->bq, impl->n_bq, PARAM_EQ_MAX,
+			&impl->port[8], (const float**)impl->port, 8, SampleCount);
+}
+
+static struct spa_fga_port param_eq_ports[] = {
+	{ .index = 0,
+	  .name = "In 1",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = 1,
+	  .name = "In 2",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = 2,
+	  .name = "In 3",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = 3,
+	  .name = "In 4",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = 4,
+	  .name = "In 5",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = 5,
+	  .name = "In 6",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = 6,
+	  .name = "In 7",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = 7,
+	  .name = "In 8",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO,
+	},
+
+	{ .index = 8,
+	  .name = "Out 1",
+	  .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = 9,
+	  .name = "Out 2",
+	  .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = 10,
+	  .name = "Out 3",
+	  .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = 11,
+	  .name = "Out 4",
+	  .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = 12,
+	  .name = "Out 5",
+	  .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = 13,
+	  .name = "Out 6",
+	  .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = 14,
+	  .name = "Out 7",
+	  .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = 15,
+	  .name = "Out 8",
+	  .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO,
+	},
+};
+
+static const struct spa_fga_descriptor param_eq_desc = {
+	.name = "param_eq",
+	.flags = SPA_FGA_DESCRIPTOR_SUPPORTS_NULL_DATA,
+
+	.n_ports = SPA_N_ELEMENTS(param_eq_ports),
+	.ports = param_eq_ports,
+
+	.instantiate = param_eq_instantiate,
+	.connect_port = param_eq_connect_port,
+	.run = param_eq_run,
+	.cleanup = free,
+};
+
+/** max */
+static void max_run(void * Instance, unsigned long SampleCount)
+{
+	struct builtin *impl = Instance;
+	float *out = impl->port[0], *in1 = impl->port[1], *in2 = impl->port[2];
+	unsigned long n;
+
+	if (out == NULL)
+		return;
+
+	if (in1 != NULL && in2 != NULL) {
+		for (n = 0; n < SampleCount; n++)
+			out[n] = SPA_MAX(in1[n], in2[n]);
+	} else if (in1 != NULL) {
+		for (n = 0; n < SampleCount; n++)
+			out[n] = in1[n];
+	} else if (in2 != NULL) {
+		for (n = 0; n < SampleCount; n++)
+			out[n] = in2[n];
+	} else {
+		for (n = 0; n < SampleCount; n++)
+			out[n] = 0.0f;
+	}
+}
+
+static struct spa_fga_port max_ports[] = {
+	{ .index = 0,
+	  .name = "Out",
+	  .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO,
+	},
+
+	{ .index = 1,
+	  .name = "In 1",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = 2,
+	  .name = "In 2",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO,
+	}
+};
+
+static const struct spa_fga_descriptor max_desc = {
+	.name = "max",
+	.flags = SPA_FGA_DESCRIPTOR_SUPPORTS_NULL_DATA,
+
+	.n_ports = SPA_N_ELEMENTS(max_ports),
+	.ports = max_ports,
+
+	.instantiate = builtin_instantiate,
+	.connect_port = builtin_connect_port,
+	.run = max_run,
+	.cleanup = builtin_cleanup,
+};
+
+/* DC blocking */
+struct dcblock {
+	float xm1;
+	float ym1;
+};
+
+struct dcblock_impl {
+	struct plugin *plugin;
+
+	struct spa_fga_dsp *dsp;
+	struct spa_log *log;
+
+	unsigned long rate;
+	float *port[17];
+
+	struct dcblock dc[8];
+};
+
+static void *dcblock_instantiate(const struct spa_fga_plugin *plugin, const struct spa_fga_descriptor * Descriptor,
+		unsigned long SampleRate, int index, const char *config)
+{
+	struct plugin *pl = SPA_CONTAINER_OF(plugin, struct plugin, plugin);
+	struct dcblock_impl *impl;
+
+	impl = calloc(1, sizeof(*impl));
+	if (impl == NULL)
+		return NULL;
+
+	impl->plugin = pl;
+	impl->dsp = pl->dsp;
+	impl->log = pl->log;
+	impl->rate = SampleRate;
+	return impl;
+}
+
+static void dcblock_run_n(struct dcblock dc[], float *dst[], const float *src[],
+		uint32_t n_src, float R, uint32_t n_samples)
+{
+	float x, y;
+	uint32_t i, n;
+
+	for (i = 0; i < n_src; i++) {
+		const float *in = src[i];
+		float *out = dst[i];
+		float xm1 = dc[i].xm1;
+		float ym1 = dc[i].ym1;
+
+		if (out == NULL || in == NULL)
+			continue;
+
+		for (n = 0; n < n_samples; n++) {
+			x = in[n];
+			y = x - xm1 + R * ym1;
+			xm1 = x;
+			ym1 = y;
+			out[n] = y;
+		}
+		dc[i].xm1 = xm1;
+		dc[i].ym1 = ym1;
+	}
+}
+
+static void dcblock_run(void * Instance, unsigned long SampleCount)
+{
+	struct dcblock_impl *impl = Instance;
+	float R = impl->port[16][0];
+	dcblock_run_n(impl->dc, &impl->port[8], (const float**)&impl->port[0], 8,
+			R, SampleCount);
+}
+
+static void dcblock_connect_port(void * Instance, unsigned long Port,
+                        float * DataLocation)
+{
+	struct dcblock_impl *impl = Instance;
+	impl->port[Port] = DataLocation;
+}
+
+static struct spa_fga_port dcblock_ports[] = {
+	{ .index = 0,
+	  .name = "In 1",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = 1,
+	  .name = "In 2",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = 2,
+	  .name = "In 3",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = 3,
+	  .name = "In 4",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = 4,
+	  .name = "In 5",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = 5,
+	  .name = "In 6",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = 6,
+	  .name = "In 7",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = 7,
+	  .name = "In 8",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO,
+	},
+
+	{ .index = 8,
+	  .name = "Out 1",
+	  .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = 9,
+	  .name = "Out 2",
+	  .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = 10,
+	  .name = "Out 3",
+	  .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = 11,
+	  .name = "Out 4",
+	  .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = 12,
+	  .name = "Out 5",
+	  .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = 13,
+	  .name = "Out 6",
+	  .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = 14,
+	  .name = "Out 7",
+	  .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = 15,
+	  .name = "Out 8",
+	  .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = 16,
+	  .name = "R",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL,
+	  .def = 0.995f, .min = 0.0f, .max = 1.0f
+	},
+};
+
+static const struct spa_fga_descriptor dcblock_desc = {
+	.name = "dcblock",
+	.flags = SPA_FGA_DESCRIPTOR_SUPPORTS_NULL_DATA,
+
+	.n_ports = SPA_N_ELEMENTS(dcblock_ports),
+	.ports = dcblock_ports,
+
+	.instantiate = dcblock_instantiate,
+	.connect_port = dcblock_connect_port,
+	.run = dcblock_run,
+	.cleanup = free,
+};
+
+/* ramp */
+static struct spa_fga_port ramp_ports[] = {
+	{ .index = 0,
+	  .name = "Out",
+	  .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = 1,
+	  .name = "Start",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL,
+	},
+	{ .index = 2,
+	  .name = "Stop",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL,
+	},
+	{ .index = 3,
+	  .name = "Current",
+	  .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_CONTROL,
+	},
+	{ .index = 4,
+	  .name = "Duration (s)",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL,
+	},
+};
+
+static void ramp_run(void * Instance, unsigned long SampleCount)
+{
+	struct builtin *impl = Instance;
+	float *out = impl->port[0];
+	float start = impl->port[1][0];
+	float stop = impl->port[2][0], last;
+	float *current = impl->port[3];
+	float duration = impl->port[4][0];
+	float inc = (stop - start) / (duration * impl->rate);
+	uint32_t n;
+
+	last = stop;
+	if (inc < 0.f)
+		SPA_SWAP(start, stop);
+
+	if (out != NULL) {
+		if (impl->accum == last) {
+			for (n = 0; n < SampleCount; n++)
+				out[n] = last;
+		} else {
+			for (n = 0; n < SampleCount; n++) {
+				out[n] = impl->accum;
+				impl->accum = SPA_CLAMP(impl->accum + inc, start, stop);
+			}
+		}
+	} else {
+		impl->accum = SPA_CLAMP(impl->accum + SampleCount * inc, start, stop);
+	}
+	if (current)
+		current[0] = impl->accum;
+}
+
+static const struct spa_fga_descriptor ramp_desc = {
+	.name = "ramp",
+	.flags = SPA_FGA_DESCRIPTOR_SUPPORTS_NULL_DATA,
+
+	.n_ports = SPA_N_ELEMENTS(ramp_ports),
+	.ports = ramp_ports,
+
+	.instantiate = builtin_instantiate,
+	.connect_port = builtin_connect_port,
+	.run = ramp_run,
+	.cleanup = builtin_cleanup,
+};
+
+/* abs */
+static void abs_run(void * Instance, unsigned long SampleCount)
+{
+	struct builtin *impl = Instance;
+	float *in = impl->port[1], *out = impl->port[0];
+
+	if (in != NULL && out != NULL) {
+		unsigned long n;
+		for (n = 0; n < SampleCount; n++) {
+			out[n] = SPA_ABS(in[n]);
+		}
+	}
+}
+
+static struct spa_fga_port abs_ports[] = {
+	{ .index = 0,
+	  .name = "Out",
+	  .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = 1,
+	  .name = "In",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO,
+	},
+};
+
+static const struct spa_fga_descriptor abs_desc = {
+	.name = "abs",
+	.flags = SPA_FGA_DESCRIPTOR_SUPPORTS_NULL_DATA,
+
+	.n_ports = SPA_N_ELEMENTS(abs_ports),
+	.ports = abs_ports,
+
+	.instantiate = builtin_instantiate,
+	.connect_port = builtin_connect_port,
+	.run = abs_run,
+	.cleanup = builtin_cleanup,
+};
+
+/* sqrt */
+static void sqrt_run(void * Instance, unsigned long SampleCount)
+{
+	struct builtin *impl = Instance;
+	float *in = impl->port[1], *out = impl->port[0];
+
+	if (in != NULL && out != NULL) {
+		unsigned long n;
+		for (n = 0; n < SampleCount; n++) {
+			if (in[n] <= 0.0f)
+				out[n] = 0.0f;
+			else
+				out[n] = sqrtf(in[n]);
+		}
+	}
+}
+
+static struct spa_fga_port sqrt_ports[] = {
+	{ .index = 0,
+	  .name = "Out",
+	  .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = 1,
+	  .name = "In",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO,
+	},
+};
+
+static const struct spa_fga_descriptor sqrt_desc = {
+	.name = "sqrt",
+	.flags = SPA_FGA_DESCRIPTOR_SUPPORTS_NULL_DATA,
+
+	.n_ports = SPA_N_ELEMENTS(sqrt_ports),
+	.ports = sqrt_ports,
+
+	.instantiate = builtin_instantiate,
+	.connect_port = builtin_connect_port,
+	.run = sqrt_run,
+	.cleanup = builtin_cleanup,
+};
+
+static const struct spa_fga_descriptor * builtin_descriptor(unsigned long Index)
+{
+	switch(Index) {
+	case 0:
+		return &mixer_desc;
+	case 1:
+		return &bq_lowpass_desc;
+	case 2:
+		return &bq_highpass_desc;
+	case 3:
+		return &bq_bandpass_desc;
+	case 4:
+		return &bq_lowshelf_desc;
+	case 5:
+		return &bq_highshelf_desc;
+	case 6:
+		return &bq_peaking_desc;
+	case 7:
+		return &bq_notch_desc;
+	case 8:
+		return &bq_allpass_desc;
+	case 9:
+		return &copy_desc;
+	case 10:
+		return &convolve_desc;
+	case 11:
+		return &delay_desc;
+	case 12:
+		return &invert_desc;
+	case 13:
+		return &bq_raw_desc;
+	case 14:
+		return &clamp_desc;
+	case 15:
+		return &linear_desc;
+	case 16:
+		return &recip_desc;
+	case 17:
+		return &exp_desc;
+	case 18:
+		return &log_desc;
+	case 19:
+		return &mult_desc;
+	case 20:
+		return &sine_desc;
+	case 21:
+		return &param_eq_desc;
+	case 22:
+		return &max_desc;
+	case 23:
+		return &dcblock_desc;
+	case 24:
+		return &ramp_desc;
+	case 25:
+		return &abs_desc;
+	case 26:
+		return &sqrt_desc;
+	}
+	return NULL;
+}
+
+static const struct spa_fga_descriptor *builtin_plugin_make_desc(void *plugin, const char *name)
+{
+	unsigned long i;
+	for (i = 0; ;i++) {
+		const struct spa_fga_descriptor *d = builtin_descriptor(i);
+		if (d == NULL)
+			break;
+		if (spa_streq(d->name, name))
+			return d;
+	}
+	return NULL;
+}
+
+static struct spa_fga_plugin_methods impl_plugin = {
+	SPA_VERSION_FGA_PLUGIN_METHODS,
+	.make_desc = builtin_plugin_make_desc,
+};
+
+static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface)
+{
+	struct plugin *impl;
+
+	spa_return_val_if_fail(handle != NULL, -EINVAL);
+	spa_return_val_if_fail(interface != NULL, -EINVAL);
+
+	impl = (struct plugin *) handle;
+
+	if (spa_streq(type, SPA_TYPE_INTERFACE_FILTER_GRAPH_AudioPlugin))
+		*interface = &impl->plugin;
+	else
+		return -ENOENT;
+
+	return 0;
+}
+
+static int impl_clear(struct spa_handle *handle)
+{
+	return 0;
+}
+
+static size_t
+impl_get_size(const struct spa_handle_factory *factory,
+	      const struct spa_dict *params)
+{
+	return sizeof(struct plugin);
+}
+
+static int
+impl_init(const struct spa_handle_factory *factory,
+	  struct spa_handle *handle,
+	  const struct spa_dict *info,
+	  const struct spa_support *support,
+	  uint32_t n_support)
+{
+	struct plugin *impl;
+
+	handle->get_interface = impl_get_interface;
+	handle->clear = impl_clear;
+
+	impl = (struct plugin *) handle;
+
+	impl->plugin.iface = SPA_INTERFACE_INIT(
+			SPA_TYPE_INTERFACE_FILTER_GRAPH_AudioPlugin,
+			SPA_VERSION_FGA_PLUGIN,
+			&impl_plugin, impl);
+
+	impl->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
+	impl->dsp = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_FILTER_GRAPH_AudioDSP);
+
+	for (uint32_t i = 0; info && i < info->n_items; i++) {
+		const char *k = info->items[i].key;
+		const char *s = info->items[i].value;
+		if (spa_streq(k, "filter.graph.audio.dsp"))
+			sscanf(s, "pointer:%p", &impl->dsp);
+	}
+	if (impl->dsp == NULL) {
+		spa_log_error(impl->log, "%p: could not find DSP functions", impl);
+		return -EINVAL;
+	}
+	return 0;
+}
+
+static const struct spa_interface_info impl_interfaces[] = {
+	{SPA_TYPE_INTERFACE_FILTER_GRAPH_AudioPlugin,},
+};
+
+static int
+impl_enum_interface_info(const struct spa_handle_factory *factory,
+			 const struct spa_interface_info **info,
+			 uint32_t *index)
+{
+	spa_return_val_if_fail(factory != NULL, -EINVAL);
+	spa_return_val_if_fail(info != NULL, -EINVAL);
+	spa_return_val_if_fail(index != NULL, -EINVAL);
+
+	switch (*index) {
+	case 0:
+		*info = &impl_interfaces[*index];
+		break;
+	default:
+		return 0;
+	}
+	(*index)++;
+	return 1;
+}
+
+static struct spa_handle_factory spa_fga_plugin_builtin_factory = {
+	SPA_VERSION_HANDLE_FACTORY,
+	"filter.graph.plugin.builtin",
+	NULL,
+	impl_get_size,
+	impl_init,
+	impl_enum_interface_info,
+};
+
+SPA_EXPORT
+int spa_handle_factory_enum(const struct spa_handle_factory **factory, uint32_t *index)
+{
+	spa_return_val_if_fail(factory != NULL, -EINVAL);
+	spa_return_val_if_fail(index != NULL, -EINVAL);
+
+	switch (*index) {
+	case 0:
+		*factory = &spa_fga_plugin_builtin_factory;
+		break;
+	default:
+		return 0;
+	}
+	(*index)++;
+	return 1;
+}
diff --git a/src/modules/module-filter-chain/convolver.c b/spa/plugins/filter-graph/convolver.c
similarity index 51%
rename from src/modules/module-filter-chain/convolver.c
rename to spa/plugins/filter-graph/convolver.c
index 3aa7230c..e5bd47b3 100644
--- a/src/modules/module-filter-chain/convolver.c
+++ b/spa/plugins/filter-graph/convolver.c
@@ -11,8 +11,6 @@
 #include <math.h>
 
 struct convolver1 {
-	struct dsp_ops *dsp;
-
 	int blockSize;
 	int segSize;
 	int segCount;
@@ -37,37 +35,6 @@ struct convolver1 {
 	float scale;
 };
 
-static void *fft_alloc(int size)
-{
-	size_t nb_bytes = size * sizeof(float);
-#define ALIGNMENT 64
-	void *p, *p0 = malloc(nb_bytes + ALIGNMENT);
-	if (!p0)
-		return (void *)0;
-	p = (void *)(((size_t)p0 + ALIGNMENT) & (~((size_t)(ALIGNMENT - 1))));
-	*((void **)p - 1) = p0;
-	return p;
-}
-static void fft_free(void *p)
-{
-	if (p)
-		free(*((void **)p - 1));
-}
-
-static inline void fft_cpx_clear(struct dsp_ops *dsp, float *v, int size)
-{
-	dsp_ops_clear(dsp, v, size * 2);
-}
-static float *fft_cpx_alloc(int size)
-{
-	return fft_alloc(size * 2);
-}
-
-static void fft_cpx_free(float *cpx)
-{
-	fft_free(cpx);
-}
-
 static int next_power_of_two(int val)
 {
 	int r = 1;
@@ -76,20 +43,44 @@ static int next_power_of_two(int val)
 	return r;
 }
 
-static void convolver1_reset(struct convolver1 *conv)
+static void convolver1_reset(struct spa_fga_dsp *dsp, struct convolver1 *conv)
 {
 	int i;
 	for (i = 0; i < conv->segCount; i++)
-		fft_cpx_clear(conv->dsp, conv->segments[i], conv->fftComplexSize);
-	dsp_ops_clear(conv->dsp, conv->overlap, conv->blockSize);
-	dsp_ops_clear(conv->dsp, conv->inputBuffer, conv->segSize);
-	fft_cpx_clear(conv->dsp, conv->pre_mult, conv->fftComplexSize);
-	fft_cpx_clear(conv->dsp, conv->conv, conv->fftComplexSize);
+		spa_fga_dsp_fft_memclear(dsp, conv->segments[i], conv->fftComplexSize, false);
+	spa_fga_dsp_fft_memclear(dsp, conv->overlap, conv->blockSize, true);
+	spa_fga_dsp_fft_memclear(dsp, conv->inputBuffer, conv->segSize, true);
+	spa_fga_dsp_fft_memclear(dsp, conv->pre_mult, conv->fftComplexSize, false);
+	spa_fga_dsp_fft_memclear(dsp, conv->conv, conv->fftComplexSize, false);
 	conv->inputBufferFill = 0;
 	conv->current = 0;
 }
 
-static struct convolver1 *convolver1_new(struct dsp_ops *dsp, int block, const float *ir, int irlen)
+static void convolver1_free(struct spa_fga_dsp *dsp, struct convolver1 *conv)
+{
+	int i;
+	for (i = 0; i < conv->segCount; i++) {
+		if (conv->segments)
+			spa_fga_dsp_fft_memfree(dsp, conv->segments[i]);
+		if (conv->segmentsIr)
+			spa_fga_dsp_fft_memfree(dsp, conv->segmentsIr[i]);
+	}
+	if (conv->fft)
+		spa_fga_dsp_fft_free(dsp, conv->fft);
+	if (conv->ifft)
+		spa_fga_dsp_fft_free(dsp, conv->ifft);
+	if (conv->fft_buffer)
+		spa_fga_dsp_fft_memfree(dsp, conv->fft_buffer);
+	free(conv->segments);
+	free(conv->segmentsIr);
+	spa_fga_dsp_fft_memfree(dsp, conv->pre_mult);
+	spa_fga_dsp_fft_memfree(dsp, conv->conv);
+	spa_fga_dsp_fft_memfree(dsp, conv->overlap);
+	spa_fga_dsp_fft_memfree(dsp, conv->inputBuffer);
+	free(conv);
+}
+
+static struct convolver1 *convolver1_new(struct spa_fga_dsp *dsp, int block, const float *ir, int irlen)
 {
 	struct convolver1 *conv;
 	int i;
@@ -107,86 +98,64 @@ static struct convolver1 *convolver1_new(struct dsp_ops *dsp, int block, const f
 	if (irlen == 0)
 		return conv;
 
-	conv->dsp = dsp;
 	conv->blockSize = next_power_of_two(block);
 	conv->segSize = 2 * conv->blockSize;
 	conv->segCount = (irlen + conv->blockSize-1) / conv->blockSize;
 	conv->fftComplexSize = (conv->segSize / 2) + 1;
 
-	conv->fft = dsp_ops_fft_new(conv->dsp, conv->segSize, true);
+	conv->fft = spa_fga_dsp_fft_new(dsp, conv->segSize, true);
 	if (conv->fft == NULL)
 		goto error;
-	conv->ifft = dsp_ops_fft_new(conv->dsp, conv->segSize, true);
+	conv->ifft = spa_fga_dsp_fft_new(dsp, conv->segSize, true);
 	if (conv->ifft == NULL)
 		goto error;
 
-	conv->fft_buffer = fft_alloc(conv->segSize);
+	conv->fft_buffer = spa_fga_dsp_fft_memalloc(dsp, conv->segSize, true);
 	if (conv->fft_buffer == NULL)
 		goto error;
 
 	conv->segments = calloc(conv->segCount, sizeof(float*));
 	conv->segmentsIr = calloc(conv->segCount, sizeof(float*));
+	if (conv->segments == NULL || conv->segmentsIr == NULL)
+		goto error;
 
 	for (i = 0; i < conv->segCount; i++) {
 		int left = irlen - (i * conv->blockSize);
 		int copy = SPA_MIN(conv->blockSize, left);
 
-		conv->segments[i] = fft_cpx_alloc(conv->fftComplexSize);
-		conv->segmentsIr[i] = fft_cpx_alloc(conv->fftComplexSize);
+		conv->segments[i] = spa_fga_dsp_fft_memalloc(dsp, conv->fftComplexSize, false);
+		conv->segmentsIr[i] = spa_fga_dsp_fft_memalloc(dsp, conv->fftComplexSize, false);
+		if (conv->segments[i] == NULL || conv->segmentsIr[i] == NULL)
+			goto error;
 
-		dsp_ops_copy(conv->dsp, conv->fft_buffer, &ir[i * conv->blockSize], copy);
+		spa_fga_dsp_copy(dsp, conv->fft_buffer, &ir[i * conv->blockSize], copy);
 		if (copy < conv->segSize)
-			dsp_ops_clear(conv->dsp, conv->fft_buffer + copy, conv->segSize - copy);
+			spa_fga_dsp_fft_memclear(dsp, conv->fft_buffer + copy, conv->segSize - copy, true);
 
-	        dsp_ops_fft_run(conv->dsp, conv->fft, 1, conv->fft_buffer, conv->segmentsIr[i]);
+	        spa_fga_dsp_fft_run(dsp, conv->fft, 1, conv->fft_buffer, conv->segmentsIr[i]);
 	}
-	conv->pre_mult = fft_cpx_alloc(conv->fftComplexSize);
-	conv->conv = fft_cpx_alloc(conv->fftComplexSize);
-	conv->overlap = fft_alloc(conv->blockSize);
-	conv->inputBuffer = fft_alloc(conv->segSize);
+	conv->pre_mult = spa_fga_dsp_fft_memalloc(dsp, conv->fftComplexSize, false);
+	conv->conv = spa_fga_dsp_fft_memalloc(dsp, conv->fftComplexSize, false);
+	conv->overlap = spa_fga_dsp_fft_memalloc(dsp, conv->blockSize, true);
+	conv->inputBuffer = spa_fga_dsp_fft_memalloc(dsp, conv->segSize, true);
+	if (conv->pre_mult == NULL || conv->conv == NULL || conv->overlap == NULL ||
+			conv->inputBuffer == NULL)
+			goto error;
 	conv->scale = 1.0f / conv->segSize;
-	convolver1_reset(conv);
+	convolver1_reset(dsp, conv);
 
 	return conv;
 error:
-	if (conv->fft)
-		dsp_ops_fft_free(dsp, conv->fft);
-	if (conv->ifft)
-		dsp_ops_fft_free(dsp, conv->ifft);
-	if (conv->fft_buffer)
-		fft_free(conv->fft_buffer);
-	free(conv);
+	convolver1_free(dsp, conv);
 	return NULL;
 }
 
-static void convolver1_free(struct convolver1 *conv)
-{
-	int i;
-	for (i = 0; i < conv->segCount; i++) {
-		fft_cpx_free(conv->segments[i]);
-		fft_cpx_free(conv->segmentsIr[i]);
-	}
-	if (conv->fft)
-		dsp_ops_fft_free(conv->dsp, conv->fft);
-	if (conv->ifft)
-		dsp_ops_fft_free(conv->dsp, conv->ifft);
-	if (conv->fft_buffer)
-		fft_free(conv->fft_buffer);
-	free(conv->segments);
-	free(conv->segmentsIr);
-	fft_cpx_free(conv->pre_mult);
-	fft_cpx_free(conv->conv);
-	fft_free(conv->overlap);
-	fft_free(conv->inputBuffer);
-	free(conv);
-}
-
-static int convolver1_run(struct convolver1 *conv, const float *input, float *output, int len)
+static int convolver1_run(struct spa_fga_dsp *dsp, struct convolver1 *conv, const float *input, float *output, int len)
 {
 	int i, processed = 0;
 
 	if (conv == NULL || conv->segCount == 0) {
-		dsp_ops_clear(conv->dsp, output, len);
+		spa_fga_dsp_fft_memclear(dsp, output, len, true);
 		return len;
 	}
 
@@ -194,17 +163,17 @@ static int convolver1_run(struct convolver1 *conv, const float *input, float *ou
 		const int processing = SPA_MIN(len - processed, conv->blockSize - conv->inputBufferFill);
 		const int inputBufferPos = conv->inputBufferFill;
 
-		dsp_ops_copy(conv->dsp, conv->inputBuffer + inputBufferPos, input + processed, processing);
+		spa_fga_dsp_copy(dsp, conv->inputBuffer + inputBufferPos, input + processed, processing);
 		if (inputBufferPos == 0 && processing < conv->blockSize)
-			dsp_ops_clear(conv->dsp, conv->inputBuffer + processing, conv->blockSize - processing);
+			spa_fga_dsp_fft_memclear(dsp, conv->inputBuffer + processing, conv->blockSize - processing, true);
 
-		dsp_ops_fft_run(conv->dsp, conv->fft, 1, conv->inputBuffer, conv->segments[conv->current]);
+		spa_fga_dsp_fft_run(dsp, conv->fft, 1, conv->inputBuffer, conv->segments[conv->current]);
 
 		if (conv->segCount > 1) {
 			if (conv->inputBufferFill == 0) {
 				int indexAudio = (conv->current + 1) % conv->segCount;
 
-				dsp_ops_fft_cmul(conv->dsp, conv->fft, conv->pre_mult,
+				spa_fga_dsp_fft_cmul(dsp, conv->fft, conv->pre_mult,
 						conv->segmentsIr[1],
 						conv->segments[indexAudio],
 						conv->fftComplexSize, conv->scale);
@@ -212,7 +181,7 @@ static int convolver1_run(struct convolver1 *conv, const float *input, float *ou
 				for (i = 2; i < conv->segCount; i++) {
 					indexAudio = (conv->current + i) % conv->segCount;
 
-					dsp_ops_fft_cmuladd(conv->dsp, conv->fft,
+					spa_fga_dsp_fft_cmuladd(dsp, conv->fft,
 							conv->pre_mult,
 							conv->pre_mult,
 							conv->segmentsIr[i],
@@ -220,30 +189,30 @@ static int convolver1_run(struct convolver1 *conv, const float *input, float *ou
 							conv->fftComplexSize, conv->scale);
 				}
 			}
-			dsp_ops_fft_cmuladd(conv->dsp, conv->fft,
+			spa_fga_dsp_fft_cmuladd(dsp, conv->fft,
 					conv->conv,
 					conv->pre_mult,
 					conv->segments[conv->current],
 					conv->segmentsIr[0],
 					conv->fftComplexSize, conv->scale);
 		} else {
-			dsp_ops_fft_cmul(conv->dsp, conv->fft,
+			spa_fga_dsp_fft_cmul(dsp, conv->fft,
 					conv->conv,
 					conv->segments[conv->current],
 					conv->segmentsIr[0],
 					conv->fftComplexSize, conv->scale);
 		}
 
-		dsp_ops_fft_run(conv->dsp, conv->ifft, -1, conv->conv, conv->fft_buffer);
+		spa_fga_dsp_fft_run(dsp, conv->ifft, -1, conv->conv, conv->fft_buffer);
 
-		dsp_ops_sum(conv->dsp, output + processed, conv->fft_buffer + inputBufferPos,
+		spa_fga_dsp_sum(dsp, output + processed, conv->fft_buffer + inputBufferPos,
 				conv->overlap + inputBufferPos, processing);
 
 		conv->inputBufferFill += processing;
 		if (conv->inputBufferFill == conv->blockSize) {
 			conv->inputBufferFill = 0;
 
-			dsp_ops_copy(conv->dsp, conv->overlap, conv->fft_buffer + conv->blockSize, conv->blockSize);
+			spa_fga_dsp_copy(dsp, conv->overlap, conv->fft_buffer + conv->blockSize, conv->blockSize);
 
 			conv->current = (conv->current > 0) ? (conv->current - 1) : (conv->segCount - 1);
 		}
@@ -255,7 +224,7 @@ static int convolver1_run(struct convolver1 *conv, const float *input, float *ou
 
 struct convolver
 {
-	struct dsp_ops *dsp;
+	struct spa_fga_dsp *dsp;
 	int headBlockSize;
 	int tailBlockSize;
 	struct convolver1 *headConvolver;
@@ -272,23 +241,25 @@ struct convolver
 
 void convolver_reset(struct convolver *conv)
 {
+	struct spa_fga_dsp *dsp = conv->dsp;
+
 	if (conv->headConvolver)
-		convolver1_reset(conv->headConvolver);
+		convolver1_reset(dsp, conv->headConvolver);
 	if (conv->tailConvolver0) {
-		convolver1_reset(conv->tailConvolver0);
-		dsp_ops_clear(conv->dsp, conv->tailOutput0, conv->tailBlockSize);
-		dsp_ops_clear(conv->dsp, conv->tailPrecalculated0, conv->tailBlockSize);
+		convolver1_reset(dsp, conv->tailConvolver0);
+		spa_fga_dsp_fft_memclear(dsp, conv->tailOutput0, conv->tailBlockSize, true);
+		spa_fga_dsp_fft_memclear(dsp, conv->tailPrecalculated0, conv->tailBlockSize, true);
 	}
 	if (conv->tailConvolver) {
-		convolver1_reset(conv->tailConvolver);
-		dsp_ops_clear(conv->dsp, conv->tailOutput, conv->tailBlockSize);
-		dsp_ops_clear(conv->dsp, conv->tailPrecalculated, conv->tailBlockSize);
+		convolver1_reset(dsp, conv->tailConvolver);
+		spa_fga_dsp_fft_memclear(dsp, conv->tailOutput, conv->tailBlockSize, true);
+		spa_fga_dsp_fft_memclear(dsp, conv->tailPrecalculated, conv->tailBlockSize, true);
 	}
 	conv->tailInputFill = 0;
 	conv->precalculatedPos = 0;
 }
 
-struct convolver *convolver_new(struct dsp_ops *dsp_ops, int head_block, int tail_block, const float *ir, int irlen)
+struct convolver *convolver_new(struct spa_fga_dsp *dsp, int head_block, int tail_block, const float *ir, int irlen)
 {
 	struct convolver *conv;
 	int head_ir_len;
@@ -307,57 +278,76 @@ struct convolver *convolver_new(struct dsp_ops *dsp_ops, int head_block, int tai
 	if (conv == NULL)
 		return NULL;
 
+	conv->dsp = dsp;
+
 	if (irlen == 0)
 		return conv;
 
-	conv->dsp = dsp_ops;
 	conv->headBlockSize = next_power_of_two(head_block);
 	conv->tailBlockSize = next_power_of_two(tail_block);
 
 	head_ir_len = SPA_MIN(irlen, conv->tailBlockSize);
-	conv->headConvolver = convolver1_new(dsp_ops, conv->headBlockSize, ir, head_ir_len);
+	conv->headConvolver = convolver1_new(dsp, conv->headBlockSize, ir, head_ir_len);
+	if (conv->headConvolver == NULL)
+		goto error;
 
 	if (irlen > conv->tailBlockSize) {
 		int conv1IrLen = SPA_MIN(irlen - conv->tailBlockSize, conv->tailBlockSize);
-		conv->tailConvolver0 = convolver1_new(dsp_ops, conv->headBlockSize, ir + conv->tailBlockSize, conv1IrLen);
-		conv->tailOutput0 = fft_alloc(conv->tailBlockSize);
-		conv->tailPrecalculated0 = fft_alloc(conv->tailBlockSize);
+		conv->tailConvolver0 = convolver1_new(dsp, conv->headBlockSize, ir + conv->tailBlockSize, conv1IrLen);
+		conv->tailOutput0 = spa_fga_dsp_fft_memalloc(dsp, conv->tailBlockSize, true);
+		conv->tailPrecalculated0 = spa_fga_dsp_fft_memalloc(dsp, conv->tailBlockSize, true);
+		if (conv->tailConvolver0 == NULL || conv->tailOutput0 == NULL ||
+				conv->tailPrecalculated0 == NULL)
+			goto error;
 	}
 
 	if (irlen > 2 * conv->tailBlockSize) {
 		int tailIrLen = irlen - (2 * conv->tailBlockSize);
-		conv->tailConvolver = convolver1_new(dsp_ops, conv->tailBlockSize, ir + (2 * conv->tailBlockSize), tailIrLen);
-		conv->tailOutput = fft_alloc(conv->tailBlockSize);
-		conv->tailPrecalculated = fft_alloc(conv->tailBlockSize);
+		conv->tailConvolver = convolver1_new(dsp, conv->tailBlockSize, ir + (2 * conv->tailBlockSize), tailIrLen);
+		conv->tailOutput = spa_fga_dsp_fft_memalloc(dsp, conv->tailBlockSize, true);
+		conv->tailPrecalculated = spa_fga_dsp_fft_memalloc(dsp, conv->tailBlockSize, true);
+		if (conv->tailConvolver == NULL || conv->tailOutput == NULL ||
+				conv->tailPrecalculated == NULL)
+			goto error;
 	}
 
-	if (conv->tailConvolver0 || conv->tailConvolver)
-		conv->tailInput = fft_alloc(conv->tailBlockSize);
+	if (conv->tailConvolver0 || conv->tailConvolver) {
+		conv->tailInput = spa_fga_dsp_fft_memalloc(dsp, conv->tailBlockSize, true);
+		if (conv->tailInput == NULL)
+			goto error;
+	}
 
 	convolver_reset(conv);
 
 	return conv;
+error:
+	convolver_free(conv);
+	return NULL;
 }
 
 void convolver_free(struct convolver *conv)
 {
+	struct spa_fga_dsp *dsp = conv->dsp;
+
 	if (conv->headConvolver)
-		convolver1_free(conv->headConvolver);
+		convolver1_free(dsp, conv->headConvolver);
 	if (conv->tailConvolver0)
-		convolver1_free(conv->tailConvolver0);
+		convolver1_free(dsp, conv->tailConvolver0);
 	if (conv->tailConvolver)
-		convolver1_free(conv->tailConvolver);
-	fft_free(conv->tailOutput0);
-	fft_free(conv->tailPrecalculated0);
-	fft_free(conv->tailOutput);
-	fft_free(conv->tailPrecalculated);
-	fft_free(conv->tailInput);
+		convolver1_free(dsp, conv->tailConvolver);
+	spa_fga_dsp_fft_memfree(dsp, conv->tailOutput0);
+	spa_fga_dsp_fft_memfree(dsp, conv->tailPrecalculated0);
+	spa_fga_dsp_fft_memfree(dsp, conv->tailOutput);
+	spa_fga_dsp_fft_memfree(dsp, conv->tailPrecalculated);
+	spa_fga_dsp_fft_memfree(dsp, conv->tailInput);
 	free(conv);
 }
 
 int convolver_run(struct convolver *conv, const float *input, float *output, int length)
 {
-	convolver1_run(conv->headConvolver, input, output, length);
+	struct spa_fga_dsp *dsp = conv->dsp;
+
+	convolver1_run(dsp, conv->headConvolver, input, output, length);
 
 	if (conv->tailInput) {
 		int processed = 0;
@@ -367,21 +357,21 @@ int convolver_run(struct convolver *conv, const float *input, float *output, int
 			int processing = SPA_MIN(remaining, conv->headBlockSize - (conv->tailInputFill % conv->headBlockSize));
 
 			if (conv->tailPrecalculated0)
-				dsp_ops_sum(conv->dsp, &output[processed], &output[processed],
+				spa_fga_dsp_sum(dsp, &output[processed], &output[processed],
 						&conv->tailPrecalculated0[conv->precalculatedPos],
 						processing);
 			if (conv->tailPrecalculated)
-				dsp_ops_sum(conv->dsp, &output[processed], &output[processed],
+				spa_fga_dsp_sum(dsp, &output[processed], &output[processed],
 						&conv->tailPrecalculated[conv->precalculatedPos],
 						processing);
 			conv->precalculatedPos += processing;
 
-			dsp_ops_copy(conv->dsp, conv->tailInput + conv->tailInputFill, input + processed, processing);
+			spa_fga_dsp_copy(dsp, conv->tailInput + conv->tailInputFill, input + processed, processing);
 			conv->tailInputFill += processing;
 
 			if (conv->tailPrecalculated0 && (conv->tailInputFill % conv->headBlockSize == 0)) {
 				int blockOffset = conv->tailInputFill - conv->headBlockSize;
-				convolver1_run(conv->tailConvolver0,
+				convolver1_run(dsp, conv->tailConvolver0,
 						conv->tailInput + blockOffset,
 						conv->tailOutput0 + blockOffset,
 						conv->headBlockSize);
@@ -392,7 +382,7 @@ int convolver_run(struct convolver *conv, const float *input, float *output, int
 			if (conv->tailPrecalculated &&
 			    conv->tailInputFill == conv->tailBlockSize) {
 				SPA_SWAP(conv->tailPrecalculated, conv->tailOutput);
-				convolver1_run(conv->tailConvolver, conv->tailInput,
+				convolver1_run(dsp, conv->tailConvolver, conv->tailInput,
 						conv->tailOutput, conv->tailBlockSize);
 			}
 			if (conv->tailInputFill == conv->tailBlockSize) {
diff --git a/src/modules/module-filter-chain/convolver.h b/spa/plugins/filter-graph/convolver.h
similarity index 72%
rename from src/modules/module-filter-chain/convolver.h
rename to spa/plugins/filter-graph/convolver.h
index e8749d7b..ad6139a3 100644
--- a/src/modules/module-filter-chain/convolver.h
+++ b/spa/plugins/filter-graph/convolver.h
@@ -5,9 +5,9 @@
 #include <stdint.h>
 #include <stddef.h>
 
-#include "dsp-ops.h"
+#include "audio-dsp.h"
 
-struct convolver *convolver_new(struct dsp_ops *dsp, int block, int tail, const float *ir, int irlen);
+struct convolver *convolver_new(struct spa_fga_dsp *dsp, int block, int tail, const float *ir, int irlen);
 void convolver_free(struct convolver *conv);
 
 void convolver_reset(struct convolver *conv);
diff --git a/spa/plugins/filter-graph/ebur128_plugin.c b/spa/plugins/filter-graph/ebur128_plugin.c
new file mode 100644
index 00000000..22bf5f51
--- /dev/null
+++ b/spa/plugins/filter-graph/ebur128_plugin.c
@@ -0,0 +1,631 @@
+#include "config.h"
+
+#include <limits.h>
+
+#include <spa/utils/json.h>
+#include <spa/support/log.h>
+
+#include "audio-plugin.h"
+#include "audio-dsp.h"
+
+#include <ebur128.h>
+
+struct plugin {
+	struct spa_handle handle;
+	struct spa_fga_plugin plugin;
+
+	struct spa_fga_dsp *dsp;
+	struct spa_log *log;
+	uint32_t quantum_limit;
+};
+
+enum {
+	PORT_IN_FL,
+	PORT_IN_FR,
+	PORT_IN_FC,
+	PORT_IN_UNUSED,
+	PORT_IN_SL,
+	PORT_IN_SR,
+	PORT_IN_DUAL_MONO,
+
+	PORT_OUT_FL,
+	PORT_OUT_FR,
+	PORT_OUT_FC,
+	PORT_OUT_UNUSED,
+	PORT_OUT_SL,
+	PORT_OUT_SR,
+	PORT_OUT_DUAL_MONO,
+
+	PORT_OUT_MOMENTARY,
+	PORT_OUT_SHORTTERM,
+	PORT_OUT_GLOBAL,
+	PORT_OUT_WINDOW,
+	PORT_OUT_RANGE,
+	PORT_OUT_PEAK,
+	PORT_OUT_TRUE_PEAK,
+
+	PORT_MAX,
+
+	PORT_IN_START = PORT_IN_FL,
+	PORT_OUT_START = PORT_OUT_FL,
+	PORT_NOTIFY_START = PORT_OUT_MOMENTARY,
+};
+
+
+struct ebur128_impl {
+	struct plugin *plugin;
+
+	struct spa_fga_dsp *dsp;
+	struct spa_log *log;
+
+	unsigned long rate;
+	float *port[PORT_MAX];
+
+	unsigned int max_history;
+	unsigned int max_window;
+	bool use_histogram;
+
+	ebur128_state *st[7];
+};
+
+static void * ebur128_instantiate(const struct spa_fga_plugin *plugin, const struct spa_fga_descriptor * Descriptor,
+		unsigned long SampleRate, int index, const char *config)
+{
+	struct plugin *pl = SPA_CONTAINER_OF(plugin, struct plugin, plugin);
+	struct ebur128_impl *impl;
+	struct spa_json it[1];
+	const char *val;
+	char key[256];
+	int len;
+	float f;
+
+	impl = calloc(1, sizeof(*impl));
+	if (impl == NULL) {
+		errno = ENOMEM;
+		return NULL;
+	}
+
+	impl->plugin = pl;
+	impl->dsp = pl->dsp;
+	impl->log = pl->log;
+	impl->max_history = 10000;
+	impl->max_window = 0;
+	impl->rate = SampleRate;
+
+	if (config == NULL)
+		return impl;
+
+	if (spa_json_begin_object(&it[0], config, strlen(config)) <= 0) {
+		spa_log_error(pl->log, "ebur128: expected object in config");
+		errno = EINVAL;
+		goto error;
+	}
+	while ((len = spa_json_object_next(&it[0], key, sizeof(key), &val)) > 0) {
+		if (spa_streq(key, "max-history")) {
+			if (spa_json_parse_float(val, len, &f) <= 0) {
+				spa_log_error(impl->log, "ebur128:max-history requires a number");
+				errno = EINVAL;
+				goto error;
+			}
+			impl->max_history = (unsigned int) (f * 1000.0f);
+		}
+		else if (spa_streq(key, "max-window")) {
+			if (spa_json_parse_float(val, len, &f) <= 0) {
+				spa_log_error(impl->log, "ebur128:max-window requires a number");
+				errno = EINVAL;
+				goto error;
+			}
+			impl->max_window = (unsigned int) (f * 1000.0f);
+		}
+		else if (spa_streq(key, "use-histogram")) {
+			if (spa_json_parse_bool(val, len, &impl->use_histogram) <= 0) {
+				spa_log_error(impl->log, "ebur128:use-histogram requires a boolean");
+				errno = EINVAL;
+				goto error;
+			}
+		} else {
+			spa_log_warn(impl->log, "ebur128: unknown key %s", key);
+		}
+	}
+	return impl;
+error:
+	free(impl);
+	return NULL;
+}
+
+static void ebur128_run(void * Instance, unsigned long SampleCount)
+{
+	struct ebur128_impl *impl = Instance;
+	int i, c;
+	double value;
+	ebur128_state *st[7];
+
+	for (i = 0; i < 7; i++) {
+		float *in = impl->port[PORT_IN_START + i];
+		float *out = impl->port[PORT_OUT_START + i];
+
+		st[i] = NULL;
+		if (in == NULL)
+			continue;
+
+		st[i] = impl->st[i];
+		if (st[i] != NULL)
+			ebur128_add_frames_float(st[i], in, SampleCount);
+
+		if (out != NULL)
+			memcpy(out, in, SampleCount * sizeof(float));
+	}
+	if (impl->port[PORT_OUT_MOMENTARY] != NULL) {
+		double sum = 0.0;
+		for (i = 0, c = 0; i < 7; i++) {
+			if (st[i] != NULL) {
+				ebur128_loudness_momentary(st[i], &value);
+				sum += value;
+				c++;
+			}
+		}
+		impl->port[PORT_OUT_MOMENTARY][0] = (float) (sum / c);
+	}
+	if (impl->port[PORT_OUT_SHORTTERM] != NULL) {
+		double sum = 0.0;
+		for (i = 0, c = 0; i < 7; i++) {
+			if (st[i] != NULL) {
+				ebur128_loudness_shortterm(st[i], &value);
+				sum += value;
+				c++;
+			}
+		}
+		impl->port[PORT_OUT_SHORTTERM][0] = (float) (sum / c);
+	}
+	if (impl->port[PORT_OUT_GLOBAL] != NULL) {
+		ebur128_loudness_global_multiple(st, 7, &value);
+		impl->port[PORT_OUT_GLOBAL][0] = (float)value;
+	}
+	if (impl->port[PORT_OUT_WINDOW] != NULL) {
+		double sum = 0.0;
+		for (i = 0, c = 0; i < 7; i++) {
+			if (st[i] != NULL) {
+				ebur128_loudness_window(st[i], impl->max_window, &value);
+				sum += value;
+				c++;
+			}
+		}
+		impl->port[PORT_OUT_WINDOW][0] = (float) (sum / c);
+	}
+	if (impl->port[PORT_OUT_RANGE] != NULL) {
+		ebur128_loudness_range_multiple(st, 7, &value);
+		impl->port[PORT_OUT_RANGE][0] = (float)value;
+	}
+	if (impl->port[PORT_OUT_PEAK] != NULL) {
+		double max = 0.0;
+		for (i = 0; i < 7; i++) {
+			if (st[i] != NULL) {
+				ebur128_sample_peak(st[i], i, &value);
+				max = SPA_MAX(max, value);
+			}
+		}
+		impl->port[PORT_OUT_PEAK][0] = (float) max;
+	}
+	if (impl->port[PORT_OUT_TRUE_PEAK] != NULL) {
+		double max = 0.0;
+		for (i = 0; i < 7; i++) {
+			if (st[i] != NULL) {
+				ebur128_true_peak(st[i], i, &value);
+				max = SPA_MAX(max, value);
+			}
+		}
+		impl->port[PORT_OUT_TRUE_PEAK][0] = (float) max;
+	}
+}
+
+static void ebur128_connect_port(void * Instance, unsigned long Port,
+                        float * DataLocation)
+{
+	struct ebur128_impl *impl = Instance;
+	if (Port < PORT_MAX)
+		impl->port[Port] = DataLocation;
+}
+
+static void ebur128_cleanup(void * Instance)
+{
+	struct ebur128_impl *impl = Instance;
+	free(impl);
+}
+
+static void ebur128_activate(void * Instance)
+{
+	struct ebur128_impl *impl = Instance;
+	int mode = 0, i;
+	int modes[] = {
+		EBUR128_MODE_M,
+  		EBUR128_MODE_S,
+  		EBUR128_MODE_I,
+		0,
+  		EBUR128_MODE_LRA,
+  		EBUR128_MODE_SAMPLE_PEAK,
+  		EBUR128_MODE_TRUE_PEAK,
+	};
+	enum channel channels[] = {
+		EBUR128_LEFT,
+		EBUR128_RIGHT,
+		EBUR128_CENTER,
+		EBUR128_UNUSED,
+		EBUR128_LEFT_SURROUND,
+		EBUR128_RIGHT_SURROUND,
+		EBUR128_DUAL_MONO,
+	};
+
+	if (impl->use_histogram)
+		mode |= EBUR128_MODE_HISTOGRAM;
+
+	/* check modes */
+	for (i = 0; i < 7; i++) {
+		if (impl->port[PORT_NOTIFY_START + i] != NULL)
+			mode |= modes[i];
+	}
+
+	for (i = 0; i < 7; i++) {
+		impl->st[i] = ebur128_init(1, impl->rate, mode);
+		if (impl->st[i]) {
+			ebur128_set_channel(impl->st[i], i, channels[i]);
+			ebur128_set_max_history(impl->st[i], impl->max_history);
+			ebur128_set_max_window(impl->st[i], impl->max_window);
+		}
+	}
+}
+
+static void ebur128_deactivate(void * Instance)
+{
+	struct ebur128_impl *impl = Instance;
+	int i;
+
+	for (i = 0; i < 7; i++) {
+		if (impl->st[i] != NULL)
+			ebur128_destroy(&impl->st[i]);
+	}
+}
+
+static struct spa_fga_port ebur128_ports[] = {
+	{ .index = PORT_IN_FL,
+	  .name = "In FL",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = PORT_IN_FR,
+	  .name = "In FR",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = PORT_IN_FC,
+	  .name = "In FC",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = PORT_IN_UNUSED,
+	  .name = "In UNUSED",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = PORT_IN_SL,
+	  .name = "In SL",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = PORT_IN_SR,
+	  .name = "In SR",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = PORT_IN_DUAL_MONO,
+	  .name = "In DUAL MONO",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO,
+	},
+
+	{ .index = PORT_OUT_FL,
+	  .name = "Out FL",
+	  .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = PORT_OUT_FR,
+	  .name = "Out FR",
+	  .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = PORT_OUT_FC,
+	  .name = "Out FC",
+	  .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = PORT_OUT_UNUSED,
+	  .name = "Out UNUSED",
+	  .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = PORT_OUT_SL,
+	  .name = "Out SL",
+	  .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = PORT_OUT_SR,
+	  .name = "Out SR",
+	  .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO,
+	},
+	{ .index = PORT_OUT_DUAL_MONO,
+	  .name = "Out DUAL MONO",
+	  .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO,
+	},
+
+	{ .index = PORT_OUT_MOMENTARY,
+	  .name = "Momentary LUFS",
+	  .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_CONTROL,
+	},
+	{ .index = PORT_OUT_SHORTTERM,
+	  .name = "Shorttem LUFS",
+	  .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_CONTROL,
+	},
+	{ .index = PORT_OUT_GLOBAL,
+	  .name = "Global LUFS",
+	  .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_CONTROL,
+	},
+	{ .index = PORT_OUT_WINDOW,
+	  .name = "Window LUFS",
+	  .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_CONTROL,
+	},
+	{ .index = PORT_OUT_RANGE,
+	  .name = "Range LU",
+	  .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_CONTROL,
+	},
+	{ .index = PORT_OUT_PEAK,
+	  .name = "Peak",
+	  .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_CONTROL,
+	},
+	{ .index = PORT_OUT_TRUE_PEAK,
+	  .name = "True Peak",
+	  .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_CONTROL,
+	},
+};
+
+static const struct spa_fga_descriptor ebur128_desc = {
+	.name = "ebur128",
+	.flags = SPA_FGA_DESCRIPTOR_SUPPORTS_NULL_DATA,
+
+	.ports = ebur128_ports,
+	.n_ports = SPA_N_ELEMENTS(ebur128_ports),
+
+	.instantiate = ebur128_instantiate,
+	.connect_port = ebur128_connect_port,
+	.activate = ebur128_activate,
+	.deactivate = ebur128_deactivate,
+	.run = ebur128_run,
+	.cleanup = ebur128_cleanup,
+};
+
+static struct spa_fga_port lufs2gain_ports[] = {
+	{ .index = 0,
+	  .name = "LUFS",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL,
+	},
+	{ .index = 1,
+	  .name = "Gain",
+	  .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_CONTROL,
+	},
+	{ .index = 2,
+	  .name = "Target LUFS",
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL,
+	  .def = -23.0f, .min = -70.0f, .max = 0.0f
+	},
+};
+
+struct lufs2gain_impl {
+	struct plugin *plugin;
+
+	struct spa_fga_dsp *dsp;
+	struct spa_log *log;
+
+	unsigned long rate;
+	float *port[3];
+};
+
+static void * lufs2gain_instantiate(const struct spa_fga_plugin *plugin, const struct spa_fga_descriptor * Descriptor,
+		unsigned long SampleRate, int index, const char *config)
+{
+	struct plugin *pl = SPA_CONTAINER_OF(plugin, struct plugin, plugin);
+	struct lufs2gain_impl *impl;
+
+	impl = calloc(1, sizeof(*impl));
+	if (impl == NULL) {
+		errno = ENOMEM;
+		return NULL;
+	}
+
+	impl->plugin = pl;
+	impl->dsp = pl->dsp;
+	impl->log = pl->log;
+	impl->rate = SampleRate;
+
+	return impl;
+}
+
+static void lufs2gain_connect_port(void * Instance, unsigned long Port,
+                        float * DataLocation)
+{
+	struct lufs2gain_impl *impl = Instance;
+	if (Port < 3)
+		impl->port[Port] = DataLocation;
+}
+
+static void lufs2gain_run(void * Instance, unsigned long SampleCount)
+{
+	struct lufs2gain_impl *impl = Instance;
+	float *in = impl->port[0];
+	float *out = impl->port[1];
+	float *target = impl->port[2];
+	float gain;
+
+	if (in == NULL || out == NULL || target == NULL)
+		return;
+
+	if (isfinite(in[0])) {
+		float gaindB = target[0] - in[0];
+		gain = powf(10.0f, gaindB / 20.0f);
+	} else {
+		gain = 1.0f;
+	}
+	out[0] = gain;
+}
+
+static void lufs2gain_cleanup(void * Instance)
+{
+	struct lufs2gain_impl *impl = Instance;
+	free(impl);
+}
+
+static const struct spa_fga_descriptor lufs2gain_desc = {
+	.name = "lufs2gain",
+	.flags = SPA_FGA_DESCRIPTOR_SUPPORTS_NULL_DATA,
+
+	.ports = lufs2gain_ports,
+	.n_ports = SPA_N_ELEMENTS(lufs2gain_ports),
+
+	.instantiate = lufs2gain_instantiate,
+	.connect_port = lufs2gain_connect_port,
+	.run = lufs2gain_run,
+	.cleanup = lufs2gain_cleanup,
+};
+
+static const struct spa_fga_descriptor * ebur128_descriptor(unsigned long Index)
+{
+	switch(Index) {
+	case 0:
+		return &ebur128_desc;
+	case 1:
+		return &lufs2gain_desc;
+	}
+	return NULL;
+}
+
+
+static const struct spa_fga_descriptor *ebur128_plugin_make_desc(void *plugin, const char *name)
+{
+	unsigned long i;
+	for (i = 0; ;i++) {
+		const struct spa_fga_descriptor *d = ebur128_descriptor(i);
+		if (d == NULL)
+			break;
+		if (spa_streq(d->name, name))
+			return d;
+	}
+	return NULL;
+}
+
+static struct spa_fga_plugin_methods impl_plugin = {
+	SPA_VERSION_FGA_PLUGIN_METHODS,
+	.make_desc = ebur128_plugin_make_desc,
+};
+
+static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface)
+{
+	struct plugin *impl;
+
+	spa_return_val_if_fail(handle != NULL, -EINVAL);
+	spa_return_val_if_fail(interface != NULL, -EINVAL);
+
+	impl = (struct plugin *) handle;
+
+	if (spa_streq(type, SPA_TYPE_INTERFACE_FILTER_GRAPH_AudioPlugin))
+		*interface = &impl->plugin;
+	else
+		return -ENOENT;
+
+	return 0;
+}
+
+static int impl_clear(struct spa_handle *handle)
+{
+	return 0;
+}
+
+static size_t
+impl_get_size(const struct spa_handle_factory *factory,
+	      const struct spa_dict *params)
+{
+	return sizeof(struct plugin);
+}
+
+static int
+impl_init(const struct spa_handle_factory *factory,
+	  struct spa_handle *handle,
+	  const struct spa_dict *info,
+	  const struct spa_support *support,
+	  uint32_t n_support)
+{
+	struct plugin *impl;
+
+	handle->get_interface = impl_get_interface;
+	handle->clear = impl_clear;
+
+	impl = (struct plugin *) handle;
+
+	impl->plugin.iface = SPA_INTERFACE_INIT(
+			SPA_TYPE_INTERFACE_FILTER_GRAPH_AudioPlugin,
+			SPA_VERSION_FGA_PLUGIN,
+			&impl_plugin, impl);
+
+	impl->quantum_limit = 8192u;
+
+	impl->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
+	impl->dsp = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_FILTER_GRAPH_AudioDSP);
+
+	for (uint32_t i = 0; info && i < info->n_items; i++) {
+		const char *k = info->items[i].key;
+		const char *s = info->items[i].value;
+		if (spa_streq(k, "clock.quantum-limit"))
+			spa_atou32(s, &impl->quantum_limit, 0);
+		if (spa_streq(k, "filter.graph.audio.dsp"))
+			sscanf(s, "pointer:%p", &impl->dsp);
+	}
+	if (impl->dsp == NULL) {
+		spa_log_error(impl->log, "%p: could not find DSP functions", impl);
+		return -EINVAL;
+	}
+	return 0;
+}
+
+static const struct spa_interface_info impl_interfaces[] = {
+	{SPA_TYPE_INTERFACE_FILTER_GRAPH_AudioPlugin,},
+};
+
+static int
+impl_enum_interface_info(const struct spa_handle_factory *factory,
+			 const struct spa_interface_info **info,
+			 uint32_t *index)
+{
+	spa_return_val_if_fail(factory != NULL, -EINVAL);
+	spa_return_val_if_fail(info != NULL, -EINVAL);
+	spa_return_val_if_fail(index != NULL, -EINVAL);
+
+	switch (*index) {
+	case 0:
+		*info = &impl_interfaces[*index];
+		break;
+	default:
+		return 0;
+	}
+	(*index)++;
+	return 1;
+}
+
+static struct spa_handle_factory spa_fga_ebur128_plugin_factory = {
+	SPA_VERSION_HANDLE_FACTORY,
+	"filter.graph.plugin.ebur128",
+	NULL,
+	impl_get_size,
+	impl_init,
+	impl_enum_interface_info,
+};
+
+SPA_EXPORT
+int spa_handle_factory_enum(const struct spa_handle_factory **factory, uint32_t *index)
+{
+	spa_return_val_if_fail(factory != NULL, -EINVAL);
+	spa_return_val_if_fail(index != NULL, -EINVAL);
+
+	switch (*index) {
+	case 0:
+		*factory = &spa_fga_ebur128_plugin_factory;
+		break;
+	default:
+		return 0;
+	}
+	(*index)++;
+	return 1;
+}
diff --git a/spa/plugins/filter-graph/filter-graph.c b/spa/plugins/filter-graph/filter-graph.c
new file mode 100644
index 00000000..bc9ff154
--- /dev/null
+++ b/spa/plugins/filter-graph/filter-graph.c
@@ -0,0 +1,2170 @@
+/* PipeWire */
+/* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */
+/* SPDX-License-Identifier: MIT */
+
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <dlfcn.h>
+#include <unistd.h>
+#include <limits.h>
+#include <math.h>
+
+#include "config.h"
+
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/utils/json.h>
+#include <spa/support/cpu.h>
+#include <spa/support/plugin-loader.h>
+#include <spa/param/latency-utils.h>
+#include <spa/param/tag-utils.h>
+#include <spa/param/audio/raw.h>
+#include <spa/param/audio/raw-json.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/pod/dynamic.h>
+#include <spa/pod/builder.h>
+#include <spa/debug/types.h>
+#include <spa/debug/log.h>
+#include <spa/filter-graph/filter-graph.h>
+
+#include "audio-plugin.h"
+#include "audio-dsp-impl.h"
+
+#undef SPA_LOG_TOPIC_DEFAULT
+#define SPA_LOG_TOPIC_DEFAULT &log_topic
+SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.filter-graph");
+
+#define MAX_HNDL 64
+
+#define DEFAULT_RATE	48000
+
+#define spa_filter_graph_emit(hooks,method,version,...)					\
+		spa_hook_list_call_simple(hooks, struct spa_filter_graph_events,	\
+				method, version, ##__VA_ARGS__)
+
+#define spa_filter_graph_emit_info(hooks,...)		spa_filter_graph_emit(hooks,info, 0, __VA_ARGS__)
+#define spa_filter_graph_emit_apply_props(hooks,...)	spa_filter_graph_emit(hooks,apply_props, 0, __VA_ARGS__)
+#define spa_filter_graph_emit_props_changed(hooks,...)	spa_filter_graph_emit(hooks,props_changed, 0, __VA_ARGS__)
+
+struct plugin {
+	struct spa_list link;
+	struct impl *impl;
+
+	int ref;
+	char type[256];
+	char path[PATH_MAX];
+
+	struct spa_handle *hndl;
+	struct spa_fga_plugin *plugin;
+	struct spa_list descriptor_list;
+};
+
+struct descriptor {
+	struct spa_list link;
+	int ref;
+	struct plugin *plugin;
+	char label[256];
+
+	const struct spa_fga_descriptor *desc;
+
+	uint32_t n_input;
+	uint32_t n_output;
+	uint32_t n_control;
+	uint32_t n_notify;
+	unsigned long *input;
+	unsigned long *output;
+	unsigned long *control;
+	unsigned long *notify;
+	float *default_control;
+};
+
+struct port {
+	struct spa_list link;
+	struct node *node;
+
+	uint32_t idx;
+	unsigned long p;
+
+	struct spa_list link_list;
+	uint32_t n_links;
+	uint32_t external;
+
+	float control_data[MAX_HNDL];
+	float *audio_data[MAX_HNDL];
+	void *audio_mem[MAX_HNDL];
+};
+
+struct node {
+	struct spa_list link;
+	struct graph *graph;
+
+	struct descriptor *desc;
+
+	char name[256];
+	char *config;
+
+	struct port *input_port;
+	struct port *output_port;
+	struct port *control_port;
+	struct port *notify_port;
+
+	uint32_t n_hndl;
+	void *hndl[MAX_HNDL];
+
+	unsigned int n_deps;
+	unsigned int visited:1;
+	unsigned int disabled:1;
+	unsigned int control_changed:1;
+};
+
+struct link {
+	struct spa_list link;
+
+	struct spa_list input_link;
+	struct spa_list output_link;
+
+	struct port *output;
+	struct port *input;
+};
+
+struct graph_port {
+	const struct spa_fga_descriptor *desc;
+	void **hndl;
+	uint32_t port;
+	unsigned next:1;
+};
+
+struct graph_hndl {
+	const struct spa_fga_descriptor *desc;
+	void **hndl;
+};
+
+struct volume {
+	bool mute;
+	uint32_t n_volumes;
+	float volumes[SPA_AUDIO_MAX_CHANNELS];
+
+	uint32_t n_ports;
+	struct port *ports[SPA_AUDIO_MAX_CHANNELS];
+	float min[SPA_AUDIO_MAX_CHANNELS];
+	float max[SPA_AUDIO_MAX_CHANNELS];
+#define SCALE_LINEAR	0
+#define SCALE_CUBIC	1
+	int scale[SPA_AUDIO_MAX_CHANNELS];
+};
+
+struct graph {
+	struct impl *impl;
+
+	struct spa_list node_list;
+	struct spa_list link_list;
+
+	uint32_t n_input;
+	struct graph_port *input;
+
+	uint32_t n_output;
+	struct graph_port *output;
+
+	uint32_t n_hndl;
+	struct graph_hndl *hndl;
+
+	uint32_t n_control;
+	struct port **control_port;
+
+	struct volume volume[2];
+
+	unsigned activated:1;
+};
+
+struct impl {
+	struct spa_handle handle;
+	struct spa_filter_graph filter_graph;
+	struct spa_hook_list hooks;
+
+	struct spa_log *log;
+	struct spa_cpu *cpu;
+	struct spa_fga_dsp *dsp;
+	struct spa_plugin_loader *loader;
+
+	uint64_t info_all;
+	struct spa_filter_graph_info info;
+
+	struct graph graph;
+
+	uint32_t quantum_limit;
+	uint32_t max_align;
+	long unsigned rate;
+
+	struct spa_list plugin_list;
+
+	float *silence_data;
+	float *discard_data;
+};
+
+static void emit_filter_graph_info(struct impl *impl, bool full)
+{
+	uint64_t old = full ? impl->info.change_mask : 0;
+
+	if (full)
+		impl->info.change_mask = impl->info_all;
+	if (impl->info.change_mask || full) {
+		spa_filter_graph_emit_info(&impl->hooks, &impl->info);
+		impl->info.change_mask = old;
+	}
+}
+static int
+impl_add_listener(void *object,
+		struct spa_hook *listener,
+		const struct spa_filter_graph_events *events,
+		void *data)
+{
+	struct impl *impl = object;
+	struct spa_hook_list save;
+
+	spa_log_trace(impl->log, "%p: add listener %p", impl, listener);
+	spa_hook_list_isolate(&impl->hooks, &save, listener, events, data);
+
+	emit_filter_graph_info(impl, true);
+
+	spa_hook_list_join(&impl->hooks, &save);
+
+	return 0;
+}
+
+static int impl_process(void *object,
+		const void *in[], void *out[], uint32_t n_samples)
+{
+	struct impl *impl = object;
+	struct graph *graph = &impl->graph;
+	uint32_t i, j, n_hndl = graph->n_hndl;
+	struct graph_port *port;
+
+	for (i = 0, j = 0; i < impl->info.n_inputs; i++) {
+		while (j < graph->n_input) {
+			port = &graph->input[j++];
+			if (port->desc && in[i])
+				port->desc->connect_port(*port->hndl, port->port, (float*)in[i]);
+			if (!port->next)
+				break;
+		}
+	}
+	for (i = 0; i < impl->info.n_outputs; i++) {
+		if (out[i] == NULL)
+			continue;
+
+		port = i < graph->n_output ? &graph->output[i] : NULL;
+
+		if (port && port->desc)
+			port->desc->connect_port(*port->hndl, port->port, out[i]);
+		else
+			memset(out[i], 0, n_samples * sizeof(float));
+	}
+	for (i = 0; i < n_hndl; i++) {
+		struct graph_hndl *hndl = &graph->hndl[i];
+		hndl->desc->run(*hndl->hndl, n_samples);
+	}
+	return 0;
+}
+
+static float get_default(struct impl *impl, struct descriptor *desc, uint32_t p)
+{
+	struct spa_fga_port *port = &desc->desc->ports[p];
+	return port->def;
+}
+
+static struct node *find_node(struct graph *graph, const char *name)
+{
+	struct node *node;
+	spa_list_for_each(node, &graph->node_list, link) {
+		if (spa_streq(node->name, name))
+			return node;
+	}
+	return NULL;
+}
+
+/* find a port by name. Valid syntax is:
+ *  "<node_name>:<port_name>"
+ *  "<node_name>:<port_id>"
+ *  "<port_name>"
+ *  "<port_id>"
+ *  When no node_name is given, the port is assumed in the current node.  */
+static struct port *find_port(struct node *node, const char *name, int descriptor)
+{
+	char *col, *node_name, *port_name, *str;
+	struct port *ports;
+	const struct spa_fga_descriptor *d;
+	uint32_t i, n_ports, port_id = SPA_ID_INVALID;
+
+	str = strdupa(name);
+	col = strchr(str, ':');
+	if (col != NULL) {
+		struct node *find;
+		node_name = str;
+		port_name = col + 1;
+		*col = '\0';
+		find = find_node(node->graph, node_name);
+		if (find == NULL) {
+			/* it's possible that the : is part of the port name,
+			 * try again without splitting things up. */
+			*col = ':';
+			col = NULL;
+		} else {
+			node = find;
+		}
+	}
+	if (col == NULL) {
+		node_name = node->name;
+		port_name = str;
+	}
+	if (node == NULL)
+		return NULL;
+
+	if (!spa_atou32(port_name, &port_id, 0))
+		port_id = SPA_ID_INVALID;
+
+	if (SPA_FGA_IS_PORT_INPUT(descriptor)) {
+		if (SPA_FGA_IS_PORT_CONTROL(descriptor)) {
+			ports = node->control_port;
+			n_ports = node->desc->n_control;
+		} else {
+			ports = node->input_port;
+			n_ports = node->desc->n_input;
+		}
+	} else if (SPA_FGA_IS_PORT_OUTPUT(descriptor)) {
+		if (SPA_FGA_IS_PORT_CONTROL(descriptor)) {
+			ports = node->notify_port;
+			n_ports = node->desc->n_notify;
+		} else {
+			ports = node->output_port;
+			n_ports = node->desc->n_output;
+		}
+	} else
+		return NULL;
+
+	d = node->desc->desc;
+	for (i = 0; i < n_ports; i++) {
+		struct port *port = &ports[i];
+		if (i == port_id ||
+		    spa_streq(d->ports[port->p].name, port_name))
+			return port;
+	}
+	return NULL;
+}
+
+static int impl_enum_prop_info(void *object, uint32_t idx, struct spa_pod_builder *b,
+		struct spa_pod **param)
+{
+	struct impl *impl = object;
+	struct graph *graph = &impl->graph;
+	struct spa_pod *pod;
+	struct spa_pod_frame f[2];
+	struct port *port;
+	struct node *node;
+	struct descriptor *desc;
+	const struct spa_fga_descriptor *d;
+	struct spa_fga_port *p;
+	float def, min, max;
+	char name[512];
+	uint32_t rate = impl->rate ? impl->rate : DEFAULT_RATE;
+
+	if (idx >= graph->n_control)
+		return 0;
+
+	port = graph->control_port[idx];
+	node = port->node;
+	desc = node->desc;
+	d = desc->desc;
+	p = &d->ports[port->p];
+
+	if (p->hint & SPA_FGA_HINT_SAMPLE_RATE) {
+		def = p->def * rate;
+		min = p->min * rate;
+		max = p->max * rate;
+	} else {
+		def = p->def;
+		min = p->min;
+		max = p->max;
+	}
+
+	if (node->name[0] != '\0')
+		snprintf(name, sizeof(name), "%s:%s", node->name, p->name);
+	else
+		snprintf(name, sizeof(name), "%s", p->name);
+
+	spa_pod_builder_push_object(b, &f[0],
+			SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo);
+	spa_pod_builder_add (b,
+			SPA_PROP_INFO_name, SPA_POD_String(name),
+			0);
+	spa_pod_builder_prop(b, SPA_PROP_INFO_type, 0);
+	if (p->hint & SPA_FGA_HINT_BOOLEAN) {
+		if (min == max) {
+			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.0f ? false : true);
+			spa_pod_builder_bool(b, false);
+			spa_pod_builder_bool(b, true);
+			spa_pod_builder_pop(b, &f[1]);
+		}
+	} else if (p->hint & SPA_FGA_HINT_INTEGER) {
+		if (min == max) {
+			spa_pod_builder_int(b, (int32_t)def);
+		} else {
+			spa_pod_builder_push_choice(b, &f[1], SPA_CHOICE_Range, 0);
+			spa_pod_builder_int(b, (int32_t)def);
+			spa_pod_builder_int(b, (int32_t)min);
+			spa_pod_builder_int(b, (int32_t)max);
+			spa_pod_builder_pop(b, &f[1]);
+		}
+	} else {
+		if (min == max) {
+			spa_pod_builder_float(b, def);
+		} else {
+			spa_pod_builder_push_choice(b, &f[1], SPA_CHOICE_Range, 0);
+			spa_pod_builder_float(b, def);
+			spa_pod_builder_float(b, min);
+			spa_pod_builder_float(b, max);
+			spa_pod_builder_pop(b, &f[1]);
+		}
+	}
+	spa_pod_builder_prop(b, SPA_PROP_INFO_params, 0);
+	spa_pod_builder_bool(b, true);
+	pod = spa_pod_builder_pop(b, &f[0]);
+	if (pod == NULL)
+		return -ENOSPC;
+	if (param)
+		*param = pod;
+
+	return 1;
+}
+
+static int impl_get_props(void *object, struct spa_pod_builder *b, struct spa_pod **props)
+{
+	struct impl *impl = object;
+	struct graph *graph = &impl->graph;
+	struct spa_pod_frame f[2];
+	uint32_t i;
+	char name[512];
+	struct spa_pod *res;
+
+	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]);
+
+	for (i = 0; i < graph->n_control; i++) {
+		struct port *port = graph->control_port[i];
+		struct node *node = port->node;
+		struct descriptor *desc = node->desc;
+		const struct spa_fga_descriptor *d = desc->desc;
+		struct spa_fga_port *p = &d->ports[port->p];
+
+		if (node->name[0] != '\0')
+			snprintf(name, sizeof(name), "%s:%s", node->name, p->name);
+		else
+			snprintf(name, sizeof(name), "%s", p->name);
+
+		spa_pod_builder_string(b, name);
+		if (p->hint & SPA_FGA_HINT_BOOLEAN) {
+			spa_pod_builder_bool(b, port->control_data[0] <= 0.0f ? false : true);
+		} else if (p->hint & SPA_FGA_HINT_INTEGER) {
+			spa_pod_builder_int(b, (int32_t)port->control_data[0]);
+		} else {
+			spa_pod_builder_float(b, port->control_data[0]);
+		}
+	}
+	spa_pod_builder_pop(b, &f[1]);
+	res = spa_pod_builder_pop(b, &f[0]);
+	if (res == NULL)
+		return -ENOSPC;
+	if (props)
+		*props = res;
+	return 1;
+}
+
+static int port_set_control_value(struct port *port, float *value, uint32_t id)
+{
+	struct node *node = port->node;
+	struct impl *impl = node->graph->impl;
+
+	struct descriptor *desc = node->desc;
+	float old;
+	bool changed;
+
+	old = port->control_data[id];
+	port->control_data[id] = value ? *value : desc->default_control[port->idx];
+	spa_log_info(impl->log, "control %d %d ('%s') from %f to %f", port->idx, id,
+			desc->desc->ports[port->p].name, old, port->control_data[id]);
+	changed = old != port->control_data[id];
+	node->control_changed |= changed;
+	return changed ? 1 : 0;
+}
+
+static int set_control_value(struct node *node, const char *name, float *value)
+{
+	struct port *port;
+	int count = 0;
+	uint32_t i, n_hndl;
+
+	port = find_port(node, name, SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL);
+	if (port == NULL)
+		return -ENOENT;
+
+	/* if we don't have any instances yet, set the first control value, we will
+	 * copy to other instances later */
+	n_hndl = SPA_MAX(1u, port->node->n_hndl);
+	for (i = 0; i < n_hndl; i++)
+		count += port_set_control_value(port, value, i);
+
+	return count;
+}
+
+static int parse_params(struct graph *graph, const struct spa_pod *pod)
+{
+	struct spa_pod_parser prs;
+	struct spa_pod_frame f;
+	int res, changed = 0;
+	struct node *def_node;
+
+	def_node = spa_list_first(&graph->node_list, struct node, link);
+
+	spa_pod_parser_pod(&prs, pod);
+	if (spa_pod_parser_push_struct(&prs, &f) < 0)
+		return 0;
+
+	while (true) {
+		const char *name;
+		float value, *val = NULL;
+		double dbl_val;
+		bool bool_val;
+		int32_t int_val;
+
+		if (spa_pod_parser_get_string(&prs, &name) < 0)
+			break;
+		if (spa_pod_parser_get_float(&prs, &value) >= 0) {
+			val = &value;
+		} else if (spa_pod_parser_get_double(&prs, &dbl_val) >= 0) {
+			value = (float)dbl_val;
+			val = &value;
+		} else if (spa_pod_parser_get_int(&prs, &int_val) >= 0) {
+			value = int_val;
+			val = &value;
+		} else if (spa_pod_parser_get_bool(&prs, &bool_val) >= 0) {
+			value = bool_val ? 1.0f : 0.0f;
+			val = &value;
+		} else {
+			struct spa_pod *pod;
+			spa_pod_parser_get_pod(&prs, &pod);
+		}
+		if ((res = set_control_value(def_node, name, val)) > 0)
+			changed += res;
+	}
+	return changed;
+}
+
+static int impl_reset(void *object)
+{
+	struct impl *impl = object;
+	struct graph *graph = &impl->graph;
+	uint32_t i;
+	for (i = 0; i < graph->n_hndl; i++) {
+		struct graph_hndl *hndl = &graph->hndl[i];
+		const struct spa_fga_descriptor *d = hndl->desc;
+		if (hndl->hndl == NULL || *hndl->hndl == NULL)
+			continue;
+		if (d->deactivate)
+			d->deactivate(*hndl->hndl);
+		if (d->activate)
+			d->activate(*hndl->hndl);
+	}
+	return 0;
+}
+
+static void node_control_changed(struct node *node)
+{
+	const struct spa_fga_descriptor *d = node->desc->desc;
+	uint32_t i;
+
+	if (!node->control_changed)
+		return;
+
+	for (i = 0; i < node->n_hndl; i++) {
+		if (node->hndl[i] == NULL)
+			continue;
+		if (d->control_changed)
+			d->control_changed(node->hndl[i]);
+	}
+	node->control_changed = false;
+}
+
+static int sync_volume(struct graph *graph, struct volume *vol)
+{
+	uint32_t i;
+	int res = 0;
+
+	if (vol->n_ports == 0)
+		return 0;
+	for (i = 0; i < vol->n_volumes; i++) {
+		uint32_t n_port = i % vol->n_ports, n_hndl;
+		struct port *p = vol->ports[n_port];
+		float v = vol->mute ? 0.0f : vol->volumes[i];
+		switch (vol->scale[n_port]) {
+		case SCALE_CUBIC:
+			v = cbrtf(v);
+			break;
+		}
+		v = v * (vol->max[n_port] - vol->min[n_port]) + vol->min[n_port];
+
+		n_hndl = SPA_MAX(1u, p->node->n_hndl);
+		res += port_set_control_value(p, &v, i % n_hndl);
+	}
+	return res;
+}
+
+static int impl_set_props(void *object, enum spa_direction direction, const struct spa_pod *props)
+{
+	struct impl *impl = object;
+	struct spa_pod_object *obj = (struct spa_pod_object *) props;
+	struct spa_pod_frame f[1];
+	const struct spa_pod_prop *prop;
+	struct graph *graph = &impl->graph;
+	int changed = 0;
+	char buf[1024];
+	struct spa_pod_dynamic_builder b;
+	struct volume *vol = &graph->volume[direction];
+	bool do_volume = false;
+
+	spa_pod_dynamic_builder_init(&b, buf, sizeof(buf), 1024);
+	spa_pod_builder_push_object(&b.b, &f[0], SPA_TYPE_OBJECT_Props, SPA_PARAM_Props);
+
+	SPA_POD_OBJECT_FOREACH(obj, prop) {
+		switch (prop->key) {
+		case SPA_PROP_params:
+			changed += parse_params(graph, &prop->value);
+			spa_pod_builder_raw_padded(&b.b, prop, SPA_POD_PROP_SIZE(prop));
+			break;
+		case SPA_PROP_mute:
+		{
+			bool mute;
+			if (spa_pod_get_bool(&prop->value, &mute) == 0) {
+				if (vol->mute != mute) {
+					vol->mute = mute;
+					do_volume = true;
+				}
+			}
+			spa_pod_builder_raw_padded(&b.b, prop, SPA_POD_PROP_SIZE(prop));
+			break;
+		}
+		case SPA_PROP_channelVolumes:
+		{
+			uint32_t i, n_vols;
+			float vols[SPA_AUDIO_MAX_CHANNELS];
+
+			if ((n_vols = spa_pod_copy_array(&prop->value, SPA_TYPE_Float, vols,
+					SPA_AUDIO_MAX_CHANNELS)) > 0) {
+				if (vol->n_volumes != n_vols)
+					do_volume = true;
+				vol->n_volumes = n_vols;
+				for (i = 0; i < n_vols; i++) {
+					float v = vols[i];
+					if (v != vol->volumes[i]) {
+						vol->volumes[i] = v;
+						do_volume = true;
+					}
+				}
+			}
+			spa_pod_builder_raw_padded(&b.b, prop, SPA_POD_PROP_SIZE(prop));
+			break;
+		}
+		case SPA_PROP_softVolumes:
+		case SPA_PROP_softMute:
+			break;
+		default:
+			spa_pod_builder_raw_padded(&b.b, prop, SPA_POD_PROP_SIZE(prop));
+			break;
+		}
+	}
+	if (do_volume && vol->n_ports != 0) {
+		float soft_vols[SPA_AUDIO_MAX_CHANNELS];
+		uint32_t i;
+
+		for (i = 0; i < vol->n_volumes; i++)
+			soft_vols[i] = (vol->mute || vol->volumes[i] == 0.0f) ? 0.0f : 1.0f;
+
+		spa_pod_builder_prop(&b.b, SPA_PROP_softMute, 0);
+		spa_pod_builder_bool(&b.b, vol->mute);
+		spa_pod_builder_prop(&b.b, SPA_PROP_softVolumes, 0);
+		spa_pod_builder_array(&b.b, sizeof(float), SPA_TYPE_Float,
+				vol->n_volumes, soft_vols);
+		props = spa_pod_builder_pop(&b.b, &f[0]);
+
+		sync_volume(graph, vol);
+
+	} else {
+		props = spa_pod_builder_pop(&b.b, &f[0]);
+	}
+	spa_filter_graph_emit_apply_props(&impl->hooks, direction, props);
+
+	spa_pod_dynamic_builder_clean(&b);
+
+	if (changed > 0) {
+		struct node *node;
+
+		spa_list_for_each(node, &graph->node_list, link)
+			node_control_changed(node);
+
+		spa_filter_graph_emit_props_changed(&impl->hooks, SPA_DIRECTION_INPUT);
+	}
+	return 0;
+}
+
+static uint32_t count_array(struct spa_json *json)
+{
+	struct spa_json it = *json;
+	char v[256];
+	uint32_t count = 0;
+	while (spa_json_get_string(&it, v, sizeof(v)) > 0)
+		count++;
+	return count;
+}
+
+static void plugin_unref(struct plugin *hndl)
+{
+	struct impl *impl = hndl->impl;
+
+	if (--hndl->ref > 0)
+		return;
+
+	spa_list_remove(&hndl->link);
+	if (hndl->hndl)
+		spa_plugin_loader_unload(impl->loader, hndl->hndl);
+	free(hndl);
+}
+
+static inline const char *split_walk(const char *str, const char *delimiter, size_t * len, const char **state)
+{
+	const char *s = *state ? *state : str;
+
+	s += strspn(s, delimiter);
+	if (*s == '\0')
+		return NULL;
+
+	*len = strcspn(s, delimiter);
+	*state = s + *len;
+
+	return s;
+}
+
+static struct plugin *plugin_load(struct impl *impl, const char *type, const char *path)
+{
+	struct spa_handle *hndl = NULL;
+	struct plugin *plugin;
+	char module[PATH_MAX];
+	char factory_name[256], dsp_ptr[256];
+	void *iface;
+	int res;
+
+	spa_list_for_each(plugin, &impl->plugin_list, link) {
+		if (spa_streq(plugin->type, type) &&
+		    spa_streq(plugin->path, path)) {
+			plugin->ref++;
+			return plugin;
+		}
+	}
+
+	spa_scnprintf(module, sizeof(module),
+			"filter-graph/libspa-filter-graph-plugin-%s", type);
+	spa_scnprintf(factory_name, sizeof(factory_name),
+			"filter.graph.plugin.%s", type);
+	spa_scnprintf(dsp_ptr, sizeof(dsp_ptr),
+			"pointer:%p", impl->dsp);
+
+	hndl = spa_plugin_loader_load(impl->loader, factory_name,
+			&SPA_DICT_ITEMS(
+				SPA_DICT_ITEM(SPA_KEY_LIBRARY_NAME, module),
+				SPA_DICT_ITEM("filter.graph.path", path),
+				SPA_DICT_ITEM("filter.graph.audio.dsp", dsp_ptr)));
+
+	if (hndl == NULL) {
+		res = -errno;
+		spa_log_error(impl->log, "can't load plugin type '%s': %m", type);
+		goto exit;
+	}
+	if ((res = spa_handle_get_interface(hndl, SPA_TYPE_INTERFACE_FILTER_GRAPH_AudioPlugin, &iface)) < 0) {
+		spa_log_error(impl->log, "can't find iface '%s': %s",
+				SPA_TYPE_INTERFACE_FILTER_GRAPH_AudioPlugin, spa_strerror(res));
+		goto exit;
+	}
+	plugin = calloc(1, sizeof(*plugin));
+	if (!plugin) {
+		res = -errno;
+		goto exit;
+	}
+
+	plugin->ref = 1;
+	snprintf(plugin->type, sizeof(plugin->type), "%s", type);
+	snprintf(plugin->path, sizeof(plugin->path), "%s", path);
+
+	spa_log_info(impl->log, "successfully opened '%s':'%s'", type, path);
+
+	plugin->impl = impl;
+	plugin->hndl = hndl;
+	plugin->plugin = iface;
+
+	spa_list_init(&plugin->descriptor_list);
+	spa_list_append(&impl->plugin_list, &plugin->link);
+
+	return plugin;
+exit:
+	if (hndl)
+		spa_plugin_loader_unload(impl->loader, hndl);
+	errno = -res;
+	return NULL;
+}
+
+static void descriptor_unref(struct descriptor *desc)
+{
+	if (--desc->ref > 0)
+		return;
+
+	spa_list_remove(&desc->link);
+	if (desc->desc)
+		spa_fga_descriptor_free(desc->desc);
+	plugin_unref(desc->plugin);
+	free(desc->input);
+	free(desc->output);
+	free(desc->control);
+	free(desc->default_control);
+	free(desc->notify);
+	free(desc);
+}
+
+static struct descriptor *descriptor_load(struct impl *impl, const char *type,
+		const char *plugin, const char *label)
+{
+	struct plugin *pl;
+	struct descriptor *desc;
+	const struct spa_fga_descriptor *d;
+	uint32_t i, n_input, n_output, n_control, n_notify;
+	unsigned long p;
+	int res;
+
+	if ((pl = plugin_load(impl, type, plugin)) == NULL)
+		return NULL;
+
+	spa_list_for_each(desc, &pl->descriptor_list, link) {
+		if (spa_streq(desc->label, label)) {
+			desc->ref++;
+
+			/*
+			 * since ladspa_handle_load() increments the reference count of the handle,
+			 * if the descriptor is found, then the handle's reference count
+			 * has already been incremented to account for the descriptor,
+			 * so we need to unref handle here since we're merely reusing
+			 * thedescriptor, not creating a new one
+			 */
+			plugin_unref(pl);
+			return desc;
+		}
+	}
+
+	desc = calloc(1, sizeof(*desc));
+	desc->ref = 1;
+	desc->plugin = pl;
+	spa_list_init(&desc->link);
+
+	if ((d = spa_fga_plugin_make_desc(pl->plugin, label)) == NULL) {
+		spa_log_error(impl->log, "cannot find label %s", label);
+		res = -ENOENT;
+		goto exit;
+	}
+	desc->desc = d;
+	snprintf(desc->label, sizeof(desc->label), "%s", label);
+
+	n_input = n_output = n_control = n_notify = 0;
+	for (p = 0; p < d->n_ports; p++) {
+		struct spa_fga_port *fp = &d->ports[p];
+		if (SPA_FGA_IS_PORT_AUDIO(fp->flags)) {
+			if (SPA_FGA_IS_PORT_INPUT(fp->flags))
+				n_input++;
+			else if (SPA_FGA_IS_PORT_OUTPUT(fp->flags))
+				n_output++;
+		} else if (SPA_FGA_IS_PORT_CONTROL(fp->flags)) {
+			if (SPA_FGA_IS_PORT_INPUT(fp->flags))
+				n_control++;
+			else if (SPA_FGA_IS_PORT_OUTPUT(fp->flags))
+				n_notify++;
+		}
+	}
+	desc->input = calloc(n_input, sizeof(unsigned long));
+	desc->output = calloc(n_output, sizeof(unsigned long));
+	desc->control = calloc(n_control, sizeof(unsigned long));
+	desc->default_control = calloc(n_control, sizeof(float));
+	desc->notify = calloc(n_notify, sizeof(unsigned long));
+
+	for (p = 0; p < d->n_ports; p++) {
+		struct spa_fga_port *fp = &d->ports[p];
+
+		if (SPA_FGA_IS_PORT_AUDIO(fp->flags)) {
+			if (SPA_FGA_IS_PORT_INPUT(fp->flags)) {
+				spa_log_info(impl->log, "using port %lu ('%s') as input %d", p,
+						fp->name, desc->n_input);
+				desc->input[desc->n_input++] = p;
+			}
+			else if (SPA_FGA_IS_PORT_OUTPUT(fp->flags)) {
+				spa_log_info(impl->log, "using port %lu ('%s') as output %d", p,
+						fp->name, desc->n_output);
+				desc->output[desc->n_output++] = p;
+			}
+		} else if (SPA_FGA_IS_PORT_CONTROL(fp->flags)) {
+			if (SPA_FGA_IS_PORT_INPUT(fp->flags)) {
+				spa_log_info(impl->log, "using port %lu ('%s') as control %d", p,
+						fp->name, desc->n_control);
+				desc->control[desc->n_control++] = p;
+			}
+			else if (SPA_FGA_IS_PORT_OUTPUT(fp->flags)) {
+				spa_log_info(impl->log, "using port %lu ('%s') as notify %d", p,
+						fp->name, desc->n_notify);
+				desc->notify[desc->n_notify++] = p;
+			}
+		}
+	}
+	if (desc->n_input == 0 && desc->n_output == 0 && desc->n_control == 0 && desc->n_notify == 0) {
+		spa_log_error(impl->log, "plugin has no input and no output ports");
+		res = -ENOTSUP;
+		goto exit;
+	}
+	for (i = 0; i < desc->n_control; i++) {
+		p = desc->control[i];
+		desc->default_control[i] = get_default(impl, desc, p);
+		spa_log_info(impl->log, "control %d ('%s') default to %f", i,
+				d->ports[p].name, desc->default_control[i]);
+	}
+	spa_list_append(&pl->descriptor_list, &desc->link);
+
+	return desc;
+
+exit:
+	descriptor_unref(desc);
+	errno = -res;
+	return NULL;
+}
+
+/**
+ * {
+ *   ...
+ * }
+ */
+static int parse_config(struct node *node, struct spa_json *config)
+{
+	const char *val, *s = config->cur;
+	struct impl *impl = node->graph->impl;
+	int res = 0, len;
+	struct spa_error_location loc;
+
+	if ((len = spa_json_next(config, &val)) <= 0) {
+		res = -EINVAL;
+		goto done;
+	}
+	if (spa_json_is_null(val, len))
+		goto done;
+
+	if (spa_json_is_container(val, len)) {
+		len = spa_json_container_len(config, val, len);
+		if (len == 0) {
+			res = -EINVAL;
+			goto done;
+		}
+	}
+	if ((node->config = malloc(len+1)) == NULL) {
+		res = -errno;
+		goto done;
+	}
+
+	spa_json_parse_stringn(val, len, node->config, len+1);
+done:
+	if (spa_json_get_error(config, s, &loc))
+		spa_debug_log_error_location(impl->log, SPA_LOG_LEVEL_WARN,
+				&loc, "error: %s", loc.reason);
+	return res;
+}
+
+/**
+ * {
+ *   "Reverb tail" = 2.0
+ *   ...
+ * }
+ */
+static int parse_control(struct node *node, struct spa_json *control)
+{
+	struct impl *impl = node->graph->impl;
+	char key[256];
+	const char *val;
+	int len;
+
+	while ((len = spa_json_object_next(control, key, sizeof(key), &val)) > 0) {
+		float fl;
+		int res;
+
+		if (spa_json_parse_float(val, len, &fl) <= 0) {
+			spa_log_warn(impl->log, "control '%s' expects a number, ignoring", key);
+		}
+		else if ((res = set_control_value(node, key, &fl)) < 0) {
+			spa_log_warn(impl->log, "control '%s' can not be set: %s", key, spa_strerror(res));
+		}
+	}
+	return 0;
+}
+
+/**
+ * output = [name:][portname]
+ * input = [name:][portname]
+ * ...
+ */
+static int parse_link(struct graph *graph, struct spa_json *json)
+{
+	struct impl *impl = graph->impl;
+	char key[256];
+	char output[256] = "";
+	char input[256] = "";
+	const char *val;
+	struct node *def_in_node, *def_out_node;
+	struct port *in_port, *out_port;
+	struct link *link;
+	int len;
+
+	if (spa_list_is_empty(&graph->node_list)) {
+		spa_log_error(impl->log, "can't make links in graph without nodes");
+		return -EINVAL;
+	}
+
+	while ((len = spa_json_object_next(json, key, sizeof(key), &val)) > 0) {
+		if (spa_streq(key, "output")) {
+			if (spa_json_parse_stringn(val, len, output, sizeof(output)) <= 0) {
+				spa_log_error(impl->log, "output expects a string");
+				return -EINVAL;
+			}
+		}
+		else if (spa_streq(key, "input")) {
+			if (spa_json_parse_stringn(val, len, input, sizeof(input)) <= 0) {
+				spa_log_error(impl->log, "input expects a string");
+				return -EINVAL;
+			}
+		}
+		else {
+			spa_log_error(impl->log, "unexpected link key '%s'", key);
+		}
+	}
+	def_out_node = spa_list_first(&graph->node_list, struct node, link);
+	def_in_node = spa_list_last(&graph->node_list, struct node, link);
+
+	out_port = find_port(def_out_node, output, SPA_FGA_PORT_OUTPUT);
+	in_port = find_port(def_in_node, input, SPA_FGA_PORT_INPUT);
+
+	if (out_port == NULL && out_port == NULL) {
+		/* try control ports */
+		out_port = find_port(def_out_node, output, SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_CONTROL);
+		in_port = find_port(def_in_node, input, SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL);
+	}
+	if (in_port == NULL || out_port == NULL) {
+		if (out_port == NULL)
+			spa_log_error(impl->log, "unknown output port %s", output);
+		if (in_port == NULL)
+			spa_log_error(impl->log, "unknown input port %s", input);
+		return -ENOENT;
+	}
+
+	if (in_port->n_links > 0) {
+		spa_log_info(impl->log, "Can't have more than 1 link to %s, use a mixer", input);
+		return -ENOTSUP;
+	}
+
+	if ((link = calloc(1, sizeof(*link))) == NULL)
+		return -errno;
+
+	link->output = out_port;
+	link->input = in_port;
+
+	spa_log_info(impl->log, "linking %s:%s -> %s:%s",
+			out_port->node->name,
+			out_port->node->desc->desc->ports[out_port->p].name,
+			in_port->node->name,
+			in_port->node->desc->desc->ports[in_port->p].name);
+
+	spa_list_append(&out_port->link_list, &link->output_link);
+	out_port->n_links++;
+	spa_list_append(&in_port->link_list, &link->input_link);
+	in_port->n_links++;
+
+	in_port->node->n_deps++;
+
+	spa_list_append(&graph->link_list, &link->link);
+
+	return 0;
+}
+
+static void link_free(struct link *link)
+{
+	spa_list_remove(&link->input_link);
+	link->input->n_links--;
+	link->input->node->n_deps--;
+	spa_list_remove(&link->output_link);
+	link->output->n_links--;
+	spa_list_remove(&link->link);
+	free(link);
+}
+
+/**
+ * {
+ *   control = [name:][portname]
+ *   min = <float, default 0.0>
+ *   max = <float, default 1.0>
+ *   scale = <string, default "linear", options "linear","cubic">
+ * }
+ */
+static int parse_volume(struct graph *graph, struct spa_json *json, enum spa_direction direction)
+{
+	struct impl *impl = graph->impl;
+	char key[256];
+	char control[256] = "";
+	char scale[64] = "linear";
+	float min = 0.0f, max = 1.0f;
+	const char *val;
+	struct node *def_control;
+	struct port *port;
+	struct volume *vol = &graph->volume[direction];
+	int len;
+
+	if (spa_list_is_empty(&graph->node_list)) {
+		spa_log_error(impl->log, "can't set volume in graph without nodes");
+		return -EINVAL;
+	}
+	while ((len = spa_json_object_next(json, key, sizeof(key), &val)) > 0) {
+		if (spa_streq(key, "control")) {
+			if (spa_json_parse_stringn(val, len, control, sizeof(control)) <= 0) {
+				spa_log_error(impl->log, "control expects a string");
+				return -EINVAL;
+			}
+		}
+		else if (spa_streq(key, "min")) {
+			if (spa_json_parse_float(val, len, &min) <= 0) {
+				spa_log_error(impl->log, "min expects a float");
+				return -EINVAL;
+			}
+		}
+		else if (spa_streq(key, "max")) {
+			if (spa_json_parse_float(val, len, &max) <= 0) {
+				spa_log_error(impl->log, "max expects a float");
+				return -EINVAL;
+			}
+		}
+		else if (spa_streq(key, "scale")) {
+			if (spa_json_parse_stringn(val, len, scale, sizeof(scale)) <= 0) {
+				spa_log_error(impl->log, "scale expects a string");
+				return -EINVAL;
+			}
+		}
+		else {
+			spa_log_error(impl->log, "unexpected volume key '%s'", key);
+		}
+	}
+	if (direction == SPA_DIRECTION_INPUT)
+		def_control = spa_list_first(&graph->node_list, struct node, link);
+	else
+		def_control = spa_list_last(&graph->node_list, struct node, link);
+
+	port = find_port(def_control, control, SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL);
+	if (port == NULL) {
+		spa_log_error(impl->log, "unknown control port %s", control);
+		return -ENOENT;
+	}
+	if (vol->n_ports >= SPA_AUDIO_MAX_CHANNELS) {
+		spa_log_error(impl->log, "too many volume controls");
+		return -ENOSPC;
+	}
+	if (spa_streq(scale, "linear")) {
+		vol->scale[vol->n_ports] = SCALE_LINEAR;
+	} else if (spa_streq(scale, "cubic")) {
+		vol->scale[vol->n_ports] = SCALE_CUBIC;
+	} else {
+		spa_log_error(impl->log, "Invalid scale value '%s', use one of linear or cubic", scale);
+		return -EINVAL;
+	}
+	spa_log_info(impl->log, "volume %d: \"%s:%s\" min:%f max:%f scale:%s", vol->n_ports, port->node->name,
+			port->node->desc->desc->ports[port->p].name, min, max, scale);
+
+	vol->ports[vol->n_ports] = port;
+	vol->min[vol->n_ports] = min;
+	vol->max[vol->n_ports] = max;
+	vol->n_ports++;
+
+	return 0;
+}
+
+/**
+ * type = ladspa
+ * name = rev
+ * plugin = g2reverb
+ * label = G2reverb
+ * config = {
+ *     ...
+ * }
+ * control = {
+ *     ...
+ * }
+ */
+static int load_node(struct graph *graph, struct spa_json *json)
+{
+	struct impl *impl = graph->impl;
+	struct spa_json control, config;
+	struct descriptor *desc;
+	struct node *node;
+	const char *val;
+	char key[256];
+	char type[256] = "";
+	char name[256] = "";
+	char plugin[256] = "";
+	char label[256] = "";
+	bool have_control = false;
+	bool have_config = false;
+	uint32_t i;
+	int res, len;
+
+	while ((len = spa_json_object_next(json, key, sizeof(key), &val)) > 0) {
+		if (spa_streq("type", key)) {
+			if (spa_json_parse_stringn(val, len, type, sizeof(type)) <= 0) {
+				spa_log_error(impl->log, "type expects a string");
+				return -EINVAL;
+			}
+		} else if (spa_streq("name", key)) {
+			if (spa_json_parse_stringn(val, len, name, sizeof(name)) <= 0) {
+				spa_log_error(impl->log, "name expects a string");
+				return -EINVAL;
+			}
+		} else if (spa_streq("plugin", key)) {
+			if (spa_json_parse_stringn(val, len, plugin, sizeof(plugin)) <= 0) {
+				spa_log_error(impl->log, "plugin expects a string");
+				return -EINVAL;
+			}
+		} else if (spa_streq("label", key)) {
+			if (spa_json_parse_stringn(val, len, label, sizeof(label)) <= 0) {
+				spa_log_error(impl->log, "label expects a string");
+				return -EINVAL;
+			}
+		} else if (spa_streq("control", key)) {
+			if (!spa_json_is_object(val, len)) {
+				spa_log_error(impl->log, "control expects an object");
+				return -EINVAL;
+			}
+			spa_json_enter(json, &control);
+			have_control = true;
+		} else if (spa_streq("config", key)) {
+			config = SPA_JSON_START(json, val);
+			have_config = true;
+		} else {
+			spa_log_warn(impl->log, "unexpected node key '%s'", key);
+		}
+	}
+	if (spa_streq(type, "builtin"))
+		snprintf(plugin, sizeof(plugin), "%s", "builtin");
+	else if (spa_streq(type, "")) {
+		spa_log_error(impl->log, "missing plugin type");
+		return -EINVAL;
+	}
+
+	spa_log_info(impl->log, "loading type:%s plugin:%s label:%s", type, plugin, label);
+
+	if ((desc = descriptor_load(graph->impl, type, plugin, label)) == NULL)
+		return -errno;
+
+	node = calloc(1, sizeof(*node));
+	if (node == NULL)
+		return -errno;
+
+	node->graph = graph;
+	node->desc = desc;
+	snprintf(node->name, sizeof(node->name), "%s", name);
+
+	node->input_port = calloc(desc->n_input, sizeof(struct port));
+	node->output_port = calloc(desc->n_output, sizeof(struct port));
+	node->control_port = calloc(desc->n_control, sizeof(struct port));
+	node->notify_port = calloc(desc->n_notify, sizeof(struct port));
+
+	spa_log_info(impl->log, "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;
+		port->idx = i;
+		port->external = SPA_ID_INVALID;
+		port->p = desc->input[i];
+		spa_list_init(&port->link_list);
+	}
+	for (i = 0; i < desc->n_output; i++) {
+		struct port *port = &node->output_port[i];
+		port->node = node;
+		port->idx = i;
+		port->external = SPA_ID_INVALID;
+		port->p = desc->output[i];
+		spa_list_init(&port->link_list);
+	}
+	for (i = 0; i < desc->n_control; i++) {
+		struct port *port = &node->control_port[i];
+		port->node = node;
+		port->idx = i;
+		port->external = SPA_ID_INVALID;
+		port->p = desc->control[i];
+		spa_list_init(&port->link_list);
+		port->control_data[0] = desc->default_control[i];
+	}
+	for (i = 0; i < desc->n_notify; i++) {
+		struct port *port = &node->notify_port[i];
+		port->node = node;
+		port->idx = i;
+		port->external = SPA_ID_INVALID;
+		port->p = desc->notify[i];
+		spa_list_init(&port->link_list);
+	}
+	if (have_config)
+		if ((res = parse_config(node, &config)) < 0)
+			spa_log_warn(impl->log, "error parsing config: %s", spa_strerror(res));
+	if (have_control)
+		parse_control(node, &control);
+
+	spa_list_append(&graph->node_list, &node->link);
+
+	return 0;
+}
+
+static void node_cleanup(struct node *node)
+{
+	const struct spa_fga_descriptor *d = node->desc->desc;
+	struct impl *impl = node->graph->impl;
+	uint32_t i;
+
+	for (i = 0; i < node->n_hndl; i++) {
+		if (node->hndl[i] == NULL)
+			continue;
+		spa_log_info(impl->log, "cleanup %s %s[%d]", d->name, node->name, i);
+		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, uint32_t max_samples)
+{
+	float *data;
+	struct node *node = port->node;
+	const struct spa_fga_descriptor *d = node->desc->desc;
+	struct impl *impl = node->graph->impl;
+
+	if ((data = port->audio_mem[i]) == NULL) {
+		data = calloc(max_samples, sizeof(float) + impl->max_align);
+		if (data == NULL) {
+			spa_log_error(impl->log, "cannot create port data: %m");
+			return -errno;
+		}
+		port->audio_mem[i] = data;
+		port->audio_data[i] = SPA_PTR_ALIGN(data, impl->max_align, void);
+	}
+	spa_log_info(impl->log, "connect output port %s[%d]:%s %p",
+			node->name, i, d->ports[port->p].name, port->audio_data[i]);
+	d->connect_port(port->node->hndl[i], port->p, port->audio_data[i]);
+	return 0;
+}
+
+static void port_free_data(struct port *port, uint32_t i)
+{
+	free(port->audio_mem[i]);
+	port->audio_mem[i] = NULL;
+	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);
+	free(node->control_port);
+	free(node->notify_port);
+	free(node->config);
+	free(node);
+}
+
+static int impl_deactivate(void *object)
+{
+	struct impl *impl = object;
+	struct graph *graph = &impl->graph;
+	struct node *node;
+
+	if (!graph->activated)
+		return 0;
+
+	graph->activated = false;
+	spa_list_for_each(node, &graph->node_list, link)
+		node_cleanup(node);
+	return 0;
+}
+
+static int impl_activate(void *object, const struct spa_dict *props)
+{
+	struct impl *impl = object;
+	struct graph *graph = &impl->graph;
+	struct node *node;
+	struct port *port;
+	struct link *link;
+	struct descriptor *desc;
+	const struct spa_fga_descriptor *d;
+	const struct spa_fga_plugin *p;
+	uint32_t i, j, max_samples = impl->quantum_limit;
+	int res;
+	float *sd, *dd, *data;
+	const char *rate;
+
+	if (graph->activated)
+		return 0;
+
+	graph->activated = true;
+
+	rate = spa_dict_lookup(props, SPA_KEY_AUDIO_RATE);
+	impl->rate = rate ? atoi(rate) : DEFAULT_RATE;
+
+	/* first make instances */
+	spa_list_for_each(node, &graph->node_list, link) {
+		node_cleanup(node);
+
+		desc = node->desc;
+		d = desc->desc;
+		p = desc->plugin->plugin;
+
+		for (i = 0; i < node->n_hndl; i++) {
+			spa_log_info(impl->log, "instantiate %s %s[%d] rate:%lu", d->name, node->name, i, impl->rate);
+			errno = EINVAL;
+			if ((node->hndl[i] = d->instantiate(p, d, impl->rate, i, node->config)) == NULL) {
+				spa_log_error(impl->log, "cannot create plugin instance %d rate:%lu: %m", i, impl->rate);
+				res = -errno;
+				goto error;
+			}
+		}
+	}
+
+	/* then link ports */
+	spa_list_for_each(node, &graph->node_list, link) {
+		desc = node->desc;
+		d = desc->desc;
+		if (d->flags & SPA_FGA_DESCRIPTOR_SUPPORTS_NULL_DATA) {
+			sd = dd = NULL;
+		}
+		else {
+			sd = impl->silence_data;
+			dd = impl->discard_data;
+		}
+		for (i = 0; i < node->n_hndl; i++) {
+			for (j = 0; j < desc->n_input; j++) {
+				port = &node->input_port[j];
+				if (!spa_list_is_empty(&port->link_list)) {
+					link = spa_list_first(&port->link_list, struct link, input_link);
+					if ((res = port_ensure_data(link->output, i, max_samples)) < 0)
+						goto error;
+					data = link->output->audio_data[i];
+				} else {
+					data = sd;
+				}
+				spa_log_info(impl->log, "connect input port %s[%d]:%s %p",
+						node->name, i, d->ports[port->p].name, data);
+				d->connect_port(node->hndl[i], port->p, data);
+			}
+			for (j = 0; j < desc->n_output; j++) {
+				port = &node->output_port[j];
+				if (port->audio_data[i] == NULL) {
+					spa_log_info(impl->log, "connect output port %s[%d]:%s %p",
+						node->name, i, d->ports[port->p].name, dd);
+					d->connect_port(node->hndl[i], port->p, dd);
+				}
+			}
+			for (j = 0; j < desc->n_control; j++) {
+				port = &node->control_port[j];
+
+				if (!spa_list_is_empty(&port->link_list)) {
+					link = spa_list_first(&port->link_list, struct link, input_link);
+					data = &link->output->control_data[i];
+				} else {
+					data = &port->control_data[i];
+				}
+				spa_log_info(impl->log, "connect control port %s[%d]:%s %p",
+						node->name, i, d->ports[port->p].name, data);
+				d->connect_port(node->hndl[i], port->p, data);
+			}
+			for (j = 0; j < desc->n_notify; j++) {
+				port = &node->notify_port[j];
+				spa_log_info(impl->log, "connect notify port %s[%d]:%s %p",
+						node->name, i, d->ports[port->p].name,
+						&port->control_data[i]);
+				d->connect_port(node->hndl[i], port->p, &port->control_data[i]);
+			}
+		}
+	}
+
+	/* now activate */
+	spa_list_for_each(node, &graph->node_list, link) {
+		desc = node->desc;
+		d = desc->desc;
+
+		for (i = 0; i < node->n_hndl; i++) {
+			if (d->activate)
+				d->activate(node->hndl[i]);
+			if (node->control_changed && d->control_changed)
+				d->control_changed(node->hndl[i]);
+		}
+	}
+
+	spa_filter_graph_emit_props_changed(&impl->hooks, SPA_DIRECTION_INPUT);
+	return 0;
+error:
+	impl_deactivate(impl);
+	return res;
+}
+
+/* any default values for the controls are set in the first instance
+ * of the control data. Duplicate this to the other instances now. */
+static void setup_node_controls(struct node *node)
+{
+	uint32_t i, j;
+	uint32_t n_hndl = node->n_hndl;
+	uint32_t n_ports = node->desc->n_control;
+	struct port *ports = node->control_port;
+
+	for (i = 0; i < n_ports; i++) {
+		struct port *port = &ports[i];
+		for (j = 1; j < n_hndl; j++)
+			port->control_data[j] = port->control_data[0];
+	}
+}
+
+static struct node *find_next_node(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;
+}
+
+static int setup_graph(struct graph *graph, struct spa_json *inputs, struct spa_json *outputs)
+{
+	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;
+	struct descriptor *desc;
+	const struct spa_fga_descriptor *d;
+	char v[256];
+	bool allow_unused;
+
+	first = spa_list_first(&graph->node_list, struct node, link);
+	last = spa_list_last(&graph->node_list, struct node, link);
+
+	/* calculate the number of inputs and outputs into the graph.
+	 * If we have a list of inputs/outputs, just count them. Otherwise
+	 * we count all input ports of the first node and all output
+	 * ports of the last node */
+	if (inputs != NULL)
+		n_input = count_array(inputs);
+	else
+		n_input = first->desc->n_input;
+
+	if (outputs != NULL)
+		n_output = count_array(outputs);
+	else
+		n_output = last->desc->n_output;
+
+	/* we allow unconnected ports when not explicitly given and the nodes support
+	 * NULL data */
+	allow_unused = inputs == NULL && outputs == NULL &&
+	    SPA_FLAG_IS_SET(first->desc->desc->flags, SPA_FGA_DESCRIPTOR_SUPPORTS_NULL_DATA) &&
+	    SPA_FLAG_IS_SET(last->desc->desc->flags, SPA_FGA_DESCRIPTOR_SUPPORTS_NULL_DATA);
+
+	if (n_input == 0) {
+		spa_log_error(impl->log, "no inputs");
+		res = -EINVAL;
+		goto error;
+	}
+	if (n_output == 0) {
+		spa_log_error(impl->log, "no outputs");
+		res = -EINVAL;
+		goto error;
+	}
+
+	if (impl->info.n_inputs == 0)
+		impl->info.n_inputs = n_input;
+
+	/* compare to the requested number of inputs and duplicate the
+	 * graph n_hndl times when needed. */
+	n_hndl = impl->info.n_inputs / n_input;
+
+	if (impl->info.n_outputs == 0)
+		impl->info.n_outputs = n_output * n_hndl;
+
+	if (n_hndl != impl->info.n_outputs / n_output) {
+		spa_log_error(impl->log, "invalid ports. The input stream has %1$d ports and "
+				"the filter has %2$d inputs. The output stream has %3$d ports "
+				"and the filter has %4$d outputs. input:%1$d / input:%2$d != "
+				"output:%3$d / output:%4$d. Check inputs and outputs objects.",
+				impl->info.n_inputs, n_input,
+				impl->info.n_outputs, n_output);
+		res = -EINVAL;
+		goto error;
+	}
+	if (n_hndl > MAX_HNDL) {
+		spa_log_error(impl->log, "too many ports. %d > %d", n_hndl, MAX_HNDL);
+		res = -EINVAL;
+		goto error;
+	}
+	if (n_hndl == 0) {
+		n_hndl = 1;
+		if (!allow_unused)
+			spa_log_warn(impl->log, "The input stream has %1$d ports and "
+				"the filter has %2$d inputs. The output stream has %3$d ports "
+				"and the filter has %4$d outputs. Some filter ports will be "
+				"unconnected..",
+				impl->info.n_inputs, n_input,
+				impl->info.n_outputs, n_output);
+
+		if (impl->info.n_outputs == 0)
+			impl->info.n_outputs = n_output * n_hndl;
+	}
+	spa_log_info(impl->log, "using %d instances %d %d", n_hndl, n_input, n_output);
+
+	/* now go over all nodes and create instances. */
+	n_control = 0;
+	n_nodes = 0;
+	spa_list_for_each(node, &graph->node_list, link) {
+		node->n_hndl = n_hndl;
+		desc = node->desc;
+		n_control += desc->n_control;
+		n_nodes++;
+		setup_node_controls(node);
+	}
+	graph->n_input = 0;
+	graph->input = calloc(n_input * 16 * n_hndl, sizeof(struct graph_port));
+	graph->n_output = 0;
+	graph->output = calloc(n_output * n_hndl, sizeof(struct graph_port));
+
+	/* now collect all input and output ports for all the handles. */
+	for (i = 0; i < n_hndl; i++) {
+		if (inputs == NULL) {
+			desc = first->desc;
+			d = desc->desc;
+			for (j = 0; j < desc->n_input; j++) {
+				gp = &graph->input[graph->n_input++];
+				spa_log_info(impl->log, "input port %s[%d]:%s",
+						first->name, i, d->ports[desc->input[j]].name);
+				gp->desc = d;
+				gp->hndl = &first->hndl[i];
+				gp->port = desc->input[j];
+			}
+		} else {
+			struct spa_json it = *inputs;
+			while (spa_json_get_string(&it, v, sizeof(v)) > 0) {
+				if (spa_streq(v, "null")) {
+					gp = &graph->input[graph->n_input++];
+					gp->desc = NULL;
+					spa_log_info(impl->log, "ignore input port %d", graph->n_input);
+				} else if ((port = find_port(first, v, SPA_FGA_PORT_INPUT)) == NULL) {
+					res = -ENOENT;
+					spa_log_error(impl->log, "input port %s not found", v);
+					goto error;
+				} else {
+					bool disabled = false;
+
+					desc = port->node->desc;
+					d = desc->desc;
+					if (i == 0 && port->external != SPA_ID_INVALID) {
+						spa_log_error(impl->log, "input port %s[%d]:%s already used as input %d, use mixer",
+							port->node->name, i, d->ports[port->p].name,
+							port->external);
+						res = -EBUSY;
+						goto error;
+					}
+					if (port->n_links > 0) {
+						spa_log_error(impl->log, "input port %s[%d]:%s already used by link, use mixer",
+							port->node->name, i, d->ports[port->p].name);
+						res = -EBUSY;
+						goto error;
+					}
+
+					if (d->flags & SPA_FGA_DESCRIPTOR_COPY) {
+						for (j = 0; j < desc->n_output; j++) {
+							struct port *p = &port->node->output_port[j];
+							struct link *link;
+
+							gp = NULL;
+							spa_list_for_each(link, &p->link_list, output_link) {
+								struct port *peer = link->input;
+
+								spa_log_info(impl->log, "copy input port %s[%d]:%s",
+									port->node->name, i,
+									d->ports[port->p].name);
+								peer->external = graph->n_input;
+								gp = &graph->input[graph->n_input++];
+								gp->desc = peer->node->desc->desc;
+								gp->hndl = &peer->node->hndl[i];
+								gp->port = peer->p;
+								gp->next = true;
+								disabled = true;
+							}
+							if (gp != NULL)
+								gp->next = false;
+						}
+						port->node->disabled = disabled;
+					}
+					if (!disabled) {
+						spa_log_info(impl->log, "input port %s[%d]:%s",
+							port->node->name, i, d->ports[port->p].name);
+						port->external = graph->n_input;
+						gp = &graph->input[graph->n_input++];
+						gp->desc = d;
+						gp->hndl = &port->node->hndl[i];
+						gp->port = port->p;
+						gp->next = false;
+					}
+				}
+			}
+		}
+		if (outputs == NULL) {
+			desc = last->desc;
+			d = desc->desc;
+			for (j = 0; j < desc->n_output; j++) {
+				gp = &graph->output[graph->n_output++];
+				spa_log_info(impl->log, "output port %s[%d]:%s",
+						last->name, i, d->ports[desc->output[j]].name);
+				gp->desc = d;
+				gp->hndl = &last->hndl[i];
+				gp->port = desc->output[j];
+			}
+		} else {
+			struct spa_json it = *outputs;
+			while (spa_json_get_string(&it, v, sizeof(v)) > 0) {
+				gp = &graph->output[graph->n_output];
+				if (spa_streq(v, "null")) {
+					gp->desc = NULL;
+					spa_log_info(impl->log, "silence output port %d", graph->n_output);
+				} else if ((port = find_port(last, v, SPA_FGA_PORT_OUTPUT)) == NULL) {
+					res = -ENOENT;
+					spa_log_error(impl->log, "output port %s not found", v);
+					goto error;
+				} else {
+					desc = port->node->desc;
+					d = desc->desc;
+					if (i == 0 && port->external != SPA_ID_INVALID) {
+						spa_log_error(impl->log, "output port %s[%d]:%s already used as output %d, use copy",
+							port->node->name, i, d->ports[port->p].name,
+							port->external);
+						res = -EBUSY;
+						goto error;
+					}
+					if (port->n_links > 0) {
+						spa_log_error(impl->log, "output port %s[%d]:%s already used by link, use copy",
+							port->node->name, i, d->ports[port->p].name);
+						res = -EBUSY;
+						goto error;
+					}
+					spa_log_info(impl->log, "output port %s[%d]:%s",
+							port->node->name, i, d->ports[port->p].name);
+					port->external = graph->n_output;
+					gp->desc = d;
+					gp->hndl = &port->node->hndl[i];
+					gp->port = port->p;
+				}
+				graph->n_output++;
+			}
+		}
+	}
+
+	/* order all nodes based on dependencies */
+	graph->n_hndl = 0;
+	graph->hndl = calloc(n_nodes * n_hndl, sizeof(struct graph_hndl));
+	graph->n_control = 0;
+	graph->control_port = calloc(n_control, sizeof(struct port *));
+	while (true) {
+		if ((node = find_next_node(graph)) == NULL)
+			break;
+
+		desc = node->desc;
+		d = desc->desc;
+
+		if (!node->disabled) {
+			for (i = 0; i < n_hndl; i++) {
+				gh = &graph->hndl[graph->n_hndl++];
+				gh->hndl = &node->hndl[i];
+				gh->desc = d;
+			}
+		}
+		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--;
+		}
+		for (i = 0; i < desc->n_notify; i++) {
+			spa_list_for_each(link, &node->notify_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++) {
+			graph->control_port[graph->n_control] = &node->control_port[i];
+			graph->n_control++;
+		}
+	}
+	res = 0;
+error:
+	return res;
+}
+
+/**
+ * filter.graph = {
+ *     nodes = [
+ *         { ... } ...
+ *     ]
+ *     links = [
+ *         { ... } ...
+ *     ]
+ *     inputs = [ ]
+ *     outputs = [ ]
+ *     input.volumes = [
+ *         ...
+ *     ]
+ *     output.volumes = [
+ *         ...
+ *     ]
+ * }
+ */
+static int load_graph(struct graph *graph, const struct spa_dict *props)
+{
+	struct impl *impl = graph->impl;
+	struct spa_json it[2];
+	struct spa_json inputs, outputs, *pinputs = NULL, *poutputs = NULL;
+	struct spa_json ivolumes, ovolumes, *pivolumes = NULL, *povolumes = NULL;
+	struct spa_json nodes, *pnodes = NULL, links, *plinks = NULL;
+	const char *json, *val;
+	char key[256];
+	int res, len;
+
+	spa_list_init(&graph->node_list);
+	spa_list_init(&graph->link_list);
+
+	if ((json = spa_dict_lookup(props, "filter.graph")) == NULL) {
+		spa_log_error(impl->log, "missing filter.graph property");
+		return -EINVAL;
+	}
+
+        if (spa_json_begin_object(&it[0], json, strlen(json)) <= 0) {
+		spa_log_error(impl->log, "filter.graph must be an object");
+		return -EINVAL;
+	}
+
+	while ((len = spa_json_object_next(&it[0], key, sizeof(key), &val)) > 0) {
+		if (spa_streq("n_inputs", key)) {
+			if (spa_json_parse_int(val, len, &res) <= 0) {
+				spa_log_error(impl->log, "%s expects an integer", key);
+				return -EINVAL;
+			}
+			impl->info.n_inputs = res;
+		}
+		else if (spa_streq("n_outputs", key)) {
+			if (spa_json_parse_int(val, len, &res) <= 0) {
+				spa_log_error(impl->log, "%s expects an integer", key);
+				return -EINVAL;
+			}
+			impl->info.n_outputs = res;
+		}
+		else if (spa_streq("nodes", key)) {
+			if (!spa_json_is_array(val, len)) {
+				spa_log_error(impl->log, "%s expects an array", key);
+				return -EINVAL;
+			}
+			spa_json_enter(&it[0], &nodes);
+			pnodes = &nodes;
+		}
+		else if (spa_streq("links", key)) {
+			if (!spa_json_is_array(val, len)) {
+				spa_log_error(impl->log, "%s expects an array", key);
+				return -EINVAL;
+			}
+			spa_json_enter(&it[0], &links);
+			plinks = &links;
+		}
+		else if (spa_streq("inputs", key)) {
+			if (!spa_json_is_array(val, len)) {
+				spa_log_error(impl->log, "%s expects an array", key);
+				return -EINVAL;
+			}
+			spa_json_enter(&it[0], &inputs);
+			pinputs = &inputs;
+		}
+		else if (spa_streq("outputs", key)) {
+			if (!spa_json_is_array(val, len)) {
+				spa_log_error(impl->log, "%s expects an array", key);
+				return -EINVAL;
+			}
+			spa_json_enter(&it[0], &outputs);
+			poutputs = &outputs;
+		}
+		else if (spa_streq("capture.volumes", key) ||
+		    spa_streq("input.volumes", key)) {
+			if (!spa_json_is_array(val, len)) {
+				spa_log_error(impl->log, "%s expects an array", key);
+				return -EINVAL;
+			}
+			spa_json_enter(&it[0], &ivolumes);
+			pivolumes = &ivolumes;
+		}
+		else if (spa_streq("playback.volumes", key) ||
+		    spa_streq("output.volumes", key)) {
+			if (!spa_json_is_array(val, len)) {
+				spa_log_error(impl->log, "%s expects an array", key);
+				return -EINVAL;
+			}
+			spa_json_enter(&it[0], &ovolumes);
+			povolumes = &ovolumes;
+		} else {
+			spa_log_warn(impl->log, "unexpected graph key '%s'", key);
+		}
+	}
+	if (pnodes == NULL) {
+		spa_log_error(impl->log, "filter.graph is missing a nodes array");
+		return -EINVAL;
+	}
+	while (spa_json_enter_object(pnodes, &it[1]) > 0) {
+		if ((res = load_node(graph, &it[1])) < 0)
+			return res;
+	}
+	if (plinks != NULL) {
+		while (spa_json_enter_object(plinks, &it[1]) > 0) {
+			if ((res = parse_link(graph, &it[1])) < 0)
+				return res;
+		}
+	}
+	if (pivolumes != NULL) {
+		while (spa_json_enter_object(pivolumes, &it[1]) > 0) {
+			if ((res = parse_volume(graph, &it[1], SPA_DIRECTION_INPUT)) < 0)
+				return res;
+		}
+	}
+	if (povolumes != NULL) {
+		while (spa_json_enter_object(povolumes, &it[1]) > 0) {
+			if ((res = parse_volume(graph, &it[1], SPA_DIRECTION_OUTPUT)) < 0)
+				return res;
+		}
+	}
+	return setup_graph(graph, pinputs, poutputs);
+}
+
+static void graph_free(struct graph *graph)
+{
+	struct link *link;
+	struct node *node;
+	spa_list_consume(link, &graph->link_list, link)
+		link_free(link);
+	spa_list_consume(node, &graph->node_list, link)
+		node_free(node);
+	free(graph->input);
+	free(graph->output);
+	free(graph->hndl);
+	free(graph->control_port);
+}
+
+static const struct spa_filter_graph_methods impl_filter_graph = {
+	SPA_VERSION_FILTER_GRAPH_METHODS,
+	.add_listener = impl_add_listener,
+	.enum_prop_info = impl_enum_prop_info,
+	.get_props = impl_get_props,
+	.set_props = impl_set_props,
+	.activate = impl_activate,
+	.deactivate = impl_deactivate,
+	.reset = impl_reset,
+	.process = impl_process,
+};
+
+static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface)
+{
+	struct impl *this;
+
+	spa_return_val_if_fail(handle != NULL, -EINVAL);
+	spa_return_val_if_fail(interface != NULL, -EINVAL);
+
+	this = (struct impl *) handle;
+
+	if (spa_streq(type, SPA_TYPE_INTERFACE_FilterGraph))
+		*interface = &this->filter_graph;
+	else
+		return -ENOENT;
+
+	return 0;
+}
+
+static int impl_clear(struct spa_handle *handle)
+{
+	struct impl *impl = (struct impl *) handle;
+
+	graph_free(&impl->graph);
+
+	if (impl->dsp)
+		spa_fga_dsp_free(impl->dsp);
+
+	free(impl->silence_data);
+	free(impl->discard_data);
+	return 0;
+}
+
+static size_t
+impl_get_size(const struct spa_handle_factory *factory,
+	      const struct spa_dict *params)
+{
+	return sizeof(struct impl);
+}
+
+static int
+impl_init(const struct spa_handle_factory *factory,
+	  struct spa_handle *handle,
+	  const struct spa_dict *info,
+	  const struct spa_support *support,
+	  uint32_t n_support)
+{
+	struct impl *impl;
+	uint32_t i;
+	int res;
+
+	handle->get_interface = impl_get_interface;
+	handle->clear = impl_clear;
+
+	impl = (struct impl *) handle;
+	impl->graph.impl = impl;
+
+	impl->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
+	spa_log_topic_init(impl->log, &log_topic);
+
+	impl->cpu = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_CPU);
+	impl->max_align = spa_cpu_get_max_align(impl->cpu);
+
+	impl->dsp = spa_fga_dsp_new(impl->cpu ? spa_cpu_get_flags(impl->cpu) : 0);
+
+	impl->loader = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_PluginLoader);
+
+	spa_list_init(&impl->plugin_list);
+
+	for (i = 0; info && i < info->n_items; i++) {
+		const char *k = info->items[i].key;
+		const char *s = info->items[i].value;
+		if (spa_streq(k, "clock.quantum-limit"))
+			spa_atou32(s, &impl->quantum_limit, 0);
+		if (spa_streq(k, "filter-graph.n_inputs"))
+			spa_atou32(s, &impl->info.n_inputs, 0);
+		if (spa_streq(k, "filter-graph.n_outputs"))
+			spa_atou32(s, &impl->info.n_outputs, 0);
+	}
+	if (impl->quantum_limit == 0)
+		return -EINVAL;
+
+	impl->silence_data = calloc(impl->quantum_limit, sizeof(float));
+	if (impl->silence_data == NULL) {
+		res = -errno;
+		goto error;
+	}
+
+	impl->discard_data = calloc(impl->quantum_limit, sizeof(float));
+	if (impl->discard_data == NULL) {
+		res = -errno;
+		goto error;
+	}
+
+	if ((res = load_graph(&impl->graph, info)) < 0) {
+		spa_log_error(impl->log, "can't load graph: %s", spa_strerror(res));
+		goto error;
+	}
+
+	impl->filter_graph.iface = SPA_INTERFACE_INIT(
+			SPA_TYPE_INTERFACE_FilterGraph,
+			SPA_VERSION_FILTER_GRAPH,
+			&impl_filter_graph, impl);
+	spa_hook_list_init(&impl->hooks);
+
+	return 0;
+error:
+	free(impl->silence_data);
+	free(impl->discard_data);
+	return res;
+}
+
+static const struct spa_interface_info impl_interfaces[] = {
+	{SPA_TYPE_INTERFACE_FilterGraph,},
+};
+
+static int
+impl_enum_interface_info(const struct spa_handle_factory *factory,
+			 const struct spa_interface_info **info,
+			 uint32_t *index)
+{
+	spa_return_val_if_fail(factory != NULL, -EINVAL);
+	spa_return_val_if_fail(info != NULL, -EINVAL);
+	spa_return_val_if_fail(index != NULL, -EINVAL);
+
+	switch (*index) {
+	case 0:
+		*info = &impl_interfaces[*index];
+		break;
+	default:
+		return 0;
+	}
+	(*index)++;
+	return 1;
+}
+
+static struct spa_handle_factory spa_filter_graph_factory = {
+	SPA_VERSION_HANDLE_FACTORY,
+	"filter.graph",
+	NULL,
+	impl_get_size,
+	impl_init,
+	impl_enum_interface_info,
+};
+
+SPA_EXPORT
+int spa_handle_factory_enum(const struct spa_handle_factory **factory, uint32_t *index)
+{
+	spa_return_val_if_fail(factory != NULL, -EINVAL);
+	spa_return_val_if_fail(index != NULL, -EINVAL);
+
+	switch (*index) {
+	case 0:
+		*factory = &spa_filter_graph_factory;
+		break;
+	default:
+		return 0;
+	}
+	(*index)++;
+	return 1;
+}
diff --git a/src/modules/module-filter-chain/ladspa.h b/spa/plugins/filter-graph/ladspa.h
similarity index 100%
rename from src/modules/module-filter-chain/ladspa.h
rename to spa/plugins/filter-graph/ladspa.h
diff --git a/spa/plugins/filter-graph/ladspa_plugin.c b/spa/plugins/filter-graph/ladspa_plugin.c
new file mode 100644
index 00000000..45026c8e
--- /dev/null
+++ b/spa/plugins/filter-graph/ladspa_plugin.c
@@ -0,0 +1,385 @@
+/* PipeWire */
+/* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */
+/* SPDX-License-Identifier: MIT */
+
+#include "config.h"
+
+#include <dlfcn.h>
+#include <math.h>
+#include <limits.h>
+
+#include <spa/utils/result.h>
+#include <spa/utils/defs.h>
+#include <spa/utils/list.h>
+#include <spa/utils/string.h>
+#include <spa/support/log.h>
+
+#include "audio-plugin.h"
+#include "ladspa.h"
+
+struct plugin {
+	struct spa_handle handle;
+	struct spa_fga_plugin plugin;
+
+	struct spa_log *log;
+
+	void *hndl;
+	LADSPA_Descriptor_Function desc_func;
+};
+
+struct descriptor {
+	struct spa_fga_descriptor desc;
+	const LADSPA_Descriptor *d;
+};
+
+static void *ladspa_instantiate(const struct spa_fga_plugin *plugin, const struct spa_fga_descriptor *desc,
+                        unsigned long SampleRate, int index, const char *config)
+{
+	struct descriptor *d = (struct descriptor *)desc;
+	return d->d->instantiate(d->d, SampleRate);
+}
+
+static const LADSPA_Descriptor *find_desc(LADSPA_Descriptor_Function desc_func, const char *name)
+{
+	unsigned long i;
+	for (i = 0; ;i++) {
+		const LADSPA_Descriptor *d = desc_func(i);
+		if (d == NULL)
+			break;
+		if (spa_streq(d->Label, name))
+			return d;
+	}
+	return NULL;
+}
+
+static float get_default(struct spa_fga_port *port, LADSPA_PortRangeHintDescriptor hint,
+		LADSPA_Data lower, LADSPA_Data upper)
+{
+	LADSPA_Data def;
+
+	switch (hint & LADSPA_HINT_DEFAULT_MASK) {
+	case LADSPA_HINT_DEFAULT_MINIMUM:
+		def = lower;
+		break;
+	case LADSPA_HINT_DEFAULT_MAXIMUM:
+		def = upper;
+		break;
+	case LADSPA_HINT_DEFAULT_LOW:
+		if (LADSPA_IS_HINT_LOGARITHMIC(hint))
+			def = (LADSPA_Data) expf(logf(lower) * 0.75f + logf(upper) * 0.25f);
+		else
+			def = (LADSPA_Data) (lower * 0.75f + upper * 0.25f);
+		break;
+	case LADSPA_HINT_DEFAULT_MIDDLE:
+		if (LADSPA_IS_HINT_LOGARITHMIC(hint))
+			def = (LADSPA_Data) expf(logf(lower) * 0.5f + logf(upper) * 0.5f);
+		else
+			def = (LADSPA_Data) (lower * 0.5f + upper * 0.5f);
+		break;
+	case LADSPA_HINT_DEFAULT_HIGH:
+		if (LADSPA_IS_HINT_LOGARITHMIC(hint))
+			def = (LADSPA_Data) expf(logf(lower) * 0.25f + logf(upper) * 0.75f);
+		else
+			def = (LADSPA_Data) (lower * 0.25f + upper * 0.75f);
+		break;
+	case LADSPA_HINT_DEFAULT_0:
+		def = 0.0f;
+		break;
+	case LADSPA_HINT_DEFAULT_1:
+		def = 1.0f;
+		break;
+	case LADSPA_HINT_DEFAULT_100:
+		def = 100.0f;
+		break;
+	case LADSPA_HINT_DEFAULT_440:
+		def = 440.0f;
+		break;
+	default:
+		if (upper == lower)
+			def = upper;
+		else
+			def = SPA_CLAMPF(0.5f * upper, lower, upper);
+		break;
+	}
+	if (LADSPA_IS_HINT_INTEGER(hint))
+		def = roundf(def);
+	return def;
+}
+
+static void ladspa_port_update_ranges(struct descriptor *dd, struct spa_fga_port *port)
+{
+	const LADSPA_Descriptor *d = dd->d;
+	unsigned long p = port->index;
+	LADSPA_PortRangeHintDescriptor hint = d->PortRangeHints[p].HintDescriptor;
+	LADSPA_Data lower, upper;
+
+	lower = d->PortRangeHints[p].LowerBound;
+	upper = d->PortRangeHints[p].UpperBound;
+
+	port->hint = hint;
+	port->def = get_default(port, hint, lower, upper);
+	port->min = lower;
+	port->max = upper;
+}
+
+static void ladspa_free(const struct spa_fga_descriptor *desc)
+{
+	struct descriptor *d = (struct descriptor*)desc;
+	free(d->desc.ports);
+	free(d);
+}
+
+static const struct spa_fga_descriptor *ladspa_plugin_make_desc(void *plugin, const char *name)
+{
+	struct plugin *p = (struct plugin *)plugin;
+	struct descriptor *desc;
+	const LADSPA_Descriptor *d;
+	uint32_t i;
+
+	d = find_desc(p->desc_func, name);
+	if (d == NULL)
+		return NULL;
+
+	desc = calloc(1, sizeof(*desc));
+	desc->d = d;
+
+	desc->desc.instantiate = ladspa_instantiate;
+	desc->desc.cleanup = d->cleanup;
+	desc->desc.connect_port = d->connect_port;
+	desc->desc.activate = d->activate;
+	desc->desc.deactivate = d->deactivate;
+	desc->desc.run = d->run;
+
+	desc->desc.free = ladspa_free;
+
+	desc->desc.name = d->Label;
+	desc->desc.flags = 0;
+
+	desc->desc.n_ports = d->PortCount;
+	desc->desc.ports = calloc(desc->desc.n_ports, sizeof(struct spa_fga_port));
+
+	for (i = 0; i < desc->desc.n_ports; i++) {
+		desc->desc.ports[i].index = i;
+		desc->desc.ports[i].name = d->PortNames[i];
+		desc->desc.ports[i].flags = d->PortDescriptors[i];
+		ladspa_port_update_ranges(desc, &desc->desc.ports[i]);
+	}
+	return &desc->desc;
+}
+
+static struct spa_fga_plugin_methods impl_plugin = {
+	SPA_VERSION_FGA_PLUGIN_METHODS,
+	.make_desc = ladspa_plugin_make_desc,
+};
+
+static int ladspa_handle_load_by_path(struct plugin *impl, const char *path)
+{
+	int res;
+	void *handle = NULL;
+	LADSPA_Descriptor_Function desc_func;
+
+	handle = dlopen(path, RTLD_NOW);
+	if (handle == NULL) {
+		spa_log_debug(impl->log, "failed to open '%s': %s", path, dlerror());
+		res = -ENOENT;
+		goto exit;
+	}
+
+	spa_log_info(impl->log, "successfully opened '%s'", path);
+
+	desc_func = (LADSPA_Descriptor_Function) dlsym(handle, "ladspa_descriptor");
+	if (desc_func == NULL) {
+		spa_log_warn(impl->log, "cannot find descriptor function in '%s': %s", path, dlerror());
+		res = -ENOSYS;
+		goto exit;
+	}
+
+	impl->hndl = handle;
+	impl->desc_func = desc_func;
+	return 0;
+
+exit:
+	if (handle)
+		dlclose(handle);
+	return res;
+}
+
+static inline const char *split_walk(const char *str, const char *delimiter, size_t * len, const char **state)
+{
+	const char *s = *state ? *state : str;
+
+	s += strspn(s, delimiter);
+	if (*s == '\0')
+		return NULL;
+
+	*len = strcspn(s, delimiter);
+	*state = s + *len;
+
+	return s;
+}
+
+static int load_ladspa_plugin(struct plugin *impl, const char *path)
+{
+	int res = -ENOENT;
+
+	if (path[0] != '/') {
+		const char *search_dirs, *p, *state = NULL;
+		char filename[PATH_MAX];
+		size_t len;
+
+		search_dirs = getenv("LADSPA_PATH");
+		if (!search_dirs)
+			search_dirs = "/usr/lib64/ladspa:/usr/lib/ladspa:" LIBDIR;
+
+		/*
+		 * set the errno for the case when `ladspa_handle_load_by_path()`
+		 * is never called, which can only happen if the supplied
+		 * LADSPA_PATH contains too long paths
+		 */
+		res = -ENAMETOOLONG;
+
+		while ((p = split_walk(search_dirs, ":", &len, &state))) {
+			int namelen;
+
+			if (len >= sizeof(filename))
+				continue;
+
+			namelen = snprintf(filename, sizeof(filename), "%.*s/%s.so", (int) len, p, path);
+			if (namelen < 0 || (size_t) namelen >= sizeof(filename))
+				continue;
+
+			res = ladspa_handle_load_by_path(impl, filename);
+			if (res >= 0)
+				break;
+		}
+	}
+	else {
+		res = ladspa_handle_load_by_path(impl, path);
+	}
+	return res;
+}
+
+static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface)
+{
+	struct plugin *impl;
+
+	spa_return_val_if_fail(handle != NULL, -EINVAL);
+	spa_return_val_if_fail(interface != NULL, -EINVAL);
+
+	impl = (struct plugin *) handle;
+
+	if (spa_streq(type, SPA_TYPE_INTERFACE_FILTER_GRAPH_AudioPlugin))
+		*interface = &impl->plugin;
+	else
+		return -ENOENT;
+
+	return 0;
+}
+
+static int impl_clear(struct spa_handle *handle)
+{
+	struct plugin *impl = (struct plugin *)handle;
+	if (impl->hndl)
+		dlclose(impl->hndl);
+	impl->hndl = NULL;
+	return 0;
+}
+
+static size_t
+impl_get_size(const struct spa_handle_factory *factory,
+	      const struct spa_dict *params)
+{
+	return sizeof(struct plugin);
+}
+
+static int
+impl_init(const struct spa_handle_factory *factory,
+	  struct spa_handle *handle,
+	  const struct spa_dict *info,
+	  const struct spa_support *support,
+	  uint32_t n_support)
+{
+	struct plugin *impl;
+	uint32_t i;
+	int res;
+	const char *path = NULL;
+
+	handle->get_interface = impl_get_interface;
+	handle->clear = impl_clear;
+
+	impl = (struct plugin *) handle;
+
+	impl->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
+
+	for (i = 0; info && i < info->n_items; i++) {
+		const char *k = info->items[i].key;
+		const char *s = info->items[i].value;
+		if (spa_streq(k, "filter.graph.path"))
+			path = s;
+	}
+	if (path == NULL)
+		return -EINVAL;
+
+	if ((res = load_ladspa_plugin(impl, path)) < 0) {
+		spa_log_error(impl->log, "failed to load plugin '%s': %s",
+				path, spa_strerror(res));
+		return res;
+	}
+
+	impl->plugin.iface = SPA_INTERFACE_INIT(
+			SPA_TYPE_INTERFACE_FILTER_GRAPH_AudioPlugin,
+			SPA_VERSION_FGA_PLUGIN,
+			&impl_plugin, impl);
+
+	return 0;
+}
+
+static const struct spa_interface_info impl_interfaces[] = {
+	{ SPA_TYPE_INTERFACE_FILTER_GRAPH_AudioPlugin },
+};
+
+static int
+impl_enum_interface_info(const struct spa_handle_factory *factory,
+			 const struct spa_interface_info **info,
+			 uint32_t *index)
+{
+	spa_return_val_if_fail(factory != NULL, -EINVAL);
+	spa_return_val_if_fail(info != NULL, -EINVAL);
+	spa_return_val_if_fail(index != NULL, -EINVAL);
+
+	switch (*index) {
+	case 0:
+		*info = &impl_interfaces[*index];
+		break;
+	default:
+		return 0;
+	}
+	(*index)++;
+	return 1;
+}
+
+static struct spa_handle_factory spa_fga_plugin_ladspa_factory = {
+	SPA_VERSION_HANDLE_FACTORY,
+	"filter.graph.plugin.ladspa",
+	NULL,
+	impl_get_size,
+	impl_init,
+	impl_enum_interface_info,
+};
+
+SPA_EXPORT
+int spa_handle_factory_enum(const struct spa_handle_factory **factory, uint32_t *index)
+{
+	spa_return_val_if_fail(factory != NULL, -EINVAL);
+	spa_return_val_if_fail(index != NULL, -EINVAL);
+
+	switch (*index) {
+	case 0:
+		*factory = &spa_fga_plugin_ladspa_factory;
+		break;
+	default:
+		return 0;
+	}
+	(*index)++;
+	return 1;
+}
diff --git a/src/modules/module-filter-chain/lv2_plugin.c b/spa/plugins/filter-graph/lv2_plugin.c
similarity index 70%
rename from src/modules/module-filter-chain/lv2_plugin.c
rename to spa/plugins/filter-graph/lv2_plugin.c
index ffa35d73..a2af22d6 100644
--- a/src/modules/module-filter-chain/lv2_plugin.c
+++ b/spa/plugins/filter-graph/lv2_plugin.c
@@ -5,16 +5,13 @@
 #include <dlfcn.h>
 #include <math.h>
 
+#include <lilv/lilv.h>
+
 #include <spa/utils/defs.h>
 #include <spa/utils/list.h>
 #include <spa/utils/string.h>
 #include <spa/support/loop.h>
-
-#include <pipewire/log.h>
-#include <pipewire/utils.h>
-#include <pipewire/array.h>
-
-#include <lilv/lilv.h>
+#include <spa/support/log.h>
 
 #if defined __has_include
 #	if __has_include (<lv2/atom/atom.h>)
@@ -37,50 +34,53 @@
 
 #endif
 
-#include "plugin.h"
+#include "audio-plugin.h"
 
 static struct context *_context;
 
 typedef struct URITable {
-	struct pw_array array;
+	char **data;
+	size_t alloc;
+	size_t len;
 } URITable;
 
 static void uri_table_init(URITable *table)
 {
-	pw_array_init(&table->array, 1024);
+	table->data = NULL;
+	table->len = table->alloc = 0;
 }
 
 static void uri_table_destroy(URITable *table)
 {
-	char **p;
-	pw_array_for_each(p, &table->array)
-		free(*p);
-	pw_array_clear(&table->array);
+	size_t i;
+	for (i = 0; i < table->len; i++)
+		free(table->data[i]);
+	free(table->data);
+	uri_table_init(table);
 }
 
 static LV2_URID uri_table_map(LV2_URID_Map_Handle handle, const char *uri)
 {
 	URITable *table = (URITable*)handle;
-	char **p;
-	size_t i = 0;
-
-	pw_array_for_each(p, &table->array) {
-		i++;
-		if (spa_streq(*p, uri))
-			goto done;
-	}
-	pw_array_add_ptr(&table->array, strdup(uri));
-	i =  pw_array_get_len(&table->array, char*);
-done:
-	return i;
+	size_t i;
+
+	for (i = 0; i < table->len; i++)
+		if (spa_streq(table->data[i], uri))
+			return i+1;
+
+	if (table->len == table->alloc) {
+		table->alloc += 64;
+		table->data = realloc(table->data, table->alloc * sizeof(char *));
+ 	}
+	table->data[table->len++] = strdup(uri);
+	return table->len;
 }
 
 static const char *uri_table_unmap(LV2_URID_Map_Handle handle, LV2_URID urid)
 {
 	URITable *table = (URITable*)handle;
-
-	if (urid > 0 && urid <= pw_array_get_len(&table->array, char*))
-		return *pw_array_get_unchecked(&table->array, urid - 1, char*);
+	if (urid > 0 && urid <= table->len)
+		return table->data[urid-1];
 	return NULL;
 }
 
@@ -88,9 +88,6 @@ struct context {
 	int ref;
 	LilvWorld *world;
 
-	struct spa_loop *data_loop;
-	struct spa_loop *main_loop;
-
 	LilvNode *lv2_InputPort;
 	LilvNode *lv2_OutputPort;
 	LilvNode *lv2_AudioPort;
@@ -144,7 +141,7 @@ static const LV2_Feature buf_size_features[3] = {
 	{ LV2_BUF_SIZE__boundedBlockLength,  NULL },
 };
 
-static struct context *context_new(const struct spa_support *support, uint32_t n_support)
+static struct context *context_new(void)
 {
 	struct context *c;
 
@@ -185,19 +182,16 @@ static struct context *context_new(const struct spa_support *support, uint32_t n
 	c->atom_Int = context_map(c, LV2_ATOM__Int);
 	c->atom_Float = context_map(c, LV2_ATOM__Float);
 
-	c->data_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataLoop);
-	c->main_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Loop);
-
 	return c;
 error:
 	context_free(c);
 	return NULL;
 }
 
-static struct context *context_ref(const struct spa_support *support, uint32_t n_support)
+static struct context *context_ref(void)
 {
 	if (_context == NULL) {
-		_context = context_new(support, n_support);
+		_context = context_new();
 		if (_context == NULL)
 			return NULL;
 	}
@@ -214,18 +208,26 @@ static void context_unref(struct context *context)
 }
 
 struct plugin {
-	struct fc_plugin plugin;
+	struct spa_handle handle;
+	struct spa_fga_plugin plugin;
+
+	struct spa_log *log;
+	struct spa_loop *data_loop;
+	struct spa_loop *main_loop;
+
 	struct context *c;
 	const LilvPlugin *p;
 };
 
 struct descriptor {
-	struct fc_descriptor desc;
+	struct spa_fga_descriptor desc;
 	struct plugin *p;
 };
 
 struct instance {
 	struct descriptor *desc;
+	struct plugin *p;
+
 	LilvInstance *instance;
 	LV2_Worker_Schedule work_schedule;
 	LV2_Feature work_schedule_feature;
@@ -254,8 +256,7 @@ static LV2_Worker_Status
 work_respond(LV2_Worker_Respond_Handle handle, uint32_t size, const void *data)
 {
 	struct instance *i = (struct instance*)handle;
-	struct context *c = i->desc->p->c;
-	spa_loop_invoke(c->data_loop, do_respond, 1, data, size, false, i);
+	spa_loop_invoke(i->p->data_loop, do_respond, 1, data, size, false, i);
 	return LV2_WORKER_SUCCESS;
 }
 
@@ -273,12 +274,11 @@ static LV2_Worker_Status
 work_schedule(LV2_Worker_Schedule_Handle handle, uint32_t size, const void *data)
 {
 	struct instance *i = (struct instance*)handle;
-	struct context *c = i->desc->p->c;
-	spa_loop_invoke(c->main_loop, do_schedule, 1, data, size, false, i);
+	spa_loop_invoke(i->p->main_loop, do_schedule, 1, data, size, false, i);
 	return LV2_WORKER_SUCCESS;
 }
 
-static void *lv2_instantiate(const struct fc_plugin *plugin, const struct fc_descriptor *desc,
+static void *lv2_instantiate(const struct spa_fga_plugin *plugin, const struct spa_fga_descriptor *desc,
                         unsigned long SampleRate, int index, const char *config)
 {
 	struct descriptor *d = (struct descriptor*)desc;
@@ -297,6 +297,7 @@ static void *lv2_instantiate(const struct fc_plugin *plugin, const struct fc_des
 
 	i->block_length = 1024;
 	i->desc = d;
+	i->p = p;
 	i->features[n_features++] = &c->map_feature;
 	i->features[n_features++] = &c->unmap_feature;
 	i->features[n_features++] = &buf_size_features[0];
@@ -383,7 +384,7 @@ static void lv2_run(void *instance, unsigned long SampleCount)
 		i->work_iface->end_run(i->instance);
 }
 
-static void lv2_free(const struct fc_descriptor *desc)
+static void lv2_free(const struct spa_fga_descriptor *desc)
 {
 	struct descriptor *d = (struct descriptor*)desc;
 	free((char*)d->desc.name);
@@ -391,7 +392,7 @@ static void lv2_free(const struct fc_descriptor *desc)
 	free(d);
 }
 
-static const struct fc_descriptor *lv2_make_desc(struct fc_plugin *plugin, const char *name)
+static const struct spa_fga_descriptor *lv2_plugin_make_desc(void *plugin, const char *name)
 {
 	struct plugin *p = (struct plugin *)plugin;
 	struct context *c = p->c;
@@ -417,7 +418,7 @@ static const struct fc_descriptor *lv2_make_desc(struct fc_plugin *plugin, const
 	desc->desc.flags = 0;
 
 	desc->desc.n_ports = lilv_plugin_get_num_ports(p->p);
-	desc->desc.ports = calloc(desc->desc.n_ports, sizeof(struct fc_port));
+	desc->desc.ports = calloc(desc->desc.n_ports, sizeof(struct spa_fga_port));
 
 	mins = alloca(desc->desc.n_ports * sizeof(float));
 	maxes = alloca(desc->desc.n_ports * sizeof(float));
@@ -428,20 +429,20 @@ static const struct fc_descriptor *lv2_make_desc(struct fc_plugin *plugin, const
 	for (i = 0; i < desc->desc.n_ports; i++) {
 		const LilvPort *port = lilv_plugin_get_port_by_index(p->p, i);
                 const LilvNode *symbol = lilv_port_get_symbol(p->p, port);
-		struct fc_port *fp = &desc->desc.ports[i];
+		struct spa_fga_port *fp = &desc->desc.ports[i];
 
 		fp->index = i;
 		fp->name = strdup(lilv_node_as_string(symbol));
 
 		fp->flags = 0;
 		if (lilv_port_is_a(p->p, port, c->lv2_InputPort))
-			fp->flags |= FC_PORT_INPUT;
+			fp->flags |= SPA_FGA_PORT_INPUT;
 		if (lilv_port_is_a(p->p, port, c->lv2_OutputPort))
-			fp->flags |= FC_PORT_OUTPUT;
+			fp->flags |= SPA_FGA_PORT_OUTPUT;
 		if (lilv_port_is_a(p->p, port, c->lv2_ControlPort))
-			fp->flags |= FC_PORT_CONTROL;
+			fp->flags |= SPA_FGA_PORT_CONTROL;
 		if (lilv_port_is_a(p->p, port, c->lv2_AudioPort))
-			fp->flags |= FC_PORT_AUDIO;
+			fp->flags |= SPA_FGA_PORT_AUDIO;
 
 		fp->hint = 0;
 		fp->min = mins[i];
@@ -451,60 +452,153 @@ static const struct fc_descriptor *lv2_make_desc(struct fc_plugin *plugin, const
 	return &desc->desc;
 }
 
-static void lv2_unload(struct fc_plugin *plugin)
+static struct spa_fga_plugin_methods impl_plugin = {
+	SPA_VERSION_FGA_PLUGIN_METHODS,
+	.make_desc = lv2_plugin_make_desc,
+};
+
+static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface)
 {
-	struct plugin *p = (struct plugin *)plugin;
+	struct plugin *impl;
+
+	spa_return_val_if_fail(handle != NULL, -EINVAL);
+	spa_return_val_if_fail(interface != NULL, -EINVAL);
+
+	impl = (struct plugin *) handle;
+
+	if (spa_streq(type, SPA_TYPE_INTERFACE_FILTER_GRAPH_AudioPlugin))
+		*interface = &impl->plugin;
+	else
+		return -ENOENT;
+
+	return 0;
+}
+
+static int impl_clear(struct spa_handle *handle)
+{
+	struct plugin *p = (struct plugin *)handle;
 	context_unref(p->c);
-	free(p);
+	return 0;
 }
 
-SPA_EXPORT
-struct fc_plugin *pipewire__filter_chain_plugin_load(const struct spa_support *support, uint32_t n_support,
-		struct dsp_ops *dsp, const char *plugin_uri, const struct spa_dict *info)
+static size_t
+impl_get_size(const struct spa_handle_factory *factory,
+	      const struct spa_dict *params)
 {
-	struct context *c;
+	return sizeof(struct plugin);
+}
+
+static int
+impl_init(const struct spa_handle_factory *factory,
+	  struct spa_handle *handle,
+	  const struct spa_dict *info,
+	  const struct spa_support *support,
+	  uint32_t n_support)
+{
+	struct plugin *impl;
+	uint32_t i;
+	int res;
+	const char *path = NULL;
 	const LilvPlugins *plugins;
-	const LilvPlugin *plugin;
 	LilvNode *uri;
-	int res;
-	struct plugin *p;
 
-	c = context_ref(support, n_support);
-	if (c == NULL)
-		return NULL;
+	handle->get_interface = impl_get_interface;
+	handle->clear = impl_clear;
+
+	impl = (struct plugin *) handle;
+	impl->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
+	impl->data_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataLoop);
+	impl->main_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Loop);
 
-	uri = lilv_new_uri(c->world, plugin_uri);
+	for (i = 0; info && i < info->n_items; i++) {
+		const char *k = info->items[i].key;
+		const char *s = info->items[i].value;
+		if (spa_streq(k, "filter.graph.path"))
+			path = s;
+	}
+	if (path == NULL)
+		return -EINVAL;
+
+	impl->c = context_ref();
+	if (impl->c == NULL)
+		return -EINVAL;
+
+	uri = lilv_new_uri(impl->c->world, path);
 	if (uri == NULL) {
-		pw_log_warn("invalid URI %s", plugin_uri);
+		spa_log_warn(impl->log, "invalid URI %s", path);
 		res = -EINVAL;
-		goto error_unref;
+		goto error_cleanup;
 	}
 
-	plugins = lilv_world_get_all_plugins(c->world);
-	plugin = lilv_plugins_get_by_uri(plugins, uri);
+	plugins = lilv_world_get_all_plugins(impl->c->world);
+	impl->p = lilv_plugins_get_by_uri(plugins, uri);
 	lilv_node_free(uri);
 
-	if (plugin == NULL) {
-		pw_log_warn("can't load plugin %s", plugin_uri);
+	if (impl->p == NULL) {
+		spa_log_warn(impl->log, "can't load plugin %s", path);
 		res = -EINVAL;
-		goto error_unref;
+		goto error_cleanup;
 	}
+	impl->plugin.iface = SPA_INTERFACE_INIT(
+			SPA_TYPE_INTERFACE_FILTER_GRAPH_AudioPlugin,
+			SPA_VERSION_FGA_PLUGIN,
+			&impl_plugin, impl);
 
-	p = calloc(1, sizeof(*p));
-	if (!p) {
-		res = -errno;
-		goto error_unref;
-	}
-	p->p = plugin;
-	p->c = c;
+	return 0;
 
-	p->plugin.make_desc = lv2_make_desc;
-	p->plugin.unload = lv2_unload;
+error_cleanup:
+	if (impl->c)
+		context_unref(impl->c);
+	return res;
+}
 
-	return &p->plugin;
 
-error_unref:
-	context_unref(c);
-	errno = -res;
-	return NULL;
+static const struct spa_interface_info impl_interfaces[] = {
+	{ SPA_TYPE_INTERFACE_FILTER_GRAPH_AudioPlugin },
+};
+
+static int
+impl_enum_interface_info(const struct spa_handle_factory *factory,
+			 const struct spa_interface_info **info,
+			 uint32_t *index)
+{
+	spa_return_val_if_fail(factory != NULL, -EINVAL);
+	spa_return_val_if_fail(info != NULL, -EINVAL);
+	spa_return_val_if_fail(index != NULL, -EINVAL);
+
+	switch (*index) {
+	case 0:
+		*info = &impl_interfaces[*index];
+		break;
+	default:
+		return 0;
+	}
+	(*index)++;
+	return 1;
+}
+
+static struct spa_handle_factory spa_fga_plugin_lv2_factory = {
+	SPA_VERSION_HANDLE_FACTORY,
+	"filter.graph.plugin.lv2",
+	NULL,
+	impl_get_size,
+	impl_init,
+	impl_enum_interface_info,
+};
+
+SPA_EXPORT
+int spa_handle_factory_enum(const struct spa_handle_factory **factory, uint32_t *index)
+{
+	spa_return_val_if_fail(factory != NULL, -EINVAL);
+	spa_return_val_if_fail(index != NULL, -EINVAL);
+
+	switch (*index) {
+	case 0:
+		*factory = &spa_fga_plugin_lv2_factory;
+		break;
+	default:
+		return 0;
+	}
+	(*index)++;
+	return 1;
 }
diff --git a/spa/plugins/filter-graph/meson.build b/spa/plugins/filter-graph/meson.build
new file mode 100644
index 00000000..c346a964
--- /dev/null
+++ b/spa/plugins/filter-graph/meson.build
@@ -0,0 +1,117 @@
+plugin_dependencies = []
+if get_option('spa-plugins').allowed()
+  plugin_dependencies += audioconvert_dep
+endif
+
+simd_cargs = []
+simd_dependencies = []
+
+if have_sse
+  filter_graph_sse = static_library('filter_graph_sse',
+    ['pffft.c',
+     'audio-dsp-sse.c' ],
+    include_directories : [configinc],
+    c_args : [sse_args, '-O3', '-DHAVE_SSE'],
+    dependencies : [ spa_dep ],
+    install : false
+    )
+  simd_cargs += ['-DHAVE_SSE']
+  simd_dependencies += filter_graph_sse
+endif
+if have_avx
+  filter_graph_avx = static_library('filter_graph_avx',
+    ['audio-dsp-avx.c' ],
+    include_directories : [configinc],
+    c_args : [avx_args, fma_args,'-O3', '-DHAVE_AVX'],
+    dependencies : [ spa_dep ],
+    install : false
+    )
+  simd_cargs += ['-DHAVE_AVX']
+  simd_dependencies += filter_graph_avx
+endif
+if have_neon
+  filter_graph_neon = static_library('filter_graph_neon',
+    ['pffft.c' ],
+    c_args : [neon_args, '-O3', '-DHAVE_NEON'],
+    dependencies : [ spa_dep ],
+    install : false
+    )
+  simd_cargs += ['-DHAVE_NEON']
+  simd_dependencies += filter_graph_neon
+endif
+
+filter_graph_c = static_library('filter_graph_c',
+  ['pffft.c',
+   'audio-dsp.c',
+   'audio-dsp-c.c' ],
+  include_directories : [configinc],
+  c_args : [simd_cargs, '-O3', '-DPFFFT_SIMD_DISABLE'],
+  dependencies : [ spa_dep, fftw_dep],
+  install : false
+)
+simd_dependencies += filter_graph_c
+
+spa_filter_graph = shared_library('spa-filter-graph',
+  ['filter-graph.c' ],
+  include_directories : [configinc],
+  dependencies : [ spa_dep, sndfile_dep, plugin_dependencies, mathlib ],
+  install : true,
+  install_dir : spa_plugindir / 'filter-graph',
+  objects : audioconvert_c.extract_objects('biquad.c'),
+  link_with: simd_dependencies
+)
+
+
+filter_graph_dependencies = [
+  spa_dep, mathlib, sndfile_dep, plugin_dependencies
+]
+
+spa_filter_graph_plugin_builtin = shared_library('spa-filter-graph-plugin-builtin',
+  [ 'builtin_plugin.c',
+    'convolver.c' ],
+  include_directories : [configinc],
+  install : true,
+  install_dir : spa_plugindir / 'filter-graph',
+  dependencies : [ filter_graph_dependencies ],
+  objects : audioconvert_c.extract_objects('biquad.c')
+)
+
+spa_filter_graph_plugin_ladspa = shared_library('spa-filter-graph-plugin-ladspa',
+  [ 'ladspa_plugin.c' ],
+  include_directories : [configinc],
+  install : true,
+  install_dir : spa_plugindir / 'filter-graph',
+  dependencies : [ filter_graph_dependencies, dl_lib ]
+)
+
+if libmysofa_dep.found()
+spa_filter_graph_plugin_sofa = shared_library('spa-filter-graph-plugin-sofa',
+  [ 'sofa_plugin.c',
+    'convolver.c' ],
+  include_directories : [configinc],
+  install : true,
+  install_dir : spa_plugindir / 'filter-graph',
+  dependencies : [ filter_graph_dependencies, libmysofa_dep ]
+)
+endif
+
+if lilv_lib.found()
+spa_filter_graph_plugin_lv2 = shared_library('spa-filter-graph-plugin-lv2',
+  [ 'lv2_plugin.c' ],
+  include_directories : [configinc],
+  install : true,
+  install_dir : spa_plugindir / 'filter-graph',
+  dependencies : [ filter_graph_dependencies, lilv_lib ]
+)
+endif
+
+if ebur128_lib.found()
+spa_filter_graph_plugin_ebur128 = shared_library('spa-filter-graph-plugin-ebur128',
+  [ 'ebur128_plugin.c' ],
+  include_directories : [configinc],
+  install : true,
+  install_dir : spa_plugindir / 'filter-graph',
+  dependencies : [ filter_graph_dependencies, lilv_lib, ebur128_lib ]
+)
+endif
+
diff --git a/src/modules/module-filter-chain/pffft.c b/spa/plugins/filter-graph/pffft.c
similarity index 100%
rename from src/modules/module-filter-chain/pffft.c
rename to spa/plugins/filter-graph/pffft.c
diff --git a/src/modules/module-filter-chain/pffft.h b/spa/plugins/filter-graph/pffft.h
similarity index 100%
rename from src/modules/module-filter-chain/pffft.h
rename to spa/plugins/filter-graph/pffft.h
diff --git a/src/modules/module-filter-chain/sofa_plugin.c b/spa/plugins/filter-graph/sofa_plugin.c
similarity index 62%
rename from src/modules/module-filter-chain/sofa_plugin.c
rename to spa/plugins/filter-graph/sofa_plugin.c
index fcc30c89..d348bc97 100644
--- a/src/modules/module-filter-chain/sofa_plugin.c
+++ b/spa/plugins/filter-graph/sofa_plugin.c
@@ -4,19 +4,20 @@
 
 #include <spa/utils/json.h>
 #include <spa/support/loop.h>
+#include <spa/support/log.h>
 
-#include <pipewire/log.h>
-
-#include "plugin.h"
+#include "audio-plugin.h"
 #include "convolver.h"
-#include "dsp-ops.h"
-#include "pffft.h"
+#include "audio-dsp.h"
 
 #include <mysofa.h>
 
 struct plugin {
-	struct fc_plugin plugin;
-	struct dsp_ops *dsp_ops;
+	struct spa_handle handle;
+	struct spa_fga_plugin plugin;
+
+	struct spa_fga_dsp *dsp;
+	struct spa_log *log;
 	struct spa_loop *data_loop;
 	struct spa_loop *main_loop;
 	uint32_t quantum_limit;
@@ -24,6 +25,10 @@ struct plugin {
 
 struct spatializer_impl {
 	struct plugin *plugin;
+
+	struct spa_fga_dsp *dsp;
+	struct spa_log *log;
+
 	unsigned long rate;
 	float *port[6];
 	int n_samples, blocksize, tailsize;
@@ -35,24 +40,25 @@ struct spatializer_impl {
 	struct convolver *r_conv[3];
 };
 
-static void * spatializer_instantiate(const struct fc_plugin *plugin, const struct fc_descriptor * Descriptor,
+static void * spatializer_instantiate(const struct spa_fga_plugin *plugin, const struct spa_fga_descriptor * Descriptor,
 		unsigned long SampleRate, int index, const char *config)
 {
+	struct plugin *pl = SPA_CONTAINER_OF(plugin, struct plugin, plugin);
 	struct spatializer_impl *impl;
-	struct spa_json it[2];
+	struct spa_json it[1];
 	const char *val;
 	char key[256];
 	char filename[PATH_MAX] = "";
+	int len;
 
 	errno = EINVAL;
 	if (config == NULL) {
-		pw_log_error("spatializer: no config was given");
+		spa_log_error(pl->log, "spatializer: no config was given");
 		return NULL;
 	}
 
-	spa_json_init(&it[0], config, strlen(config));
-	if (spa_json_enter_object(&it[0], &it[1]) <= 0) {
-		pw_log_error("spatializer: expected object in config");
+	if (spa_json_begin_object(&it[0], config, strlen(config)) <= 0) {
+		spa_log_error(pl->log, "spatializer: expected object in config");
 		return NULL;
 	}
 
@@ -61,35 +67,36 @@ static void * spatializer_instantiate(const struct fc_plugin *plugin, const stru
 		errno = ENOMEM;
 		return NULL;
 	}
-	impl->plugin = (struct plugin *) plugin;
 
-	while (spa_json_get_string(&it[1], key, sizeof(key)) > 0) {
+	impl->plugin = pl;
+	impl->dsp = pl->dsp;
+	impl->log = pl->log;
+
+	while ((len = spa_json_object_next(&it[0], key, sizeof(key), &val)) > 0) {
 		if (spa_streq(key, "blocksize")) {
-			if (spa_json_get_int(&it[1], &impl->blocksize) <= 0) {
-				pw_log_error("spatializer:blocksize requires a number");
+			if (spa_json_parse_int(val, len, &impl->blocksize) <= 0) {
+				spa_log_error(impl->log, "spatializer:blocksize requires a number");
 				errno = EINVAL;
 				goto error;
 			}
 		}
 		else if (spa_streq(key, "tailsize")) {
-			if (spa_json_get_int(&it[1], &impl->tailsize) <= 0) {
-				pw_log_error("spatializer:tailsize requires a number");
+			if (spa_json_parse_int(val, len, &impl->tailsize) <= 0) {
+				spa_log_error(impl->log, "spatializer:tailsize requires a number");
 				errno = EINVAL;
 				goto error;
 			}
 		}
 		else if (spa_streq(key, "filename")) {
-			if (spa_json_get_string(&it[1], filename, sizeof(filename)) <= 0) {
-				pw_log_error("spatializer:filename requires a string");
+			if (spa_json_parse_stringn(val, len, filename, sizeof(filename)) <= 0) {
+				spa_log_error(impl->log, "spatializer:filename requires a string");
 				errno = EINVAL;
 				goto error;
 			}
 		}
-		else if (spa_json_next(&it[1], &val) < 0)
-			break;
 	}
 	if (!filename[0]) {
-		pw_log_error("spatializer:filename was not given");
+		spa_log_error(impl->log, "spatializer:filename was not given");
 		errno = EINVAL;
 		goto error;
 	}
@@ -167,7 +174,7 @@ static void * spatializer_instantiate(const struct fc_plugin *plugin, const stru
 			reason = "Internal error";
 			break;
 		}
-		pw_log_error("Unable to load HRTF from %s: %s (%d)", filename, reason, ret);
+		spa_log_error(impl->log, "Unable to load HRTF from %s: %s (%d)", filename, reason, ret);
 		goto error;
 	}
 
@@ -176,7 +183,7 @@ static void * spatializer_instantiate(const struct fc_plugin *plugin, const stru
 	if (impl->tailsize <= 0)
 		impl->tailsize = SPA_CLAMP(4096, impl->blocksize, 32768);
 
-	pw_log_info("using n_samples:%u %d:%d blocksize sofa:%s", impl->n_samples,
+	spa_log_info(impl->log, "using n_samples:%u %d:%d blocksize sofa:%s", impl->n_samples,
 		impl->blocksize, impl->tailsize, filename);
 
 	impl->tmp[0] = calloc(impl->plugin->quantum_limit, sizeof(float));
@@ -220,7 +227,7 @@ static void spatializer_reload(void * Instance)
 	for (uint8_t i = 0; i < 3; i++)
 		coords[i] = impl->port[3 + i][0];
 
-	pw_log_info("making spatializer with %f %f %f", coords[0], coords[2], coords[2]);
+	spa_log_info(impl->log, "making spatializer with %f %f %f", coords[0], coords[1], coords[2]);
 
 	mysofa_s2c(coords);
 	mysofa_getfilter_float(
@@ -236,23 +243,23 @@ static void spatializer_reload(void * Instance)
 
 	// TODO: make use of delay
 	if ((left_delay != 0.0f || right_delay != 0.0f) && (!isnan(left_delay) || !isnan(right_delay)))
-		pw_log_warn("delay dropped l: %f, r: %f", left_delay, right_delay);
+		spa_log_warn(impl->log, "delay dropped l: %f, r: %f", left_delay, right_delay);
 
 	if (impl->l_conv[2])
 		convolver_free(impl->l_conv[2]);
 	if (impl->r_conv[2])
 		convolver_free(impl->r_conv[2]);
 
-	impl->l_conv[2] = convolver_new(impl->plugin->dsp_ops, impl->blocksize, impl->tailsize,
+	impl->l_conv[2] = convolver_new(impl->dsp, impl->blocksize, impl->tailsize,
 			left_ir, impl->n_samples);
-	impl->r_conv[2] = convolver_new(impl->plugin->dsp_ops, impl->blocksize, impl->tailsize,
+	impl->r_conv[2] = convolver_new(impl->dsp, impl->blocksize, impl->tailsize,
 			right_ir, impl->n_samples);
 
 	free(left_ir);
 	free(right_ir);
 
 	if (impl->l_conv[2] == NULL || impl->r_conv[2] == NULL) {
-		pw_log_error("reloading left or right convolver failed");
+		spa_log_error(impl->log, "reloading left or right convolver failed");
 		return;
 	}
 	spa_loop_invoke(impl->plugin->data_loop, do_switch, 1, NULL, 0, true, impl);
@@ -349,38 +356,38 @@ static void spatializer_deactivate(void * Instance)
 	impl->interpolate = false;
 }
 
-static struct fc_port spatializer_ports[] = {
+static struct spa_fga_port spatializer_ports[] = {
 	{ .index = 0,
 	  .name = "Out L",
-	  .flags = FC_PORT_OUTPUT | FC_PORT_AUDIO,
+	  .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO,
 	},
 	{ .index = 1,
 	  .name = "Out R",
-	  .flags = FC_PORT_OUTPUT | FC_PORT_AUDIO,
+	  .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO,
 	},
 	{ .index = 2,
 	  .name = "In",
-	  .flags = FC_PORT_INPUT | FC_PORT_AUDIO,
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO,
 	},
 
 	{ .index = 3,
 	  .name = "Azimuth",
-	  .flags = FC_PORT_INPUT | FC_PORT_CONTROL,
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL,
 	  .def = 0.0f, .min = 0.0f, .max = 360.0f
 	},
 	{ .index = 4,
 	  .name = "Elevation",
-	  .flags = FC_PORT_INPUT | FC_PORT_CONTROL,
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL,
 	  .def = 0.0f, .min = -90.0f, .max = 90.0f
 	},
 	{ .index = 5,
 	  .name = "Radius",
-	  .flags = FC_PORT_INPUT | FC_PORT_CONTROL,
+	  .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL,
 	  .def = 1.0f, .min = 0.0f, .max = 100.0f
 	},
 };
 
-static const struct fc_descriptor spatializer_desc = {
+static const struct spa_fga_descriptor spatializer_desc = {
 	.name = "spatializer",
 
 	.n_ports = 6,
@@ -394,7 +401,7 @@ static const struct fc_descriptor spatializer_desc = {
 	.cleanup = spatializer_cleanup,
 };
 
-static const struct fc_descriptor * sofa_descriptor(unsigned long Index)
+static const struct spa_fga_descriptor * sofa_descriptor(unsigned long Index)
 {
 	switch(Index) {
 	case 0:
@@ -404,11 +411,11 @@ static const struct fc_descriptor * sofa_descriptor(unsigned long Index)
 }
 
 
-static const struct fc_descriptor *sofa_make_desc(struct fc_plugin *plugin, const char *name)
+static const struct spa_fga_descriptor *sofa_plugin_make_desc(void *plugin, const char *name)
 {
 	unsigned long i;
 	for (i = 0; ;i++) {
-		const struct fc_descriptor *d = sofa_descriptor(i);
+		const struct spa_fga_descriptor *d = sofa_descriptor(i);
 		if (d == NULL)
 			break;
 		if (spa_streq(d->name, name))
@@ -417,33 +424,132 @@ static const struct fc_descriptor *sofa_make_desc(struct fc_plugin *plugin, cons
 	return NULL;
 }
 
-static void sofa_plugin_unload(struct fc_plugin *p)
+static struct spa_fga_plugin_methods impl_plugin = {
+	SPA_VERSION_FGA_PLUGIN_METHODS,
+	.make_desc = sofa_plugin_make_desc,
+};
+
+static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface)
 {
-	free(p);
+	struct plugin *impl;
+
+	spa_return_val_if_fail(handle != NULL, -EINVAL);
+	spa_return_val_if_fail(interface != NULL, -EINVAL);
+
+	impl = (struct plugin *) handle;
+
+	if (spa_streq(type, SPA_TYPE_INTERFACE_FILTER_GRAPH_AudioPlugin))
+		*interface = &impl->plugin;
+	else
+		return -ENOENT;
+
+	return 0;
 }
 
-SPA_EXPORT
-struct fc_plugin *pipewire__filter_chain_plugin_load(const struct spa_support *support, uint32_t n_support,
-		struct dsp_ops *dsp, const char *plugin, const struct spa_dict *info)
+static int impl_clear(struct spa_handle *handle)
+{
+	return 0;
+}
+
+static size_t
+impl_get_size(const struct spa_handle_factory *factory,
+	      const struct spa_dict *params)
+{
+	return sizeof(struct plugin);
+}
+
+static int
+impl_init(const struct spa_handle_factory *factory,
+	  struct spa_handle *handle,
+	  const struct spa_dict *info,
+	  const struct spa_support *support,
+	  uint32_t n_support)
 {
-	struct plugin *impl = calloc(1, sizeof (struct plugin));
+	struct plugin *impl;
 
-	impl->plugin.make_desc = sofa_make_desc;
-	impl->plugin.unload = sofa_plugin_unload;
+	handle->get_interface = impl_get_interface;
+	handle->clear = impl_clear;
+
+	impl = (struct plugin *) handle;
+
+	impl->plugin.iface = SPA_INTERFACE_INIT(
+			SPA_TYPE_INTERFACE_FILTER_GRAPH_AudioPlugin,
+			SPA_VERSION_FGA_PLUGIN,
+			&impl_plugin, impl);
 
 	impl->quantum_limit = 8192u;
 
+	impl->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
+	impl->data_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataLoop);
+	impl->main_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Loop);
+	impl->dsp = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_FILTER_GRAPH_AudioDSP);
+
 	for (uint32_t i = 0; info && i < info->n_items; i++) {
 		const char *k = info->items[i].key;
 		const char *s = info->items[i].value;
 		if (spa_streq(k, "clock.quantum-limit"))
 			spa_atou32(s, &impl->quantum_limit, 0);
+		if (spa_streq(k, "filter.graph.audio.dsp"))
+			sscanf(s, "pointer:%p", &impl->dsp);
 	}
-	impl->dsp_ops = dsp;
-	pffft_select_cpu(dsp->cpu_flags);
 
-	impl->data_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataLoop);
-	impl->main_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Loop);
+	if (impl->data_loop == NULL || impl->main_loop == NULL) {
+		spa_log_error(impl->log, "%p: could not find a data/main loop", impl);
+		return -EINVAL;
+	}
+	if (impl->dsp == NULL) {
+		spa_log_error(impl->log, "%p: could not find DSP functions", impl);
+		return -EINVAL;
+	}
+	return 0;
+}
+
+static const struct spa_interface_info impl_interfaces[] = {
+	{SPA_TYPE_INTERFACE_FILTER_GRAPH_AudioPlugin,},
+};
+
+static int
+impl_enum_interface_info(const struct spa_handle_factory *factory,
+			 const struct spa_interface_info **info,
+			 uint32_t *index)
+{
+	spa_return_val_if_fail(factory != NULL, -EINVAL);
+	spa_return_val_if_fail(info != NULL, -EINVAL);
+	spa_return_val_if_fail(index != NULL, -EINVAL);
+
+	switch (*index) {
+	case 0:
+		*info = &impl_interfaces[*index];
+		break;
+	default:
+		return 0;
+	}
+	(*index)++;
+	return 1;
+}
+
+static struct spa_handle_factory spa_fga_sofa_plugin_factory = {
+	SPA_VERSION_HANDLE_FACTORY,
+	"filter.graph.plugin.sofa",
+	NULL,
+	impl_get_size,
+	impl_init,
+	impl_enum_interface_info,
+};
+
+SPA_EXPORT
+int spa_handle_factory_enum(const struct spa_handle_factory **factory, uint32_t *index)
+{
+	spa_return_val_if_fail(factory != NULL, -EINVAL);
+	spa_return_val_if_fail(index != NULL, -EINVAL);
 
-	return (struct fc_plugin *) impl;
+	switch (*index) {
+	case 0:
+		*factory = &spa_fga_sofa_plugin_factory;
+		break;
+	default:
+		return 0;
+	}
+	(*index)++;
+	return 1;
 }
diff --git a/spa/plugins/libcamera/libcamera-device.cpp b/spa/plugins/libcamera/libcamera-device.cpp
index 25a9e2fa..5eb46a1a 100644
--- a/spa/plugins/libcamera/libcamera-device.cpp
+++ b/spa/plugins/libcamera/libcamera-device.cpp
@@ -8,6 +8,8 @@
 
 #include <stddef.h>
 
+#include <sstream>
+
 #include <spa/support/plugin.h>
 #include <spa/support/log.h>
 #include <spa/support/loop.h>
@@ -53,32 +55,25 @@ struct impl {
 
 }
 
-static const libcamera::Span<const int64_t> cameraDevice(
-			const Camera *camera)
+static const libcamera::Span<const int64_t> cameraDevice(const Camera& camera)
 {
-	const ControlList &props = camera->properties();
-
-	if (auto devices = props.get(properties::SystemDevices))
+	if (auto devices = camera.properties().get(properties::SystemDevices))
 		return devices.value();
 
 	return {};
 }
 
-static std::string cameraModel(const Camera *camera)
+static std::string cameraModel(const Camera& camera)
 {
-	const ControlList &props = camera->properties();
-
-	if (auto model = props.get(properties::Model))
+	if (auto model = camera.properties().get(properties::Model))
 		return std::move(model.value());
 
-	return camera->id();
+	return camera.id();
 }
 
-static const char *cameraLoc(const Camera *camera)
+static const char *cameraLoc(const Camera& camera)
 {
-	const ControlList &props = camera->properties();
-
-	if (auto location = props.get(properties::Location)) {
+	if (auto location = camera.properties().get(properties::Location)) {
 		switch (location.value()) {
 		case properties::CameraLocationFront:
 			return "front";
@@ -92,11 +87,9 @@ static const char *cameraLoc(const Camera *camera)
 	return nullptr;
 }
 
-static const char *cameraRot(const Camera *camera)
+static const char *cameraRot(const Camera& camera)
 {
-	const ControlList &props = camera->properties();
-
-	if (auto rotation = props.get(properties::Rotation)) {
+	if (auto rotation = camera.properties().get(properties::Rotation)) {
 		switch (rotation.value()) {
 		case 90:
 			return "90";
@@ -119,44 +112,48 @@ static int emit_info(struct impl *impl, bool full)
 	uint32_t n_items = 0;
 	struct spa_device_info info;
 	struct spa_param_info params[2];
-	char path[256], name[256], devices_str[256];
-	struct spa_strbuf buf;
+	Camera& camera = *impl->camera;
 
 	info = SPA_DEVICE_INFO_INIT();
 
 	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->device_id.c_str());
-	ADD_ITEM(SPA_KEY_OBJECT_PATH, path);
+
+	const auto path = "libcamera:" + impl->device_id;
+	ADD_ITEM(SPA_KEY_OBJECT_PATH, path.c_str());
+
 	ADD_ITEM(SPA_KEY_DEVICE_API, "libcamera");
 	ADD_ITEM(SPA_KEY_MEDIA_CLASS, "Video/Device");
 	ADD_ITEM(SPA_KEY_API_LIBCAMERA_PATH, impl->device_id.c_str());
 
-	if (auto location = cameraLoc(impl->camera.get()))
+	if (auto location = cameraLoc(camera))
 		ADD_ITEM(SPA_KEY_API_LIBCAMERA_LOCATION, location);
-	if (auto rotation = cameraRot(impl->camera.get()))
+	if (auto rotation = cameraRot(camera))
 		ADD_ITEM(SPA_KEY_API_LIBCAMERA_ROTATION, rotation);
 
-	const auto model = cameraModel(impl->camera.get());
+	const auto model = cameraModel(camera);
 	ADD_ITEM(SPA_KEY_DEVICE_PRODUCT_NAME, model.c_str());
 	ADD_ITEM(SPA_KEY_DEVICE_DESCRIPTION, model.c_str());
 
-	snprintf(name, sizeof(name), "libcamera_device.%s", impl->device_id.c_str());
-	ADD_ITEM(SPA_KEY_DEVICE_NAME, name);
+	const auto name = "libcamera_device." + impl->device_id;
+	ADD_ITEM(SPA_KEY_DEVICE_NAME, name.c_str());
 
-	auto device_numbers = cameraDevice(impl->camera.get());
+	auto device_numbers = cameraDevice(camera);
+	std::string devids;
 
 	if (!device_numbers.empty()) {
-		spa_strbuf_init(&buf, devices_str, sizeof(devices_str));
+		std::ostringstream s;
+
 
 		/* encode device numbers into a json array */
-		spa_strbuf_append(&buf, "[ ");
-		for(int64_t device_number : device_numbers)
-			spa_strbuf_append(&buf, "%" PRId64 " ", device_number);
+		s << "[ ";
+		for (const auto& devid : device_numbers)
+			s << devid << ' ';
+		s << ']';
 
-		spa_strbuf_append(&buf, "]");
-		ADD_ITEM(SPA_KEY_DEVICE_DEVIDS, devices_str);
+		devids = std::move(s).str();
+		ADD_ITEM(SPA_KEY_DEVICE_DEVIDS, devids.c_str());
 	}
 
 #undef ADD_ITEM
diff --git a/spa/plugins/libcamera/libcamera-source.cpp b/spa/plugins/libcamera/libcamera-source.cpp
index f9a1adbc..f570499d 100644
--- a/spa/plugins/libcamera/libcamera-source.cpp
+++ b/spa/plugins/libcamera/libcamera-source.cpp
@@ -19,6 +19,7 @@
 #include <spa/utils/result.h>
 #include <spa/utils/string.h>
 #include <spa/utils/ringbuffer.h>
+#include <spa/utils/dll.h>
 #include <spa/monitor/device.h>
 #include <spa/node/node.h>
 #include <spa/node/io.h>
@@ -69,6 +70,7 @@ struct port {
 	StreamConfiguration streamConfig;
 
 	uint32_t memtype = 0;
+	uint32_t buffers_blocks = 1;
 
 	struct buffer buffers[MAX_BUFFERS];
 	uint32_t n_buffers = 0;
@@ -166,6 +168,8 @@ struct 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);
+
+	struct spa_dll dll;
 };
 
 }
@@ -359,6 +363,8 @@ static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size)
 	switch (id) {
 	case SPA_IO_Clock:
 		impl->clock = (struct spa_io_clock*)data;
+		if (impl->clock)
+			SPA_FLAG_SET(impl->clock->flags, SPA_IO_CLOCK_FLAG_NO_RATE);
 		break;
 	case SPA_IO_Position:
 		impl->position = (struct spa_io_position*)data;
@@ -553,7 +559,7 @@ next:
 		param = (struct spa_pod*)spa_pod_builder_add_object(&b,
 			SPA_TYPE_OBJECT_ParamBuffers, id,
 			SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(n_buffers, n_buffers, n_buffers),
-			SPA_PARAM_BUFFERS_blocks,  SPA_POD_Int(1),
+			SPA_PARAM_BUFFERS_blocks,  SPA_POD_Int(port->buffers_blocks),
 			SPA_PARAM_BUFFERS_size,    SPA_POD_Int(port->streamConfig.frameSize),
 			SPA_PARAM_BUFFERS_stride,  SPA_POD_Int(port->streamConfig.stride));
 		break;
diff --git a/spa/plugins/libcamera/libcamera-utils.cpp b/spa/plugins/libcamera/libcamera-utils.cpp
index 4a5e0f67..92185c4b 100644
--- a/spa/plugins/libcamera/libcamera-utils.cpp
+++ b/spa/plugins/libcamera/libcamera-utils.cpp
@@ -109,6 +109,30 @@ static int allocBuffers(struct impl *impl, struct port *port, unsigned int count
 		}
 		impl->requestPool.push_back(std::move(request));
 	}
+
+	/* Some devices require data for each output video frame to be
+	 * placed in discontiguous memory buffers. In such cases, one
+	 * video frame has to be addressed using more than one memory.
+	 * address. Therefore, need calculate the number of discontiguous
+	 * memory and allocate the specified amount of memory */
+	Stream *stream = impl->config->at(0).stream();
+	const std::vector<std::unique_ptr<FrameBuffer>> &bufs =
+			impl->allocator->buffers(stream);
+	const std::vector<libcamera::FrameBuffer::Plane> &planes = bufs[0]->planes();
+	int fd = -1;
+	uint32_t buffers_blocks = 0;
+
+	for (const FrameBuffer::Plane &plane : planes) {
+		const int current_fd = plane.fd.get();
+		if (current_fd >= 0 && current_fd != fd) {
+			buffers_blocks += 1;
+			fd = current_fd;
+		}
+	}
+
+	if (buffers_blocks > 0) {
+		port->buffers_blocks = buffers_blocks;
+	}
 	return res;
 }
 
@@ -599,7 +623,7 @@ static int do_update_ctrls(struct spa_loop *loop,
 		impl->ctrls.set(d->id, d->f_val);
 		break;
 	case ControlTypeInteger32:
-		//impl->ctrls.set(d->id, (int32_t)d->i_val);
+		impl->ctrls.set(d->id, (int32_t)d->i_val);
 		break;
 	default:
 		break;
@@ -798,17 +822,40 @@ mmap_init(struct impl *impl, struct port *port,
 			d[j].type = port->memtype;
 			d[j].flags = SPA_DATA_FLAG_READABLE;
 			d[j].mapoffset = 0;
-			d[j].maxsize = port->streamConfig.frameSize;
-			d[j].chunk->offset = 0;
-			d[j].chunk->size = port->streamConfig.frameSize;
 			d[j].chunk->stride = port->streamConfig.stride;
 			d[j].chunk->flags = 0;
+			/* Update parameters according to the plane information */
+			unsigned int numPlanes = bufs[i]->planes().size();
+			if (buffers[i]->n_datas < numPlanes) {
+				if (j < buffers[i]->n_datas - 1) {
+					d[j].maxsize = bufs[i]->planes()[j].length;
+					d[j].chunk->offset = bufs[i]->planes()[j].offset;
+					d[j].chunk->size = bufs[i]->planes()[j].length;
+				} else {
+					d[j].chunk->offset = bufs[i]->planes()[j].offset;
+					for (uint8_t k = j; k < numPlanes; k++) {
+						d[j].maxsize += bufs[i]->planes()[k].length;
+						d[j].chunk->size += bufs[i]->planes()[k].length;
+					}
+				}
+			} else if (buffers[i]->n_datas == numPlanes) {
+				d[j].maxsize = bufs[i]->planes()[j].length;
+				d[j].chunk->offset = bufs[i]->planes()[j].offset;
+				d[j].chunk->size = bufs[i]->planes()[j].length;
+			} else {
+				spa_log_warn(impl->log, "buffer index: i: %d, data member "
+					"numbers: %d is greater than plane number: %d",
+					i, buffers[i]->n_datas, numPlanes);
+				d[j].maxsize = port->streamConfig.frameSize;
+				d[j].chunk->offset = 0;
+				d[j].chunk->size = port->streamConfig.frameSize;
+			}
 
 			if (port->memtype == SPA_DATA_DmaBuf ||
 			    port->memtype == SPA_DATA_MemFd) {
 				d[j].flags |= SPA_DATA_FLAG_MAPPABLE;
 				d[j].fd = bufs[i]->planes()[j].fd.get();
-				spa_log_debug(impl->log, "Got fd = %ld for buffer: #%d", d[j].fd, i);
+				spa_log_debug(impl->log, "Got fd = %" PRId64 " for buffer: #%d", d[j].fd, i);
 				d[j].data = NULL;
 				SPA_FLAG_SET(b->flags, BUFFER_FLAG_ALLOCATED);
 			}
@@ -884,6 +931,18 @@ void impl::requestComplete(libcamera::Request *request)
 	const FrameMetadata &fmd = buffer->metadata();
 
 	if (impl->clock) {
+		double target = (double)port->info.rate.num / port->info.rate.denom;
+		double corr;
+
+		if (impl->dll.bw == 0.0) {
+			spa_dll_set_bw(&impl->dll, SPA_DLL_BW_MAX, port->info.rate.denom, port->info.rate.denom);
+			impl->clock->next_nsec = fmd.timestamp;
+			corr = 1.0;
+		} else {
+			double diff = ((double)impl->clock->next_nsec - (double)fmd.timestamp) / SPA_NSEC_PER_SEC;
+			double error = port->info.rate.denom * (diff - target);
+			corr = spa_dll_update(&impl->dll, SPA_CLAMPD(error, -128., 128.));
+		}
 		/* FIXME, we should follow the driver clock and target_ values.
 		 * for now we ignore and use our own. */
 		impl->clock->target_rate = port->rate;
@@ -894,8 +953,8 @@ void impl::requestComplete(libcamera::Request *request)
 		impl->clock->position = fmd.sequence;
 		impl->clock->duration = 1;
 		impl->clock->delay = 0;
-		impl->clock->rate_diff = 1.0;
-		impl->clock->next_nsec = fmd.timestamp;
+		impl->clock->rate_diff = corr;
+		impl->clock->next_nsec += (uint64_t) (target * SPA_NSEC_PER_SEC * corr);
 	}
 	if (b->h) {
 		b->h->flags = 0;
@@ -940,6 +999,8 @@ static int spa_libcamera_stream_on(struct impl *impl)
 	}
 	impl->pendingRequests.clear();
 
+	impl->dll.bw = 0.0;
+
 	impl->source.func = libcamera_on_fd_events;
 	impl->source.data = impl;
 	impl->source.fd = spa_system_eventfd_create(impl->system, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK);
diff --git a/spa/plugins/meson.build b/spa/plugins/meson.build
index 97ba78af..42aec7ed 100644
--- a/spa/plugins/meson.build
+++ b/spa/plugins/meson.build
@@ -55,3 +55,4 @@ if libcamera_dep.found()
 endif
 
 subdir('aec')
+subdir('filter-graph')
diff --git a/spa/plugins/support/cpu-riscv.c b/spa/plugins/support/cpu-riscv.c
new file mode 100644
index 00000000..e6ffa0ad
--- /dev/null
+++ b/spa/plugins/support/cpu-riscv.c
@@ -0,0 +1,29 @@
+/* Spa */
+/* SPDX-FileCopyrightText: Copyright (c) 2023 Institue of Software Chinese Academy of Sciences (ISCAS). */
+/* SPDX-License-Identifier: MIT */
+
+#ifdef HAVE_SYS_AUXV_H
+#include <sys/auxv.h>
+#define HWCAP_RV(letter) (1ul << ((letter) - 'A'))
+#endif
+
+static int
+riscv_init(struct impl *impl)
+{
+	uint32_t flags = 0;
+
+#ifdef HAVE_SYS_AUXV_H
+	const unsigned long hwcap = getauxval(AT_HWCAP);
+	if (hwcap & HWCAP_RV('V'))
+		flags |= SPA_CPU_FLAG_RISCV_V;
+#endif
+
+	impl->flags = flags;
+
+	return 0;
+}
+
+static int riscv_zero_denormals(void *object, bool enable)
+{
+	return 0;
+}
diff --git a/spa/plugins/support/cpu.c b/spa/plugins/support/cpu.c
index 6db8278e..c2c419f7 100644
--- a/spa/plugins/support/cpu.c
+++ b/spa/plugins/support/cpu.c
@@ -2,6 +2,8 @@
 /* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */
 /* SPDX-License-Identifier: MIT */
 
+#include "config.h"
+
 #include <stddef.h>
 #include <unistd.h>
 #include <string.h>
@@ -64,6 +66,10 @@ static char *spa_cpu_read_file(const char *name, char *buffer, size_t len)
 #include "cpu-arm.c"
 #define init(t)	arm_init(t)
 #define impl_cpu_zero_denormals arm_zero_denormals
+# elif defined (__riscv)
+#include "cpu-riscv.c"
+#define init(t)	riscv_init(t)
+#define impl_cpu_zero_denormals riscv_zero_denormals
 # else
 #define init(t)
 #define impl_cpu_zero_denormals NULL
diff --git a/spa/plugins/support/logger.c b/spa/plugins/support/logger.c
index 1df503cc..c6e6ca4b 100644
--- a/spa/plugins/support/logger.c
+++ b/spa/plugins/support/logger.c
@@ -22,6 +22,10 @@
 
 #if defined(__FreeBSD__) || defined(__MidnightBSD__)
 #define CLOCK_MONOTONIC_RAW CLOCK_MONOTONIC
+#elif defined(_MSC_VER)
+static inline void setlinebuf(FILE* stream) {
+	setvbuf(stream, NULL, _IOLBF, 0);
+}
 #endif
 
 #undef SPA_LOG_TOPIC_DEFAULT
@@ -44,9 +48,12 @@ struct impl {
 	struct spa_ringbuffer trace_rb;
 	uint8_t trace_data[TRACE_BUFFER];
 
+	clockid_t clock_id;
+
 	unsigned int have_source:1;
 	unsigned int colors:1;
 	unsigned int timestamp:1;
+	unsigned int local_timestamp:1;
 	unsigned int line:1;
 };
 
@@ -63,7 +70,7 @@ impl_log_logtv(void *object,
 #define RESERVED_LENGTH 24
 
 	struct impl *impl = object;
-	char timestamp[15] = {0};
+	char timestamp[18] = {0};
 	char topicstr[32] = {0};
 	char filename[64] = {0};
 	char location[1000 + RESERVED_LENGTH], *p, *s;
@@ -89,9 +96,19 @@ impl_log_logtv(void *object,
 	p = location;
 	len = sizeof(location) - RESERVED_LENGTH;
 
-	if (impl->timestamp) {
+	if (impl->local_timestamp) {
+		char buf[64];
 		struct timespec now;
-		clock_gettime(CLOCK_MONOTONIC_RAW, &now);
+		struct tm now_tm;
+
+		clock_gettime(impl->clock_id, &now);
+		localtime_r(&now.tv_sec, &now_tm);
+		strftime(buf, sizeof(buf), "%H:%M:%S", &now_tm);
+		spa_scnprintf(timestamp, sizeof(timestamp), "[%s.%06d]", buf,
+				(int)(now.tv_nsec / SPA_NSEC_PER_USEC));
+	} else if (impl->timestamp) {
+		struct timespec now;
+		clock_gettime(impl->clock_id, &now);
 		spa_scnprintf(timestamp, sizeof(timestamp), "[%05jd.%06jd]",
 			(intmax_t) (now.tv_sec & 0x1FFFFFFF) % 100000, (intmax_t) now.tv_nsec / 1000);
 	}
@@ -315,8 +332,20 @@ impl_init(const struct spa_handle_factory *factory,
 		}
 	}
 	if (info) {
-		if ((str = spa_dict_lookup(info, SPA_KEY_LOG_TIMESTAMP)) != NULL)
-			this->timestamp = spa_atob(str);
+		str = spa_dict_lookup(info, SPA_KEY_LOG_TIMESTAMP);
+		if (spa_atob(str) || spa_streq(str, "local")) {
+			this->clock_id = CLOCK_REALTIME;
+			this->local_timestamp = true;
+		} else if (spa_streq(str, "monotonic")) {
+			this->clock_id = CLOCK_MONOTONIC;
+			this->timestamp = true;
+		} else if (spa_streq(str, "monotonic-raw")) {
+			this->clock_id = CLOCK_MONOTONIC_RAW;
+			this->timestamp = true;
+		} else if (spa_streq(str, "realtime")) {
+			this->clock_id = CLOCK_REALTIME;
+			this->timestamp = true;
+		}
 		if ((str = spa_dict_lookup(info, SPA_KEY_LOG_LINE)) != NULL)
 			this->line = spa_atob(str);
 		if ((str = spa_dict_lookup(info, SPA_KEY_LOG_COLORS)) != NULL) {
diff --git a/spa/plugins/support/loop.c b/spa/plugins/support/loop.c
index 2d2ca865..8af61e7a 100644
--- a/spa/plugins/support/loop.c
+++ b/spa/plugins/support/loop.c
@@ -10,6 +10,7 @@
 #include <stdio.h>
 #include <pthread.h>
 #include <threads.h>
+#include <stdatomic.h>
 
 #include <spa/support/loop.h>
 #include <spa/support/system.h>
@@ -36,6 +37,12 @@ SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.loop");
 #define DATAS_SIZE	(4096*8)
 #define MAX_EP		32
 
+/* the number of concurrent queues for invoke. This is also the number
+ * of threads that can concurrently invoke. When there are more, the
+ * retry timeout will be used to retry. */
+#define QUEUES_MAX	128
+#define DEFAULT_RETRY	(1 * SPA_USEC_PER_SEC)
+
 /** \cond */
 
 struct invoke_item {
@@ -52,6 +59,17 @@ struct invoke_item {
 
 static int loop_signal_event(void *object, struct spa_source *source);
 
+struct queue;
+
+#define IDX_INVALID	((uint16_t)0xffff)
+union tag {
+	struct {
+		uint16_t idx;
+		uint16_t count;
+	} t;
+	uint32_t v;
+};
+
 struct impl {
 	struct spa_handle handle;
 	struct spa_loop loop;
@@ -63,17 +81,23 @@ struct impl {
 
 	struct spa_list source_list;
 	struct spa_list destroy_list;
-	struct spa_list queue_list;
 	struct spa_hook_list hooks_list;
 
+	struct spa_ratelimit rate_limit;
+	int retry_timeout;
+
+	union tag head;
+
+	uint32_t n_queues;
+	struct queue *queues[QUEUES_MAX];
+	pthread_mutex_t queue_lock;
+
 	int poll_fd;
 	pthread_t thread;
 	int enter_count;
 
 	struct spa_source *wakeup;
 
-	tss_t queue_tss_id;
-	pthread_mutex_t queue_lock;
 	uint32_t count;
 	uint32_t flush_count;
 
@@ -82,15 +106,14 @@ struct impl {
 
 struct queue {
 	struct impl *impl;
-	struct spa_list link;
 
-#define QUEUE_FLAG_NONE		(0)
-#define QUEUE_FLAG_ACK_FD	(1<<0)
-	uint32_t flags;
-	struct queue *overflow;
+	uint16_t idx;
+	uint16_t next;
 
 	int ack_fd;
-	struct spa_ratelimit rate_limit;
+	bool close_fd;
+
+	struct queue *overflow;
 
 	struct spa_ringbuffer buffer;
 	uint8_t *buffer_data;
@@ -175,7 +198,23 @@ static int loop_remove_source(void *object, struct spa_source *source)
 	return res;
 }
 
-static struct queue *loop_create_queue(void *object, uint32_t flags)
+static void loop_queue_destroy(void *data)
+{
+	struct queue *queue = data;
+	struct impl *impl = queue->impl;
+
+	if (queue->close_fd)
+		spa_system_close(impl->system, queue->ack_fd);
+
+	if (queue->overflow)
+		loop_queue_destroy(queue->overflow);
+
+	spa_log_info(impl->log, "%p destroyed queue %p idx:%d", impl, queue, queue->idx);
+
+	free(queue);
+}
+
+static struct queue *loop_create_queue(void *object, bool with_fd)
 {
 	struct impl *impl = object;
 	struct queue *queue;
@@ -185,16 +224,14 @@ static struct queue *loop_create_queue(void *object, uint32_t flags)
 	if (queue == NULL)
 		return NULL;
 
+	queue->idx = IDX_INVALID;
+	queue->next = IDX_INVALID;
 	queue->impl = impl;
-	queue->flags = flags;
-
-	queue->rate_limit.interval = 2 * SPA_NSEC_PER_SEC;
-	queue->rate_limit.burst = 1;
 
 	queue->buffer_data = SPA_PTR_ALIGN(queue->buffer_mem, MAX_ALIGN, uint8_t);
 	spa_ringbuffer_init(&queue->buffer);
 
-	if (flags & QUEUE_FLAG_ACK_FD) {
+	if (with_fd) {
 		if ((res = spa_system_eventfd_create(impl->system,
 				SPA_FD_EVENT_SEMAPHORE | SPA_FD_CLOEXEC)) < 0) {
 			spa_log_error(impl->log, "%p: can't create ack event: %s",
@@ -202,25 +239,82 @@ static struct queue *loop_create_queue(void *object, uint32_t flags)
 			goto error;
 		}
 		queue->ack_fd = res;
-	} else {
-		queue->ack_fd = -1;
+		queue->close_fd = true;
+
+		while (true) {
+			uint16_t idx = SPA_ATOMIC_LOAD(impl->n_queues);
+			if (idx >= QUEUES_MAX) {
+				/* this is pretty bad, there are QUEUES_MAX concurrent threads
+				 * that are doing an invoke */
+				spa_log_error(impl->log, "max queues %d exceeded!", idx);
+				res = -ENOSPC;
+				goto error;
+			}
+			queue->idx = idx;
+			if (SPA_ATOMIC_CAS(impl->queues[queue->idx], NULL, queue)) {
+				SPA_ATOMIC_INC(impl->n_queues);
+				break;
+			}
+		}
 	}
-
-	pthread_mutex_lock(&impl->queue_lock);
-	spa_list_append(&impl->queue_list, &queue->link);
-	pthread_mutex_unlock(&impl->queue_lock);
-
-	spa_log_info(impl->log, "%p created queue %p", impl, queue);
+	spa_log_info(impl->log, "%p created queue %p idx:%d %p", impl, queue, queue->idx,
+			(void*)pthread_self());
 
 	return queue;
 
 error:
-	free(queue);
+	loop_queue_destroy(queue);
 	errno = -res;
 	return NULL;
 }
 
 
+static inline struct queue *get_queue(struct impl *impl)
+{
+	union tag head, next;
+
+	head.v = SPA_ATOMIC_LOAD(impl->head.v);
+
+	while (true) {
+		struct queue *queue;
+
+		if (SPA_UNLIKELY(head.t.idx == IDX_INVALID))
+			return NULL;
+
+		queue = impl->queues[head.t.idx];
+		next.t.idx = queue->next;
+		next.t.count = head.t.count+1;
+
+		if (SPA_LIKELY(__atomic_compare_exchange_n(&impl->head.v, &head.v, next.v,
+						0, __ATOMIC_ACQ_REL, __ATOMIC_RELAXED))) {
+			spa_log_trace(impl->log, "%p idx:%d %p", queue, queue->idx, (void*)pthread_self());
+			return queue;
+		}
+	}
+	return NULL;
+}
+
+static inline void put_queue(struct impl *impl, struct queue *queue)
+{
+	union tag head, next;
+
+	spa_log_trace(impl->log, "%p idx:%d %p", queue, queue->idx, (void*)pthread_self());
+
+	head.v = SPA_ATOMIC_LOAD(impl->head.v);
+
+	while (true) {
+		queue->next = head.t.idx;
+
+		next.t.idx = queue->idx;
+		next.t.count = head.t.count+1;
+
+		if (SPA_LIKELY(__atomic_compare_exchange_n(&impl->head.v, &head.v, next.v,
+						0, __ATOMIC_ACQ_REL, __ATOMIC_RELAXED)))
+			break;
+	}
+}
+
+
 static inline int32_t item_compare(struct invoke_item *a, struct invoke_item *b)
 {
 	return (int32_t)(a->count - b->count);
@@ -231,25 +325,32 @@ static void flush_all_queues(struct impl *impl)
 	uint32_t flush_count;
 	int res;
 
-	pthread_mutex_lock(&impl->queue_lock);
-	flush_count = ++impl->flush_count;
+	flush_count = SPA_ATOMIC_INC(impl->flush_count);
 	while (true) {
 		struct queue *cqueue, *queue = NULL;
 		struct invoke_item *citem, *item = NULL;
 		uint32_t cindex, index;
 		spa_invoke_func_t func;
 		bool block;
-
-		spa_list_for_each(cqueue, &impl->queue_list, link) {
-			if (spa_ringbuffer_get_read_index(&cqueue->buffer, &cindex) <
-					(int32_t)sizeof(struct invoke_item))
-				continue;
-			citem = SPA_PTROFF(cqueue->buffer_data, cindex & (DATAS_SIZE - 1), struct invoke_item);
-
-			if (item == NULL || item_compare(citem, item) < 0) {
-				item = citem;
-				queue = cqueue;
-				index = cindex;
+		uint32_t i, n_queues;
+
+		n_queues = SPA_ATOMIC_LOAD(impl->n_queues);
+		for (i = 0; i < n_queues; i++) {
+			/* loop over all queues and overflow queues */
+			for (cqueue = impl->queues[i]; cqueue != NULL;
+					cqueue = SPA_ATOMIC_LOAD(cqueue->overflow)) {
+				if (spa_ringbuffer_get_read_index(&cqueue->buffer, &cindex) <
+						(int32_t)sizeof(struct invoke_item))
+					continue;
+
+				citem = SPA_PTROFF(cqueue->buffer_data,
+						cindex & (DATAS_SIZE - 1), struct invoke_item);
+
+				if (item == NULL || item_compare(citem, item) < 0) {
+					item = citem;
+					queue = cqueue;
+					index = cindex;
+				}
 			}
 		}
 		if (item == NULL)
@@ -262,15 +363,13 @@ static void flush_all_queues(struct impl *impl)
 		 * might get overwritten. */
 		func = spa_steal_ptr(item->func);
 		if (func) {
-			pthread_mutex_unlock(&impl->queue_lock);
 			item->res = func(&impl->loop, true, item->seq, item->data,
 				item->size, item->user_data);
-			pthread_mutex_lock(&impl->queue_lock);
 		}
 
 		/* if this function did a recursive invoke, it now flushed the
 		 * ringbuffer and we can exit */
-		if (flush_count != impl->flush_count)
+		if (flush_count != SPA_ATOMIC_LOAD(impl->flush_count))
 			break;
 
 		index += item->item_size;
@@ -283,7 +382,6 @@ static void flush_all_queues(struct impl *impl)
 						queue, queue->ack_fd, spa_strerror(res));
 		}
 	}
-	pthread_mutex_unlock(&impl->queue_lock);
 }
 
 static int
@@ -295,26 +393,24 @@ loop_queue_invoke(void *object,
 	    bool block,
 	    void *user_data)
 {
-	struct queue *queue = object;
+	struct queue *queue = object, *orig = queue, *overflow;
 	struct impl *impl = queue->impl;
 	struct invoke_item *item;
-	int res, suppressed;
+	int res;
 	int32_t filled;
 	uint32_t avail, idx, offset, l0;
-	size_t need;
-	uint64_t nsec;
 	bool in_thread;
+	pthread_t loop_thread, current_thread = pthread_self();
 
-	in_thread = (impl->thread == 0 || pthread_equal(impl->thread, pthread_self()));
+again:
+	loop_thread = impl->thread;
+	in_thread = (loop_thread == 0 || pthread_equal(loop_thread, current_thread));
 
-retry:
 	filled = spa_ringbuffer_get_write_index(&queue->buffer, &idx);
 	spa_assert_se(filled >= 0 && filled <= DATAS_SIZE && "queue xrun");
 	avail = (uint32_t)(DATAS_SIZE - filled);
-	if (avail < sizeof(struct invoke_item)) {
-		need = sizeof(struct invoke_item);
+	if (avail < sizeof(struct invoke_item))
 		goto xrun;
-	}
 	offset = idx & (DATAS_SIZE - 1);
 
 	/* l0 is remaining size in ringbuffer, this should always be larger than
@@ -331,7 +427,7 @@ retry:
 	item->res = 0;
 	item->item_size = SPA_ROUND_UP_N(sizeof(struct invoke_item) + size, ITEM_ALIGN);
 
-	spa_log_trace_fp(impl->log, "%p: add item %p filled:%d", queue, item, filled);
+	spa_log_trace(impl->log, "%p: add item %p filled:%d block:%d", queue, item, filled, block);
 
 	if (l0 >= item->item_size) {
 		/* item + size fit in current ringbuffer idx */
@@ -347,17 +443,28 @@ retry:
 		item->data = queue->buffer_data;
 		item->item_size = SPA_ROUND_UP_N(l0 + size, ITEM_ALIGN);
 	}
-	if (avail < item->item_size) {
-		need = item->item_size;
+	if (avail < item->item_size)
 		goto xrun;
-	}
+
 	if (data && size > 0)
 		memcpy(item->data, data, size);
 
 	spa_ringbuffer_write_update(&queue->buffer, idx + item->item_size);
 
 	if (in_thread) {
+		put_queue(impl, orig);
+
+		/* when there is no thread running the loop we flush the queues from
+		 * this invoking thread but we need to serialize the flushing here with
+		 * a mutex */
+		if (loop_thread == 0)
+			pthread_mutex_lock(&impl->queue_lock);
+
 		flush_all_queues(impl);
+
+		if (loop_thread == 0)
+			pthread_mutex_unlock(&impl->queue_lock);
+
 		res = item->res;
 	} else {
 		loop_signal_event(impl, impl->wakeup);
@@ -381,23 +488,23 @@ retry:
 			else
 				res = 0;
 		}
+		put_queue(impl, orig);
 	}
 	return res;
-
 xrun:
-	if (queue->overflow == NULL) {
-		nsec = get_time_ns(impl->system);
-		if ((suppressed = spa_ratelimit_test(&queue->rate_limit, nsec)) >= 0) {
-			spa_log_warn(impl->log, "%p: queue full %d, need %zd (%d suppressed)",
-					queue, avail, need, suppressed);
-		}
-		queue->overflow = loop_create_queue(impl, QUEUE_FLAG_NONE);
-		if (queue->overflow == NULL)
+	/* we overflow, make a new queue that shares the same fd
+	 * and place it in the overflow array. We hold the queue so there
+	 * is only ever one writer to the overflow field. */
+	overflow = queue->overflow;
+	if (overflow == NULL) {
+		overflow = loop_create_queue(impl, false);
+		if (overflow == NULL)
 			return -errno;
-		queue->overflow->ack_fd = queue->ack_fd;
+		overflow->ack_fd = queue->ack_fd;
+		SPA_ATOMIC_STORE(queue->overflow, overflow);
 	}
-	queue = queue->overflow;
-	goto retry;
+	queue = overflow;
+	goto again;
 }
 
 static void wakeup_func(void *data, uint64_t count)
@@ -406,33 +513,40 @@ static void wakeup_func(void *data, uint64_t count)
 	flush_all_queues(impl);
 }
 
-static void loop_queue_destroy(void *data)
-{
-	struct queue *queue = data;
-	struct impl *impl = queue->impl;
-
-	pthread_mutex_lock(&impl->queue_lock);
-	spa_list_remove(&queue->link);
-	pthread_mutex_unlock(&impl->queue_lock);
-
-	if (queue->flags & QUEUE_FLAG_ACK_FD)
-		spa_system_close(impl->system, queue->ack_fd);
-	free(queue);
-}
-
 static int loop_invoke(void *object, spa_invoke_func_t func, uint32_t seq,
 		const void *data, size_t size, bool block, void *user_data)
 {
 	struct impl *impl = object;
-	struct queue *local_queue = tss_get(impl->queue_tss_id);
+	struct queue *queue;
+	int res = 0, suppressed;
+	uint64_t nsec;
 
-	if (local_queue == NULL) {
-		local_queue = loop_create_queue(impl, QUEUE_FLAG_ACK_FD);
-		if (local_queue == NULL)
-			return -errno;
-		tss_set(impl->queue_tss_id, local_queue);
+	while (true) {
+		queue = get_queue(impl);
+		if (SPA_UNLIKELY(queue == NULL))
+			queue = loop_create_queue(impl, true);
+		if (SPA_UNLIKELY(queue == NULL)) {
+			if (SPA_UNLIKELY(errno != ENOSPC))
+				return -errno;
+
+			/* there was no space for a new queue. This means QUEUE_MAX
+			 * threads are concurrently doing an invoke. We can wait a little
+			 * and retry to get a queue */
+			if (impl->retry_timeout == 0)
+				return -EPIPE;
+
+			nsec = get_time_ns(impl->system);
+			if ((suppressed = spa_ratelimit_test(&impl->rate_limit, nsec)) >= 0) {
+				spa_log_warn(impl->log, "%p: out of queues, retrying (%d suppressed)",
+						impl, suppressed);
+			}
+			usleep(impl->retry_timeout);
+		} else {
+			res = loop_queue_invoke(queue, func, seq, data, size, block, user_data);
+			break;
+		}
 	}
-	return loop_queue_invoke(local_queue, func, seq, data, size, block, user_data);
+	return res;
 }
 
 static int loop_get_fd(void *object)
@@ -1063,24 +1177,26 @@ static int impl_clear(struct spa_handle *handle)
 {
 	struct impl *impl;
 	struct source_impl *source;
-	struct queue *queue;
+	uint32_t i;
 
 	spa_return_val_if_fail(handle != NULL, -EINVAL);
 
 	impl = (struct impl *) handle;
 
+	spa_log_debug(impl->log, "%p: clear", impl);
+
 	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);
-	spa_list_consume(queue, &impl->queue_list, link)
-		loop_queue_destroy(queue);
+	for (i = 0; i < impl->n_queues; i++)
+		loop_queue_destroy(impl->queues[i]);
 
 	spa_system_close(impl->system, impl->poll_fd);
+
 	pthread_mutex_destroy(&impl->queue_lock);
-	tss_delete(impl->queue_tss_id);
 
 	return 0;
 }
@@ -1133,10 +1249,15 @@ impl_init(const struct spa_handle_factory *factory,
 			SPA_VERSION_LOOP_UTILS,
 			&impl_loop_utils, impl);
 
+	impl->rate_limit.interval = 2 * SPA_NSEC_PER_SEC;
+	impl->rate_limit.burst = 1;
+	impl->retry_timeout = DEFAULT_RETRY;
 	if (info) {
 		if ((str = spa_dict_lookup(info, "loop.cancel")) != NULL &&
 		    spa_atob(str))
 			impl->control.iface.cb.funcs = &impl_loop_control_cancel;
+		if ((str = spa_dict_lookup(info, "loop.retry-timeout")) != NULL)
+			impl->retry_timeout = atoi(str);
 	}
 
 	CHECK(pthread_mutexattr_init(&attr), error_exit);
@@ -1160,7 +1281,6 @@ impl_init(const struct spa_handle_factory *factory,
 	impl->poll_fd = res;
 
 	spa_list_init(&impl->source_list);
-	spa_list_init(&impl->queue_list);
 	spa_list_init(&impl->destroy_list);
 	spa_hook_list_init(&impl->hooks_list);
 
@@ -1171,18 +1291,12 @@ impl_init(const struct spa_handle_factory *factory,
 		goto error_exit_free_poll;
 	}
 
-	if (tss_create(&impl->queue_tss_id, (tss_dtor_t)loop_queue_destroy) != thrd_success) {
-		res = -errno;
-		spa_log_error(impl->log, "%p: can't create tss: %m", impl);
-		goto error_exit_free_wakeup;
-	}
+	impl->head.t.idx = IDX_INVALID;
 
 	spa_log_debug(impl->log, "%p: initialized", impl);
 
 	return 0;
 
-error_exit_free_wakeup:
-	loop_destroy_source(impl, impl->wakeup);
 error_exit_free_poll:
 	spa_system_close(impl->system, impl->poll_fd);
 error_exit_free_mutex:
diff --git a/spa/plugins/support/meson.build b/spa/plugins/support/meson.build
index c9824551..67884577 100644
--- a/spa/plugins/support/meson.build
+++ b/spa/plugins/support/meson.build
@@ -19,6 +19,7 @@ stdthreads_lib = cc.find_library('stdthreads', required: false)
 spa_support_lib = shared_library('spa-support',
   spa_support_sources,
   c_args : [ simd_cargs ],
+  include_directories : [ configinc ],
   dependencies : [ spa_dep, pthread_lib, epoll_shim_dep, mathlib, stdthreads_lib ],
   install : true,
   install_dir : spa_plugindir / 'support')
diff --git a/spa/plugins/support/null-audio-sink.c b/spa/plugins/support/null-audio-sink.c
index 91d28db5..997a6681 100644
--- a/spa/plugins/support/null-audio-sink.c
+++ b/spa/plugins/support/null-audio-sink.c
@@ -22,6 +22,7 @@
 #include <spa/node/io.h>
 #include <spa/node/keys.h>
 #include <spa/param/audio/format-utils.h>
+#include <spa/param/audio/raw-json.h>
 #include <spa/debug/types.h>
 #include <spa/debug/mem.h>
 #include <spa/param/audio/type-info.h>
@@ -859,42 +860,6 @@ impl_get_size(const struct spa_handle_factory *factory,
 	return sizeof(struct impl);
 }
 
-static uint32_t format_from_name(const char *name)
-{
-	int i;
-	for (i = 0; spa_type_audio_format[i].name; i++) {
-		if (spa_streq(name, spa_debug_type_short_name(spa_type_audio_format[i].name)))
-			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 inline void parse_position(struct impl *this, 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);
-
-	this->props.channels = 0;
-	while (spa_json_get_string(&it[1], v, sizeof(v)) > 0 &&
-	    this->props.channels < SPA_AUDIO_MAX_CHANNELS) {
-		this->props.pos[this->props.channels++] = channel_from_name(v);
-	}
-}
-
 static int
 impl_init(const struct spa_handle_factory *factory,
 	  struct spa_handle *handle,
@@ -976,7 +941,7 @@ 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, SPA_KEY_AUDIO_FORMAT)) {
-			this->props.format = format_from_name(s);
+			this->props.format = spa_type_audio_format_from_short_name(s);
 		} else if (spa_streq(k, SPA_KEY_AUDIO_CHANNELS)) {
 			this->props.channels = atoi(s);
 		} else if (spa_streq(k, SPA_KEY_AUDIO_RATE)) {
@@ -984,7 +949,7 @@ impl_init(const struct spa_handle_factory *factory,
 		} else if (spa_streq(k, SPA_KEY_NODE_DRIVER)) {
 			this->props.driver = spa_atob(s);
 		} else if (spa_streq(k, SPA_KEY_AUDIO_POSITION)) {
-			parse_position(this, s, strlen(s));
+			spa_audio_parse_position(s, strlen(s), this->props.pos, &this->props.channels);
 		} else if (spa_streq(k, "clock.name")) {
 			spa_scnprintf(this->props.clock_name,
 					sizeof(this->props.clock_name),
diff --git a/spa/plugins/v4l2/meson.build b/spa/plugins/v4l2/meson.build
index e7d09fe2..22746a16 100644
--- a/spa/plugins/v4l2/meson.build
+++ b/spa/plugins/v4l2/meson.build
@@ -1,7 +1,7 @@
 v4l2_sources = ['v4l2.c',
                 'v4l2-device.c',
                 'v4l2-source.c']
-v4l2_dependencies = [ spa_dep, libinotify_dep ]
+v4l2_dependencies = [ spa_dep, libinotify_dep, mathlib ]
 
 if libudev_dep.found()
   v4l2_sources += [ 'v4l2-udev.c' ]
diff --git a/spa/plugins/v4l2/v4l2-source.c b/spa/plugins/v4l2/v4l2-source.c
index 899a566c..f19dffde 100644
--- a/spa/plugins/v4l2/v4l2-source.c
+++ b/spa/plugins/v4l2/v4l2-source.c
@@ -16,6 +16,7 @@
 #include <spa/utils/keys.h>
 #include <spa/utils/names.h>
 #include <spa/utils/string.h>
+#include <spa/utils/dll.h>
 #include <spa/monitor/device.h>
 #include <spa/node/node.h>
 #include <spa/node/io.h>
@@ -31,16 +32,19 @@
 #include "v4l2.h"
 
 static const char default_device[] = "/dev/video0";
+static const char default_clock_name[] = "api.v4l2.unknown";
 
 struct props {
 	char device[64];
 	char device_name[128];
 	int device_fd;
+	char clock_name[64];
 };
 
 static void reset_props(struct props *props)
 {
-	strncpy(props->device, default_device, 64);
+	strncpy(props->device, default_device, sizeof(props->device));
+	strncpy(props->clock_name, default_clock_name, sizeof(props->clock_name));
 }
 
 #define MAX_BUFFERS     32
@@ -147,6 +151,8 @@ struct impl {
 	struct spa_io_clock *clock;
 
 	struct spa_latency_info latency[2];
+
+	struct spa_dll dll;
 };
 
 #define CHECK_PORT(this,direction,port_id)  ((direction) == SPA_DIRECTION_OUTPUT && (port_id) == 0)
@@ -414,6 +420,11 @@ static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size)
 	switch (id) {
 	case SPA_IO_Clock:
 		this->clock = data;
+		if (this->clock) {
+			SPA_FLAG_SET(this->clock->flags, SPA_IO_CLOCK_FLAG_NO_RATE);
+			spa_scnprintf(this->clock->name, sizeof(this->clock->name),
+					"%s", this->props.clock_name);
+		}
 		break;
 	case SPA_IO_Position:
 		this->position = data;
@@ -997,6 +1008,7 @@ impl_init(const struct spa_handle_factory *factory,
 	struct port *port;
 	uint32_t i;
 	int res;
+	bool have_clock = false;
 
 	spa_return_val_if_fail(factory != NULL, -EINVAL);
 	spa_return_val_if_fail(handle != NULL, -EINVAL);
@@ -1068,13 +1080,22 @@ impl_init(const struct spa_handle_factory *factory,
 		const char *s = info->items[i].value;
 		if (spa_streq(k, SPA_KEY_API_V4L2_PATH)) {
 			strncpy(this->props.device, s, 63);
-			if ((res = spa_v4l2_open(&port->dev, this->props.device)) < 0)
-				return res;
-			spa_v4l2_close(&port->dev);
 		} else if (spa_streq(k, "meta.videotransform.transform")) {
 			this->transform = spa_debug_type_find_type_short(spa_type_meta_videotransform_type, s);
+		} else if (spa_streq(k, "clock.name")) {
+			spa_scnprintf(this->props.clock_name,
+					sizeof(this->props.clock_name), "%s", s);
+			have_clock = true;
 		}
 	}
+	if ((res = spa_v4l2_open(&port->dev, this->props.device)) < 0)
+		return res;
+	spa_v4l2_close(&port->dev);
+
+	if (!have_clock) {
+		spa_scnprintf(this->props.clock_name,
+				sizeof(this->props.clock_name), "api.v4l2.%s", port->dev.cap.bus_info);
+	}
 	return 0;
 }
 
diff --git a/spa/plugins/v4l2/v4l2-utils.c b/spa/plugins/v4l2/v4l2-utils.c
index c14037d1..be38ab32 100644
--- a/spa/plugins/v4l2/v4l2-utils.c
+++ b/spa/plugins/v4l2/v4l2-utils.c
@@ -476,17 +476,17 @@ filter_framerate(struct v4l2_frmivalenum *frmival,
 		frmival->stepwise.step.denominator *= step->num;
 		frmival->stepwise.step.numerator *= step->denom;
 
-		if (compare_fraction(&frmival->stepwise.max, min) < 0 ||
-		    compare_fraction(&frmival->stepwise.min, max) > 0)
+		if (compare_fraction(&frmival->stepwise.min, min) < 0 ||
+		    compare_fraction(&frmival->stepwise.max, max) > 0)
 			return false;
 
-		if (compare_fraction(&frmival->stepwise.min, min) < 0) {
-			frmival->stepwise.min.denominator = min->num;
-			frmival->stepwise.min.numerator = min->denom;
+		if (compare_fraction(&frmival->stepwise.max, min) < 0) {
+			frmival->stepwise.max.denominator = min->num;
+			frmival->stepwise.max.numerator = min->denom;
 		}
-		if (compare_fraction(&frmival->stepwise.max, max) > 0) {
-			frmival->stepwise.max.denominator = max->num;
-			frmival->stepwise.max.numerator = max->denom;
+		if (compare_fraction(&frmival->stepwise.min, max) > 0) {
+			frmival->stepwise.min.denominator = max->num;
+			frmival->stepwise.min.numerator = max->denom;
 		}
 	} else
 		return false;
@@ -778,9 +778,9 @@ do_frmsize_filter:
 			if (errno == EINVAL || errno == ENOTTY) {
 				if (port->frmival.index == 0) {
 					port->frmival.type = V4L2_FRMIVAL_TYPE_CONTINUOUS;
-					port->frmival.stepwise.min.denominator = 1;
+					port->frmival.stepwise.min.denominator = 120;
 					port->frmival.stepwise.min.numerator = 1;
-					port->frmival.stepwise.max.denominator = 120;
+					port->frmival.stepwise.max.denominator = 1;
 					port->frmival.stepwise.max.numerator = 1;
 					goto do_frminterval_filter;
 				}
@@ -853,14 +853,25 @@ do_frminterval_filter:
 			n_fractions++;
 		} else if (port->frmival.type == V4L2_FRMIVAL_TYPE_CONTINUOUS ||
 			   port->frmival.type == V4L2_FRMIVAL_TYPE_STEPWISE) {
-			if (n_fractions == 0)
-				spa_pod_builder_fraction(&b.b, 25, 1);
-			spa_pod_builder_fraction(&b.b,
-						 port->frmival.stepwise.min.denominator,
-						 port->frmival.stepwise.min.numerator);
+			if (n_fractions == 0) {
+				struct spa_fraction f = { 25, 1 };
+				if (compare_fraction(&port->frmival.stepwise.max, &f) > 0) {
+					f.denom = port->frmival.stepwise.max.numerator;
+					f.num = port->frmival.stepwise.max.denominator;
+				}
+				if (compare_fraction(&port->frmival.stepwise.min, &f) < 0) {
+					f.denom = port->frmival.stepwise.min.numerator;
+					f.num = port->frmival.stepwise.min.denominator;
+				}
+
+				spa_pod_builder_fraction(&b.b, f.num, f.denom);
+			}
 			spa_pod_builder_fraction(&b.b,
 						 port->frmival.stepwise.max.denominator,
 						 port->frmival.stepwise.max.numerator);
+			spa_pod_builder_fraction(&b.b,
+						 port->frmival.stepwise.min.denominator,
+						 port->frmival.stepwise.min.numerator);
 
 			if (port->frmival.type == V4L2_FRMIVAL_TYPE_CONTINUOUS) {
 				choice->body.type = SPA_CHOICE_Range;
@@ -1072,7 +1083,7 @@ static int query_ext_ctrl_ioctl(struct port *port, struct v4l2_query_ext_ctrl *q
 
 	if (port->have_query_ext_ctrl) {
 		res = xioctl(dev->fd, VIDIOC_QUERY_EXT_CTRL, qctrl);
-		if (errno != ENOTTY)
+		if (res == 0 || errno != ENOTTY)
 			return res;
 		port->have_query_ext_ctrl = false;
 	}
@@ -1368,6 +1379,14 @@ spa_v4l2_set_control(struct impl *this, uint32_t id,
 		control.value = val;
 		break;
 	}
+	case SPA_TYPE_Float:
+	{
+		float val;
+		if ((res = spa_pod_get_float(&prop->value, &val)) < 0)
+			goto done;
+		control.value = (int32_t) val;
+		break;
+	}
 	case SPA_TYPE_Int:
 	{
 		int32_t val;
@@ -1421,7 +1440,21 @@ static int mmap_read(struct impl *this)
 
 	pts = SPA_TIMEVAL_TO_NSEC(&buf.timestamp);
 
+
 	if (this->clock) {
+		double target = (double)port->info.rate.num / port->info.rate.denom;
+		double corr;
+
+		if (this->dll.bw == 0.0) {
+			spa_dll_set_bw(&this->dll, SPA_DLL_BW_MAX, port->info.rate.denom, port->info.rate.denom);
+			this->clock->next_nsec = pts;
+			corr = 1.0;
+		} else {
+			double diff = ((double)this->clock->next_nsec - (double)pts) / SPA_NSEC_PER_SEC;
+			double error = port->info.rate.denom * (diff - target);
+			corr = spa_dll_update(&this->dll, SPA_CLAMPD(error, -128., 128.));
+		}
+
 		/* FIXME, we should follow the driver clock and target_ values.
 		 * for now we ignore and use our own. */
 		this->clock->target_rate = port->info.rate;
@@ -1432,8 +1465,8 @@ static int mmap_read(struct impl *this)
 		this->clock->position = buf.sequence;
 		this->clock->duration = 1;
 		this->clock->delay = 0;
-		this->clock->rate_diff = 1.0;
-		this->clock->next_nsec = pts + port->info.rate.num * SPA_NSEC_PER_SEC / port->info.rate.denom;
+		this->clock->rate_diff = corr;
+		this->clock->next_nsec += (uint64_t) (target * SPA_NSEC_PER_SEC * corr);
 	}
 
 	b = &port->buffers[buf.index];
@@ -1848,6 +1881,7 @@ static int spa_v4l2_stream_on(struct impl *this)
 		spa_log_error(this->log, "'%s' VIDIOC_STREAMON: %m", this->props.device);
 		return -errno;
 	}
+	this->dll.bw = 0.0;
 
 	port->source.func = v4l2_on_fd_events;
 	port->source.data = this;
diff --git a/spa/plugins/videoconvert/meson.build b/spa/plugins/videoconvert/meson.build
index 24673a54..c345257d 100644
--- a/spa/plugins/videoconvert/meson.build
+++ b/spa/plugins/videoconvert/meson.build
@@ -1,15 +1,26 @@
 videoconvert_sources = [
   'videoadapter.c',
+  'videoconvert-dummy.c',
   'plugin.c'
 ]
 
-simd_cargs = []
-simd_dependencies = []
+extra_cargs = []
+extra_dependencies = []
+
+if avcodec_dep.found() and avutil_dep.found() and swscale_dep.found()
+  videoconvert_ffmpeg = static_library('videoconvert_fmmpeg',
+    ['videoconvert-ffmpeg.c' ],
+    dependencies : [ spa_dep, avcodec_dep, avutil_dep, swscale_dep ],
+    install : false
+    )
+  extra_cargs += '-D HAVE_VIDEOCONVERT_FFMPEG'
+  extra_dependencies += videoconvert_ffmpeg
+endif
 
 videoconvertlib = shared_library('spa-videoconvert',
   videoconvert_sources,
-  c_args : simd_cargs,
+  c_args : extra_cargs,
   dependencies : [ spa_dep, mathlib ],
-  link_with : simd_dependencies,
+  link_with : extra_dependencies,
   install : true,
   install_dir : spa_plugindir / 'videoconvert')
diff --git a/spa/plugins/videoconvert/plugin.c b/spa/plugins/videoconvert/plugin.c
index 78528141..d3d717b8 100644
--- a/spa/plugins/videoconvert/plugin.c
+++ b/spa/plugins/videoconvert/plugin.c
@@ -8,6 +8,10 @@
 #include <spa/support/log.h>
 
 extern const struct spa_handle_factory spa_videoadapter_factory;
+extern const struct spa_handle_factory spa_videoconvert_dummy_factory;
+#if HAVE_VIDEOCONVERT_FFMPEG
+extern const struct spa_handle_factory spa_videoconvert_ffmpeg_factory;
+#endif
 
 SPA_LOG_TOPIC_ENUM_DEFINE_REGISTERED;
 
@@ -21,6 +25,14 @@ int spa_handle_factory_enum(const struct spa_handle_factory **factory, uint32_t
 	case 0:
 		*factory = &spa_videoadapter_factory;
 		break;
+	case 1:
+		*factory = &spa_videoconvert_dummy_factory;
+		break;
+#if HAVE_VIDEOCONVERT_FFMPEG
+	case 2:
+		*factory = &spa_videoconvert_ffmpeg_factory;
+		break;
+#endif
 	default:
 		return 0;
 	}
diff --git a/spa/plugins/videoconvert/videoadapter.c b/spa/plugins/videoconvert/videoadapter.c
index 22cf8c4c..8c1c8de7 100644
--- a/spa/plugins/videoconvert/videoadapter.c
+++ b/spa/plugins/videoconvert/videoadapter.c
@@ -2,7 +2,7 @@
 /* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */
 /* SPDX-License-Identifier: MIT */
 
-#include "spa/utils/dict.h"
+#include <spa/utils/dict.h>
 #include <spa/support/plugin.h>
 #include <spa/support/plugin-loader.h>
 #include <spa/support/log.h>
@@ -61,7 +61,7 @@ struct impl {
 	struct spa_handle *hnd_convert;
 	struct spa_node *convert;
 	struct spa_hook convert_listener;
-	uint64_t convert_flags;
+	uint64_t convert_port_flags;
 	char *convertname;
 
 	uint32_t n_buffers;
@@ -93,6 +93,7 @@ struct impl {
 
 	unsigned int add_listener:1;
 	unsigned int have_format:1;
+	unsigned int recheck_format:1;
 	unsigned int started:1;
 	unsigned int ready:1;
 	unsigned int async:1;
@@ -114,9 +115,9 @@ static int follower_enum_params(struct impl *this,
 				 struct spa_pod_builder *builder)
 {
 	int res;
-	if (result->next < 0x100000) {
-		if (this->convert != NULL &&
-		    (res = spa_node_enum_params_sync(this->convert,
+	if (result->next < 0x100000 &&
+	    this->follower != this->target) {
+		if ((res = spa_node_enum_params_sync(this->target,
 				id, &result->next, filter, &result->param, builder)) == 1)
 			return res;
 		result->next = 0x100000;
@@ -140,6 +141,9 @@ static int convert_enum_port_config(struct impl *this,
 	struct spa_pod *f1, *f2 = NULL;
 	int res;
 
+	if (this->convert == NULL)
+		return 0;
+
 	f1 = spa_pod_builder_add_object(builder,
 		SPA_TYPE_OBJECT_ParamPortConfig, id,
 			SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(this->direction));
@@ -183,9 +187,6 @@ next:
 
 	switch (id) {
 	case SPA_PARAM_EnumPortConfig:
-		if (this->convert == NULL)
-			return 0;
-		return convert_enum_port_config(this, seq, id, start, num, filter, &b.b);
 	case SPA_PARAM_PortConfig:
 		if (this->passthrough) {
 			switch (result.index) {
@@ -202,8 +203,6 @@ next:
 				return 0;
 			}
 		} else {
-			if (this->convert == NULL)
-				return 0;
 			return convert_enum_port_config(this, seq, id, start, num, filter, &b.b);
 		}
 		break;
@@ -248,9 +247,6 @@ static int link_io(struct impl *this)
 	struct spa_io_rate_match *rate_match;
 	size_t rate_match_size;
 
-	if (this->convert == NULL)
-		return 0;
-
 	spa_log_debug(this->log, "%p: controls", this);
 
 	spa_zero(this->io_rate_match);
@@ -271,31 +267,40 @@ static int link_io(struct impl *this)
 		spa_log_debug(this->log, "%p: set RateMatch on follower disabled %d %s", this,
 			res, spa_strerror(res));
 	}
-	else if ((res = spa_node_port_set_io(this->convert,
-			SPA_DIRECTION_REVERSE(this->direction), 0,
-			SPA_IO_RateMatch,
-			rate_match, rate_match_size)) < 0) {
-		spa_log_warn(this->log, "%p: set RateMatch on convert failed %d %s", this,
-			res, spa_strerror(res));
+	else if (this->follower != this->target) {
+		if ((res = spa_node_port_set_io(this->target,
+				SPA_DIRECTION_REVERSE(this->direction), 0,
+				SPA_IO_RateMatch,
+				rate_match, rate_match_size)) < 0) {
+			spa_log_warn(this->log, "%p: set RateMatch on target failed %d %s", this,
+				res, spa_strerror(res));
+		}
 	}
+	return 0;
+}
+
+static int activate_io(struct impl *this, bool active)
+{
+	int res;
+	struct spa_io_buffers *data = active ? &this->io_buffers : NULL;
+	uint32_t size = active ? sizeof(this->io_buffers) : 0;
 
 	if (this->follower == this->target)
 		return 0;
 
-	this->io_buffers = SPA_IO_BUFFERS_INIT;
+	if (active)
+		this->io_buffers = SPA_IO_BUFFERS_INIT;
 
 	if ((res = spa_node_port_set_io(this->follower,
 			this->direction, 0,
-			SPA_IO_Buffers,
-			&this->io_buffers, sizeof(this->io_buffers))) < 0) {
+			SPA_IO_Buffers, data, size)) < 0) {
 		spa_log_warn(this->log, "%p: set Buffers on follower failed %d %s", this,
 			res, spa_strerror(res));
 		return res;
 	}
-	else if ((res = spa_node_port_set_io(this->convert,
+	else if ((res = spa_node_port_set_io(this->target,
 			SPA_DIRECTION_REVERSE(this->direction), 0,
-			SPA_IO_Buffers,
-			&this->io_buffers, sizeof(this->io_buffers))) < 0) {
+			SPA_IO_Buffers, data, size)) < 0) {
 		spa_log_warn(this->log, "%p: set Buffers on convert failed %d %s", this,
 			res, spa_strerror(res));
 		return res;
@@ -314,6 +319,18 @@ static void emit_node_info(struct impl *this, bool full)
 	if (full)
 		this->info.change_mask = this->info_all;
 	if (this->info.change_mask) {
+		struct spa_dict_item *items;
+		uint32_t n_items = 0;
+
+		if (this->info.props)
+			n_items = this->info.props->n_items;
+		items = alloca((n_items + 2) * sizeof(struct spa_dict_item));
+		for (i = 0; i < n_items; i++)
+			items[i] = this->info.props->items[i];
+		items[n_items++] = SPA_DICT_ITEM_INIT("adapter.auto-port-config", NULL);
+		items[n_items++] = SPA_DICT_ITEM_INIT("video.adapt.follower", NULL);
+		this->info.props = &SPA_DICT_INIT(items, n_items);
+
 		if (this->info.change_mask & SPA_NODE_CHANGE_MASK_PARAMS) {
 			for (i = 0; i < this->info.n_params; i++) {
 				if (this->params[i].user > 0) {
@@ -326,6 +343,7 @@ static void emit_node_info(struct impl *this, bool full)
 		}
 		spa_node_emit_info(&this->hooks, &this->info);
 		this->info.change_mask = old;
+		spa_zero(this->info.props);
 	}
 }
 
@@ -389,6 +407,9 @@ static int negotiate_buffers(struct impl *this)
 
 	spa_log_debug(this->log, "%p: n_buffers:%d", this, this->n_buffers);
 
+	if (this->follower == this->target)
+		return 0;
+
 	if (this->n_buffers > 0)
 		return 0;
 
@@ -408,11 +429,11 @@ static int negotiate_buffers(struct impl *this)
 	}
 
 	state = 0;
-	if ((res = spa_node_port_enum_params_sync(this->convert,
+	if ((res = spa_node_port_enum_params_sync(this->target,
 				SPA_DIRECTION_REVERSE(this->direction), 0,
 				SPA_PARAM_Buffers, &state,
 				param, &param, &b)) != 1) {
-		debug_params(this, this->convert,
+		debug_params(this, this->target,
 				SPA_DIRECTION_REVERSE(this->direction), 0,
 				SPA_PARAM_Buffers, param, "convert buffers", res);
 		return -ENOTSUP;
@@ -422,8 +443,8 @@ static int negotiate_buffers(struct impl *this)
 
 	spa_pod_fixate(param);
 
-	follower_flags = this->follower_flags;
-	conv_flags = this->convert_flags;
+	follower_flags = this->follower_port_flags;
+	conv_flags = this->convert_port_flags;
 
 	follower_alloc = SPA_FLAG_IS_SET(follower_flags, SPA_PORT_FLAG_CAN_ALLOC_BUFFERS);
 	conv_alloc = SPA_FLAG_IS_SET(conv_flags, SPA_PORT_FLAG_CAN_ALLOC_BUFFERS);
@@ -470,7 +491,7 @@ static int negotiate_buffers(struct impl *this)
 		return -errno;
 	this->n_buffers = buffers;
 
-	if ((res = spa_node_port_use_buffers(this->convert,
+	if ((res = spa_node_port_use_buffers(this->target,
 		       SPA_DIRECTION_REVERSE(this->direction), 0,
 		       conv_alloc ? SPA_NODE_BUFFERS_FLAG_ALLOC : 0,
 		       this->buffers, this->n_buffers)) < 0)
@@ -482,20 +503,33 @@ static int negotiate_buffers(struct impl *this)
 		       this->buffers, this->n_buffers)) < 0)
 		return res;
 
+	activate_io(this, true);
+
 	return 0;
 }
 
+static void clear_buffers(struct impl *this)
+{
+	free(this->buffers);
+	this->buffers = NULL;
+	this->n_buffers = 0;
+}
+
 static int configure_format(struct impl *this, uint32_t flags, const struct spa_pod *format)
 {
 	uint8_t buffer[4096];
 	int res;
 
-	if (format == NULL && !this->have_format)
-		return 0;
-
 	spa_log_debug(this->log, "%p: configure format:", this);
-	if (format)
+
+	if (format == NULL) {
+		if (!this->have_format)
+			return 0;
+		activate_io(this, false);
+	}
+	else {
 		spa_debug_log_format(this->log, SPA_LOG_LEVEL_DEBUG, 0, NULL, format);
+	}
 
 	if ((res = spa_node_port_set_param(this->follower,
 					   this->direction, 0,
@@ -519,8 +553,8 @@ static int configure_format(struct impl *this, uint32_t flags, const struct spa_
 		format = fmt;
 	}
 
-	if (this->convert && this->target != this->follower) {
-		if ((res = spa_node_port_set_param(this->convert,
+	if (this->target != this->follower) {
+		if ((res = spa_node_port_set_param(this->target,
 					   SPA_DIRECTION_REVERSE(this->direction), 0,
 					   SPA_PARAM_Format, flags,
 					   format)) < 0)
@@ -528,11 +562,11 @@ static int configure_format(struct impl *this, uint32_t flags, const struct spa_
 	}
 
 	this->have_format = format != NULL;
-	if (format == NULL) {
-		this->n_buffers = 0;
-	} else if (this->target != this->follower) {
+	clear_buffers(this);
+
+	if (format != NULL)
 		res = negotiate_buffers(this);
-	}
+
 	return res;
 }
 
@@ -547,7 +581,7 @@ static int configure_convert(struct impl *this, uint32_t mode)
 
 	spa_pod_builder_init(&b, buffer, sizeof(buffer));
 
-	spa_log_debug(this->log, "%p: configure convert %p %d", this, this->target, mode);
+	spa_log_debug(this->log, "%p: configure convert %p", this, this->target);
 
 	param = spa_pod_builder_add_object(&b,
 		SPA_TYPE_OBJECT_ParamPortConfig, SPA_PARAM_PortConfig,
@@ -574,9 +608,6 @@ static int recalc_latency(struct impl *this, struct spa_node *src, enum spa_dire
 	if (this->target == this->follower)
 		return 0;
 
-	if (dst == NULL)
-		return 0;
-
 	while (true) {
 		spa_pod_builder_init(&b, buffer, sizeof(buffer));
 		if ((res = spa_node_port_enum_params_sync(src,
@@ -614,9 +645,6 @@ static int recalc_tag(struct impl *this, struct spa_node *src, enum spa_directio
 	if (this->target == this->follower)
 		return 0;
 
-	if (dst == NULL)
-		return 0;
-
 	spa_pod_dynamic_builder_init(&b, buffer, sizeof(buffer), 2048);
 	spa_pod_builder_get_state(&b.b, &state);
 
@@ -639,14 +667,18 @@ static int recalc_tag(struct impl *this, struct spa_node *src, enum spa_directio
 }
 
 
-static int reconfigure_mode(struct impl *this, bool passthrough,
+static int reconfigure_mode(struct impl *this, enum spa_param_port_config_mode mode,
                 enum spa_direction direction, struct spa_pod *format)
 {
 	int res = 0;
 	struct spa_hook l;
+	bool passthrough = mode == SPA_PARAM_PORT_CONFIG_MODE_passthrough;
 
 	spa_log_debug(this->log, "%p: passthrough mode %d", this, passthrough);
 
+	if (!passthrough && this->convert == NULL)
+		return -ENOTSUP;
+
 	if (this->passthrough != passthrough) {
 		if (passthrough) {
 			/* remove converter split/merge ports */
@@ -676,7 +708,7 @@ static int reconfigure_mode(struct impl *this, bool passthrough,
 			spa_hook_remove(&l);
 		} else {
 			/* add converter ports */
-			configure_convert(this, SPA_PARAM_PORT_CONFIG_MODE_dsp);
+			configure_convert(this, mode);
 		}
 		link_io(this);
 	}
@@ -709,12 +741,9 @@ static int impl_node_set_param(void *object, uint32_t id, uint32_t flags,
 		if (param == NULL)
 			return -EINVAL;
 
-		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;
-		if (spa_format_video_raw_parse(param, &info.info.raw) < 0)
+		if (spa_format_video_parse(param, &info) < 0)
+			return -EINVAL;
+		if (info.media_subtype != SPA_MEDIA_SUBTYPE_raw)
 			return -EINVAL;
 
 		this->follower_current_format = info;
@@ -742,29 +771,22 @@ static int impl_node_set_param(void *object, uint32_t id, uint32_t flags,
 			struct spa_video_info info;
 
 			spa_zero(info);
-			if ((res = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0)
+			if ((res = spa_format_video_parse(format, &info)) < 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;
-			}
+			this->default_format = info;
 		}
 
 		switch (mode) {
 		case SPA_PARAM_PORT_CONFIG_MODE_none:
 			return -ENOTSUP;
 		case SPA_PARAM_PORT_CONFIG_MODE_passthrough:
-			if ((res = reconfigure_mode(this, true, dir, format)) < 0)
+			if ((res = reconfigure_mode(this, mode, dir, format)) < 0)
 				return res;
 			break;
 		case SPA_PARAM_PORT_CONFIG_MODE_convert:
 		case SPA_PARAM_PORT_CONFIG_MODE_dsp:
-			if (this->convert == NULL)
-				return -ENOTSUP;
-			if ((res = reconfigure_mode(this, false, dir, NULL)) < 0)
+			if ((res = reconfigure_mode(this, mode, dir, NULL)) < 0)
 				return res;
 			break;
 		default:
@@ -774,6 +796,8 @@ static int impl_node_set_param(void *object, uint32_t id, uint32_t flags,
 		if (this->target != this->follower) {
 			if ((res = spa_node_set_param(this->target, id, flags, param)) < 0)
 				return res;
+
+			res = recalc_latency(this, this->follower, this->direction, 0, this->target);
 		}
 		break;
 	}
@@ -859,59 +883,80 @@ static struct spa_pod *merge_objects(struct impl *this, struct spa_pod_builder *
 
 static int negotiate_format(struct impl *this)
 {
-	uint32_t state;
+	uint32_t fstate, tstate;
 	struct spa_pod *format, *def;
 	uint8_t buffer[4096];
 	struct spa_pod_builder b = { 0 };
-	int res;
+	int res, fres;
+
+	spa_log_debug(this->log, "%p: have_format:%d recheck:%d", this, this->have_format,
+			this->recheck_format);
 
 	if (this->target == this->follower)
 		return 0;
 
-	spa_log_debug(this->log, "%p: have_format:%d", this, this->have_format);
-
-	if (this->have_format)
+	if (this->have_format && !this->recheck_format)
 		return 0;
 
-	spa_pod_builder_init(&b, buffer, sizeof(buffer));
+	this->recheck_format = false;
 
-	spa_log_debug(this->log, "%p: negiotiate", this);
+	spa_pod_builder_init(&b, buffer, sizeof(buffer));
 
 	spa_node_send_command(this->follower,
 			&SPA_NODE_COMMAND_INIT(SPA_NODE_COMMAND_ParamBegin));
 
-	state = 0;
-	format = NULL;
-	if ((res = spa_node_port_enum_params_sync(this->follower,
-				this->direction, 0,
-				SPA_PARAM_EnumFormat, &state,
-				format, &format, &b)) < 0) {
+	/* first try the ideal converter format, which is likely passthrough */
+	tstate = 0;
+	fres = spa_node_port_enum_params_sync(this->target,
+				SPA_DIRECTION_REVERSE(this->direction), 0,
+				SPA_PARAM_EnumFormat, &tstate,
+				NULL, &format, &b);
+	if (fres == 1) {
+		fstate = 0;
+		res = spa_node_port_enum_params_sync(this->follower,
+					this->direction, 0,
+					SPA_PARAM_EnumFormat, &fstate,
+					format, &format, &b);
+		if (res == 1)
+			goto found;
+	}
+
+	/* then try something the follower can accept */
+	for (fstate = 0;;) {
+		format = NULL;
+		res = spa_node_port_enum_params_sync(this->follower,
+					this->direction, 0,
+					SPA_PARAM_EnumFormat, &fstate,
+					NULL, &format, &b);
+
 		if (res == -ENOENT)
 			format = NULL;
-		else {
-			debug_params(this, this->follower, this->direction, 0,
-					SPA_PARAM_EnumFormat, format, "follower format", res);
-			goto done;
-		}
+		else if (res <= 0)
+			break;
+
+		tstate = 0;
+		fres = spa_node_port_enum_params_sync(this->target,
+					SPA_DIRECTION_REVERSE(this->direction), 0,
+					SPA_PARAM_EnumFormat, &tstate,
+					format, &format, &b);
+		if (fres == 0 && res == 1)
+			continue;
+
+		res = fres;
+		break;
 	}
-	state = 0;
-	if (this->convert && (res = spa_node_port_enum_params_sync(this->convert,
-				SPA_DIRECTION_REVERSE(this->direction), 0,
-				SPA_PARAM_EnumFormat, &state,
-				format, &format, &b)) != 1) {
-		debug_params(this, this->convert,
+found:
+	if (format == NULL) {
+		debug_params(this, this->follower, this->direction, 0,
+				SPA_PARAM_EnumFormat, format, "follower format", res);
+		debug_params(this, this->target,
 				SPA_DIRECTION_REVERSE(this->direction), 0,
 				SPA_PARAM_EnumFormat, format, "convert format", res);
 		res = -ENOTSUP;
 		goto done;
 	}
-	if (format == NULL) {
-		res = -ENOTSUP;
-		goto done;
-	}
-
-	def = spa_format_video_raw_build(&b,
-			SPA_PARAM_Format, &this->default_format.info.raw);
+	def = spa_format_video_build(&b,
+			SPA_PARAM_Format, &this->default_format);
 
 	format = merge_objects(this, &b, SPA_PARAM_Format,
 			(struct spa_pod_object*)format,
@@ -941,12 +986,10 @@ 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->target != this->follower) {
-			if (this->started)
-				return 0;
-			if ((res = negotiate_format(this)) < 0)
-				return res;
-		}
+		if (this->started)
+			return 0;
+		if ((res = negotiate_format(this)) < 0)
+			return res;
 		this->ready = true;
 		this->warned = false;
 		break;
@@ -1064,11 +1107,16 @@ static void follower_convert_port_info(void *data,
 	uint32_t i;
 	int res;
 
+	if (info == NULL)
+		return;
+
 	spa_log_debug(this->log, "%p: convert port info %s %p %08"PRIx64, this,
 			this->direction == SPA_DIRECTION_INPUT ?
 				"Input" : "Output", info, info->change_mask);
 
-	if (this->convert && info->change_mask & SPA_PORT_CHANGE_MASK_PARAMS) {
+	this->convert_port_flags = info->flags;
+
+	if (info->change_mask & SPA_PORT_CHANGE_MASK_PARAMS) {
 		for (i = 0; i < info->n_params; i++) {
 			uint32_t idx;
 
@@ -1094,14 +1142,14 @@ static void follower_convert_port_info(void *data,
 
 			if (idx == IDX_Latency) {
 				this->in_recalc++;
-				res = recalc_latency(this, this->convert, direction, port_id, this->follower);
+				res = recalc_latency(this, this->target, direction, port_id, this->follower);
 				this->in_recalc--;
 				spa_log_debug(this->log, "latency: %d (%s)", res,
 						spa_strerror(res));
 			}
 			if (idx == IDX_Tag) {
 				this->in_recalc++;
-				res = recalc_tag(this, this->convert, direction, port_id, this->follower);
+				res = recalc_tag(this, this->target, direction, port_id, this->follower);
 				this->in_recalc--;
 				spa_log_debug(this->log, "tag: %d (%s)", res,
 						spa_strerror(res));
@@ -1128,7 +1176,10 @@ static void convert_port_info(void *data,
 			port_id--;
 	} else if (info) {
 		pi = *info;
-		pi.flags = this->follower_port_flags;
+		pi.flags = this->follower_port_flags &
+			(SPA_PORT_FLAG_LIVE |
+			 SPA_PORT_FLAG_PHYSICAL |
+			 SPA_PORT_FLAG_TERMINAL);
 		info = &pi;
 	}
 
@@ -1241,15 +1292,15 @@ static void follower_port_info(void *data,
 	uint32_t i;
 	int res;
 
+	if (info == NULL)
+		return;
+
 	if (this->follower_removing) {
 	      spa_node_emit_port_info(&this->hooks, direction, port_id, NULL);
 	      return;
 	}
 
-	this->follower_port_flags = info->flags &
-		(SPA_PORT_FLAG_LIVE |
-		 SPA_PORT_FLAG_PHYSICAL |
-		 SPA_PORT_FLAG_TERMINAL);
+	this->follower_port_flags = info->flags;
 
 	spa_log_debug(this->log, "%p: follower port info %s %p %08"PRIx64" recalc:%u", this,
 			this->direction == SPA_DIRECTION_INPUT ?
@@ -1303,6 +1354,7 @@ static void follower_port_info(void *data,
 			if (idx == IDX_EnumFormat) {
 				spa_log_debug(this->log, "new formats");
 				/* we will renegotiate when restarting */
+				this->recheck_format = true;
 			}
 
 			this->params[idx].user++;
@@ -1369,12 +1421,12 @@ static int follower_ready(void *data, int status)
 		if (this->direction == SPA_DIRECTION_OUTPUT) {
 			int retry = MAX_RETRY;
 			while (retry--) {
-				status = spa_node_process(this->convert);
+				status = spa_node_process_fast(this->target);
 				if (status & SPA_STATUS_HAVE_DATA)
 					break;
 
 				if (status & SPA_STATUS_NEED_DATA) {
-					status = spa_node_process(this->follower);
+					status = spa_node_process_fast(this->follower);
 					if (!(status & SPA_STATUS_HAVE_DATA))
 						break;
 				}
@@ -1391,8 +1443,8 @@ static int follower_reuse_buffer(void *data, uint32_t port_id, uint32_t buffer_i
 	int res;
 	struct impl *this = data;
 
-	if (this->convert && this->target != this->follower)
-		res = spa_node_port_reuse_buffer(this->convert, port_id, buffer_id);
+	if (this->target != this->follower)
+		res = spa_node_port_reuse_buffer(this->target, port_id, buffer_id);
 	else
 		res = spa_node_call_reuse_buffer(&this->callbacks, port_id, buffer_id);
 
@@ -1434,12 +1486,11 @@ static int impl_node_add_listener(void *object,
 		spa_node_add_listener(this->follower, &l, &follower_node_events, this);
 		spa_hook_remove(&l);
 
-		if (this->convert) {
+		if (this->follower != this->target) {
 			spa_zero(l);
-			spa_node_add_listener(this->convert, &l, &convert_node_events, this);
+			spa_node_add_listener(this->target, &l, &convert_node_events, this);
 			spa_hook_remove(&l);
 		}
-
 		this->add_listener = false;
 
 		emit_node_info(this, true);
@@ -1500,6 +1551,33 @@ impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_
 	return spa_node_remove_port(this->target, direction, port_id);
 }
 
+static int follower_port_enum_params(struct impl *this,
+				enum spa_direction direction, uint32_t port_id,
+				uint32_t id, uint32_t idx,
+				struct spa_result_node_params *result,
+				const struct spa_pod *filter,
+				struct spa_pod_builder *builder)
+{
+	int res;
+	if (result->next < 0x100000 && this->follower_params_flags[idx] & SPA_PARAM_INFO_READ) {
+		if ((res = spa_node_port_enum_params_sync(this->follower, direction, port_id,
+				id, &result->next, filter, &result->param, builder)) == 1)
+			return res;
+		result->next = 0x100000;
+	}
+	if (result->next < 0x200000 &&
+	    this->follower != this->target) {
+		result->next &= 0xfffff;
+		if ((res = spa_node_port_enum_params_sync(this->target, direction, port_id,
+				id, &result->next, filter, &result->param, builder)) == 1) {
+			result->next |= 0x100000;
+			return res;
+		}
+		result->next = 0x200000;
+	}
+	return 0;
+}
+
 static int
 impl_node_port_enum_params(void *object, int seq,
 			   enum spa_direction direction, uint32_t port_id,
@@ -1507,6 +1585,12 @@ impl_node_port_enum_params(void *object, int seq,
 			   const struct spa_pod *filter)
 {
 	struct impl *this = object;
+	uint8_t buffer[4096];
+	spa_auto(spa_pod_dynamic_builder) b = { 0 };
+	struct spa_pod_builder_state state;
+	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);
@@ -1514,10 +1598,37 @@ impl_node_port_enum_params(void *object, int seq,
 	if (direction != this->direction)
 		port_id++;
 
-	spa_log_debug(this->log, "%p: %d %u", this, seq, id);
+	spa_pod_dynamic_builder_init(&b, buffer, sizeof(buffer), 4096);
+	spa_pod_builder_get_state(&b.b, &state);
+
+	result.id = id;
+	result.next = start;
+next:
+	result.index = result.next;
+
+	spa_log_debug(this->log, "%p: %d id:%u", this, seq, id);
+
+	spa_pod_builder_reset(&b.b, &state);
 
-	return spa_node_port_enum_params(this->target, seq, direction, port_id, id,
+	switch (id) {
+	case SPA_PARAM_EnumFormat:
+		res = follower_port_enum_params(this, direction, port_id,
+				id, IDX_EnumFormat, &result, filter, &b.b);
+		break;
+	default:
+		return spa_node_port_enum_params(this->target, seq, direction, port_id, id,
 			start, num, filter);
+	}
+	if (res != 1)
+		return res;
+
+	spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+	count++;
+
+	if (count != num)
+		goto next;
+
+	return 0;
 }
 
 static int
@@ -1612,7 +1723,7 @@ static int impl_node_process(void *object)
 	if (this->target == this->follower) {
 		if (this->io_position)
 			this->io_rate_match.size = this->io_position->clock.duration;
-		return spa_node_process(this->follower);
+		return spa_node_process_fast(this->follower);
 	}
 
 	if (this->direction == SPA_DIRECTION_INPUT) {
@@ -1620,7 +1731,7 @@ static int impl_node_process(void *object)
 		 * First we run the converter to process the input for the follower
 		 * then if it produced data, we run the follower. */
 		while (retry--) {
-			status = this->convert ? spa_node_process(this->convert) : 0;
+			status = spa_node_process_fast(this->target);
 			/* schedule the follower when the converter needed
 			 * a recycled buffer */
 			if (status == -EPIPE || status == 0)
@@ -1631,7 +1742,7 @@ static int impl_node_process(void *object)
 			if (status & (SPA_STATUS_HAVE_DATA | SPA_STATUS_DRAINED)) {
 				/* as long as the converter produced something or
 				 * is drained, process the follower. */
-				fstatus = spa_node_process(this->follower);
+				fstatus = spa_node_process_fast(this->follower);
 				if (fstatus < 0) {
 					status = fstatus;
 					break;
@@ -1652,24 +1763,20 @@ static int impl_node_process(void *object)
 			/* output node (source). First run the converter to make
 			 * sure we push out any queued data. Then when it needs
 			 * more data, schedule the follower. */
-			status = this->convert ? spa_node_process(this->convert) : 0;
+			status = spa_node_process_fast(this->target);
 			if (status == 0)
 				status = SPA_STATUS_NEED_DATA;
 			else if (status < 0)
 				break;
 
 			done = (status & (SPA_STATUS_HAVE_DATA | SPA_STATUS_DRAINED));
-
-			/* when not async, we can return the data when we are done.
-			 * In async mode we might first need to wake up the follower
-			 * to asynchronously provide more data for the next round. */
-			if (!this->async && done)
+			if (done)
 				break;
 
 			if (status & SPA_STATUS_NEED_DATA) {
 				/* the converter needs more data, schedule the
 				 * follower */
-				fstatus = spa_node_process(this->follower);
+				fstatus = spa_node_process_fast(this->follower);
 				if (fstatus < 0) {
 					status = fstatus;
 					break;
@@ -1679,16 +1786,12 @@ static int impl_node_process(void *object)
 				if ((fstatus & (SPA_STATUS_HAVE_DATA | SPA_STATUS_DRAINED)) == 0)
 					break;
 			}
-			/* converter produced something or is drained and we
-			 * scheduled the follower above, we can stop now*/
-			if (done)
-				break;
 		}
 		if (!done)
 			spa_node_call_xrun(&this->callbacks, 0, 0, NULL);
 
 	} else {
-		status = spa_node_process(this->follower);
+		status = spa_node_process_fast(this->follower);
 	}
 	spa_log_trace_fp(this->log, "%p: process status:%d", this, status);
 
@@ -1721,7 +1824,7 @@ static int load_plugin_from(struct impl *this, const struct spa_dict *info,
 {
 	struct spa_handle *hnd_convert = NULL;
 	void *iface_conv = NULL;
-	hnd_convert = spa_plugin_loader_load(this->ploader, convertname, NULL);
+	hnd_convert = spa_plugin_loader_load(this->ploader, convertname, info);
 	if (!hnd_convert)
 		return -EINVAL;
 
@@ -1788,10 +1891,7 @@ static int impl_clear(struct spa_handle *handle)
 		free(this->convertname);
 	}
 
-	if (this->buffers)
-		free(this->buffers);
-	this->buffers = NULL;
-
+	clear_buffers(this);
 	return 0;
 }
 
@@ -1850,16 +1950,26 @@ impl_init(const struct spa_handle_factory *factory,
 			&impl_node, this);
 
 	ret = load_converter(this, info);
-	spa_log_debug(this->log, "%p: loaded converter %s, hnd %p, convert %p", this, this->convertname, this->hnd_convert, this->convert);
+	spa_log_info(this->log, "%p: loaded converter %s, hnd %p, convert %p", this,
+			this->convertname, this->hnd_convert, this->convert);
 	if (ret < 0)
 		return ret;
-	this->target = this->convert;
+
+	if (this->convert == NULL) {
+		this->target = this->follower;
+		this->passthrough = true;
+	} else {
+		this->target = this->convert;
+		this->passthrough = false;
+	}
 
 	this->info_all = SPA_NODE_CHANGE_MASK_FLAGS |
+		SPA_NODE_CHANGE_MASK_PROPS |
 		SPA_NODE_CHANGE_MASK_PARAMS;
 	this->info = SPA_NODE_INFO_INIT();
 	this->info.flags = SPA_NODE_FLAG_RT |
-		SPA_NODE_FLAG_NEED_CONFIGURE;
+		0;
+		//SPA_NODE_FLAG_NEED_CONFIGURE;
 	this->params[IDX_EnumFormat] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ);
 	this->params[IDX_PropInfo] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ);
 	this->params[IDX_Props] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE);
@@ -1881,11 +1991,15 @@ impl_init(const struct spa_handle_factory *factory,
 		spa_node_add_listener(this->convert,
 				&this->convert_listener, &convert_node_events, this);
 
-		configure_convert(this, SPA_PARAM_PORT_CONFIG_MODE_convert);
+		if (strcmp(this->convertname, "video.convert.dummy") == 0) {
+			configure_convert(this, SPA_PARAM_PORT_CONFIG_MODE_none);
+			reconfigure_mode(this, SPA_PARAM_PORT_CONFIG_MODE_passthrough, this->direction, NULL);
+		} else {
+			configure_convert(this, SPA_PARAM_PORT_CONFIG_MODE_convert);
+		}
 	} else {
-		reconfigure_mode(this, true, this->direction, NULL);
+		reconfigure_mode(this, SPA_PARAM_PORT_CONFIG_MODE_passthrough, this->direction, NULL);
 	}
-
 	link_io(this);
 
 	return 0;
diff --git a/spa/plugins/videoconvert/videoconvert-dummy.c b/spa/plugins/videoconvert/videoconvert-dummy.c
new file mode 100644
index 00000000..fab51d7a
--- /dev/null
+++ b/spa/plugins/videoconvert/videoconvert-dummy.c
@@ -0,0 +1,720 @@
+/* Spa */
+/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */
+/* SPDX-FileCopyrightText: Copyright © 2023 columbarius */
+/* SPDX-License-Identifier: MIT */
+
+#include <errno.h>
+#include <stddef.h>
+#include <unistd.h>
+#include <string.h>
+#include <stdio.h>
+
+#include <spa/support/plugin.h>
+#include <spa/support/loop.h>
+#include <spa/support/log.h>
+#include <spa/utils/result.h>
+#include <spa/utils/list.h>
+#include <spa/utils/keys.h>
+#include <spa/utils/names.h>
+#include <spa/utils/string.h>
+#include <spa/node/node.h>
+#include <spa/node/io.h>
+#include <spa/node/utils.h>
+#include <spa/node/keys.h>
+#include <spa/param/video/format-utils.h>
+#include <spa/param/param.h>
+#include <spa/pod/filter.h>
+#include <spa/debug/types.h>
+
+#undef SPA_LOG_TOPIC_DEFAULT
+#define SPA_LOG_TOPIC_DEFAULT &log_topic
+SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.videoconvert.dummy");
+
+#define MAX_PORTS 1
+
+struct props {
+};
+
+struct port {
+	uint32_t direction;
+	uint32_t id;
+
+	struct spa_io_buffers *io;
+
+	uint64_t info_all;
+	struct spa_port_info info;
+#define IDX_EnumFormat	0
+#define IDX_Meta	1
+#define IDX_IO		2
+#define IDX_Format	3
+#define IDX_Buffers	4
+#define IDX_Latency	5
+#define IDX_Tag		6
+#define N_PORT_PARAMS	7
+	struct spa_param_info params[N_PORT_PARAMS];
+};
+
+struct dir {
+	struct port ports[MAX_PORTS];
+	uint32_t n_ports;
+
+	enum spa_direction direction;
+	enum spa_param_port_config_mode mode;
+
+	struct spa_video_info format;
+	unsigned int have_profile:1;
+	struct spa_pod *tag;
+};
+
+struct impl {
+	struct spa_handle handle;
+	struct spa_node node;
+
+	struct spa_log *log;
+
+	struct props props;
+
+	struct spa_io_position *io_position;
+
+	uint64_t info_all;
+	struct spa_node_info info;
+#define IDX_EnumPortConfig	0
+#define IDX_PortConfig		1
+#define IDX_PropInfo		2
+#define IDX_Props		3
+#define N_NODE_PARAMS		4
+	struct spa_param_info params[N_NODE_PARAMS];
+
+	struct spa_hook_list hooks;
+	struct spa_callbacks callbacks;
+
+	struct dir dir[2];
+};
+
+#define CHECK_PORT(this,d,p)		((p) < this->dir[d].n_ports)
+
+static const struct spa_dict_item node_info_items[] = {
+	{ SPA_KEY_MEDIA_CLASS, "Video/Filter" },
+};
+
+static void emit_node_info(struct impl *this, bool full)
+{
+	uint64_t old = full ? this->info.change_mask : 0;
+
+	if (full)
+		this->info.change_mask = this->info_all;
+	if (this->info.change_mask) {
+		this->info.props = &SPA_DICT_INIT_ARRAY(node_info_items);
+		if (this->info.change_mask & SPA_NODE_CHANGE_MASK_PARAMS) {
+			SPA_FOR_EACH_ELEMENT_VAR(this->params, p) {
+				if (p->user > 0) {
+					p->flags ^= SPA_PARAM_INFO_SERIAL;
+					p->user = 0;
+				}
+			}
+		}
+		spa_node_emit_info(&this->hooks, &this->info);
+		this->info.change_mask = old;
+	}
+}
+
+static void emit_port_info(struct impl *this, struct port *port, bool full)
+{
+	uint64_t old = full ? port->info.change_mask : 0;
+
+	if (full)
+		port->info.change_mask = port->info_all;
+	if (port->info.change_mask) {
+		struct spa_dict_item items[1];
+
+		items[0] = SPA_DICT_ITEM_INIT(SPA_KEY_FORMAT_DSP, "32 bit float RGBA video");
+		port->info.props = &SPA_DICT_INIT(items, 1);
+		spa_node_emit_port_info(&this->hooks, port->direction, port->id, &port->info);
+		port->info.change_mask = old;
+	}
+}
+
+
+static int impl_node_enum_params(void *object, int seq,
+				 uint32_t id, uint32_t start, uint32_t num,
+				 const struct spa_pod *filter)
+{
+	struct impl *this = object;
+	struct spa_pod *param;
+	struct spa_pod_builder b = { 0 };
+	uint8_t buffer[1024];
+	struct spa_result_node_params result;
+	uint32_t count = 0;
+
+	spa_return_val_if_fail(this != NULL, -EINVAL);
+	spa_return_val_if_fail(num != 0, -EINVAL);
+
+	result.id = id;
+	result.next = start;
+      next:
+	result.index = result.next++;
+
+	spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+	switch (id) {
+	case SPA_PARAM_EnumPortConfig:
+	{
+		struct dir *dir;
+		switch (result.index) {
+		case 0:
+			dir = &this->dir[SPA_DIRECTION_INPUT];;
+			break;
+		case 1:
+			dir = &this->dir[SPA_DIRECTION_OUTPUT];;
+			break;
+		default:
+			return 0;
+		}
+		param = spa_pod_builder_add_object(&b,
+			SPA_TYPE_OBJECT_ParamPortConfig, id,
+			SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(dir->direction),
+			SPA_PARAM_PORT_CONFIG_mode,      SPA_POD_Id(SPA_PARAM_PORT_CONFIG_MODE_none),
+			SPA_PARAM_PORT_CONFIG_monitor,   SPA_POD_Bool(false),
+			SPA_PARAM_PORT_CONFIG_control,   SPA_POD_Bool(false));
+		break;
+	}
+	case SPA_PARAM_PortConfig:
+	{
+		struct dir *dir;
+		struct spa_pod_frame f[1];
+
+		switch (result.index) {
+		case 0:
+			dir = &this->dir[SPA_DIRECTION_INPUT];;
+			break;
+		case 1:
+			dir = &this->dir[SPA_DIRECTION_OUTPUT];;
+			break;
+		default:
+			return 0;
+		}
+		spa_pod_builder_push_object(&b, &f[0], SPA_TYPE_OBJECT_ParamPortConfig, id);
+		spa_pod_builder_add(&b,
+			SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(dir->direction),
+			SPA_PARAM_PORT_CONFIG_mode,      SPA_POD_Id(dir->mode),
+			SPA_PARAM_PORT_CONFIG_monitor,   SPA_POD_Bool(false),
+			SPA_PARAM_PORT_CONFIG_control,   SPA_POD_Bool(false),
+			0);
+
+		param = spa_pod_builder_pop(&b, &f[0]);
+		break;
+	}
+	case SPA_PARAM_PropInfo:
+	{
+		switch (result.index) {
+		case 0:
+			param = spa_pod_builder_add_object(&b,
+				SPA_TYPE_OBJECT_PropInfo, id,
+				SPA_PROP_INFO_name,   SPA_POD_String("video.convert.converter"),
+				SPA_PROP_INFO_description, SPA_POD_String("Name of the used videoconverter"),
+				SPA_PROP_INFO_type, SPA_POD_String("dummy"),
+				SPA_PROP_INFO_params, SPA_POD_Bool(true));
+			break;
+		default:
+			return 0;
+		}
+		break;
+	}
+	default:
+		return 0;
+	}
+
+	if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+		goto next;
+
+	spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+	if (++count != num)
+		goto next;
+
+	return 0;
+}
+
+static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size)
+{
+	struct impl *this = object;
+
+	spa_return_val_if_fail(this != NULL, -EINVAL);
+
+	spa_log_debug(this->log, "%p: io %d %p/%zu", this, id, data, size);
+
+	switch (id) {
+	case SPA_IO_Position:
+		if (size > 0 && size < sizeof(struct spa_io_position))
+			return -EINVAL;
+		this->io_position = data;
+		break;
+	default:
+		return -ENOENT;
+	}
+	return 0;
+}
+
+static int reconfigure_mode(struct impl *this, enum spa_param_port_config_mode mode,
+		enum spa_direction direction, struct spa_video_info *info)
+{
+	struct dir *dir;
+	uint32_t i;
+
+	dir = &this->dir[direction];
+
+	if (dir->have_profile && dir->mode == mode &&
+	    (info == NULL || memcmp(&dir->format, info, sizeof(*info)) == 0))
+		return 0;
+
+	spa_log_info(this->log, "%p: port config direction:%d mode:%d %d %p", this,
+			direction, mode, dir->n_ports, info);
+
+	for (i = 0; i < dir->n_ports; i++) {
+		spa_node_emit_port_info(&this->hooks, direction, i, NULL);
+	}
+
+	dir->have_profile = true;
+	dir->mode = mode;
+
+	switch (mode) {
+	case SPA_PARAM_PORT_CONFIG_MODE_none:
+		break;
+	default:
+		return -ENOTSUP;
+	}
+
+	this->info.change_mask |= SPA_NODE_CHANGE_MASK_FLAGS | SPA_NODE_CHANGE_MASK_PARAMS;
+	this->info.flags &= ~SPA_NODE_FLAG_NEED_CONFIGURE;
+	this->params[IDX_Props].user++;
+	this->params[IDX_PortConfig].user++;
+	return 0;
+}
+
+static int impl_node_set_param(void *object, uint32_t id, uint32_t flags,
+			       const struct spa_pod *param)
+{
+	struct impl *this = object;
+
+	spa_return_val_if_fail(this != NULL, -EINVAL);
+
+	if (param == NULL)
+		return 0;
+
+	switch (id) {
+	case SPA_PARAM_PortConfig:
+	{
+		struct spa_video_info info = { 0, }, *infop = NULL;
+		struct spa_pod *format = NULL;
+		enum spa_direction direction;
+		enum spa_param_port_config_mode mode;
+		bool monitor = false, control = false;
+		int res;
+
+		if (spa_pod_parse_object(param,
+				SPA_TYPE_OBJECT_ParamPortConfig, NULL,
+				SPA_PARAM_PORT_CONFIG_direction,	SPA_POD_Id(&direction),
+				SPA_PARAM_PORT_CONFIG_mode,		SPA_POD_Id(&mode),
+				SPA_PARAM_PORT_CONFIG_monitor,		SPA_POD_OPT_Bool(&monitor),
+				SPA_PARAM_PORT_CONFIG_control,		SPA_POD_OPT_Bool(&control),
+				SPA_PARAM_PORT_CONFIG_format,		SPA_POD_OPT_Pod(&format)) < 0)
+			return -EINVAL;
+
+		if (format) {
+			if (!spa_pod_is_object_type(format, SPA_TYPE_OBJECT_Format))
+				return -EINVAL;
+
+			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 -EINVAL;
+
+			if (spa_format_video_raw_parse(format, &info.info.raw) < 0)
+				return -EINVAL;
+
+			if (info.info.raw.format == 0)
+				return -EINVAL;
+
+			infop = &info;
+		}
+
+		if ((res = reconfigure_mode(this, mode, direction, infop)) < 0)
+			return res;
+
+		emit_node_info(this, false);
+		break;
+	}
+
+	default:
+		return -ENOENT;
+	}
+	return 0;
+}
+
+static int impl_node_send_command(void *object, const struct spa_command *command)
+{
+	struct impl *this = object;
+
+	spa_return_val_if_fail(this != NULL, -EINVAL);
+	spa_return_val_if_fail(command != NULL, -EINVAL);
+
+	switch (SPA_NODE_COMMAND_ID(command)) {
+	default:
+		return -ENOTSUP;
+	}
+	return 0;
+}
+
+static int
+impl_node_add_listener(void *object,
+		struct spa_hook *listener,
+		const struct spa_node_events *events,
+		void *data)
+{
+	struct impl *this = object;
+	struct spa_hook_list save;
+
+	spa_return_val_if_fail(this != NULL, -EINVAL);
+
+	spa_log_trace(this->log, "%p: add listener %p", this, listener);
+	spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
+
+	emit_node_info(this, true);
+	emit_port_info(this, &this->dir[0].ports[0], true);
+	emit_port_info(this, &this->dir[1].ports[0], true);
+
+	spa_hook_list_join(&this->hooks, &save);
+
+	return 0;
+}
+
+static int
+impl_node_set_callbacks(void *object,
+			const struct spa_node_callbacks *callbacks,
+			void *data)
+{
+	struct impl *this = object;
+
+	spa_return_val_if_fail(this != NULL, -EINVAL);
+
+	this->callbacks = SPA_CALLBACKS_INIT(callbacks, data);
+
+	return 0;
+}
+
+static int impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id,
+		const struct spa_dict *props)
+{
+	return -ENOTSUP;
+}
+
+static int
+impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id)
+{
+	return -ENOTSUP;
+}
+
+static int port_enum_formats(void *object,
+			     enum spa_direction direction, uint32_t port_id,
+			     uint32_t index,
+			     const struct spa_pod *filter,
+			     struct spa_pod **param,
+			     struct spa_pod_builder *builder)
+{
+	return -ENOTSUP;
+}
+
+
+static int
+impl_node_port_enum_params(void *object, int seq,
+			   enum spa_direction direction, uint32_t port_id,
+			   uint32_t id, uint32_t start, uint32_t num,
+			   const struct spa_pod *filter)
+{
+	struct impl *this = object;
+	struct spa_pod *param;
+	struct spa_pod_builder b = { 0 };
+	uint8_t buffer[4096];
+	struct spa_result_node_params result;
+	uint32_t count = 0;
+	int res;
+
+	spa_return_val_if_fail(this != NULL, -EINVAL);
+	spa_return_val_if_fail(num != 0, -EINVAL);
+
+	spa_log_debug(this->log, "%p: enum params port %d.%d %d %u",
+			this, direction, port_id, seq, id);
+
+	spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+	result.id = id;
+	result.next = start;
+      next:
+	result.index = result.next++;
+
+	spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+	switch (id) {
+	case SPA_PARAM_EnumFormat:
+		if ((res = port_enum_formats(this, direction, port_id,
+						result.index, filter, &param, &b)) <= 0)
+			return res;
+		break;
+	case SPA_PARAM_Meta:
+		switch (result.index) {
+		case 0:
+			param = spa_pod_builder_add_object(&b,
+				SPA_TYPE_OBJECT_ParamMeta, id,
+				SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header),
+				SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header)));
+			break;
+
+		default:
+			return 0;
+		}
+		break;
+	default:
+		return -ENOENT;
+	}
+
+	if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+		goto next;
+
+	spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+	if (++count != num)
+		goto next;
+
+	return 0;
+}
+
+static int port_set_format(struct impl *this,
+			   enum spa_direction direction,
+			   uint32_t port_id,
+			   uint32_t flags,
+			   const struct spa_pod *format)
+{
+	return -ENOTSUP;
+}
+
+
+static int
+impl_node_port_set_param(void *object,
+			 enum spa_direction direction, uint32_t port_id,
+			 uint32_t id, uint32_t flags,
+			 const struct spa_pod *param)
+{
+	struct impl *this = object;
+
+	spa_return_val_if_fail(this != NULL, -EINVAL);
+
+	spa_log_debug(this->log, "%p: set param port %d.%d %u",
+			this, direction, port_id, id);
+
+	spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+	switch (id) {
+	case SPA_PARAM_Format:
+		return port_set_format(this, direction, port_id, flags, param);
+	default:
+		return -ENOENT;
+	}
+}
+
+static int
+impl_node_port_use_buffers(void *object,
+			   enum spa_direction direction,
+			   uint32_t port_id,
+			   uint32_t flags,
+			   struct spa_buffer **buffers,
+			   uint32_t n_buffers)
+{
+	struct impl *this = object;
+
+	spa_return_val_if_fail(this != NULL, -EINVAL);
+
+	spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+	return -ENOTSUP;
+}
+
+static int
+impl_node_port_set_io(void *object,
+		      enum spa_direction direction, uint32_t port_id,
+		      uint32_t id, void *data, size_t size)
+{
+	struct impl *this = object;
+
+	spa_return_val_if_fail(this != NULL, -EINVAL);
+
+	return -ENOTSUP;
+
+}
+
+static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id)
+{
+	struct impl *this = object;
+
+	spa_return_val_if_fail(this != NULL, -EINVAL);
+	spa_return_val_if_fail(CHECK_PORT(this, SPA_DIRECTION_OUTPUT, port_id), -EINVAL);
+
+	return -ENOTSUP;
+}
+
+static int impl_node_process(void *object)
+{
+	struct impl *this = object;
+
+	spa_return_val_if_fail(this != NULL, -EINVAL);
+
+	return -ENOTSUP;
+}
+
+static const struct spa_node_methods impl_node = {
+	SPA_VERSION_NODE_METHODS,
+	.add_listener = impl_node_add_listener,
+	.set_callbacks = impl_node_set_callbacks,
+	.enum_params = impl_node_enum_params,
+	.set_param = impl_node_set_param,
+	.set_io = impl_node_set_io,
+	.send_command = impl_node_send_command,
+	.add_port = impl_node_add_port,
+	.remove_port = impl_node_remove_port,
+	.port_enum_params = impl_node_port_enum_params,
+	.port_set_param = impl_node_port_set_param,
+	.port_use_buffers = impl_node_port_use_buffers,
+	.port_set_io = impl_node_port_set_io,
+	.port_reuse_buffer = impl_node_port_reuse_buffer,
+	.process = impl_node_process,
+};
+
+static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface)
+{
+	struct impl *this;
+
+	spa_return_val_if_fail(handle != NULL, -EINVAL);
+	spa_return_val_if_fail(interface != NULL, -EINVAL);
+
+	this = (struct impl *) handle;
+
+	if (spa_streq(type, SPA_TYPE_INTERFACE_Node))
+		*interface = &this->node;
+	else
+		return -ENOENT;
+
+	return 0;
+}
+
+static int impl_clear(struct spa_handle *handle)
+{
+
+	spa_return_val_if_fail(handle != NULL, -EINVAL);
+
+	return 0;
+}
+
+static size_t
+impl_get_size(const struct spa_handle_factory *factory,
+	      const struct spa_dict *params)
+{
+	return sizeof(struct impl);
+}
+
+static int
+impl_init(const struct spa_handle_factory *factory,
+	  struct spa_handle *handle,
+	  const struct spa_dict *info,
+	  const struct spa_support *support,
+	  uint32_t n_support)
+{
+	struct impl *this;
+	struct dir *dir;
+
+	spa_return_val_if_fail(factory != NULL, -EINVAL);
+	spa_return_val_if_fail(handle != NULL, -EINVAL);
+
+	handle->get_interface = impl_get_interface;
+	handle->clear = impl_clear;
+
+	this = (struct impl *) handle;
+
+	this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
+	spa_log_topic_init(this->log, &log_topic);
+
+	// props_reset(&this->props);
+
+	this->node.iface = SPA_INTERFACE_INIT(
+			SPA_TYPE_INTERFACE_Node,
+			SPA_VERSION_NODE,
+			&impl_node, this);
+	spa_hook_list_init(&this->hooks);
+
+	this->info_all = SPA_NODE_CHANGE_MASK_FLAGS |
+			SPA_NODE_CHANGE_MASK_PROPS |
+			SPA_NODE_CHANGE_MASK_PARAMS;
+	this->info = SPA_NODE_INFO_INIT();
+	this->info.max_output_ports = 1;
+	this->info.max_input_ports = 1;
+	this->info.flags = SPA_NODE_FLAG_RT |
+		SPA_NODE_FLAG_IN_PORT_CONFIG |
+		SPA_NODE_FLAG_OUT_PORT_CONFIG |
+		SPA_NODE_FLAG_NEED_CONFIGURE;
+	this->params[IDX_EnumPortConfig] = SPA_PARAM_INFO(SPA_PARAM_EnumPortConfig, SPA_PARAM_INFO_READ);
+	this->params[IDX_PortConfig] = SPA_PARAM_INFO(SPA_PARAM_PortConfig, SPA_PARAM_INFO_READWRITE);
+	this->params[IDX_PropInfo] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ);
+	this->params[IDX_Props] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE);
+	this->info.params = this->params;
+	this->info.n_params = N_NODE_PARAMS;
+
+	dir = &this->dir[SPA_DIRECTION_INPUT];
+	dir->direction = SPA_DIRECTION_INPUT;
+
+	dir = &this->dir[SPA_DIRECTION_OUTPUT];
+	dir->direction = SPA_DIRECTION_OUTPUT;
+
+	reconfigure_mode(this, SPA_PARAM_PORT_CONFIG_MODE_none, SPA_DIRECTION_INPUT, NULL);
+	reconfigure_mode(this, SPA_PARAM_PORT_CONFIG_MODE_none, SPA_DIRECTION_OUTPUT, NULL);
+	return 0;
+}
+
+static const struct spa_interface_info impl_interfaces[] = {
+	{SPA_TYPE_INTERFACE_Node,},
+};
+
+static int
+impl_enum_interface_info(const struct spa_handle_factory *factory,
+			 const struct spa_interface_info **info,
+			 uint32_t *index)
+{
+	spa_return_val_if_fail(factory != NULL, -EINVAL);
+	spa_return_val_if_fail(info != NULL, -EINVAL);
+	spa_return_val_if_fail(index != NULL, -EINVAL);
+
+	switch (*index) {
+	case 0:
+		*info = &impl_interfaces[*index];
+		break;
+	default:
+		return 0;
+	}
+	(*index)++;
+	return 1;
+}
+
+static const struct spa_dict_item info_items[] = {
+	{ SPA_KEY_FACTORY_AUTHOR, "Columbarius <co1umbarius@protonmail.com>" },
+	{ SPA_KEY_FACTORY_DESCRIPTION, "Dummy video convert plugin" },
+};
+
+static const struct spa_dict info = SPA_DICT_INIT_ARRAY(info_items);
+
+const struct spa_handle_factory spa_videoconvert_dummy_factory = {
+	SPA_VERSION_HANDLE_FACTORY,
+	SPA_NAME_VIDEO_CONVERT_DUMMY,
+	&info,
+	impl_get_size,
+	impl_init,
+	impl_enum_interface_info,
+};
diff --git a/spa/plugins/videoconvert/videoconvert-ffmpeg.c b/spa/plugins/videoconvert/videoconvert-ffmpeg.c
new file mode 100644
index 00000000..955bfa93
--- /dev/null
+++ b/spa/plugins/videoconvert/videoconvert-ffmpeg.c
@@ -0,0 +1,2082 @@
+/* Spa */
+/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */
+/* SPDX-License-Identifier: MIT */
+
+#include <errno.h>
+#include <string.h>
+#include <stdio.h>
+#include <limits.h>
+#include <sys/mman.h>
+
+#include <libavcodec/avcodec.h>
+#include <libavformat/avformat.h>
+#include <libswscale/swscale.h>
+#include <libavutil/pixfmt.h>
+
+#include <spa/support/plugin.h>
+#include <spa/support/cpu.h>
+#include <spa/support/loop.h>
+#include <spa/support/log.h>
+#include <spa/utils/result.h>
+#include <spa/utils/list.h>
+#include <spa/utils/json.h>
+#include <spa/utils/names.h>
+#include <spa/utils/string.h>
+#include <spa/utils/ratelimit.h>
+#include <spa/node/node.h>
+#include <spa/node/io.h>
+#include <spa/node/utils.h>
+#include <spa/node/keys.h>
+#include <spa/param/video/format-utils.h>
+#include <spa/param/param.h>
+#include <spa/param/latency-utils.h>
+#include <spa/param/tag-utils.h>
+#include <spa/pod/filter.h>
+#include <spa/pod/dynamic.h>
+#include <spa/debug/types.h>
+#include <spa/debug/format.h>
+#include <spa/control/ump-utils.h>
+
+#undef SPA_LOG_TOPIC_DEFAULT
+#define SPA_LOG_TOPIC_DEFAULT &log_topic
+SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.videoconvert.ffmpeg");
+
+#define MAX_ALIGN	64u
+#define MAX_BUFFERS	32
+#define MAX_DATAS	4
+#define MAX_PORTS	(1+1)
+
+struct props {
+	unsigned int dummy:1;
+};
+
+static void props_reset(struct props *props)
+{
+	props->dummy = false;
+}
+
+struct buffer {
+	uint32_t id;
+#define BUFFER_FLAG_QUEUED	(1<<0)
+	uint32_t flags;
+	struct spa_list link;
+	struct spa_buffer *buf;
+	void *datas[MAX_DATAS];
+};
+
+struct port {
+	uint32_t direction;
+	uint32_t id;
+
+	struct spa_io_buffers *io;
+
+	uint64_t info_all;
+	struct spa_port_info info;
+#define IDX_EnumFormat	0
+#define IDX_Meta	1
+#define IDX_IO		2
+#define IDX_Format	3
+#define IDX_Buffers	4
+#define IDX_Latency	5
+#define IDX_Tag		6
+#define N_PORT_PARAMS	7
+	struct spa_param_info params[N_PORT_PARAMS];
+
+	struct buffer buffers[MAX_BUFFERS];
+	uint32_t n_buffers;
+
+	struct spa_latency_info latency[2];
+	unsigned int have_latency:1;
+
+	struct spa_video_info format;
+	unsigned int valid:1;
+	unsigned int have_format:1;
+	unsigned int is_dsp:1;
+	unsigned int is_monitor:1;
+	unsigned int is_control:1;
+
+	uint32_t blocks;
+	uint32_t stride;
+	uint32_t maxsize;
+
+	struct spa_list queue;
+};
+
+struct dir {
+	struct port *ports[MAX_PORTS];
+	uint32_t n_ports;
+
+	enum spa_direction direction;
+	enum spa_param_port_config_mode mode;
+
+	struct spa_video_info format;
+	unsigned int have_format:1;
+	unsigned int have_profile:1;
+	struct spa_pod *tag;
+	enum AVPixelFormat pix_fmt;
+	int width;
+	int height;
+
+	unsigned int control:1;
+};
+
+struct impl {
+	struct spa_handle handle;
+	struct spa_node node;
+
+	struct spa_log *log;
+	struct spa_cpu *cpu;
+	struct spa_loop *data_loop;
+
+	uint32_t cpu_flags;
+	uint32_t max_align;
+	uint32_t quantum_limit;
+	enum spa_direction direction;
+
+	struct spa_ratelimit rate_limit;
+
+	struct props props;
+
+	struct spa_io_position *io_position;
+	struct spa_io_rate_match *io_rate_match;
+
+	uint64_t info_all;
+	struct spa_node_info info;
+#define IDX_EnumPortConfig	0
+#define IDX_PortConfig		1
+#define IDX_PropInfo		2
+#define IDX_Props		3
+#define N_NODE_PARAMS		4
+	struct spa_param_info params[N_NODE_PARAMS];
+
+	struct spa_hook_list hooks;
+
+	unsigned int monitor:1;
+
+	struct dir dir[2];
+
+	unsigned int started:1;
+	unsigned int setup:1;
+	unsigned int fmt_passthrough:1;
+	unsigned int drained:1;
+	unsigned int port_ignore_latency:1;
+	unsigned int monitor_passthrough:1;
+
+	char group_name[128];
+
+	struct {
+		const AVCodec *codec;
+		AVCodecContext *context;
+		AVPacket *packet;
+		AVFrame *frame;
+	} decoder;
+	struct {
+		struct SwsContext *context;
+		AVFrame *frame;
+	} convert;
+	struct {
+		const AVCodec *codec;
+		AVCodecContext *context;
+		AVFrame *frame;
+		AVPacket *packet;
+	} encoder;
+};
+
+#define CHECK_PORT(this,d,p)		((p) < this->dir[d].n_ports)
+#define GET_PORT(this,d,p)		(this->dir[d].ports[p])
+#define GET_IN_PORT(this,p)		GET_PORT(this,SPA_DIRECTION_INPUT,p)
+#define GET_OUT_PORT(this,p)		GET_PORT(this,SPA_DIRECTION_OUTPUT,p)
+
+#define PORT_IS_DSP(this,d,p)		(GET_PORT(this,d,p)->is_dsp)
+#define PORT_IS_CONTROL(this,d,p)	(GET_PORT(this,d,p)->is_control)
+
+static void emit_node_info(struct impl *this, bool full)
+{
+	uint64_t old = full ? this->info.change_mask : 0;
+
+	if (full)
+		this->info.change_mask = this->info_all;
+	if (this->info.change_mask) {
+		if (this->info.change_mask & SPA_NODE_CHANGE_MASK_PARAMS) {
+			SPA_FOR_EACH_ELEMENT_VAR(this->params, p) {
+				if (p->user > 0) {
+					p->flags ^= SPA_PARAM_INFO_SERIAL;
+					p->user = 0;
+				}
+			}
+		}
+		spa_node_emit_info(&this->hooks, &this->info);
+		this->info.change_mask = old;
+	}
+}
+
+static void emit_port_info(struct impl *this, struct port *port, bool full)
+{
+	uint64_t old = full ? port->info.change_mask : 0;
+
+	if (full)
+		port->info.change_mask = port->info_all;
+	if (port->info.change_mask) {
+		struct spa_dict_item items[5];
+		uint32_t n_items = 0;
+
+		if (PORT_IS_DSP(this, port->direction, port->id)) {
+			items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_FORMAT_DSP, "32 bit float video");
+			if (port->is_monitor)
+				items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_PORT_MONITOR, "true");
+			if (this->port_ignore_latency)
+				items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_PORT_IGNORE_LATENCY, "true");
+		} else if (PORT_IS_CONTROL(this, port->direction, port->id)) {
+			items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_PORT_NAME, "control");
+			items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_FORMAT_DSP, "32 bit raw UMP");
+		}
+		if (this->group_name[0] != '\0')
+			items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_PORT_GROUP, this->group_name);
+		port->info.props = &SPA_DICT_INIT(items, n_items);
+
+		if (port->info.change_mask & SPA_PORT_CHANGE_MASK_PARAMS) {
+			SPA_FOR_EACH_ELEMENT_VAR(port->params, p) {
+				if (p->user > 0) {
+					p->flags ^= SPA_PARAM_INFO_SERIAL;
+					p->user = 0;
+				}
+			}
+		}
+		spa_node_emit_port_info(&this->hooks, port->direction, port->id, &port->info);
+		port->info.change_mask = old;
+	}
+}
+
+static int init_port(struct impl *this, enum spa_direction direction, uint32_t port_id,
+		bool is_dsp, bool is_monitor, bool is_control)
+{
+	struct port *port = GET_PORT(this, direction, port_id);
+
+	spa_assert(port_id < MAX_PORTS);
+
+	if (port == NULL) {
+		port = calloc(1, sizeof(struct port));
+		if (port == NULL)
+			return -errno;
+		this->dir[direction].ports[port_id] = port;
+	}
+	port->direction = direction;
+	port->id = port_id;
+	port->latency[SPA_DIRECTION_INPUT] = SPA_LATENCY_INFO(SPA_DIRECTION_INPUT);
+	port->latency[SPA_DIRECTION_OUTPUT] = SPA_LATENCY_INFO(SPA_DIRECTION_OUTPUT);
+
+	port->info_all = SPA_PORT_CHANGE_MASK_FLAGS |
+			SPA_PORT_CHANGE_MASK_PROPS |
+			SPA_PORT_CHANGE_MASK_PARAMS;
+	port->info = SPA_PORT_INFO_INIT();
+	port->info.flags = SPA_PORT_FLAG_NO_REF |
+		SPA_PORT_FLAG_DYNAMIC_DATA;
+	port->params[IDX_EnumFormat] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ);
+	port->params[IDX_Meta] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ);
+	port->params[IDX_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
+	port->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+	port->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+	port->params[IDX_Latency] = SPA_PARAM_INFO(SPA_PARAM_Latency, SPA_PARAM_INFO_READWRITE);
+	port->params[IDX_Tag] = SPA_PARAM_INFO(SPA_PARAM_Tag, SPA_PARAM_INFO_READWRITE);
+	port->info.params = port->params;
+	port->info.n_params = N_PORT_PARAMS;
+
+	port->n_buffers = 0;
+	port->have_format = false;
+	port->is_monitor = is_monitor;
+	port->is_dsp = is_dsp;
+	if (port->is_dsp) {
+		port->format.media_type = SPA_MEDIA_TYPE_video;
+		port->format.media_subtype = SPA_MEDIA_SUBTYPE_dsp;
+		port->format.info.dsp.format = SPA_VIDEO_FORMAT_DSP_F32;
+		port->blocks = 1;
+		port->stride = 16;
+	}
+	port->is_control = is_control;
+	if (port->is_control) {
+		port->format.media_type = SPA_MEDIA_TYPE_application;
+		port->format.media_subtype = SPA_MEDIA_SUBTYPE_control;
+		port->blocks = 1;
+		port->stride = 1;
+	}
+	port->valid = true;
+	spa_list_init(&port->queue);
+
+	spa_log_debug(this->log, "%p: add port %d:%d %d %d %d",
+			this, direction, port_id, is_dsp, is_monitor, is_control);
+	emit_port_info(this, port, true);
+
+	return 0;
+}
+
+static int deinit_port(struct impl *this, enum spa_direction direction, uint32_t port_id)
+{
+	struct port *port = GET_PORT(this, direction, port_id);
+	if (port == NULL || !port->valid)
+		return -ENOENT;
+	port->valid = false;
+	spa_node_emit_port_info(&this->hooks, direction, port_id, NULL);
+	return 0;
+}
+
+static int impl_node_enum_params(void *object, int seq,
+				 uint32_t id, uint32_t start, uint32_t num,
+				 const struct spa_pod *filter)
+{
+	struct impl *this = object;
+	struct spa_pod *param;
+	struct spa_pod_builder b = { 0 };
+	uint8_t buffer[4096];
+	struct spa_result_node_params result;
+	uint32_t count = 0;
+
+	spa_return_val_if_fail(this != NULL, -EINVAL);
+	spa_return_val_if_fail(num != 0, -EINVAL);
+
+	result.id = id;
+	result.next = start;
+      next:
+	result.index = result.next++;
+
+	spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+	switch (id) {
+	case SPA_PARAM_EnumPortConfig:
+	{
+		struct dir *dir;
+		switch (result.index) {
+		case 0:
+			dir = &this->dir[SPA_DIRECTION_INPUT];;
+			break;
+		case 1:
+			dir = &this->dir[SPA_DIRECTION_OUTPUT];;
+			break;
+		default:
+			return 0;
+		}
+		param = spa_pod_builder_add_object(&b,
+			SPA_TYPE_OBJECT_ParamPortConfig, id,
+			SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(dir->direction),
+			SPA_PARAM_PORT_CONFIG_mode,      SPA_POD_CHOICE_ENUM_Id(4,
+				SPA_PARAM_PORT_CONFIG_MODE_none,
+				SPA_PARAM_PORT_CONFIG_MODE_none,
+				SPA_PARAM_PORT_CONFIG_MODE_dsp,
+				SPA_PARAM_PORT_CONFIG_MODE_convert),
+			SPA_PARAM_PORT_CONFIG_monitor,   SPA_POD_CHOICE_Bool(false),
+			SPA_PARAM_PORT_CONFIG_control,   SPA_POD_CHOICE_Bool(false));
+		break;
+	}
+	case SPA_PARAM_PortConfig:
+	{
+		struct dir *dir;
+		struct spa_pod_frame f[1];
+
+		switch (result.index) {
+		case 0:
+			dir = &this->dir[SPA_DIRECTION_INPUT];;
+			break;
+		case 1:
+			dir = &this->dir[SPA_DIRECTION_OUTPUT];;
+			break;
+		default:
+			return 0;
+		}
+		spa_pod_builder_push_object(&b, &f[0], SPA_TYPE_OBJECT_ParamPortConfig, id);
+		spa_pod_builder_add(&b,
+			SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(dir->direction),
+			SPA_PARAM_PORT_CONFIG_mode,      SPA_POD_Id(dir->mode),
+			SPA_PARAM_PORT_CONFIG_monitor,   SPA_POD_Bool(this->monitor),
+			SPA_PARAM_PORT_CONFIG_control,   SPA_POD_Bool(dir->control),
+			0);
+
+		if (dir->have_format) {
+			spa_pod_builder_prop(&b, SPA_PARAM_PORT_CONFIG_format, 0);
+			spa_format_video_build(&b, SPA_PARAM_PORT_CONFIG_format,
+					&dir->format);
+		}
+		param = spa_pod_builder_pop(&b, &f[0]);
+		break;
+	}
+	case SPA_PARAM_PropInfo:
+	{
+		switch (result.index) {
+		default:
+			return 0;
+		}
+		break;
+	}
+
+	case SPA_PARAM_Props:
+	{
+		struct spa_pod_frame f[2];
+
+		switch (result.index) {
+		case 0:
+			spa_pod_builder_push_object(&b, &f[0],
+                                SPA_TYPE_OBJECT_Props, id);
+			param = spa_pod_builder_pop(&b, &f[0]);
+			break;
+		default:
+			return 0;
+		}
+		break;
+	}
+	default:
+		return 0;
+	}
+
+	if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+		goto next;
+
+	spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+	if (++count != num)
+		goto next;
+
+	return 0;
+}
+
+static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size)
+{
+	struct impl *this = object;
+
+	spa_return_val_if_fail(this != NULL, -EINVAL);
+
+	spa_log_debug(this->log, "%p: io %d %p/%zd", this, id, data, size);
+
+	switch (id) {
+	case SPA_IO_Position:
+		this->io_position = data;
+		break;
+	default:
+		return -ENOENT;
+	}
+	return 0;
+}
+
+static int videoconvert_set_param(struct impl *this, const char *k, const char *s)
+{
+	return 0;
+}
+
+static int parse_prop_params(struct impl *this, struct spa_pod *params)
+{
+	struct spa_pod_parser prs;
+	struct spa_pod_frame f;
+	int changed = 0;
+
+	spa_pod_parser_pod(&prs, params);
+	if (spa_pod_parser_push_struct(&prs, &f) < 0)
+		return 0;
+
+	while (true) {
+		const char *name;
+		struct spa_pod *pod;
+		char value[512];
+
+		if (spa_pod_parser_get_string(&prs, &name) < 0)
+			break;
+
+		if (spa_pod_parser_get_pod(&prs, &pod) < 0)
+			break;
+
+		if (spa_pod_is_string(pod)) {
+			spa_pod_copy_string(pod, sizeof(value), value);
+		} else if (spa_pod_is_float(pod)) {
+			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));
+		} else if (spa_pod_is_bool(pod)) {
+			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 += videoconvert_set_param(this, name, value);
+	}
+	return changed;
+}
+
+static int apply_props(struct impl *this, const struct spa_pod *param)
+{
+	struct spa_pod_prop *prop;
+	struct spa_pod_object *obj = (struct spa_pod_object *) param;
+	int changed = 0;
+
+	SPA_POD_OBJECT_FOREACH(obj, prop) {
+		switch (prop->key) {
+		case SPA_PROP_params:
+			changed += parse_prop_params(this, &prop->value);
+			break;
+		default:
+			break;
+		}
+	}
+	return changed;
+}
+
+static int reconfigure_mode(struct impl *this, enum spa_param_port_config_mode mode,
+		enum spa_direction direction, bool monitor, bool control, struct spa_video_info *info)
+{
+	struct dir *dir;
+	uint32_t i;
+
+	dir = &this->dir[direction];
+
+	if (dir->have_profile && this->monitor == monitor && dir->mode == mode &&
+	    dir->control == control &&
+	    (info == NULL || memcmp(&dir->format, info, sizeof(*info)) == 0))
+		return 0;
+
+	spa_log_debug(this->log, "%p: port config direction:%d monitor:%d "
+			"control:%d mode:%d %d", this, direction, monitor,
+			control, mode, dir->n_ports);
+
+	for (i = 0; i < dir->n_ports; i++) {
+		deinit_port(this, direction, i);
+		if (this->monitor && direction == SPA_DIRECTION_INPUT)
+			deinit_port(this, SPA_DIRECTION_OUTPUT, i+1);
+	}
+
+	this->monitor = monitor;
+	this->setup = false;
+	dir->control = control;
+	dir->have_profile = true;
+	dir->mode = mode;
+
+	switch (mode) {
+	case SPA_PARAM_PORT_CONFIG_MODE_dsp:
+	{
+		if (info) {
+			dir->n_ports = 1;
+			dir->format = *info;
+			dir->format.info.dsp.format = SPA_VIDEO_FORMAT_DSP_F32;
+			dir->have_format = true;
+		} else {
+			dir->n_ports = 0;
+		}
+
+		if (this->monitor && direction == SPA_DIRECTION_INPUT)
+			this->dir[SPA_DIRECTION_OUTPUT].n_ports = dir->n_ports + 1;
+
+		for (i = 0; i < dir->n_ports; i++) {
+			init_port(this, direction, i, true, false, false);
+			if (this->monitor && direction == SPA_DIRECTION_INPUT)
+				init_port(this, SPA_DIRECTION_OUTPUT, i+1, true, true, false);
+		}
+		break;
+	}
+	case SPA_PARAM_PORT_CONFIG_MODE_convert:
+	{
+		dir->n_ports = 1;
+		dir->have_format = false;
+		init_port(this, direction, 0, false, false, false);
+		break;
+	}
+	case SPA_PARAM_PORT_CONFIG_MODE_none:
+		break;
+	default:
+		return -ENOTSUP;
+	}
+	if (direction == SPA_DIRECTION_INPUT && dir->control) {
+		i = dir->n_ports++;
+		init_port(this, direction, i, false, false, true);
+	}
+	/* when output is convert mode, we are in OUTPUT (merge) mode, we always output all
+	 * the incoming data to output. When output is DSP, we need to output quantum size
+	 * chunks. */
+	this->direction = this->dir[SPA_DIRECTION_OUTPUT].mode == SPA_PARAM_PORT_CONFIG_MODE_convert ?
+		SPA_DIRECTION_OUTPUT : SPA_DIRECTION_INPUT;
+
+	this->info.change_mask |= SPA_NODE_CHANGE_MASK_FLAGS | SPA_NODE_CHANGE_MASK_PARAMS;
+	this->info.flags &= ~SPA_NODE_FLAG_NEED_CONFIGURE;
+	this->params[IDX_Props].user++;
+	this->params[IDX_PortConfig].user++;
+	return 0;
+}
+
+static int impl_node_set_param(void *object, uint32_t id, uint32_t flags,
+			       const struct spa_pod *param)
+{
+	struct impl *this = object;
+
+	spa_return_val_if_fail(this != NULL, -EINVAL);
+
+	if (param == NULL)
+		return 0;
+
+	switch (id) {
+	case SPA_PARAM_PortConfig:
+	{
+		struct spa_video_info info = { 0, }, *infop = NULL;
+		struct spa_pod *format = NULL;
+		enum spa_direction direction;
+		enum spa_param_port_config_mode mode;
+		bool monitor = false, control = false;
+		int res;
+
+		if (spa_pod_parse_object(param,
+				SPA_TYPE_OBJECT_ParamPortConfig, NULL,
+				SPA_PARAM_PORT_CONFIG_direction,	SPA_POD_Id(&direction),
+				SPA_PARAM_PORT_CONFIG_mode,		SPA_POD_Id(&mode),
+				SPA_PARAM_PORT_CONFIG_monitor,		SPA_POD_OPT_Bool(&monitor),
+				SPA_PARAM_PORT_CONFIG_control,		SPA_POD_OPT_Bool(&control),
+				SPA_PARAM_PORT_CONFIG_format,		SPA_POD_OPT_Pod(&format)) < 0)
+			return -EINVAL;
+
+		if (format) {
+			if (!spa_pod_is_object_type(format, SPA_TYPE_OBJECT_Format))
+				return -EINVAL;
+
+			if ((res = spa_format_video_parse(format, &info)) < 0)
+				return res;
+
+			infop = &info;
+		}
+
+		if ((res = reconfigure_mode(this, mode, direction, monitor, control, infop)) < 0)
+			return res;
+
+		emit_node_info(this, false);
+		break;
+	}
+	case SPA_PARAM_Props:
+		if (apply_props(this, param) > 0)
+			emit_node_info(this, false);
+		break;
+	default:
+		return -ENOENT;
+	}
+	return 0;
+}
+
+static enum AVPixelFormat format_to_pix_fmt(uint32_t format)
+{
+	switch (format) {
+	case SPA_VIDEO_FORMAT_I420:
+		return AV_PIX_FMT_YUV420P;
+	case SPA_VIDEO_FORMAT_YV12:
+		break;
+	case SPA_VIDEO_FORMAT_YUY2:
+		return AV_PIX_FMT_YUYV422;
+	case SPA_VIDEO_FORMAT_UYVY:
+		return AV_PIX_FMT_UYVY422;
+	case SPA_VIDEO_FORMAT_AYUV:
+		break;
+	case SPA_VIDEO_FORMAT_RGBx:
+		return AV_PIX_FMT_RGB0;
+	case SPA_VIDEO_FORMAT_BGRx:
+		return AV_PIX_FMT_BGR0;
+	case SPA_VIDEO_FORMAT_xRGB:
+		return AV_PIX_FMT_0RGB;
+	case SPA_VIDEO_FORMAT_xBGR:
+		return AV_PIX_FMT_0BGR;
+	case SPA_VIDEO_FORMAT_RGBA:
+		return AV_PIX_FMT_RGBA;
+	case SPA_VIDEO_FORMAT_BGRA:
+		return AV_PIX_FMT_BGRA;
+	case SPA_VIDEO_FORMAT_ARGB:
+		return AV_PIX_FMT_ARGB;
+	case SPA_VIDEO_FORMAT_ABGR:
+		return AV_PIX_FMT_ABGR;
+	case SPA_VIDEO_FORMAT_RGB:
+		return AV_PIX_FMT_RGB24;
+	case SPA_VIDEO_FORMAT_BGR:
+		return AV_PIX_FMT_BGR24;
+	case SPA_VIDEO_FORMAT_Y41B:
+		return AV_PIX_FMT_YUV411P;
+	case SPA_VIDEO_FORMAT_Y42B:
+		return AV_PIX_FMT_YUV422P;
+	case SPA_VIDEO_FORMAT_YVYU:
+		return AV_PIX_FMT_YVYU422;
+	case SPA_VIDEO_FORMAT_Y444:
+		return AV_PIX_FMT_YUV444P;
+	case SPA_VIDEO_FORMAT_v210:
+	case SPA_VIDEO_FORMAT_v216:
+		break;
+	case SPA_VIDEO_FORMAT_NV12:
+		return AV_PIX_FMT_NV12;
+	case SPA_VIDEO_FORMAT_NV21:
+		return AV_PIX_FMT_NV21;
+	case SPA_VIDEO_FORMAT_GRAY8:
+		return AV_PIX_FMT_GRAY8;
+	case SPA_VIDEO_FORMAT_GRAY16_BE:
+		return AV_PIX_FMT_GRAY16BE;
+	case SPA_VIDEO_FORMAT_GRAY16_LE:
+		return AV_PIX_FMT_GRAY16LE;
+	case SPA_VIDEO_FORMAT_v308:
+		break;
+	case SPA_VIDEO_FORMAT_RGB16:
+		return AV_PIX_FMT_RGB565;
+	case SPA_VIDEO_FORMAT_BGR16:
+		break;
+	case SPA_VIDEO_FORMAT_RGB15:
+		return AV_PIX_FMT_RGB555;
+	case SPA_VIDEO_FORMAT_BGR15:
+	case SPA_VIDEO_FORMAT_UYVP:
+		break;
+	case SPA_VIDEO_FORMAT_A420:
+		return AV_PIX_FMT_YUVA420P;
+	case SPA_VIDEO_FORMAT_RGB8P:
+		return AV_PIX_FMT_PAL8;
+	case SPA_VIDEO_FORMAT_YUV9:
+		return AV_PIX_FMT_YUV410P;
+	case SPA_VIDEO_FORMAT_YVU9:
+	case SPA_VIDEO_FORMAT_IYU1:
+	case SPA_VIDEO_FORMAT_ARGB64:
+	case SPA_VIDEO_FORMAT_AYUV64:
+	case SPA_VIDEO_FORMAT_r210:
+		break;
+	case SPA_VIDEO_FORMAT_I420_10BE:
+		return AV_PIX_FMT_YUV420P10BE;
+	case SPA_VIDEO_FORMAT_I420_10LE:
+		return AV_PIX_FMT_YUV420P10LE;
+	case SPA_VIDEO_FORMAT_I422_10BE:
+		return AV_PIX_FMT_YUV422P10BE;
+	case SPA_VIDEO_FORMAT_I422_10LE:
+		return AV_PIX_FMT_YUV422P10LE;
+	case SPA_VIDEO_FORMAT_Y444_10BE:
+		return AV_PIX_FMT_YUV444P10BE;
+	case SPA_VIDEO_FORMAT_Y444_10LE:
+		return AV_PIX_FMT_YUV444P10LE;
+	case SPA_VIDEO_FORMAT_GBR:
+		return AV_PIX_FMT_GBRP;
+	case SPA_VIDEO_FORMAT_GBR_10BE:
+		return AV_PIX_FMT_GBRP10BE;
+	case SPA_VIDEO_FORMAT_GBR_10LE:
+		return AV_PIX_FMT_GBRP10LE;
+	case SPA_VIDEO_FORMAT_NV16:
+	case SPA_VIDEO_FORMAT_NV24:
+	case SPA_VIDEO_FORMAT_NV12_64Z32:
+		break;
+	case SPA_VIDEO_FORMAT_A420_10BE:
+		return AV_PIX_FMT_YUVA420P10BE;
+	case SPA_VIDEO_FORMAT_A420_10LE:
+		return AV_PIX_FMT_YUVA420P10LE;
+	case SPA_VIDEO_FORMAT_A422_10BE:
+		return AV_PIX_FMT_YUVA422P10BE;
+	case SPA_VIDEO_FORMAT_A422_10LE:
+		return AV_PIX_FMT_YUVA422P10LE;
+	case SPA_VIDEO_FORMAT_A444_10BE:
+		return AV_PIX_FMT_YUVA444P10BE;
+	case SPA_VIDEO_FORMAT_A444_10LE:
+		return AV_PIX_FMT_YUVA444P10LE;
+	case SPA_VIDEO_FORMAT_NV61:
+	case SPA_VIDEO_FORMAT_P010_10BE:
+	case SPA_VIDEO_FORMAT_P010_10LE:
+	case SPA_VIDEO_FORMAT_IYU2:
+	case SPA_VIDEO_FORMAT_VYUY:
+		break;
+	case SPA_VIDEO_FORMAT_GBRA:
+		return AV_PIX_FMT_GBRAP;
+	case SPA_VIDEO_FORMAT_GBRA_10BE:
+		return AV_PIX_FMT_GBRAP10BE;
+	case SPA_VIDEO_FORMAT_GBRA_10LE:
+		return AV_PIX_FMT_GBRAP10LE;
+	case SPA_VIDEO_FORMAT_GBR_12BE:
+		return AV_PIX_FMT_GBRP12BE;
+	case SPA_VIDEO_FORMAT_GBR_12LE:
+		return AV_PIX_FMT_GBRP12LE;
+	case SPA_VIDEO_FORMAT_GBRA_12BE:
+		return AV_PIX_FMT_GBRAP12BE;
+	case SPA_VIDEO_FORMAT_GBRA_12LE:
+		return AV_PIX_FMT_GBRAP12LE;
+	case SPA_VIDEO_FORMAT_I420_12BE:
+		return AV_PIX_FMT_YUV420P12BE;
+	case SPA_VIDEO_FORMAT_I420_12LE:
+		return AV_PIX_FMT_YUV420P12LE;
+	case SPA_VIDEO_FORMAT_I422_12BE:
+		return AV_PIX_FMT_YUV422P12BE;
+	case SPA_VIDEO_FORMAT_I422_12LE:
+		return AV_PIX_FMT_YUV422P12LE;
+	case SPA_VIDEO_FORMAT_Y444_12BE:
+		return AV_PIX_FMT_YUV444P12BE;
+	case SPA_VIDEO_FORMAT_Y444_12LE:
+		return AV_PIX_FMT_YUV444P12LE;
+
+	case SPA_VIDEO_FORMAT_RGBA_F16:
+	case SPA_VIDEO_FORMAT_RGBA_F32:
+		break;
+
+	case SPA_VIDEO_FORMAT_xRGB_210LE:
+		return AV_PIX_FMT_X2RGB10LE;
+	case SPA_VIDEO_FORMAT_xBGR_210LE:
+		return AV_PIX_FMT_X2BGR10LE;
+
+	case SPA_VIDEO_FORMAT_RGBx_102LE:
+	case SPA_VIDEO_FORMAT_BGRx_102LE:
+	case SPA_VIDEO_FORMAT_ARGB_210LE:
+	case SPA_VIDEO_FORMAT_ABGR_210LE:
+	case SPA_VIDEO_FORMAT_RGBA_102LE:
+	case SPA_VIDEO_FORMAT_BGRA_102LE:
+		break;
+	default:
+		break;
+	}
+	return AV_PIX_FMT_NONE;
+}
+
+static int get_format(struct dir *dir, int *width, int *height, uint32_t *format)
+{
+	if (dir->have_format) {
+		switch (dir->format.media_subtype) {
+		case SPA_MEDIA_SUBTYPE_raw:
+			*width = dir->format.info.raw.size.width;
+			*height = dir->format.info.raw.size.height;
+			*format = dir->format.info.raw.format;
+			break;
+		case SPA_MEDIA_SUBTYPE_mjpg:
+			*width = dir->format.info.mjpg.size.width;
+			*height = dir->format.info.mjpg.size.height;
+			break;
+		case SPA_MEDIA_SUBTYPE_h264:
+			*width = dir->format.info.h264.size.width;
+			*height = dir->format.info.h264.size.height;
+			break;
+		default:
+			*width = *height = 0;
+			break;
+		}
+	} else {
+		*width = *height = 0;
+	}
+	return 0;
+}
+
+
+static int setup_convert(struct impl *this)
+{
+	struct dir *in, *out;
+	uint32_t format;
+
+	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 -EIO;
+
+	switch (in->format.media_subtype) {
+	case SPA_MEDIA_SUBTYPE_raw:
+		in->pix_fmt = format_to_pix_fmt(in->format.info.raw.format);
+		switch (out->format.media_subtype) {
+		case SPA_MEDIA_SUBTYPE_raw:
+			out->pix_fmt = format_to_pix_fmt(out->format.info.raw.format);
+			break;
+		case SPA_MEDIA_SUBTYPE_mjpg:
+			if ((this->encoder.codec = avcodec_find_encoder(AV_CODEC_ID_MJPEG)) == NULL) {
+				spa_log_error(this->log, "failed to find MJPEG encoder");
+				return -ENOTSUP;
+			}
+			out->format.media_subtype = SPA_MEDIA_SUBTYPE_raw;
+			out->format.info.raw.format = SPA_VIDEO_FORMAT_I420;
+			out->format.info.raw.size = in->format.info.raw.size;
+			out->pix_fmt = AV_PIX_FMT_YUVJ420P;
+			break;
+		case SPA_MEDIA_SUBTYPE_h264:
+			if ((this->encoder.codec = avcodec_find_encoder(AV_CODEC_ID_H264)) == NULL) {
+				spa_log_error(this->log, "failed to find H264 encoder");
+				return -ENOTSUP;
+			}
+			break;
+		default:
+			return -ENOTSUP;
+		}
+		break;
+	case SPA_MEDIA_SUBTYPE_mjpg:
+		switch (out->format.media_subtype) {
+		case SPA_MEDIA_SUBTYPE_mjpg:
+			/* passthrough */
+			break;
+		case SPA_MEDIA_SUBTYPE_raw:
+			out->pix_fmt = format_to_pix_fmt(out->format.info.raw.format);
+			if ((this->decoder.codec = avcodec_find_decoder(AV_CODEC_ID_MJPEG)) == NULL) {
+				spa_log_error(this->log, "failed to find MJPEG decoder");
+				return -ENOTSUP;
+			}
+			break;
+		default:
+			return -ENOTSUP;
+		}
+		break;
+	case SPA_MEDIA_SUBTYPE_h264:
+		switch (out->format.media_subtype) {
+		case SPA_MEDIA_SUBTYPE_h264:
+			/* passthrough */
+			break;
+		case SPA_MEDIA_SUBTYPE_raw:
+			out->pix_fmt = format_to_pix_fmt(out->format.info.raw.format);
+			if ((this->decoder.codec = avcodec_find_decoder(AV_CODEC_ID_H264)) == NULL) {
+				spa_log_error(this->log, "failed to find H264 decoder");
+				return -ENOTSUP;
+			}
+			break;
+		default:
+			return -ENOTSUP;
+		}
+		break;
+	default:
+		return -ENOTSUP;
+	}
+
+	get_format(in, &in->width, &in->height, &format);
+	get_format(out, &out->width, &out->height, &format);
+
+	if (this->decoder.codec) {
+		if ((this->decoder.context = avcodec_alloc_context3(this->decoder.codec)) == NULL)
+			return -EIO;
+
+		if ((this->decoder.packet = av_packet_alloc()) == NULL)
+			return -EIO;
+
+		this->decoder.context->flags2 |= AV_CODEC_FLAG2_FAST;
+
+		if (avcodec_open2(this->decoder.context, this->decoder.codec, NULL) < 0) {
+			spa_log_error(this->log, "failed to open decoder codec");
+			return -EIO;
+		}
+	}
+	if ((this->decoder.frame = av_frame_alloc()) == NULL)
+		return -EIO;
+	if (this->encoder.codec) {
+		if ((this->encoder.context = avcodec_alloc_context3(this->encoder.codec)) == NULL)
+			return -EIO;
+
+		if ((this->encoder.packet = av_packet_alloc()) == NULL)
+			return -EIO;
+		if ((this->encoder.frame = av_frame_alloc()) == NULL)
+			return -EIO;
+
+		this->encoder.context->flags2 |= AV_CODEC_FLAG2_FAST;
+		this->encoder.context->time_base.num = 1;
+		this->encoder.context->width = out->width;
+		this->encoder.context->height = out->height;
+		this->encoder.context->pix_fmt = out->pix_fmt;
+
+		if (avcodec_open2(this->encoder.context, this->encoder.codec, NULL) < 0) {
+			spa_log_error(this->log, "failed to open encoder codec");
+			return -EIO;
+		}
+	}
+	if ((this->convert.frame = av_frame_alloc()) == NULL)
+		return -EIO;
+
+
+	this->setup = true;
+
+	emit_node_info(this, false);
+
+	return 0;
+}
+
+static void reset_node(struct impl *this)
+{
+}
+
+static int impl_node_send_command(void *object, const struct spa_command *command)
+{
+	struct impl *this = object;
+	int res;
+
+	spa_return_val_if_fail(this != NULL, -EINVAL);
+	spa_return_val_if_fail(command != NULL, -EINVAL);
+
+	switch (SPA_NODE_COMMAND_ID(command)) {
+	case SPA_NODE_COMMAND_Start:
+		if (this->started)
+			return 0;
+		if ((res = setup_convert(this)) < 0)
+			return res;
+		this->started = true;
+		break;
+	case SPA_NODE_COMMAND_Suspend:
+		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;
+	}
+	return 0;
+}
+
+static int
+impl_node_add_listener(void *object,
+		struct spa_hook *listener,
+		const struct spa_node_events *events,
+		void *data)
+{
+	struct impl *this = object;
+	uint32_t i;
+	struct spa_hook_list save;
+	struct port *p;
+
+	spa_return_val_if_fail(this != NULL, -EINVAL);
+
+	spa_log_trace(this->log, "%p: add listener %p", this, listener);
+	spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
+
+	emit_node_info(this, true);
+	for (i = 0; i < this->dir[SPA_DIRECTION_INPUT].n_ports; i++) {
+		if ((p = GET_IN_PORT(this, i)) && p->valid)
+			emit_port_info(this, p, true);
+	}
+	for (i = 0; i < this->dir[SPA_DIRECTION_OUTPUT].n_ports; i++) {
+		if ((p = GET_OUT_PORT(this, i)) && p->valid)
+			emit_port_info(this, p, true);
+	}
+	spa_hook_list_join(&this->hooks, &save);
+
+	return 0;
+}
+
+static int
+impl_node_set_callbacks(void *object,
+			const struct spa_node_callbacks *callbacks,
+			void *user_data)
+{
+	return 0;
+}
+
+static int impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id,
+		const struct spa_dict *props)
+{
+	return -ENOTSUP;
+}
+
+static int
+impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id)
+{
+	return -ENOTSUP;
+}
+
+static int port_enum_formats(void *object,
+			     enum spa_direction direction, uint32_t port_id,
+			     uint32_t index,
+			     struct spa_pod **param,
+			     struct spa_pod_builder *builder)
+{
+	struct impl *this = object;
+	struct dir *other = &this->dir[SPA_DIRECTION_REVERSE(direction)];
+	struct spa_pod_frame f[1];
+	int width, height;
+	uint32_t format = 0;
+
+	get_format(other, &width, &height, &format);
+
+	switch (index) {
+	case 0:
+		if (PORT_IS_DSP(this, direction, port_id)) {
+			struct spa_video_info_dsp info;
+			info.format = SPA_VIDEO_FORMAT_DSP_F32;
+			*param = spa_format_video_dsp_build(builder,
+				SPA_PARAM_EnumFormat, &info);
+		} else if (PORT_IS_CONTROL(this, direction, port_id)) {
+			*param = spa_pod_builder_add_object(builder,
+				SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat,
+				SPA_FORMAT_mediaType,      SPA_POD_Id(SPA_MEDIA_TYPE_application),
+				SPA_FORMAT_mediaSubtype,   SPA_POD_Id(SPA_MEDIA_SUBTYPE_control),
+				SPA_FORMAT_CONTROL_types,  SPA_POD_CHOICE_FLAGS_Int(
+					(1u<<SPA_CONTROL_UMP) | (1u<<SPA_CONTROL_Properties)));
+		} else {
+			if (other->have_format) {
+				*param = spa_format_video_build(builder, SPA_PARAM_EnumFormat, &other->format);
+			} else {
+				*param = NULL;
+			}
+		}
+		break;
+	case 1:
+		if (PORT_IS_DSP(this, direction, port_id) ||
+		    PORT_IS_CONTROL(this, direction, port_id))
+			return 0;
+
+		spa_pod_builder_push_object(builder, &f[0],
+				SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat);
+		spa_pod_builder_add(builder,
+			SPA_FORMAT_mediaType,      SPA_POD_Id(SPA_MEDIA_TYPE_video),
+			SPA_FORMAT_mediaSubtype,   SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw),
+			SPA_FORMAT_VIDEO_format,   SPA_POD_CHOICE_ENUM_Id(7,
+						format,
+						SPA_VIDEO_FORMAT_YUY2,
+						SPA_VIDEO_FORMAT_I420,
+						SPA_VIDEO_FORMAT_UYVY,
+						SPA_VIDEO_FORMAT_YVYU,
+						SPA_VIDEO_FORMAT_RGBA,
+						SPA_VIDEO_FORMAT_BGRx),
+			0);
+		if (width != 0 && height != 0) {
+			spa_pod_builder_add(builder,
+				SPA_FORMAT_VIDEO_size,     SPA_POD_CHOICE_RANGE_Rectangle(
+					&SPA_RECTANGLE(width, height),
+					&SPA_RECTANGLE(1, 1),
+					&SPA_RECTANGLE(INT32_MAX, INT32_MAX)),
+				0);
+		}
+		*param = spa_pod_builder_pop(builder, &f[0]);
+		break;
+	case 2:
+		if (PORT_IS_DSP(this, direction, port_id) ||
+		    PORT_IS_CONTROL(this, direction, port_id))
+			return 0;
+
+		spa_pod_builder_push_object(builder, &f[0],
+				SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat);
+		spa_pod_builder_add(builder,
+			SPA_FORMAT_mediaType,      SPA_POD_Id(SPA_MEDIA_TYPE_video),
+			SPA_FORMAT_mediaSubtype,   SPA_POD_Id(SPA_MEDIA_SUBTYPE_mjpg),
+			0);
+		if (width != 0 && height != 0) {
+			spa_pod_builder_add(builder,
+				SPA_FORMAT_VIDEO_size,     SPA_POD_CHOICE_RANGE_Rectangle(
+					&SPA_RECTANGLE(width, height),
+					&SPA_RECTANGLE(1, 1),
+					&SPA_RECTANGLE(INT32_MAX, INT32_MAX)),
+				0);
+		}
+		*param = spa_pod_builder_pop(builder, &f[0]);
+		break;
+	default:
+		return 0;
+	}
+	return 1;
+}
+
+static int
+impl_node_port_enum_params(void *object, int seq,
+			   enum spa_direction direction, uint32_t port_id,
+			   uint32_t id, uint32_t start, uint32_t num,
+			   const struct spa_pod *filter)
+{
+	struct impl *this = object;
+	struct port *port, *other;
+	struct spa_pod *param;
+	struct spa_pod_builder b = { 0 };
+	uint8_t buffer[4096];
+	struct spa_result_node_params result;
+	uint32_t count = 0;
+	int res;
+
+	spa_return_val_if_fail(this != NULL, -EINVAL);
+	spa_return_val_if_fail(num != 0, -EINVAL);
+
+	spa_log_debug(this->log, "%p: enum params port %d.%d %d %u",
+			this, direction, port_id, seq, id);
+
+	spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+	port = GET_PORT(this, direction, port_id);
+
+	result.id = id;
+	result.next = start;
+      next:
+	result.index = result.next++;
+
+	spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+	switch (id) {
+	case SPA_PARAM_EnumFormat:
+		if ((res = port_enum_formats(object, direction, port_id, result.index, &param, &b)) <= 0)
+			return res;
+		break;
+	case SPA_PARAM_Format:
+		if (!port->have_format)
+			return -EIO;
+		if (result.index > 0)
+			return 0;
+
+		if (PORT_IS_DSP(this, direction, port_id))
+			param = spa_format_video_dsp_build(&b, id, &port->format.info.dsp);
+		else if (PORT_IS_CONTROL(this, direction, port_id))
+			param = spa_pod_builder_add_object(&b,
+				SPA_TYPE_OBJECT_Format,  id,
+				SPA_FORMAT_mediaType,    SPA_POD_Id(SPA_MEDIA_TYPE_application),
+				SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_control),
+				SPA_FORMAT_CONTROL_types,  SPA_POD_Int(
+					(1u<<SPA_CONTROL_UMP) | (1u<<SPA_CONTROL_Properties)));
+		else
+			param = spa_format_video_build(&b, id, &port->format);
+		break;
+	case SPA_PARAM_Buffers:
+	{
+		uint32_t size, min, max;
+
+		if (!port->have_format)
+			return -EIO;
+		if (result.index > 0)
+			return 0;
+
+		if (PORT_IS_DSP(this, direction, port_id)) {
+			size = 1024 * 1024 * 16;
+		} else {
+			size = 1024 * 1024 * 4;
+		}
+
+		other = GET_PORT(this, SPA_DIRECTION_REVERSE(direction), port_id);
+		if (other->n_buffers > 0) {
+			min = max = other->n_buffers;
+		} else {
+			min = 2;
+			max = MAX_BUFFERS;
+		}
+
+		param = spa_pod_builder_add_object(&b,
+			SPA_TYPE_OBJECT_ParamBuffers, id,
+			SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(8, min, max),
+			SPA_PARAM_BUFFERS_blocks,  SPA_POD_Int(port->blocks),
+			SPA_PARAM_BUFFERS_size,    SPA_POD_CHOICE_RANGE_Int(
+								size * port->stride,
+								16 * port->stride,
+								INT32_MAX),
+			SPA_PARAM_BUFFERS_stride,  SPA_POD_Int(port->stride));
+		break;
+	}
+	case SPA_PARAM_Meta:
+		switch (result.index) {
+		case 0:
+			param = spa_pod_builder_add_object(&b,
+				SPA_TYPE_OBJECT_ParamMeta, id,
+				SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header),
+				SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header)));
+			break;
+		default:
+			return 0;
+		}
+		break;
+	case SPA_PARAM_IO:
+		switch (result.index) {
+		case 0:
+			param = spa_pod_builder_add_object(&b,
+				SPA_TYPE_OBJECT_ParamIO, id,
+				SPA_PARAM_IO_id,   SPA_POD_Id(SPA_IO_Buffers),
+				SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers)));
+			break;
+		default:
+			return 0;
+		}
+		break;
+	case SPA_PARAM_Latency:
+		switch (result.index) {
+		case 0: case 1:
+		{
+			uint32_t idx = result.index;
+			param = spa_latency_build(&b, id, &port->latency[idx]);
+			break;
+		}
+		default:
+			return 0;
+		}
+		break;
+	case SPA_PARAM_Tag:
+		switch (result.index) {
+		case 0: case 1:
+		{
+			uint32_t idx = result.index;
+			if (port->is_monitor)
+				idx = idx ^ 1;
+			param = this->dir[idx].tag;
+			if (param == NULL)
+				goto next;
+			break;
+		}
+		default:
+			return 0;
+		}
+		break;
+	default:
+		return -ENOENT;
+	}
+
+	if (param == NULL || spa_pod_filter(&b, &result.param, param, filter) < 0)
+		goto next;
+
+	spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+	if (++count != num)
+		goto next;
+
+	return 0;
+}
+
+static int clear_buffers(struct impl *this, struct port *port)
+{
+	if (port->n_buffers > 0) {
+		spa_log_debug(this->log, "%p: clear buffers %p", this, port);
+		port->n_buffers = 0;
+		spa_list_init(&port->queue);
+	}
+	return 0;
+}
+
+static int port_set_latency(void *object,
+			   enum spa_direction direction,
+			   uint32_t port_id,
+			   uint32_t flags,
+			   const struct spa_pod *latency)
+{
+	struct impl *this = object;
+	struct port *port, *oport;
+	enum spa_direction other = SPA_DIRECTION_REVERSE(direction);
+	struct spa_latency_info info;
+	bool have_latency, emit = false;;
+	uint32_t i;
+
+	spa_log_debug(this->log, "%p: set latency direction:%d id:%d %p",
+			this, direction, port_id, latency);
+
+	port = GET_PORT(this, direction, port_id);
+	if (latency == NULL) {
+		info = SPA_LATENCY_INFO(other);
+		have_latency = false;
+	} else {
+		if (spa_latency_parse(latency, &info) < 0 ||
+		    info.direction != other)
+			return -EINVAL;
+		have_latency = true;
+	}
+	emit = spa_latency_info_compare(&info, &port->latency[other]) != 0 ||
+	    port->have_latency == have_latency;
+
+	port->latency[other] = info;
+	port->have_latency = have_latency;
+
+	spa_log_debug(this->log, "%p: set %s latency %f-%f %d-%d %"PRIu64"-%"PRIu64, this,
+			info.direction == SPA_DIRECTION_INPUT ? "input" : "output",
+			info.min_quantum, info.max_quantum,
+			info.min_rate, info.max_rate,
+			info.min_ns, info.max_ns);
+
+	if (this->monitor_passthrough) {
+		if (port->is_monitor)
+			oport = GET_PORT(this, other, port_id-1);
+		else if (this->monitor && direction == SPA_DIRECTION_INPUT)
+			oport = GET_PORT(this, other, port_id+1);
+		else
+			return 0;
+
+		if (oport != NULL &&
+		    spa_latency_info_compare(&info, &oport->latency[other]) != 0) {
+			oport->latency[other] = info;
+			oport->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+			oport->params[IDX_Latency].user++;
+			emit_port_info(this, oport, false);
+		}
+	} else {
+		spa_latency_info_combine_start(&info, other);
+		for (i = 0; i < this->dir[direction].n_ports; i++) {
+			oport = GET_PORT(this, direction, i);
+			if ((oport->is_monitor) || !oport->have_latency)
+				continue;
+			spa_log_debug(this->log, "%p: combine %d", this, i);
+			spa_latency_info_combine(&info, &oport->latency[other]);
+		}
+		spa_latency_info_combine_finish(&info);
+
+		spa_log_debug(this->log, "%p: combined %s latency %f-%f %d-%d %"PRIu64"-%"PRIu64, this,
+				info.direction == SPA_DIRECTION_INPUT ? "input" : "output",
+				info.min_quantum, info.max_quantum,
+				info.min_rate, info.max_rate,
+				info.min_ns, info.max_ns);
+
+		for (i = 0; i < this->dir[other].n_ports; i++) {
+			oport = GET_PORT(this, other, i);
+			if (oport->is_monitor)
+				continue;
+			spa_log_debug(this->log, "%p: change %d", this, i);
+			if (spa_latency_info_compare(&info, &oport->latency[other]) != 0) {
+				oport->latency[other] = info;
+				oport->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+				oport->params[IDX_Latency].user++;
+				emit_port_info(this, oport, false);
+			}
+		}
+	}
+	if (emit) {
+		port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+		port->params[IDX_Latency].user++;
+		emit_port_info(this, port, false);
+	}
+	return 0;
+}
+
+static int port_set_tag(void *object,
+			   enum spa_direction direction,
+			   uint32_t port_id,
+			   uint32_t flags,
+			   const struct spa_pod *tag)
+{
+	struct impl *this = object;
+	struct port *port, *oport;
+	enum spa_direction other = SPA_DIRECTION_REVERSE(direction);
+	uint32_t i;
+
+	spa_log_debug(this->log, "%p: set tag direction:%d id:%d %p",
+			this, direction, port_id, tag);
+
+	port = GET_PORT(this, direction, port_id);
+	if (port->is_monitor && !this->monitor_passthrough)
+		return 0;
+
+	if (tag != NULL) {
+		struct spa_tag_info info;
+		void *state = NULL;
+		if (spa_tag_parse(tag, &info, &state) < 0 ||
+		    info.direction != other)
+			return -EINVAL;
+	}
+	if (spa_tag_compare(tag, this->dir[other].tag) != 0) {
+		free(this->dir[other].tag);
+		this->dir[other].tag = tag ? spa_pod_copy(tag) : NULL;
+
+		for (i = 0; i < this->dir[other].n_ports; i++) {
+			oport = GET_PORT(this, other, i);
+			oport->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+			oport->params[IDX_Tag].user++;
+			emit_port_info(this, oport, false);
+		}
+	}
+	port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+	port->params[IDX_Tag].user++;
+	emit_port_info(this, port, false);
+	return 0;
+}
+
+static int port_set_format(void *object,
+			   enum spa_direction direction,
+			   uint32_t port_id,
+			   uint32_t flags,
+			   const struct spa_pod *format)
+{
+	struct impl *this = object;
+	struct port *port;
+	int res;
+
+	port = GET_PORT(this, direction, port_id);
+
+	spa_log_debug(this->log, "%p: %d:%d set format", this, direction, port_id);
+
+	if (format == NULL) {
+		port->have_format = false;
+		clear_buffers(this, port);
+	} else {
+		struct spa_video_info info = { 0 };
+		spa_debug_format(2, NULL, format);
+
+		if ((res = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0) {
+			spa_log_error(this->log, "can't parse format %s", spa_strerror(res));
+			return res;
+		}
+		if (PORT_IS_DSP(this, direction, port_id)) {
+			if (info.media_type != SPA_MEDIA_TYPE_video ||
+			    info.media_subtype != SPA_MEDIA_SUBTYPE_dsp) {
+				spa_log_error(this->log, "unexpected types %d/%d",
+						info.media_type, info.media_subtype);
+				return -EINVAL;
+			}
+			if ((res = spa_format_video_dsp_parse(format, &info.info.dsp)) < 0) {
+				spa_log_error(this->log, "can't parse format %s", spa_strerror(res));
+				return res;
+			}
+			if (info.info.dsp.format != SPA_VIDEO_FORMAT_DSP_F32) {
+				spa_log_error(this->log, "unexpected format %d<->%d",
+					info.info.dsp.format, SPA_VIDEO_FORMAT_DSP_F32);
+				return -EINVAL;
+			}
+			port->blocks = 1;
+			port->stride = 16;
+		}
+		else if (PORT_IS_CONTROL(this, direction, port_id)) {
+			if (info.media_type != SPA_MEDIA_TYPE_application ||
+			    info.media_subtype != SPA_MEDIA_SUBTYPE_control) {
+				spa_log_error(this->log, "unexpected types %d/%d",
+						info.media_type, info.media_subtype);
+				return -EINVAL;
+			}
+			port->blocks = 1;
+			port->stride = 1;
+		}
+		else {
+			struct dir *dir = &this->dir[direction];
+			struct dir *odir = &this->dir[SPA_DIRECTION_REVERSE(direction)];
+
+			if (info.media_type != SPA_MEDIA_TYPE_video) {
+				spa_log_error(this->log, "unexpected types %d/%d",
+						info.media_type, info.media_subtype);
+				return -EINVAL;
+			}
+			if ((res = spa_format_video_parse(format, &info)) < 0) {
+				spa_log_error(this->log, "can't parse format %s", spa_strerror(res));
+				return res;
+			}
+			port->stride = 2;
+			port->stride *= info.info.raw.size.width;
+			port->blocks = 1;
+			dir->format = info;
+			dir->have_format = true;
+			if (odir->have_format) {
+				if (memcmp(&odir->format, &dir->format, sizeof(dir->format)) == 0)
+					this->fmt_passthrough = true;
+			}
+			this->setup = false;
+		}
+		port->format = info;
+		port->have_format = true;
+
+		spa_log_debug(this->log, "%p: %d %d %d", this,
+				port_id, port->stride, port->blocks);
+	}
+
+	port->info.change_mask |= SPA_PORT_CHANGE_MASK_FLAGS;
+	SPA_FLAG_UPDATE(port->info.flags, SPA_PORT_FLAG_CAN_ALLOC_BUFFERS, this->fmt_passthrough);
+
+	port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+	if (port->have_format) {
+		port->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE);
+		port->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ);
+	} else {
+		port->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+		port->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+	}
+	emit_port_info(this, port, false);
+
+	return 0;
+}
+
+
+static int
+impl_node_port_set_param(void *object,
+			 enum spa_direction direction, uint32_t port_id,
+			 uint32_t id, uint32_t flags,
+			 const struct spa_pod *param)
+{
+	struct impl *this = object;
+
+	spa_return_val_if_fail(this != NULL, -EINVAL);
+
+	spa_log_debug(this->log, "%p: set param port %d.%d %u",
+			this, direction, port_id, id);
+
+	spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+	switch (id) {
+	case SPA_PARAM_Latency:
+		return port_set_latency(this, direction, port_id, flags, param);
+	case SPA_PARAM_Tag:
+		return port_set_tag(this, direction, port_id, flags, param);
+	case SPA_PARAM_Format:
+		return port_set_format(this, direction, port_id, flags, param);
+	default:
+		return -ENOENT;
+	}
+}
+
+static inline void queue_buffer(struct impl *this, struct port *port, uint32_t id)
+{
+	struct buffer *b = &port->buffers[id];
+
+	spa_log_trace_fp(this->log, "%p: queue buffer %d on port %d %d",
+			this, id, port->id, b->flags);
+	if (SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_QUEUED))
+		return;
+
+	spa_list_append(&port->queue, &b->link);
+	SPA_FLAG_SET(b->flags, BUFFER_FLAG_QUEUED);
+}
+
+static inline struct buffer *peek_buffer(struct impl *this, struct port *port)
+{
+	struct buffer *b;
+
+	if (spa_list_is_empty(&port->queue))
+		return NULL;
+
+	b = spa_list_first(&port->queue, struct buffer, link);
+	spa_log_trace_fp(this->log, "%p: peek buffer %d/%d on port %d %u",
+			this, b->id, port->n_buffers, port->id, b->flags);
+	return b;
+}
+
+static inline void dequeue_buffer(struct impl *this, struct port *port, struct buffer *b)
+{
+	spa_log_trace_fp(this->log, "%p: dequeue buffer %d on port %d %u",
+			this, b->id, port->id, b->flags);
+	if (!SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_QUEUED))
+		return;
+	spa_list_remove(&b->link);
+	SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_QUEUED);
+}
+
+static int
+impl_node_port_use_buffers(void *object,
+			   enum spa_direction direction,
+			   uint32_t port_id,
+			   uint32_t flags,
+			   struct spa_buffer **buffers,
+			   uint32_t n_buffers)
+{
+	struct impl *this = object;
+	struct port *port;
+	uint32_t i, j, maxsize;
+
+	spa_return_val_if_fail(this != NULL, -EINVAL);
+
+	spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+	port = GET_PORT(this, direction, port_id);
+
+	spa_log_debug(this->log, "%p: use buffers %d on port %d:%d",
+			this, n_buffers, direction, port_id);
+
+	clear_buffers(this, port);
+
+	if (n_buffers > 0 && !port->have_format)
+		return -EIO;
+	if (n_buffers > MAX_BUFFERS)
+		return -ENOSPC;
+
+	maxsize = this->quantum_limit * sizeof(float);
+
+	for (i = 0; i < n_buffers; i++) {
+		struct buffer *b;
+		uint32_t n_datas = buffers[i]->n_datas;
+		struct spa_data *d = buffers[i]->datas;
+
+		b = &port->buffers[i];
+		b->id = i;
+		b->flags = 0;
+		b->buf = buffers[i];
+
+		if (n_datas != port->blocks) {
+			spa_log_error(this->log, "%p: invalid blocks %d on buffer %d",
+					this, n_datas, i);
+			return -EINVAL;
+		}
+		if (SPA_FLAG_IS_SET(flags, SPA_NODE_BUFFERS_FLAG_ALLOC)) {
+			struct port *other = GET_PORT(this, SPA_DIRECTION_REVERSE(direction), port_id);
+
+			if (other->n_buffers <= 0)
+				return -EIO;
+			*b->buf = *other->buffers[i % other->n_buffers].buf;
+			b->datas[0] = other->buffers[i % other->n_buffers].datas[0];
+		} else {
+			for (j = 0; j < n_datas; j++) {
+				void *data = d[j].data;
+				if (data == NULL && SPA_FLAG_IS_SET(d[j].flags, SPA_DATA_FLAG_MAPPABLE)) {
+					data = mmap(NULL, d[j].maxsize,
+						PROT_READ, MAP_SHARED, d[j].fd,
+						d[j].mapoffset);
+					if (data == MAP_FAILED) {
+						spa_log_error(this->log, "%p: mmap failed %d on buffer %d %d %p: %m",
+								this, j, i, d[j].type, data);
+						return -EINVAL;
+					}
+				}
+				if (data != NULL && !SPA_IS_ALIGNED(data, this->max_align)) {
+					spa_log_warn(this->log, "%p: memory %d on buffer %d not aligned",
+							this, j, i);
+				}
+				b->datas[j] = data;
+				maxsize = SPA_MAX(maxsize, d[j].maxsize);
+			}
+		}
+		if (direction == SPA_DIRECTION_OUTPUT)
+			queue_buffer(this, port, i);
+	}
+	port->maxsize = maxsize;
+	port->n_buffers = n_buffers;
+
+	return 0;
+}
+
+struct io_data {
+	struct port *port;
+	void *data;
+	size_t size;
+};
+
+static int do_set_port_io(struct spa_loop *loop, bool async, uint32_t seq,
+		const void *data, size_t size, void *user_data)
+{
+	const struct io_data *d = user_data;
+	d->port->io = d->data;
+	return 0;
+}
+
+static int
+impl_node_port_set_io(void *object,
+		      enum spa_direction direction, uint32_t port_id,
+		      uint32_t id, void *data, size_t size)
+{
+	struct impl *this = object;
+	struct port *port;
+
+	spa_return_val_if_fail(this != NULL, -EINVAL);
+
+	spa_log_debug(this->log, "%p: set io %d on port %d:%d %p",
+			this, id, direction, port_id, data);
+
+	spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+	port = GET_PORT(this, direction, port_id);
+
+	switch (id) {
+	case SPA_IO_Buffers:
+		if (this->data_loop) {
+			struct io_data d = { .port = port, .data = data, .size = size };
+			spa_loop_invoke(this->data_loop, do_set_port_io, 0, NULL, 0, true, &d);
+		}
+		else
+			port->io = data;
+		break;
+	case SPA_IO_RateMatch:
+		this->io_rate_match = data;
+		break;
+	default:
+		return -ENOENT;
+	}
+	return 0;
+}
+
+static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id)
+{
+	struct impl *this = object;
+	struct port *port;
+
+	spa_return_val_if_fail(this != NULL, -EINVAL);
+	spa_return_val_if_fail(CHECK_PORT(this, SPA_DIRECTION_OUTPUT, port_id), -EINVAL);
+
+	port = GET_OUT_PORT(this, port_id);
+	queue_buffer(this, port, buffer_id);
+
+	return 0;
+}
+
+static int impl_node_process(void *object)
+{
+	struct impl *this = object;
+	struct port *in_port, *out_port;
+	struct spa_io_buffers *input, *output;
+	struct buffer *dbuf, *sbuf;
+	struct dir *in, *out;
+	struct AVFrame *f;
+	void *datas[8];
+	uint32_t sizes[8], strides[8];
+	int res;
+
+	spa_return_val_if_fail(this != NULL, -EINVAL);
+
+	in = &this->dir[SPA_DIRECTION_INPUT];
+	out = &this->dir[SPA_DIRECTION_OUTPUT];
+
+	out_port = GET_OUT_PORT(this, 0);
+	if ((output = out_port->io) == NULL)
+		return -EIO;
+
+	if (output->status == SPA_STATUS_HAVE_DATA)
+		return SPA_STATUS_HAVE_DATA;
+
+	/* recycle */
+	if (output->buffer_id < out_port->n_buffers) {
+		queue_buffer(this, out_port, output->buffer_id);
+		output->buffer_id = SPA_ID_INVALID;
+	}
+
+	in_port = GET_IN_PORT(this, 0);
+	if ((input = in_port->io) == NULL)
+		return -EIO;
+
+	if (input->status != SPA_STATUS_HAVE_DATA)
+		return SPA_STATUS_NEED_DATA;
+
+	if (input->buffer_id >= in_port->n_buffers) {
+		input->status = -EINVAL;
+		return -EINVAL;
+	}
+
+	sbuf = &in_port->buffers[input->buffer_id];
+
+	if ((dbuf = peek_buffer(this, out_port)) == NULL) {
+                spa_log_error(this->log, "%p: out of buffers", this);
+		return -EPIPE;
+	}
+	dbuf = &out_port->buffers[input->buffer_id];
+
+	spa_log_trace(this->log, "%d %p:%p %d %d %d", input->buffer_id, sbuf->buf->datas[0].chunk,
+			dbuf->buf->datas[0].chunk, sbuf->buf->datas[0].chunk->size,
+			sbuf->id, dbuf->id);
+
+	/* do decoding */
+	if (this->decoder.codec) {
+		this->decoder.packet->data = sbuf->datas[0];
+		this->decoder.packet->size = sbuf->buf->datas[0].chunk->size;
+
+		if ((res = avcodec_send_packet(this->decoder.context, this->decoder.packet)) < 0) {
+			spa_log_error(this->log, "failed to send frame to codec: %d %p:%d",
+					res, this->decoder.packet->data, this->decoder.packet->size);
+			return -EIO;
+		}
+
+		f = this->decoder.frame;
+		if (avcodec_receive_frame(this->decoder.context, f) < 0) {
+			spa_log_error(this->log, "failed to receive frame from codec");
+			return -EIO;
+		}
+
+		in->pix_fmt = f->format;
+		in->width = f->width;
+		in->height = f->height;
+	} else {
+		f = this->decoder.frame;
+		f->format = in->pix_fmt;
+		f->width = in->width;
+		f->height = in->height;
+		f->data[0] = sbuf->datas[0];
+		f->linesize[0] = sbuf->buf->datas[0].chunk->stride;
+	}
+
+	/* do conversion */
+	if (f->format != out->pix_fmt ||
+	    f->width != out->width ||
+	    f->height != out->height) {
+		if (this->convert.context == NULL) {
+			this->convert.context = sws_getContext(
+					f->width, f->height, f->format,
+					out->width, out->height, out->pix_fmt,
+					0, NULL, NULL, NULL);
+		}
+		sws_scale_frame(this->convert.context, this->convert.frame, f);
+		f = this->convert.frame;
+	}
+	/* do encoding */
+	if (this->encoder.codec) {
+		if ((res = avcodec_send_frame(this->encoder.context, f)) < 0) {
+			spa_log_error(this->log, "failed to send frame to codec: %d", res);
+			return -EIO;
+		}
+		if (avcodec_receive_packet(this->encoder.context, this->encoder.packet) < 0) {
+			spa_log_error(this->log, "failed to receive frame from codec");
+			return -EIO;
+		}
+		datas[0] = this->encoder.packet->data;
+		sizes[0] = this->encoder.packet->size;
+		strides[0] = 1;
+
+	} else {
+		datas[0] = f->data[0];
+		strides[0] = f->linesize[0];
+		sizes[0] = strides[0] * out->height;
+	}
+
+	/* write to output */
+	for (uint_fast32_t i = 0; i < dbuf->buf->n_datas; ++i) {
+		if (SPA_FLAG_IS_SET(dbuf->buf->datas[i].flags, SPA_DATA_FLAG_DYNAMIC))
+			dbuf->buf->datas[i].data = datas[i];
+		else if (datas[i] && dbuf->datas[i] && dbuf->datas[i] != datas[i])
+			memcpy(dbuf->datas[i], datas[i], sizes[i]);
+
+		if (dbuf->buf->datas[i].chunk != sbuf->buf->datas[i].chunk) {
+			dbuf->buf->datas[i].chunk->stride = strides[i];
+			dbuf->buf->datas[i].chunk->size = sizes[i];
+		}
+	}
+
+	dequeue_buffer(this, out_port, dbuf);
+	output->buffer_id = dbuf->id;
+	output->status = SPA_STATUS_HAVE_DATA;
+
+	input->status = SPA_STATUS_NEED_DATA;
+
+	return SPA_STATUS_HAVE_DATA;
+}
+
+static const struct spa_node_methods impl_node = {
+	SPA_VERSION_NODE_METHODS,
+	.add_listener = impl_node_add_listener,
+	.set_callbacks = impl_node_set_callbacks,
+	.enum_params = impl_node_enum_params,
+	.set_param = impl_node_set_param,
+	.set_io = impl_node_set_io,
+	.send_command = impl_node_send_command,
+	.add_port = impl_node_add_port,
+	.remove_port = impl_node_remove_port,
+	.port_enum_params = impl_node_port_enum_params,
+	.port_set_param = impl_node_port_set_param,
+	.port_use_buffers = impl_node_port_use_buffers,
+	.port_set_io = impl_node_port_set_io,
+	.port_reuse_buffer = impl_node_port_reuse_buffer,
+	.process = impl_node_process,
+};
+
+static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface)
+{
+	struct impl *this;
+
+	spa_return_val_if_fail(handle != NULL, -EINVAL);
+	spa_return_val_if_fail(interface != NULL, -EINVAL);
+
+	this = (struct impl *) handle;
+
+	if (spa_streq(type, SPA_TYPE_INTERFACE_Node))
+		*interface = &this->node;
+	else
+		return -ENOENT;
+
+	return 0;
+}
+
+static void free_dir(struct dir *dir)
+{
+	uint32_t i;
+	for (i = 0; i < MAX_PORTS; i++)
+		free(dir->ports[i]);
+	free(dir->tag);
+}
+
+static int impl_clear(struct spa_handle *handle)
+{
+	struct impl *this;
+
+	spa_return_val_if_fail(handle != NULL, -EINVAL);
+
+	this = (struct impl *) handle;
+
+	free_dir(&this->dir[SPA_DIRECTION_INPUT]);
+	free_dir(&this->dir[SPA_DIRECTION_OUTPUT]);
+	return 0;
+}
+
+static size_t
+impl_get_size(const struct spa_handle_factory *factory,
+	      const struct spa_dict *params)
+{
+	return sizeof(struct impl);
+}
+
+static int
+impl_init(const struct spa_handle_factory *factory,
+	  struct spa_handle *handle,
+	  const struct spa_dict *info,
+	  const struct spa_support *support,
+	  uint32_t n_support)
+{
+	struct impl *this;
+	uint32_t i;
+
+	spa_return_val_if_fail(factory != NULL, -EINVAL);
+	spa_return_val_if_fail(handle != NULL, -EINVAL);
+
+	handle->get_interface = impl_get_interface;
+	handle->clear = impl_clear;
+
+	this = (struct impl *) handle;
+
+	this->data_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataLoop);
+	this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
+	spa_log_topic_init(this->log, &log_topic);
+
+	this->cpu = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_CPU);
+	if (this->cpu) {
+		this->cpu_flags = spa_cpu_get_flags(this->cpu);
+		this->max_align = SPA_MIN(MAX_ALIGN, spa_cpu_get_max_align(this->cpu));
+	}
+	props_reset(&this->props);
+
+	this->rate_limit.interval = 2 * SPA_NSEC_PER_SEC;
+	this->rate_limit.burst = 1;
+
+	for (i = 0; info && i < info->n_items; i++) {
+		const char *k = info->items[i].key;
+		const char *s = info->items[i].value;
+		if (spa_streq(k, "clock.quantum-limit"))
+			spa_atou32(s, &this->quantum_limit, 0);
+		else if (spa_streq(k, SPA_KEY_PORT_IGNORE_LATENCY))
+			this->port_ignore_latency = spa_atob(s);
+		else if (spa_streq(k, SPA_KEY_PORT_GROUP))
+			spa_scnprintf(this->group_name, sizeof(this->group_name), "%s", s);
+		else if (spa_streq(k, "monitor.passthrough"))
+			this->monitor_passthrough = spa_atob(s);
+		else
+			videoconvert_set_param(this, k, s);
+	}
+
+	this->dir[SPA_DIRECTION_INPUT].direction = SPA_DIRECTION_INPUT;
+	this->dir[SPA_DIRECTION_OUTPUT].direction = SPA_DIRECTION_OUTPUT;
+
+	this->node.iface = SPA_INTERFACE_INIT(
+			SPA_TYPE_INTERFACE_Node,
+			SPA_VERSION_NODE,
+			&impl_node, this);
+	spa_hook_list_init(&this->hooks);
+
+	this->info_all = SPA_NODE_CHANGE_MASK_FLAGS |
+			SPA_NODE_CHANGE_MASK_PARAMS;
+	this->info = SPA_NODE_INFO_INIT();
+	this->info.max_input_ports = MAX_PORTS;
+	this->info.max_output_ports = MAX_PORTS;
+	this->info.flags = SPA_NODE_FLAG_RT |
+		SPA_NODE_FLAG_IN_PORT_CONFIG |
+		SPA_NODE_FLAG_OUT_PORT_CONFIG |
+		SPA_NODE_FLAG_NEED_CONFIGURE;
+	this->params[IDX_EnumPortConfig] = SPA_PARAM_INFO(SPA_PARAM_EnumPortConfig, SPA_PARAM_INFO_READ);
+	this->params[IDX_PortConfig] = SPA_PARAM_INFO(SPA_PARAM_PortConfig, SPA_PARAM_INFO_READWRITE);
+	this->params[IDX_PropInfo] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ);
+	this->params[IDX_Props] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE);
+	this->info.params = this->params;
+	this->info.n_params = N_NODE_PARAMS;
+
+	reconfigure_mode(this, SPA_PARAM_PORT_CONFIG_MODE_convert, SPA_DIRECTION_INPUT, false, false, NULL);
+	reconfigure_mode(this, SPA_PARAM_PORT_CONFIG_MODE_convert, SPA_DIRECTION_OUTPUT, false, false, NULL);
+
+	return 0;
+}
+
+static const struct spa_interface_info impl_interfaces[] = {
+	{SPA_TYPE_INTERFACE_Node,},
+};
+
+static int
+impl_enum_interface_info(const struct spa_handle_factory *factory,
+			 const struct spa_interface_info **info,
+			 uint32_t *index)
+{
+	spa_return_val_if_fail(factory != NULL, -EINVAL);
+	spa_return_val_if_fail(info != NULL, -EINVAL);
+	spa_return_val_if_fail(index != NULL, -EINVAL);
+
+	switch (*index) {
+	case 0:
+		*info = &impl_interfaces[*index];
+		break;
+	default:
+		return 0;
+	}
+	(*index)++;
+	return 1;
+}
+
+const struct spa_handle_factory spa_videoconvert_ffmpeg_factory = {
+	SPA_VERSION_HANDLE_FACTORY,
+	SPA_NAME_VIDEO_CONVERT".ffmpeg",
+	NULL,
+	impl_get_size,
+	impl_init,
+	impl_enum_interface_info,
+};
diff --git a/spa/plugins/videotestsrc/videotestsrc.c b/spa/plugins/videotestsrc/videotestsrc.c
index a10c3be6..aff3f536 100644
--- a/spa/plugins/videotestsrc/videotestsrc.c
+++ b/spa/plugins/videotestsrc/videotestsrc.c
@@ -84,7 +84,7 @@ struct impl {
 
 	struct spa_log *log;
 	struct spa_loop *data_loop;
-	struct spa_system *data_system;
+	struct spa_loop_utils *loop_utils;
 
 	uint64_t info_all;
 	struct spa_node_info info;
@@ -98,7 +98,7 @@ struct impl {
 	struct spa_callbacks callbacks;
 
 	bool async;
-	struct spa_source timer_source;
+	struct spa_source *timer_source;
 	struct itimerspec timerspec;
 
 	bool started;
@@ -277,27 +277,10 @@ static void set_timer(struct impl *this, bool enabled)
 			this->timerspec.it_value.tv_sec = 0;
 			this->timerspec.it_value.tv_nsec = 0;
 		}
-		spa_system_timerfd_settime(this->data_system,
-				this->timer_source.fd, SPA_FD_TIMER_ABSTIME, &this->timerspec, NULL);
+		spa_loop_utils_update_timer(this->loop_utils, this->timer_source, &this->timerspec.it_value, &this->timerspec.it_interval, true);
 	}
 }
 
-static int read_timer(struct impl *this)
-{
-	uint64_t expirations;
-	int res = 0;
-
-	if (this->async || this->props.live) {
-		if ((res = spa_system_timerfd_read(this->data_system,
-						this->timer_source.fd, &expirations)) < 0) {
-			if (res != -EAGAIN)
-				spa_log_error(this->log, "%p: timerfd error: %s",
-						this, spa_strerror(res));
-		}
-	}
-	return res;
-}
-
 static int make_buffer(struct impl *this)
 {
 	struct buffer *b;
@@ -305,9 +288,6 @@ static int make_buffer(struct impl *this)
 	struct spa_io_buffers *io = port->io;
 	uint32_t n_bytes;
 
-	if (read_timer(this) < 0)
-		return 0;
-
 	if (spa_list_is_empty(&port->empty)) {
 		set_timer(this, false);
 		spa_log_error(this->log, "%p: out of buffers", this);
@@ -343,9 +323,9 @@ static int make_buffer(struct impl *this)
 	return io->status;
 }
 
-static void on_output(struct spa_source *source)
+static void on_output(void* data, uint64_t expirations)
 {
-	struct impl *this = source->data;
+	struct impl *this = data;
 	int res;
 
 	res = make_buffer(this);
@@ -859,7 +839,7 @@ static int impl_get_interface(struct spa_handle *handle, const char *type, void
 static int do_remove_timer(struct spa_loop *loop, bool async, uint32_t seq, const void *data, size_t size, void *user_data)
 {
 	struct impl *this = user_data;
-	spa_loop_remove_source(this->data_loop, &this->timer_source);
+	spa_loop_remove_source(this->data_loop, this->timer_source);
 	return 0;
 }
 
@@ -873,7 +853,7 @@ static int impl_clear(struct spa_handle *handle)
 
 	if (this->data_loop)
 		spa_loop_invoke(this->data_loop, do_remove_timer, 0, NULL, 0, true, this);
-	spa_system_close(this->data_system, this->timer_source.fd);
+	spa_loop_utils_destroy_source(this->loop_utils, this->timer_source);
 
 	return 0;
 }
@@ -905,7 +885,7 @@ impl_init(const struct spa_handle_factory *factory,
 
 	this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
 	this->data_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataLoop);
-	this->data_system = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataSystem);
+	this->loop_utils = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_LoopUtils);
 
 	spa_hook_list_init(&this->hooks);
 
@@ -926,20 +906,12 @@ impl_init(const struct spa_handle_factory *factory,
 	this->info.n_params = 2;
 	reset_props(&this->props);
 
-	this->timer_source.func = on_output;
-	this->timer_source.data = this;
-	this->timer_source.fd = spa_system_timerfd_create(this->data_system, CLOCK_MONOTONIC,
-							  SPA_FD_CLOEXEC | SPA_FD_NONBLOCK);
-	this->timer_source.mask = SPA_IO_IN;
-	this->timer_source.rmask = 0;
+	this->timer_source = spa_loop_utils_add_timer(this->loop_utils, on_output, this);
 	this->timerspec.it_value.tv_sec = 0;
 	this->timerspec.it_value.tv_nsec = 0;
 	this->timerspec.it_interval.tv_sec = 0;
 	this->timerspec.it_interval.tv_nsec = 0;
 
-	if (this->data_loop)
-		spa_loop_add_source(this->data_loop, &this->timer_source);
-
 	port = &this->port;
 	port->info_all = SPA_PORT_CHANGE_MASK_FLAGS |
 			SPA_PORT_CHANGE_MASK_PARAMS;
diff --git a/spa/plugins/vulkan/vulkan-utils.c b/spa/plugins/vulkan/vulkan-utils.c
index cbf30f3a..64323135 100644
--- a/spa/plugins/vulkan/vulkan-utils.c
+++ b/spa/plugins/vulkan/vulkan-utils.c
@@ -244,11 +244,8 @@ int vulkan_write_pixels(struct vulkan_base *s, struct vulkan_write_pixels_info *
 	void *vmap;
 	VK_CHECK_RESULT(vkMapMemory(s->device, vk_sbuf->memory, 0, VK_WHOLE_SIZE, 0, &vmap));
 
-	char *map = (char *)vmap;
-
 	// upload data
-	const char *pdata = info->data;
-	memcpy(map, pdata, info->stride * info->size.height);
+	memcpy(vmap, info->data, info->stride * info->size.height);
 
 	info->copies[0] = (VkBufferImageCopy) {
 		.imageExtent.width = info->size.width,
@@ -626,7 +623,7 @@ int vulkan_create_dmabuf(struct vulkan_base *s, struct external_buffer_info *inf
 	vkGetImageMemoryRequirements(s->device,
 			vk_buf->image, &memoryRequirements);
 
-	spa_log_info(s->log, "export DMABUF %zd", memoryRequirements.size);
+	spa_log_info(s->log, "export DMABUF %" PRIu64, memoryRequirements.size);
 
 	for (uint32_t i = 0; i < info->spa_buf->n_datas; i++) {
 		VkImageSubresource subresource = {
diff --git a/spa/tools/spa-json-dump.c b/spa/tools/spa-json-dump.c
index 0e2e8cec..a1f2e619 100644
--- a/spa/tools/spa-json-dump.c
+++ b/spa/tools/spa-json-dump.c
@@ -82,14 +82,12 @@ static int dump(FILE *file, int indent, struct spa_json *it, const char *value,
 			spa_json_enter(it, &sub);
 		else
 			sub = *it;
-		while (spa_json_get_string(&sub, key, sizeof(key)) > 0) {
+		while ((len = spa_json_object_next(&sub, key, sizeof(key), &value)) > 0) {
 			fprintf(file, "%s\n%*s",
 					count++ > 0 ? "," : "",
 					indent+2, "");
 			encode_string(file, key, strlen(key));
 			fprintf(file, ": ");
-			if ((len = spa_json_next(&sub, &value)) <= 0)
-				break;
 			res = dump(file, indent+2, &sub, value, len);
 			if (res < 0) {
 				if (toplevel)
@@ -123,8 +121,7 @@ static int process_json(const char *filename, void *buf, size_t size)
 	struct spa_json it;
 	const char *value;
 
-	spa_json_init(&it, buf, size);
-	if ((len = spa_json_next(&it, &value)) <= 0) {
+	if ((len = spa_json_begin(&it, buf, size, &value)) <= 0) {
                 fprintf(stderr, "not a valid file '%s': %s\n", filename, spa_strerror(len));
 		return -EINVAL;
 	}
diff --git a/src/daemon/client-rt.conf.avail/20-upmix.conf.in b/src/daemon/client-rt.conf.avail/20-upmix.conf.in
deleted file mode 100644
index 064eba14..00000000
--- a/src/daemon/client-rt.conf.avail/20-upmix.conf.in
+++ /dev/null
@@ -1,8 +0,0 @@
-# Enables upmixing
-stream.properties = {
-    channelmix.upmix      = true
-    channelmix.upmix-method = psd  # none, simple
-    channelmix.lfe-cutoff = 150
-    channelmix.fc-cutoff  = 12000
-    channelmix.rear-delay = 12.0
-}
diff --git a/src/daemon/client-rt.conf.avail/meson.build b/src/daemon/client-rt.conf.avail/meson.build
deleted file mode 100644
index a6dc07bb..00000000
--- a/src/daemon/client-rt.conf.avail/meson.build
+++ /dev/null
@@ -1,12 +0,0 @@
-conf_files = [
- '20-upmix.conf',
-]
-
-foreach c : conf_files
-  res = configure_file(input : '@0@.in'.format(c),
-                 output : c,
-                 configuration : conf_config,
-                 install_dir : pipewire_confdatadir / 'client-rt.conf.avail')
-  test(f'validate-json-client-rt-@c@', spa_json_dump_exe, args : res)
-endforeach
-
diff --git a/src/daemon/client-rt.conf.in b/src/daemon/client-rt.conf.in
deleted file mode 100644
index ed08a5f1..00000000
--- a/src/daemon/client-rt.conf.in
+++ /dev/null
@@ -1,136 +0,0 @@
-# Real-time Client config file for PipeWire version @VERSION@ #
-#
-# Copy and edit this file in @PIPEWIRE_CONFIG_DIR@ for system-wide changes
-# or in ~/.config/pipewire for local changes.
-#
-# It is also possible to place a file with an updated section in
-# @PIPEWIRE_CONFIG_DIR@/client-rt.conf.d/ for system-wide changes or in
-# ~/.config/pipewire/client-rt.conf.d/ for local changes.
-#
-
-context.properties = {
-    ## Configure properties in the system.
-    #mem.warn-mlock  = false
-    #mem.allow-mlock = true
-    #mem.mlock-all   = false
-    log.level        = 0
-
-    #default.clock.quantum-limit = 8192
-}
-
-context.spa-libs = {
-    #<factory-name regex> = <library-name>
-    #
-    # Used to find spa factory names. It maps an spa factory name
-    # regular expression to a library name that should contain
-    # that factory.
-    #
-    audio.convert.* = audioconvert/libspa-audioconvert
-    support.*       = support/libspa-support
-}
-
-context.modules = [
-    #{ name = <module-name>
-    #    ( args  = { <key> = <value> ... } )
-    #    ( flags = [ ( ifexists ) ( nofail ) ] )
-    #    ( condition = [ { <key> = <value> ... } ... ] )
-    #}
-    #
-    # Loads a module with the given parameters.
-    # If ifexists is given, the module is ignored when it is not found.
-    # If nofail is given, module initialization failures are ignored.
-    #
-    # Uses realtime scheduling to boost the audio thread priorities
-    { name = libpipewire-module-rt
-        args = {
-            #rt.prio      = @rtprio_client@
-            #rt.time.soft = -1
-            #rt.time.hard = -1
-        }
-        flags = [ ifexists nofail ]
-    }
-
-    # The native communication protocol.
-    { name = libpipewire-module-protocol-native }
-
-    # Allows creating nodes that run in the context of the
-    # client. Is used by all clients that want to provide
-    # data to PipeWire.
-    { name = libpipewire-module-client-node }
-
-    # Allows creating devices that run in the context of the
-    # client. Is used by the session manager.
-    { name = libpipewire-module-client-device }
-
-    # Makes a factory for wrapping nodes in an adapter with a
-    # converter and resampler.
-    { name = libpipewire-module-adapter }
-
-    # Allows applications to create metadata objects. It creates
-    # a factory for Metadata objects.
-    { name = libpipewire-module-metadata }
-
-    # Provides factories to make session manager objects.
-    { name = libpipewire-module-session-manager }
-]
-
-filter.properties = {
-    #node.latency = 1024/48000
-}
-
-stream.properties = {
-    #node.latency          = 1024/48000
-    #node.autoconnect      = true
-    #resample.quality      = 4
-    #channelmix.normalize  = false
-    #channelmix.mix-lfe    = true
-    #channelmix.upmix      = true
-    #channelmix.upmix-method = psd  # none, simple
-    #channelmix.lfe-cutoff = 150
-    #channelmix.fc-cutoff  = 12000
-    #channelmix.rear-delay = 12.0
-    #channelmix.stereo-widen = 0.0
-    #channelmix.hilbert-taps = 0
-    #dither.noise = 0
-}
-
-stream.rules = [
-    {   matches = [
-            {
-                # all keys must match the value. ! negates. ~ starts regex.
-                #application.name       = "pw-cat"
-                #node.name 		= "~Google Chrome$"
-            }
-        ]
-        actions = {
-            update-props = {
-                #node.latency = 512/48000
-            }
-        }
-    }
-]
-
-alsa.properties = {
-    #alsa.deny = false
-    # ALSA params take a single value, an array [] of values
-    # or a range { min=.. max=... }
-    #alsa.access = [ MMAP_INTERLEAVED MMAP_NONINTERLEAVED RW_INTERLEAVED RW_NONINTERLEAVED ]
-    #alsa.format = [ FLOAT S32 S24 S24_3 S16 U8 ]
-    #alsa.rate = { min=1 max=384000 }		# or [ 44100 48000 .. ]
-    #alsa.channels = { min=1 max=64 }		# or [ 2 4 6 .. ]
-    #alsa.period-bytes = { min=128 max=2097152 } # or [ 128 256 1024 .. ]
-    #alsa.buffer-bytes = { min=256 max=4194304 } # or [ 256 512 4096 .. ]
-
-    #alsa.volume-method = cubic			# linear, cubic
-}
-
-# client specific properties
-alsa.rules = [
-    {   matches = [ { application.process.binary = "resolve" } ]
-        actions = {
-            update-props = {
-		alsa.buffer-bytes = 131072
-            }
-        }
-    }
-]
diff --git a/src/daemon/client.conf.in b/src/daemon/client.conf.in
index 896a7381..46874af9 100644
--- a/src/daemon/client.conf.in
+++ b/src/daemon/client.conf.in
@@ -27,6 +27,7 @@ context.spa-libs = {
     #
     audio.convert.* = audioconvert/libspa-audioconvert
     support.*       = support/libspa-support
+    video.convert.* = videoconvert/libspa-videoconvert
 }
 
 context.modules = [
@@ -40,6 +41,16 @@ context.modules = [
     # If ifexists is given, the module is ignored when it is not found.
     # If nofail is given, module initialization failures are ignored.
     #
+    # Uses realtime scheduling to boost the audio thread priorities
+    { name = libpipewire-module-rt
+        args = {
+            #rt.prio      = @rtprio_client@
+            #rt.time.soft = -1
+            #rt.time.hard = -1
+        }
+        flags = [ ifexists nofail ]
+        condition = [ { module.rt = !false } ]
+    }
 
     # The native communication protocol.
     { name = libpipewire-module-protocol-native }
@@ -47,22 +58,32 @@ context.modules = [
     # Allows creating nodes that run in the context of the
     # client. Is used by all clients that want to provide
     # data to PipeWire.
-    { name = libpipewire-module-client-node }
+    { name = libpipewire-module-client-node
+        condition = [ { module.client-node = !false } ]
+    }
 
     # Allows creating devices that run in the context of the
     # client. Is used by the session manager.
-    { name = libpipewire-module-client-device }
+    { name = libpipewire-module-client-device
+        condition = [ { module.client-device = !false } ]
+    }
 
     # Makes a factory for wrapping nodes in an adapter with a
     # converter and resampler.
-    { name = libpipewire-module-adapter }
+    { name = libpipewire-module-adapter
+        condition = [ { module.adapter = !false } ]
+    }
 
     # Allows applications to create metadata objects. It creates
     # a factory for Metadata objects.
-    { name = libpipewire-module-metadata }
+    { name = libpipewire-module-metadata
+        condition = [ { module.metadata = !false } ]
+    }
 
     # Provides factories to make session manager objects.
-    { name = libpipewire-module-session-manager }
+    { name = libpipewire-module-session-manager
+        condition = [ { module.session-manager = !false } ]
+    }
 ]
 
 filter.properties = {
@@ -84,3 +105,44 @@ stream.properties = {
     #channelmix.hilbert-taps = 0
     #dither.noise = 0
 }
+
+stream.rules = [
+    {   matches = [
+            {
+                # all keys must match the value. ! negates. ~ starts regex.
+                #application.name       = "pw-cat"
+                #node.name 		= "~Google Chrome$"
+            }
+        ]
+        actions = {
+            update-props = {
+                #node.latency = 512/48000
+            }
+        }
+    }
+]
+
+alsa.properties = {
+    #alsa.deny = false
+    # ALSA params take a single value, an array [] of values
+    # or a range { min=.. max=... }
+    #alsa.access = [ MMAP_INTERLEAVED MMAP_NONINTERLEAVED RW_INTERLEAVED RW_NONINTERLEAVED ]
+    #alsa.format = [ FLOAT S32 S24 S24_3 S16 U8 ]
+    #alsa.rate = { min=1 max=384000 }		# or [ 44100 48000 .. ]
+    #alsa.channels = { min=1 max=64 }		# or [ 2 4 6 .. ]
+    #alsa.period-bytes = { min=128 max=2097152 } # or [ 128 256 1024 .. ]
+    #alsa.buffer-bytes = { min=256 max=4194304 } # or [ 256 512 4096 .. ]
+
+    #alsa.volume-method = cubic			# linear, cubic
+}
+
+# client specific properties
+alsa.rules = [
+    {   matches = [ { application.process.binary = "resolve" } ]
+        actions = {
+            update-props = {
+		alsa.buffer-bytes = 131072
+            }
+        }
+    }
+]
diff --git a/src/daemon/filter-chain/35-ebur128.conf b/src/daemon/filter-chain/35-ebur128.conf
new file mode 100644
index 00000000..f12c4a9c
--- /dev/null
+++ b/src/daemon/filter-chain/35-ebur128.conf
@@ -0,0 +1,63 @@
+context.modules = [
+    { name = libpipewire-module-filter-chain
+        args = {
+            node.description = "EBU R128 Normalizer"
+            media.name = "EBU R128 Normalizer"
+            filter.graph = {
+                nodes = [
+                    {
+                        name  = ebur128
+                        type  = ebur128
+                        label = ebur128
+                    }
+                    {
+                        name  = lufsL
+                        type  = ebur128
+                        label = lufs2gain
+			control = {
+			   "Target LUFS" = -16.0
+			}
+                    }
+                    {
+                        name  = lufsR
+                        type  = ebur128
+                        label = lufs2gain
+			control = {
+			   "Target LUFS" = -16.0
+			}
+                    }
+                    {
+                        name  = volumeL
+                        type  = builtin
+                        label = linear
+                    }
+                    {
+                        name  = volumeR
+                        type  = builtin
+                        label = linear
+                    }
+		]
+		links = [
+		  { output = "ebur128:Out FL" input = "volumeL:In" }
+		  { output = "ebur128:Global LUFS" input = "lufsL:LUFS" }
+		  { output = "lufsL:Gain" input = "volumeL:Mult" }
+		  { output = "ebur128:Out FR" input = "volumeR:In" }
+		  { output = "ebur128:Global LUFS" input = "lufsR:LUFS" }
+		  { output = "lufsR:Gain" input = "volumeR:Mult" }
+		]
+		inputs  = [ "ebur128:In FL"  "ebur128:In FR" ]
+                outputs = [ "volumeL:Out" "volumeR:Out" ]
+            } 
+	    capture.props = {
+                node.name      = "effect_input.ebur128_normalize"
+                audio.position = [ FL FR  ]
+                media.class    = Audio/Sink
+            }
+            playback.props = {
+                node.name      = "effect_output.ebur128_normalize"
+                audio.position = [ FL FR ]
+                node.passive   = true
+            }
+        }
+    }
+]
diff --git a/src/daemon/filter-chain/36-dcblock.conf b/src/daemon/filter-chain/36-dcblock.conf
new file mode 100644
index 00000000..b3b6feb6
--- /dev/null
+++ b/src/daemon/filter-chain/36-dcblock.conf
@@ -0,0 +1,59 @@
+context.modules = [
+    { name = libpipewire-module-filter-chain
+        args = {
+            node.description = "DCBlock Filter"
+            media.name       = "DCBlock Filter"
+            filter.graph = {
+                nodes = [
+                    {
+                        name  = dcblock
+                        type  = builtin
+                        label = dcblock
+			control = {
+			   "R" = 0.995
+			}
+                    }
+                    {
+		        # add a short 20ms ramp
+                        name  = ramp
+                        type  = builtin
+                        label = ramp
+			control = {
+			   "Start" = 0.0
+			   "Stop" = 1.0
+			   "Duration (s)" = 0.020
+			}
+                    }
+                    {
+                        name  = volumeL
+                        type  = builtin
+                        label = mult
+                    }
+                    {
+                        name  = volumeR
+                        type  = builtin
+                        label = mult
+                    }
+		]
+		links = [
+		  { output = "dcblock:Out 1" input = "volumeL:In 1" }
+		  { output = "dcblock:Out 2" input = "volumeR:In 1" }
+		  { output = "ramp:Out" input = "volumeL:In 2" }
+		  { output = "ramp:Out" input = "volumeR:In 2" }
+		]
+		inputs  = [ "dcblock:In 1"  "dcblock:In 2" ]
+                outputs = [ "volumeL:Out" "volumeR:Out" ]
+            } 
+	    capture.props = {
+                node.name      = "effect_input.dcblock"
+                audio.position = [ FL FR  ]
+                media.class    = Audio/Sink
+            }
+            playback.props = {
+                node.name      = "effect_output.dcblock"
+                audio.position = [ FL FR ]
+                node.passive   = true
+            }
+        }
+    }
+]
diff --git a/src/daemon/filter-chain/meson.build b/src/daemon/filter-chain/meson.build
index 4bbcc355..0e634064 100644
--- a/src/daemon/filter-chain/meson.build
+++ b/src/daemon/filter-chain/meson.build
@@ -9,6 +9,7 @@ conf_files = [
   [ 'sink-eq6.conf', 'sink-eq6.conf' ],
   [ 'sink-matrix-spatialiser.conf', 'sink-matrix-spatialiser.conf' ],
   [ 'source-rnnoise.conf', 'source-rnnoise.conf' ],
+  [ 'sink-upmix-5.1-filter.conf', 'sink-upmix-5.1-filter.conf' ],
 ]
 
 foreach c : conf_files
diff --git a/src/daemon/filter-chain/sink-upmix-5.1-filter.conf b/src/daemon/filter-chain/sink-upmix-5.1-filter.conf
new file mode 100644
index 00000000..19773304
--- /dev/null
+++ b/src/daemon/filter-chain/sink-upmix-5.1-filter.conf
@@ -0,0 +1,151 @@
+# Stereo to 5.1 upmix sink
+#
+# Copy this file into a conf.d/ directory such as
+# ~/.config/pipewire/filter-chain.conf.d/
+#
+context.modules = [
+    {   name = libpipewire-module-filter-chain
+        args = {
+            node.description = "Upmix Sink"
+            filter.graph = {
+                nodes = [
+                    {   type = builtin name = copyFL label = copy }
+                    {   type = builtin name = copyFR label = copy }
+                    {   type = builtin name = copyOFL label = copy }
+                    {   type = builtin name = copyOFR label = copy }
+		    {
+		        # this mixes the front left and right together
+			# for filtering the center and subwoofer signal-
+                        name   = mixF
+                        type   = builtin
+                        label  = mixer
+                        control = {
+                          "Gain 1" = 0.707
+                          "Gain 2" = 0.707
+                        }
+                    }
+                    {   
+		        # filtering of the FC and LFE channel. We use a 2 channel
+			# parametric equalizer with custom filters for each channel.
+			# This makes it possible to run the filters in parallel.
+                        type = builtin
+                        name = eq_FC_LFE
+                        label = param_eq
+                        config = {
+                            filters1 = [
+			       # FC is a crossover filter (with 2 lowpass biquads)
+                               { type = bq_lowpass freq = 12000 },
+                               { type = bq_lowpass freq = 12000 },
+                            ]
+                            filters2 = [
+			       # LFE is first a gain adjustment (with a highself) and
+			       # then a crossover filter (with 2 lowpass biquads)
+                               { type = bq_highshelf freq = 0 gain = -20.0 }, # gain -20dB
+                               { type = bq_lowpass freq = 120 },
+                               { type = bq_lowpass freq = 120 },
+                            ]
+		        }
+                    }
+		    {
+		        # for the rear channels, we subtract the front channels. Do this
+			# with a mixer with negative gain to flip the sign.
+                        name   = subR
+                        type   = builtin
+                        label  = mixer
+                        control = {
+                          "Gain 1" = 0.707
+                          "Gain 2" = -0.707
+                        }
+                    }
+                    {
+		        # a delay for the rear Left channel. This can be
+			# replaced with the convolver below. */
+                        type   = builtin
+                        name   = delayRL
+                        label  = delay
+                        config = { "max-delay" = 1 }
+                        control = { "Delay (s)" = 0.012 }
+                    }
+                    {
+		        # a delay for the rear Right channel. This can be
+			# replaced with the convolver below. */
+                        type   = builtin
+                        name   = delayRR
+                        label  = delay
+                        config = { "max-delay" = 1 }
+                        control = { "Delay (s)" = 0.012 }
+                    }
+                    {
+		        # an optional convolver with a hilbert curve to
+			# change the phase. It also has a delay, making the above
+			# left delay filter optional.
+                        type   = builtin
+                        name   = convRL
+                        label  = convolver
+                        config = {
+                            gain = 1.0
+			    delay = 0.012
+                            filename = "/hilbert"
+			    length = 33
+			}
+		    }
+                    {
+		        # an optional convolver with a hilbert curve to
+			# change the phase. It also has a delay, making the above
+			# right delay filter optional.
+                        type   = builtin
+                        name   = convRR
+                        label  = convolver
+                        config = {
+                            gain = -1.0
+			    delay = 0.012
+                            filename = "/hilbert"
+			    length = 33
+			}
+		    }
+                 ]
+                 links = [
+                     { output = "copyFL:Out"  input="mixF:In 1" }
+                     { output = "copyFR:Out"  input="mixF:In 2" }
+                     { output = "copyFL:Out"  input="copyOFR:In" }
+                     { output = "copyFR:Out"  input="copyOFL:In" }
+                     { output = "mixF:Out"  input="eq_FC_LFE:In 1" }
+                     { output = "mixF:Out"  input="eq_FC_LFE:In 2" }
+                     { output = "copyFL:Out"  input="subR:In 1" }
+                     { output = "copyFR:Out"  input="subR:In 2" }
+		     # here we can choose to just delay or also convolve
+		     #
+                     #{ output = "subR:Out"  input="delayRL:In" }
+                     #{ output = "subR:Out"  input="delayRR:In" }
+                     { output = "subR:Out"  input="convRL:In" }
+                     { output = "subR:Out"  input="convRR:In" }
+                 ]
+                 inputs = [ "copyFL:In" "copyFR:In" ]
+                 outputs = [ 
+		              "copyOFL:Out"
+			      "copyOFR:Out"
+		              "eq_FC_LFE:Out 1"
+			      "eq_FC_LFE:Out 2"
+		              # here we can choose to just delay or also convolve
+                              #
+		              #"delayRL:Out"
+			      #"delayRR:Out"
+		              "convRL:Out"
+			      "convRR:Out"
+			   ]
+            }
+            capture.props = {
+                node.name = "effect_input.upmix_5.1"
+                media.class = "Audio/Sink"
+                audio.position = [ FL FR ]
+            }
+            playback.props = {
+                node.name = "effect_output.upmix_5.1"
+                audio.position = [ FL FR FC LFE RL RR ]
+                stream.dont-remix = true
+                node.passive = true
+            }
+        }
+    }
+]
+
diff --git a/src/daemon/jack.conf.in b/src/daemon/jack.conf.in
index b3be1c68..32d8076d 100644
--- a/src/daemon/jack.conf.in
+++ b/src/daemon/jack.conf.in
@@ -95,6 +95,7 @@ jack.properties = {
      #jack.max-client-ports   = 768
      #jack.fill-aliases       = false
      #jack.writable-input     = true
+     #jack.flag-midi2         = false
 }
 
 # client specific properties
diff --git a/src/daemon/meson.build b/src/daemon/meson.build
index 6d98f4b5..b2ebb937 100644
--- a/src/daemon/meson.build
+++ b/src/daemon/meson.build
@@ -66,7 +66,6 @@ endif
 conf_files = [
  'pipewire.conf',
  'client.conf',
- 'client-rt.conf',
  'filter-chain.conf',
  'jack.conf',
  'minimal.conf',
@@ -95,7 +94,6 @@ test('validate-json-pipewire-uninstalled.conf', spa_json_dump_exe, args : res)
 conf_avail_folders = [
   'pipewire.conf.avail',
   'client.conf.avail',
-  'client-rt.conf.avail',
   'pipewire-pulse.conf.avail',
 ]
 
diff --git a/src/daemon/minimal.conf.in b/src/daemon/minimal.conf.in
index 031402c7..cfaab1c1 100644
--- a/src/daemon/minimal.conf.in
+++ b/src/daemon/minimal.conf.in
@@ -176,11 +176,11 @@ context.objects = [
     # Creates an object from a PipeWire factory with the given parameters.
     # If nofail is given, errors are ignored (and no object is created).
     #
-    #{ factory = spa-node-factory   args = { factory.name = videotestsrc node.name = videotestsrc node.description = videotestsrc "Spa:Pod:Object:Param:Props:patternType" = 1 } }
+    #{ factory = spa-node-factory   args = { factory.name = videotestsrc node.name = videotestsrc node.description = videotestsrc node.param.Props = { patternType = 1 } } }
     #{ factory = spa-device-factory args = { factory.name = api.jack.device foo=bar } flags = [ nofail ] }
     #{ factory = spa-device-factory args = { factory.name = api.alsa.enum.udev } }
     #{ factory = spa-node-factory   args = { factory.name = api.alsa.seq.bridge node.name = Internal-MIDI-Bridge } }
-    #{ factory = adapter            args = { factory.name = audiotestsrc node.name = my-test node.description = audiotestsrc } }
+    #{ factory = adapter            args = { factory.name = audiotestsrc node.name = my-test node.description = audiotestsrc node.param.Props = { live = false } } }
     #{ factory = spa-node-factory   args = { factory.name = api.vulkan.compute.source node.name = my-compute-source } }
 
     # Make a default metadata store
diff --git a/src/daemon/pipewire-aes67.conf.in b/src/daemon/pipewire-aes67.conf.in
index 34c432f6..479c12c9 100644
--- a/src/daemon/pipewire-aes67.conf.in
+++ b/src/daemon/pipewire-aes67.conf.in
@@ -135,6 +135,10 @@ context.modules = [
             audio.channels = 2
             # These channel names will be visible both to applications and AES67 receivers
             node.channel-names = ["CH1", "CH2"]
+            # Uncomment this and comment node.group in send/recv stream.props to allow
+            # separate drivers for the RTP sink and PTP sending (i.e. force rate matching on
+            # the AES67 node rather than other nodes)
+            #aes67.driver-group = "pipewire.ptp0"
 
             stream.props = {
                 ### Please change the sink name, this is necessary when you create multiple sinks
diff --git a/src/daemon/pipewire-pulse.conf.in b/src/daemon/pipewire-pulse.conf.in
index 0d54cf9b..affb993d 100644
--- a/src/daemon/pipewire-pulse.conf.in
+++ b/src/daemon/pipewire-pulse.conf.in
@@ -60,11 +60,17 @@ context.exec = [
 #   load-module : loads a module with args and flags
 #      args = "<module-name> <module-args>"
 #      ( flags = [ nofail ] )
+#      ( condition = [ { <key1> = <value1>, ... } ... ] )
+# conditions will check the pulse.properties key/values.
 pulse.cmd = [
-    { cmd = "load-module" args = "module-always-sink" flags = [ ] }
-    { cmd = "load-module" args = "module-device-manager" flags = [ ] }
-    { cmd = "load-module" args = "module-device-restore" flags = [ ] }
-    { cmd = "load-module" args = "module-stream-restore" flags = [ ] }
+    { cmd = "load-module" args = "module-always-sink" flags = [ ]
+        condition = [ { pulse.cmd.always-sink = !false } ] }
+    { cmd = "load-module" args = "module-device-manager" flags = [ ]
+        condition = [ { pulse.cmd.device-manager = !false } ] }
+    { cmd = "load-module" args = "module-device-restore" flags = [ ]
+        condition = [ { pulse.cmd.device-restore = !false } ] }
+    { cmd = "load-module" args = "module-stream-restore" flags = [ ]
+        condition = [ { pulse.cmd.stream-restore = !false } ] }
     #{ cmd = "load-module" args = "module-switch-on-connect" }
     #{ cmd = "load-module" args = "module-gsettings" flags = [ nofail ] }
 ]
@@ -152,6 +158,7 @@ pulse.rules = [
         matches = [
              { application.process.binary = "teams" }
              { application.process.binary = "teams-insiders" }
+             { application.process.binary = "teams-for-linux" }
              { application.process.binary = "skypeforlinux" }
         ]
         actions = { quirks = [ force-s16-info ] }
diff --git a/src/daemon/pipewire-vulkan.conf.in b/src/daemon/pipewire-vulkan.conf.in
index b00c8d35..388a4fc5 100644
--- a/src/daemon/pipewire-vulkan.conf.in
+++ b/src/daemon/pipewire-vulkan.conf.in
@@ -87,11 +87,11 @@ context.objects = [
     # If condition is given, the object is created only when the context properties
     # all match the match rules.
     #
-    #{ factory = spa-node-factory   args = { factory.name = videotestsrc node.name = videotestsrc node.description = videotestsrc "Spa:Pod:Object:Param:Props:patternType" = 1 } }
+    #{ factory = spa-node-factory   args = { factory.name = videotestsrc node.name = videotestsrc node.description = videotestsrc node.param.Props = { patternType = 1 } } }
     #{ factory = spa-device-factory args = { factory.name = api.jack.device foo=bar } flags = [ nofail ] }
     #{ factory = spa-device-factory args = { factory.name = api.alsa.enum.udev } }
     #{ factory = spa-node-factory   args = { factory.name = api.alsa.seq.bridge node.name = Internal-MIDI-Bridge } }
-    #{ factory = adapter            args = { factory.name = audiotestsrc node.name = my-test node.description = audiotestsrc } }
+    #{ factory = adapter            args = { factory.name = audiotestsrc node.name = my-test node.description = audiotestsrc node.param.Props = { live = false } } }
     { factory = spa-node-factory   args = { factory.name = api.vulkan.compute.source node.name = vulkan-compute-source object.export = true } }
     { factory = spa-node-factory   args = { factory.name = api.vulkan.compute.filter node.name = vulkan-compute-filter object.export = true } }
     { factory = spa-node-factory   args = { factory.name = api.vulkan.blit.filter node.name = vulkan-blit-filter object.export = true } }
diff --git a/src/daemon/pipewire.conf.in b/src/daemon/pipewire.conf.in
index 470cf960..14f7c6ea 100644
--- a/src/daemon/pipewire.conf.in
+++ b/src/daemon/pipewire.conf.in
@@ -54,14 +54,6 @@ context.properties = {
     #
     #settings.check-quantum      = false
     #settings.check-rate         = false
-
-    # keys checked below to disable module loading
-    module.x11.bell = true
-    # enables autoloading of access module, when disabled an alternative
-    # access module needs to be loaded.
-    module.access = true
-    # enables autoloading of module-jackdbus-detect
-    module.jackdbus-detect = true
 }
 
 context.properties.rules = [
@@ -92,6 +84,7 @@ context.spa-libs = {
     api.jack.*      = jack/libspa-jack
     support.*       = support/libspa-support
     video.convert.* = videoconvert/libspa-videoconvert
+    #filter.graph    = filter-graph/libspa-filter-graph
     #videotestsrc   = videotestsrc/libspa-videotestsrc
     #audiotestsrc   = audiotestsrc/libspa-audiotestsrc
 }
@@ -114,6 +107,7 @@ context.modules = [
     # RTKit if the user doesn't have permission to use regular realtime
     # scheduling. You can also clamp utilisation values to improve scheduling
     # on embedded and heterogeneous systems, e.g. Arm big.LITTLE devices.
+    # use module.rt.args = { ... } to override the arguments.
     { name = libpipewire-module-rt
         args = {
             nice.level    = -11
@@ -124,6 +118,7 @@ context.modules = [
             #uclamp.max = 1024
         }
         flags = [ ifexists nofail ]
+        condition = [ { module.rt = !false } ]
     }
 
     # The native communication protocol.
@@ -137,34 +132,51 @@ context.modules = [
     # The profile module. Allows application to access profiler
     # and performance data. It provides an interface that is used
     # by pw-top and pw-profiler.
-    { name = libpipewire-module-profiler }
+    # use module.profiler.args = { ... } to override the arguments.
+    { name = libpipewire-module-profiler
+        args = {
+            #profile.interval.ms = 0
+        }
+        condition = [ { module.profiler = !false } ]
+    }
 
     # Allows applications to create metadata objects. It creates
     # a factory for Metadata objects.
-    { name = libpipewire-module-metadata }
+    { name = libpipewire-module-metadata
+        condition = [ { module.metadata = !false } ]
+    }
 
     # Creates a factory for making devices that run in the
     # context of the PipeWire server.
-    { name = libpipewire-module-spa-device-factory }
+    { name = libpipewire-module-spa-device-factory
+        condition = [ { module.spa-device-factory = !false } ]
+    }
 
     # Creates a factory for making nodes that run in the
     # context of the PipeWire server.
-    { name = libpipewire-module-spa-node-factory }
+    { name = libpipewire-module-spa-node-factory
+        condition = [ { module.spa-node-factory = !false } ]
+    }
 
     # Allows creating nodes that run in the context of the
     # client. Is used by all clients that want to provide
     # data to PipeWire.
-    { name = libpipewire-module-client-node }
+    { name = libpipewire-module-client-node
+        condition = [ { module.client-node = !false } ]
+    }
 
     # Allows creating devices that run in the context of the
     # client. Is used by the session manager.
-    { name = libpipewire-module-client-device }
+    { name = libpipewire-module-client-device
+        condition = [ { module.client-device = !false } ]
+    }
 
     # The portal module monitors the PID of the portal process
     # and tags connections with the same PID as portal
     # connections.
     { name = libpipewire-module-portal
         flags = [ ifexists nofail ]
+        condition = [ { module.portal = !false } ]
     }
 
     # The access module can perform access checks and block
@@ -178,18 +190,28 @@ context.modules = [
             # for now enabled by default if access.socket is not specified
             #access.legacy = true
         }
-        condition = [ { module.access = true } ]
+        condition = [ { module.access = !false } ]
     }
 
     # Makes a factory for wrapping nodes in an adapter with a
     # converter and resampler.
-    { name = libpipewire-module-adapter }
+    { name = libpipewire-module-adapter
+        condition = [ { module.adapter = !false } ]
+    }
 
     # Makes a factory for creating links between ports.
-    { name = libpipewire-module-link-factory }
+    # use module.link-factory.args = { ... } to override the arguments.
+    { name = libpipewire-module-link-factory
+        args = {
+            #allow.link.passive = false
+	}
+        condition = [ { module.link-factory = !false } ]
+    }
 
     # Provides factories to make session manager objects.
-    { name = libpipewire-module-session-manager }
+    { name = libpipewire-module-session-manager
+        condition = [ { module.session-manager = !false } ]
+    }
 
     # Use libcanberra to play X11 Bell
     { name = libpipewire-module-x11-bell
@@ -200,8 +222,11 @@ context.modules = [
             #x11.xauthority = null
         }
         flags = [ ifexists nofail ]
-        condition = [ { module.x11.bell = true } ]
+        condition = [ { module.x11.bell = !false } ]
     }
+    # The JACK DBus detection module. When jackdbus is started, this
+    # will automatically make PipeWire become a JACK client.
+    # use module.jackdbus-detect.args = { ... } to override the arguments.
     { name = libpipewire-module-jackdbus-detect
         args = {
             #jack.library     = libjack.so.0
@@ -223,7 +248,7 @@ context.modules = [
             }
         }
         flags = [ ifexists nofail ]
-        condition = [ { module.jackdbus-detect = true } ]
+        condition = [ { module.jackdbus-detect = !false } ]
     }
 ]
 
@@ -239,11 +264,11 @@ context.objects = [
     # If condition is given, the object is created only when the context properties
     # all match the match rules.
     #
-    #{ factory = spa-node-factory   args = { factory.name = videotestsrc node.name = videotestsrc node.description = videotestsrc "Spa:Pod:Object:Param:Props:patternType" = 1 } }
+    #{ factory = spa-node-factory   args = { factory.name = videotestsrc node.name = videotestsrc node.description = videotestsrc node.param.Props = { patternType = 1 } } }
     #{ factory = spa-device-factory args = { factory.name = api.jack.device foo=bar } flags = [ nofail ] }
     #{ factory = spa-device-factory args = { factory.name = api.alsa.enum.udev } }
     #{ factory = spa-node-factory   args = { factory.name = api.alsa.seq.bridge node.name = Internal-MIDI-Bridge } }
-    #{ factory = adapter            args = { factory.name = audiotestsrc node.name = my-test node.description = audiotestsrc } }
+    #{ factory = adapter            args = { factory.name = audiotestsrc node.name = my-test node.description = audiotestsrc node.param.Props = { live = false }} }
     #{ factory = spa-node-factory   args = { factory.name = api.vulkan.compute.source node.name = my-compute-source } }
 
     # A default dummy driver. This handles nodes marked with the "node.always-process"
@@ -258,6 +283,7 @@ context.objects = [
             #clock.id       = monotonic # realtime | tai | monotonic-raw | boottime
             #clock.name     = "clock.system.monotonic"
         }
+        condition = [ { factory.dummy-driver = !false } ]
     }
     { factory = spa-node-factory
         args = {
@@ -269,6 +295,7 @@ context.objects = [
             node.freewheel  = true
             #freewheel.wait = 10
         }
+        condition = [ { factory.freewheel-driver = !false } ]
     }
 
     # This creates a new Source node. It will have input ports
@@ -332,7 +359,7 @@ context.exec = [
     # Run the session manager with -h for options.
     #
     @sm_comment@{ path = "@session_manager_path@" args = "@session_manager_args@"
-    @sm_comment@  condition = [ { exec.session-manager = null } { exec.session-manager = true } ] }
+    @sm_comment@  condition = [ { exec.session-manager = !false } ] }
     #
     # You can optionally start the pulseaudio-server here as well
     # but it is better to start it as a systemd service.
@@ -340,5 +367,5 @@ context.exec = [
     # on another address with the -a option (eg. -a tcp:4713).
     #
     @pulse_comment@{ path = "@pipewire_path@" args = [ "-c" "pipewire-pulse.conf" ]
-    @pulse_comment@  condition = [ { exec.pipewire-pulse = null } { exec.pipewire-pulse = true } ] }
+    @pulse_comment@  condition = [ { exec.pipewire-pulse = !false } ] }
 ]
diff --git a/src/daemon/systemd/system/meson.build b/src/daemon/systemd/system/meson.build
index d06d3adf..0cc17670 100644
--- a/src/daemon/systemd/system/meson.build
+++ b/src/daemon/systemd/system/meson.build
@@ -3,13 +3,19 @@ if get_option('systemd-system-unit-dir') != ''
   systemd_system_services_dir = get_option('systemd-system-unit-dir')
 endif
 
-install_data(sources : ['pipewire.socket', 'pipewire-manager.socket'],
+install_data(sources : ['pipewire.socket', 'pipewire-manager.socket', 'pipewire-pulse.socket' ],
              install_dir : systemd_system_services_dir)
 
 systemd_config = configuration_data()
 systemd_config.set('PW_BINARY', pipewire_bindir / 'pipewire')
+systemd_config.set('PW_PULSE_BINARY', pipewire_bindir / 'pipewire-pulse')
 
 configure_file(input : 'pipewire.service.in',
                output : 'pipewire.service',
                configuration : systemd_config,
                install_dir : systemd_system_services_dir)
+
+configure_file(input : 'pipewire-pulse.service.in',
+               output : 'pipewire-pulse.service',
+               configuration : systemd_config,
+               install_dir : systemd_system_services_dir)
diff --git a/src/daemon/systemd/system/pipewire-pulse.service.in b/src/daemon/systemd/system/pipewire-pulse.service.in
new file mode 100644
index 00000000..8752f785
--- /dev/null
+++ b/src/daemon/systemd/system/pipewire-pulse.service.in
@@ -0,0 +1,24 @@
+[Unit]
+Description=PipeWire PulseAudio Service
+Requires=pipewire-pulse.socket
+Wants=pipewire.service pipewire-session-manager.service
+After=pipewire.service pipewire-session-manager.service
+
+[Service]
+LockPersonality=yes
+MemoryDenyWriteExecute=yes
+NoNewPrivileges=yes
+SystemCallArchitectures=native
+SystemCallFilter=@system-service
+Type=simple
+AmbientCapabilities=CAP_SYS_NICE
+ExecStart=@PW_PULSE_BINARY@
+Restart=on-failure
+User=pipewire
+Environment=PIPEWIRE_RUNTIME_DIR=%t/pipewire
+Environment=PULSE_RUNTIME_PATH=%t/pulse
+
+[Install]
+Also=pipewire-pulse.socket
+WantedBy=pipewire.service
+
diff --git a/src/daemon/systemd/system/pipewire-pulse.socket b/src/daemon/systemd/system/pipewire-pulse.socket
new file mode 100644
index 00000000..0a692949
--- /dev/null
+++ b/src/daemon/systemd/system/pipewire-pulse.socket
@@ -0,0 +1,12 @@
+[Unit]
+Description=PipeWire PulseAudio System Socket
+
+[Socket]
+Priority=6
+ListenStream=%t/pulse/native
+SocketUser=pipewire
+SocketGroup=pipewire
+SocketMode=0660
+
+[Install]
+WantedBy=sockets.target
diff --git a/src/examples/audio-capture.c b/src/examples/audio-capture.c
index f6a31dfa..c44f905f 100644
--- a/src/examples/audio-capture.c
+++ b/src/examples/audio-capture.c
@@ -143,7 +143,6 @@ int main(int argc, char *argv[])
 	 * the data.
 	 */
 	props = pw_properties_new(PW_KEY_MEDIA_TYPE, "Audio",
-			PW_KEY_CONFIG_NAME, "client-rt.conf",
 			PW_KEY_MEDIA_CATEGORY, "Capture",
 			PW_KEY_MEDIA_ROLE, "Music",
 			NULL);
diff --git a/src/examples/audio-src-ring.c b/src/examples/audio-src-ring.c
new file mode 100644
index 00000000..b96e26f1
--- /dev/null
+++ b/src/examples/audio-src-ring.c
@@ -0,0 +1,226 @@
+/* PipeWire */
+/* SPDX-FileCopyrightText: Copyright © 2024 Wim Taymans */
+/* SPDX-License-Identifier: MIT */
+
+/*
+ [title]
+ Audio source using \ref pw_stream "pw_stream" and ringbuffer.
+ [title]
+ */
+
+#include <stdio.h>
+#include <errno.h>
+#include <math.h>
+#include <signal.h>
+
+#include <spa/param/audio/format-utils.h>
+#include <spa/utils/ringbuffer.h>
+
+#include <pipewire/pipewire.h>
+
+#define M_PI_M2f (float)(M_PI+M_PI)
+
+#define DEFAULT_RATE		44100
+#define DEFAULT_CHANNELS	2
+#define DEFAULT_VOLUME		0.7f
+
+#define BUFFER_SIZE		(16*1024)
+
+struct data {
+	struct pw_main_loop *main_loop;
+	struct pw_loop *loop;
+	struct pw_stream *stream;
+
+	float accumulator;
+
+	struct spa_source *refill_event;
+
+	struct spa_ringbuffer ring;
+	float buffer[BUFFER_SIZE * DEFAULT_CHANNELS];
+};
+
+static void fill_f32(struct data *d, uint32_t offset, int n_frames)
+{
+	float val;
+	int i, c;
+
+        for (i = 0; i < n_frames; i++) {
+                d->accumulator += M_PI_M2f * 440 / DEFAULT_RATE;
+                if (d->accumulator >= M_PI_M2f)
+                        d->accumulator -= M_PI_M2f;
+
+                val = sinf(d->accumulator) * DEFAULT_VOLUME;
+                for (c = 0; c < DEFAULT_CHANNELS; c++)
+                        d->buffer[((offset + i) % BUFFER_SIZE) * DEFAULT_CHANNELS + c] = val;
+        }
+}
+
+/* this is called from the main-thread when we need to fill up the ringbuffer
+ * with more data */
+static void do_refill(void *userdata, uint64_t count)
+{
+	struct data *data = userdata;
+	int32_t filled;
+	uint32_t index, avail;
+
+	filled = spa_ringbuffer_get_write_index(&data->ring, &index);
+	/* we xrun, this can not happen because we never read more
+	 * than what there is in the ringbuffer and we never write more than
+	 * what is left */
+	spa_assert(filled >= 0);
+	spa_assert(filled <= BUFFER_SIZE);
+
+	/* this is how much samples we can write */
+	avail = BUFFER_SIZE - filled;
+
+	/* write new samples to the ringbuffer from the given index */
+	fill_f32(data, index, avail);
+
+	/* and advance the ringbuffer */
+	spa_ringbuffer_write_update(&data->ring, index + avail);
+}
+
+/* our data processing function is in general:
+ *
+ *  struct pw_buffer *b;
+ *  b = pw_stream_dequeue_buffer(stream);
+ *
+ *  .. generate stuff in the buffer ...
+ *  In this case we read samples from a ringbuffer. The ringbuffer is
+ *  filled up by another thread.
+ *
+ *  pw_stream_queue_buffer(stream, b);
+ */
+static void on_process(void *userdata)
+{
+	struct data *data = userdata;
+	struct pw_buffer *b;
+	struct spa_buffer *buf;
+	uint8_t *p;
+	uint32_t index, to_read, to_silence;
+	int32_t avail, n_frames, stride;
+
+	if ((b = pw_stream_dequeue_buffer(data->stream)) == NULL) {
+		pw_log_warn("out of buffers: %m");
+		return;
+	}
+
+	buf = b->buffer;
+	if ((p = buf->datas[0].data) == NULL)
+		return;
+
+	/* the amount of space in the ringbuffer and the read index */
+	avail = spa_ringbuffer_get_read_index(&data->ring, &index);
+
+	stride = sizeof(float) * DEFAULT_CHANNELS;
+	n_frames = buf->datas[0].maxsize / stride;
+	if (b->requested)
+		n_frames = SPA_MIN((int32_t)b->requested, n_frames);
+
+	/* we can read if there is something available */
+	to_read = avail > 0 ? SPA_MIN(avail, n_frames) : 0;
+	/* and fill the remainder with silence */
+	to_silence = n_frames - to_read;
+
+	if (to_read > 0) {
+		/* read data into the buffer */
+		spa_ringbuffer_read_data(&data->ring,
+				data->buffer, BUFFER_SIZE * stride,
+				(index % BUFFER_SIZE) * stride,
+				p, to_read * stride);
+		/* update the read pointer */
+		spa_ringbuffer_read_update(&data->ring, index + to_read);
+	}
+	if (to_silence > 0)
+		/* set the rest of the buffer to silence */
+		memset(SPA_PTROFF(p, to_read * stride, void), 0, to_silence * stride);
+
+	buf->datas[0].chunk->offset = 0;
+	buf->datas[0].chunk->stride = stride;
+	buf->datas[0].chunk->size = n_frames * stride;
+
+	pw_stream_queue_buffer(data->stream, b);
+
+	/* signal the main thread to fill the ringbuffer, we can only do this, for
+	 * example when the available ringbuffer space falls below a certain
+	 * level. */
+	pw_loop_signal_event(data->loop, data->refill_event);
+}
+
+static const struct pw_stream_events stream_events = {
+	PW_VERSION_STREAM_EVENTS,
+	.process = on_process,
+};
+
+static void do_quit(void *userdata, int signal_number)
+{
+	struct data *data = userdata;
+	pw_main_loop_quit(data->main_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);
+
+	data.main_loop = pw_main_loop_new(NULL);
+	data.loop = pw_main_loop_get_loop(data.main_loop);
+
+	pw_loop_add_signal(data.loop, SIGINT, do_quit, &data);
+	pw_loop_add_signal(data.loop, SIGTERM, do_quit, &data);
+
+	/* we're going to refill a ringbuffer from the main loop. Make an
+	 * event for this. */
+	spa_ringbuffer_init(&data.ring);
+	data.refill_event = pw_loop_add_event(data.loop, do_refill, &data);
+	/* prefill the ringbuffer */
+	do_refill(&data, 0);
+
+	props = pw_properties_new(PW_KEY_MEDIA_TYPE, "Audio",
+			PW_KEY_MEDIA_CATEGORY, "Playback",
+			PW_KEY_MEDIA_ROLE, "Music",
+			NULL);
+	if (argc > 1)
+		/* Set stream target if given on command line */
+		pw_properties_set(props, PW_KEY_TARGET_OBJECT, argv[1]);
+
+	data.stream = pw_stream_new_simple(
+			data.loop,
+			"audio-src-ring",
+			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). */
+	params[0] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat,
+			&SPA_AUDIO_INFO_RAW_INIT(
+				.format = SPA_AUDIO_FORMAT_F32,
+				.channels = DEFAULT_CHANNELS,
+				.rate = DEFAULT_RATE ));
+
+	/* Now connect this stream. We ask that our process function is
+	 * called in a realtime thread. */
+	pw_stream_connect(data.stream,
+			  PW_DIRECTION_OUTPUT,
+			  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.main_loop);
+
+	pw_stream_destroy(data.stream);
+	pw_loop_destroy_source(data.loop, data.refill_event);
+	pw_main_loop_destroy(data.main_loop);
+	pw_deinit();
+
+	return 0;
+}
diff --git a/src/examples/audio-src-ring2.c b/src/examples/audio-src-ring2.c
new file mode 100644
index 00000000..19f8eb4d
--- /dev/null
+++ b/src/examples/audio-src-ring2.c
@@ -0,0 +1,269 @@
+/* PipeWire */
+/* SPDX-FileCopyrightText: Copyright © 2024 Wim Taymans */
+/* SPDX-License-Identifier: MIT */
+
+/*
+ [title]
+ Audio source using \ref pw_stream "pw_stream" and ringbuffer.
+
+ This one uses a thread-loop and does a blocking push into a
+ ringbuffer.
+ [title]
+ */
+
+#include <stdio.h>
+#include <errno.h>
+#include <math.h>
+#include <signal.h>
+
+#include <spa/param/audio/format-utils.h>
+#include <spa/utils/ringbuffer.h>
+
+#include <pipewire/pipewire.h>
+
+#define M_PI_M2f (float)(M_PI+M_PI)
+
+#define DEFAULT_RATE		44100
+#define DEFAULT_CHANNELS	2
+#define DEFAULT_VOLUME		0.7f
+
+#define BUFFER_SIZE		(16*1024)
+
+#define MIN_SIZE	256
+#define MAX_SIZE	BUFFER_SIZE
+
+static float samples[BUFFER_SIZE * DEFAULT_CHANNELS];
+
+struct data {
+	struct pw_thread_loop *thread_loop;
+	struct pw_loop *loop;
+	struct pw_stream *stream;
+	int eventfd;
+	bool running;
+
+	float accumulator;
+
+	struct spa_ringbuffer ring;
+	float buffer[BUFFER_SIZE * DEFAULT_CHANNELS];
+};
+
+static void fill_f32(struct data *d, float *samples, int n_frames)
+{
+	float val;
+	int i, c;
+
+        for (i = 0; i < n_frames; i++) {
+                d->accumulator += M_PI_M2f * 440 / DEFAULT_RATE;
+                if (d->accumulator >= M_PI_M2f)
+                        d->accumulator -= M_PI_M2f;
+
+                val = sinf(d->accumulator) * DEFAULT_VOLUME;
+                for (c = 0; c < DEFAULT_CHANNELS; c++)
+			samples[i * DEFAULT_CHANNELS + c] = val;
+        }
+}
+
+/* this can be called from any thread with a block of samples to write into
+ * the ringbuffer. It will block until all data has been written */
+static void push_samples(void *userdata, float *samples, uint32_t n_samples)
+{
+	struct data *data = userdata;
+	int32_t filled;
+	uint32_t index, avail, stride = sizeof(float) * DEFAULT_CHANNELS;
+	uint64_t count;
+	float *s = samples;
+
+	while (n_samples > 0) {
+		while (true) {
+			filled = spa_ringbuffer_get_write_index(&data->ring, &index);
+			/* we xrun, this can not happen because we never read more
+			 * than what there is in the ringbuffer and we never write more than
+			 * what is left */
+			spa_assert(filled >= 0);
+			spa_assert(filled <= BUFFER_SIZE);
+
+			/* this is how much samples we can write */
+			avail = BUFFER_SIZE - filled;
+			if (avail > 0)
+				break;
+
+			/* no space.. block and wait for free space */
+			spa_system_eventfd_read(data->loop->system, data->eventfd, &count);
+		}
+		if (avail > n_samples)
+			avail = n_samples;
+
+		spa_ringbuffer_write_data(&data->ring,
+				data->buffer, BUFFER_SIZE * stride,
+				(index % BUFFER_SIZE) * stride,
+				s, avail * stride);
+
+		s += avail * DEFAULT_CHANNELS;
+		n_samples -= avail;
+
+		/* and advance the ringbuffer */
+		spa_ringbuffer_write_update(&data->ring, index + avail);
+	}
+
+}
+
+/* our data processing function is in general:
+ *
+ *  struct pw_buffer *b;
+ *  b = pw_stream_dequeue_buffer(stream);
+ *
+ *  .. generate stuff in the buffer ...
+ *  In this case we read samples from a ringbuffer. The ringbuffer is
+ *  filled up by another thread.
+ *
+ *  pw_stream_queue_buffer(stream, b);
+ */
+static void on_process(void *userdata)
+{
+	struct data *data = userdata;
+	struct pw_buffer *b;
+	struct spa_buffer *buf;
+	uint8_t *p;
+	uint32_t index, to_read, to_silence;
+	int32_t avail, n_frames, stride;
+
+	if ((b = pw_stream_dequeue_buffer(data->stream)) == NULL) {
+		pw_log_warn("out of buffers: %m");
+		return;
+	}
+
+	buf = b->buffer;
+	if ((p = buf->datas[0].data) == NULL)
+		return;
+
+	/* the amount of space in the ringbuffer and the read index */
+	avail = spa_ringbuffer_get_read_index(&data->ring, &index);
+
+	stride = sizeof(float) * DEFAULT_CHANNELS;
+	n_frames = buf->datas[0].maxsize / stride;
+	if (b->requested)
+		n_frames = SPA_MIN((int32_t)b->requested, n_frames);
+
+	/* we can read if there is something available */
+	to_read = avail > 0 ? SPA_MIN(avail, n_frames) : 0;
+	/* and fill the remainder with silence */
+	to_silence = n_frames - to_read;
+
+	if (to_read > 0) {
+		/* read data into the buffer */
+		spa_ringbuffer_read_data(&data->ring,
+				data->buffer, BUFFER_SIZE * stride,
+				(index % BUFFER_SIZE) * stride,
+				p, to_read * stride);
+		/* update the read pointer */
+		spa_ringbuffer_read_update(&data->ring, index + to_read);
+	}
+	if (to_silence > 0)
+		/* set the rest of the buffer to silence */
+		memset(SPA_PTROFF(p, to_read * stride, void), 0, to_silence * stride);
+
+	buf->datas[0].chunk->offset = 0;
+	buf->datas[0].chunk->stride = stride;
+	buf->datas[0].chunk->size = n_frames * stride;
+
+	pw_stream_queue_buffer(data->stream, b);
+
+	/* signal the main thread to fill the ringbuffer, we can only do this, for
+	 * example when the available ringbuffer space falls below a certain
+	 * level. */
+	spa_system_eventfd_write(data->loop->system, data->eventfd, 1);
+}
+
+static const struct pw_stream_events stream_events = {
+	PW_VERSION_STREAM_EVENTS,
+	.process = on_process,
+};
+
+static void do_quit(void *userdata, int signal_number)
+{
+	struct data *data = userdata;
+	data->running = false;
+}
+
+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);
+
+	data.thread_loop = pw_thread_loop_new("audio-src", NULL);
+	data.loop = pw_thread_loop_get_loop(data.thread_loop);
+	data.running = true;
+
+	pw_thread_loop_lock(data.thread_loop);
+	pw_loop_add_signal(data.loop, SIGINT, do_quit, &data);
+	pw_loop_add_signal(data.loop, SIGTERM, do_quit, &data);
+
+	spa_ringbuffer_init(&data.ring);
+	if ((data.eventfd = spa_system_eventfd_create(data.loop->system, SPA_FD_CLOEXEC)) < 0)
+                return data.eventfd;
+
+	pw_thread_loop_start(data.thread_loop);
+
+	props = pw_properties_new(PW_KEY_MEDIA_TYPE, "Audio",
+			PW_KEY_MEDIA_CATEGORY, "Playback",
+			PW_KEY_MEDIA_ROLE, "Music",
+			NULL);
+	if (argc > 1)
+		/* Set stream target if given on command line */
+		pw_properties_set(props, PW_KEY_TARGET_OBJECT, argv[1]);
+
+	data.stream = pw_stream_new_simple(
+			data.loop,
+			"audio-src-ring",
+			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). */
+	params[0] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat,
+			&SPA_AUDIO_INFO_RAW_INIT(
+				.format = SPA_AUDIO_FORMAT_F32,
+				.channels = DEFAULT_CHANNELS,
+				.rate = DEFAULT_RATE ));
+
+	/* Now connect this stream. We ask that our process function is
+	 * called in a realtime thread. */
+	pw_stream_connect(data.stream,
+			  PW_DIRECTION_OUTPUT,
+			  PW_ID_ANY,
+			  PW_STREAM_FLAG_AUTOCONNECT |
+			  PW_STREAM_FLAG_MAP_BUFFERS |
+			  PW_STREAM_FLAG_RT_PROCESS,
+			  params, 1);
+
+	/* prefill the ringbuffer */
+	fill_f32(&data, samples, BUFFER_SIZE);
+	push_samples(&data, samples, BUFFER_SIZE);
+
+	srand(time(NULL));
+
+	pw_thread_loop_start(data.thread_loop);
+	pw_thread_loop_unlock(data.thread_loop);
+
+	while (data.running) {
+		uint32_t size = rand() % ((MAX_SIZE - MIN_SIZE + 1) + MIN_SIZE);
+		/* make new random sized block of samples and push */
+		fill_f32(&data, samples, size);
+		push_samples(&data, samples, size);
+	}
+
+	pw_thread_loop_lock(data.thread_loop);
+	pw_stream_destroy(data.stream);
+	pw_thread_loop_unlock(data.thread_loop);
+	pw_thread_loop_destroy(data.thread_loop);
+	close(data.eventfd);
+	pw_deinit();
+
+	return 0;
+}
diff --git a/src/examples/gmain.c b/src/examples/gmain.c
new file mode 100644
index 00000000..6a13b03b
--- /dev/null
+++ b/src/examples/gmain.c
@@ -0,0 +1,101 @@
+
+#include <glib.h>
+
+#include <pipewire/pipewire.h>
+#include <spa/utils/result.h>
+
+typedef struct _PipeWireSource
+{
+  GSource base;
+
+  struct pw_loop *loop;
+} PipeWireSource;
+
+static gboolean
+pipewire_loop_source_dispatch (GSource     *source,
+                               GSourceFunc  callback,
+                               gpointer     user_data)
+{
+	PipeWireSource *s = (PipeWireSource *) source;
+	int result;
+
+	result = pw_loop_iterate (s->loop, 0);
+	if (result < 0)
+		g_warning ("pipewire_loop_iterate failed: %s", spa_strerror (result));
+
+	return TRUE;
+}
+
+static GSourceFuncs pipewire_source_funcs =
+{
+	.dispatch = pipewire_loop_source_dispatch,
+};
+
+static void registry_event_global(void *data, uint32_t id,
+		uint32_t permissions, const char *type, uint32_t version,
+		const struct spa_dict *props)
+{
+	printf("object: id:%u type:%s/%d\n", id, type, version);
+}
+
+static const struct pw_registry_events registry_events = {
+	PW_VERSION_REGISTRY_EVENTS,
+	.global = registry_event_global,
+};
+
+int main(int argc, char *argv[])
+{
+	GMainLoop *main_loop;
+	PipeWireSource *source;
+	struct pw_loop *loop;
+	struct pw_context *context;
+	struct pw_core *core;
+	struct pw_registry *registry;
+	struct spa_hook registry_listener;
+
+	main_loop = g_main_loop_new (NULL, FALSE);
+
+	pw_init(&argc, &argv);
+
+	loop = pw_loop_new(NULL /* properties */);
+	/* wrap */
+	source = (PipeWireSource *) g_source_new (&pipewire_source_funcs,
+                                        sizeof (PipeWireSource));
+	source->loop = loop;
+	g_source_add_unix_fd (&source->base,
+                        pw_loop_get_fd (loop),
+                        G_IO_IN | G_IO_ERR);
+	g_source_attach (&source->base, NULL);
+	g_source_unref (&source->base);
+
+	context = pw_context_new(loop,
+			NULL /* properties */,
+			0 /* user_data size */);
+
+	core = pw_context_connect(context,
+			NULL /* properties */,
+			0 /* user_data size */);
+
+	registry = pw_core_get_registry(core, PW_VERSION_REGISTRY,
+			0 /* user_data size */);
+
+	spa_zero(registry_listener);
+	pw_registry_add_listener(registry, &registry_listener,
+				       &registry_events, NULL);
+
+	/* enter and leave must be called from the same thread that runs
+	 * the mainloop */
+	pw_loop_enter(loop);
+	g_main_loop_run(main_loop);
+	pw_loop_leave(loop);
+
+	pw_proxy_destroy((struct pw_proxy*)registry);
+	pw_core_disconnect(core);
+	pw_context_destroy(context);
+	pw_loop_destroy(loop);
+
+	g_main_loop_unref(main_loop);
+
+	return 0;
+}
+/* [code] */
diff --git a/src/examples/internal.c b/src/examples/internal.c
index aced5283..67d39b5d 100644
--- a/src/examples/internal.c
+++ b/src/examples/internal.c
@@ -40,6 +40,7 @@ static void do_quit(void *userdata, int signal_number)
 int main(int argc, char *argv[])
 {
 	struct data data = { 0, };
+	struct pw_loop *loop;
 	struct pw_properties *props;
 	const char *dev = "hw:0";
 
@@ -47,16 +48,15 @@ int main(int argc, char *argv[])
 
 	data.loop = pw_main_loop_new(NULL);
 
+	loop = pw_main_loop_get_loop(data.loop);
+
 	if (argc > 1)
 		dev = argv[1];
 
-	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);
+	pw_loop_add_signal(loop, SIGINT, do_quit, &data);
+	pw_loop_add_signal(loop, SIGTERM, do_quit, &data);
 
-	data.context = pw_context_new(pw_main_loop_get_loop(data.loop),
-			pw_properties_new(
-				PW_KEY_CONFIG_NAME, "client-rt.conf",
-				NULL), 0);
+	data.context = pw_context_new(loop, NULL, 0);
 
 	pw_context_load_module(data.context, "libpipewire-module-spa-node-factory", NULL, NULL);
 	pw_context_load_module(data.context, "libpipewire-module-link-factory", NULL, NULL);
@@ -72,7 +72,7 @@ int main(int argc, char *argv[])
                         SPA_KEY_LIBRARY_NAME, "audiotestsrc/libspa-audiotestsrc",
                         SPA_KEY_FACTORY_NAME, "audiotestsrc",
                         PW_KEY_NODE_NAME, "test_source",
-			"Spa:Pod:Object:Param:Props:live", "false",
+			"node.param.Props", "{ live = false }",
                         NULL);
 	data.source = pw_core_create_object(data.core,
 			"spa-node-factory",
@@ -94,13 +94,15 @@ int main(int argc, char *argv[])
 			PW_VERSION_NODE,
 			&props->dict, 0);
 
+	pw_loop_enter(loop);
 	while (true) {
 		if (pw_proxy_get_bound_id(data.source) != SPA_ID_INVALID &&
 		    pw_proxy_get_bound_id(data.sink) != SPA_ID_INVALID)
 			break;
 
-		pw_loop_iterate(pw_main_loop_get_loop(data.loop), -1);
+		pw_loop_iterate(loop, -1);
         }
+	pw_loop_leave(loop);
 
 	pw_properties_clear(props);
 	pw_properties_setf(props,
diff --git a/src/examples/local-v4l2.c b/src/examples/local-v4l2.c
index b8ce37fa..2093a9b4 100644
--- a/src/examples/local-v4l2.c
+++ b/src/examples/local-v4l2.c
@@ -33,7 +33,8 @@ struct data {
 	SDL_Window *window;
 	SDL_Texture *texture;
 
-	struct pw_main_loop *loop;
+	struct pw_main_loop *main_loop;
+	struct pw_loop *loop;
 
 	struct pw_context *context;
 	struct pw_core *core;
@@ -61,7 +62,7 @@ static void handle_events(struct data *data)
 	while (SDL_PollEvent(&event)) {
 		switch (event.type) {
 		case SDL_QUIT:
-			pw_main_loop_quit(data->loop);
+			pw_main_loop_quit(data->main_loop);
 			break;
 		}
 	}
@@ -309,7 +310,7 @@ static int impl_node_process(void *object)
 	struct data *d = object;
 	int res;
 
-	if ((res = pw_loop_invoke(pw_main_loop_get_loop(d->loop), do_render,
+	if ((res = pw_loop_invoke(d->loop, do_render,
 				  SPA_ID_INVALID, NULL, 0, true, d)) < 0)
 		return res;
 
@@ -372,14 +373,16 @@ static int make_nodes(struct data *data)
 			&props->dict, 0);
 
 
+	pw_loop_enter(data->loop);
 	while (true) {
 
 		if (pw_proxy_get_bound_id(data->out) != SPA_ID_INVALID &&
 		    pw_proxy_get_bound_id(data->in) != SPA_ID_INVALID)
 			break;
 
-		pw_loop_iterate(pw_main_loop_get_loop(data->loop), -1);
+		pw_loop_iterate(data->loop, -1);
 	}
+	pw_loop_leave(data->loop);
 
 	pw_properties_clear(props);
 
@@ -405,9 +408,10 @@ int main(int argc, char *argv[])
 
 	pw_init(&argc, &argv);
 
-	data.loop = pw_main_loop_new(NULL);
+	data.main_loop = pw_main_loop_new(NULL);
+	data.loop = pw_main_loop_get_loop(data.main_loop);
 	data.context = pw_context_new(
-			pw_main_loop_get_loop(data.loop),
+			data.loop,
 			pw_properties_new(
 				PW_KEY_CORE_DAEMON, "false",
 				NULL), 0);
@@ -436,13 +440,13 @@ int main(int argc, char *argv[])
 
 	make_nodes(&data);
 
-	pw_main_loop_run(data.loop);
+	pw_main_loop_run(data.main_loop);
 
 	pw_proxy_destroy(data.link);
 	pw_proxy_destroy(data.in);
 	pw_proxy_destroy(data.out);
 	pw_context_destroy(data.context);
-	pw_main_loop_destroy(data.loop);
+	pw_main_loop_destroy(data.main_loop);
 	pw_deinit();
 
 	return 0;
diff --git a/src/examples/meson.build b/src/examples/meson.build
index 889116c8..7d45a34f 100644
--- a/src/examples/meson.build
+++ b/src/examples/meson.build
@@ -1,6 +1,8 @@
 # Examples, in order from simple to complicated
 examples = [
   'audio-src',
+  'audio-src-ring',
+  'audio-src-ring2',
   'audio-dsp-src',
   'audio-dsp-filter',
   'audio-capture',
@@ -22,6 +24,7 @@ examples = [
   'export-spa-device',
   'bluez-session',
   'local-v4l2',
+  'gmain',
 ]
 
 if not get_option('examples').allowed()
@@ -37,6 +40,7 @@ examples_extra_deps = {
   'video-dsp-play': [sdl_dep],
   'local-v4l2': [sdl_dep],
   'export-sink': [sdl_dep],
+  'gmain': [glib2_dep],
 }
 
 foreach c : examples
diff --git a/src/examples/midi-src.c b/src/examples/midi-src.c
index ee5f3261..edcaa0f0 100644
--- a/src/examples/midi-src.c
+++ b/src/examples/midi-src.c
@@ -103,7 +103,7 @@ static void on_process(void *userdata, struct spa_io_position *position)
 	while (sample_offset < position->clock.duration) {
 		if (cycle % 2 == 0) {
 			/* MIDI note on, channel 0, middle C, max velocity */
-			uint8_t buf[] = { 0x90, 0x3c, 0x7f };
+			uint32_t event = 0x20903c7f;
 
 			/* The time position of the message in the graph cycle
 			 * is given as offset from the cycle start, in
@@ -111,18 +111,18 @@ static void on_process(void *userdata, struct spa_io_position *position)
 			 * samples, and the sample offset should satisfy
 			 * 0 <= sample_offset < position->clock.duration.
 			 */
-			spa_pod_builder_control(&builder, sample_offset, SPA_CONTROL_Midi);
+			spa_pod_builder_control(&builder, sample_offset, SPA_CONTROL_UMP);
 
 			/* Raw MIDI data for the message */
-			spa_pod_builder_bytes(&builder, buf, sizeof(buf));
+			spa_pod_builder_bytes(&builder, &event, sizeof(event));
 
 			pw_log_info("note on at %"PRIu64, sample_position + sample_offset);
 		} else {
 			/* MIDI note off, channel 0, middle C, max velocity */
-			uint8_t buf[] = { 0x80, 0x3c, 0x7f };
+			uint32_t event = 0x20803c7f;
 
-			spa_pod_builder_control(&builder, sample_offset, SPA_CONTROL_Midi);
-			spa_pod_builder_bytes(&builder, buf, sizeof(buf));
+			spa_pod_builder_control(&builder, sample_offset, SPA_CONTROL_UMP);
+			spa_pod_builder_bytes(&builder, &event, sizeof(event));
 
 			pw_log_info("note off at %"PRIu64, sample_position + sample_offset);
 		}
@@ -213,7 +213,7 @@ int main(int argc, char *argv[])
 			PW_FILTER_PORT_FLAG_MAP_BUFFERS,
 			sizeof(struct port),
 			pw_properties_new(
-				PW_KEY_FORMAT_DSP, "8 bit raw midi",
+				PW_KEY_FORMAT_DSP, "32 bit raw UMP",
 				PW_KEY_PORT_NAME, "output",
 				NULL),
 			NULL, 0);
diff --git a/src/examples/video-src.c b/src/examples/video-src.c
index 770ea596..f439f168 100644
--- a/src/examples/video-src.c
+++ b/src/examples/video-src.c
@@ -326,11 +326,11 @@ int main(int argc, char *argv[])
 
 	{
 		struct spa_pod_frame f;
-		struct spa_dict_item items[1];
 		/* send a tag, output tags travel downstream */
 		spa_tag_build_start(&b, &f, SPA_PARAM_Tag, SPA_DIRECTION_OUTPUT);
-		items[0] = SPA_DICT_ITEM_INIT("my-tag-key", "my-special-tag-value");
-		spa_tag_build_add_dict(&b, &SPA_DICT_INIT(items, 1));
+		spa_tag_build_add_dict(&b,
+				&SPA_DICT_ITEMS(
+					SPA_DICT_ITEM("my-tag-key", "my-special-tag-value")));
 		params[1] = spa_tag_build_end(&b, &f);
 	}
 
diff --git a/src/gst/gstpipewireclock.c b/src/gst/gstpipewireclock.c
index 0502a0a6..701bb6ff 100644
--- a/src/gst/gstpipewireclock.c
+++ b/src/gst/gstpipewireclock.c
@@ -38,7 +38,7 @@ gst_pipewire_clock_get_internal_time (GstClock * clock)
     return pclock->last_time;
 
   now = pw_stream_get_nsec(s->pwstream);
-#if 0
+#if 1
   struct pw_time t;
   if (s->pwstream == NULL ||
       pw_stream_get_time_n (s->pwstream, &t, sizeof(t)) < 0 ||
diff --git a/src/gst/gstpipewirecore.c b/src/gst/gstpipewirecore.c
index 6c4fb4b8..dcb45ef0 100644
--- a/src/gst/gstpipewirecore.c
+++ b/src/gst/gstpipewirecore.c
@@ -3,6 +3,7 @@
 /* SPDX-License-Identifier: MIT */
 
 #include "config.h"
+#include <errno.h>
 #include <unistd.h>
 #include <fcntl.h>
 
@@ -105,7 +106,7 @@ mainloop_failed:
   }
 connection_failed:
   {
-    GST_ERROR ("error connect: %m");
+    GST_ERROR ("error connect: %s", strerror (errno));
     pw_thread_loop_unlock (core->loop);
     pw_context_destroy (core->context);
     pw_thread_loop_destroy (core->loop);
diff --git a/src/gst/gstpipewiredeviceprovider.c b/src/gst/gstpipewiredeviceprovider.c
index 363ca3e2..c9d0d7ad 100644
--- a/src/gst/gstpipewiredeviceprovider.c
+++ b/src/gst/gstpipewiredeviceprovider.c
@@ -6,6 +6,7 @@
 
 #include <string.h>
 
+#include <spa/utils/json.h>
 #include <spa/utils/result.h>
 #include <spa/utils/string.h>
 
@@ -203,6 +204,28 @@ static struct node_data *find_node_data(struct spa_list *nodes, uint32_t id)
   return NULL;
 }
 
+static GstPipeWireDevice *
+gst_pipewire_device_new (int fd, uint32_t id, uint64_t serial,
+    GstPipeWireDeviceType type, const gchar * element, int priority,
+    const gchar * klass, const gchar * display_name, const GstCaps * caps,
+    const GstStructure * props)
+{
+  GstPipeWireDevice *gstdev;
+
+  gstdev =
+      g_object_new (GST_TYPE_PIPEWIRE_DEVICE, "display-name", display_name,
+      "caps", caps, "device-class", klass, "id", id, "serial", serial, "fd", fd,
+      "properties", props, NULL);
+
+  gstdev->id = id;
+  gstdev->serial = serial;
+  gstdev->type = type;
+  gstdev->element = element;
+  gstdev->priority = priority;
+
+  return gstdev;
+}
+
 static GstDevice *
 new_node (GstPipeWireDeviceProvider *self, struct node_data *data)
 {
@@ -224,16 +247,27 @@ new_node (GstPipeWireDeviceProvider *self, struct node_data *data)
     return NULL;
   }
 
-  props = gst_structure_new_empty ("pipewire-proplist");
+  props = gst_structure_new ("pipewire-proplist", "is-default", G_TYPE_BOOLEAN, FALSE, NULL);
   if (info->props) {
     const struct spa_dict_item *item;
     const char *str;
 
-    spa_dict_for_each (item, info->props)
-      gst_structure_set (props, item->key, G_TYPE_STRING, item->value, NULL);
+    klass = spa_dict_lookup(info->props, PW_KEY_MEDIA_CLASS);
+    name = spa_dict_lookup(info->props, PW_KEY_NODE_DESCRIPTION);
 
-    klass = spa_dict_lookup (info->props, PW_KEY_MEDIA_CLASS);
-    name = spa_dict_lookup (info->props, PW_KEY_NODE_DESCRIPTION);
+    spa_dict_for_each (item, info->props) {
+      gst_structure_set (props, item->key, G_TYPE_STRING, item->value, NULL);
+      if (spa_streq(item->key, "node.name") && klass) {
+        if (spa_streq(klass, "Audio/Source") && spa_streq(item->value, self->default_audio_source_name))
+          gst_structure_set(props, "is-default", G_TYPE_BOOLEAN, TRUE, NULL);
+        else if (spa_streq(klass, "Audio/Sink") &&
+            spa_streq(item->value, self->default_audio_sink_name))
+          gst_structure_set(props, "is-default", G_TYPE_BOOLEAN, TRUE, NULL);
+        else if (spa_streq(klass, "Video/Source") &&
+            spa_streq(item->value, self->default_video_source_name))
+          gst_structure_set(props, "is-default", G_TYPE_BOOLEAN, TRUE, NULL);
+      }
+    }
 
     if ((str = spa_dict_lookup(info->props, PW_KEY_PRIORITY_SESSION)))
       priority = atoi(str);
@@ -243,16 +277,8 @@ new_node (GstPipeWireDeviceProvider *self, struct node_data *data)
   if (name == NULL)
     name = "unknown";
 
-  gstdev = g_object_new (GST_TYPE_PIPEWIRE_DEVICE,
-      "display-name", name, "caps", data->caps, "device-class", klass,
-      "id", data->id, "serial", data->serial, "fd", self->fd,
-      "properties", props, NULL);
-
-  gstdev->id = data->id;
-  gstdev->serial = data->serial;
-  gstdev->type = type;
-  gstdev->element = element;
-  gstdev->priority = priority;
+  gstdev = gst_pipewire_device_new (self->fd, data->id, data->serial, type,
+      element, priority, klass, name, data->caps, props);
   if (props)
     gst_structure_free (props);
 
@@ -498,6 +524,133 @@ static const struct pw_proxy_events proxy_port_events = {
   .destroy = destroy_port,
 };
 
+static gboolean
+is_default_device_name (GstPipeWireDeviceProvider * self,
+    const gchar * name, const gchar * klass, GstPipeWireDeviceType type)
+{
+  gboolean ret = FALSE;
+
+  GST_OBJECT_LOCK (self);
+  switch (type) {
+    case GST_PIPEWIRE_DEVICE_TYPE_SINK:
+      if (g_str_has_prefix (klass, "Audio"))
+        ret = !g_strcmp0 (name, self->default_audio_sink_name);
+      break;
+    case GST_PIPEWIRE_DEVICE_TYPE_SOURCE:
+      if (g_str_has_prefix (klass, "Audio"))
+        ret = !g_strcmp0 (name, self->default_audio_source_name);
+      else if (g_str_has_prefix (klass, "Video"))
+        ret = !g_strcmp0 (name, self->default_video_source_name);
+      break;
+    default:
+      GST_ERROR_OBJECT (self, "Unknown pipewire device type!");
+      break;
+  }
+  GST_OBJECT_UNLOCK (self);
+
+  return ret;
+}
+
+static void
+sync_default_devices (GstPipeWireDeviceProvider * self)
+{
+  GList *tmp, *devices = NULL;
+
+  for (tmp = GST_DEVICE_PROVIDER_CAST (self)->devices; tmp; tmp = tmp->next)
+    devices = g_list_prepend (devices, gst_object_ref (tmp->data));
+
+  for (tmp = devices; tmp; tmp = tmp->next) {
+    GstPipeWireDevice *dev = tmp->data;
+    GstStructure *props = gst_device_get_properties (GST_DEVICE_CAST (dev));
+    gboolean was_default = FALSE, is_default = FALSE;
+    const gchar *name;
+    gchar *klass = gst_device_get_device_class (GST_DEVICE_CAST (dev));
+
+    g_assert (props);
+    gst_structure_get_boolean (props, "is-default", &was_default);
+    name = gst_structure_get_string (props, "node.name");
+
+    switch (dev->type) {
+      case GST_PIPEWIRE_DEVICE_TYPE_SINK:
+        is_default =
+            is_default_device_name (self, name, klass, dev->type);
+        break;
+      case GST_PIPEWIRE_DEVICE_TYPE_SOURCE:
+        is_default =
+            is_default_device_name (self, name, klass, dev->type);
+        break;
+      case GST_PIPEWIRE_DEVICE_TYPE_UNKNOWN:
+        break;
+    }
+
+    if (was_default != is_default) {
+      GstPipeWireDevice *updated_device;
+      gchar *display_name = gst_device_get_display_name (GST_DEVICE_CAST (dev));
+      GstCaps *caps = gst_device_get_caps (GST_DEVICE_CAST (dev));
+
+      gst_structure_set (props, "is-default", G_TYPE_BOOLEAN, is_default, NULL);
+      updated_device =
+          gst_pipewire_device_new (self->fd, dev->id, dev->serial, dev->type,
+          dev->element, dev->priority, klass, display_name, caps, props);
+
+      gst_device_provider_device_changed (GST_DEVICE_PROVIDER_CAST (self),
+          GST_DEVICE_CAST (updated_device), GST_DEVICE_CAST (dev));
+
+      g_free (display_name);
+      gst_caps_unref (caps);
+    }
+    gst_structure_free (props);
+    g_free (klass);
+  }
+  g_list_free_full (devices, gst_object_unref);
+}
+
+static int metadata_property(void *data, uint32_t id, const char *key,
+                             const char *type, const char *value) {
+  GstPipeWireDeviceProvider *self = data;
+  char name[1024];
+
+  if (value == NULL)
+    return 0;
+
+  if (spa_streq(key, "default.audio.source")) {
+    if (!spa_streq(type, "Spa:String:JSON"))
+      return 0;
+
+    g_free(self->default_audio_source_name);
+    if (spa_json_str_object_find(value, strlen(value), "name", name, sizeof(name)) >= 0)
+      self->default_audio_source_name = g_strdup(name);
+    goto sync_devices;
+  }
+  if (spa_streq(key, "default.audio.sink")) {
+    if (!spa_streq(type, "Spa:String:JSON"))
+      return 0;
+
+    g_free(self->default_audio_sink_name);
+    if (spa_json_str_object_find(value, strlen(value), "name", name, sizeof(name)) >= 0)
+      self->default_audio_sink_name = g_strdup(name);
+    goto sync_devices;
+  }
+  if (spa_streq(key, "default.video.source")) {
+    if (!spa_streq(type, "Spa:String:JSON"))
+      return 0;
+
+    g_free(self->default_video_source_name);
+    if (spa_json_str_object_find(value, strlen(value), "name", name, sizeof(name)) >= 0)
+      self->default_video_source_name = g_strdup(name);
+    goto sync_devices;
+  }
+
+  return 0;
+
+ sync_devices:
+  sync_default_devices (self);
+  return 0;
+}
+
+static const struct pw_metadata_events metadata_events = {
+    PW_VERSION_METADATA_EVENTS, .property = metadata_property};
+
 static void registry_event_global(void *data, uint32_t id, uint32_t permissions,
                                 const char *type, uint32_t version,
                                 const struct spa_dict *props)
@@ -564,6 +717,20 @@ static void registry_event_global(void *data, uint32_t id, uint32_t permissions,
     pw_port_add_listener(port, &pd->port_listener, &port_events, pd);
     pw_proxy_add_listener((struct pw_proxy*)port, &pd->proxy_listener, &proxy_port_events, pd);
     resync(self);
+  } else if (spa_streq(type, PW_TYPE_INTERFACE_Metadata) && props) {
+    const char *name;
+
+    name = spa_dict_lookup(props, PW_KEY_METADATA_NAME);
+    if (name == NULL)
+      return;
+
+    if (!spa_streq(name, "default"))
+      return;
+
+    self->metadata =
+        pw_registry_bind(self->registry, id, type, PW_VERSION_METADATA, 0);
+    pw_metadata_add_listener(self->metadata, &self->metadata_listener,
+                             &metadata_events, self);
   }
 
   return;
@@ -689,6 +856,12 @@ gst_pipewire_device_provider_stop (GstDeviceProvider * provider)
   }
   GST_DEBUG_OBJECT (self, "stopping provider");
 
+  if (self->metadata) {
+    spa_hook_remove(&self->metadata_listener);
+    pw_proxy_destroy((struct pw_proxy *)self->metadata);
+    self->metadata = NULL;
+  }
+
   g_clear_pointer ((struct pw_proxy**)&self->registry, pw_proxy_destroy);
   if (self->core != NULL) {
     pw_thread_loop_unlock (self->core->loop);
@@ -751,6 +924,9 @@ gst_pipewire_device_provider_finalize (GObject * object)
   GstPipeWireDeviceProvider *self = GST_PIPEWIRE_DEVICE_PROVIDER (object);
 
   g_free (self->client_name);
+  g_free (self->default_audio_source_name);
+  g_free (self->default_audio_sink_name);
+  g_free (self->default_video_source_name);
 
   G_OBJECT_CLASS (gst_pipewire_device_provider_parent_class)->finalize (object);
 }
diff --git a/src/gst/gstpipewiredeviceprovider.h b/src/gst/gstpipewiredeviceprovider.h
index f909cc49..82b2a8a1 100644
--- a/src/gst/gstpipewiredeviceprovider.h
+++ b/src/gst/gstpipewiredeviceprovider.h
@@ -9,8 +9,9 @@
 
 #include <gst/gst.h>
 
-#include <pipewire/pipewire.h>
 #include <gst/gstpipewirecore.h>
+#include <pipewire/extensions/metadata.h>
+#include <pipewire/pipewire.h>
 
 G_BEGIN_DECLS
 
@@ -49,6 +50,14 @@ struct _GstPipeWireDeviceProvider {
   struct spa_hook core_listener;
   struct pw_registry *registry;
   struct spa_hook registry_listener;
+
+  struct pw_metadata *metadata;
+  struct spa_hook metadata_listener;
+
+  gchar *default_audio_source_name;
+  gchar *default_audio_sink_name;
+  gchar *default_video_source_name;
+
   struct spa_list nodes;
   int seq;
 
diff --git a/src/gst/gstpipewireformat.c b/src/gst/gstpipewireformat.c
index 1116e8c6..24115325 100644
--- a/src/gst/gstpipewireformat.c
+++ b/src/gst/gstpipewireformat.c
@@ -131,6 +131,13 @@ static const uint32_t video_format_map[] = {
   SPA_VIDEO_FORMAT_Y444_12LE,
 };
 
+static const uint32_t interlace_mode_map[] = {
+  SPA_VIDEO_INTERLACE_MODE_PROGRESSIVE,
+  SPA_VIDEO_INTERLACE_MODE_INTERLEAVED,
+  SPA_VIDEO_INTERLACE_MODE_MIXED,
+  SPA_VIDEO_INTERLACE_MODE_FIELDS,
+};
+
 #if __BYTE_ORDER == __BIG_ENDIAN
 #define _FORMAT_LE(fmt)  SPA_AUDIO_FORMAT_ ## fmt ## _OE
 #define _FORMAT_BE(fmt)  SPA_AUDIO_FORMAT_ ## fmt
@@ -825,6 +832,14 @@ static char *video_id_to_dma_drm_fourcc(uint32_t id, uint64_t mod)
 }
 #endif
 
+static const char *interlace_mode_id_to_string(uint32_t id)
+{
+  int idx;
+  if ((idx = find_index(interlace_mode_map, SPA_N_ELEMENTS(interlace_mode_map), id)) == -1)
+    return NULL;
+  return gst_video_interlace_mode_to_string(idx);
+}
+
 static const char *audio_id_to_string(uint32_t id)
 {
   int idx;
@@ -1149,6 +1164,11 @@ gst_caps_from_format (const struct spa_pod *format)
           handle_id_prop (prop, "format", video_id_to_string, res);
         }
       }
+      if ((prop = spa_pod_object_find_prop (obj, prop, SPA_FORMAT_VIDEO_interlaceMode))) {
+        handle_id_prop (prop, "interlace-mode", interlace_mode_id_to_string, res);
+      } else {
+        gst_caps_set_simple(res, "interlace-mode", G_TYPE_STRING, "progressive", NULL);
+      }
     }
     else if (media_subtype == SPA_MEDIA_SUBTYPE_mjpg) {
       res = gst_caps_new_empty_simple ("image/jpeg");
diff --git a/src/gst/gstpipewirepool.c b/src/gst/gstpipewirepool.c
index 64982306..78869e16 100644
--- a/src/gst/gstpipewirepool.c
+++ b/src/gst/gstpipewirepool.c
@@ -16,6 +16,8 @@
 #include "gstpipewirepool.h"
 
 #include <spa/debug/types.h>
+#include <spa/utils/result.h>
+
 
 GST_DEBUG_CATEGORY_STATIC (gst_pipewire_pool_debug_category);
 #define GST_CAT_DEFAULT gst_pipewire_pool_debug_category
@@ -161,21 +163,31 @@ acquire_buffer (GstBufferPool * pool, GstBuffer ** buffer,
     if (G_UNLIKELY (GST_BUFFER_POOL_IS_FLUSHING (pool)))
       goto flushing;
 
-    if ((b = pw_stream_dequeue_buffer(s->pwstream)))
+    if ((b = pw_stream_dequeue_buffer(s->pwstream))) {
+      GST_LOG_OBJECT (pool, "dequeued buffer %p", b);
       break;
+    }
 
-    if (params && (params->flags & GST_BUFFER_POOL_ACQUIRE_FLAG_DONTWAIT))
-      goto no_more_buffers;
+    if (params) {
+      if (params->flags & GST_BUFFER_POOL_ACQUIRE_FLAG_DONTWAIT)
+        goto no_more_buffers;
+
+      if ((params->flags & GST_BUFFER_POOL_ACQUIRE_FLAG_LAST) &&
+	      p->paused)
+        goto paused;
+    }
 
-    GST_WARNING ("queue empty");
+    GST_WARNING_OBJECT (pool, "failed to dequeue buffer: %s", strerror(errno));
     g_cond_wait (&p->cond, GST_OBJECT_GET_LOCK (pool));
   }
 
   data = b->user_data;
+  data->queued = FALSE;
+
   *buffer = data->buf;
 
   GST_OBJECT_UNLOCK (pool);
-  GST_LOG_OBJECT (pool, "acquire buffer %p", *buffer);
+  GST_LOG_OBJECT (pool, "acquired gstbuffer %p", *buffer);
 
   return GST_FLOW_OK;
 
@@ -184,6 +196,11 @@ flushing:
     GST_OBJECT_UNLOCK (pool);
     return GST_FLOW_FLUSHING;
   }
+paused:
+  {
+    GST_OBJECT_UNLOCK (pool);
+    return GST_FLOW_CUSTOM_ERROR_1;
+  }
 no_more_buffers:
   {
     GST_LOG_OBJECT (pool, "no more buffers");
@@ -238,12 +255,22 @@ set_config (GstBufferPool * pool, GstStructure * config)
   return GST_BUFFER_POOL_CLASS (gst_pipewire_pool_parent_class)->set_config (pool, config);
 }
 
+
+void gst_pipewire_pool_set_paused (GstPipeWirePool *pool, gboolean paused)
+{
+  GST_DEBUG_OBJECT (pool, "pause: %u", paused);
+  GST_OBJECT_LOCK (pool);
+  pool->paused = paused;
+  g_cond_signal (&pool->cond);
+  GST_OBJECT_UNLOCK (pool);
+}
+
 static void
 flush_start (GstBufferPool * pool)
 {
   GstPipeWirePool *p = GST_PIPEWIRE_POOL (pool);
 
-  GST_DEBUG ("flush start");
+  GST_DEBUG_OBJECT (pool, "flush start");
   GST_OBJECT_LOCK (pool);
   g_cond_signal (&p->cond);
   GST_OBJECT_UNLOCK (pool);
@@ -253,6 +280,29 @@ static void
 release_buffer (GstBufferPool * pool, GstBuffer *buffer)
 {
   GST_LOG_OBJECT (pool, "release buffer %p", buffer);
+
+  GstPipeWirePoolData *data = gst_pipewire_pool_get_data(buffer);
+
+  GST_OBJECT_LOCK (pool);
+
+  if (!data->queued && data->b != NULL)
+  {
+    GstPipeWirePool *p = GST_PIPEWIRE_POOL (pool);
+    g_autoptr (GstPipeWireStream) s = g_weak_ref_get (&p->stream);
+    int res;
+
+    pw_thread_loop_lock (s->core->loop);
+
+    if ((res = pw_stream_return_buffer (s->pwstream, data->b)) < 0) {
+      GST_ERROR_OBJECT (pool,"can't return buffer %p; gstbuffer : %p, %s",data->b, buffer, spa_strerror(res));
+    } else {
+      data->queued = TRUE;
+      GST_DEBUG_OBJECT (pool, "returned buffer %p; gstbuffer:%p", data->b, buffer);
+    }
+
+    pw_thread_loop_unlock (s->core->loop);
+  }
+  GST_OBJECT_UNLOCK (pool);
 }
 
 static gboolean
diff --git a/src/gst/gstpipewirepool.h b/src/gst/gstpipewirepool.h
index b629f8a8..fb00a100 100644
--- a/src/gst/gstpipewirepool.h
+++ b/src/gst/gstpipewirepool.h
@@ -44,6 +44,13 @@ struct _GstPipeWirePool {
   GstAllocator *dmabuf_allocator;
 
   GCond cond;
+  gboolean paused;
+};
+
+enum GstPipeWirePoolMode {
+    USE_BUFFERPOOL_NO = 0,
+    USE_BUFFERPOOL_AUTO,
+    USE_BUFFERPOOL_YES
 };
 
 GstPipeWirePool *  gst_pipewire_pool_new (GstPipeWireStream *stream);
@@ -59,6 +66,8 @@ gst_pipewire_pool_has_buffers (GstPipeWirePool *pool)
 
 GstPipeWirePoolData *gst_pipewire_pool_get_data (GstBuffer *buffer);
 
+void gst_pipewire_pool_set_paused (GstPipeWirePool *pool, gboolean paused);
+
 G_END_DECLS
 
 #endif /* __GST_PIPEWIRE_POOL_H__ */
diff --git a/src/gst/gstpipewiresink.c b/src/gst/gstpipewiresink.c
index b39a335d..aa9d94b9 100644
--- a/src/gst/gstpipewiresink.c
+++ b/src/gst/gstpipewiresink.c
@@ -26,6 +26,7 @@
 
 #include <spa/pod/builder.h>
 #include <spa/utils/result.h>
+#include <spa/utils/dll.h>
 
 #include <gst/video/video.h>
 
@@ -36,6 +37,8 @@ GST_DEBUG_CATEGORY_STATIC (pipewire_sink_debug);
 #define GST_CAT_DEFAULT pipewire_sink_debug
 
 #define DEFAULT_PROP_MODE GST_PIPEWIRE_SINK_MODE_DEFAULT
+#define DEFAULT_PROP_SLAVE_METHOD GST_PIPEWIRE_SINK_SLAVE_METHOD_NONE
+#define DEFAULT_PROP_USE_BUFFERPOOL USE_BUFFERPOOL_AUTO
 
 #define MIN_BUFFERS     8u
 
@@ -48,7 +51,9 @@ enum
   PROP_CLIENT_PROPERTIES,
   PROP_STREAM_PROPERTIES,
   PROP_MODE,
-  PROP_FD
+  PROP_FD,
+  PROP_SLAVE_METHOD,
+  PROP_USE_BUFFERPOOL,
 };
 
 GType
@@ -71,6 +76,26 @@ gst_pipewire_sink_mode_get_type (void)
   return (GType) mode_type;
 }
 
+GType
+gst_pipewire_sink_slave_method_get_type (void)
+{
+  static gsize method_type = 0;
+  static const GEnumValue method[] = {
+    {GST_PIPEWIRE_SINK_SLAVE_METHOD_NONE, "GST_PIPEWIRE_SINK_SLAVE_METHOD_NONE", "none"},
+    {GST_PIPEWIRE_SINK_SLAVE_METHOD_RESAMPLE, "GST_PIPEWIRE_SINK_SLAVE_METHOD_RESAMPLE", "resample"},
+    {0, NULL, NULL},
+  };
+
+  if (g_once_init_enter (&method_type)) {
+    GType tmp =
+        g_enum_register_static ("GstPipeWireSinkSlaveMethod", method);
+    g_once_init_leave (&method_type, tmp);
+  }
+
+  return (GType) method_type;
+}
+
+
 
 static GstStaticPadTemplate gst_pipewire_sink_template =
 GST_STATIC_PAD_TEMPLATE ("sink",
@@ -97,6 +122,8 @@ static GstCaps *gst_pipewire_sink_sink_fixate (GstBaseSink * bsink,
 static GstFlowReturn gst_pipewire_sink_render (GstBaseSink * psink,
     GstBuffer * buffer);
 
+static  gboolean gst_pipewire_sink_event (GstBaseSink *sink, GstEvent *event);
+
 static GstClock *
 gst_pipewire_sink_provide_clock (GstElement * elem)
 {
@@ -139,7 +166,9 @@ gst_pipewire_sink_propose_allocation (GstBaseSink * bsink, GstQuery * query)
 {
   GstPipeWireSink *pwsink = GST_PIPEWIRE_SINK (bsink);
 
-  gst_query_add_allocation_pool (query, GST_BUFFER_POOL_CAST (pwsink->stream->pool), 0, 0, 0);
+  if (pwsink->use_bufferpool != USE_BUFFERPOOL_NO)
+    gst_query_add_allocation_pool (query, GST_BUFFER_POOL_CAST (pwsink->stream->pool), 0, 0, 0);
+
   gst_query_add_allocation_meta (query, GST_VIDEO_META_API_TYPE, NULL);
   return TRUE;
 }
@@ -224,6 +253,25 @@ gst_pipewire_sink_class_init (GstPipeWireSinkClass * klass)
                                                       G_PARAM_READWRITE |
                                                       G_PARAM_STATIC_STRINGS));
 
+  g_object_class_install_property (gobject_class,
+                                   PROP_SLAVE_METHOD,
+                                   g_param_spec_enum ("slave-method",
+                                                      "Slave Method",
+                                                      "Algorithm used to match the rate of the masterclock",
+                                                      GST_TYPE_PIPEWIRE_SINK_SLAVE_METHOD,
+                                                      DEFAULT_PROP_SLAVE_METHOD,
+                                                      G_PARAM_READWRITE |
+                                                      G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_property (gobject_class,
+                                   PROP_USE_BUFFERPOOL,
+                                   g_param_spec_boolean ("use-bufferpool",
+                                                      "Use bufferpool",
+                                                      "Use bufferpool (default: true for video, false for audio)",
+                                                      DEFAULT_PROP_USE_BUFFERPOOL,
+                                                      G_PARAM_READWRITE |
+                                                      G_PARAM_STATIC_STRINGS));
+
   gstelement_class->provide_clock = gst_pipewire_sink_provide_clock;
   gstelement_class->change_state = gst_pipewire_sink_change_state;
 
@@ -238,6 +286,7 @@ gst_pipewire_sink_class_init (GstPipeWireSinkClass * klass)
   gstbasesink_class->fixate = gst_pipewire_sink_sink_fixate;
   gstbasesink_class->propose_allocation = gst_pipewire_sink_propose_allocation;
   gstbasesink_class->render = gst_pipewire_sink_render;
+  gstbasesink_class->event = gst_pipewire_sink_event;
 
   GST_DEBUG_CATEGORY_INIT (pipewire_sink_debug, "pipewiresink", 0,
       "PipeWire Sink");
@@ -306,6 +355,8 @@ gst_pipewire_sink_init (GstPipeWireSink * sink)
   sink->stream =  gst_pipewire_stream_new (GST_ELEMENT (sink));
 
   sink->mode = DEFAULT_PROP_MODE;
+  sink->use_bufferpool = DEFAULT_PROP_USE_BUFFERPOOL;
+  sink->is_video = false;
 
   GST_OBJECT_FLAG_SET (sink, GST_ELEMENT_FLAG_PROVIDE_CLOCK);
 
@@ -316,12 +367,14 @@ static GstCaps *
 gst_pipewire_sink_sink_fixate (GstBaseSink * bsink, GstCaps * caps)
 {
   GstStructure *structure;
+  GstPipeWireSink *pwsink = GST_PIPEWIRE_SINK(bsink);
 
   caps = gst_caps_make_writable (caps);
 
   structure = gst_caps_get_structure (caps, 0);
 
   if (gst_structure_has_name (structure, "video/x-raw")) {
+    pwsink->is_video = true;
     gst_structure_fixate_field_nearest_int (structure, "width", 320);
     gst_structure_fixate_field_nearest_int (structure, "height", 240);
     gst_structure_fixate_field_nearest_fraction (structure, "framerate", 30, 1);
@@ -407,6 +460,17 @@ gst_pipewire_sink_set_property (GObject * object, guint prop_id,
       pwsink->stream->fd = g_value_get_int (value);
       break;
 
+    case PROP_SLAVE_METHOD:
+      pwsink->slave_method = g_value_get_enum (value);
+      break;
+
+    case PROP_USE_BUFFERPOOL:
+      if(g_value_get_boolean (value))
+        pwsink->use_bufferpool = USE_BUFFERPOOL_YES;
+      else
+        pwsink->use_bufferpool = USE_BUFFERPOOL_NO;
+      break;
+
     default:
       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
       break;
@@ -448,12 +512,70 @@ gst_pipewire_sink_get_property (GObject * object, guint prop_id,
       g_value_set_int (value, pwsink->stream->fd);
       break;
 
+    case PROP_SLAVE_METHOD:
+      g_value_set_enum (value, pwsink->slave_method);
+      break;
+
+    case PROP_USE_BUFFERPOOL:
+      g_value_set_boolean (value, !!pwsink->use_bufferpool);
+      break;
+
     default:
       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
       break;
   }
 }
 
+static void rate_match_resample(GstPipeWireSink *pwsink)
+{
+  GstPipeWireStream *stream = pwsink->stream;
+  double err, corr;
+  struct pw_time ts;
+  guint64 queued, now, elapsed, target;
+
+  if (!pwsink->rate_match)
+    return;
+
+  pw_stream_get_time_n(stream->pwstream, &ts, sizeof(ts));
+  now = pw_stream_get_nsec(stream->pwstream);
+  if (ts.now != 0)
+    elapsed = gst_util_uint64_scale_int (now - ts.now, ts.rate.denom, GST_SECOND * ts.rate.num);
+  else
+    elapsed = 0;
+
+  queued = ts.queued - ts.size;
+  target = elapsed;
+  err = ((gint64)queued - ((gint64)target));
+
+  corr = spa_dll_update(&stream->dll, SPA_CLAMPD(err, -128.0, 128.0));
+
+  stream->err_wdw = (double)ts.rate.denom/ts.size;
+
+  double avg = (stream->err_avg * stream->err_wdw + (err - stream->err_avg)) / (stream->err_wdw + 1.0);
+  stream->err_var = (stream->err_var * stream->err_wdw +
+                    (err - stream->err_avg) * (err - avg)) / (stream->err_wdw + 1.0);
+  stream->err_avg = avg;
+
+  if (stream->last_ts == 0 || stream->last_ts + SPA_NSEC_PER_SEC < now) {
+    double bw;
+
+    stream->last_ts = now;
+
+    if (stream->err_var == 0.0)
+      bw = 0.0;
+    else
+      bw = fabs(stream->err_avg) / sqrt(fabs(stream->err_var));
+
+    spa_dll_set_bw(&stream->dll, SPA_CLAMPD(bw, 0.001, SPA_DLL_BW_MAX), ts.size, ts.rate.denom);
+
+    GST_INFO_OBJECT (pwsink, "q:%"PRIi64"/%"PRIi64" e:%"PRIu64" err:%+03f corr:%f %f %f %f",
+                    ts.queued, ts.size, elapsed, err, corr,
+		    stream->err_avg, stream->err_var, stream->dll.bw);
+  }
+
+  pw_stream_set_rate (stream->pwstream, corr);
+}
+
 static void
 on_add_buffer (void *_data, struct pw_buffer *b)
 {
@@ -481,14 +603,13 @@ static void
 do_send_buffer (GstPipeWireSink *pwsink, GstBuffer *buffer)
 {
   GstPipeWirePoolData *data;
+  GstPipeWireStream *stream = pwsink->stream;
   gboolean res;
   guint i;
   struct spa_buffer *b;
 
   data = gst_pipewire_pool_get_data(buffer);
 
-  GST_LOG_OBJECT (pwsink, "queue buffer %p, pw_buffer %p", buffer, data->b);
-
   b = data->b->buffer;
 
   if (data->header) {
@@ -508,12 +629,15 @@ do_send_buffer (GstPipeWireSink *pwsink, GstBuffer *buffer)
       data->crop->region.size.height = meta->width;
     }
   }
+  data->b->size = 0;
   for (i = 0; i < b->n_datas; i++) {
     struct spa_data *d = &b->datas[i];
     GstMemory *mem = gst_buffer_peek_memory (buffer, i);
     d->chunk->offset = mem->offset;
     d->chunk->size = mem->size;
-    d->chunk->stride = pwsink->stream->pool->video_info.stride[i];
+    d->chunk->stride = stream->pool->video_info.stride[i];
+
+    data->b->size += mem->size / 4;
   }
 
   GstVideoMeta *meta = gst_buffer_get_video_meta (buffer);
@@ -532,8 +656,19 @@ do_send_buffer (GstPipeWireSink *pwsink, GstBuffer *buffer)
     }
   }
 
-  if ((res = pw_stream_queue_buffer (pwsink->stream->pwstream, data->b)) < 0) {
-    g_warning ("can't send buffer %s", spa_strerror(res));
+  if ((res = pw_stream_queue_buffer (stream->pwstream, data->b)) < 0) {
+    GST_WARNING_OBJECT (pwsink, "can't send buffer %s", spa_strerror(res));
+  } else {
+    data->queued = TRUE;
+    GST_LOG_OBJECT(pwsink, "queued pwbuffer: %p; gstbuffer %p ",data->b, buffer);
+  }
+
+  switch (pwsink->slave_method) {
+    case GST_PIPEWIRE_SINK_SLAVE_METHOD_NONE:
+      break;
+    case GST_PIPEWIRE_SINK_SLAVE_METHOD_RESAMPLE:
+      rate_match_resample(pwsink);
+      break;
   }
 }
 
@@ -602,7 +737,6 @@ gst_pipewire_sink_setcaps (GstBaseSink * bsink, GstCaps * caps)
   g_autoptr(GPtrArray) possible = NULL;
   enum pw_stream_state state;
   const char *error = NULL;
-  gboolean res = FALSE;
   GstStructure *config, *s;
   guint size;
   guint min_buffers;
@@ -613,9 +747,21 @@ gst_pipewire_sink_setcaps (GstBaseSink * bsink, GstCaps * caps)
   pwsink = GST_PIPEWIRE_SINK (bsink);
 
   s = gst_caps_get_structure (caps, 0);
-  rate = 0;
-  if (gst_structure_has_name (s, "audio/x-raw"))
+  if (gst_structure_has_name (s, "audio/x-raw")) {
     gst_structure_get_int (s, "rate", &rate);
+    pwsink->rate = rate;
+    pwsink->rate_match = true;
+
+    /* Don't provide bufferpool for audio if not requested by the application/user */
+    if (pwsink->use_bufferpool != USE_BUFFERPOOL_YES)
+      pwsink->use_bufferpool = USE_BUFFERPOOL_NO;
+  } else {
+    pwsink->rate = rate = 0;
+    pwsink->rate_match = false;
+    pwsink->is_video = true;
+  }
+
+  spa_dll_set_bw(&pwsink->stream->dll, SPA_DLL_BW_MIN, 4096, rate);
 
   possible = gst_caps_to_format_all (caps);
 
@@ -633,6 +779,7 @@ gst_pipewire_sink_setcaps (GstBaseSink * bsink, GstCaps * caps)
     char buf[64];
 
     flags = PW_STREAM_FLAG_ASYNC;
+    flags |= PW_STREAM_FLAG_EARLY_PROCESS;
     if (pwsink->mode != GST_PIPEWIRE_SINK_MODE_PROVIDE)
       flags |= PW_STREAM_FLAG_AUTOCONNECT;
     else
@@ -685,20 +832,21 @@ gst_pipewire_sink_setcaps (GstBaseSink * bsink, GstCaps * caps)
       }
     }
   }
-  res = TRUE;
 
   gst_pipewire_clock_reset (GST_PIPEWIRE_CLOCK (pwsink->stream->clock), 0);
 
   config = gst_buffer_pool_get_config (GST_BUFFER_POOL_CAST (pwsink->stream->pool));
   gst_buffer_pool_config_get_params (config, NULL, &size, &min_buffers, &max_buffers);
   gst_buffer_pool_config_set_params (config, caps, size, min_buffers, max_buffers);
+  if(pwsink->is_video)
+    gst_buffer_pool_config_add_option(config, GST_BUFFER_POOL_OPTION_VIDEO_META);
   gst_buffer_pool_set_config (GST_BUFFER_POOL_CAST (pwsink->stream->pool), config);
 
   pw_thread_loop_unlock (pwsink->stream->core->loop);
 
-  pwsink->negotiated = res;
+  pwsink->negotiated = TRUE;
 
-  return res;
+  return TRUE;
 
 start_error:
   {
@@ -714,7 +862,6 @@ gst_pipewire_sink_render (GstBaseSink * bsink, GstBuffer * buffer)
   GstPipeWireSink *pwsink;
   GstFlowReturn res = GST_FLOW_OK;
   const char *error = NULL;
-  gboolean unref_buffer = FALSE;
 
   pwsink = GST_PIPEWIRE_SINK (bsink);
 
@@ -747,34 +894,99 @@ gst_pipewire_sink_render (GstBaseSink * bsink, GstBuffer * buffer)
     goto done_unlock;
 
   if (buffer->pool != GST_BUFFER_POOL_CAST (pwsink->stream->pool)) {
-    GstBuffer *b = NULL;
-    GstMapInfo info = { 0, };
-    GstBufferPoolAcquireParams params = { 0, };
-
-    pw_thread_loop_unlock (pwsink->stream->core->loop);
+    gsize offset = 0;
+    gsize buf_size = gst_buffer_get_size (buffer);
+
+    GST_TRACE_OBJECT(pwsink, "Buffer is not from pipewirepool, copying into our pool");
+
+    /* For some streams, the buffer size is changed and may exceed the acquired
+     * buffer size which is acquired from the pool of pipewiresink. Need split
+     * the buffer and send them in turn for this case */
+    while (buf_size) {
+      GstBuffer *b = NULL;
+      GstMapInfo info = { 0, };
+      GstBufferPoolAcquireParams params = { 0, };
+
+      pw_thread_loop_unlock (pwsink->stream->core->loop);
+
+      params.flags = GST_BUFFER_POOL_ACQUIRE_FLAG_LAST;
+      res = gst_buffer_pool_acquire_buffer (GST_BUFFER_POOL_CAST (pwsink->stream->pool),
+          &b, &params);
+      if (res == GST_FLOW_CUSTOM_ERROR_1) {
+        res = gst_base_sink_wait_preroll (bsink);
+        if (res != GST_FLOW_OK)
+          goto done;
+        continue;
+      }
+      if (res != GST_FLOW_OK)
+        goto done;
+
+      if (pwsink->is_video) {
+        GstVideoFrame src, dst;
+        gboolean copied = FALSE;
+        buf_size = 0; // to break from the loop
+
+        /*
+          splitting of buffers in the case of video might break the frame layout
+          and that seems to be causing issues while retrieving the buffers on the receiver
+          side. Hence use the video_frame_map to copy the buffer of bigger size into the
+          pipewirepool's buffer
+        */
+
+        if (!gst_video_frame_map (&dst, &pwsink->stream->pool->video_info, b,
+          GST_MAP_WRITE)) {
+          GST_ERROR_OBJECT(pwsink, "Failed to map dest buffer");
+          return GST_FLOW_ERROR;
+        }
+
+        if (!gst_video_frame_map (&src, &pwsink->stream->pool->video_info, buffer, GST_MAP_READ)) {
+          gst_video_frame_unmap (&dst);
+          GST_ERROR_OBJECT(pwsink, "Failed to map src buffer");
+          return GST_FLOW_ERROR;
+        }
+
+        copied = gst_video_frame_copy (&dst, &src);
+
+        gst_video_frame_unmap (&src);
+        gst_video_frame_unmap (&dst);
+
+        if (!copied) {
+          GST_ERROR_OBJECT(pwsink, "Failed to copy the frame");
+          return GST_FLOW_ERROR;
+        }
+
+        gst_buffer_copy_into(b, buffer, GST_BUFFER_COPY_METADATA, 0, -1);
+      } else {
+        gst_buffer_map (b, &info, GST_MAP_WRITE);
+        gsize extract_size = (buf_size <= info.maxsize) ? buf_size: info.maxsize;
+        gst_buffer_extract (buffer, offset, info.data, info.maxsize);
+        gst_buffer_unmap (b, &info);
+        gst_buffer_resize (b, 0, extract_size);
+        gst_buffer_copy_into(b, buffer, GST_BUFFER_COPY_METADATA, 0, -1);
+        buf_size -= extract_size;
+        offset += extract_size;
+      }
 
-    if ((res = gst_buffer_pool_acquire_buffer (GST_BUFFER_POOL_CAST (pwsink->stream->pool), &b, &params)) != GST_FLOW_OK)
-      goto done;
+      pw_thread_loop_lock (pwsink->stream->core->loop);
+      if (pw_stream_get_state (pwsink->stream->pwstream, &error) != PW_STREAM_STATE_STREAMING) {
+        gst_buffer_unref (b);
+        goto done_unlock;
+      }
 
-    gst_buffer_map (b, &info, GST_MAP_WRITE);
-    gst_buffer_extract (buffer, 0, info.data, info.maxsize);
-    gst_buffer_unmap (b, &info);
-    gst_buffer_resize (b, 0, gst_buffer_get_size (buffer));
-    gst_buffer_copy_into(b, buffer, GST_BUFFER_COPY_METADATA, 0, -1);
-    buffer = b;
-    unref_buffer = TRUE;
+      do_send_buffer (pwsink, b);
+      gst_buffer_unref (b);
 
-    pw_thread_loop_lock (pwsink->stream->core->loop);
-    if (pw_stream_get_state (pwsink->stream->pwstream, &error) != PW_STREAM_STATE_STREAMING)
-      goto done_unlock;
-  }
+      if (pw_stream_is_driving (pwsink->stream->pwstream))
+        pw_stream_trigger_process (pwsink->stream->pwstream);
+    }
+  } else {
+    GST_TRACE_OBJECT(pwsink, "Buffer is from pipewirepool");
 
-  do_send_buffer (pwsink, buffer);
-  if (unref_buffer)
-    gst_buffer_unref (buffer);
+    do_send_buffer (pwsink, buffer);
 
-  if (pw_stream_is_driving (pwsink->stream->pwstream))
-    pw_stream_trigger_process (pwsink->stream->pwstream);
+    if (pw_stream_is_driving (pwsink->stream->pwstream))
+      pw_stream_trigger_process (pwsink->stream->pwstream);
+  }
 
 done_unlock:
   pw_thread_loop_unlock (pwsink->stream->core->loop);
@@ -817,20 +1029,14 @@ gst_pipewire_sink_change_state (GstElement * element, GstStateChange transition)
       pw_thread_loop_lock (this->stream->core->loop);
       pw_stream_set_active(this->stream->pwstream, false);
       pw_thread_loop_unlock (this->stream->core->loop);
-      break;
-    case GST_STATE_CHANGE_PAUSED_TO_PLAYING:
-      /* uncork and start play */
-      pw_thread_loop_lock (this->stream->core->loop);
-      pw_stream_set_active(this->stream->pwstream, true);
-      pw_thread_loop_unlock (this->stream->core->loop);
-      gst_buffer_pool_set_flushing(GST_BUFFER_POOL_CAST(this->stream->pool), FALSE);
+      gst_pipewire_pool_set_paused(this->stream->pool, TRUE);
       break;
     case GST_STATE_CHANGE_PLAYING_TO_PAUSED:
       /* stop play ASAP by corking */
+      gst_pipewire_pool_set_paused(this->stream->pool, TRUE);
       pw_thread_loop_lock (this->stream->core->loop);
       pw_stream_set_active(this->stream->pwstream, false);
       pw_thread_loop_unlock (this->stream->core->loop);
-      gst_buffer_pool_set_flushing(GST_BUFFER_POOL_CAST(this->stream->pool), TRUE);
       break;
     default:
       break;
@@ -839,6 +1045,16 @@ gst_pipewire_sink_change_state (GstElement * element, GstStateChange transition)
   ret = GST_ELEMENT_CLASS (parent_class)->change_state (element, transition);
 
   switch (transition) {
+    case GST_STATE_CHANGE_PAUSED_TO_PLAYING:
+      /* For some cases, the param_changed event is earlier than the state switch
+       * from paused state to playing state which will wait until buffer pool is ready.
+       * Guarantee to finish preoll if needed to active buffer pool before uncorking and
+       * starting play */
+      gst_pipewire_pool_set_paused(this->stream->pool, FALSE);
+      pw_thread_loop_lock (this->stream->core->loop);
+      pw_stream_set_active(this->stream->pwstream, true);
+      pw_thread_loop_unlock (this->stream->core->loop);
+      break;
     case GST_STATE_CHANGE_PLAYING_TO_PAUSED:
       break;
     case GST_STATE_CHANGE_PAUSED_TO_READY:
@@ -859,3 +1075,42 @@ open_failed:
     return GST_STATE_CHANGE_FAILURE;
   }
 }
+
+static  gboolean gst_pipewire_sink_event (GstBaseSink *sink, GstEvent *event) {
+  GstPipeWireSink *pw_sink = GST_PIPEWIRE_SINK(sink);
+  GstState current_state = GST_ELEMENT(sink)->current_state;
+
+  switch (GST_EVENT_TYPE (event)) {
+    case GST_EVENT_FLUSH_START:
+    {
+      GST_DEBUG_OBJECT (pw_sink, "flush-start");
+      pw_thread_loop_lock (pw_sink->stream->core->loop);
+
+      /* The stream would be already inactive if the sink is not PLAYING */
+      if (current_state == GST_STATE_PLAYING)
+        pw_stream_set_active(pw_sink->stream->pwstream, false);
+
+      gst_buffer_pool_set_flushing(GST_BUFFER_POOL_CAST(pw_sink->stream->pool), TRUE);
+      pw_stream_flush(pw_sink->stream->pwstream, false);
+      pw_thread_loop_unlock (pw_sink->stream->core->loop);
+      break;
+    }
+    case GST_EVENT_FLUSH_STOP:
+    {
+      GST_DEBUG_OBJECT (pw_sink, "flush-stop");
+      pw_thread_loop_lock (pw_sink->stream->core->loop);
+
+      /* The stream needs to remain inactive if the sink is not PLAYING */
+      if (current_state == GST_STATE_PLAYING)
+        pw_stream_set_active(pw_sink->stream->pwstream, true);
+
+      gst_buffer_pool_set_flushing(GST_BUFFER_POOL_CAST(pw_sink->stream->pool), FALSE);
+      pw_thread_loop_unlock (pw_sink->stream->core->loop);
+      break;
+    }
+    default:
+      break;
+  }
+
+  return GST_BASE_SINK_CLASS (parent_class)->event (sink, event);
+}
diff --git a/src/gst/gstpipewiresink.h b/src/gst/gstpipewiresink.h
index 74e6667e..60eb3b79 100644
--- a/src/gst/gstpipewiresink.h
+++ b/src/gst/gstpipewiresink.h
@@ -37,6 +37,22 @@ typedef enum
 
 #define GST_TYPE_PIPEWIRE_SINK_MODE (gst_pipewire_sink_mode_get_type ())
 
+
+/**
+ * GstPipeWireSinkSlaveMethod:
+ * @GST_PIPEWIRE_SINK_SLAVE_METHOD_NONE: no clock and timestamp slaving
+ * @GST_PIPEWIRE_SINK_SLAVE_METHOD_RESAMPLE: resample audio
+ *
+ * Different clock slaving methods
+ */
+typedef enum
+{
+  GST_PIPEWIRE_SINK_SLAVE_METHOD_NONE,
+  GST_PIPEWIRE_SINK_SLAVE_METHOD_RESAMPLE,
+} GstPipeWireSinkSlaveMethod;
+
+#define GST_TYPE_PIPEWIRE_SINK_SLAVE_METHOD (gst_pipewire_sink_slave_method_get_type ())
+
 /**
  * GstPipeWireSink:
  *
@@ -47,11 +63,16 @@ struct _GstPipeWireSink {
 
   /*< private >*/
   GstPipeWireStream *stream;
+  gboolean use_bufferpool;
 
   /* video state */
   gboolean negotiated;
+  gboolean rate_match;
+  gint rate;
+  gboolean is_video;
 
   GstPipeWireSinkMode mode;
+  GstPipeWireSinkSlaveMethod slave_method;
 };
 
 GType gst_pipewire_sink_mode_get_type (void);
diff --git a/src/gst/gstpipewiresrc.c b/src/gst/gstpipewiresrc.c
index 1bb31143..1cd04941 100644
--- a/src/gst/gstpipewiresrc.c
+++ b/src/gst/gstpipewiresrc.c
@@ -46,6 +46,7 @@ GST_DEBUG_CATEGORY_STATIC (pipewire_src_debug);
 #define DEFAULT_RESEND_LAST     false
 #define DEFAULT_KEEPALIVE_TIME  0
 #define DEFAULT_AUTOCONNECT     true
+#define DEFAULT_USE_BUFFERPOOL USE_BUFFERPOOL_AUTO
 
 enum
 {
@@ -62,6 +63,7 @@ enum
   PROP_RESEND_LAST,
   PROP_KEEPALIVE_TIME,
   PROP_AUTOCONNECT,
+  PROP_USE_BUFFERPOOL,
 };
 
 
@@ -130,7 +132,11 @@ gst_pipewire_src_set_property (GObject * object, guint prop_id,
       break;
 
     case PROP_ALWAYS_COPY:
-      pwsrc->always_copy = g_value_get_boolean (value);
+      /* don't provide buffer if always copy*/
+      if (g_value_get_boolean (value))
+        pwsrc->use_bufferpool = USE_BUFFERPOOL_NO;
+      else
+        pwsrc->use_bufferpool = USE_BUFFERPOOL_YES;
       break;
 
     case PROP_MIN_BUFFERS:
@@ -157,6 +163,13 @@ gst_pipewire_src_set_property (GObject * object, guint prop_id,
       pwsrc->autoconnect = g_value_get_boolean (value);
       break;
 
+    case PROP_USE_BUFFERPOOL:
+      if(g_value_get_boolean (value))
+        pwsrc->use_bufferpool = USE_BUFFERPOOL_YES;
+      else
+        pwsrc->use_bufferpool = USE_BUFFERPOOL_NO;
+      break;
+
     default:
       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
       break;
@@ -191,7 +204,7 @@ gst_pipewire_src_get_property (GObject * object, guint prop_id,
       break;
 
     case PROP_ALWAYS_COPY:
-      g_value_set_boolean (value, pwsrc->always_copy);
+      g_value_set_boolean (value, !pwsrc->use_bufferpool);
       break;
 
     case PROP_MIN_BUFFERS:
@@ -218,6 +231,10 @@ gst_pipewire_src_get_property (GObject * object, guint prop_id,
       g_value_set_boolean (value, pwsrc->autoconnect);
       break;
 
+    case PROP_USE_BUFFERPOOL:
+      g_value_set_boolean (value, !!pwsrc->use_bufferpool);
+      break;
+
     default:
       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
       break;
@@ -331,7 +348,8 @@ gst_pipewire_src_class_init (GstPipeWireSrcClass * klass)
                                                          "Always copy the buffer and data",
                                                          DEFAULT_ALWAYS_COPY,
                                                          G_PARAM_READWRITE |
-                                                         G_PARAM_STATIC_STRINGS));
+                                                         G_PARAM_STATIC_STRINGS |
+                                                         G_PARAM_DEPRECATED));
 
   g_object_class_install_property (gobject_class,
                                    PROP_MIN_BUFFERS,
@@ -387,6 +405,15 @@ gst_pipewire_src_class_init (GstPipeWireSrcClass * klass)
                                                          G_PARAM_READWRITE |
                                                          G_PARAM_STATIC_STRINGS));
 
+  g_object_class_install_property (gobject_class,
+                                   PROP_USE_BUFFERPOOL,
+                                   g_param_spec_boolean ("use-bufferpool",
+                                                         "Use bufferpool",
+                                                         "Use bufferpool (default: true for video, false for audio)",
+                                                         DEFAULT_USE_BUFFERPOOL,
+                                                         G_PARAM_READWRITE |
+                                                         G_PARAM_STATIC_STRINGS));
+
   gstelement_class->provide_clock = gst_pipewire_src_provide_clock;
   gstelement_class->change_state = gst_pipewire_src_change_state;
   gstelement_class->send_event = gst_pipewire_src_send_event;
@@ -427,7 +454,7 @@ gst_pipewire_src_init (GstPipeWireSrc * src)
 
   src->stream = gst_pipewire_stream_new (GST_ELEMENT (src));
 
-  src->always_copy = DEFAULT_ALWAYS_COPY;
+  src->use_bufferpool = DEFAULT_USE_BUFFERPOOL;
   src->min_buffers = DEFAULT_MIN_BUFFERS;
   src->max_buffers = DEFAULT_MAX_BUFFERS;
   src->resend_last = DEFAULT_RESEND_LAST;
@@ -655,21 +682,38 @@ static GstBuffer *dequeue_buffer(GstPipeWireSrc *pwsrc)
 
   for (i = 0; i < b->buffer->n_datas; i++) {
     struct spa_data *d = &b->buffer->datas[i];
+
+    if (d->chunk->size == 0) {
+      // Skip the 0 sized chunk, not adding to the buffer
+      GST_DEBUG_OBJECT(pwsrc, "Chunk size is 0, skipping");
+      continue;
+    }
+
     GstMemory *pmem = gst_buffer_peek_memory (data->buf, i);
     if (pmem) {
       GstMemory *mem;
-      if (!pwsrc->always_copy)
+      if (pwsrc->use_bufferpool != USE_BUFFERPOOL_NO)
         mem = gst_memory_share (pmem, d->chunk->offset, d->chunk->size);
       else
         mem = gst_memory_copy (pmem, d->chunk->offset, d->chunk->size);
       gst_buffer_insert_memory (buf, i, mem);
     }
-    if (d->chunk->flags & SPA_CHUNK_FLAG_CORRUPTED)
+    if (d->chunk->flags & SPA_CHUNK_FLAG_CORRUPTED) {
+      GST_DEBUG_OBJECT(pwsrc, "Buffer corrupted");
       GST_BUFFER_FLAG_SET (buf, GST_BUFFER_FLAG_CORRUPTED);
+    }
   }
-  if (!pwsrc->always_copy)
+  if (pwsrc->use_bufferpool != USE_BUFFERPOOL_NO)
     gst_buffer_add_parent_buffer_meta (buf, data->buf);
   gst_buffer_unref (data->buf);
+
+  if (gst_buffer_get_size(buf) == 0)
+  {
+    GST_ERROR_OBJECT(pwsrc, "Buffer is empty, dropping this");
+    gst_buffer_unref(buf);
+    buf = NULL;
+  }
+
   return buf;
 }
 
@@ -938,6 +982,9 @@ gst_pipewire_src_negotiate (GstBaseSrc * basesrc)
   GST_DEBUG_OBJECT (basesrc, "connect capture with path %s, target-object %s",
                     pwsrc->stream->path, pwsrc->stream->target_object);
 
+  pwsrc->possible_caps = possible_caps;
+  pwsrc->negotiated = FALSE;
+
   enum pw_stream_flags flags;
   flags = PW_STREAM_FLAG_DONT_RECONNECT |
 	  PW_STREAM_FLAG_ASYNC;
@@ -953,9 +1000,6 @@ gst_pipewire_src_negotiate (GstBaseSrc * basesrc)
   pw_thread_loop_get_time (pwsrc->stream->core->loop, &abstime,
                   GST_PIPEWIRE_DEFAULT_TIMEOUT * SPA_NSEC_PER_SEC);
 
-  pwsrc->possible_caps = possible_caps;
-  pwsrc->negotiated = FALSE;
-
   while (TRUE) {
     enum pw_stream_state state = pw_stream_get_state (pwsrc->stream->pwstream, &error);
 
@@ -1047,7 +1091,7 @@ handle_format_change (GstPipeWireSrc *pwsrc,
   }
 
   pw_peer_caps = gst_caps_from_format (param);
-  if (pw_peer_caps) {
+  if (pw_peer_caps && pwsrc->possible_caps) {
     pwsrc->caps = gst_caps_intersect_full (pw_peer_caps,
                                            pwsrc->possible_caps,
                                            GST_CAPS_INTERSECT_FIRST);
@@ -1087,6 +1131,10 @@ handle_format_change (GstPipeWireSrc *pwsrc,
   } else {
     pwsrc->negotiated = FALSE;
     pwsrc->is_video = FALSE;
+
+    /* Don't provide bufferpool for audio if not requested by the application/user */
+    if (pwsrc->use_bufferpool != USE_BUFFERPOOL_YES)
+      pwsrc->use_bufferpool = USE_BUFFERPOOL_NO;
   }
 
   if (pwsrc->caps) {
diff --git a/src/gst/gstpipewiresrc.h b/src/gst/gstpipewiresrc.h
index d8533c78..d5728cdc 100644
--- a/src/gst/gstpipewiresrc.h
+++ b/src/gst/gstpipewiresrc.h
@@ -36,7 +36,7 @@ struct _GstPipeWireSrc {
   GstPipeWireStream *stream;
 
   /*< private >*/
-  gboolean always_copy;
+  gint use_bufferpool;
   gint min_buffers;
   gint max_buffers;
   gboolean resend_last;
diff --git a/src/gst/gstpipewirestream.c b/src/gst/gstpipewirestream.c
index bf764154..68cb9be2 100644
--- a/src/gst/gstpipewirestream.c
+++ b/src/gst/gstpipewirestream.c
@@ -19,6 +19,7 @@ gst_pipewire_stream_init (GstPipeWireStream * self)
   self->fd = -1;
   self->client_name = g_strdup (pw_get_client_name());
   self->pool = gst_pipewire_pool_new (self);
+  spa_dll_init(&self->dll);
 }
 
 static void
diff --git a/src/gst/gstpipewirestream.h b/src/gst/gstpipewirestream.h
index ff8c8e2e..a301375c 100644
--- a/src/gst/gstpipewirestream.h
+++ b/src/gst/gstpipewirestream.h
@@ -11,6 +11,7 @@
 #include "gstpipewirecore.h"
 
 #include <gst/gst.h>
+#include <spa/utils/dll.h>
 #include <pipewire/pipewire.h>
 
 G_BEGIN_DECLS
@@ -29,6 +30,13 @@ struct _GstPipeWireStream {
   GstPipeWirePool *pool;
   GstClock *clock;
 
+  guint64 position;
+  struct spa_dll dll;
+  double err_avg, err_var, err_wdw;
+  guint64 last_ts;
+  guint64 base_buffer_ts;
+  guint64 base_ts;
+
   /* the actual pw stream */
   struct pw_stream *pwstream;
   struct spa_hook pwstream_listener;
diff --git a/src/gst/meson.build b/src/gst/meson.build
index ba1f6d55..1e39bcf8 100644
--- a/src/gst/meson.build
+++ b/src/gst/meson.build
@@ -27,7 +27,7 @@ pipewire_gst_headers = [
 pipewire_gst = shared_library('gstpipewire',
     pipewire_gst_sources,
     include_directories : [ configinc ],
-    dependencies : [ spa_dep, gst_dep, pipewire_dep ],
+    dependencies : [ spa_dep, gst_dep, pipewire_dep, mathlib ],
     install : true,
     install_dir : '@0@/gstreamer-1.0'.format(get_option('libdir')),
 )
diff --git a/src/modules/meson.build b/src/modules/meson.build
index 3f400f08..51db7733 100644
--- a/src/modules/meson.build
+++ b/src/modules/meson.build
@@ -1,5 +1,4 @@
 subdir('module-rt')
-subdir('spa')
 
 # The list of "main" source files for modules, the ones that have the
 # doxygen documentation
@@ -39,6 +38,10 @@ module_sources = [
   'module-rtp-session.c',
   'module-rtp-source.c',
   'module-rtp-sink.c',
+  'module-spa-node.c',
+  'module-spa-node-factory.c',
+  'module-spa-device.c',
+  'module-spa-device-factory.c',
   'module-snapcast-discover.c',
   'module-vban-recv.c',
   'module-vban-send.c',
@@ -71,101 +74,14 @@ pipewire_module_loopback = shared_library('pipewire-module-loopback',
   dependencies : [spa_dep, mathlib, dl_lib, pipewire_dep],
 )
 
-plugin_dependencies = []
-if get_option('spa-plugins').allowed()
-  plugin_dependencies += audioconvert_dep
-endif
-
-simd_cargs = []
-simd_dependencies = []
-
-if have_sse
-  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 += filter_chain_sse
-endif
-if have_avx
-  filter_chain_avx = static_library('filter_chain_avx',
-    ['module-filter-chain/dsp-ops-avx.c' ],
-    c_args : [avx_args, fma_args,'-O3', '-DHAVE_AVX'],
-    dependencies : [ spa_dep ],
-    install : false
-    )
-  simd_cargs += ['-DHAVE_AVX']
-  simd_dependencies += filter_chain_avx
-endif
-if have_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 += filter_chain_neon
-endif
-
-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 += filter_chain_c
-
-filter_chain_sources = [
-  'module-filter-chain.c',
-  'module-filter-chain/biquad.c',
-  'module-filter-chain/ladspa_plugin.c',
-  'module-filter-chain/builtin_plugin.c',
-  'module-filter-chain/convolver.c'
-]
-filter_chain_dependencies = [
-  mathlib, dl_lib, pipewire_dep, sndfile_dep, plugin_dependencies
-]
-
 pipewire_module_filter_chain = shared_library('pipewire-module-filter-chain',
-  filter_chain_sources,
-  include_directories : [configinc],
-  install : true,
-  install_dir : modules_install_dir,
-  install_rpath: modules_install_dir,
-  link_with : simd_dependencies,
-  dependencies : filter_chain_dependencies,
-)
-
-if libmysofa_dep.found()
-pipewire_module_filter_chain_sofa = shared_library('pipewire-module-filter-chain-sofa',
-  [ 'module-filter-chain/sofa_plugin.c',
-    'module-filter-chain/convolver.c' ],
-  include_directories : [configinc],
-  install : true,
-  install_dir : modules_install_dir,
-  install_rpath: modules_install_dir,
-  link_with : simd_dependencies,
-  dependencies : [ filter_chain_dependencies, libmysofa_dep ]
-)
-endif
-
-if lilv_lib.found()
-pipewire_module_filter_chain_lv2 = shared_library('pipewire-module-filter-chain-lv2',
-  [ 'module-filter-chain/lv2_plugin.c' ],
+  [ 'module-filter-chain.c' ],
   include_directories : [configinc],
   install : true,
   install_dir : modules_install_dir,
   install_rpath: modules_install_dir,
-  dependencies : [ filter_chain_dependencies, lilv_lib ]
+  dependencies : [spa_dep, mathlib, dl_lib, pipewire_dep],
 )
-endif
-
 
 pipewire_module_combine_stream = shared_library('pipewire-module-combine-stream',
   [ 'module-combine-stream.c' ],
@@ -259,7 +175,7 @@ pipewire_module_parametric_equalizer = shared_library('pipewire-module-parametri
   install : true,
   install_dir : modules_install_dir,
   install_rpath: modules_install_dir,
-  dependencies : [filter_chain_dependencies],
+  dependencies : [spa_dep, mathlib, dl_lib, pipewire_dep],
 )
 
 pipewire_module_profiler = shared_library('pipewire-module-profiler',
@@ -292,6 +208,39 @@ if build_module_rtkit
 endif
 summary({'rt': '@0@ RTKit'.format(build_module_rtkit ? 'with' : 'without')}, section: 'Optional Modules')
 
+pipewire_module_spa_node = shared_library('pipewire-module-spa-node',
+  [ 'module-spa-node.c', 'spa/spa-node.c' ],
+  include_directories : [configinc],
+  install : true,
+  install_dir : modules_install_dir,
+  install_rpath: modules_install_dir,
+  dependencies : [spa_dep, mathlib, dl_lib, pipewire_dep],
+)
+pipewire_module_spa_node_factory = shared_library('pipewire-module-spa-node-factory',
+  [ 'module-spa-node-factory.c', 'spa/spa-node.c' ],
+  include_directories : [configinc],
+  install : true,
+  install_dir : modules_install_dir,
+  install_rpath: modules_install_dir,
+  dependencies : [spa_dep, mathlib, dl_lib, pipewire_dep],
+)
+pipewire_module_spa_device = shared_library('pipewire-module-spa-device',
+  [ 'module-spa-device.c', 'spa/spa-device.c' ],
+  include_directories : [configinc],
+  install : true,
+  install_dir : modules_install_dir,
+  install_rpath: modules_install_dir,
+  dependencies : [spa_dep, mathlib, dl_lib, pipewire_dep],
+)
+pipewire_module_spa_device_factory = shared_library('pipewire-module-spa-device-factory',
+  [ 'module-spa-device-factory.c', 'spa/spa-device.c' ],
+  include_directories : [configinc],
+  install : true,
+  install_dir : modules_install_dir,
+  install_rpath: modules_install_dir,
+  dependencies : [spa_dep, mathlib, dl_lib, pipewire_dep],
+)
+
 build_module_portal = dbus_dep.found()
 if build_module_portal
   pipewire_module_portal = shared_library('pipewire-module-portal', [ 'module-portal.c' ],
diff --git a/src/modules/module-access.c b/src/modules/module-access.c
index 9e06e567..2e246ae9 100644
--- a/src/modules/module-access.c
+++ b/src/modules/module-access.c
@@ -95,6 +95,22 @@
  * - \ref PW_KEY_ACCESS
  * - \ref PW_KEY_CLIENT_ACCESS
  *
+ * ## Config override
+ *
+ * A `module.access.args` config section can be added
+ * to override the module arguments.
+ *
+ *\code{.unparsed}
+ * # ~/.config/pipewire/pipewire.conf.d/my-access-args.conf
+ *
+ * module.access.args = {
+ *      access.socket = {
+ *          pipewire-0 = "default",
+ *          pipewire-0-manager = "unrestricted",
+ *      }
+ * }
+ *\endcode
+ *
  * ## Example configuration
  *
  *\code{.unparsed}
@@ -274,21 +290,16 @@ get_server_name(const struct spa_dict *props)
 
 static int parse_socket_args(struct impl *impl, const char *str)
 {
-	struct spa_json it[2];
+	struct spa_json it[1];
 	char socket[PATH_MAX];
+	const char *val;
+	int len;
 
-	spa_json_init(&it[0], str, strlen(str));
-
-	if (spa_json_enter_object(&it[0], &it[1]) <= 0)
+	if (spa_json_begin_object(&it[0], str, strlen(str)) <= 0)
 		return -EINVAL;
 
-	while (spa_json_get_string(&it[1], socket, sizeof(socket)) > 0) {
+	while ((len = spa_json_object_next(&it[0], socket, sizeof(socket), &val)) > 0) {
 		char value[256];
-		const char *val;
-		int len;
-
-		if ((len = spa_json_next(&it[1], &val)) <= 0)
-			return -EINVAL;
 
 		if (spa_json_parse_stringn(val, len, value, sizeof(value)) <= 0)
 			return -EINVAL;
@@ -299,17 +310,11 @@ static int parse_socket_args(struct impl *impl, const char *str)
 	return 0;
 }
 
-static int parse_args(struct impl *impl, const struct pw_properties *props, const char *args_str)
+static int parse_args(struct impl *impl, const struct pw_properties *props, const struct pw_properties *args)
 {
-	spa_autoptr(pw_properties) args = NULL;
 	const char *str;
 	int res;
 
-	if (args_str)
-		args = pw_properties_new_string(args_str);
-	else
-		args = pw_properties_new(NULL, NULL);
-
 	if ((str = pw_properties_get(args, "access.legacy")) != NULL) {
 		impl->legacy = spa_atob(str);
 	} else if (pw_properties_get(args, "access.socket")) {
@@ -352,10 +357,11 @@ static int parse_args(struct impl *impl, const struct pw_properties *props, cons
 }
 
 SPA_EXPORT
-int pipewire__module_init(struct pw_impl_module *module, const char *args)
+int pipewire__module_init(struct pw_impl_module *module, const char *args_str)
 {
 	struct pw_context *context = pw_impl_module_get_context(module);
 	const struct pw_properties *props = pw_context_get_properties(context);
+	spa_autoptr(pw_properties) args = NULL;
 	struct impl *impl;
 	int res;
 
@@ -365,7 +371,19 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
 	if (impl == NULL)
 		return -errno;
 
-	pw_log_debug("module %p: new %s", impl, args);
+	pw_log_debug("module %p: new %s", impl, args_str);
+
+	if (args_str)
+		args = pw_properties_new_string(args_str);
+	else
+		args = pw_properties_new(NULL, NULL);
+
+	if (!args) {
+		res = -errno;
+		goto error;
+	}
+
+	pw_context_conf_update_props(context, "module."NAME".args", args);
 
 	impl->socket_access = pw_properties_new(NULL, NULL);
 
diff --git a/src/modules/module-adapter.c b/src/modules/module-adapter.c
index d92bc965..ea913eba 100644
--- a/src/modules/module-adapter.c
+++ b/src/modules/module-adapter.c
@@ -147,6 +147,22 @@ static const struct pw_impl_node_events node_events = {
 	.initialized = node_initialized,
 };
 
+struct match {
+	struct pw_properties *props;
+	int count;
+};
+#define MATCH_INIT(p) ((struct match){ .props = (p) })
+
+static int execute_match(void *data, const char *location, const char *action,
+		const char *val, size_t len)
+{
+	struct match *match = data;
+	if (spa_streq(action, "update-props")) {
+		match->count += pw_properties_update_string(match->props, val, len);
+	}
+	return 1;
+}
+
 static void *create_object(void *_data,
 			   struct pw_resource *resource,
 			   const char *type,
@@ -197,14 +213,20 @@ static void *create_object(void *_data,
 		if (sscanf(str, "pointer:%p", &spa_follower) != 1)
 			goto error_properties;
 	}
+
 	if (spa_follower == NULL) {
 		void *iface;
 		const char *factory_name;
+		struct match match;
 
 		factory_name = pw_properties_get(properties, SPA_KEY_FACTORY_NAME);
 		if (factory_name == NULL)
 			goto error_properties;
 
+		match = MATCH_INIT(properties);
+		pw_context_conf_section_match_rules(d->context, "node.rules",
+				&properties->dict, execute_match, &match);
+
 		handle = pw_context_load_spa_handle(d->context,
 				factory_name,
 				&properties->dict);
diff --git a/src/modules/module-avb/adp.c b/src/modules/module-avb/adp.c
index 19216bc8..2275b7b0 100644
--- a/src/modules/module-avb/adp.c
+++ b/src/modules/module-avb/adp.c
@@ -283,22 +283,18 @@ static int do_help(struct adp *adp, const char *args, FILE *out)
 
 static int do_discover(struct adp *adp, const char *args, FILE *out)
 {
-	struct spa_json it[2];
+	struct spa_json it[1];
 	char key[128];
 	uint64_t entity_id = 0ULL;
+	int len;
+	const char *value;
 
-	spa_json_init(&it[0], args, strlen(args));
-	if (spa_json_enter_object(&it[0], &it[1]) <= 0)
+	if (spa_json_begin_object(&it[0], args, strlen(args)) <= 0)
 		return -EINVAL;
 
-	while (spa_json_get_string(&it[1], key, sizeof(key)) > 0) {
-		int len;
-		const char *value;
+	while ((len = spa_json_object_next(&it[0], key, sizeof(key), &value)) > 0) {
 		uint64_t id_val;
 
-		if ((len = spa_json_next(&it[1], &value)) <= 0)
-			break;
-
 		if (spa_json_is_null(value, len))
 			continue;
 
diff --git a/src/modules/module-avb/maap.c b/src/modules/module-avb/maap.c
index 5dbe962e..2ba40cd3 100644
--- a/src/modules/module-avb/maap.c
+++ b/src/modules/module-avb/maap.c
@@ -253,9 +253,10 @@ static int load_state(struct maap *maap)
 {
 	const char *str;
 	char key[512];
-	struct spa_json it[3];
+	struct spa_json it[2];
 	bool have_offset = false;
-	int count = 0, offset = 0;
+	int count = 0, offset = 0, len;
+	const char *val;
 
 	snprintf(key, sizeof(key), "maap.%s", maap->server->ifname);
 	pw_conf_load_state("module-avb", key, maap->props);
@@ -263,20 +264,13 @@ static int load_state(struct maap *maap)
 	if ((str = pw_properties_get(maap->props, "maap.addresses")) == NULL)
 		return 0;
 
-	spa_json_init(&it[0], str, strlen(str));
-	if (spa_json_enter_array(&it[0], &it[1]) <= 0)
+	if (spa_json_begin_array(&it[0], str, strlen(str)) <= 0)
 		return 0;
 
-	if (spa_json_enter_object(&it[1], &it[2]) <= 0)
+	if (spa_json_enter_object(&it[0], &it[1]) <= 0)
 		return 0;
 
-	while (spa_json_get_string(&it[2], key, sizeof(key)) > 0) {
-		const char *val;
-		int len;
-
-		if ((len = spa_json_next(&it[2], &val)) <= 0)
-			break;
-
+	while ((len = spa_json_object_next(&it[1], key, sizeof(key), &val)) > 0) {
 		if (spa_streq(key, "start")) {
 			uint8_t addr[6];
 			if (avb_utils_parse_addr(val, len, addr) >= 0 &&
diff --git a/src/modules/module-client-device.c b/src/modules/module-client-device.c
index e2450e81..84b8456b 100644
--- a/src/modules/module-client-device.c
+++ b/src/modules/module-client-device.c
@@ -16,10 +16,63 @@
 #include "module-client-device/client-device.h"
 
 /** \page page_module_client_device Client Device
+ *
+ * Allow clients to export devices to the PipeWire daemon.
+ *
+ * This module creates an export type for the \ref SPA_TYPE_INTERFACE_Device
+ * interface.
+ *
+ * With \ref pw_core_export(), objects of this type can be exported to the
+ * PipeWire server. All actions performed on the device locally will be visible
+ * to connecteced clients.
+ *
+ * In some cases, it is possible to use this factory directly.
+ * With \ref pw_core_create_object() on the `client-device`
+ * factory will result in a \ref SPA_TYPE_INTERFACE_Device proxy that can be
+ * used to control the server side created \ref pw_impl_device.
+ *
+ * Schematically, the client side \ref spa_device is wrapped in the ClientDevice
+ * proxy and unwrapped by the server side resource so that all actions on the client
+ * side device are reflected on the server side device and server side actions are
+ * reflected in the client.
+ *
+ *\code{.unparsed}
+ *
+ *   client side proxy                            server side resource
+ * .------------------------------.            .----------------------------------.
+ * | SPA_TYPE_INTERFACE_Device    |            |  PW_TYPE_INTERFACE_Device        |
+ * |                              |  IPC       |.--------------------------------.|
+ * |                              | ----->     || SPA_TYPE_INTERFACE_Device      ||
+ * |                              |            |'--------------------------------'|
+ * '------------------------------'            '----------------------------------'
+ *\endcode
  *
  * ## Module Name
  *
  * `libpipewire-module-client-device`
+ *
+ * ## Module Options
+ *
+ * This module has no options.
+ *
+ * ## Properties for the create_object call
+ *
+ * All properties are passed directly to the \ref pw_context_create_device() call.
+ *
+ * ## Example configuration
+ *
+ * The module is usually added to the config file of the main PipeWire daemon and the
+ * clients.
+ *
+ *\code{.unparsed}
+ * context.modules = [
+ * { name = libpipewire-module-client-device }
+ * ]
+ *\endcode
+ *
+ * ## See also
+ *
+ * - `module-spa-device-factory`: make nodes from a factory
  */
 
 #define NAME "client-device"
diff --git a/src/modules/module-client-node.c b/src/modules/module-client-node.c
index 9e8922db..dc978084 100644
--- a/src/modules/module-client-node.c
+++ b/src/modules/module-client-node.c
@@ -13,14 +13,82 @@
 
 #include <pipewire/impl.h>
 
+#define PW_API_CLIENT_NODE_IMPL	SPA_EXPORT
 #include "module-client-node/v0/client-node.h"
 #include "module-client-node/client-node.h"
 
 /** \page page_module_client_node Client Node
+ *
+ * Allow clients to export processing nodes to the PipeWire daemon.
+ *
+ * This module creates 2 export types, one for the \ref PW_TYPE_INTERFACE_Node and
+ * another for the \ref SPA_TYPE_INTERFACE_Node interfaces.
+ *
+ * With \ref pw_core_export(), objects of these types can be exported to the
+ * PipeWire server. All actions performed on the node locally will be visible
+ * to connecteced clients and scheduling of the Node will be performed.
+ *
+ * Objects of the \ref PW_TYPE_INTERFACE_Node interface can be made with
+ * \ref pw_context_create_node(), for example. You would manually need to create
+ * and add an object of the \ref SPA_TYPE_INTERFACE_Node interface. Exporting a
+ * \ref SPA_TYPE_INTERFACE_Node directly will first wrap it in a
+ * \ref PW_TYPE_INTERFACE_Node interface.
+ *
+ * Usually this module is not used directly but through the \ref pw_stream and
+ * \ref pw_filter APIs, which provides API to implement the \ref SPA_TYPE_INTERFACE_Node
+ * interface.
+ *
+ * In some cases, it is possible to use this factory directly (the PipeWire JACK
+ * implementation does this). With \ref pw_core_create_object() on the `client-node`
+ * factory will result in a \ref PW_TYPE_INTERFACE_ClientNode proxy that can be
+ * used to control the server side created \ref pw_impl_node.
+ *
+ * Schematically, the client side \ref pw_impl_node is wrapped in the ClientNode
+ * proxy and unwrapped by the server side resource so that all actions on the client
+ * side node are reflected on the server side node and server side actions are
+ * reflected in the client.
+ *
+ *\code{.unparsed}
+ *
+ *   client side proxy                            server side resource
+ * .------------------------------.            .----------------------------------.
+ * | PW_TYPE_INTERFACE_ClientNode |            |  PW_TYPE_INTERFACE_Node          |
+ * |.----------------------------.|  IPC       |.--------------------------------.|
+ * || PW_TYPE_INTERFACE_Node     || ----->     || SPA_TYPE_INTERFACE_Node        ||
+ * ||.--------------------------.||            ||.------------------------------.||
+ * ||| SPA_TYPE_INTERFACE_Node  |||            ||| PW_TYPE_INTERFACE_ClientNode |||
+ * |||                          |||            |||                              |||
+ * ||'--------------------------'||            ||'------------------------------'||
+ * |'----------------------------'|            |'--------------------------------'|
+ * '------------------------------'            '----------------------------------'
+ *\endcode
  *
  * ## Module Name
  *
  * `libpipewire-module-client-node`
+ *
+ * ## Module Options
+ *
+ * This module has no options.
+ *
+ * ## Properties for the create_object call
+ *
+ * All properties are passed directly to the \ref pw_context_create_node() call.
+ *
+ * ## Example configuration
+ *
+ * The module is usually added to the config file of the main PipeWire daemon and the
+ * clients.
+ *
+ *\code{.unparsed}
+ * context.modules = [
+ * { name = libpipewire-module-client-node }
+ * ]
+ *\endcode
+ *
+ * ## See also
+ *
+ * - `module-spa-node-factory`: make nodes from a factory
  */
 
 #define NAME "client-node"
diff --git a/src/modules/module-combine-stream.c b/src/modules/module-combine-stream.c
index 20a23290..4afcb8f0 100644
--- a/src/modules/module-combine-stream.c
+++ b/src/modules/module-combine-stream.c
@@ -25,6 +25,7 @@
 #include <spa/pod/builder.h>
 #include <spa/param/audio/format-utils.h>
 #include <spa/param/audio/raw.h>
+#include <spa/param/audio/raw-json.h>
 #include <spa/param/latency-utils.h>
 #include <spa/param/tag-utils.h>
 
@@ -324,44 +325,15 @@ struct stream {
 	unsigned int have_latency:1;
 };
 
-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);
-	info->format = SPA_AUDIO_FORMAT_F32P;
-	info->channels = pw_properties_get_uint32(props, PW_KEY_AUDIO_CHANNELS, 0);
-	info->channels = SPA_MIN(info->channels, SPA_AUDIO_MAX_CHANNELS);
-	if ((str = pw_properties_get(props, SPA_KEY_AUDIO_POSITION)) != NULL)
-		parse_position(info, str, strlen(str));
-	if (info->channels == 0)
-		parse_position(info, DEFAULT_POSITION, strlen(DEFAULT_POSITION));
+	spa_audio_info_raw_init_dict_keys(info,
+			&SPA_DICT_ITEMS(
+				SPA_DICT_ITEM(SPA_KEY_AUDIO_FORMAT, "F32P"),
+				SPA_DICT_ITEM(SPA_KEY_AUDIO_POSITION, DEFAULT_POSITION)),
+			&props->dict,
+			SPA_KEY_AUDIO_CHANNELS,
+			SPA_KEY_AUDIO_POSITION, NULL);
 }
 
 static void ringbuffer_init(struct ringbuffer *r, void *buf, uint32_t size)
@@ -397,6 +369,40 @@ static void ringbuffer_memcpy(struct ringbuffer *r, void *dst, void *src, uint32
 	}
 }
 
+static void mix_f32(float *dst, float *src, uint32_t size)
+{
+	uint32_t i, s = size / sizeof(float);
+	for (i = 0; i < s; i++)
+		dst[i] += src[i];
+}
+
+static void ringbuffer_mix(struct ringbuffer *r, void *dst, void *src, uint32_t size)
+{
+	uint32_t avail;
+
+	avail = SPA_MIN(size, r->size);
+
+	/* buf to dst */
+	if (dst && avail > 0) {
+		uint32_t l0 = SPA_MIN(avail, r->size - r->idx), l1 = avail - l0;
+		mix_f32(dst, SPA_PTROFF(r->buf, r->idx, void), l0);
+		if (SPA_UNLIKELY(l1 > 0))
+			mix_f32(SPA_PTROFF(dst, l0, void), r->buf, l1);
+		dst = SPA_PTROFF(dst, avail, void);
+	}
+	/* src to dst */
+	if (size > avail) {
+		if (dst)
+			mix_f32(dst, src, size - avail);
+		src = SPA_PTROFF(src, size - avail, void);
+	}
+	/* src to buf */
+	if (avail > 0) {
+		spa_ringbuffer_write_data(NULL, r->buf, r->size, r->idx, src, avail);
+		r->idx = (r->idx + avail) % r->size;
+	}
+}
+
 static void ringbuffer_copy(struct ringbuffer *dst, struct ringbuffer *src)
 {
 	uint32_t l0, l1;
@@ -775,7 +781,7 @@ static int create_stream(struct stream_info *info)
 	int res;
 	uint32_t n_params, i, j;
 	const struct spa_pod *params[1];
-	const char *str, *node_name;
+	const char *str, *node_name, *dir_name;
 	uint8_t buffer[1024];
 	struct spa_pod_builder b;
 	struct spa_audio_info_raw remap_info, tmp_info;
@@ -802,16 +808,32 @@ static int create_stream(struct stream_info *info)
 
 	s->id = info->id;
 	s->impl = impl;
+	s->stream_events = stream_events;
+
+	flags = PW_STREAM_FLAG_AUTOCONNECT |
+			PW_STREAM_FLAG_MAP_BUFFERS |
+			PW_STREAM_FLAG_RT_PROCESS |
+			PW_STREAM_FLAG_ASYNC;
+
+	if (impl->mode == MODE_SINK || impl->mode == MODE_CAPTURE) {
+		direction = PW_DIRECTION_OUTPUT;
+		flags |= PW_STREAM_FLAG_TRIGGER;
+		dir_name = "output";
+	} else {
+		direction = PW_DIRECTION_INPUT;
+		s->stream_events.process = stream_input_process;
+		dir_name = "input";
+	}
 
 	s->info = impl->info;
 	if ((str = pw_properties_get(info->stream_props, SPA_KEY_AUDIO_POSITION)) != NULL)
-		parse_position(&s->info, str, strlen(str));
+		spa_audio_parse_position(str, strlen(str), s->info.position, &s->info.channels);
 	if (s->info.channels == 0)
 		s->info = impl->info;
 
 	spa_zero(remap_info);
 	if ((str = pw_properties_get(info->stream_props, "combine.audio.position")) != NULL)
-		parse_position(&remap_info, str, strlen(str));
+		spa_audio_parse_position(str, strlen(str), remap_info.position, &remap_info.channels);
 	if (remap_info.channels == 0)
 		remap_info = s->info;
 
@@ -835,10 +857,10 @@ static int create_stream(struct stream_info *info)
 
 	if (pw_properties_get(info->stream_props, PW_KEY_MEDIA_NAME) == NULL)
 		pw_properties_setf(info->stream_props, PW_KEY_MEDIA_NAME,
-				"%s output", str);
+				"%s %s", str, dir_name);
 	if (pw_properties_get(info->stream_props, PW_KEY_NODE_DESCRIPTION) == NULL)
 		pw_properties_setf(info->stream_props, PW_KEY_NODE_DESCRIPTION,
-				"%s output", str);
+				"%s %s", str, dir_name);
 
 	str = pw_properties_get(impl->props, PW_KEY_NODE_NAME);
 	if (str == NULL)
@@ -846,7 +868,7 @@ static int create_stream(struct stream_info *info)
 
 	if (pw_properties_get(info->stream_props, PW_KEY_NODE_NAME) == NULL)
 		pw_properties_setf(info->stream_props, PW_KEY_NODE_NAME,
-				"output.%s_%s", str, node_name);
+				"%s.%s_%s", dir_name, str, node_name);
 
 	if (info->on_demand_id) {
 		s->on_demand_id = strdup(info->on_demand_id);
@@ -861,21 +883,6 @@ static int create_stream(struct stream_info *info)
 	if (s->stream == NULL)
 		goto error_errno;
 
-	s->stream_events = stream_events;
-
-	flags = PW_STREAM_FLAG_AUTOCONNECT |
-			PW_STREAM_FLAG_MAP_BUFFERS |
-			PW_STREAM_FLAG_RT_PROCESS |
-			PW_STREAM_FLAG_ASYNC;
-
-	if (impl->mode == MODE_SINK || impl->mode == MODE_CAPTURE) {
-		direction = PW_DIRECTION_OUTPUT;
-		flags |= PW_STREAM_FLAG_TRIGGER;
-	} else {
-		direction = PW_DIRECTION_INPUT;
-		s->stream_events.process = stream_input_process;
-	}
-
 	pw_stream_add_listener(s->stream,
 			&s->stream_listener,
 			&s->stream_events, s);
@@ -1177,11 +1184,14 @@ static void combine_output_process(void *d)
 	struct pw_buffer *in, *out;
 	struct stream *s;
 	bool delay_changed = false;
+	bool mix[SPA_AUDIO_MAX_CHANNELS];
 
 	if ((out = pw_stream_dequeue_buffer(impl->combine)) == NULL) {
 		pw_log_debug("%p: out of output buffers: %m", impl);
 		return;
 	}
+	for (uint32_t i = 0; i < out->buffer->n_datas; i++)
+		mix[i] = false;
 
 	spa_list_for_each(s, &impl->streams, link) {
 		uint32_t j;
@@ -1214,7 +1224,6 @@ static void combine_output_process(void *d)
 
 			ds = &in->buffer->datas[j];
 
-			/* FIXME, need to do mixing for overlapping streams */
 			remap = s->remap[j];
 			if (remap < out->buffer->n_datas) {
 				uint32_t offs, size;
@@ -1225,8 +1234,14 @@ static void combine_output_process(void *d)
 				size = SPA_MIN(ds->chunk->size, ds->maxsize - offs);
 				size = SPA_MIN(size, dd->maxsize);
 
-				ringbuffer_memcpy(&s->delay[j],
-					dd->data, SPA_PTROFF(ds->data, offs, void), size);
+				if (mix[remap]) {
+					ringbuffer_mix(&s->delay[j],
+						dd->data, SPA_PTROFF(ds->data, offs, void), size);
+				} else {
+					ringbuffer_memcpy(&s->delay[j],
+						dd->data, SPA_PTROFF(ds->data, offs, void), size);
+					mix[remap] = true;
+				}
 
 				outsize = SPA_MAX(outsize, size);
 				stride = SPA_MAX(stride, ds->chunk->stride);
diff --git a/src/modules/module-echo-cancel.c b/src/modules/module-echo-cancel.c
index dd86d792..4f08927a 100644
--- a/src/modules/module-echo-cancel.c
+++ b/src/modules/module-echo-cancel.c
@@ -21,6 +21,7 @@
 #include <spa/debug/types.h>
 #include <spa/param/audio/format-utils.h>
 #include <spa/param/audio/raw.h>
+#include <spa/param/audio/raw-json.h>
 #include <spa/param/latency-utils.h>
 #include <spa/pod/builder.h>
 #include <spa/pod/dynamic.h>
@@ -152,7 +153,6 @@ PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
 #define PW_LOG_TOPIC_DEFAULT mod_topic
 
 #define DEFAULT_RATE 48000
-#define DEFAULT_CHANNELS 2
 #define DEFAULT_POSITION "[ FL FR ]"
 
 /* Hopefully this is enough for any combination of AEC engine and resampler
@@ -1191,48 +1191,17 @@ static const struct pw_impl_module_events module_events = {
 	.destroy = module_destroy,
 };
 
-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(struct pw_properties *props, struct spa_audio_info_raw *info)
 {
-	const char *str;
-
-	*info = SPA_AUDIO_INFO_RAW_INIT(
-			.format = SPA_AUDIO_FORMAT_F32P);
-	info->rate = pw_properties_get_uint32(props, PW_KEY_AUDIO_RATE, info->rate);
-	if (info->rate == 0)
-		info->rate = DEFAULT_RATE;
-
-	info->channels = pw_properties_get_uint32(props, PW_KEY_AUDIO_CHANNELS, info->channels);
-	info->channels = SPA_MIN(info->channels, SPA_AUDIO_MAX_CHANNELS);
-	if ((str = pw_properties_get(props, SPA_KEY_AUDIO_POSITION)) != NULL)
-		parse_position(info, str, strlen(str));
-	if (info->channels == 0)
-		parse_position(info, DEFAULT_POSITION, strlen(DEFAULT_POSITION));
+	spa_audio_info_raw_init_dict_keys(info,
+			&SPA_DICT_ITEMS(
+				SPA_DICT_ITEM(SPA_KEY_AUDIO_FORMAT, "F32P"),
+				SPA_DICT_ITEM(SPA_KEY_AUDIO_RATE, SPA_STRINGIFY(DEFAULT_RATE)),
+				SPA_DICT_ITEM(SPA_KEY_AUDIO_POSITION, DEFAULT_POSITION)),
+			&props->dict,
+			SPA_KEY_AUDIO_RATE,
+			SPA_KEY_AUDIO_CHANNELS,
+			SPA_KEY_AUDIO_POSITION, NULL);
 }
 
 static void copy_props(struct impl *impl, struct pw_properties *props, const char *key)
@@ -1390,17 +1359,21 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
 	}
 
 	if ((str = pw_properties_get(impl->capture_props, SPA_KEY_AUDIO_POSITION)) != NULL) {
-		parse_position(&impl->capture_info, str, strlen(str));
+		spa_audio_parse_position(str, strlen(str),
+				impl->capture_info.position, &impl->capture_info.channels);
 	}
 	if ((str = pw_properties_get(impl->source_props, SPA_KEY_AUDIO_POSITION)) != NULL) {
-		parse_position(&impl->source_info, str, strlen(str));
+		spa_audio_parse_position(str, strlen(str),
+				impl->source_info.position, &impl->source_info.channels);
 	}
 	if ((str = pw_properties_get(impl->sink_props, SPA_KEY_AUDIO_POSITION)) != NULL) {
-		parse_position(&impl->sink_info, str, strlen(str));
+		spa_audio_parse_position(str, strlen(str),
+				impl->sink_info.position, &impl->sink_info.channels);
 		impl->playback_info = impl->sink_info;
 	}
 	if ((str = pw_properties_get(impl->playback_props, SPA_KEY_AUDIO_POSITION)) != NULL) {
-		parse_position(&impl->playback_info, str, strlen(str));
+		spa_audio_parse_position(str, strlen(str),
+				impl->playback_info.position, &impl->playback_info.channels);
 		if (impl->playback_info.channels != impl->sink_info.channels)
 			impl->playback_info = impl->sink_info;
 	}
diff --git a/src/modules/module-example-filter.c b/src/modules/module-example-filter.c
index 6a834643..5f6268dd 100644
--- a/src/modules/module-example-filter.c
+++ b/src/modules/module-example-filter.c
@@ -17,6 +17,7 @@
 #include <spa/utils/json.h>
 #include <spa/utils/ringbuffer.h>
 #include <spa/param/latency-utils.h>
+#include <spa/param/audio/raw-json.h>
 #include <spa/debug/types.h>
 
 #include <pipewire/impl.h>
@@ -96,6 +97,8 @@
 PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
 #define PW_LOG_TOPIC_DEFAULT mod_topic
 
+#define DEFAULT_POSITION "[ FL FR ]"
+
 static const struct spa_dict_item module_props[] = {
 	{ PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
 	{ PW_KEY_MODULE_DESCRIPTION, "Create example filter streams" },
@@ -158,7 +161,15 @@ static void capture_destroy(void *d)
 static void capture_process(void *d)
 {
 	struct impl *impl = d;
-	pw_stream_trigger_process(impl->playback);
+	int res;
+	if ((res = pw_stream_trigger_process(impl->playback)) < 0) {
+		while (true) {
+			struct pw_buffer *t;
+			if ((t = pw_stream_dequeue_buffer(impl->capture)) == NULL)
+				break;
+			pw_stream_queue_buffer(impl->capture, t);
+		}
+	}
 }
 
 static void playback_process(void *d)
@@ -447,43 +458,16 @@ static const struct pw_impl_module_events module_events = {
 	.destroy = module_destroy,
 };
 
-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(struct pw_properties *props, struct spa_audio_info_raw *info)
 {
-	const char *str;
-
-	*info = SPA_AUDIO_INFO_RAW_INIT(
-			.format = SPA_AUDIO_FORMAT_F32P);
-	info->rate = pw_properties_get_int32(props, PW_KEY_AUDIO_RATE, 0);
-	info->channels = pw_properties_get_uint32(props, PW_KEY_AUDIO_CHANNELS, 0);
-	info->channels = SPA_MIN(info->channels, SPA_AUDIO_MAX_CHANNELS);
-	if ((str = pw_properties_get(props, SPA_KEY_AUDIO_POSITION)) != NULL)
-		parse_position(info, str, strlen(str));
+	spa_audio_info_raw_init_dict_keys(info,
+			&SPA_DICT_ITEMS(
+				SPA_DICT_ITEM(SPA_KEY_AUDIO_FORMAT, "F32P"),
+				SPA_DICT_ITEM(SPA_KEY_AUDIO_POSITION, DEFAULT_POSITION)),
+			&props->dict,
+			SPA_KEY_AUDIO_RATE,
+			SPA_KEY_AUDIO_CHANNELS,
+			SPA_KEY_AUDIO_POSITION, NULL);
 }
 
 static void copy_props(struct impl *impl, struct pw_properties *props, const char *key)
diff --git a/src/modules/module-example-sink.c b/src/modules/module-example-sink.c
index a8fcb13c..8014f61f 100644
--- a/src/modules/module-example-sink.c
+++ b/src/modules/module-example-sink.c
@@ -24,6 +24,7 @@
 #include <spa/pod/builder.h>
 #include <spa/param/audio/format-utils.h>
 #include <spa/param/audio/raw.h>
+#include <spa/param/audio/raw-json.h>
 
 #include <pipewire/impl.h>
 #include <pipewire/i18n.h>
@@ -272,61 +273,18 @@ static const struct pw_impl_module_events module_events = {
 	.destroy = module_destroy,
 };
 
-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));
+	spa_audio_info_raw_init_dict_keys(info,
+			&SPA_DICT_ITEMS(
+				SPA_DICT_ITEM(SPA_KEY_AUDIO_FORMAT, DEFAULT_FORMAT),
+				SPA_DICT_ITEM(SPA_KEY_AUDIO_RATE, SPA_STRINGIFY(DEFAULT_RATE)),
+				SPA_DICT_ITEM(SPA_KEY_AUDIO_POSITION, DEFAULT_POSITION)),
+			&props->dict,
+			SPA_KEY_AUDIO_FORMAT,
+			SPA_KEY_AUDIO_RATE,
+			SPA_KEY_AUDIO_CHANNELS,
+			SPA_KEY_AUDIO_POSITION, NULL);
 }
 
 static int calc_frame_size(const struct spa_audio_info_raw *info)
diff --git a/src/modules/module-example-source.c b/src/modules/module-example-source.c
index c8f739c2..47a06189 100644
--- a/src/modules/module-example-source.c
+++ b/src/modules/module-example-source.c
@@ -24,6 +24,7 @@
 #include <spa/pod/builder.h>
 #include <spa/param/audio/format-utils.h>
 #include <spa/param/audio/raw.h>
+#include <spa/param/audio/raw-json.h>
 
 #include <pipewire/impl.h>
 #include <pipewire/i18n.h>
@@ -278,61 +279,18 @@ static const struct pw_impl_module_events module_events = {
 	.destroy = module_destroy,
 };
 
-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));
+	spa_audio_info_raw_init_dict_keys(info,
+			&SPA_DICT_ITEMS(
+				 SPA_DICT_ITEM(SPA_KEY_AUDIO_FORMAT, DEFAULT_FORMAT),
+				 SPA_DICT_ITEM(SPA_KEY_AUDIO_RATE, SPA_STRINGIFY(DEFAULT_RATE)),
+				 SPA_DICT_ITEM(SPA_KEY_AUDIO_POSITION, DEFAULT_POSITION)),
+			&props->dict,
+			SPA_KEY_AUDIO_FORMAT,
+			SPA_KEY_AUDIO_RATE,
+			SPA_KEY_AUDIO_CHANNELS,
+			SPA_KEY_AUDIO_POSITION, NULL);
 }
 
 static int calc_frame_size(const struct spa_audio_info_raw *info)
diff --git a/src/modules/module-ffado-driver.c b/src/modules/module-ffado-driver.c
index 26e281e4..4345e5a1 100644
--- a/src/modules/module-ffado-driver.c
+++ b/src/modules/module-ffado-driver.c
@@ -24,6 +24,8 @@
 #include <spa/param/audio/format-utils.h>
 #include <spa/param/latency-utils.h>
 #include <spa/param/audio/raw.h>
+#include <spa/param/audio/raw-json.h>
+#include <spa/control/ump-utils.h>
 
 #include <pipewire/impl.h>
 #include <pipewire/i18n.h>
@@ -341,30 +343,30 @@ static void midi_to_ffado(struct port *p, float *src, uint32_t n_samples)
 	p->event_pos = 0;
 
 	SPA_POD_SEQUENCE_FOREACH(seq, c) {
-		switch(c->type) {
-		case SPA_CONTROL_Midi:
-		{
-			uint8_t *data = SPA_POD_BODY(&c->value);
-			size_t size = SPA_POD_BODY_SIZE(&c->value);
-
-			if (index < c->offset)
-				index = SPA_ROUND_UP_N(c->offset, 8);
-			for (i = 0; i < size; i++) {
-				if (index >= n_samples) {
-					/* keep events that don't fit for the next cycle */
-					if (p->event_pos < sizeof(p->event_buffer))
-						p->event_buffer[p->event_pos++] = data[i];
-					else
-						unhandled++;
-				}
+		uint8_t data[16];
+		int j, size;
+
+		if (c->type != SPA_CONTROL_UMP)
+			continue;
+
+		size = spa_ump_to_midi(SPA_POD_BODY(&c->value),
+				SPA_POD_BODY_SIZE(&c->value), data, sizeof(data));
+		if (size <= 0)
+			continue;
+
+		if (index < c->offset)
+			index = SPA_ROUND_UP_N(c->offset, 8);
+		for (j = 0; j < size; j++) {
+			if (index >= n_samples) {
+				/* keep events that don't fit for the next cycle */
+				if (p->event_pos < sizeof(p->event_buffer))
+					p->event_buffer[p->event_pos++] = data[j];
 				else
-					dst[index] = 0x01000000 | (uint32_t) data[i];
-				index += 8;
+					unhandled++;
 			}
-			break;
-		}
-		default:
-			break;
+			else
+				dst[index] = 0x01000000 | (uint32_t) data[j];
+			index += 8;
 		}
 	}
 	if (unhandled > 0)
@@ -489,8 +491,16 @@ static void ffado_to_midi(struct port *p, float *dst, uint32_t *src, uint32_t si
 			continue;
 
 		if (process_byte(p, i, data & 0xff, &frame, &bytes, &size)) {
-			spa_pod_builder_control(&b, frame, SPA_CONTROL_Midi);
-	                spa_pod_builder_bytes(&b, bytes, size);
+			uint64_t state = 0;
+			while (size > 0) {
+				uint32_t ev[4];
+				int ev_size = spa_ump_from_midi(&bytes, &size, ev, sizeof(ev), 0, &state);
+				if (ev_size <= 0)
+					break;
+
+				spa_pod_builder_control(&b, frame, SPA_CONTROL_UMP);
+		                spa_pod_builder_bytes(&b, ev, ev_size);
+			}
 		}
         }
 	spa_pod_builder_pop(&b, &f);
@@ -762,7 +772,7 @@ static int make_stream_ports(struct stream *s)
 			break;
 		case ffado_stream_type_midi:
 			props = pw_properties_new(
-					PW_KEY_FORMAT_DSP, "8 bit raw midi",
+					PW_KEY_FORMAT_DSP, "32 bit raw UMP",
 					PW_KEY_PORT_NAME, port->name,
 					PW_KEY_PORT_PHYSICAL, "true",
 					PW_KEY_PORT_TERMINAL, "true",
@@ -1397,61 +1407,30 @@ static const struct pw_impl_module_events module_events = {
 	.destroy = module_destroy,
 };
 
-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_devices(struct impl *impl, const char *val, size_t len)
 {
-	struct spa_json it[2];
+	struct spa_json it[1];
 	char v[FFADO_MAX_SPECSTRING_LENGTH];
 
-	spa_json_init(&it[0], val, len);
-        if (spa_json_enter_array(&it[0], &it[1]) <= 0)
-                spa_json_init(&it[1], val, len);
+        if (spa_json_begin_array_relax(&it[0], val, len) <= 0)
+		return;
 
 	impl->n_devices = 0;
-	while (spa_json_get_string(&it[1], v, sizeof(v)) > 0 &&
+	while (spa_json_get_string(&it[0], v, sizeof(v)) > 0 &&
 	    impl->n_devices < FFADO_MAX_SPECSTRINGS) {
 		impl->devices[impl->n_devices++] = strdup(v);
 	}
 }
 
-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);
-	info->format = SPA_AUDIO_FORMAT_F32P;
-	info->rate = 0;
-	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));
+	spa_audio_info_raw_init_dict_keys(info,
+			&SPA_DICT_ITEMS(
+				 SPA_DICT_ITEM(SPA_KEY_AUDIO_FORMAT, "F32P"),
+				 SPA_DICT_ITEM(SPA_KEY_AUDIO_POSITION, DEFAULT_POSITION)),
+			&props->dict,
+			SPA_KEY_AUDIO_CHANNELS,
+			SPA_KEY_AUDIO_POSITION, NULL);
 }
 
 static void copy_props(struct impl *impl, struct pw_properties *props, const char *key)
diff --git a/src/modules/module-filter-chain.c b/src/modules/module-filter-chain.c
index f2f6ad93..1446cc43 100644
--- a/src/modules/module-filter-chain.c
+++ b/src/modules/module-filter-chain.c
@@ -13,27 +13,21 @@
 
 #include "config.h"
 
-#include "module-filter-chain/plugin.h"
-
-#include <spa/utils/result.h>
-#include <spa/utils/string.h>
-#include <spa/utils/json.h>
-#include <spa/support/cpu.h>
 #include <spa/param/latency-utils.h>
 #include <spa/param/tag-utils.h>
+#include <spa/param/audio/raw-json.h>
 #include <spa/pod/dynamic.h>
-#include <spa/debug/types.h>
-#include <spa/debug/log.h>
+#include <spa/filter-graph/filter-graph.h>
 
-#include <pipewire/utils.h>
 #include <pipewire/impl.h>
-#include <pipewire/extensions/profiler.h>
 
 #define NAME "filter-chain"
 
 PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
 #define PW_LOG_TOPIC_DEFAULT mod_topic
 
+extern struct spa_handle_factory spa_filter_graph_factory;
+
 /**
  * \page page_module_filter_chain Filter-Chain
  *
@@ -101,18 +95,18 @@ PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
  * Nodes describe the processing filters in the graph. Use a tool like lv2ls
  * or listplugins to get a list of available plugins, labels and the port names.
  *
- * - `type` is one of `ladspa`, `lv2`, `builtin` or `sofa`.
+ * - `type` is one of `ladspa`, `lv2`, `builtin`, `sofa` or `ebur128`.
  * - `name` is the name for this node, you might need this later to refer to this node
  *    and its ports when setting controls or making links.
  * - `plugin` is the type specific plugin name.
  *    - For LADSPA plugins it will append `.so` to find the shared object with that
  *       name in the LADSPA plugin path.
  *    - For LV2, this is the plugin URI obtained with lv2ls.
- *    - For builtin and sofa this is ignored
+ *    - For builtin, sofa and ebur128 this is ignored
  * - `label` is the type specific filter inside the plugin.
  *    - For LADSPA this is the label
  *    - For LV2 this is unused
- *    - For builtin and sofa this is the name of the filter to use
+ *    - For builtin, sofa and ebur128 this is the name of the filter to use
  *
  * - `config` contains a filter specific configuration section. Some plugins need
  *            this. (convolver, sofa, delay, ...)
@@ -232,6 +226,84 @@ PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
  * }
  *\endcode
  *
+ * ### Parametric EQ
+ *
+ * The parametric EQ chains a number of biquads together. It is more efficient than
+ * specifying a number of chained biquads and it can also load configuration from a
+ * file.
+ *
+ * The parametric EQ supports multichannel processing and has 8 input and 8 output ports
+ * that don't all need to be connected. The ports are named `In 1` to `In 8` and
+ * `Out 1` to `Out 8`.
+ *
+ *\code{.unparsed}
+ * filter.graph = {
+ *     nodes = [
+ *         {
+ *             type   = builtin
+ *             name   = ...
+ *             label  = param_eq
+ *             config = {
+ *                 filename = "..."
+ *                 #filename1 = "...", filename2 = "...", ...
+ *                 filters = [
+ *                     { type = ..., freq = ..., gain = ..., q = ... },
+ *                     { type = ..., freq = ..., gain = ..., q = ... },
+ *                     ....
+ *                 ]
+ *                 #filters1 = [ ... ], filters2 = [ ... ], ...
+ *             }
+ *             ...
+ *         }
+ *     }
+ *     ...
+ * }
+ *\endcode
+ *
+ * Either a `filename` or a `filters` array can be specified. The configuration
+ * will be used for all channels. Alternatively `filenameX` or `filtersX` where
+ * X is the channel number (between 1 and 8) can be used to load a channel
+ * specific configuration.
+ *
+ * The `filename` must point to a parametric equalizer configuration
+ * generated from the AutoEQ project or Squiglink. Both the projects allow
+ * equalizing headphones or an in-ear monitor to a target curve.
+ *
+ * A popular example of the above being EQ'ing to the Harman target curve
+ * or EQ'ing one headphone/IEM to another.
+ *
+ * For AutoEQ, see https://github.com/jaakkopasanen/AutoEq.
+ * For SquigLink, see https://squig.link/.
+ *
+ * Parametric equalizer configuration generated from AutoEQ or Squiglink looks
+ * like below.
+ *
+ * \code{.unparsed}
+ * Preamp: -6.8 dB
+ * Filter 1: ON PK Fc 21 Hz Gain 6.7 dB Q 1.100
+ * Filter 2: ON PK Fc 85 Hz Gain 6.9 dB Q 3.000
+ * Filter 3: ON PK Fc 110 Hz Gain -2.6 dB Q 2.700
+ * Filter 4: ON PK Fc 210 Hz Gain 5.9 dB Q 2.100
+ * Filter 5: ON PK Fc 710 Hz Gain -1.0 dB Q 0.600
+ * Filter 6: ON PK Fc 1600 Hz Gain 2.3 dB Q 2.700
+ * \endcode
+ *
+ * Fc, Gain and Q specify the frequency, gain and Q factor respectively.
+ * The fourth column can be one of PK, LSC or HSC specifying peaking, low
+ * shelf and high shelf filter respectively. More often than not only peaking
+ * filters are involved.
+ *
+ * The `filters` (or channel specific `filtersX` where X is the channel between 1 and
+ * 8) can contain an array of filter specification object with the following keys:
+ *
+ *   `type` specifies the filter type, choose one from the available biquad labels.
+ *   `freq` is the frequency passed to the biquad.
+ *   `gain` is the gain passed to the biquad.
+ *   `q` is the Q passed to the biquad.
+ *
+ * This makes it possible to also use the param eq without a file and with all the
+ * available biquads.
+ *
  * ### Convolver
  *
  * The convolver can be used to apply an impulse response to a signal. It is usually used
@@ -271,7 +343,9 @@ PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
  *               computed automatically from the number of samples in the file.
  * - `tailsize` specifies the size of the tail blocks to use in the FFT.
  * - `gain`     the overall gain to apply to the IR file.
- * - `delay`    The extra delay (in samples) to add to the IR.
+ * - `delay`    The extra delay to add to the IR. A float number will be interpreted as seconds,
+ *              and integer as samples. Using the delay in seconds is independent of the graph
+ *              and IR rate and is recommended.
  * - `filename` The IR to load or create. Possible values are:
  *     - `/hilbert` creates a [hilbert function](https://en.wikipedia.org/wiki/Hilbert_transform)
  *                that can be used to phase shift the signal by +/-90 degrees. The
@@ -353,6 +427,18 @@ PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
  * It has an input port "In" and an output port "Out". It also has a "Control"
  * and "Notify" port for the control values.
  *
+ * ### Abs
+ *
+ * The abs plugin can be used to calculate the absolute value of samples.
+ *
+ * It has an input port "In" and an output port "Out".
+ *
+ * ### Sqrt
+ *
+ * The sqrt plugin can be used to calculate the square root of samples.
+ *
+ * It has an input port "In" and an output port "Out".
+ *
  * ### Exp
  *
  * The exp plugin can be used to calculate the exponential (base^x) of samples
@@ -392,6 +478,34 @@ PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
  * "Freq", "Ampl", "Offset" and "Phase" can be used to control the sine wave
  * frequency, amplitude, offset and phase.
  *
+ * ### Max
+ *
+ * Use the `max` plugin if you need to select the max value of two channels.
+ *
+ * It has two input ports "In 1" and "In 2" and one output port "Out".
+ *
+ * ### dcblock
+ *
+ * Use the `dcblock` plugin implements a [DC blocker](https://www.dsprelated.com/freebooks/filters/DC_Blocker.html).
+ *
+ * It has 8 input ports "In 1" to "In 8" and corresponding output ports "Out 1"
+ * to "Out 8". Not all ports need to be connected.
+ *
+ * It also has 1 control input port "R" that controls the DC block R factor.
+ *
+ * ### Ramp
+ *
+ * Use the `ramp` plugin creates a linear ramp from `Start` to `Stop`.
+ *
+ * It has 3 input control ports "Start", "Stop" and "Duration (s)". It also has one
+ * output port "Out". A linear ramp will be created from "Start" to "Stop" for a duration
+ * given by the "Duration (s)" control in (fractional) seconds. The current value will
+ * be stored in the output notify port "Current".
+ *
+ * The ramp output can, for example, be used as input for the `mult` plugin to create
+ * a volume ramp up or down. For more a more coarse volume ramp, the "Current" value
+ * can be used in the `linear` plugin.
+ *
  * ## SOFA filter
  *
  * There is an optional builtin SOFA filter available.
@@ -447,6 +561,84 @@ PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
  * - `Radius`    controls how far away the signal is as a value between 0 and 100.
  *               default is 1.0.
  *
+ * ## EBUR128 filter
+ *
+ * There is an optional EBU R128 filter available.
+ *
+ * ### ebur128
+ *
+ * The ebur128 plugin can be used to measure the loudness of a signal.
+ *
+ * It has 7 input ports "In FL", "In FR", "In FC", "In UNUSED", "In SL", "In SR"
+ * and "In DUAL MONO", corresponding to the different input channels for EBUR128.
+ * Not all ports need to be connected for this filter.
+ *
+ * The input signal is passed unmodified on the "Out FL", "Out FR", "Out FC",
+ * "Out UNUSED", "Out SL", "Out SR" and "Out DUAL MONO" output ports.
+ *
+ * There are 7 output control ports that contain the measured loudness information
+ * and that can be used to control the processing of the audio. Some of these ports
+ * contain values in LUFS, or "Loudness Units relative to Full Scale". These are
+ * negative values, closer to 0 is louder. You can use the lufs2gain plugin to
+ * convert this value to again to adjust a volume (See below).
+ *
+ * "Momentary LUFS" contains the momentary loudness measurement with a 400ms window
+ *                  and 75% overlap. It works mostly like an R.M.S. meter.
+ *
+ * "Shortterm LUFS" contains the shortterm loudness in LUFS over a 3 second window.
+ *
+ * "Global LUFS" contains the global integrated loudness in LUFS over the max-history
+ *               window.
+ * "Window LUFS" contains the global integrated loudness in LUFS over the max-window
+ *               window.
+ *
+ * "Range LU" contains the loudness range (LRA) in LU units.
+ *
+ * "Peak" contains the peak loudness.
+ *
+ * "True Peak" contains the true peak loudness oversampling the signal. This can more
+ *             accurately reflect the peak compared to "Peak".
+ *
+ * The node also has an optional `config` section with extra configuration:
+ *
+ *\code{.unparsed}
+ * filter.graph = {
+ *     nodes = [
+ *         {
+ *             type   = ebur128
+ *             name   = ...
+ *             label  = ebur128
+ *             config = {
+ *                 max-history = ...
+ *                 max-window = ...
+ *                 use-histogram = ...
+ *             }
+ *             ...
+ *         }
+ *     }
+ *     ...
+ * }
+ *\endcode
+ *
+ * - `max-history` the maximum history to keep in (float) seconds. Default to 10.0
+ *
+ * - `max-window` the maximum window to keep in (float) seconds. Default to 0.0
+ *                You will need to set this to some value to get "Window LUFS"
+ *                output control values.
+ *
+ * - `use-histogram` uses the histogram algorithm to calculate loudness. Defaults
+ *                   to false.
+ *
+ * ### lufs2gain
+ *
+ * The lufs2gain plugin can be used to convert LUFS control values to gain. It needs
+ * a target LUFS control input to drive the conversion.
+ *
+ * It has 2 input control ports "LUFS" and "Target LUFS" and will produce 1 output
+ * control value "Gain". This gain can be used as input for the builtin `linear`
+ * node, for example, to adust the gain.
+ *
+ *
  * ## General options
  *
  * Options with well-known behavior. Most options can be added to the global
@@ -615,149 +807,8 @@ static const struct spa_dict_item module_props[] = {
 
 #include <pipewire/pipewire.h>
 
-#define MAX_HNDL 64
-
 #define DEFAULT_RATE	48000
 
-struct fc_plugin *load_ladspa_plugin(const struct spa_support *support, uint32_t n_support,
-		struct dsp_ops *dsp, const char *path, const struct spa_dict *info);
-struct fc_plugin *load_builtin_plugin(const struct spa_support *support, uint32_t n_support,
-		struct dsp_ops *dsp, const char *path, const struct spa_dict *info);
-
-struct plugin {
-	struct spa_list link;
-	int ref;
-	char type[256];
-	char path[PATH_MAX];
-
-	struct fc_plugin *plugin;
-	struct spa_list descriptor_list;
-};
-
-struct plugin_func {
-	struct spa_list link;
-	char type[256];
-	fc_plugin_load_func *func;
-	void *hndl;
-};
-
-struct descriptor {
-	struct spa_list link;
-	int ref;
-	struct plugin *plugin;
-	char label[256];
-
-	const struct fc_descriptor *desc;
-
-	uint32_t n_input;
-	uint32_t n_output;
-	uint32_t n_control;
-	uint32_t n_notify;
-	unsigned long *input;
-	unsigned long *output;
-	unsigned long *control;
-	unsigned long *notify;
-	float *default_control;
-};
-
-struct port {
-	struct spa_list link;
-	struct node *node;
-
-	uint32_t idx;
-	unsigned long p;
-
-	struct spa_list link_list;
-	uint32_t n_links;
-	uint32_t external;
-
-	float control_data[MAX_HNDL];
-	float *audio_data[MAX_HNDL];
-};
-
-struct node {
-	struct spa_list link;
-	struct graph *graph;
-
-	struct descriptor *desc;
-
-	char name[256];
-	char *config;
-
-	struct port *input_port;
-	struct port *output_port;
-	struct port *control_port;
-	struct port *notify_port;
-
-	uint32_t n_hndl;
-	void *hndl[MAX_HNDL];
-
-	unsigned int n_deps;
-	unsigned int visited:1;
-	unsigned int disabled:1;
-	unsigned int control_changed:1;
-};
-
-struct link {
-	struct spa_list link;
-
-	struct spa_list input_link;
-	struct spa_list output_link;
-
-	struct port *output;
-	struct port *input;
-};
-
-struct graph_port {
-	const struct fc_descriptor *desc;
-	void **hndl;
-	uint32_t port;
-	unsigned next:1;
-};
-
-struct graph_hndl {
-	const struct fc_descriptor *desc;
-	void **hndl;
-};
-
-struct volume {
-	bool mute;
-	uint32_t n_volumes;
-	float volumes[SPA_AUDIO_MAX_CHANNELS];
-
-	uint32_t n_ports;
-	struct port *ports[SPA_AUDIO_MAX_CHANNELS];
-	float min[SPA_AUDIO_MAX_CHANNELS];
-	float max[SPA_AUDIO_MAX_CHANNELS];
-#define SCALE_LINEAR	0
-#define SCALE_CUBIC	1
-	int scale[SPA_AUDIO_MAX_CHANNELS];
-};
-
-struct graph {
-	struct impl *impl;
-
-	struct spa_list node_list;
-	struct spa_list link_list;
-
-	uint32_t n_input;
-	struct graph_port *input;
-
-	uint32_t n_output;
-	struct graph_port *output;
-
-	uint32_t n_hndl;
-	struct graph_hndl *hndl;
-
-	uint32_t n_control;
-	struct port **control_port;
-
-	struct volume capture_volume;
-	struct volume playback_volume;
-
-	unsigned instantiated:1;
-};
-
 struct impl {
 	struct pw_context *context;
 
@@ -770,12 +821,6 @@ struct impl {
 	struct spa_hook core_proxy_listener;
 	struct spa_hook core_listener;
 
-	uint32_t quantum_limit;
-	struct dsp_ops dsp;
-
-	struct spa_list plugin_list;
-	struct spa_list plugin_func_list;
-
 	struct pw_properties *capture_props;
 	struct pw_stream *capture;
 	struct spa_hook capture_listener;
@@ -794,16 +839,13 @@ struct impl {
 
 	long unsigned rate;
 
-	struct graph graph;
-
-	float *silence_data;
-	float *discard_data;
+	struct spa_handle *handle;
+	struct spa_filter_graph *graph;
+	struct spa_hook graph_listener;
+	uint32_t n_inputs;
+	uint32_t n_outputs;
 };
 
-static int graph_instantiate(struct graph *graph);
-static void graph_cleanup(struct graph *graph);
-
-
 static void capture_destroy(void *d)
 {
 	struct impl *impl = d;
@@ -814,18 +856,27 @@ static void capture_destroy(void *d)
 static void capture_process(void *d)
 {
 	struct impl *impl = d;
-	pw_stream_trigger_process(impl->playback);
+	int res;
+	if ((res = pw_stream_trigger_process(impl->playback)) < 0) {
+		pw_log_debug("playback trigger error: %s", spa_strerror(res));
+		while (true) {
+			struct pw_buffer *t;
+			if ((t = pw_stream_dequeue_buffer(impl->capture)) == NULL)
+				break;
+			pw_stream_queue_buffer(impl->capture, t);
+		}
+	}
 }
 
 static void playback_process(void *d)
 {
 	struct impl *impl = d;
 	struct pw_buffer *in, *out;
-	struct graph *graph = &impl->graph;
-	uint32_t i, j, insize = 0, outsize = 0, n_hndl = graph->n_hndl;
+	uint32_t i, data_size = 0;
 	int32_t stride = 0;
-	struct graph_port *port;
 	struct spa_data *bd;
+	const void *cin[128];
+	void *cout[128];
 
 	in = NULL;
 	while (true) {
@@ -845,7 +896,7 @@ static void playback_process(void *d)
 	if (in == NULL || out == NULL)
 		goto done;
 
-	for (i = 0, j = 0; i < in->buffer->n_datas; i++) {
+	for (i = 0; i < in->buffer->n_datas; i++) {
 		uint32_t offs, size;
 
 		bd = &in->buffer->datas[i];
@@ -853,44 +904,32 @@ static void playback_process(void *d)
 		offs = SPA_MIN(bd->chunk->offset, bd->maxsize);
 		size = SPA_MIN(bd->chunk->size, bd->maxsize - offs);
 
-		while (j < graph->n_input) {
-			port = &graph->input[j++];
-			if (port->desc)
-				port->desc->connect_port(*port->hndl, port->port,
-					SPA_PTROFF(bd->data, offs, void));
-			if (!port->next)
-				break;
+		cin[i] = SPA_PTROFF(bd->data, offs, void);
 
-		}
-		insize = i == 0 ? size : SPA_MIN(insize, size);
+		data_size = i == 0 ? size : SPA_MIN(data_size, size);
 		stride = SPA_MAX(stride, bd->chunk->stride);
 	}
-	outsize = insize;
+	for (; i < impl->n_inputs; i++)
+		cin[i] = NULL;
 
 	for (i = 0; i < out->buffer->n_datas; i++) {
 		bd = &out->buffer->datas[i];
 
-		outsize = SPA_MIN(outsize, bd->maxsize);
+		data_size = SPA_MIN(data_size, bd->maxsize);
 
-		port = i < graph->n_output ? &graph->output[i] : NULL;
-
-		if (port && port->desc)
-			port->desc->connect_port(*port->hndl, port->port, bd->data);
-		else
-			memset(bd->data, 0, outsize);
+		cout[i] = bd->data;
 
 		bd->chunk->offset = 0;
-		bd->chunk->size = outsize;
+		bd->chunk->size = data_size;
 		bd->chunk->stride = stride;
 	}
+	for (; i < impl->n_outputs; i++)
+		cout[i] = NULL;
 
-	pw_log_trace_fp("%p: stride:%d in:%d out:%d requested:%"PRIu64" (%"PRIu64")", impl,
-			stride, insize, outsize, out->requested, out->requested * stride);
+	pw_log_trace_fp("%p: stride:%d size:%d requested:%"PRIu64" (%"PRIu64")", impl,
+			stride, data_size, 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));
-	}
+	spa_filter_graph_process(impl->graph, cin, cout, data_size / sizeof(float));
 
 done:
 	if (in != NULL)
@@ -899,441 +938,6 @@ done:
 		pw_stream_queue_buffer(impl->playback, out);
 }
 
-static float get_default(struct impl *impl, struct descriptor *desc, uint32_t p)
-{
-	struct fc_port *port = &desc->desc->ports[p];
-	return port->def;
-}
-
-static struct node *find_node(struct graph *graph, const char *name)
-{
-	struct node *node;
-	spa_list_for_each(node, &graph->node_list, link) {
-		if (spa_streq(node->name, name))
-			return node;
-	}
-	return NULL;
-}
-
-/* find a port by name. Valid syntax is:
- *  "<node_name>:<port_name>"
- *  "<node_name>:<port_id>"
- *  "<port_name>"
- *  "<port_id>"
- *  When no node_name is given, the port is assumed in the current node.  */
-static struct port *find_port(struct node *node, const char *name, int descriptor)
-{
-	char *col, *node_name, *port_name, *str;
-	struct port *ports;
-	const struct fc_descriptor *d;
-	uint32_t i, n_ports, port_id = SPA_ID_INVALID;
-
-	str = strdupa(name);
-	col = strchr(str, ':');
-	if (col != NULL) {
-		struct node *find;
-		node_name = str;
-		port_name = col + 1;
-		*col = '\0';
-		find = find_node(node->graph, node_name);
-		if (find == NULL) {
-			/* it's possible that the : is part of the port name,
-			 * try again without splitting things up. */
-			*col = ':';
-			col = NULL;
-		} else {
-			node = find;
-		}
-	}
-	if (col == NULL) {
-		node_name = node->name;
-		port_name = str;
-	}
-	if (node == NULL)
-		return NULL;
-
-	if (!spa_atou32(port_name, &port_id, 0))
-		port_id = SPA_ID_INVALID;
-
-	if (FC_IS_PORT_INPUT(descriptor)) {
-		if (FC_IS_PORT_CONTROL(descriptor)) {
-			ports = node->control_port;
-			n_ports = node->desc->n_control;
-		} else {
-			ports = node->input_port;
-			n_ports = node->desc->n_input;
-		}
-	} else if (FC_IS_PORT_OUTPUT(descriptor)) {
-		if (FC_IS_PORT_CONTROL(descriptor)) {
-			ports = node->notify_port;
-			n_ports = node->desc->n_notify;
-		} else {
-			ports = node->output_port;
-			n_ports = node->desc->n_output;
-		}
-	} else
-		return NULL;
-
-	d = node->desc->desc;
-	for (i = 0; i < n_ports; i++) {
-		struct port *port = &ports[i];
-		if (i == port_id ||
-		    spa_streq(d->ports[port->p].name, port_name))
-			return port;
-	}
-	return NULL;
-}
-
-static struct spa_pod *get_prop_info(struct graph *graph, struct spa_pod_builder *b, uint32_t idx)
-{
-	struct impl *impl = graph->impl;
-	struct spa_pod_frame f[2];
-	struct port *port = graph->control_port[idx];
-	struct node *node = port->node;
-	struct descriptor *desc = node->desc;
-	const struct fc_descriptor *d = desc->desc;
-	struct fc_port *p = &d->ports[port->p];
-	float def, min, max;
-	char name[512];
-	uint32_t rate = impl->rate ? impl->rate : DEFAULT_RATE;
-
-	if (p->hint & FC_HINT_SAMPLE_RATE) {
-		def = p->def * rate;
-		min = p->min * rate;
-		max = p->max * rate;
-	} else {
-		def = p->def;
-		min = p->min;
-		max = p->max;
-	}
-
-	if (node->name[0] != '\0')
-		snprintf(name, sizeof(name), "%s:%s", node->name, p->name);
-	else
-		snprintf(name, sizeof(name), "%s", p->name);
-
-	spa_pod_builder_push_object(b, &f[0],
-			SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo);
-	spa_pod_builder_add (b,
-			SPA_PROP_INFO_name, SPA_POD_String(name),
-			0);
-	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.0f ? false : true);
-		} else  {
-			spa_pod_builder_push_choice(b, &f[1], SPA_CHOICE_Enum, 0);
-			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]);
-		}
-	} else if (p->hint & FC_HINT_INTEGER) {
-		if (min == max) {
-			spa_pod_builder_int(b, (int32_t)def);
-		} else {
-			spa_pod_builder_push_choice(b, &f[1], SPA_CHOICE_Range, 0);
-			spa_pod_builder_int(b, (int32_t)def);
-			spa_pod_builder_int(b, (int32_t)min);
-			spa_pod_builder_int(b, (int32_t)max);
-			spa_pod_builder_pop(b, &f[1]);
-		}
-	} else {
-		if (min == max) {
-			spa_pod_builder_float(b, def);
-		} else {
-			spa_pod_builder_push_choice(b, &f[1], SPA_CHOICE_Range, 0);
-			spa_pod_builder_float(b, def);
-			spa_pod_builder_float(b, min);
-			spa_pod_builder_float(b, max);
-			spa_pod_builder_pop(b, &f[1]);
-		}
-	}
-	spa_pod_builder_prop(b, SPA_PROP_INFO_params, 0);
-	spa_pod_builder_bool(b, true);
-	return spa_pod_builder_pop(b, &f[0]);
-}
-
-static struct spa_pod *get_props_param(struct graph *graph, struct spa_pod_builder *b)
-{
-	struct spa_pod_frame f[2];
-	uint32_t i;
-	char name[512];
-
-	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]);
-
-	for (i = 0; i < graph->n_control; i++) {
-		struct port *port = graph->control_port[i];
-		struct node *node = port->node;
-		struct descriptor *desc = node->desc;
-		const struct fc_descriptor *d = desc->desc;
-		struct fc_port *p = &d->ports[port->p];
-
-		if (node->name[0] != '\0')
-			snprintf(name, sizeof(name), "%s:%s", node->name, p->name);
-		else
-			snprintf(name, sizeof(name), "%s", p->name);
-
-		spa_pod_builder_string(b, name);
-		if (p->hint & FC_HINT_BOOLEAN) {
-			spa_pod_builder_bool(b, port->control_data[0] <= 0.0f ? false : true);
-		} else if (p->hint & FC_HINT_INTEGER) {
-			spa_pod_builder_int(b, (int32_t)port->control_data[0]);
-		} else {
-			spa_pod_builder_float(b, port->control_data[0]);
-		}
-	}
-	spa_pod_builder_pop(b, &f[1]);
-	return spa_pod_builder_pop(b, &f[0]);
-}
-
-static int port_set_control_value(struct port *port, float *value, uint32_t id)
-{
-	struct node *node = port->node;
-	struct descriptor *desc = node->desc;
-	float old;
-	bool changed;
-
-	old = port->control_data[id];
-	port->control_data[id] = value ? *value : desc->default_control[port->idx];
-	pw_log_info("control %d %d ('%s') from %f to %f", port->idx, id,
-			desc->desc->ports[port->p].name, old, port->control_data[id]);
-	changed = old != port->control_data[id];
-	node->control_changed |= changed;
-	return changed ? 1 : 0;
-}
-
-static int set_control_value(struct node *node, const char *name, float *value)
-{
-	struct port *port;
-	int count = 0;
-	uint32_t i, n_hndl;
-
-	port = find_port(node, name, FC_PORT_INPUT | FC_PORT_CONTROL);
-	if (port == NULL)
-		return -ENOENT;
-
-	/* if we don't have any instances yet, set the first control value, we will
-	 * copy to other instances later */
-	n_hndl = SPA_MAX(1u, port->node->n_hndl);
-	for (i = 0; i < n_hndl; i++)
-		count += port_set_control_value(port, value, i);
-
-	return count;
-}
-
-static int parse_params(struct graph *graph, const struct spa_pod *pod)
-{
-	struct spa_pod_parser prs;
-	struct spa_pod_frame f;
-	int res, changed = 0;
-	struct node *def_node;
-
-	def_node = spa_list_first(&graph->node_list, struct node, link);
-
-	spa_pod_parser_pod(&prs, pod);
-	if (spa_pod_parser_push_struct(&prs, &f) < 0)
-		return 0;
-
-	while (true) {
-		const char *name;
-		float value, *val = NULL;
-		double dbl_val;
-		bool bool_val;
-		int32_t int_val;
-
-		if (spa_pod_parser_get_string(&prs, &name) < 0)
-			break;
-		if (spa_pod_parser_get_float(&prs, &value) >= 0) {
-			val = &value;
-		} else if (spa_pod_parser_get_double(&prs, &dbl_val) >= 0) {
-			value = (float)dbl_val;
-			val = &value;
-		} else if (spa_pod_parser_get_int(&prs, &int_val) >= 0) {
-			value = int_val;
-			val = &value;
-		} else if (spa_pod_parser_get_bool(&prs, &bool_val) >= 0) {
-			value = bool_val ? 1.0f : 0.0f;
-			val = &value;
-		} else {
-			struct spa_pod *pod;
-			spa_pod_parser_get_pod(&prs, &pod);
-		}
-		if ((res = set_control_value(def_node, name, val)) > 0)
-			changed += res;
-	}
-	return changed;
-}
-
-static void graph_reset(struct graph *graph)
-{
-	uint32_t i;
-	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);
-		if (d->activate)
-			d->activate(*hndl->hndl);
-	}
-}
-
-static void node_control_changed(struct node *node)
-{
-	const struct fc_descriptor *d = node->desc->desc;
-	uint32_t i;
-
-	if (!node->control_changed)
-		return;
-
-	for (i = 0; i < node->n_hndl; i++) {
-		if (node->hndl[i] == NULL)
-			continue;
-		if (d->control_changed)
-			d->control_changed(node->hndl[i]);
-	}
-	node->control_changed = false;
-}
-
-static void update_props_param(struct impl *impl)
-{
-	struct graph *graph = &impl->graph;
-	uint8_t buffer[1024];
-	struct spa_pod_dynamic_builder b;
-	const struct spa_pod *params[1];
-
-	spa_pod_dynamic_builder_init(&b, buffer, sizeof(buffer), 4096);
-	params[0] = get_props_param(graph, &b.b);
-
-	pw_stream_update_params(impl->capture, params, 1);
-	spa_pod_dynamic_builder_clean(&b);
-}
-
-static int sync_volume(struct graph *graph, struct volume *vol)
-{
-	uint32_t i;
-	int res = 0;
-
-	if (vol->n_ports == 0)
-		return 0;
-	for (i = 0; i < vol->n_volumes; i++) {
-		uint32_t n_port = i % vol->n_ports, n_hndl;
-		struct port *p = vol->ports[n_port];
-		float v = vol->mute ? 0.0f : vol->volumes[i];
-		switch (vol->scale[n_port]) {
-		case SCALE_CUBIC:
-			v = cbrtf(v);
-			break;
-		}
-		v = v * (vol->max[n_port] - vol->min[n_port]) + vol->min[n_port];
-
-		n_hndl = SPA_MAX(1u, p->node->n_hndl);
-		res += port_set_control_value(p, &v, i % n_hndl);
-	}
-	return res;
-}
-
-static void param_props_changed(struct impl *impl, const struct spa_pod *param,
-		bool capture)
-{
-	struct spa_pod_object *obj = (struct spa_pod_object *) param;
-	struct spa_pod_frame f[1];
-	const struct spa_pod_prop *prop;
-	struct graph *graph = &impl->graph;
-	int changed = 0;
-	char buf[1024];
-	struct spa_pod_dynamic_builder b;
-	struct volume *vol = capture ? &graph->capture_volume :
-		&graph->playback_volume;
-	bool do_volume = false;
-
-	spa_pod_dynamic_builder_init(&b, buf, sizeof(buf), 1024);
-	spa_pod_builder_push_object(&b.b, &f[0], SPA_TYPE_OBJECT_Props, SPA_PARAM_Props);
-
-	SPA_POD_OBJECT_FOREACH(obj, prop) {
-		switch (prop->key) {
-		case SPA_PROP_params:
-			changed += parse_params(graph, &prop->value);
-			spa_pod_builder_raw_padded(&b.b, prop, SPA_POD_PROP_SIZE(prop));
-			break;
-		case SPA_PROP_mute:
-		{
-			bool mute;
-			if (spa_pod_get_bool(&prop->value, &mute) == 0) {
-				if (vol->mute != mute) {
-					vol->mute = mute;
-					do_volume = true;
-				}
-			}
-			spa_pod_builder_raw_padded(&b.b, prop, SPA_POD_PROP_SIZE(prop));
-			break;
-		}
-		case SPA_PROP_channelVolumes:
-		{
-			uint32_t i, n_vols;
-			float vols[SPA_AUDIO_MAX_CHANNELS];
-
-			if ((n_vols = spa_pod_copy_array(&prop->value, SPA_TYPE_Float, vols,
-					SPA_AUDIO_MAX_CHANNELS)) > 0) {
-				if (vol->n_volumes != n_vols)
-					do_volume = true;
-				vol->n_volumes = n_vols;
-				for (i = 0; i < n_vols; i++) {
-					float v = vols[i];
-					if (v != vol->volumes[i]) {
-						vol->volumes[i] = v;
-						do_volume = true;
-					}
-				}
-			}
-			spa_pod_builder_raw_padded(&b.b, prop, SPA_POD_PROP_SIZE(prop));
-			break;
-		}
-		case SPA_PROP_softVolumes:
-		case SPA_PROP_softMute:
-			break;
-		default:
-			spa_pod_builder_raw_padded(&b.b, prop, SPA_POD_PROP_SIZE(prop));
-			break;
-		}
-	}
-	if (do_volume && vol->n_ports != 0) {
-		float soft_vols[SPA_AUDIO_MAX_CHANNELS];
-		uint32_t i;
-
-		for (i = 0; i < vol->n_volumes; i++)
-			soft_vols[i] = (vol->mute || vol->volumes[i] == 0.0f) ? 0.0f : 1.0f;
-
-		spa_pod_builder_prop(&b.b, SPA_PROP_softMute, 0);
-		spa_pod_builder_bool(&b.b, vol->mute);
-		spa_pod_builder_prop(&b.b, SPA_PROP_softVolumes, 0);
-		spa_pod_builder_array(&b.b, sizeof(float), SPA_TYPE_Float,
-				vol->n_volumes, soft_vols);
-		param = spa_pod_builder_pop(&b.b, &f[0]);
-
-		sync_volume(graph, vol);
-		pw_stream_set_param(capture ? impl->capture :
-				impl->playback, SPA_PARAM_Props, param);
-	}
-
-	spa_pod_dynamic_builder_clean(&b);
-
-	if (changed > 0) {
-		struct node *node;
-
-		spa_list_for_each(node, &graph->node_list, link)
-			node_control_changed(node);
-
-		update_props_param(impl);
-	}
-
-}
-
 static void param_latency_changed(struct impl *impl, const struct spa_pod *param)
 {
 	struct spa_latency_info latency;
@@ -1372,14 +976,14 @@ static void state_changed(void *data, enum pw_stream_state old,
 		enum pw_stream_state state, const char *error)
 {
 	struct impl *impl = data;
-	struct graph *graph = &impl->graph;
+	struct spa_filter_graph *graph = impl->graph;
 	int res;
 
 	switch (state) {
 	case PW_STREAM_STATE_PAUSED:
 		pw_stream_flush(impl->playback, false);
 		pw_stream_flush(impl->capture, false);
-		graph_reset(graph);
+		spa_filter_graph_reset(graph);
 		break;
 	case PW_STREAM_STATE_UNCONNECTED:
 		pw_log_info("module %p: unconnected", impl);
@@ -1399,9 +1003,13 @@ static void state_changed(void *data, enum pw_stream_state old,
 			goto error;
 		}
 		if (impl->rate != target) {
+			char rate[64];
 			impl->rate = target;
-			graph_cleanup(graph);
-			if ((res = graph_instantiate(graph)) < 0)
+			snprintf(rate, sizeof(rate), "%lu", impl->rate);
+			spa_filter_graph_deactivate(graph);
+			if ((res = spa_filter_graph_activate(graph,
+					&SPA_DICT_ITEMS(
+						SPA_DICT_ITEM(SPA_KEY_AUDIO_RATE, rate)))) < 0)
 				goto error;
 		}
 		break;
@@ -1431,7 +1039,7 @@ static void param_changed(void *data, uint32_t id, const struct spa_pod *param,
 		bool capture)
 {
 	struct impl *impl = data;
-	struct graph *graph = &impl->graph;
+	struct spa_filter_graph *graph = impl->graph;
 	int res;
 
 	switch (id) {
@@ -1440,7 +1048,7 @@ static void param_changed(void *data, uint32_t id, const struct spa_pod *param,
 		struct spa_audio_info_raw info;
 		spa_zero(info);
 		if (param == NULL) {
-			graph_cleanup(graph);
+			spa_filter_graph_deactivate(graph);
 			impl->rate = 0;
 		} else {
 			if ((res = spa_format_audio_raw_parse(param, &info)) < 0)
@@ -1451,7 +1059,9 @@ static void param_changed(void *data, uint32_t id, const struct spa_pod *param,
 	}
 	case SPA_PARAM_Props:
 		if (param != NULL)
-			param_props_changed(impl, param, capture);
+			spa_filter_graph_set_props(impl->graph,
+					capture ? SPA_DIRECTION_INPUT : SPA_DIRECTION_OUTPUT, param);
+
 		break;
 	case SPA_PARAM_Latency:
 		param_latency_changed(impl, param);
@@ -1509,7 +1119,7 @@ static int setup_streams(struct impl *impl)
 	struct pw_array offsets;
 	const struct spa_pod **params = NULL;
 	struct spa_pod_dynamic_builder b;
-	struct graph *graph = &impl->graph;
+	struct spa_filter_graph *graph = impl->graph;
 
 	impl->capture = pw_stream_new(impl->core,
 			"filter capture", impl->capture_props);
@@ -1542,15 +1152,16 @@ static int setup_streams(struct impl *impl)
 	spa_format_audio_raw_build(&b.b,
 			SPA_PARAM_EnumFormat, &impl->capture_info);
 
-	for (i = 0; i < graph->n_control; i++) {
+	for (i = 0;; i++) {
 		if ((offs = pw_array_add(&offsets, sizeof(uint32_t))) != NULL)
 			*offs = b.b.state.offset;
-		get_prop_info(graph, &b.b, i);
+		if (spa_filter_graph_enum_prop_info(graph, i, &b.b, NULL) != 1)
+			break;
 	}
 
 	if ((offs = pw_array_add(&offsets, sizeof(uint32_t))) != NULL)
 		*offs = b.b.state.offset;
-	get_props_param(graph, &b.b);
+	spa_filter_graph_get_props(graph, &b.b, NULL);
 
 	n_params = pw_array_get_len(&offsets, uint32_t);
 	if (n_params == 0) {
@@ -1601,1256 +1212,62 @@ done:
 	return res < 0 ? res : 0;
 }
 
-static uint32_t count_array(struct spa_json *json)
+static void copy_position(struct spa_audio_info_raw *dst, const struct spa_audio_info_raw *src)
 {
-	struct spa_json it = *json;
-	char v[256];
-	uint32_t count = 0;
-	while (spa_json_get_string(&it, v, sizeof(v)) > 0)
-		count++;
-	return count;
-}
-
-static void plugin_unref(struct plugin *hndl)
-{
-	if (--hndl->ref > 0)
-		return;
-
-	fc_plugin_free(hndl->plugin);
-
-	spa_list_remove(&hndl->link);
-	free(hndl);
-}
-
-
-static struct plugin_func *add_plugin_func(struct impl *impl, const char *type,
-		fc_plugin_load_func *func, void *hndl)
-{
-	struct plugin_func *pl;
-
-	pl = calloc(1, sizeof(*pl));
-	if (pl == NULL)
-		return NULL;
-
-	snprintf(pl->type, sizeof(pl->type), "%s", type);
-	pl->func = func;
-	pl->hndl = hndl;
-	spa_list_append(&impl->plugin_func_list, &pl->link);
-	return pl;
-}
-
-static void free_plugin_func(struct plugin_func *pl)
-{
-	spa_list_remove(&pl->link);
-	if (pl->hndl)
-		dlclose(pl->hndl);
-	free(pl);
-}
-
-static fc_plugin_load_func *find_plugin_func(struct impl *impl, const char *type)
-{
-	fc_plugin_load_func *func = NULL;
-	void *hndl = NULL;
-	int res;
-	struct plugin_func *pl;
-	char module[PATH_MAX];
-	const char *module_dir;
-	const char *state = NULL, *p;
-	size_t len;
-
-	spa_list_for_each(pl, &impl->plugin_func_list, link) {
-		if (spa_streq(pl->type, type))
-			return pl->func;
-	}
-	module_dir = getenv("PIPEWIRE_MODULE_DIR");
-	if (module_dir == NULL)
-		module_dir = MODULEDIR;
-	pw_log_debug("moduledir set to: %s", module_dir);
-
-	while ((p = pw_split_walk(module_dir, ":", &len, &state))) {
-		if ((res = spa_scnprintf(module, sizeof(module),
-				"%.*s/libpipewire-module-filter-chain-%s.so",
-						(int)len, p, type)) <= 0)
-			continue;
-
-		hndl = dlopen(module, RTLD_NOW | RTLD_LOCAL);
-		if (hndl != NULL)
-			break;
-
-		pw_log_debug("open plugin module %s failed: %s", module, dlerror());
-	}
-	if (hndl == NULL) {
-		errno = ENOENT;
-		return NULL;
-	}
-	func = dlsym(hndl, FC_PLUGIN_LOAD_FUNC);
-	if (func != NULL) {
-		pw_log_info("opened plugin module %s", module);
-		pl = add_plugin_func(impl, type, func, hndl);
-		if (pl == NULL)
-			goto error_close;
-	} else {
-		errno = ENOSYS;
-		pw_log_error("%s is not a filter chain plugin: %m", module);
-		goto error_close;
-	}
-	return func;
-
-error_close:
-	dlclose(hndl);
-	return NULL;
-}
-
-static struct plugin *plugin_load(struct impl *impl, const char *type, const char *path)
-{
-	struct fc_plugin *pl = NULL;
-	struct plugin *hndl;
-	const struct spa_support *support;
-	uint32_t n_support;
-	fc_plugin_load_func *plugin_func;
-
-	spa_list_for_each(hndl, &impl->plugin_list, link) {
-		if (spa_streq(hndl->type, type) &&
-		    spa_streq(hndl->path, path)) {
-			hndl->ref++;
-			return hndl;
-		}
-	}
-	support = pw_context_get_support(impl->context, &n_support);
-
-	plugin_func = find_plugin_func(impl, type);
-	if (plugin_func == NULL) {
-		pw_log_error("can't load plugin type '%s': %m", type);
-		pl = NULL;
-	} else {
-		pl = plugin_func(support, n_support, &impl->dsp, path, &impl->props->dict);
-	}
-	if (pl == NULL)
-		goto exit;
-
-	hndl = calloc(1, sizeof(*hndl));
-	if (!hndl)
-		return NULL;
-
-	hndl->ref = 1;
-	snprintf(hndl->type, sizeof(hndl->type), "%s", type);
-	snprintf(hndl->path, sizeof(hndl->path), "%s", path);
-
-	pw_log_info("successfully opened '%s':'%s'", type, path);
-
-	hndl->plugin = pl;
-
-	spa_list_init(&hndl->descriptor_list);
-	spa_list_append(&impl->plugin_list, &hndl->link);
-
-	return hndl;
-exit:
-	return NULL;
-}
-
-static void descriptor_unref(struct descriptor *desc)
-{
-	if (--desc->ref > 0)
-		return;
-
-	spa_list_remove(&desc->link);
-	plugin_unref(desc->plugin);
-	if (desc->desc)
-		fc_descriptor_free(desc->desc);
-	free(desc->input);
-	free(desc->output);
-	free(desc->control);
-	free(desc->default_control);
-	free(desc->notify);
-	free(desc);
-}
-
-static struct descriptor *descriptor_load(struct impl *impl, const char *type,
-		const char *plugin, const char *label)
-{
-	struct plugin *hndl;
-	struct descriptor *desc;
-	const struct fc_descriptor *d;
-	uint32_t i, n_input, n_output, n_control, n_notify;
-	unsigned long p;
-	int res;
-
-	if ((hndl = plugin_load(impl, type, plugin)) == NULL)
-		return NULL;
-
-	spa_list_for_each(desc, &hndl->descriptor_list, link) {
-		if (spa_streq(desc->label, label)) {
-			desc->ref++;
-
-			/*
-			 * since ladspa_handle_load() increments the reference count of the handle,
-			 * if the descriptor is found, then the handle's reference count
-			 * has already been incremented to account for the descriptor,
-			 * so we need to unref handle here since we're merely reusing
-			 * thedescriptor, not creating a new one
-			 */
-			plugin_unref(hndl);
-			return desc;
-		}
+	if (SPA_FLAG_IS_SET(dst->flags, SPA_AUDIO_FLAG_UNPOSITIONED) &&
+	    !SPA_FLAG_IS_SET(src->flags, SPA_AUDIO_FLAG_UNPOSITIONED)) {
+		for (uint32_t i = 0; i < src->channels; i++)
+			dst->position[i] = src->position[i];
+		SPA_FLAG_CLEAR(dst->flags, SPA_AUDIO_FLAG_UNPOSITIONED);
 	}
-
-	desc = calloc(1, sizeof(*desc));
-	desc->ref = 1;
-	desc->plugin = hndl;
-	spa_list_init(&desc->link);
-
-	if ((d = hndl->plugin->make_desc(hndl->plugin, label)) == NULL) {
-		pw_log_error("cannot find label %s", label);
-		res = -ENOENT;
-		goto exit;
-	}
-	desc->desc = d;
-	snprintf(desc->label, sizeof(desc->label), "%s", label);
-
-	n_input = n_output = n_control = n_notify = 0;
-	for (p = 0; p < d->n_ports; p++) {
-		struct fc_port *fp = &d->ports[p];
-		if (FC_IS_PORT_AUDIO(fp->flags)) {
-			if (FC_IS_PORT_INPUT(fp->flags))
-				n_input++;
-			else if (FC_IS_PORT_OUTPUT(fp->flags))
-				n_output++;
-		} else if (FC_IS_PORT_CONTROL(fp->flags)) {
-			if (FC_IS_PORT_INPUT(fp->flags))
-				n_control++;
-			else if (FC_IS_PORT_OUTPUT(fp->flags))
-				n_notify++;
-		}
-	}
-	desc->input = calloc(n_input, sizeof(unsigned long));
-	desc->output = calloc(n_output, sizeof(unsigned long));
-	desc->control = calloc(n_control, sizeof(unsigned long));
-	desc->default_control = calloc(n_control, sizeof(float));
-	desc->notify = calloc(n_notify, sizeof(unsigned long));
-
-	for (p = 0; p < d->n_ports; p++) {
-		struct fc_port *fp = &d->ports[p];
-
-		if (FC_IS_PORT_AUDIO(fp->flags)) {
-			if (FC_IS_PORT_INPUT(fp->flags)) {
-				pw_log_info("using port %lu ('%s') as input %d", p,
-						fp->name, desc->n_input);
-				desc->input[desc->n_input++] = p;
-			}
-			else if (FC_IS_PORT_OUTPUT(fp->flags)) {
-				pw_log_info("using port %lu ('%s') as output %d", p,
-						fp->name, desc->n_output);
-				desc->output[desc->n_output++] = p;
-			}
-		} else if (FC_IS_PORT_CONTROL(fp->flags)) {
-			if (FC_IS_PORT_INPUT(fp->flags)) {
-				pw_log_info("using port %lu ('%s') as control %d", p,
-						fp->name, desc->n_control);
-				desc->control[desc->n_control++] = p;
-			}
-			else if (FC_IS_PORT_OUTPUT(fp->flags)) {
-				pw_log_info("using port %lu ('%s') as notify %d", p,
-						fp->name, desc->n_notify);
-				desc->notify[desc->n_notify++] = p;
-			}
-		}
-	}
-	if (desc->n_input == 0 && desc->n_output == 0) {
-		pw_log_error("plugin has no input and no output ports");
-		res = -ENOTSUP;
-		goto exit;
-	}
-	for (i = 0; i < desc->n_control; i++) {
-		p = desc->control[i];
-		desc->default_control[i] = get_default(impl, desc, p);
-		pw_log_info("control %d ('%s') default to %f", i,
-				d->ports[p].name, desc->default_control[i]);
-	}
-	spa_list_append(&hndl->descriptor_list, &desc->link);
-
-	return desc;
-
-exit:
-	descriptor_unref(desc);
-	errno = -res;
-	return NULL;
 }
 
-/**
- * {
- *   ...
- * }
- */
-static int parse_config(struct node *node, struct spa_json *config)
+static void graph_info(void *object, const struct spa_filter_graph_info *info)
 {
-	const char *val, *s = config->cur;
-	int res = 0, len;
-	struct spa_error_location loc;
-
-	if ((len = spa_json_next(config, &val)) <= 0) {
-		res = -EINVAL;
-		goto done;
-	}
-	if (spa_json_is_null(val, len))
-		goto done;
-
-	if (spa_json_is_container(val, len)) {
-		len = spa_json_container_len(config, val, len);
-		if (len == 0) {
-			res = -EINVAL;
-			goto done;
-		}
-	}
-	if ((node->config = malloc(len+1)) == NULL) {
-		res = -errno;
-		goto done;
-	}
-
-	spa_json_parse_stringn(val, len, node->config, len+1);
-done:
-	if (spa_json_get_error(config, s, &loc))
-		spa_debug_log_error_location(pw_log_get(), SPA_LOG_LEVEL_WARN,
-				&loc, "error: %s", loc.reason);
-	return res;
-}
-
-/**
- * {
- *   "Reverb tail" = 2.0
- *   ...
- * }
- */
-static int parse_control(struct node *node, struct spa_json *control)
-{
-	char key[256];
-
-	while (spa_json_get_string(control, key, sizeof(key)) > 0) {
-		float fl;
-		const char *val;
-		int res, len;
-
-		if ((len = spa_json_next(control, &val)) < 0)
-			break;
-
-		if (spa_json_parse_float(val, len, &fl) <= 0) {
-			pw_log_warn("control '%s' expects a number, ignoring", key);
-		}
-		else if ((res = set_control_value(node, key, &fl)) < 0) {
-			pw_log_warn("control '%s' can not be set: %s", key, spa_strerror(res));
-		}
-	}
-	return 0;
-}
-
-/**
- * output = [name:][portname]
- * input = [name:][portname]
- * ...
- */
-static int parse_link(struct graph *graph, struct spa_json *json)
-{
-	char key[256];
-	char output[256] = "";
-	char input[256] = "";
-	const char *val;
-	struct node *def_in_node, *def_out_node;
-	struct port *in_port, *out_port;
-	struct link *link;
-
-	if (spa_list_is_empty(&graph->node_list)) {
-		pw_log_error("can't make links in graph without nodes");
-		return -EINVAL;
-	}
-
-	while (spa_json_get_string(json, key, sizeof(key)) > 0) {
-		if (spa_streq(key, "output")) {
-			if (spa_json_get_string(json, output, sizeof(output)) <= 0) {
-				pw_log_error("output expects a string");
-				return -EINVAL;
-			}
-		}
-		else if (spa_streq(key, "input")) {
-			if (spa_json_get_string(json, input, sizeof(input)) <= 0) {
-				pw_log_error("input expects a string");
-				return -EINVAL;
-			}
-		}
-		else {
-			pw_log_error("unexpected link key '%s'", key);
-			if (spa_json_next(json, &val) < 0)
-				break;
-		}
-	}
-	def_out_node = spa_list_first(&graph->node_list, struct node, link);
-	def_in_node = spa_list_last(&graph->node_list, struct node, link);
-
-	out_port = find_port(def_out_node, output, FC_PORT_OUTPUT);
-	in_port = find_port(def_in_node, input, FC_PORT_INPUT);
-
-	if (out_port == NULL && out_port == NULL) {
-		/* try control ports */
-		out_port = find_port(def_out_node, output, FC_PORT_OUTPUT | FC_PORT_CONTROL);
-		in_port = find_port(def_in_node, input, FC_PORT_INPUT | FC_PORT_CONTROL);
-	}
-	if (in_port == NULL || out_port == NULL) {
-		if (out_port == NULL)
-			pw_log_error("unknown output port %s", output);
-		if (in_port == NULL)
-			pw_log_error("unknown input port %s", input);
-		return -ENOENT;
-	}
-
-	if (in_port->n_links > 0) {
-		pw_log_info("Can't have more than 1 link to %s, use a mixer", input);
-		return -ENOTSUP;
-	}
-
-	if ((link = calloc(1, sizeof(*link))) == NULL)
-		return -errno;
-
-	link->output = out_port;
-	link->input = in_port;
-
-	pw_log_info("linking %s:%s -> %s:%s",
-			out_port->node->name,
-			out_port->node->desc->desc->ports[out_port->p].name,
-			in_port->node->name,
-			in_port->node->desc->desc->ports[in_port->p].name);
-
-	spa_list_append(&out_port->link_list, &link->output_link);
-	out_port->n_links++;
-	spa_list_append(&in_port->link_list, &link->input_link);
-	in_port->n_links++;
-
-	in_port->node->n_deps++;
-
-	spa_list_append(&graph->link_list, &link->link);
-
-	return 0;
-}
-
-static void link_free(struct link *link)
-{
-	spa_list_remove(&link->input_link);
-	link->input->n_links--;
-	link->input->node->n_deps--;
-	spa_list_remove(&link->output_link);
-	link->output->n_links--;
-	spa_list_remove(&link->link);
-	free(link);
-}
-
-/**
- * {
- *   control = [name:][portname]
- *   min = <float, default 0.0>
- *   max = <float, default 1.0>
- *   scale = <string, default "linear", options "linear","cubic">
- * }
- */
-static int parse_volume(struct graph *graph, struct spa_json *json, bool capture)
-{
-	char key[256];
-	char control[256] = "";
-	char scale[64] = "linear";
-	float min = 0.0f, max = 1.0f;
-	const char *val;
-	struct node *def_control;
-	struct port *port;
-	struct volume *vol = capture ? &graph->capture_volume :
-		&graph->playback_volume;
-
-	if (spa_list_is_empty(&graph->node_list)) {
-		pw_log_error("can't set volume in graph without nodes");
-		return -EINVAL;
-	}
-	while (spa_json_get_string(json, key, sizeof(key)) > 0) {
-		if (spa_streq(key, "control")) {
-			if (spa_json_get_string(json, control, sizeof(control)) <= 0) {
-				pw_log_error("control expects a string");
-				return -EINVAL;
-			}
-		}
-		else if (spa_streq(key, "min")) {
-			if (spa_json_get_float(json, &min) <= 0) {
-				pw_log_error("min expects a float");
-				return -EINVAL;
-			}
-		}
-		else if (spa_streq(key, "max")) {
-			if (spa_json_get_float(json, &max) <= 0) {
-				pw_log_error("max expects a float");
-				return -EINVAL;
-			}
-		}
-		else if (spa_streq(key, "scale")) {
-			if (spa_json_get_string(json, scale, sizeof(scale)) <= 0) {
-				pw_log_error("scale expects a string");
-				return -EINVAL;
-			}
-		}
-		else {
-			pw_log_error("unexpected volume key '%s'", key);
-			if (spa_json_next(json, &val) < 0)
-				break;
-		}
-	}
-	if (capture)
-		def_control = spa_list_first(&graph->node_list, struct node, link);
-	else
-		def_control = spa_list_last(&graph->node_list, struct node, link);
-
-	port = find_port(def_control, control, FC_PORT_INPUT | FC_PORT_CONTROL);
-	if (port == NULL) {
-		pw_log_error("unknown control port %s", control);
-		return -ENOENT;
-	}
-	if (vol->n_ports >= SPA_AUDIO_MAX_CHANNELS) {
-		pw_log_error("too many volume controls");
-		return -ENOSPC;
-	}
-	if (spa_streq(scale, "linear")) {
-		vol->scale[vol->n_ports] = SCALE_LINEAR;
-	} else if (spa_streq(scale, "cubic")) {
-		vol->scale[vol->n_ports] = SCALE_CUBIC;
-	} else {
-		pw_log_error("Invalid scale value '%s', use one of linear or cubic", scale);
-		return -EINVAL;
-	}
-	pw_log_info("volume %d: \"%s:%s\" min:%f max:%f scale:%s", vol->n_ports, port->node->name,
-			port->node->desc->desc->ports[port->p].name, min, max, scale);
-
-	vol->ports[vol->n_ports] = port;
-	vol->min[vol->n_ports] = min;
-	vol->max[vol->n_ports] = max;
-	vol->n_ports++;
-
-	return 0;
-}
-
-/**
- * type = ladspa
- * name = rev
- * plugin = g2reverb
- * label = G2reverb
- * config = {
- *     ...
- * }
- * control = {
- *     ...
- * }
- */
-static int load_node(struct graph *graph, struct spa_json *json)
-{
-	struct spa_json control, config;
-	struct descriptor *desc;
-	struct node *node;
-	const char *val;
-	char key[256];
-	char type[256] = "";
-	char name[256] = "";
-	char plugin[256] = "";
-	char label[256] = "";
-	bool have_control = false;
-	bool have_config = false;
-	uint32_t i;
-	int res;
-
-	while (spa_json_get_string(json, key, sizeof(key)) > 0) {
-		if (spa_streq("type", key)) {
-			if (spa_json_get_string(json, type, sizeof(type)) <= 0) {
-				pw_log_error("type expects a string");
-				return -EINVAL;
-			}
-		} else if (spa_streq("name", key)) {
-			if (spa_json_get_string(json, name, sizeof(name)) <= 0) {
-				pw_log_error("name expects a string");
-				return -EINVAL;
-			}
-		} else if (spa_streq("plugin", key)) {
-			if (spa_json_get_string(json, plugin, sizeof(plugin)) <= 0) {
-				pw_log_error("plugin expects a string");
-				return -EINVAL;
-			}
-		} else if (spa_streq("label", key)) {
-			if (spa_json_get_string(json, label, sizeof(label)) <= 0) {
-				pw_log_error("label expects a string");
-				return -EINVAL;
-			}
-		} else if (spa_streq("control", key)) {
-			if (spa_json_enter_object(json, &control) <= 0) {
-				pw_log_error("control expects an object");
-				return -EINVAL;
-			}
-			have_control = true;
-		} else if (spa_streq("config", key)) {
-			config = SPA_JSON_SAVE(json);
-			have_config = true;
-			if (spa_json_next(json, &val) < 0)
-				break;
-		} else {
-			pw_log_warn("unexpected node key '%s'", key);
-			if (spa_json_next(json, &val) < 0)
-				break;
-		}
-	}
-	if (spa_streq(type, "builtin"))
-		snprintf(plugin, sizeof(plugin), "%s", "builtin");
-	else if (spa_streq(type, "")) {
-		pw_log_error("missing plugin type");
-		return -EINVAL;
-	}
-
-	pw_log_info("loading type:%s plugin:%s label:%s", type, plugin, label);
-
-	if ((desc = descriptor_load(graph->impl, type, plugin, label)) == NULL)
-		return -errno;
-
-	node = calloc(1, sizeof(*node));
-	if (node == NULL)
-		return -errno;
-
-	node->graph = graph;
-	node->desc = desc;
-	snprintf(node->name, sizeof(node->name), "%s", name);
-
-	node->input_port = calloc(desc->n_input, sizeof(struct port));
-	node->output_port = calloc(desc->n_output, sizeof(struct port));
-	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;
-		port->idx = i;
-		port->external = SPA_ID_INVALID;
-		port->p = desc->input[i];
-		spa_list_init(&port->link_list);
-	}
-	for (i = 0; i < desc->n_output; i++) {
-		struct port *port = &node->output_port[i];
-		port->node = node;
-		port->idx = i;
-		port->external = SPA_ID_INVALID;
-		port->p = desc->output[i];
-		spa_list_init(&port->link_list);
-	}
-	for (i = 0; i < desc->n_control; i++) {
-		struct port *port = &node->control_port[i];
-		port->node = node;
-		port->idx = i;
-		port->external = SPA_ID_INVALID;
-		port->p = desc->control[i];
-		spa_list_init(&port->link_list);
-		port->control_data[0] = desc->default_control[i];
-	}
-	for (i = 0; i < desc->n_notify; i++) {
-		struct port *port = &node->notify_port[i];
-		port->node = node;
-		port->idx = i;
-		port->external = SPA_ID_INVALID;
-		port->p = desc->notify[i];
-		spa_list_init(&port->link_list);
-	}
-	if (have_config)
-		if ((res = parse_config(node, &config)) < 0)
-			pw_log_warn("error parsing config: %s", spa_strerror(res));
-	if (have_control)
-		parse_control(node, &control);
-
-	spa_list_append(&graph->node_list, &node->link);
-
-	return 0;
-}
-
-static void node_cleanup(struct node *node)
-{
-	const struct fc_descriptor *d = node->desc->desc;
-	uint32_t i;
-
-	for (i = 0; i < node->n_hndl; i++) {
-		if (node->hndl[i] == NULL)
-			continue;
-		pw_log_info("cleanup %s %d", d->name, i);
-		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, uint32_t max_samples)
-{
-	float *data;
-	if ((data = port->audio_data[i]) == NULL) {
-		data = calloc(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);
-	free(node->control_port);
-	free(node->notify_port);
-	free(node->config);
-	free(node);
-}
-
-static void graph_cleanup(struct graph *graph)
-{
-	struct node *node;
-	if (!graph->instantiated)
-		return;
-	graph->instantiated = false;
-	spa_list_for_each(node, &graph->node_list, link)
-		node_cleanup(node);
-}
-
-static int graph_instantiate(struct graph *graph)
-{
-	struct impl *impl = graph->impl;
-	struct node *node;
-	struct port *port;
-	struct link *link;
-	struct descriptor *desc;
-	const struct fc_descriptor *d;
-	const struct fc_plugin *p;
-	uint32_t i, j, max_samples = impl->quantum_limit;
-	int res;
-	float *sd, *dd;
-
-	if (graph->instantiated)
-		return 0;
-
-	graph->instantiated = true;
-
-	/* first make instances */
-	spa_list_for_each(node, &graph->node_list, link) {
-		node_cleanup(node);
-
-		desc = node->desc;
-		d = desc->desc;
-		p = desc->plugin->plugin;
-
-		for (i = 0; i < node->n_hndl; i++) {
-			pw_log_info("instantiate %s %d rate:%lu", d->name, i, impl->rate);
-			errno = EINVAL;
-			if ((node->hndl[i] = d->instantiate(p, d, impl->rate, i, node->config)) == NULL) {
-				pw_log_error("cannot create plugin instance %d rate:%lu: %m", i, impl->rate);
-				res = -errno;
-				goto error;
-			}
-		}
-	}
-
-	/* then link ports and activate */
-	spa_list_for_each(node, &graph->node_list, link) {
-		desc = node->desc;
-		d = desc->desc;
-		if (d->flags & FC_DESCRIPTOR_SUPPORTS_NULL_DATA) {
-			sd = dd = NULL;
-		}
-		else {
-			sd = impl->silence_data;
-			dd = impl->discard_data;
-		}
-		for (i = 0; i < node->n_hndl; i++) {
-			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, max_samples)) < 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, max_samples)) < 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[i]);
-
-				spa_list_for_each(link, &port->link_list, input_link) {
-					struct port *peer = link->output;
-					pw_log_info("connect control port %s[%d]:%s %p",
-							node->name, i, d->ports[port->p].name,
-							&peer->control_data[i]);
-					d->connect_port(node->hndl[i], port->p, &peer->control_data[i]);
-				}
-			}
-			for (j = 0; j < desc->n_notify; j++) {
-				port = &node->notify_port[j];
-				pw_log_info("connect notify port %s[%d]:%s %p",
-						node->name, i, d->ports[port->p].name,
-						&port->control_data[i]);
-				d->connect_port(node->hndl[i], port->p, &port->control_data[i]);
-			}
-			if (d->activate)
-				d->activate(node->hndl[i]);
-			if (node->control_changed && d->control_changed)
-				d->control_changed(node->hndl[i]);
-		}
-	}
-	update_props_param(impl);
-	return 0;
-error:
-	graph_cleanup(graph);
-	return res;
-}
-
-/* any default values for the controls are set in the first instance
- * of the control data. Duplicate this to the other instances now. */
-static void setup_node_controls(struct node *node)
-{
-	uint32_t i, j;
-	uint32_t n_hndl = node->n_hndl;
-	uint32_t n_ports = node->desc->n_control;
-	struct port *ports = node->control_port;
-
-	for (i = 0; i < n_ports; i++) {
-		struct port *port = &ports[i];
-		for (j = 1; j < n_hndl; j++)
-			port->control_data[j] = port->control_data[0];
-	}
-}
-
-static struct node *find_next_node(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;
-}
-
-static int setup_graph(struct graph *graph, struct spa_json *inputs, struct spa_json *outputs)
-{
-	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;
-	struct descriptor *desc;
-	const struct fc_descriptor *d;
-	char v[256];
-
-	first = spa_list_first(&graph->node_list, struct node, link);
-	last = spa_list_last(&graph->node_list, struct node, link);
-
-	/* calculate the number of inputs and outputs into the graph.
-	 * If we have a list of inputs/outputs, just count them. Otherwise
-	 * we count all input ports of the first node and all output
-	 * ports of the last node */
-	if (inputs != NULL) {
-		n_input = count_array(inputs);
-	} else {
-		n_input = first->desc->n_input;
-	}
-	if (outputs != NULL) {
-		n_output = count_array(outputs);
-	} else {
-		n_output = last->desc->n_output;
-	}
-	if (n_input == 0) {
-		pw_log_error("no inputs");
-		res = -EINVAL;
-		goto error;
-	}
-	if (n_output == 0) {
-		pw_log_error("no outputs");
-		res = -EINVAL;
-		goto error;
-	}
-
+	struct impl *impl = object;
 	if (impl->capture_info.channels == 0)
-		impl->capture_info.channels = n_input;
+		impl->capture_info.channels = info->n_inputs;
 	if (impl->playback_info.channels == 0)
-		impl->playback_info.channels = n_output;
-
-	/* compare to the requested number of channels and duplicate the
-	 * graph n_hndl times when needed. */
-	n_hndl = impl->capture_info.channels / n_input;
-	if (n_hndl != impl->playback_info.channels / n_output) {
-		pw_log_error("invalid channels. The capture stream has %1$d channels and "
-				"the filter has %2$d inputs. The playback stream has %3$d channels "
-				"and the filter has %4$d outputs. capture:%1$d / input:%2$d != "
-				"playback:%3$d / output:%4$d. Check inputs and outputs objects.",
-				impl->capture_info.channels, n_input,
-				impl->playback_info.channels, n_output);
-		res = -EINVAL;
-		goto error;
-	}
-	if (n_hndl > MAX_HNDL) {
-		pw_log_error("too many channels. %d > %d", n_hndl, MAX_HNDL);
-		res = -EINVAL;
-		goto error;
-	}
-	if (n_hndl == 0) {
-		n_hndl = 1;
-		pw_log_warn("The capture stream has %1$d channels and "
-				"the filter has %2$d inputs. The playback stream has %3$d channels "
-				"and the filter has %4$d outputs. Some filter ports will be "
-				"unconnected..",
-				impl->capture_info.channels, n_input,
-				impl->playback_info.channels, n_output);
-	}
-	pw_log_info("using %d instances %d %d", n_hndl, n_input, n_output);
-
-	/* now go over all nodes and create instances. */
-	n_control = 0;
-	n_nodes = 0;
-	spa_list_for_each(node, &graph->node_list, link) {
-		node->n_hndl = n_hndl;
-		desc = node->desc;
-		n_control += desc->n_control;
-		n_nodes++;
-		setup_node_controls(node);
-	}
-	graph->n_input = 0;
-	graph->input = calloc(n_input * 16 * n_hndl, sizeof(struct graph_port));
-	graph->n_output = 0;
-	graph->output = calloc(n_output * n_hndl, sizeof(struct graph_port));
-
-	/* now collect all input and output ports for all the handles. */
-	for (i = 0; i < n_hndl; i++) {
-		if (inputs == NULL) {
-			desc = first->desc;
-			d = desc->desc;
-			for (j = 0; j < desc->n_input; j++) {
-				gp = &graph->input[graph->n_input++];
-				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->port = desc->input[j];
-			}
-		} else {
-			struct spa_json it = *inputs;
-			while (spa_json_get_string(&it, v, sizeof(v)) > 0) {
-				if (spa_streq(v, "null")) {
-					gp = &graph->input[graph->n_input++];
-					gp->desc = NULL;
-					pw_log_info("ignore input port %d", graph->n_input);
-				} else if ((port = find_port(first, v, FC_PORT_INPUT)) == NULL) {
-					res = -ENOENT;
-					pw_log_error("input port %s not found", v);
-					goto error;
-				} else {
-					bool disabled = false;
-
-					desc = port->node->desc;
-					d = desc->desc;
-					if (i == 0 && port->external != SPA_ID_INVALID) {
-						pw_log_error("input port %s[%d]:%s already used as input %d, use mixer",
-							port->node->name, i, d->ports[port->p].name,
-							port->external);
-						res = -EBUSY;
-						goto error;
-					}
-					if (port->n_links > 0) {
-						pw_log_error("input port %s[%d]:%s already used by link, use mixer",
-							port->node->name, i, d->ports[port->p].name);
-						res = -EBUSY;
-						goto error;
-					}
-
-					if (d->flags & FC_DESCRIPTOR_COPY) {
-						for (j = 0; j < desc->n_output; j++) {
-							struct port *p = &port->node->output_port[j];
-							struct link *link;
-
-							gp = NULL;
-							spa_list_for_each(link, &p->link_list, output_link) {
-								struct port *peer = link->input;
-
-								pw_log_info("copy input port %s[%d]:%s",
-									port->node->name, i,
-									d->ports[port->p].name);
-								peer->external = graph->n_input;
-								gp = &graph->input[graph->n_input++];
-								gp->desc = peer->node->desc->desc;
-								gp->hndl = &peer->node->hndl[i];
-								gp->port = peer->p;
-								gp->next = true;
-								disabled = true;
-							}
-							if (gp != NULL)
-								gp->next = false;
-						}
-						port->node->disabled = disabled;
-					}
-					if (!disabled) {
-						pw_log_info("input port %s[%d]:%s",
-							port->node->name, i, d->ports[port->p].name);
-						port->external = graph->n_input;
-						gp = &graph->input[graph->n_input++];
-						gp->desc = d;
-						gp->hndl = &port->node->hndl[i];
-						gp->port = port->p;
-						gp->next = false;
-					}
-				}
-			}
-		}
-		if (outputs == NULL) {
-			desc = last->desc;
-			d = desc->desc;
-			for (j = 0; j < desc->n_output; j++) {
-				gp = &graph->output[graph->n_output++];
-				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->port = desc->output[j];
-			}
-		} else {
-			struct spa_json it = *outputs;
-			while (spa_json_get_string(&it, v, sizeof(v)) > 0) {
-				gp = &graph->output[graph->n_output];
-				if (spa_streq(v, "null")) {
-					gp->desc = NULL;
-					pw_log_info("silence output port %d", graph->n_output);
-				} else if ((port = find_port(last, v, FC_PORT_OUTPUT)) == NULL) {
-					res = -ENOENT;
-					pw_log_error("output port %s not found", v);
-					goto error;
-				} else {
-					desc = port->node->desc;
-					d = desc->desc;
-					if (i == 0 && port->external != SPA_ID_INVALID) {
-						pw_log_error("output port %s[%d]:%s already used as output %d, use copy",
-							port->node->name, i, d->ports[port->p].name,
-							port->external);
-						res = -EBUSY;
-						goto error;
-					}
-					if (port->n_links > 0) {
-						pw_log_error("output port %s[%d]:%s already used by link, use copy",
-							port->node->name, i, d->ports[port->p].name);
-						res = -EBUSY;
-						goto error;
-					}
-					pw_log_info("output port %s[%d]:%s",
-							port->node->name, i, d->ports[port->p].name);
-					port->external = graph->n_output;
-					gp->desc = d;
-					gp->hndl = &port->node->hndl[i];
-					gp->port = port->p;
-				}
-				graph->n_output++;
-			}
-		}
-	}
-
-	/* order all nodes based on dependencies */
-	graph->n_hndl = 0;
-	graph->hndl = calloc(n_nodes * n_hndl, sizeof(struct graph_hndl));
-	graph->n_control = 0;
-	graph->control_port = calloc(n_control, sizeof(struct port *));
-	while (true) {
-		if ((node = find_next_node(graph)) == NULL)
-			break;
+		impl->playback_info.channels = info->n_outputs;
 
-		desc = node->desc;
-		d = desc->desc;
+	impl->n_inputs = info->n_inputs;
+	impl->n_outputs = info->n_outputs;
 
-		if (!node->disabled) {
-			for (i = 0; i < n_hndl; i++) {
-				gh = &graph->hndl[graph->n_hndl++];
-				gh->hndl = &node->hndl[i];
-				gh->desc = d;
-			}
-		}
-		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--;
-		}
-		for (i = 0; i < desc->n_notify; i++) {
-			spa_list_for_each(link, &node->notify_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++) {
-			graph->control_port[graph->n_control] = &node->control_port[i];
-			graph->n_control++;
-		}
+	if (impl->capture_info.channels == impl->playback_info.channels) {
+		copy_position(&impl->capture_info, &impl->playback_info);
+		copy_position(&impl->playback_info, &impl->capture_info);
 	}
-	res = 0;
-error:
-	return res;
 }
 
-/**
- * filter.graph = {
- *     nodes = [
- *         { ... } ...
- *     ]
- *     links = [
- *         { ... } ...
- *     ]
- *     inputs = [ ]
- *     outputs = [ ]
- * }
- */
-static int load_graph(struct graph *graph, struct pw_properties *props)
+static void graph_apply_props(void *object, enum spa_direction direction, const struct spa_pod *props)
 {
-	struct spa_json it[3];
-	struct spa_json inputs, outputs, *pinputs = NULL, *poutputs = NULL;
-	struct spa_json cvolumes, pvolumes, *pcvolumes = NULL, *ppvolumes = NULL;
-	struct spa_json nodes, *pnodes = NULL, links, *plinks = NULL;
-	const char *json, *val;
-	char key[256];
-	int res;
-
-	spa_list_init(&graph->node_list);
-	spa_list_init(&graph->link_list);
+	struct impl *impl = object;
+	pw_stream_set_param(direction == SPA_DIRECTION_INPUT ?
+			impl->capture : impl->playback,
+			SPA_PARAM_Props, props);
+}
 
-	if ((json = pw_properties_get(props, "filter.graph")) == NULL) {
-		pw_log_error("missing filter.graph property");
-		return -EINVAL;
-	}
+static void graph_props_changed(void *object, enum spa_direction direction)
+{
+	struct impl *impl = object;
+	struct spa_filter_graph *graph = impl->graph;
+	uint8_t buffer[1024];
+	struct spa_pod_dynamic_builder b;
+	const struct spa_pod *params[1];
 
-	spa_json_init(&it[0], json, strlen(json));
-        if (spa_json_enter_object(&it[0], &it[1]) <= 0) {
-		pw_log_error("filter.graph must be an object");
-		return -EINVAL;
-	}
+	spa_pod_dynamic_builder_init(&b, buffer, sizeof(buffer), 4096);
+	spa_filter_graph_get_props(graph, &b.b, (struct spa_pod **)&params[0]);
 
-	while (spa_json_get_string(&it[1], key, sizeof(key)) > 0) {
-		if (spa_streq("nodes", key)) {
-			if (spa_json_enter_array(&it[1], &nodes) <= 0) {
-				pw_log_error("nodes expects an array");
-				return -EINVAL;
-			}
-			pnodes = &nodes;
-		}
-		else if (spa_streq("links", key)) {
-			if (spa_json_enter_array(&it[1], &links) <= 0) {
-				pw_log_error("links expects an array");
-				return -EINVAL;
-			}
-			plinks = &links;
-		}
-		else if (spa_streq("inputs", key)) {
-			if (spa_json_enter_array(&it[1], &inputs) <= 0) {
-				pw_log_error("inputs expects an array");
-				return -EINVAL;
-			}
-			pinputs = &inputs;
-		}
-		else if (spa_streq("outputs", key)) {
-			if (spa_json_enter_array(&it[1], &outputs) <= 0) {
-				pw_log_error("outputs expects an array");
-				return -EINVAL;
-			}
-			poutputs = &outputs;
-		}
-		else if (spa_streq("capture.volumes", key)) {
-			if (spa_json_enter_array(&it[1], &cvolumes) <= 0) {
-				pw_log_error("capture.volumes expects an array");
-				return -EINVAL;
-			}
-			pcvolumes = &cvolumes;
-		}
-		else if (spa_streq("playback.volumes", key)) {
-			if (spa_json_enter_array(&it[1], &pvolumes) <= 0) {
-				pw_log_error("playback.volumes expects an array");
-				return -EINVAL;
-			}
-			ppvolumes = &pvolumes;
-		} else {
-			pw_log_warn("unexpected graph key '%s'", key);
-			if (spa_json_next(&it[1], &val) < 0)
-				break;
-		}
-	}
-	if (pnodes == NULL) {
-		pw_log_error("filter.graph is missing a nodes array");
-		return -EINVAL;
-	}
-	while (spa_json_enter_object(pnodes, &it[2]) > 0) {
-		if ((res = load_node(graph, &it[2])) < 0)
-			return res;
-	}
-	if (plinks != NULL) {
-		while (spa_json_enter_object(plinks, &it[2]) > 0) {
-			if ((res = parse_link(graph, &it[2])) < 0)
-				return res;
-		}
-	}
-	if (pcvolumes != NULL) {
-		while (spa_json_enter_object(pcvolumes, &it[2]) > 0) {
-			if ((res = parse_volume(graph, &it[2], true)) < 0)
-				return res;
-		}
-	}
-	if (ppvolumes != NULL) {
-		while (spa_json_enter_object(ppvolumes, &it[2]) > 0) {
-			if ((res = parse_volume(graph, &it[2], false)) < 0)
-				return res;
-		}
-	}
-	return setup_graph(graph, pinputs, poutputs);
+	pw_stream_update_params(impl->capture, params, 1);
+	spa_pod_dynamic_builder_clean(&b);
 }
 
-static void graph_free(struct graph *graph)
-{
-	struct link *link;
-	struct node *node;
-	spa_list_consume(link, &graph->link_list, link)
-		link_free(link);
-	spa_list_consume(node, &graph->node_list, link)
-		node_free(node);
-	free(graph->input);
-	free(graph->output);
-	free(graph->hndl);
-	free(graph->control_port);
-}
+struct spa_filter_graph_events graph_events = {
+	SPA_VERSION_FILTER_GRAPH_EVENTS,
+	.info = graph_info,
+	.apply_props = graph_apply_props,
+	.props_changed = graph_props_changed,
+};
 
 static void core_error(void *data, uint32_t id, int seq, int res, const char *message)
 {
@@ -2887,8 +1304,6 @@ static const struct pw_proxy_events core_proxy_events = {
 
 static void impl_destroy(struct impl *impl)
 {
-	struct plugin_func *pl;
-
 	/* disconnect both streams before destroying any of them */
 	if (impl->capture)
 		pw_stream_disconnect(impl->capture);
@@ -2903,15 +1318,13 @@ static void impl_destroy(struct impl *impl)
 	if (impl->core && impl->do_disconnect)
 		pw_core_disconnect(impl->core);
 
+	if (impl->handle)
+		pw_unload_spa_handle(impl->handle);
+
 	pw_properties_free(impl->capture_props);
 	pw_properties_free(impl->playback_props);
-	graph_free(&impl->graph);
-	spa_list_consume(pl, &impl->plugin_func_list, link)
-		free_plugin_func(pl);
 
 	pw_properties_free(impl->props);
-	free(impl->silence_data);
-	free(impl->discard_data);
 	free(impl);
 }
 
@@ -2927,43 +1340,14 @@ static const struct pw_impl_module_events module_events = {
 	.destroy = module_destroy,
 };
 
-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(struct pw_properties *props, struct spa_audio_info_raw *info)
 {
-	const char *str;
-
-	*info = SPA_AUDIO_INFO_RAW_INIT(
-			.format = SPA_AUDIO_FORMAT_F32P);
-	info->rate = pw_properties_get_int32(props, PW_KEY_AUDIO_RATE, info->rate);
-	info->channels = pw_properties_get_int32(props, PW_KEY_AUDIO_CHANNELS, info->channels);
-	info->channels = SPA_MIN(info->channels, SPA_AUDIO_MAX_CHANNELS);
-	if ((str = pw_properties_get(props, SPA_KEY_AUDIO_POSITION)) != NULL)
-		parse_position(info, str, strlen(str));
+	spa_audio_info_raw_init_dict_keys(info,
+			&SPA_DICT_ITEMS(
+				 SPA_DICT_ITEM(SPA_KEY_AUDIO_FORMAT, "F32P")),
+			&props->dict,
+			SPA_KEY_AUDIO_CHANNELS,
+			SPA_KEY_AUDIO_POSITION, NULL);
 }
 
 static void copy_props(struct impl *impl, struct pw_properties *props, const char *key)
@@ -2981,15 +1365,14 @@ SPA_EXPORT
 int pipewire__module_init(struct pw_impl_module *module, const char *args)
 {
 	struct pw_context *context = pw_impl_module_get_context(module);
+	const struct pw_properties *p;
 	struct pw_properties *props;
 	struct impl *impl;
 	uint32_t id = pw_global_get_id(pw_impl_module_get_global(module));
 	uint32_t pid = getpid();
 	const char *str;
 	int res;
-	const struct spa_support *support;
-	uint32_t n_support;
-	struct spa_cpu *cpu_iface;
+	void *iface = NULL;
 
 	PW_LOG_TOPIC_INIT(mod_topic);
 
@@ -3021,35 +1404,6 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
 
 	impl->module = module;
 	impl->context = context;
-	impl->graph.impl = impl;
-
-	spa_list_init(&impl->plugin_list);
-	spa_list_init(&impl->plugin_func_list);
-
-	add_plugin_func(impl, "builtin", load_builtin_plugin, NULL);
-	add_plugin_func(impl, "ladspa", load_ladspa_plugin, NULL);
-
-	support = pw_context_get_support(impl->context, &n_support);
-	impl->quantum_limit = pw_properties_get_uint32(
-			pw_context_get_properties(impl->context),
-			"default.clock.quantum-limit", 8192u);
-
-	pw_properties_setf(props, "clock.quantum-limit", "%u", impl->quantum_limit);
-
-	impl->silence_data = calloc(impl->quantum_limit, sizeof(float));
-	if (impl->silence_data == NULL) {
-		res = -errno;
-		goto error;
-	}
-
-	impl->discard_data = calloc(impl->quantum_limit, sizeof(float));
-	if (impl->discard_data == NULL) {
-		res = -errno;
-		goto error;
-	}
-
-	cpu_iface = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_CPU);
-	dsp_ops_init(&impl->dsp, cpu_iface ? spa_cpu_get_flags(cpu_iface) : 0);
 
 	if (pw_properties_get(props, PW_KEY_NODE_GROUP) == NULL)
 		pw_properties_setf(props, PW_KEY_NODE_GROUP, "filter-chain-%u-%u", pid, id);
@@ -3117,11 +1471,29 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
 		pw_properties_setf(impl->playback_props, PW_KEY_MEDIA_NAME, "%s output",
 				pw_properties_get(impl->playback_props, PW_KEY_NODE_DESCRIPTION));
 
-	if ((res = load_graph(&impl->graph, props)) < 0) {
-		pw_log_error("can't load graph: %s", spa_strerror(res));
+	p = pw_context_get_properties(impl->context);
+	pw_properties_set(props, "clock.quantum-limit",
+			pw_properties_get(p, "default.clock.quantum-limit"));
+
+	pw_properties_setf(props, "filter-graph.n_inputs", "%d", impl->capture_info.channels);
+	pw_properties_setf(props, "filter-graph.n_outputs", "%d", impl->playback_info.channels);
+
+	pw_properties_set(props, SPA_KEY_LIBRARY_NAME, "filter-graph/libspa-filter-graph");
+	impl->handle = pw_context_load_spa_handle(impl->context, "filter.graph", &props->dict);
+	if (impl->handle == NULL) {
+		res = -errno;
 		goto error;
 	}
 
+	res = spa_handle_get_interface(impl->handle, SPA_TYPE_INTERFACE_FilterGraph, &iface);
+	if (res < 0 || iface == NULL)
+		goto error;
+
+	impl->graph = iface;
+
+	spa_filter_graph_add_listener(impl->graph, &impl->graph_listener,
+			&graph_events, impl);
+
 	impl->core = pw_context_get_object(impl->context, PW_TYPE_INTERFACE_Core);
 	if (impl->core == NULL) {
 		str = pw_properties_get(props, PW_KEY_REMOTE_NAME);
diff --git a/src/modules/module-filter-chain/biquad.c b/src/modules/module-filter-chain/biquad.c
deleted file mode 100644
index c11f76e0..00000000
--- a/src/modules/module-filter-chain/biquad.c
+++ /dev/null
@@ -1,364 +0,0 @@
-/* Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
- * Use of this source code is governed by a BSD-style license that can be
- * found in the LICENSE file.
- */
-
-/* Copyright (C) 2010 Google Inc. All rights reserved.
- * Use of this source code is governed by a BSD-style license that can be
- * found in the LICENSE.WEBKIT file.
- */
-
-#include <math.h>
-#include "biquad.h"
-
-#ifndef M_PI
-#define M_PI 3.14159265358979323846
-#endif
-
-static void set_coefficient(struct biquad *bq, double b0, double b1, double b2,
-			    double a0, double a1, double a2)
-{
-	double a0_inv = 1 / a0;
-	bq->b0 = (float)(b0 * a0_inv);
-	bq->b1 = (float)(b1 * a0_inv);
-	bq->b2 = (float)(b2 * a0_inv);
-	bq->a1 = (float)(a1 * a0_inv);
-	bq->a2 = (float)(a2 * a0_inv);
-}
-
-static void biquad_lowpass(struct biquad *bq, double cutoff, double resonance)
-{
-	/* Limit cutoff to 0 to 1. */
-	cutoff = fmax(0.0, fmin(cutoff, 1.0));
-
-	if (cutoff == 1 || cutoff == 0) {
-		/* When cutoff is 1, the z-transform is 1.
-		 * When cutoff is zero, nothing gets through the filter, so set
-		 * coefficients up correctly.
-		 */
-		set_coefficient(bq, cutoff, 0, 0, 1, 0, 0);
-		return;
-	}
-
-	/* Compute biquad coefficients for lowpass filter */
-	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);
-
-	double theta = M_PI * cutoff;
-	double sn = 0.5 * d * sin(theta);
-	double beta = 0.5 * (1 - sn) / (1 + sn);
-	double gamma = (0.5 + beta) * cos(theta);
-	double alpha = 0.25 * (0.5 + beta - gamma);
-
-	double b0 = 2 * alpha;
-	double b1 = 2 * 2 * alpha;
-	double b2 = 2 * alpha;
-	double a1 = 2 * -gamma;
-	double a2 = 2 * beta;
-
-	set_coefficient(bq, b0, b1, b2, 1, a1, a2);
-}
-
-static void biquad_highpass(struct biquad *bq, double cutoff, double resonance)
-{
-	/* Limit cutoff to 0 to 1. */
-	cutoff = fmax(0.0, fmin(cutoff, 1.0));
-
-	if (cutoff == 1 || cutoff == 0) {
-		/* When cutoff is one, the z-transform is 0. */
-		/* When cutoff is zero, we need to be careful because the above
-		 * gives a quadratic divided by the same quadratic, with poles
-		 * and zeros on the unit circle in the same place. When cutoff
-		 * is zero, the z-transform is 1.
-		 */
-		set_coefficient(bq, 1 - cutoff, 0, 0, 1, 0, 0);
-		return;
-	}
-
-	/* Compute biquad coefficients for highpass filter */
-	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);
-
-	double theta = M_PI * cutoff;
-	double sn = 0.5 * d * sin(theta);
-	double beta = 0.5 * (1 - sn) / (1 + sn);
-	double gamma = (0.5 + beta) * cos(theta);
-	double alpha = 0.25 * (0.5 + beta + gamma);
-
-	double b0 = 2 * alpha;
-	double b1 = 2 * -2 * alpha;
-	double b2 = 2 * alpha;
-	double a1 = 2 * -gamma;
-	double a2 = 2 * beta;
-
-	set_coefficient(bq, b0, b1, b2, 1, a1, a2);
-}
-
-static void biquad_bandpass(struct biquad *bq, double frequency, double Q)
-{
-	/* No negative frequencies allowed. */
-	frequency = fmax(0.0, frequency);
-
-	/* Don't let Q go negative, which causes an unstable filter. */
-	Q = fmax(0.0, Q);
-
-	if (frequency <= 0 || frequency >= 1) {
-		/* When the cutoff is zero, the z-transform approaches 0, if Q
-		 * > 0. When both Q and cutoff are zero, the z-transform is
-		 * pretty much undefined. What should we do in this case?
-		 * For now, just make the filter 0. When the cutoff is 1, the
-		 * z-transform also approaches 0.
-		 */
-		set_coefficient(bq, 0, 0, 0, 1, 0, 0);
-		return;
-	}
-	if (Q <= 0) {
-		/* When Q = 0, the above formulas have problems. If we
-		 * look at the z-transform, we can see that the limit
-		 * as Q->0 is 1, so set the filter that way.
-		 */
-		set_coefficient(bq, 1, 0, 0, 1, 0, 0);
-		return;
-	}
-
-	double w0 = M_PI * frequency;
-	double alpha = sin(w0) / (2 * Q);
-	double k = cos(w0);
-
-	double b0 = alpha;
-	double b1 = 0;
-	double b2 = -alpha;
-	double a0 = 1 + alpha;
-	double a1 = -2 * k;
-	double a2 = 1 - alpha;
-
-	set_coefficient(bq, b0, b1, b2, a0, a1, a2);
-}
-
-static void biquad_lowshelf(struct biquad *bq, double frequency, double db_gain)
-{
-	/* Clip frequencies to between 0 and 1, inclusive. */
-	frequency = fmax(0.0, fmin(frequency, 1.0));
-
-	double A = pow(10.0, db_gain / 40);
-
-	if (frequency == 1) {
-		/* The z-transform is a constant gain. */
-		set_coefficient(bq, A * A, 0, 0, 1, 0, 0);
-		return;
-	}
-	if (frequency <= 0) {
-		/* When frequency is 0, the z-transform is 1. */
-		set_coefficient(bq, 1, 0, 0, 1, 0, 0);
-		return;
-	}
-
-	double w0 = M_PI * frequency;
-	double S = 1; /* filter slope (1 is max value) */
-	double alpha = 0.5 * sin(w0) * sqrt((A + 1 / A) * (1 / S - 1) + 2);
-	double k = cos(w0);
-	double k2 = 2 * sqrt(A) * alpha;
-	double a_plus_one = A + 1;
-	double a_minus_one = A - 1;
-
-	double b0 = A * (a_plus_one - a_minus_one * k + k2);
-	double b1 = 2 * A * (a_minus_one - a_plus_one * k);
-	double b2 = A * (a_plus_one - a_minus_one * k - k2);
-	double a0 = a_plus_one + a_minus_one * k + k2;
-	double a1 = -2 * (a_minus_one + a_plus_one * k);
-	double a2 = a_plus_one + a_minus_one * k - k2;
-
-	set_coefficient(bq, b0, b1, b2, a0, a1, a2);
-}
-
-static void biquad_highshelf(struct biquad *bq, double frequency,
-			     double db_gain)
-{
-	/* Clip frequencies to between 0 and 1, inclusive. */
-	frequency = fmax(0.0, fmin(frequency, 1.0));
-
-	double A = pow(10.0, db_gain / 40);
-
-	if (frequency == 1) {
-		/* The z-transform is 1. */
-		set_coefficient(bq, 1, 0, 0, 1, 0, 0);
-		return;
-	}
-	if (frequency <= 0) {
-		/* When frequency = 0, the filter is just a gain, A^2. */
-		set_coefficient(bq, A * A, 0, 0, 1, 0, 0);
-		return;
-	}
-
-	double w0 = M_PI * frequency;
-	double S = 1; /* filter slope (1 is max value) */
-	double alpha = 0.5 * sin(w0) * sqrt((A + 1 / A) * (1 / S - 1) + 2);
-	double k = cos(w0);
-	double k2 = 2 * sqrt(A) * alpha;
-	double a_plus_one = A + 1;
-	double a_minus_one = A - 1;
-
-	double b0 = A * (a_plus_one + a_minus_one * k + k2);
-	double b1 = -2 * A * (a_minus_one + a_plus_one * k);
-	double b2 = A * (a_plus_one + a_minus_one * k - k2);
-	double a0 = a_plus_one - a_minus_one * k + k2;
-	double a1 = 2 * (a_minus_one - a_plus_one * k);
-	double a2 = a_plus_one - a_minus_one * k - k2;
-
-	set_coefficient(bq, b0, b1, b2, a0, a1, a2);
-}
-
-static void biquad_peaking(struct biquad *bq, double frequency, double Q,
-			   double db_gain)
-{
-	/* Clip frequencies to between 0 and 1, inclusive. */
-	frequency = fmax(0.0, fmin(frequency, 1.0));
-
-	/* Don't let Q go negative, which causes an unstable filter. */
-	Q = fmax(0.0, Q);
-
-	double A = pow(10.0, db_gain / 40);
-
-	if (frequency <= 0 || frequency >= 1) {
-		/* When frequency is 0 or 1, the z-transform is 1. */
-		set_coefficient(bq, 1, 0, 0, 1, 0, 0);
-		return;
-	}
-	if (Q <= 0) {
-		/* When Q = 0, the above formulas have problems. If we
-		 * look at the z-transform, we can see that the limit
-		 * as Q->0 is A^2, so set the filter that way.
-		 */
-		set_coefficient(bq, A * A, 0, 0, 1, 0, 0);
-		return;
-	}
-
-	double w0 = M_PI * frequency;
-	double alpha = sin(w0) / (2 * Q);
-	double k = cos(w0);
-
-	double b0 = 1 + alpha * A;
-	double b1 = -2 * k;
-	double b2 = 1 - alpha * A;
-	double a0 = 1 + alpha / A;
-	double a1 = -2 * k;
-	double a2 = 1 - alpha / A;
-
-	set_coefficient(bq, b0, b1, b2, a0, a1, a2);
-}
-
-static void biquad_notch(struct biquad *bq, double frequency, double Q)
-{
-	/* Clip frequencies to between 0 and 1, inclusive. */
-	frequency = fmax(0.0, fmin(frequency, 1.0));
-
-	/* Don't let Q go negative, which causes an unstable filter. */
-	Q = fmax(0.0, Q);
-
-	if (frequency <= 0 || frequency >= 1) {
-		/* When frequency is 0 or 1, the z-transform is 1. */
-		set_coefficient(bq, 1, 0, 0, 1, 0, 0);
-		return;
-	}
-	if (Q <= 0) {
-		/* When Q = 0, the above formulas have problems. If we
-		 * look at the z-transform, we can see that the limit
-		 * as Q->0 is 0, so set the filter that way.
-		 */
-		set_coefficient(bq, 0, 0, 0, 1, 0, 0);
-		return;
-	}
-
-	double w0 = M_PI * frequency;
-	double alpha = sin(w0) / (2 * Q);
-	double k = cos(w0);
-
-	double b0 = 1;
-	double b1 = -2 * k;
-	double b2 = 1;
-	double a0 = 1 + alpha;
-	double a1 = -2 * k;
-	double a2 = 1 - alpha;
-
-	set_coefficient(bq, b0, b1, b2, a0, a1, a2);
-}
-
-static void biquad_allpass(struct biquad *bq, double frequency, double Q)
-{
-	/* Clip frequencies to between 0 and 1, inclusive. */
-	frequency = fmax(0.0, fmin(frequency, 1.0));
-
-	/* Don't let Q go negative, which causes an unstable filter. */
-	Q = fmax(0.0, Q);
-
-	if (frequency <= 0 || frequency >= 1) {
-		/* When frequency is 0 or 1, the z-transform is 1. */
-		set_coefficient(bq, 1, 0, 0, 1, 0, 0);
-		return;
-	}
-
-	if (Q <= 0) {
-		/* When Q = 0, the above formulas have problems. If we
-		 * look at the z-transform, we can see that the limit
-		 * as Q->0 is -1, so set the filter that way.
-		 */
-		set_coefficient(bq, -1, 0, 0, 1, 0, 0);
-		return;
-	}
-
-	double w0 = M_PI * frequency;
-	double alpha = sin(w0) / (2 * Q);
-	double k = cos(w0);
-
-	double b0 = 1 - alpha;
-	double b1 = -2 * k;
-	double b2 = 1 + alpha;
-	double a0 = 1 + alpha;
-	double a1 = -2 * k;
-	double a2 = 1 - alpha;
-
-	set_coefficient(bq, b0, b1, b2, a0, a1, a2);
-}
-
-void biquad_set(struct biquad *bq, enum biquad_type type, double freq, double Q,
-		double gain)
-{
-	/* Clear history values. */
-	bq->x1 = 0;
-	bq->x2 = 0;
-	bq->y1 = 0;
-	bq->y2 = 0;
-
-	switch (type) {
-	case BQ_LOWPASS:
-		biquad_lowpass(bq, freq, Q);
-		break;
-	case BQ_HIGHPASS:
-		biquad_highpass(bq, freq, Q);
-		break;
-	case BQ_BANDPASS:
-		biquad_bandpass(bq, freq, Q);
-		break;
-	case BQ_LOWSHELF:
-		biquad_lowshelf(bq, freq, gain);
-		break;
-	case BQ_HIGHSHELF:
-		biquad_highshelf(bq, freq, gain);
-		break;
-	case BQ_PEAKING:
-		biquad_peaking(bq, freq, Q, gain);
-		break;
-	case BQ_NOTCH:
-		biquad_notch(bq, freq, Q);
-		break;
-	case BQ_ALLPASS:
-		biquad_allpass(bq, freq, Q);
-		break;
-	case BQ_NONE:
-		/* Default is an identity filter. */
-		set_coefficient(bq, 1, 0, 0, 1, 0, 0);
-		break;
-	}
-}
diff --git a/src/modules/module-filter-chain/builtin_plugin.c b/src/modules/module-filter-chain/builtin_plugin.c
deleted file mode 100644
index f7d75ae7..00000000
--- a/src/modules/module-filter-chain/builtin_plugin.c
+++ /dev/null
@@ -1,1771 +0,0 @@
-/* PipeWire */
-/* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */
-/* SPDX-License-Identifier: MIT */
-
-#include "config.h"
-
-#include <float.h>
-#include <math.h>
-#ifdef HAVE_SNDFILE
-#include <sndfile.h>
-#endif
-#include <unistd.h>
-#include <limits.h>
-
-#include <spa/utils/json.h>
-#include <spa/utils/result.h>
-#include <spa/support/cpu.h>
-#include <spa/plugins/audioconvert/resample.h>
-
-#include <pipewire/log.h>
-
-#include "plugin.h"
-
-#include "biquad.h"
-#include "pffft.h"
-#include "convolver.h"
-#include "dsp-ops.h"
-
-#define MAX_RATES	32u
-
-struct plugin {
-	struct fc_plugin plugin;
-	struct dsp_ops *dsp_ops;
-};
-
-struct builtin {
-	struct plugin *plugin;
-	unsigned long rate;
-	float *port[64];
-
-	int type;
-	struct biquad bq;
-	float freq;
-	float Q;
-	float gain;
-	float b0, b1, b2;
-	float a0, a1, a2;
-	float accum;
-};
-
-static void *builtin_instantiate(const struct fc_plugin *plugin, const struct fc_descriptor * Descriptor,
-		unsigned long SampleRate, int index, const char *config)
-{
-	struct builtin *impl;
-
-	impl = calloc(1, sizeof(*impl));
-	if (impl == NULL)
-		return NULL;
-
-	impl->plugin = (struct plugin *) plugin;
-	impl->rate = SampleRate;
-
-	return impl;
-}
-
-static void builtin_connect_port(void *Instance, unsigned long Port, float * DataLocation)
-{
-	struct builtin *impl = Instance;
-	impl->port[Port] = DataLocation;
-}
-
-static void builtin_cleanup(void * Instance)
-{
-	struct builtin *impl = Instance;
-	free(impl);
-}
-
-/** copy */
-static void copy_run(void * Instance, unsigned long SampleCount)
-{
-	struct builtin *impl = Instance;
-	float *in = impl->port[1], *out = impl->port[0];
-	dsp_ops_copy(impl->plugin->dsp_ops, out, in, SampleCount);
-}
-
-static struct fc_port copy_ports[] = {
-	{ .index = 0,
-	  .name = "Out",
-	  .flags = FC_PORT_OUTPUT | FC_PORT_AUDIO,
-	},
-	{ .index = 1,
-	  .name = "In",
-	  .flags = FC_PORT_INPUT | FC_PORT_AUDIO,
-	}
-};
-
-static const struct fc_descriptor copy_desc = {
-	.name = "copy",
-	.flags = FC_DESCRIPTOR_COPY,
-
-	.n_ports = 2,
-	.ports = copy_ports,
-
-	.instantiate = builtin_instantiate,
-	.connect_port = builtin_connect_port,
-	.run = copy_run,
-	.cleanup = builtin_cleanup,
-};
-
-/** mixer */
-static void mixer_run(void * Instance, unsigned long SampleCount)
-{
-	struct builtin *impl = Instance;
-	int i, n_src = 0;
-	float *out = impl->port[0];
-	const void *src[8];
-	float gains[8];
-
-	if (out == NULL)
-		return;
-
-	for (i = 0; i < 8; i++) {
-		float *in = impl->port[1+i];
-		float gain = impl->port[9+i][0];
-
-		if (in == NULL || gain == 0.0f)
-			continue;
-
-		src[n_src] = in;
-		gains[n_src++] = gain;
-	}
-	dsp_ops_mix_gain(impl->plugin->dsp_ops, out, src, gains, n_src, SampleCount);
-}
-
-static struct fc_port mixer_ports[] = {
-	{ .index = 0,
-	  .name = "Out",
-	  .flags = FC_PORT_OUTPUT | FC_PORT_AUDIO,
-	},
-
-	{ .index = 1,
-	  .name = "In 1",
-	  .flags = FC_PORT_INPUT | FC_PORT_AUDIO,
-	},
-	{ .index = 2,
-	  .name = "In 2",
-	  .flags = FC_PORT_INPUT | FC_PORT_AUDIO,
-	},
-	{ .index = 3,
-	  .name = "In 3",
-	  .flags = FC_PORT_INPUT | FC_PORT_AUDIO,
-	},
-	{ .index = 4,
-	  .name = "In 4",
-	  .flags = FC_PORT_INPUT | FC_PORT_AUDIO,
-	},
-	{ .index = 5,
-	  .name = "In 5",
-	  .flags = FC_PORT_INPUT | FC_PORT_AUDIO,
-	},
-	{ .index = 6,
-	  .name = "In 6",
-	  .flags = FC_PORT_INPUT | FC_PORT_AUDIO,
-	},
-	{ .index = 7,
-	  .name = "In 7",
-	  .flags = FC_PORT_INPUT | FC_PORT_AUDIO,
-	},
-	{ .index = 8,
-	  .name = "In 8",
-	  .flags = FC_PORT_INPUT | FC_PORT_AUDIO,
-	},
-
-	{ .index = 9,
-	  .name = "Gain 1",
-	  .flags = FC_PORT_INPUT | FC_PORT_CONTROL,
-	  .def = 1.0f, .min = 0.0f, .max = 10.0f
-	},
-	{ .index = 10,
-	  .name = "Gain 2",
-	  .flags = FC_PORT_INPUT | FC_PORT_CONTROL,
-	  .def = 1.0f, .min = 0.0f, .max = 10.0f
-	},
-	{ .index = 11,
-	  .name = "Gain 3",
-	  .flags = FC_PORT_INPUT | FC_PORT_CONTROL,
-	  .def = 1.0f, .min = 0.0f, .max = 10.0f
-	},
-	{ .index = 12,
-	  .name = "Gain 4",
-	  .flags = FC_PORT_INPUT | FC_PORT_CONTROL,
-	  .def = 1.0f, .min = 0.0f, .max = 10.0f
-	},
-	{ .index = 13,
-	  .name = "Gain 5",
-	  .flags = FC_PORT_INPUT | FC_PORT_CONTROL,
-	  .def = 1.0f, .min = 0.0f, .max = 10.0f
-	},
-	{ .index = 14,
-	  .name = "Gain 6",
-	  .flags = FC_PORT_INPUT | FC_PORT_CONTROL,
-	  .def = 1.0f, .min = 0.0f, .max = 10.0f
-	},
-	{ .index = 15,
-	  .name = "Gain 7",
-	  .flags = FC_PORT_INPUT | FC_PORT_CONTROL,
-	  .def = 1.0f, .min = 0.0f, .max = 10.0f
-	},
-	{ .index = 16,
-	  .name = "Gain 8",
-	  .flags = FC_PORT_INPUT | FC_PORT_CONTROL,
-	  .def = 1.0f, .min = 0.0f, .max = 10.0f
-	},
-};
-
-static const struct fc_descriptor mixer_desc = {
-	.name = "mixer",
-	.flags = FC_DESCRIPTOR_SUPPORTS_NULL_DATA,
-
-	.n_ports = 17,
-	.ports = mixer_ports,
-
-	.instantiate = builtin_instantiate,
-	.connect_port = builtin_connect_port,
-	.run = mixer_run,
-	.cleanup = builtin_cleanup,
-};
-
-/** biquads */
-static int bq_type_from_name(const char *name)
-{
-	if (spa_streq(name, "bq_lowpass"))
-		return BQ_LOWPASS;
-	if (spa_streq(name, "bq_highpass"))
-		return BQ_HIGHPASS;
-	if (spa_streq(name, "bq_bandpass"))
-		return BQ_BANDPASS;
-	if (spa_streq(name, "bq_lowshelf"))
-		return BQ_LOWSHELF;
-	if (spa_streq(name, "bq_highshelf"))
-		return BQ_HIGHSHELF;
-	if (spa_streq(name, "bq_peaking"))
-		return BQ_PEAKING;
-	if (spa_streq(name, "bq_notch"))
-		return BQ_NOTCH;
-	if (spa_streq(name, "bq_allpass"))
-		return BQ_ALLPASS;
-	if (spa_streq(name, "bq_raw"))
-		return BQ_NONE;
-	return BQ_NONE;
-}
-
-static void bq_raw_update(struct builtin *impl, float b0, float b1, float b2,
-		float a0, float a1, float a2)
-{
-	struct biquad *bq = &impl->bq;
-	impl->b0 = b0;
-	impl->b1 = b1;
-	impl->b2 = b2;
-	impl->a0 = a0;
-	impl->a1 = a1;
-	impl->a2 = a2;
-	if (a0 != 0.0f)
-		a0 = 1.0f / a0;
-	bq->b0 = impl->b0 * a0;
-	bq->b1 = impl->b1 * a0;
-	bq->b2 = impl->b2 * a0;
-	bq->a1 = impl->a1 * a0;
-	bq->a2 = impl->a2 * a0;
-	bq->x1 = bq->x2 = bq->y1 = bq->y2 = 0.0;
-}
-
-/*
- * config = {
- *     coefficients = [
- *         { rate =  44100, b0=.., b1=.., b2=.., a0=.., a1=.., a2=.. },
- *         { rate =  48000, b0=.., b1=.., b2=.., a0=.., a1=.., a2=.. },
- *         { rate = 192000, b0=.., b1=.., b2=.., a0=.., a1=.., a2=.. }
- *     ]
- * }
- */
-static void *bq_instantiate(const struct fc_plugin *plugin, const struct fc_descriptor * Descriptor,
-		unsigned long SampleRate, int index, const char *config)
-{
-	struct builtin *impl;
-	struct spa_json it[4];
-	const char *val;
-	char key[256];
-	uint32_t best_rate = 0;
-
-	impl = calloc(1, sizeof(*impl));
-	if (impl == NULL)
-		return NULL;
-
-	impl->plugin = (struct plugin *) plugin;
-	impl->rate = SampleRate;
-	impl->b0 = impl->a0 = 1.0f;
-	impl->type = bq_type_from_name(Descriptor->name);
-	if (impl->type != BQ_NONE)
-		return impl;
-
-	if (config == NULL) {
-		pw_log_error("biquads:bq_raw requires a config section");
-		goto error;
-	}
-
-	spa_json_init(&it[0], config, strlen(config));
-	if (spa_json_enter_object(&it[0], &it[1]) <= 0) {
-		pw_log_error("biquads:config section must be an object");
-		goto error;
-	}
-
-	while (spa_json_get_string(&it[1], key, sizeof(key)) > 0) {
-		if (spa_streq(key, "coefficients")) {
-			if (spa_json_enter_array(&it[1], &it[2]) <= 0) {
-				pw_log_error("biquads:coefficients require an array");
-				goto error;
-			}
-			while (spa_json_enter_object(&it[2], &it[3]) > 0) {
-				int32_t rate = 0;
-				float b0 = 1.0f, b1 = 0.0f, b2 = 0.0f;
-				float a0 = 1.0f, a1 = 0.0f, a2 = 0.0f;
-
-				while (spa_json_get_string(&it[3], key, sizeof(key)) > 0) {
-					if (spa_streq(key, "rate")) {
-						if (spa_json_get_int(&it[3], &rate) <= 0) {
-							pw_log_error("biquads:rate requires a number");
-							goto error;
-						}
-					}
-					else if (spa_streq(key, "b0")) {
-						if (spa_json_get_float(&it[3], &b0) <= 0) {
-							pw_log_error("biquads:b0 requires a float");
-							goto error;
-						}
-					}
-					else if (spa_streq(key, "b1")) {
-						if (spa_json_get_float(&it[3], &b1) <= 0) {
-							pw_log_error("biquads:b1 requires a float");
-							goto error;
-						}
-					}
-					else if (spa_streq(key, "b2")) {
-						if (spa_json_get_float(&it[3], &b2) <= 0) {
-							pw_log_error("biquads:b2 requires a float");
-							goto error;
-						}
-					}
-					else if (spa_streq(key, "a0")) {
-						if (spa_json_get_float(&it[3], &a0) <= 0) {
-							pw_log_error("biquads:a0 requires a float");
-							goto error;
-						}
-					}
-					else if (spa_streq(key, "a1")) {
-						if (spa_json_get_float(&it[3], &a1) <= 0) {
-							pw_log_error("biquads:a1 requires a float");
-							goto error;
-						}
-					}
-					else if (spa_streq(key, "a2")) {
-						if (spa_json_get_float(&it[3], &a2) <= 0) {
-							pw_log_error("biquads:a0 requires a float");
-							goto error;
-						}
-					}
-					else {
-						pw_log_warn("biquads: ignoring coefficients key: '%s'", key);
-						if (spa_json_next(&it[3], &val) < 0)
-							break;
-					}
-				}
-				if (labs((long)rate - (long)SampleRate) <
-				    labs((long)best_rate - (long)SampleRate)) {
-					best_rate = rate;
-					bq_raw_update(impl, b0, b1, b2, a0, a1, a2);
-				}
-			}
-		}
-		else {
-			pw_log_warn("biquads: ignoring config key: '%s'", key);
-			if (spa_json_next(&it[1], &val) < 0)
-				break;
-		}
-	}
-
-	return impl;
-error:
-	free(impl);
-	errno = EINVAL;
-	return NULL;
-}
-
-#define BQ_NUM_PORTS		11
-static struct fc_port bq_ports[] = {
-	{ .index = 0,
-	  .name = "Out",
-	  .flags = FC_PORT_OUTPUT | FC_PORT_AUDIO,
-	},
-	{ .index = 1,
-	  .name = "In",
-	  .flags = FC_PORT_INPUT | FC_PORT_AUDIO,
-	},
-	{ .index = 2,
-	  .name = "Freq",
-	  .flags = FC_PORT_INPUT | FC_PORT_CONTROL,
-	  .hint = FC_HINT_SAMPLE_RATE,
-	  .def = 0.0f, .min = 0.0f, .max = 1.0f,
-	},
-	{ .index = 3,
-	  .name = "Q",
-	  .flags = FC_PORT_INPUT | FC_PORT_CONTROL,
-	  .def = 0.0f, .min = 0.0f, .max = 10.0f,
-	},
-	{ .index = 4,
-	  .name = "Gain",
-	  .flags = FC_PORT_INPUT | FC_PORT_CONTROL,
-	  .def = 0.0f, .min = -120.0f, .max = 20.0f,
-	},
-	{ .index = 5,
-	  .name = "b0",
-	  .flags = FC_PORT_INPUT | FC_PORT_CONTROL,
-	  .def = 1.0f, .min = -10.0f, .max = 10.0f,
-	},
-	{ .index = 6,
-	  .name = "b1",
-	  .flags = FC_PORT_INPUT | FC_PORT_CONTROL,
-	  .def = 0.0f, .min = -10.0f, .max = 10.0f,
-	},
-	{ .index = 7,
-	  .name = "b2",
-	  .flags = FC_PORT_INPUT | FC_PORT_CONTROL,
-	  .def = 0.0f, .min = -10.0f, .max = 10.0f,
-	},
-	{ .index = 8,
-	  .name = "a0",
-	  .flags = FC_PORT_INPUT | FC_PORT_CONTROL,
-	  .def = 1.0f, .min = -10.0f, .max = 10.0f,
-	},
-	{ .index = 9,
-	  .name = "a1",
-	  .flags = FC_PORT_INPUT | FC_PORT_CONTROL,
-	  .def = 0.0f, .min = -10.0f, .max = 10.0f,
-	},
-	{ .index = 10,
-	  .name = "a2",
-	  .flags = FC_PORT_INPUT | FC_PORT_CONTROL,
-	  .def = 0.0f, .min = -10.0f, .max = 10.0f,
-	},
-
-};
-
-static void bq_freq_update(struct builtin *impl, int type, float freq, float Q, float gain)
-{
-	struct biquad *bq = &impl->bq;
-	impl->freq = freq;
-	impl->Q = Q;
-	impl->gain = gain;
-	biquad_set(bq, type, freq * 2 / impl->rate, Q, gain);
-	impl->port[5][0] = impl->b0 = bq->b0;
-	impl->port[6][0] = impl->b1 = bq->b1;
-	impl->port[7][0] = impl->b2 = bq->b2;
-	impl->port[8][0] = impl->a0 = 1.0f;
-	impl->port[9][0] = impl->a1 = bq->a1;
-	impl->port[10][0] = impl->a2 = bq->a2;
-}
-
-static void bq_activate(void * Instance)
-{
-	struct builtin *impl = Instance;
-	if (impl->type == BQ_NONE) {
-		impl->port[5][0] = impl->b0;
-		impl->port[6][0] = impl->b1;
-		impl->port[7][0] = impl->b2;
-		impl->port[8][0] = impl->a0;
-		impl->port[9][0] = impl->a1;
-		impl->port[10][0] = impl->a2;
-	} else {
-		float freq = impl->port[2][0];
-		float Q = impl->port[3][0];
-		float gain = impl->port[4][0];
-		bq_freq_update(impl, impl->type, freq, Q, gain);
-	}
-}
-
-static void bq_run(void *Instance, unsigned long samples)
-{
-	struct builtin *impl = Instance;
-	struct biquad *bq = &impl->bq;
-	float *out = impl->port[0];
-	float *in = impl->port[1];
-
-	if (impl->type == BQ_NONE) {
-		float b0, b1, b2, a0, a1, a2;
-		b0 = impl->port[5][0];
-		b1 = impl->port[6][0];
-		b2 = impl->port[7][0];
-		a0 = impl->port[8][0];
-		a1 = impl->port[9][0];
-		a2 = impl->port[10][0];
-		if (impl->b0 != b0 || impl->b1 != b1 || impl->b2 != b2 ||
-		    impl->a0 != a0 || impl->a1 != a1 || impl->a2 != a2) {
-			bq_raw_update(impl, b0, b1, b2, a0, a1, a2);
-		}
-	} else {
-		float freq = impl->port[2][0];
-		float Q = impl->port[3][0];
-		float gain = impl->port[4][0];
-		if (impl->freq != freq || impl->Q != Q || impl->gain != gain)
-			bq_freq_update(impl, impl->type, freq, Q, gain);
-	}
-	dsp_ops_biquad_run(impl->plugin->dsp_ops, bq, out, in, samples);
-}
-
-/** bq_lowpass */
-static const struct fc_descriptor bq_lowpass_desc = {
-	.name = "bq_lowpass",
-
-	.n_ports = BQ_NUM_PORTS,
-	.ports = bq_ports,
-
-	.instantiate = bq_instantiate,
-	.connect_port = builtin_connect_port,
-	.activate = bq_activate,
-	.run = bq_run,
-	.cleanup = builtin_cleanup,
-};
-
-/** bq_highpass */
-static const struct fc_descriptor bq_highpass_desc = {
-	.name = "bq_highpass",
-
-	.n_ports = BQ_NUM_PORTS,
-	.ports = bq_ports,
-
-	.instantiate = bq_instantiate,
-	.connect_port = builtin_connect_port,
-	.activate = bq_activate,
-	.run = bq_run,
-	.cleanup = builtin_cleanup,
-};
-
-/** bq_bandpass */
-static const struct fc_descriptor bq_bandpass_desc = {
-	.name = "bq_bandpass",
-
-	.n_ports = BQ_NUM_PORTS,
-	.ports = bq_ports,
-
-	.instantiate = bq_instantiate,
-	.connect_port = builtin_connect_port,
-	.activate = bq_activate,
-	.run = bq_run,
-	.cleanup = builtin_cleanup,
-};
-
-/** bq_lowshelf */
-static const struct fc_descriptor bq_lowshelf_desc = {
-	.name = "bq_lowshelf",
-
-	.n_ports = BQ_NUM_PORTS,
-	.ports = bq_ports,
-
-	.instantiate = bq_instantiate,
-	.connect_port = builtin_connect_port,
-	.activate = bq_activate,
-	.run = bq_run,
-	.cleanup = builtin_cleanup,
-};
-
-/** bq_highshelf */
-static const struct fc_descriptor bq_highshelf_desc = {
-	.name = "bq_highshelf",
-
-	.n_ports = BQ_NUM_PORTS,
-	.ports = bq_ports,
-
-	.instantiate = bq_instantiate,
-	.connect_port = builtin_connect_port,
-	.activate = bq_activate,
-	.run = bq_run,
-	.cleanup = builtin_cleanup,
-};
-
-/** bq_peaking */
-static const struct fc_descriptor bq_peaking_desc = {
-	.name = "bq_peaking",
-
-	.n_ports = BQ_NUM_PORTS,
-	.ports = bq_ports,
-
-	.instantiate = bq_instantiate,
-	.connect_port = builtin_connect_port,
-	.activate = bq_activate,
-	.run = bq_run,
-	.cleanup = builtin_cleanup,
-};
-
-/** bq_notch */
-static const struct fc_descriptor bq_notch_desc = {
-	.name = "bq_notch",
-
-	.n_ports = BQ_NUM_PORTS,
-	.ports = bq_ports,
-
-	.instantiate = bq_instantiate,
-	.connect_port = builtin_connect_port,
-	.activate = bq_activate,
-	.run = bq_run,
-	.cleanup = builtin_cleanup,
-};
-
-
-/** bq_allpass */
-static const struct fc_descriptor bq_allpass_desc = {
-	.name = "bq_allpass",
-
-	.n_ports = BQ_NUM_PORTS,
-	.ports = bq_ports,
-
-	.instantiate = bq_instantiate,
-	.connect_port = builtin_connect_port,
-	.activate = bq_activate,
-	.run = bq_run,
-	.cleanup = builtin_cleanup,
-};
-
-/* bq_raw */
-static const struct fc_descriptor bq_raw_desc = {
-	.name = "bq_raw",
-
-	.n_ports = BQ_NUM_PORTS,
-	.ports = bq_ports,
-
-	.instantiate = bq_instantiate,
-	.connect_port = builtin_connect_port,
-	.activate = bq_activate,
-	.run = bq_run,
-	.cleanup = builtin_cleanup,
-};
-
-/** convolve */
-struct convolver_impl {
-	struct plugin *plugin;
-	unsigned long rate;
-	float *port[64];
-
-	struct convolver *conv;
-};
-
-#ifdef HAVE_SNDFILE
-static float *read_samples_from_sf(SNDFILE *f, SF_INFO info, float gain, int delay,
-		int offset, int length, int channel, long unsigned *rate, int *n_samples) {
-	float *samples;
-	int i, n;
-
-	if (length <= 0)
-		length = info.frames;
-	else
-		length = SPA_MIN(length, info.frames);
-
-	length -= SPA_MIN(offset, length);
-
-	n = delay + length;
-	if (n == 0)
-		return NULL;
-
-	samples = calloc(n * info.channels, sizeof(float));
-	if (samples == NULL)
-		return NULL;
-
-	if (offset > 0)
-		sf_seek(f, offset, SEEK_SET);
-	sf_readf_float(f, samples + (delay * info.channels), length);
-
-	channel = channel % info.channels;
-
-	for (i = 0; i < n; i++)
-		samples[i] = samples[info.channels * i + channel] * gain;
-
-	*n_samples = n;
-	*rate = info.samplerate;
-	return samples;
-}
-#endif
-
-static float *read_closest(char **filenames, float gain, int delay, int offset,
-		int length, int channel, long unsigned *rate, int *n_samples)
-{
-#ifdef HAVE_SNDFILE
-	SF_INFO infos[MAX_RATES];
-	SNDFILE *fs[MAX_RATES];
-
-	spa_zero(infos);
-	spa_zero(fs);
-
-	int diff = INT_MAX;
-	uint32_t best = 0, i;
-	float *samples = NULL;
-
-	for (i = 0; i < MAX_RATES && filenames[i] && filenames[i][0]; i++) {
-		fs[i] = sf_open(filenames[i], SFM_READ, &infos[i]);
-		if (fs[i] == NULL)
-			continue;
-
-		if (labs((long)infos[i].samplerate - (long)*rate) < diff) {
-			best = i;
-			diff = labs((long)infos[i].samplerate - (long)*rate);
-			pw_log_debug("new closest match: %d", infos[i].samplerate);
-		}
-	}
-	if (fs[best] != NULL) {
-		pw_log_info("loading best rate:%u %s", infos[best].samplerate, filenames[best]);
-		samples = read_samples_from_sf(fs[best], infos[best], gain, delay,
-			offset, length, channel, rate, n_samples);
-	} else {
-		char buf[PATH_MAX];
-		pw_log_error("Can't open any sample file (CWD %s):",
-				getcwd(buf, sizeof(buf)));
-		for (i = 0; i < MAX_RATES && filenames[i] && filenames[i][0]; i++) {
-			fs[i] = sf_open(filenames[i], SFM_READ, &infos[i]);
-			if (fs[i] == NULL)
-				pw_log_error(" failed file %s: %s", filenames[i], sf_strerror(fs[i]));
-			else
-				pw_log_warn(" unexpectedly opened file %s", filenames[i]);
-		}
-	}
-	for (i = 0; i < MAX_RATES; i++)
-		if (fs[i] != NULL)
-			sf_close(fs[i]);
-
-	return samples;
-#else
-	pw_log_error("compiled without sndfile support, can't load samples: "
-			"using dirac impulse");
-	float *samples = calloc(1, sizeof(float));
-	samples[0] = gain;
-	*n_samples = 1;
-	return samples;
-#endif
-}
-
-static float *create_hilbert(const char *filename, float gain, int delay, int offset,
-		int length, int *n_samples)
-{
-	float *samples, v;
-	int i, n, h;
-
-	if (length <= 0)
-		length = 1024;
-
-	length -= SPA_MIN(offset, length);
-
-	n = delay + length;
-	if (n == 0)
-		return NULL;
-
-	samples = calloc(n, sizeof(float));
-        if (samples == NULL)
-		return NULL;
-
-	gain *= 2 / (float)M_PI;
-	h = length / 2;
-	for (i = 1; i < h; i += 2) {
-		v = (gain / i) * (0.43f + 0.57f * cosf(i * (float)M_PI / h));
-		samples[delay + h + i] = -v;
-		samples[delay + h - i] =  v;
-	}
-	*n_samples = n;
-	return samples;
-}
-
-static float *create_dirac(const char *filename, float gain, int delay, int offset,
-		int length, int *n_samples)
-{
-	float *samples;
-	int n;
-
-	n = delay + 1;
-
-	samples = calloc(n, sizeof(float));
-        if (samples == NULL)
-		return NULL;
-
-	samples[delay] = gain;
-
-	*n_samples = n;
-	return samples;
-}
-
-static float *resample_buffer(struct dsp_ops *dsp_ops, float *samples, int *n_samples,
-		unsigned long in_rate, unsigned long out_rate, uint32_t quality)
-{
-#ifdef HAVE_SPA_PLUGINS
-	uint32_t in_len, out_len, total_out = 0;
-	int out_n_samples;
-	float *out_samples, *out_buf, *in_buf;
-	struct resample r;
-	int res;
-
-	spa_zero(r);
-	r.channels = 1;
-	r.i_rate = in_rate;
-	r.o_rate = out_rate;
-	r.cpu_flags = dsp_ops->cpu_flags;
-	r.quality = quality;
-	if ((res = resample_native_init(&r)) < 0) {
-		pw_log_error("resampling failed: %s", spa_strerror(res));
-		errno = -res;
-		return NULL;
-	}
-
-	out_n_samples = SPA_ROUND_UP(*n_samples * out_rate, in_rate) / in_rate;
-	out_samples = calloc(out_n_samples, sizeof(float));
-	if (out_samples == NULL)
-		goto error;
-
-	in_len = *n_samples;
-	in_buf = samples;
-	out_len = out_n_samples;
-	out_buf = out_samples;
-
-	pw_log_info("Resampling filter: rate: %lu => %lu, n_samples: %u => %u, q:%u",
-		    in_rate, out_rate, in_len, out_len, quality);
-
-	resample_process(&r, (void*)&in_buf, &in_len, (void*)&out_buf, &out_len);
-	pw_log_debug("resampled: %u -> %u samples", in_len, out_len);
-	total_out += out_len;
-
-	in_len = resample_delay(&r);
-	in_buf = calloc(in_len, sizeof(float));
-	if (in_buf == NULL)
-		goto error;
-
-	out_buf = out_samples + total_out;
-	out_len = out_n_samples - total_out;
-
-	pw_log_debug("flushing resampler: %u in %u out", in_len, out_len);
-	resample_process(&r, (void*)&in_buf, &in_len, (void*)&out_buf, &out_len);
-	pw_log_debug("flushed: %u -> %u samples", in_len, out_len);
-	total_out += out_len;
-
-	free(in_buf);
-	free(samples);
-	resample_free(&r);
-
-	*n_samples = total_out;
-
-	float gain = (float)in_rate / (float)out_rate;
-	for (uint32_t i = 0; i < total_out; i++)
-		out_samples[i] = out_samples[i] * gain;
-
-	return out_samples;
-
-error:
-	resample_free(&r);
-	free(samples);
-	free(out_samples);
-	return NULL;
-#else
-	pw_log_error("compiled without spa-plugins support, can't resample");
-	float *out_samples = calloc(*n_samples, sizeof(float));
-	memcpy(out_samples, samples, *n_samples * sizeof(float));
-	return out_samples;
-#endif
-}
-
-static void * convolver_instantiate(const struct fc_plugin *plugin, const struct fc_descriptor * Descriptor,
-		unsigned long SampleRate, int index, const char *config)
-{
-	struct convolver_impl *impl;
-	float *samples;
-	int offset = 0, length = 0, channel = index, n_samples = 0, len;
-	uint32_t i = 0;
-	struct spa_json it[3];
-	const char *val;
-	char key[256], v[256];
-	char *filenames[MAX_RATES] = { 0 };
-	int blocksize = 0, tailsize = 0;
-	int delay = 0;
-	int resample_quality = RESAMPLE_DEFAULT_QUALITY;
-	float gain = 1.0f;
-	unsigned long rate;
-
-	errno = EINVAL;
-	if (config == NULL) {
-		pw_log_error("convolver: requires a config section");
-		return NULL;
-	}
-
-	spa_json_init(&it[0], config, strlen(config));
-	if (spa_json_enter_object(&it[0], &it[1]) <= 0) {
-		pw_log_error("convolver:config must be an object");
-		return NULL;
-	}
-
-	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) {
-				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) {
-				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) {
-				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) {
-				pw_log_error("convolver:delay requires a number");
-				return NULL;
-			}
-		}
-		else if (spa_streq(key, "filename")) {
-			if ((len = spa_json_next(&it[1], &val)) <= 0) {
-				pw_log_error("convolver:filename requires a string or an array");
-				return NULL;
-			}
-			if (spa_json_is_array(val, len)) {
-				spa_json_enter(&it[1], &it[2]);
-				while (spa_json_get_string(&it[2], v, sizeof(v)) > 0 &&
-					i < SPA_N_ELEMENTS(filenames)) {
-						filenames[i] = strdup(v);
-						i++;
-				}
-			}
-			else if (spa_json_parse_stringn(val, len, v, sizeof(v)) <= 0) {
-				pw_log_error("convolver:filename requires a string or an array");
-				return NULL;
-			} else {
-				filenames[0] = strdup(v);
-			}
-		}
-		else if (spa_streq(key, "offset")) {
-			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) {
-				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) {
-				pw_log_error("convolver:channel requires a number");
-				return NULL;
-			}
-		}
-		else if (spa_streq(key, "resample_quality")) {
-			if (spa_json_get_int(&it[1], &resample_quality) <= 0) {
-				pw_log_error("convolver:resample_quality requires a number");
-				return NULL;
-			}
-		}
-		else {
-			pw_log_warn("convolver: ignoring config key: '%s'", key);
-			if (spa_json_next(&it[1], &val) < 0)
-				break;
-		}
-	}
-	if (filenames[0] == NULL) {
-		pw_log_error("convolver:filename was not given");
-		return NULL;
-	}
-
-	if (delay < 0)
-		delay = 0;
-	if (offset < 0)
-		offset = 0;
-
-	if (spa_streq(filenames[0], "/hilbert")) {
-		samples = create_hilbert(filenames[0], gain, delay, offset,
-				length, &n_samples);
-	} else if (spa_streq(filenames[0], "/dirac")) {
-		samples = create_dirac(filenames[0], gain, delay, offset,
-				length, &n_samples);
-	} else {
-		rate = SampleRate;
-		samples = read_closest(filenames, gain, delay, offset,
-				length, channel, &rate, &n_samples);
-		if (samples != NULL && rate != SampleRate) {
-			struct plugin *p = (struct plugin *) plugin;
-			samples = resample_buffer(p->dsp_ops, samples, &n_samples,
-					rate, SampleRate, resample_quality);
-		}
-	}
-
-	for (i = 0; i < MAX_RATES; i++)
-		if (filenames[i])
-			free(filenames[i]);
-
-	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, 32768);
-
-	pw_log_info("using n_samples:%u %d:%d blocksize", n_samples,
-			blocksize, tailsize);
-
-	impl = calloc(1, sizeof(*impl));
-	if (impl == NULL)
-		goto error;
-
-	impl->plugin = (struct plugin *) plugin;
-	impl->rate = SampleRate;
-
-	impl->conv = convolver_new(impl->plugin->dsp_ops, blocksize, tailsize, samples, n_samples);
-	if (impl->conv == NULL)
-		goto error;
-
-	free(samples);
-
-	return impl;
-error:
-	free(samples);
-	free(impl);
-	return NULL;
-}
-
-static void convolver_connect_port(void * Instance, unsigned long Port,
-                        float * DataLocation)
-{
-	struct convolver_impl *impl = Instance;
-	impl->port[Port] = DataLocation;
-}
-
-static void convolver_cleanup(void * Instance)
-{
-	struct convolver_impl *impl = Instance;
-	if (impl->conv)
-		convolver_free(impl->conv);
-	free(impl);
-}
-
-static struct fc_port convolve_ports[] = {
-	{ .index = 0,
-	  .name = "Out",
-	  .flags = FC_PORT_OUTPUT | FC_PORT_AUDIO,
-	},
-	{ .index = 1,
-	  .name = "In",
-	  .flags = FC_PORT_INPUT | FC_PORT_AUDIO,
-	},
-};
-
-static void convolver_deactivate(void * Instance)
-{
-	struct convolver_impl *impl = Instance;
-	convolver_reset(impl->conv);
-}
-
-static void convolve_run(void * Instance, unsigned long SampleCount)
-{
-	struct convolver_impl *impl = Instance;
-	convolver_run(impl->conv, impl->port[1], impl->port[0], SampleCount);
-}
-
-static const struct fc_descriptor convolve_desc = {
-	.name = "convolver",
-
-	.n_ports = 2,
-	.ports = convolve_ports,
-
-	.instantiate = convolver_instantiate,
-	.connect_port = convolver_connect_port,
-	.deactivate = convolver_deactivate,
-	.run = convolve_run,
-	.cleanup = convolver_cleanup,
-};
-
-/** delay */
-struct delay_impl {
-	struct plugin *plugin;
-	unsigned long rate;
-	float *port[4];
-
-	float delay;
-	uint32_t delay_samples;
-	uint32_t buffer_samples;
-	float *buffer;
-	uint32_t ptr;
-};
-
-static void delay_cleanup(void * Instance)
-{
-	struct delay_impl *impl = Instance;
-	free(impl->buffer);
-	free(impl);
-}
-
-static void *delay_instantiate(const struct fc_plugin *plugin, const struct fc_descriptor * Descriptor,
-		unsigned long SampleRate, int index, const char *config)
-{
-	struct delay_impl *impl;
-	struct spa_json it[2];
-	const char *val;
-	char key[256];
-	float max_delay = 1.0f;
-
-	if (config == NULL) {
-		pw_log_error("delay: requires a config section");
-		errno = EINVAL;
-		return NULL;
-	}
-
-	spa_json_init(&it[0], config, strlen(config));
-	if (spa_json_enter_object(&it[0], &it[1]) <= 0) {
-		pw_log_error("delay:config must be an object");
-		return NULL;
-	}
-
-	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) {
-				pw_log_error("delay:max-delay requires a number");
-				return NULL;
-			}
-		} else {
-			pw_log_warn("delay: ignoring config key: '%s'", key);
-			if (spa_json_next(&it[1], &val) < 0)
-				break;
-		}
-	}
-	if (max_delay <= 0.0f)
-		max_delay = 1.0f;
-
-	impl = calloc(1, sizeof(*impl));
-	if (impl == NULL)
-		return NULL;
-
-	impl->plugin = (struct plugin *) plugin;
-	impl->rate = SampleRate;
-	impl->buffer_samples = (uint32_t)(max_delay * impl->rate);
-	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) {
-		delay_cleanup(impl);
-		return NULL;
-	}
-	return impl;
-}
-
-static void delay_connect_port(void * Instance, unsigned long Port,
-                        float * DataLocation)
-{
-	struct delay_impl *impl = Instance;
-	if (Port > 2)
-		return;
-	impl->port[Port] = DataLocation;
-}
-
-static void delay_run(void * Instance, unsigned long SampleCount)
-{
-	struct delay_impl *impl = Instance;
-	float *in = impl->port[1], *out = impl->port[0];
-	float delay = impl->port[2][0];
-	unsigned long n;
-	uint32_t r, w;
-
-	if (delay != impl->delay) {
-		impl->delay_samples = SPA_CLAMP((uint32_t)(delay * impl->rate), 0u, impl->buffer_samples-1);
-		impl->delay = delay;
-	}
-	r = impl->ptr;
-	w = impl->ptr + impl->delay_samples;
-	if (w >= impl->buffer_samples)
-		w -= impl->buffer_samples;
-
-	for (n = 0; n < SampleCount; n++) {
-		impl->buffer[w] = in[n];
-		out[n] = impl->buffer[r];
-		if (++r >= impl->buffer_samples)
-			r = 0;
-		if (++w >= impl->buffer_samples)
-			w = 0;
-	}
-	impl->ptr = r;
-}
-
-static struct fc_port delay_ports[] = {
-	{ .index = 0,
-	  .name = "Out",
-	  .flags = FC_PORT_OUTPUT | FC_PORT_AUDIO,
-	},
-	{ .index = 1,
-	  .name = "In",
-	  .flags = FC_PORT_INPUT | FC_PORT_AUDIO,
-	},
-	{ .index = 2,
-	  .name = "Delay (s)",
-	  .flags = FC_PORT_INPUT | FC_PORT_CONTROL,
-	  .def = 0.0f, .min = 0.0f, .max = 100.0f
-	},
-};
-
-static const struct fc_descriptor delay_desc = {
-	.name = "delay",
-
-	.n_ports = 3,
-	.ports = delay_ports,
-
-	.instantiate = delay_instantiate,
-	.connect_port = delay_connect_port,
-	.run = delay_run,
-	.cleanup = delay_cleanup,
-};
-
-/* invert */
-static void invert_run(void * Instance, unsigned long SampleCount)
-{
-	struct builtin *impl = Instance;
-	float *in = impl->port[1], *out = impl->port[0];
-	unsigned long n;
-	for (n = 0; n < SampleCount; n++)
-		out[n] = -in[n];
-}
-
-static struct fc_port invert_ports[] = {
-	{ .index = 0,
-	  .name = "Out",
-	  .flags = FC_PORT_OUTPUT | FC_PORT_AUDIO,
-	},
-	{ .index = 1,
-	  .name = "In",
-	  .flags = FC_PORT_INPUT | FC_PORT_AUDIO,
-	},
-};
-
-static const struct fc_descriptor invert_desc = {
-	.name = "invert",
-
-	.n_ports = 2,
-	.ports = invert_ports,
-
-	.instantiate = builtin_instantiate,
-	.connect_port = builtin_connect_port,
-	.run = invert_run,
-	.cleanup = builtin_cleanup,
-};
-
-/* clamp */
-static void clamp_run(void * Instance, unsigned long SampleCount)
-{
-	struct builtin *impl = Instance;
-	float min = impl->port[4][0], max = impl->port[5][0];
-	float *in = impl->port[1], *out = impl->port[0];
-	float *ctrl = impl->port[3], *notify = impl->port[2];
-
-	if (in != NULL && out != NULL) {
-		unsigned long n;
-		for (n = 0; n < SampleCount; n++)
-			out[n] = SPA_CLAMPF(in[n], min, max);
-	}
-	if (ctrl != NULL && notify != NULL)
-		notify[0] = SPA_CLAMPF(ctrl[0], min, max);
-}
-
-static struct fc_port clamp_ports[] = {
-	{ .index = 0,
-	  .name = "Out",
-	  .flags = FC_PORT_OUTPUT | FC_PORT_AUDIO,
-	},
-	{ .index = 1,
-	  .name = "In",
-	  .flags = FC_PORT_INPUT | FC_PORT_AUDIO,
-	},
-	{ .index = 2,
-	  .name = "Notify",
-	  .flags = FC_PORT_OUTPUT | FC_PORT_CONTROL,
-	},
-	{ .index = 3,
-	  .name = "Control",
-	  .flags = FC_PORT_INPUT | FC_PORT_CONTROL,
-	},
-	{ .index = 4,
-	  .name = "Min",
-	  .flags = FC_PORT_INPUT | FC_PORT_CONTROL,
-	  .def = 0.0f, .min = -100.0f, .max = 100.0f
-	},
-	{ .index = 5,
-	  .name = "Max",
-	  .flags = FC_PORT_INPUT | FC_PORT_CONTROL,
-	  .def = 1.0f, .min = -100.0f, .max = 100.0f
-	},
-};
-
-static const struct fc_descriptor clamp_desc = {
-	.name = "clamp",
-	.flags = FC_DESCRIPTOR_SUPPORTS_NULL_DATA,
-
-	.n_ports = SPA_N_ELEMENTS(clamp_ports),
-	.ports = clamp_ports,
-
-	.instantiate = builtin_instantiate,
-	.connect_port = builtin_connect_port,
-	.run = clamp_run,
-	.cleanup = builtin_cleanup,
-};
-
-/* linear */
-static void linear_run(void * Instance, unsigned long SampleCount)
-{
-	struct builtin *impl = Instance;
-	float mult = impl->port[4][0], add = impl->port[5][0];
-	float *in = impl->port[1], *out = impl->port[0];
-	float *ctrl = impl->port[3], *notify = impl->port[2];
-
-	if (in != NULL && out != NULL)
-		dsp_ops_linear(impl->plugin->dsp_ops, out, in, mult, add, SampleCount);
-
-	if (ctrl != NULL && notify != NULL)
-		notify[0] = ctrl[0] * mult + add;
-}
-
-static struct fc_port linear_ports[] = {
-	{ .index = 0,
-	  .name = "Out",
-	  .flags = FC_PORT_OUTPUT | FC_PORT_AUDIO,
-	},
-	{ .index = 1,
-	  .name = "In",
-	  .flags = FC_PORT_INPUT | FC_PORT_AUDIO,
-	},
-	{ .index = 2,
-	  .name = "Notify",
-	  .flags = FC_PORT_OUTPUT | FC_PORT_CONTROL,
-	},
-	{ .index = 3,
-	  .name = "Control",
-	  .flags = FC_PORT_INPUT | FC_PORT_CONTROL,
-	},
-	{ .index = 4,
-	  .name = "Mult",
-	  .flags = FC_PORT_INPUT | FC_PORT_CONTROL,
-	  .def = 1.0f, .min = -10.0f, .max = 10.0f
-	},
-	{ .index = 5,
-	  .name = "Add",
-	  .flags = FC_PORT_INPUT | FC_PORT_CONTROL,
-	  .def = 0.0f, .min = -10.0f, .max = 10.0f
-	},
-};
-
-static const struct fc_descriptor linear_desc = {
-	.name = "linear",
-	.flags = FC_DESCRIPTOR_SUPPORTS_NULL_DATA,
-
-	.n_ports = SPA_N_ELEMENTS(linear_ports),
-	.ports = linear_ports,
-
-	.instantiate = builtin_instantiate,
-	.connect_port = builtin_connect_port,
-	.run = linear_run,
-	.cleanup = builtin_cleanup,
-};
-
-
-/* reciprocal */
-static void recip_run(void * Instance, unsigned long SampleCount)
-{
-	struct builtin *impl = Instance;
-	float *in = impl->port[1], *out = impl->port[0];
-	float *ctrl = impl->port[3], *notify = impl->port[2];
-
-	if (in != NULL && out != NULL) {
-		unsigned long n;
-		for (n = 0; n < SampleCount; n++) {
-			if (in[0] == 0.0f)
-				out[n] = 0.0f;
-			else
-				out[n] = 1.0f / in[n];
-		}
-	}
-	if (ctrl != NULL && notify != NULL) {
-		if (ctrl[0] == 0.0f)
-			notify[0] = 0.0f;
-		else
-			notify[0] = 1.0f / ctrl[0];
-	}
-}
-
-static struct fc_port recip_ports[] = {
-	{ .index = 0,
-	  .name = "Out",
-	  .flags = FC_PORT_OUTPUT | FC_PORT_AUDIO,
-	},
-	{ .index = 1,
-	  .name = "In",
-	  .flags = FC_PORT_INPUT | FC_PORT_AUDIO,
-	},
-	{ .index = 2,
-	  .name = "Notify",
-	  .flags = FC_PORT_OUTPUT | FC_PORT_CONTROL,
-	},
-	{ .index = 3,
-	  .name = "Control",
-	  .flags = FC_PORT_INPUT | FC_PORT_CONTROL,
-	},
-};
-
-static const struct fc_descriptor recip_desc = {
-	.name = "recip",
-	.flags = FC_DESCRIPTOR_SUPPORTS_NULL_DATA,
-
-	.n_ports = SPA_N_ELEMENTS(recip_ports),
-	.ports = recip_ports,
-
-	.instantiate = builtin_instantiate,
-	.connect_port = builtin_connect_port,
-	.run = recip_run,
-	.cleanup = builtin_cleanup,
-};
-
-/* exp */
-static void exp_run(void * Instance, unsigned long SampleCount)
-{
-	struct builtin *impl = Instance;
-	float base = impl->port[4][0];
-	float *in = impl->port[1], *out = impl->port[0];
-	float *ctrl = impl->port[3], *notify = impl->port[2];
-
-	if (in != NULL && out != NULL) {
-		unsigned long n;
-		for (n = 0; n < SampleCount; n++)
-			out[n] = powf(base, in[n]);
-	}
-	if (ctrl != NULL && notify != NULL)
-		notify[0] = powf(base, ctrl[0]);
-}
-
-static struct fc_port exp_ports[] = {
-	{ .index = 0,
-	  .name = "Out",
-	  .flags = FC_PORT_OUTPUT | FC_PORT_AUDIO,
-	},
-	{ .index = 1,
-	  .name = "In",
-	  .flags = FC_PORT_INPUT | FC_PORT_AUDIO,
-	},
-	{ .index = 2,
-	  .name = "Notify",
-	  .flags = FC_PORT_OUTPUT | FC_PORT_CONTROL,
-	},
-	{ .index = 3,
-	  .name = "Control",
-	  .flags = FC_PORT_INPUT | FC_PORT_CONTROL,
-	},
-	{ .index = 4,
-	  .name = "Base",
-	  .flags = FC_PORT_INPUT | FC_PORT_CONTROL,
-	  .def = (float)M_E, .min = -10.0f, .max = 10.0f
-	},
-};
-
-static const struct fc_descriptor exp_desc = {
-	.name = "exp",
-	.flags = FC_DESCRIPTOR_SUPPORTS_NULL_DATA,
-
-	.n_ports = SPA_N_ELEMENTS(exp_ports),
-	.ports = exp_ports,
-
-	.instantiate = builtin_instantiate,
-	.connect_port = builtin_connect_port,
-	.run = exp_run,
-	.cleanup = builtin_cleanup,
-};
-
-/* log */
-static void log_run(void * Instance, unsigned long SampleCount)
-{
-	struct builtin *impl = Instance;
-	float base = impl->port[4][0];
-	float m1 = impl->port[5][0];
-	float m2 = impl->port[6][0];
-	float *in = impl->port[1], *out = impl->port[0];
-	float *ctrl = impl->port[3], *notify = impl->port[2];
-	float lb = log2f(base);
-
-	if (in != NULL && out != NULL) {
-		unsigned long n;
-		for (n = 0; n < SampleCount; n++)
-			out[n] = m2 * log2f(fabsf(in[n] * m1)) / lb;
-	}
-	if (ctrl != NULL && notify != NULL)
-		notify[0] = m2 * log2f(fabsf(ctrl[0] * m1)) / lb;
-}
-
-static struct fc_port log_ports[] = {
-	{ .index = 0,
-	  .name = "Out",
-	  .flags = FC_PORT_OUTPUT | FC_PORT_AUDIO,
-	},
-	{ .index = 1,
-	  .name = "In",
-	  .flags = FC_PORT_INPUT | FC_PORT_AUDIO,
-	},
-	{ .index = 2,
-	  .name = "Notify",
-	  .flags = FC_PORT_OUTPUT | FC_PORT_CONTROL,
-	},
-	{ .index = 3,
-	  .name = "Control",
-	  .flags = FC_PORT_INPUT | FC_PORT_CONTROL,
-	},
-	{ .index = 4,
-	  .name = "Base",
-	  .flags = FC_PORT_INPUT | FC_PORT_CONTROL,
-	  .def = (float)M_E, .min = 2.0f, .max = 100.0f
-	},
-	{ .index = 5,
-	  .name = "M1",
-	  .flags = FC_PORT_INPUT | FC_PORT_CONTROL,
-	  .def = 1.0f, .min = -10.0f, .max = 10.0f
-	},
-	{ .index = 6,
-	  .name = "M2",
-	  .flags = FC_PORT_INPUT | FC_PORT_CONTROL,
-	  .def = 1.0f, .min = -10.0f, .max = 10.0f
-	},
-};
-
-static const struct fc_descriptor log_desc = {
-	.name = "log",
-	.flags = FC_DESCRIPTOR_SUPPORTS_NULL_DATA,
-
-	.n_ports = SPA_N_ELEMENTS(log_ports),
-	.ports = log_ports,
-
-	.instantiate = builtin_instantiate,
-	.connect_port = builtin_connect_port,
-	.run = log_run,
-	.cleanup = builtin_cleanup,
-};
-
-/* mult */
-static void mult_run(void * Instance, unsigned long SampleCount)
-{
-	struct builtin *impl = Instance;
-	int i, n_src = 0;
-	float *out = impl->port[0];
-	const void *src[8];
-
-	if (out == NULL)
-		return;
-
-	for (i = 0; i < 8; i++) {
-		float *in = impl->port[1+i];
-
-		if (in == NULL)
-			continue;
-
-		src[n_src++] = in;
-	}
-	dsp_ops_mult(impl->plugin->dsp_ops, out, src, n_src, SampleCount);
-}
-
-static struct fc_port mult_ports[] = {
-	{ .index = 0,
-	  .name = "Out",
-	  .flags = FC_PORT_OUTPUT | FC_PORT_AUDIO,
-	},
-	{ .index = 1,
-	  .name = "In 1",
-	  .flags = FC_PORT_INPUT | FC_PORT_AUDIO,
-	},
-	{ .index = 2,
-	  .name = "In 2",
-	  .flags = FC_PORT_INPUT | FC_PORT_AUDIO,
-	},
-	{ .index = 3,
-	  .name = "In 3",
-	  .flags = FC_PORT_INPUT | FC_PORT_AUDIO,
-	},
-	{ .index = 4,
-	  .name = "In 4",
-	  .flags = FC_PORT_INPUT | FC_PORT_AUDIO,
-	},
-	{ .index = 5,
-	  .name = "In 5",
-	  .flags = FC_PORT_INPUT | FC_PORT_AUDIO,
-	},
-	{ .index = 6,
-	  .name = "In 6",
-	  .flags = FC_PORT_INPUT | FC_PORT_AUDIO,
-	},
-	{ .index = 7,
-	  .name = "In 7",
-	  .flags = FC_PORT_INPUT | FC_PORT_AUDIO,
-	},
-	{ .index = 8,
-	  .name = "In 8",
-	  .flags = FC_PORT_INPUT | FC_PORT_AUDIO,
-	},
-};
-
-static const struct fc_descriptor mult_desc = {
-	.name = "mult",
-	.flags = FC_DESCRIPTOR_SUPPORTS_NULL_DATA,
-
-	.n_ports = SPA_N_ELEMENTS(mult_ports),
-	.ports = mult_ports,
-
-	.instantiate = builtin_instantiate,
-	.connect_port = builtin_connect_port,
-	.run = mult_run,
-	.cleanup = builtin_cleanup,
-};
-
-#define M_PI_M2f (float)(M_PI+M_PI)
-
-/* sine */
-static void sine_run(void * Instance, unsigned long SampleCount)
-{
-	struct builtin *impl = Instance;
-	float *out = impl->port[0];
-	float *notify = impl->port[1];
-	float freq = impl->port[2][0];
-	float ampl = impl->port[3][0];
-	float offs = impl->port[5][0];
-	unsigned long n;
-
-	for (n = 0; n < SampleCount; n++) {
-		if (out != NULL)
-			out[n] = sinf(impl->accum) * ampl + offs;
-		if (notify != NULL && n == 0)
-			notify[0] = sinf(impl->accum) * ampl + offs;
-
-		impl->accum += M_PI_M2f * freq / impl->rate;
-		if (impl->accum >= M_PI_M2f)
-			impl->accum -= M_PI_M2f;
-	}
-}
-
-static struct fc_port sine_ports[] = {
-	{ .index = 0,
-	  .name = "Out",
-	  .flags = FC_PORT_OUTPUT | FC_PORT_AUDIO,
-	},
-	{ .index = 1,
-	  .name = "Notify",
-	  .flags = FC_PORT_OUTPUT | FC_PORT_CONTROL,
-	},
-	{ .index = 2,
-	  .name = "Freq",
-	  .flags = FC_PORT_INPUT | FC_PORT_CONTROL,
-	  .def = 440.0f, .min = 0.0f, .max = 1000000.0f
-	},
-	{ .index = 3,
-	  .name = "Ampl",
-	  .flags = FC_PORT_INPUT | FC_PORT_CONTROL,
-	  .def = 1.0, .min = 0.0f, .max = 10.0f
-	},
-	{ .index = 4,
-	  .name = "Phase",
-	  .flags = FC_PORT_INPUT | FC_PORT_CONTROL,
-	  .def = 0.0f, .min = (float)-M_PI, .max = (float)M_PI
-	},
-	{ .index = 5,
-	  .name = "Offset",
-	  .flags = FC_PORT_INPUT | FC_PORT_CONTROL,
-	  .def = 0.0f, .min = -10.0f, .max = 10.0f
-	},
-};
-
-static const struct fc_descriptor sine_desc = {
-	.name = "sine",
-	.flags = FC_DESCRIPTOR_SUPPORTS_NULL_DATA,
-
-	.n_ports = SPA_N_ELEMENTS(sine_ports),
-	.ports = sine_ports,
-
-	.instantiate = builtin_instantiate,
-	.connect_port = builtin_connect_port,
-	.run = sine_run,
-	.cleanup = builtin_cleanup,
-};
-
-static const struct fc_descriptor * builtin_descriptor(unsigned long Index)
-{
-	switch(Index) {
-	case 0:
-		return &mixer_desc;
-	case 1:
-		return &bq_lowpass_desc;
-	case 2:
-		return &bq_highpass_desc;
-	case 3:
-		return &bq_bandpass_desc;
-	case 4:
-		return &bq_lowshelf_desc;
-	case 5:
-		return &bq_highshelf_desc;
-	case 6:
-		return &bq_peaking_desc;
-	case 7:
-		return &bq_notch_desc;
-	case 8:
-		return &bq_allpass_desc;
-	case 9:
-		return &copy_desc;
-	case 10:
-		return &convolve_desc;
-	case 11:
-		return &delay_desc;
-	case 12:
-		return &invert_desc;
-	case 13:
-		return &bq_raw_desc;
-	case 14:
-		return &clamp_desc;
-	case 15:
-		return &linear_desc;
-	case 16:
-		return &recip_desc;
-	case 17:
-		return &exp_desc;
-	case 18:
-		return &log_desc;
-	case 19:
-		return &mult_desc;
-	case 20:
-		return &sine_desc;
-	}
-	return NULL;
-}
-
-static const struct fc_descriptor *builtin_make_desc(struct fc_plugin *plugin, const char *name)
-{
-	unsigned long i;
-	for (i = 0; ;i++) {
-		const struct fc_descriptor *d = builtin_descriptor(i);
-		if (d == NULL)
-			break;
-		if (spa_streq(d->name, name))
-			return d;
-	}
-	return NULL;
-}
-
-static void builtin_plugin_unload(struct fc_plugin *p)
-{
-	free(p);
-}
-
-struct fc_plugin *load_builtin_plugin(const struct spa_support *support, uint32_t n_support,
-		struct dsp_ops *dsp, const char *plugin, const struct spa_dict *info)
-{
-	struct plugin *impl = calloc (1, sizeof (struct plugin));
-	impl->plugin.make_desc = builtin_make_desc;
-	impl->plugin.unload = builtin_plugin_unload;
-	impl->dsp_ops = dsp;
-	pffft_select_cpu(dsp->cpu_flags);
-	return (struct fc_plugin *) impl;
-}
diff --git a/src/modules/module-filter-chain/dsp-ops-avx.c b/src/modules/module-filter-chain/dsp-ops-avx.c
deleted file mode 100644
index b2f7a683..00000000
--- a/src/modules/module-filter-chain/dsp-ops-avx.c
+++ /dev/null
@@ -1,65 +0,0 @@
-/* Spa */
-/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */
-/* SPDX-License-Identifier: MIT */
-
-#include <string.h>
-#include <stdio.h>
-#include <math.h>
-
-#include <spa/utils/defs.h>
-
-#include "dsp-ops.h"
-
-#include <immintrin.h>
-
-void dsp_sum_avx(struct dsp_ops *ops, float *r, const float *a, const float *b, uint32_t n_samples)
-{
-	uint32_t n, unrolled;
-	__m256 in[4];
-
-	unrolled = n_samples & ~31;
-
-	if (SPA_LIKELY(SPA_IS_ALIGNED(r, 32)) &&
-	    SPA_LIKELY(SPA_IS_ALIGNED(a, 32)) &&
-	    SPA_LIKELY(SPA_IS_ALIGNED(b, 32))) {
-		for (n = 0; n < unrolled; n += 32) {
-			in[0] = _mm256_load_ps(&a[n+ 0]);
-			in[1] = _mm256_load_ps(&a[n+ 8]);
-			in[2] = _mm256_load_ps(&a[n+16]);
-			in[3] = _mm256_load_ps(&a[n+24]);
-
-			in[0] = _mm256_add_ps(in[0], _mm256_load_ps(&b[n+ 0]));
-			in[1] = _mm256_add_ps(in[1], _mm256_load_ps(&b[n+ 8]));
-			in[2] = _mm256_add_ps(in[2], _mm256_load_ps(&b[n+16]));
-			in[3] = _mm256_add_ps(in[3], _mm256_load_ps(&b[n+24]));
-
-			_mm256_store_ps(&r[n+ 0], in[0]);
-			_mm256_store_ps(&r[n+ 8], in[1]);
-			_mm256_store_ps(&r[n+16], in[2]);
-			_mm256_store_ps(&r[n+24], in[3]);
-		}
-	} else {
-		for (n = 0; n < unrolled; n += 32) {
-			in[0] = _mm256_loadu_ps(&a[n+ 0]);
-			in[1] = _mm256_loadu_ps(&a[n+ 8]);
-			in[2] = _mm256_loadu_ps(&a[n+16]);
-			in[3] = _mm256_loadu_ps(&a[n+24]);
-
-			in[0] = _mm256_add_ps(in[0], _mm256_loadu_ps(&b[n+ 0]));
-			in[1] = _mm256_add_ps(in[1], _mm256_loadu_ps(&b[n+ 8]));
-			in[2] = _mm256_add_ps(in[2], _mm256_loadu_ps(&b[n+16]));
-			in[3] = _mm256_add_ps(in[3], _mm256_loadu_ps(&b[n+24]));
-
-			_mm256_storeu_ps(&r[n+ 0], in[0]);
-			_mm256_storeu_ps(&r[n+ 8], in[1]);
-			_mm256_storeu_ps(&r[n+16], in[2]);
-			_mm256_storeu_ps(&r[n+24], in[3]);
-		}
-	}
-	for (; n < n_samples; n++) {
-		__m128 in[1];
-		in[0] = _mm_load_ss(&a[n]);
-		in[0] = _mm_add_ss(in[0], _mm_load_ss(&b[n]));
-		_mm_store_ss(&r[n], in[0]);
-	}
-}
diff --git a/src/modules/module-filter-chain/dsp-ops-c.c b/src/modules/module-filter-chain/dsp-ops-c.c
deleted file mode 100644
index 82d20b50..00000000
--- a/src/modules/module-filter-chain/dsp-ops-c.c
+++ /dev/null
@@ -1,196 +0,0 @@
-/* Spa */
-/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */
-/* SPDX-License-Identifier: MIT */
-
-#include <string.h>
-#include <stdio.h>
-#include <math.h>
-#include <float.h>
-
-#include <spa/utils/defs.h>
-
-#include "pffft.h"
-#include "dsp-ops.h"
-
-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;
-	if (gain == 0.0f)
-		dsp_clear_c(ops, dst, n_samples);
-	else if (gain == 1.0f)
-		dsp_copy_c(ops, dst, src, n_samples);
-	else  {
-		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;
-
-	if (gain == 0.0f)
-		return;
-	else if (gain == 1.0f)
-		dsp_add_c(ops, dst, src, n_samples);
-	else {
-		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 {
-		dsp_gain_c(ops, dst, src[0], gain[0], n_samples);
-		for (i = 1; i < n_src; i++)
-			dsp_gain_add_c(ops, dst, src[i], gain[i], n_samples);
-	}
-}
-
-static inline void dsp_mult1_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];
-}
-
-void dsp_mult_c(struct dsp_ops *ops,
-		void * SPA_RESTRICT dst,
-		const void * SPA_RESTRICT src[],
-		uint32_t n_src, uint32_t n_samples)
-{
-	uint32_t i;
-	if (n_src == 0) {
-		dsp_clear_c(ops, dst, n_samples);
-	} else {
-		dsp_copy_c(ops, dst, src[0], n_samples);
-		for (i = 1; i < n_src; i++)
-			dsp_mult1_c(ops, dst, src[i], n_samples);
-	}
-}
-
-void dsp_biquad_run_c(struct dsp_ops *ops, struct biquad *bq,
-		float *out, const float *in, uint32_t n_samples)
-{
-	float x, y, x1, x2;
-	float b0, b1, b2, a1, a2;
-	uint32_t i;
-
-	x1 = bq->x1;
-	x2 = bq->x2;
-	b0 = bq->b0;
-	b1 = bq->b1;
-	b2 = bq->b2;
-	a1 = bq->a1;
-	a2 = bq->a2;
-	for (i = 0; i < n_samples; i++) {
-		x  = in[i];
-		y  = b0 * x          + x1;
-		x1 = b1 * x - a1 * y + x2;
-		x2 = b2 * x - a2 * y;
-		out[i] = y;
-	}
-#define F(x) (-FLT_MIN < (x) && (x) < FLT_MIN ? 0.0f : (x))
-	bq->x1 = F(x1);
-	bq->x2 = F(x2);
-#undef F
-}
-
-void dsp_sum_c(struct dsp_ops *ops, float * dst,
-		const float * SPA_RESTRICT a, const float * SPA_RESTRICT b, uint32_t n_samples)
-{
-	uint32_t i;
-	for (i = 0; i < n_samples; i++)
-		dst[i] = a[i] + b[i];
-}
-
-void dsp_linear_c(struct dsp_ops *ops, float * dst,
-		const float * SPA_RESTRICT src, const float mult,
-		const float add, uint32_t n_samples)
-{
-	uint32_t i;
-	if (add == 0.0f) {
-		dsp_gain_c(ops, dst, src, mult, n_samples);
-	} else {
-		if (mult == 0.0f) {
-			for (i = 0; i < n_samples; i++)
-				dst[i] = add;
-		} else if (mult == 1.0f) {
-			for (i = 0; i < n_samples; i++)
-				dst[i] = src[i] + add;
-		} else {
-			for (i = 0; i < n_samples; i++)
-				dst[i] = mult * src[i] + add;
-		}
-	}
-}
-
-void *dsp_fft_new_c(struct dsp_ops *ops, int32_t size, bool real)
-{
-	return pffft_new_setup(size, real ? PFFFT_REAL : PFFFT_COMPLEX);
-}
-
-void dsp_fft_free_c(struct dsp_ops *ops, void *fft)
-{
-	pffft_destroy_setup(fft);
-}
-void dsp_fft_run_c(struct dsp_ops *ops, void *fft, int direction,
-	const float * SPA_RESTRICT src, float * SPA_RESTRICT dst)
-{
-	pffft_transform(fft, src, dst, NULL, direction < 0 ? PFFFT_BACKWARD : PFFFT_FORWARD);
-}
-
-void dsp_fft_cmul_c(struct dsp_ops *ops, void *fft,
-	float * SPA_RESTRICT dst, const float * SPA_RESTRICT a,
-	const float * SPA_RESTRICT b, uint32_t len, const float scale)
-{
-	pffft_zconvolve(fft, a, b, dst, scale);
-}
-
-void dsp_fft_cmuladd_c(struct dsp_ops *ops, void *fft,
-	float * SPA_RESTRICT dst, const float * SPA_RESTRICT src,
-	const float * SPA_RESTRICT a, const float * SPA_RESTRICT b,
-	uint32_t len, const float scale)
-{
-	pffft_zconvolve_accumulate(fft, a, b, src, dst, scale);
-}
-
diff --git a/src/modules/module-filter-chain/dsp-ops-sse.c b/src/modules/module-filter-chain/dsp-ops-sse.c
deleted file mode 100644
index 9eb94f79..00000000
--- a/src/modules/module-filter-chain/dsp-ops-sse.c
+++ /dev/null
@@ -1,122 +0,0 @@
-/* Spa */
-/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */
-/* SPDX-License-Identifier: MIT */
-
-#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 && gain[0] == 1.0f) {
-		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]);
-		}
-	}
-}
-
-void dsp_sum_sse(struct dsp_ops *ops, float *r, const float *a, const float *b, uint32_t n_samples)
-{
-	uint32_t n, unrolled;
-	__m128 in[4];
-
-	unrolled = n_samples & ~15;
-
-	if (SPA_LIKELY(SPA_IS_ALIGNED(r, 16)) &&
-	    SPA_LIKELY(SPA_IS_ALIGNED(a, 16)) &&
-	    SPA_LIKELY(SPA_IS_ALIGNED(b, 16))) {
-		for (n = 0; n < unrolled; n += 16) {
-			in[0] = _mm_load_ps(&a[n+ 0]);
-			in[1] = _mm_load_ps(&a[n+ 4]);
-			in[2] = _mm_load_ps(&a[n+ 8]);
-			in[3] = _mm_load_ps(&a[n+12]);
-
-			in[0] = _mm_add_ps(in[0], _mm_load_ps(&b[n+ 0]));
-			in[1] = _mm_add_ps(in[1], _mm_load_ps(&b[n+ 4]));
-			in[2] = _mm_add_ps(in[2], _mm_load_ps(&b[n+ 8]));
-			in[3] = _mm_add_ps(in[3], _mm_load_ps(&b[n+12]));
-
-			_mm_store_ps(&r[n+ 0], in[0]);
-			_mm_store_ps(&r[n+ 4], in[1]);
-			_mm_store_ps(&r[n+ 8], in[2]);
-			_mm_store_ps(&r[n+12], in[3]);
-		}
-	} else {
-		for (n = 0; n < unrolled; n += 16) {
-			in[0] = _mm_loadu_ps(&a[n+ 0]);
-			in[1] = _mm_loadu_ps(&a[n+ 4]);
-			in[2] = _mm_loadu_ps(&a[n+ 8]);
-			in[3] = _mm_loadu_ps(&a[n+12]);
-
-			in[0] = _mm_add_ps(in[0], _mm_loadu_ps(&b[n+ 0]));
-			in[1] = _mm_add_ps(in[1], _mm_loadu_ps(&b[n+ 4]));
-			in[2] = _mm_add_ps(in[2], _mm_loadu_ps(&b[n+ 8]));
-			in[3] = _mm_add_ps(in[3], _mm_loadu_ps(&b[n+12]));
-
-			_mm_storeu_ps(&r[n+ 0], in[0]);
-			_mm_storeu_ps(&r[n+ 4], in[1]);
-			_mm_storeu_ps(&r[n+ 8], in[2]);
-			_mm_storeu_ps(&r[n+12], in[3]);
-		}
-	}
-	for (; n < n_samples; n++) {
-		in[0] = _mm_load_ss(&a[n]);
-		in[0] = _mm_add_ss(in[0], _mm_load_ss(&b[n]));
-		_mm_store_ss(&r[n], in[0]);
-	}
-}
diff --git a/src/modules/module-filter-chain/dsp-ops.h b/src/modules/module-filter-chain/dsp-ops.h
deleted file mode 100644
index a1a7ab90..00000000
--- a/src/modules/module-filter-chain/dsp-ops.h
+++ /dev/null
@@ -1,136 +0,0 @@
-/* Spa */
-/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */
-/* SPDX-License-Identifier: MIT */
-
-#ifndef DSP_OPS_H
-#define DSP_OPS_H
-
-#include <spa/utils/defs.h>
-
-#include "biquad.h"
-
-struct dsp_ops;
-
-struct dsp_ops_funcs {
-	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 (*biquad_run) (struct dsp_ops *ops, struct biquad *bq,
-			float *out, const float *in, uint32_t n_samples);
-	void (*sum) (struct dsp_ops *ops,
-			float * dst, const float * SPA_RESTRICT a,
-			const float * SPA_RESTRICT b, uint32_t n_samples);
-
-	void *(*fft_new) (struct dsp_ops *ops, int32_t size, bool real);
-	void (*fft_free) (struct dsp_ops *ops, void *fft);
-	void (*fft_run) (struct dsp_ops *ops, void *fft, int direction,
-			const float * SPA_RESTRICT src, float * SPA_RESTRICT dst);
-	void (*fft_cmul) (struct dsp_ops *ops, void *fft,
-			float * SPA_RESTRICT dst, const float * SPA_RESTRICT a,
-			const float * SPA_RESTRICT b, uint32_t len, const float scale);
-	void (*fft_cmuladd) (struct dsp_ops *ops, void *fft,
-			float * dst, const float * src,
-			const float * SPA_RESTRICT a, const float * SPA_RESTRICT b,
-			uint32_t len, const float scale);
-	void (*linear) (struct dsp_ops *ops,
-			float * dst, const float * SPA_RESTRICT src,
-			const float mult, const float add, uint32_t n_samples);
-	void (*mult) (struct dsp_ops *ops,
-			void * SPA_RESTRICT dst,
-			const void * SPA_RESTRICT src[], uint32_t n_src, uint32_t n_samples);
-};
-
-struct dsp_ops {
-	uint32_t cpu_flags;
-
-	void (*free) (struct dsp_ops *ops);
-
-	struct dsp_ops_funcs funcs;
-
-	const void *priv;
-};
-
-int dsp_ops_init(struct dsp_ops *ops, uint32_t cpu_flags);
-
-#define dsp_ops_free(ops)		(ops)->free(ops)
-
-#define dsp_ops_clear(ops,...)		(ops)->funcs.clear(ops, __VA_ARGS__)
-#define dsp_ops_copy(ops,...)		(ops)->funcs.copy(ops, __VA_ARGS__)
-#define dsp_ops_mix_gain(ops,...)	(ops)->funcs.mix_gain(ops, __VA_ARGS__)
-#define dsp_ops_biquad_run(ops,...)	(ops)->funcs.biquad_run(ops, __VA_ARGS__)
-#define dsp_ops_sum(ops,...)		(ops)->funcs.sum(ops, __VA_ARGS__)
-#define dsp_ops_linear(ops,...)		(ops)->funcs.linear(ops, __VA_ARGS__)
-#define dsp_ops_mult(ops,...)		(ops)->funcs.mult(ops, __VA_ARGS__)
-
-#define dsp_ops_fft_new(ops,...)	(ops)->funcs.fft_new(ops, __VA_ARGS__)
-#define dsp_ops_fft_free(ops,...)	(ops)->funcs.fft_free(ops, __VA_ARGS__)
-#define dsp_ops_fft_run(ops,...)	(ops)->funcs.fft_run(ops, __VA_ARGS__)
-#define dsp_ops_fft_cmul(ops,...)	(ops)->funcs.fft_cmul(ops, __VA_ARGS__)
-#define dsp_ops_fft_cmuladd(ops,...)	(ops)->funcs.fft_cmuladd(ops, __VA_ARGS__)
-
-#define MAKE_CLEAR_FUNC(arch) \
-void dsp_clear_##arch(struct dsp_ops *ops, void * SPA_RESTRICT dst, uint32_t n_samples)
-#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)
-#define MAKE_BIQUAD_RUN_FUNC(arch) \
-void dsp_biquad_run_##arch (struct dsp_ops *ops, struct biquad *bq,	\
-	float *out, const float *in, uint32_t n_samples)
-#define MAKE_SUM_FUNC(arch) \
-void dsp_sum_##arch (struct dsp_ops *ops, float * SPA_RESTRICT dst, \
-	const float * SPA_RESTRICT a, const float * SPA_RESTRICT b, uint32_t n_samples)
-#define MAKE_LINEAR_FUNC(arch) \
-void dsp_linear_##arch (struct dsp_ops *ops, float * SPA_RESTRICT dst, \
-	const float * SPA_RESTRICT src, const float mult, const float add, uint32_t n_samples)
-#define MAKE_MULT_FUNC(arch) \
-void dsp_mult_##arch(struct dsp_ops *ops, void * SPA_RESTRICT dst,	\
-	const void * SPA_RESTRICT src[], uint32_t n_src, uint32_t n_samples)
-
-#define MAKE_FFT_NEW_FUNC(arch) \
-void *dsp_fft_new_##arch(struct dsp_ops *ops, int32_t size, bool real)
-#define MAKE_FFT_FREE_FUNC(arch) \
-void dsp_fft_free_##arch(struct dsp_ops *ops, void *fft)
-#define MAKE_FFT_RUN_FUNC(arch) \
-void dsp_fft_run_##arch(struct dsp_ops *ops, void *fft, int direction, \
-	const float * SPA_RESTRICT src, float * SPA_RESTRICT dst)
-#define MAKE_FFT_CMUL_FUNC(arch) \
-void dsp_fft_cmul_##arch(struct dsp_ops *ops, void *fft, \
-	float * SPA_RESTRICT dst, const float * SPA_RESTRICT a, \
-	const float * SPA_RESTRICT b, uint32_t len, const float scale)
-#define MAKE_FFT_CMULADD_FUNC(arch) \
-void dsp_fft_cmuladd_##arch(struct dsp_ops *ops, void *fft,		\
-	float * dst, const float * src,					\
-	const float * SPA_RESTRICT a, const float * SPA_RESTRICT b,	\
-	uint32_t len, const float scale)
-
-MAKE_CLEAR_FUNC(c);
-MAKE_COPY_FUNC(c);
-MAKE_MIX_GAIN_FUNC(c);
-MAKE_BIQUAD_RUN_FUNC(c);
-MAKE_SUM_FUNC(c);
-MAKE_LINEAR_FUNC(c);
-MAKE_MULT_FUNC(c);
-
-MAKE_FFT_NEW_FUNC(c);
-MAKE_FFT_FREE_FUNC(c);
-MAKE_FFT_RUN_FUNC(c);
-MAKE_FFT_CMUL_FUNC(c);
-MAKE_FFT_CMULADD_FUNC(c);
-
-#if defined (HAVE_SSE)
-MAKE_MIX_GAIN_FUNC(sse);
-MAKE_SUM_FUNC(sse);
-#endif
-#if defined (HAVE_AVX)
-MAKE_SUM_FUNC(avx);
-#endif
-
-#endif /* DSP_OPS_H */
diff --git a/src/modules/module-filter-chain/ladspa_plugin.c b/src/modules/module-filter-chain/ladspa_plugin.c
deleted file mode 100644
index bfc21dc4..00000000
--- a/src/modules/module-filter-chain/ladspa_plugin.c
+++ /dev/null
@@ -1,256 +0,0 @@
-/* PipeWire */
-/* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */
-/* SPDX-License-Identifier: MIT */
-
-#include "config.h"
-
-#include <dlfcn.h>
-#include <math.h>
-#include <limits.h>
-
-#include <spa/utils/defs.h>
-#include <spa/utils/list.h>
-#include <spa/utils/string.h>
-
-#include <pipewire/log.h>
-#include <pipewire/utils.h>
-
-#include "plugin.h"
-#include "ladspa.h"
-
-struct plugin {
-	struct fc_plugin plugin;
-	void *handle;
-	LADSPA_Descriptor_Function desc_func;
-};
-
-struct descriptor {
-	struct fc_descriptor desc;
-	const LADSPA_Descriptor *d;
-};
-
-static void *ladspa_instantiate(const struct fc_plugin *plugin, const struct fc_descriptor *desc,
-                        unsigned long SampleRate, int index, const char *config)
-{
-	struct descriptor *d = (struct descriptor *)desc;
-	return d->d->instantiate(d->d, SampleRate);
-}
-
-static const LADSPA_Descriptor *find_desc(LADSPA_Descriptor_Function desc_func, const char *name)
-{
-	unsigned long i;
-	for (i = 0; ;i++) {
-		const LADSPA_Descriptor *d = desc_func(i);
-		if (d == NULL)
-			break;
-		if (spa_streq(d->Label, name))
-			return d;
-	}
-	return NULL;
-}
-
-static float get_default(struct fc_port *port, LADSPA_PortRangeHintDescriptor hint,
-		LADSPA_Data lower, LADSPA_Data upper)
-{
-	LADSPA_Data def;
-
-	switch (hint & LADSPA_HINT_DEFAULT_MASK) {
-	case LADSPA_HINT_DEFAULT_MINIMUM:
-		def = lower;
-		break;
-	case LADSPA_HINT_DEFAULT_MAXIMUM:
-		def = upper;
-		break;
-	case LADSPA_HINT_DEFAULT_LOW:
-		if (LADSPA_IS_HINT_LOGARITHMIC(hint))
-			def = (LADSPA_Data) expf(logf(lower) * 0.75f + logf(upper) * 0.25f);
-		else
-			def = (LADSPA_Data) (lower * 0.75f + upper * 0.25f);
-		break;
-	case LADSPA_HINT_DEFAULT_MIDDLE:
-		if (LADSPA_IS_HINT_LOGARITHMIC(hint))
-			def = (LADSPA_Data) expf(logf(lower) * 0.5f + logf(upper) * 0.5f);
-		else
-			def = (LADSPA_Data) (lower * 0.5f + upper * 0.5f);
-		break;
-	case LADSPA_HINT_DEFAULT_HIGH:
-		if (LADSPA_IS_HINT_LOGARITHMIC(hint))
-			def = (LADSPA_Data) expf(logf(lower) * 0.25f + logf(upper) * 0.75f);
-		else
-			def = (LADSPA_Data) (lower * 0.25f + upper * 0.75f);
-		break;
-	case LADSPA_HINT_DEFAULT_0:
-		def = 0.0f;
-		break;
-	case LADSPA_HINT_DEFAULT_1:
-		def = 1.0f;
-		break;
-	case LADSPA_HINT_DEFAULT_100:
-		def = 100.0f;
-		break;
-	case LADSPA_HINT_DEFAULT_440:
-		def = 440.0f;
-		break;
-	default:
-		if (upper == lower)
-			def = upper;
-		else
-			def = SPA_CLAMPF(0.5f * upper, lower, upper);
-		break;
-	}
-	if (LADSPA_IS_HINT_INTEGER(hint))
-		def = roundf(def);
-	return def;
-}
-
-static void ladspa_port_update_ranges(struct descriptor *dd, struct fc_port *port)
-{
-	const LADSPA_Descriptor *d = dd->d;
-	unsigned long p = port->index;
-	LADSPA_PortRangeHintDescriptor hint = d->PortRangeHints[p].HintDescriptor;
-	LADSPA_Data lower, upper;
-
-	lower = d->PortRangeHints[p].LowerBound;
-	upper = d->PortRangeHints[p].UpperBound;
-
-	port->hint = hint;
-	port->def = get_default(port, hint, lower, upper);
-	port->min = lower;
-	port->max = upper;
-}
-
-static void ladspa_free(const struct fc_descriptor *desc)
-{
-	struct descriptor *d = (struct descriptor*)desc;
-	free(d->desc.ports);
-	free(d);
-}
-
-static const struct fc_descriptor *ladspa_make_desc(struct fc_plugin *plugin, const char *name)
-{
-	struct plugin *p = (struct plugin *)plugin;
-	struct descriptor *desc;
-	const LADSPA_Descriptor *d;
-	uint32_t i;
-
-	d = find_desc(p->desc_func, name);
-	if (d == NULL)
-		return NULL;
-
-	desc = calloc(1, sizeof(*desc));
-	desc->d = d;
-
-	desc->desc.instantiate = ladspa_instantiate;
-	desc->desc.cleanup = d->cleanup;
-	desc->desc.connect_port = d->connect_port;
-	desc->desc.activate = d->activate;
-	desc->desc.deactivate = d->deactivate;
-	desc->desc.run = d->run;
-
-	desc->desc.free = ladspa_free;
-
-	desc->desc.name = d->Label;
-	desc->desc.flags = 0;
-
-	desc->desc.n_ports = d->PortCount;
-	desc->desc.ports = calloc(desc->desc.n_ports, sizeof(struct fc_port));
-
-	for (i = 0; i < desc->desc.n_ports; i++) {
-		desc->desc.ports[i].index = i;
-		desc->desc.ports[i].name = d->PortNames[i];
-		desc->desc.ports[i].flags = d->PortDescriptors[i];
-		ladspa_port_update_ranges(desc, &desc->desc.ports[i]);
-	}
-	return &desc->desc;
-}
-
-static void ladspa_unload(struct fc_plugin *plugin)
-{
-	struct plugin *p = (struct plugin *)plugin;
-	if (p->handle)
-		dlclose(p->handle);
-	free(p);
-}
-
-static struct fc_plugin *ladspa_handle_load_by_path(const char *path)
-{
-	struct plugin *p;
-	int res;
-
-	p = calloc(1, sizeof(*p));
-	if (!p)
-		return NULL;
-
-	p->handle = dlopen(path, RTLD_NOW);
-	if (!p->handle) {
-		pw_log_debug("failed to open '%s': %s", path, dlerror());
-		res = -ENOENT;
-		goto exit;
-	}
-
-	pw_log_info("successfully opened '%s'", path);
-
-	p->desc_func = (LADSPA_Descriptor_Function) dlsym(p->handle, "ladspa_descriptor");
-	if (!p->desc_func) {
-		pw_log_warn("cannot find descriptor function in '%s': %s", path, dlerror());
-		res = -ENOSYS;
-		goto exit;
-	}
-	p->plugin.make_desc = ladspa_make_desc;
-	p->plugin.unload = ladspa_unload;
-
-	return &p->plugin;
-
-exit:
-	if (p->handle)
-		dlclose(p->handle);
-	free(p);
-	errno = -res;
-	return NULL;
-}
-
-struct fc_plugin *load_ladspa_plugin(const struct spa_support *support, uint32_t n_support,
-		struct dsp_ops *dsp, const char *plugin, const struct spa_dict *info)
-{
-	struct fc_plugin *pl = NULL;
-
-	if (plugin[0] != '/') {
-		const char *search_dirs, *p, *state = NULL;
-		char path[PATH_MAX];
-		size_t len;
-
-		search_dirs = getenv("LADSPA_PATH");
-		if (!search_dirs)
-			search_dirs = "/usr/lib64/ladspa:/usr/lib/ladspa:" LIBDIR;
-
-		/*
-		 * set the errno for the case when `ladspa_handle_load_by_path()`
-		 * is never called, which can only happen if the supplied
-		 * LADSPA_PATH contains too long paths
-		 */
-		errno = ENAMETOOLONG;
-
-		while ((p = pw_split_walk(search_dirs, ":", &len, &state))) {
-			int pathlen;
-
-			if (len >= sizeof(path))
-				continue;
-
-			pathlen = snprintf(path, sizeof(path), "%.*s/%s.so", (int) len, p, plugin);
-			if (pathlen < 0 || (size_t) pathlen >= sizeof(path))
-				continue;
-
-			pl = ladspa_handle_load_by_path(path);
-			if (pl != NULL)
-				break;
-		}
-	}
-	else {
-		pl = ladspa_handle_load_by_path(plugin);
-	}
-
-	if (pl == NULL)
-		pw_log_error("failed to load plugin '%s': %s", plugin, strerror(errno));
-
-	return pl;
-}
diff --git a/src/modules/module-filter-chain/plugin.h b/src/modules/module-filter-chain/plugin.h
deleted file mode 100644
index 08932615..00000000
--- a/src/modules/module-filter-chain/plugin.h
+++ /dev/null
@@ -1,86 +0,0 @@
-/* PipeWire */
-/* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */
-/* SPDX-License-Identifier: MIT */
-
-#ifndef PLUGIN_H
-#define PLUGIN_H
-
-#include <stdint.h>
-#include <stddef.h>
-
-#include <spa/support/plugin.h>
-
-#include "dsp-ops.h"
-
-struct fc_plugin {
-	const struct fc_descriptor *(*make_desc)(struct fc_plugin *plugin, const char *name);
-	void (*unload) (struct fc_plugin *plugin);
-};
-
-struct fc_port {
-	uint32_t index;
-	const char *name;
-#define FC_PORT_INPUT	(1ULL << 0)
-#define FC_PORT_OUTPUT	(1ULL << 1)
-#define FC_PORT_CONTROL	(1ULL << 2)
-#define FC_PORT_AUDIO	(1ULL << 3)
-	uint64_t flags;
-
-#define FC_HINT_BOOLEAN		(1ULL << 2)
-#define FC_HINT_SAMPLE_RATE	(1ULL << 3)
-#define FC_HINT_INTEGER		(1ULL << 5)
-	uint64_t hint;
-	float def;
-	float min;
-	float max;
-};
-
-#define FC_IS_PORT_INPUT(x)	((x) & FC_PORT_INPUT)
-#define FC_IS_PORT_OUTPUT(x)	((x) & FC_PORT_OUTPUT)
-#define FC_IS_PORT_CONTROL(x)	((x) & FC_PORT_CONTROL)
-#define FC_IS_PORT_AUDIO(x)	((x) & FC_PORT_AUDIO)
-
-struct fc_descriptor {
-	const char *name;
-#define FC_DESCRIPTOR_SUPPORTS_NULL_DATA	(1ULL << 0)
-#define FC_DESCRIPTOR_COPY			(1ULL << 1)
-	uint64_t flags;
-
-	void (*free) (const struct fc_descriptor *desc);
-
-	uint32_t n_ports;
-	struct fc_port *ports;
-
-	void *(*instantiate) (const struct fc_plugin *plugin, const struct fc_descriptor *desc,
-			unsigned long SampleRate, int index, const char *config);
-
-	void (*cleanup) (void *instance);
-
-	void (*connect_port) (void *instance, unsigned long port, float *data);
-	void (*control_changed) (void *instance);
-
-	void (*activate) (void *instance);
-	void (*deactivate) (void *instance);
-
-	void (*run) (void *instance, unsigned long SampleCount);
-};
-
-static inline void fc_plugin_free(struct fc_plugin *plugin)
-{
-	if (plugin->unload)
-		plugin->unload(plugin);
-}
-
-static inline void fc_descriptor_free(const struct fc_descriptor *desc)
-{
-	if (desc->free)
-		desc->free(desc);
-}
-
-#define FC_PLUGIN_LOAD_FUNC "pipewire__filter_chain_plugin_load"
-
-typedef struct fc_plugin *(fc_plugin_load_func)(const struct spa_support *support, uint32_t n_support,
-		struct dsp_ops *dsp, const char *path, const struct spa_dict *info);
-
-
-#endif /* PLUGIN_H */
diff --git a/src/modules/module-jack-tunnel.c b/src/modules/module-jack-tunnel.c
index 635b4470..48e8c6f8 100644
--- a/src/modules/module-jack-tunnel.c
+++ b/src/modules/module-jack-tunnel.c
@@ -25,6 +25,8 @@
 #include <spa/param/audio/format-utils.h>
 #include <spa/param/latency-utils.h>
 #include <spa/param/audio/raw.h>
+#include <spa/param/audio/raw-json.h>
+#include <spa/control/ump-utils.h>
 
 #include <pipewire/impl.h>
 #include <pipewire/i18n.h>
@@ -108,7 +110,6 @@ PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
 #define MAX_PORTS	128
 
 #define DEFAULT_CLIENT_NAME	"PipeWire"
-#define DEFAULT_CHANNELS	2
 #define DEFAULT_POSITION	"[ FL FR ]"
 #define DEFAULT_MIDI_PORTS	1
 
@@ -259,23 +260,23 @@ static void midi_to_jack(struct impl *impl, float *dst, float *src, uint32_t n_s
 	seq = (struct spa_pod_sequence*)pod;
 
 	SPA_POD_SEQUENCE_FOREACH(seq, c) {
-		switch(c->type) {
-		case SPA_CONTROL_Midi:
-		{
-			uint8_t *data = SPA_POD_BODY(&c->value);
-			size_t size = SPA_POD_BODY_SIZE(&c->value);
+		uint8_t data[16];
+		int size;
 
-			if (impl->fix_midi)
-				fix_midi_event(data, size);
+		if (c->type != SPA_CONTROL_UMP)
+			continue;
 
-			if ((res = jack.midi_event_write(dst, c->offset, data, size)) < 0)
-				pw_log_warn("midi %p: can't write event: %s", dst,
-						spa_strerror(res));
-			break;
-		}
-		default:
-			break;
-		}
+		size = spa_ump_to_midi(SPA_POD_BODY(&c->value),
+				SPA_POD_BODY_SIZE(&c->value), data, sizeof(data));
+		if (size <= 0)
+			continue;
+
+		if (impl->fix_midi)
+			fix_midi_event(data, size);
+
+		if ((res = jack.midi_event_write(dst, c->offset, data, size)) < 0)
+			pw_log_warn("midi %p: can't write event: %s", dst,
+					spa_strerror(res));
 	}
 }
 
@@ -291,9 +292,19 @@ static void jack_to_midi(float *dst, float *src, uint32_t size)
 	spa_pod_builder_push_sequence(&b, &f, 0);
 	for (i = 0; i < count; i++) {
 		jack_midi_event_t ev;
+		uint64_t state = 0;
+
 		jack.midi_event_get(&ev, src, i);
-		spa_pod_builder_control(&b, ev.time, SPA_CONTROL_Midi);
-		spa_pod_builder_bytes(&b, ev.buffer, ev.size);
+
+		while (ev.size > 0) {
+			uint32_t ump[4];
+			int ump_size = spa_ump_from_midi(&ev.buffer, &ev.size, ump, sizeof(ump), 0, &state);
+			if (ump_size <= 0)
+				break;
+
+			spa_pod_builder_control(&b, ev.time, SPA_CONTROL_UMP);
+	                spa_pod_builder_bytes(&b, ump, ump_size);
+		}
 	}
 	spa_pod_builder_pop(&b, &f);
 }
@@ -492,7 +503,7 @@ static void make_stream_ports(struct stream *s)
 		} else {
 			snprintf(name, sizeof(name), "%s_%d", prefix, i - s->info.channels);
 			props = pw_properties_new(
-					PW_KEY_FORMAT_DSP, "8 bit raw midi",
+					PW_KEY_FORMAT_DSP, "32 bit raw UMP",
 					PW_KEY_PORT_NAME, name,
 					PW_KEY_PORT_PHYSICAL, "true",
 					NULL);
@@ -988,45 +999,15 @@ static const struct pw_impl_module_events module_events = {
 	.destroy = module_destroy,
 };
 
-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);
-	info->format = SPA_AUDIO_FORMAT_F32P;
-	info->rate = 0;
-	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));
+	spa_audio_info_raw_init_dict_keys(info,
+			&SPA_DICT_ITEMS(
+				 SPA_DICT_ITEM(SPA_KEY_AUDIO_FORMAT, "F32P"),
+				 SPA_DICT_ITEM(SPA_KEY_AUDIO_POSITION, DEFAULT_POSITION)),
+			&props->dict,
+			SPA_KEY_AUDIO_CHANNELS,
+			SPA_KEY_AUDIO_POSITION, NULL);
 }
 
 static void copy_props(struct impl *impl, struct pw_properties *props, const char *key)
diff --git a/src/modules/module-jackdbus-detect.c b/src/modules/module-jackdbus-detect.c
index 152ba6c9..23bce1b4 100644
--- a/src/modules/module-jackdbus-detect.c
+++ b/src/modules/module-jackdbus-detect.c
@@ -40,6 +40,19 @@
  * There are no module-specific options, all arguments are passed to
  * \ref page_module_jack_tunnel.
  *
+ * ## Config override
+ *
+ * A `module.jackdbus-detect.args` config section can be added
+ * to override the module arguments.
+ *
+ *\code{.unparsed}
+ * # ~/.config/pipewire/pipewire.conf.d/my-jack-dbus-detect-args.conf
+ *
+ * module.jackdbus-detect.args = {
+ *     #tunnel.mode    = duplex
+ * }
+ *\endcode
+ *
  * ## Example configuration
  *\code{.unparsed}
  * # ~/.config/pipewire/pipewire.conf.d/my-jack-dbus-detect.conf
@@ -359,6 +372,9 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
 	impl->context = context;
 	impl->properties = args ? pw_properties_new_string(args) : NULL;
 
+	if (impl->properties)
+		pw_context_conf_update_props(context, "module."NAME".args", impl->properties);
+
 	impl->conn = spa_dbus_get_connection(dbus, SPA_DBUS_TYPE_SESSION);
 	if (impl->conn == NULL) {
 		res = -errno;
diff --git a/src/modules/module-link-factory.c b/src/modules/module-link-factory.c
index 56a0b963..ca2b8a75 100644
--- a/src/modules/module-link-factory.c
+++ b/src/modules/module-link-factory.c
@@ -15,10 +15,109 @@
 #include <pipewire/impl.h>
 
 /** \page page_module_link_factory Link Factory
+ *
+ * Allows clients to create links between ports.
+ *
+ * This module creates a new factory. Clients that can see the factory
+ * can use the factory name (`link-factory`) to create new link
+ * objects with \ref pw_core_create_object(). It is also possible to create
+ * objects in the config file.
+ *
+ * Object of the \ref PW_TYPE_INTERFACE_Link will be created and a proxy
+ * to it will be returned.
+ *
+ * As an argument to the create_object call, a set of properties will
+ * control what ports will be linked.
  *
  * ## Module Name
  *
  * `libpipewire-module-link-factory`
+ *
+ * ## Module Options
+ *
+ * - `allow.link.passive`: if the `link.passive` property is allowed. Default false.
+ *                        By default, the core will decide when a link is passive
+ *                        based on the properties of the node and ports.
+ *
+ * ## Properties for the create_object call
+ *
+ * - `link.output.node`: The output node to use. This can be the node object.id, node.name,
+ *                     node.nick, node.description or object.path of a node. When the
+ *                     property is not given or NULL, the output port should be
+ *                     specified.
+ * - `link.output.port`: The output port to link. This can be a port object.id, port.name,
+ *                     port.alias or object.path. If an output node is specified, the
+ *                     port must belong to the node. Finding a port in a node using the
+ *                     port.id is deprecated and may lead to unexpected results when the
+ *                     port.id also matches an object.id. If no output port is given, an
+ *                     output node must be specified and a random (unlinked) port will
+ *                     be used from the node.
+ * - `link.input.node`: The input node to use. This can be the node object.id, node.name,
+ *                     node.nick, node.description or object.path of a node. When the
+ *                     property is not given or NULL, the input port should be
+ *                     specified.
+ * - `link.input.port`: The input port to link. This can be a port object.id, port.name,
+ *                     port.alias or object.path. If an input node is specified, the
+ *                     port must belong to the node. Finding a port in a node using the
+ *                     port.id is deprecated and may lead to unexpected results when the
+ *                     port.id also matches an object.id. If no input port is given, an
+ *                     input node must be specified and a random (unlinked) port will
+ *                     be used from the node.
+ * - `object.linger`: Keep the link around even when the client that created it is gone.
+ * - `link.passive`: The link is passive, meaning that it will not keep nodes busy.
+ *                 By default this property is ignored and the node and port properties
+ *                 are used to determine the passive state of the link.
+ *
+ * ## Example configuration
+ *
+ * The module is usually added to the config file of the main pipewire daemon.
+ *
+ *\code{.unparsed}
+ * context.modules = [
+ * { name = libpipewire-link-factory
+ *   args = {
+ *       #allow.link.passive = false
+ *   }
+ * }
+ * ]
+ *\endcode
+
+ * ## Config override
+ *
+ * A `module.link-factory.args` config section can be added
+ * to override the module arguments.
+ *
+ *\code{.unparsed}
+ * # ~/.config/pipewire/pipewire.conf.d/my-link-factory-args.conf
+ *
+ * module.link-factory.args = {
+ *     #allow.link.passive = false
+ * }
+ *\endcode
+ *
+ * ## Config objects
+ *
+ * To create an object from the factory, one can use the \ref pw_core_create_object()
+ * method or make an object in the `context.objects` section like:
+ *
+ *\code{.unparsed}
+ * context.objects = [
+ * { factory = link-factory
+ *     args = {
+ *         link.output.node = system
+ *         link.output.port = capture_2
+ *         link.input.node  = my-mic
+ *         link.input.port  = input_FR
+ *     }
+ * }
+ *\endcode
+ *
+ * Note that this only works when the ports that need to be linked are available
+ * at the time the config file is parsed.
+ *
+ * ## See also
+ *
+ * - `pw-link`: a tool to manage port links
  */
 
 #define NAME "link-factory"
@@ -163,8 +262,7 @@ static void link_state_changed(void *data, enum pw_link_state old,
 
 	switch (state) {
 	case PW_LINK_STATE_ERROR:
-		if (ld->linger)
-			pw_work_queue_add(d->work, ld, 0, destroy_link, ld);
+		pw_work_queue_add(d->work, ld, 0, destroy_link, ld);
 		break;
 	default:
 		break;
@@ -274,11 +372,15 @@ static struct pw_impl_port *find_port(struct pw_context *context,
 	if (find.id != SPA_ID_INVALID) {
 		struct pw_global *global = pw_context_find_global(context, find.id);
 		/* find port by global id */
-		if (global != NULL && pw_global_is_type(global, PW_TYPE_INTERFACE_Port))
-			return pw_global_get_object(global);
+		if (global != NULL && pw_global_is_type(global, PW_TYPE_INTERFACE_Port)) {
+			find.port = pw_global_get_object(global);
+			if (find.port != NULL &&
+			    (node == NULL || pw_impl_port_get_node(find.port) == node))
+				return find.port;
+		}
 	}
 	if (node != NULL) {
-		/* find port by local id */
+		/* find port by local id (deprecated) */
 		if (find.id != SPA_ID_INVALID) {
 			find.port = pw_impl_node_find_port(node, find.direction, find.id);
 			if (find.port != NULL)
@@ -544,6 +646,8 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
 	data->props = args ? pw_properties_new_string(args) : NULL;
 
 	if (data->props) {
+		pw_context_conf_update_props(context, "module."NAME".args", data->props);
+
 		data->allow_passive = pw_properties_get_bool(data->props,
 				"allow.link.passive", false);
 	}
diff --git a/src/modules/module-loopback.c b/src/modules/module-loopback.c
index 32eb592f..f4b068be 100644
--- a/src/modules/module-loopback.c
+++ b/src/modules/module-loopback.c
@@ -17,6 +17,7 @@
 #include <spa/utils/json.h>
 #include <spa/utils/ringbuffer.h>
 #include <spa/param/latency-utils.h>
+#include <spa/param/audio/raw-json.h>
 #include <spa/debug/types.h>
 
 #include <pipewire/impl.h>
@@ -77,6 +78,36 @@
  * to and from this common channel layout. This can be used to implement up or
  * downmixing loopback sinks/sources.
  *
+ * ## Example configuration of source to sink link
+ *
+ * This loopback links a source node to a sink node. You can change the target.object
+ * properties to match your source/sink node.name.
+ *
+ *\code{.unparsed}
+ * # ~/.config/pipewire/pipewire.conf.d/my-loopback-0.conf
+ *
+ * context.modules = [
+ * {   name = libpipewire-module-loopback
+ *     args = {
+ *         capture.props = {
+ *             #  if you want to capture sink monitor ports,
+ *             #  uncomment the next line and set the target.object
+ *             #  to the sink name.
+ *             #stream.capture.sink = true
+ *             target.object = "alsa_input.usb-C-Media_Electronics_Inc._TONOR_TC-777_Audio_Device-00.mono-fallback"
+ *             node.passive = true
+ *             node.dont-reconnect = true
+ *         }
+ *         playback.props = {
+ *             target.object = "alsa_output.usb-0d8c_USB_Sound_Device-00.analog-surround-71"
+ *             node.dont-reconnect = true
+ *             node.passive = true
+ *         }
+ *     }
+ * }
+ * ]
+ *\endcode
+ *
  * ## Example configuration of a virtual sink
  *
  * This Virtual sink routes stereo input to the rear channels of a 7.1 sink.
@@ -168,6 +199,35 @@
  * ]
  *\endcode
  *
+ * ## Example configuration of a downmix source
+ *
+ * This Virtual source has 2 input channels and a mono output channel. It will perform
+ * downmixing from the two first AUX channels of a pro-audio device.
+ *
+ *\code{.unparsed}
+ * # ~/.config/pipewire/pipewire.conf.d/my-loopback-4.conf
+ *
+ * context.modules = [
+ * {   name = libpipewire-module-loopback
+ *     args = {
+ *         node.description = "Downmix Source"
+ *         audio.position = [ AUX0 AUX1 ]
+ *         capture.props = {
+ *             node.name = "effect_input.downmix"
+ *             target.object = "alsa_input.usb-BEHRINGER_UMC404HD_192k-00.pro-input-0"
+ *             node.passive = true
+ *             stream.dont-remix = true
+ *         }
+ *         playback.props = {
+ *             node.name = "effect_output.downmix"
+ *             media.class = Audio/Source
+ *             audio.position = [ MONO ]
+ *         }
+ *     }
+ * }
+ * ]
+ *\endcode
+ *
  * ## See also
  *
  * `pw-loopback` : a tool that loads the loopback module with given parameters.
@@ -230,6 +290,9 @@ struct impl {
 	struct spa_hook playback_listener;
 	struct spa_audio_info_raw playback_info;
 
+	struct spa_process_latency_info process_latency[2];
+	struct spa_latency_info latency[2];
+
 	unsigned int do_disconnect:1;
 	unsigned int recalc_delay:1;
 
@@ -277,7 +340,15 @@ static void recalculate_delay(struct impl *impl)
 static void capture_process(void *d)
 {
 	struct impl *impl = d;
-	pw_stream_trigger_process(impl->playback);
+	int res;
+	if ((res = pw_stream_trigger_process(impl->playback)) < 0) {
+		while (true) {
+			struct pw_buffer *t;
+			if ((t = pw_stream_dequeue_buffer(impl->capture)) == NULL)
+				break;
+			pw_stream_queue_buffer(impl->capture, t);
+		}
+	}
 }
 
 static void playback_process(void *d)
@@ -370,24 +441,99 @@ static void playback_process(void *d)
 		pw_stream_queue_buffer(impl->playback, out);
 }
 
+static void build_latency_params(struct impl *impl, struct spa_pod_builder *b,
+		const struct spa_pod *params[], uint32_t max_params)
+{
+	struct spa_latency_info latency;
+	latency = impl->latency[0];
+	spa_process_latency_info_add(&impl->process_latency[0], &latency);
+	params[0] = spa_latency_build(b, SPA_PARAM_Latency, &latency);
+	latency = impl->latency[1];
+	spa_process_latency_info_add(&impl->process_latency[1], &latency);
+	params[1] = spa_latency_build(b, SPA_PARAM_Latency, &latency);
+}
+
+static struct spa_pod *build_props(struct impl *impl, struct spa_pod_builder *b,
+		enum spa_direction direction)
+{
+	int64_t nsec = impl->process_latency[direction].ns;
+
+	return spa_pod_builder_add_object(b,
+			SPA_TYPE_OBJECT_Props, SPA_PARAM_Props,
+			SPA_PROP_latencyOffsetNsec, SPA_POD_Long(nsec));
+}
+
 static void param_latency_changed(struct impl *impl, const struct spa_pod *param,
-		struct pw_stream *other)
+		struct pw_stream *stream, struct pw_stream *other)
 {
 	struct spa_latency_info latency;
 	uint8_t buffer[1024];
 	struct spa_pod_builder b;
-	const struct spa_pod *params[1];
+	const struct spa_pod *params[2];
 
 	if (param == NULL || spa_latency_parse(param, &latency) < 0)
 		return;
 
+	impl->latency[latency.direction] = 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);
+	build_latency_params(impl, &b, params, 2);
+
+	pw_stream_update_params(stream, params, 2);
+	pw_stream_update_params(other, params, 2);
 
 	impl->recalc_delay = true;
 }
 
+static void emit_process_latency_changed(struct impl *impl,
+		enum spa_direction direction, struct pw_stream *stream)
+{
+	uint8_t buffer[4096];
+	struct spa_pod_builder b;
+	const struct spa_pod *params[4];
+
+	spa_pod_builder_init(&b, buffer, sizeof(buffer));
+	params[0] = spa_process_latency_build(&b, SPA_PARAM_ProcessLatency,
+			&impl->process_latency[direction]);
+	if (stream == impl->capture)
+		params[1] = build_props(impl, &b, SPA_DIRECTION_INPUT);
+	else
+		params[1] = build_props(impl, &b, SPA_DIRECTION_OUTPUT);
+	build_latency_params(impl, &b, &params[2], 2);
+	pw_stream_update_params(stream, params, 4);
+}
+
+static void param_process_latency_changed(struct impl *impl, const struct spa_pod *param,
+		enum spa_direction direction, struct pw_stream *stream)
+{
+	struct spa_process_latency_info info;
+
+	if (param == NULL)
+		spa_zero(info);
+	else if (spa_process_latency_parse(param, &info) < 0)
+		return;
+
+	impl->process_latency[direction] = info;
+
+	emit_process_latency_changed(impl, direction, stream);
+}
+
+static void param_props_changed(struct impl *impl, const struct spa_pod *param,
+		enum spa_direction direction, struct pw_stream *stream)
+{
+	int64_t nsec;
+
+	if (!param)
+		nsec = 0;
+	else if (spa_pod_parse_object(param,
+					SPA_TYPE_OBJECT_Props, NULL,
+					SPA_PROP_latencyOffsetNsec, SPA_POD_Long(&nsec)) < 0)
+		return;
+
+	impl->process_latency[direction].ns = nsec;
+	emit_process_latency_changed(impl, direction, stream);
+}
+
 static void param_tag_changed(struct impl *impl, const struct spa_pod *param,
 		struct pw_stream *other)
 {
@@ -506,7 +652,13 @@ static void capture_param_changed(void *data, uint32_t id, const struct spa_pod
 		param_format_changed(impl, param, impl->capture, true);
 		break;
 	case SPA_PARAM_Latency:
-		param_latency_changed(impl, param, impl->playback);
+		param_latency_changed(impl, param, impl->capture, impl->playback);
+		break;
+	case SPA_PARAM_Props:
+		param_props_changed(impl, param, SPA_DIRECTION_INPUT, impl->capture);
+		break;
+	case SPA_PARAM_ProcessLatency:
+		param_process_latency_changed(impl, param, SPA_DIRECTION_INPUT, impl->capture);
 		break;
 	case SPA_PARAM_Tag:
 		param_tag_changed(impl, param, impl->playback);
@@ -551,7 +703,13 @@ static void playback_param_changed(void *data, uint32_t id, const struct spa_pod
 		param_format_changed(impl, param, impl->playback, false);
 		break;
 	case SPA_PARAM_Latency:
-		param_latency_changed(impl, param, impl->capture);
+		param_latency_changed(impl, param, impl->playback, impl->capture);
+		break;
+	case SPA_PARAM_Props:
+		param_props_changed(impl, param, SPA_DIRECTION_OUTPUT, impl->playback);
+		break;
+	case SPA_PARAM_ProcessLatency:
+		param_process_latency_changed(impl, param, SPA_DIRECTION_OUTPUT, impl->playback);
 		break;
 	case SPA_PARAM_Tag:
 		param_tag_changed(impl, param, impl->capture);
@@ -694,43 +852,15 @@ static const struct pw_impl_module_events module_events = {
 	.destroy = module_destroy,
 };
 
-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(struct pw_properties *props, struct spa_audio_info_raw *info)
 {
-	const char *str;
-
-	*info = SPA_AUDIO_INFO_RAW_INIT(
-			.format = SPA_AUDIO_FORMAT_F32P);
-	info->rate = pw_properties_get_int32(props, PW_KEY_AUDIO_RATE, 0);
-	info->channels = pw_properties_get_uint32(props, PW_KEY_AUDIO_CHANNELS, 0);
-	info->channels = SPA_MIN(info->channels, SPA_AUDIO_MAX_CHANNELS);
-	if ((str = pw_properties_get(props, SPA_KEY_AUDIO_POSITION)) != NULL)
-		parse_position(info, str, strlen(str));
+	spa_audio_info_raw_init_dict_keys(info,
+			&SPA_DICT_ITEMS(
+				 SPA_DICT_ITEM(SPA_KEY_AUDIO_FORMAT, "F32P")),
+			&props->dict,
+			SPA_KEY_AUDIO_RATE,
+			SPA_KEY_AUDIO_CHANNELS,
+			SPA_KEY_AUDIO_POSITION, NULL);
 }
 
 static void copy_props(struct impl *impl, struct pw_properties *props, const char *key)
@@ -783,6 +913,8 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
 
 	impl->module = module;
 	impl->context = context;
+	impl->latency[SPA_DIRECTION_INPUT] = SPA_LATENCY_INFO(SPA_DIRECTION_INPUT);
+	impl->latency[SPA_DIRECTION_OUTPUT] = SPA_LATENCY_INFO(SPA_DIRECTION_OUTPUT);
 
 	if (pw_properties_get(props, PW_KEY_NODE_GROUP) == NULL)
 		pw_properties_setf(props, PW_KEY_NODE_GROUP, "loopback-%u-%u", pid, id);
diff --git a/src/modules/module-metadata.c b/src/modules/module-metadata.c
index 2d05fb83..a1255da3 100644
--- a/src/modules/module-metadata.c
+++ b/src/modules/module-metadata.c
@@ -12,14 +12,95 @@
 #include <spa/utils/result.h>
 #include <spa/utils/json.h>
 
+#define PW_API_METADATA_IMPL	SPA_EXPORT
 #include <pipewire/impl.h>
 #include <pipewire/extensions/metadata.h>
 
 /** \page page_module_metadata Metadata
+ *
+ * Allows clients to export a metadata store to the PipeWire server.
+ *
+ * Both the client and the server need to load this module for the metadata
+ * to be exported.
+ *
+ * This module creates a new factory and a new export type for the
+ * \ref PW_TYPE_INTERFACE_Metadata interface.
+ *
+ * A client will first create an implementation of the PW_TYPE_INTERFACE_Metadata
+ * interface with \ref pw_context_create_metadata(), for example. With the
+ * \ref pw_core_export(), this module will create a server side resource to expose
+ * the metadata implementation to other clients. Modifications done by the client
+ * on the local metadata interface will be visible to all PipeWire clients.
+ *
+ * It is also possible to use the factory to create metadata in the current
+ * processes using a config file fragment.
+ *
+ * As an argument to the create_object call, a set of properties will
+ * control the name of the metadata and some initial values.
  *
  * ## Module Name
  *
  * `libpipewire-module-metadata`
+ *
+ * ## Module Options
+ *
+ * This module has no options.
+ *
+ * ## Properties for the create_object call
+ *
+ * - `metadata.name`: The name of the new metadata object. If not given, the metadata
+ *                    object name will be `default`.
+ * - `metadata.values`: A JSON array of objects with initial values for the metadata object.
+ *
+ *   the `metadata.values` key has the following layout:
+ *
+ *  \code{.unparsed}
+ *   metadata.values = [
+ *      { id = <int>  key = <key>  type = <type> value = <object> }
+ *      ....
+ *   ]
+ *  \endcode
+ *
+ *     - `id`: an optional object id for the metadata, default 0
+ *     - `key`: a string, the metadata key
+ *     - `type`: an optional metadata value type
+ *     - `value`: a JSON item, the metadata value.
+ *
+ * ## Example configuration
+ *
+ * The module is usually added to the config file of the main PipeWire daemon and the
+ * clients.
+ *
+ *\code{.unparsed}
+ * context.modules = [
+ * { name = libpipewire-module-metadata }
+ * ]
+ *\endcode
+
+ * ## Config objects
+ *
+ * To create an object from the factory, one can use the \ref pw_core_create_object()
+ * method or make an object in the `context.objects` section like in the main PipeWire
+ * daemon config file:
+ *
+ *\code{.unparsed}
+ * context.objects = [
+ * { factory = metadata
+ *     args = {
+ *         metadata.name = default
+ *         metadata.values = [
+ *            { key = default.audio.sink   value = { name = somesink } }
+ *            { key = default.audio.source value = { name = somesource } }
+ *         ]
+ *     }
+ * }
+ *\endcode
+ *
+ * This creates a new default metadata store with 2 key/values.
+ *
+ * ## See also
+ *
+ * - `pw-metadata`: a tool to manage metadata
  */
 
 #define NAME "metadata"
@@ -66,23 +147,17 @@ struct factory_data {
  */
 static int fill_metadata(struct pw_metadata *metadata, const char *str)
 {
-	struct spa_json it[3];
+	struct spa_json it[2];
 
-	spa_json_init(&it[0], str, strlen(str));
-	if (spa_json_enter_array(&it[0], &it[1]) <= 0)
+	if (spa_json_begin_array(&it[0], str, strlen(str)) <= 0)
 		return -EINVAL;
 
-	while (spa_json_enter_object(&it[1], &it[2]) > 0) {
+	while (spa_json_enter_object(&it[0], &it[1]) > 0) {
 		char key[256], *k = NULL, *v = NULL, *t = NULL;
-		int id = 0;
-
-		while (spa_json_get_string(&it[2], key, sizeof(key)) > 0) {
-			int len;
-			const char *val;
-
-			if ((len = spa_json_next(&it[2], &val)) <= 0)
-				return -EINVAL;
+		int id = 0, len;
+		const char *val;
 
+		while ((len = spa_json_object_next(&it[1], key, sizeof(key), &val)) > 0) {
 			if (spa_streq(key, "id")) {
 				if (spa_json_parse_int(val, len, &id) <= 0)
 					return -EINVAL;
@@ -94,7 +169,7 @@ static int fill_metadata(struct pw_metadata *metadata, const char *str)
 					spa_json_parse_stringn(val, len, t, len+1);
 			} else if (spa_streq(key, "value")) {
 				if (spa_json_is_container(val, len))
-					len = spa_json_container_len(&it[2], val, len);
+					len = spa_json_container_len(&it[1], val, len);
 				if ((v = malloc(len+1)) != NULL)
 					spa_json_parse_stringn(val, len, v, len+1);
 			}
diff --git a/src/modules/module-netjack2-driver.c b/src/modules/module-netjack2-driver.c
index 15806c71..3675a33f 100644
--- a/src/modules/module-netjack2-driver.c
+++ b/src/modules/module-netjack2-driver.c
@@ -26,6 +26,7 @@
 #include <spa/debug/types.h>
 #include <spa/pod/builder.h>
 #include <spa/param/audio/format-utils.h>
+#include <spa/param/audio/raw-json.h>
 #include <spa/param/latency-utils.h>
 #include <spa/param/audio/raw.h>
 
@@ -55,10 +56,12 @@
  * - `driver.mode`: the driver mode, sink|source|duplex, default duplex
  * - `local.ifname = <str>`: interface name to use
  * - `net.ip =<str>`: multicast IP address, default "225.3.19.154"
- * - `net.port =<int>`: control port, default "19000"
+ * - `net.port =<int>`: control port, default 19000
  * - `net.mtu = <int>`: MTU to use, default 1500
  * - `net.ttl = <int>`: TTL to use, default 1
  * - `net.loop = <bool>`: loopback multicast, default false
+ * - `source.ip =<str>`: IP address to bind to, default "0.0.0.0"
+ * - `source.port =<int>`: port to bind to, default 0 (allocate)
  * - `netjack2.client-name`: the name of the NETJACK2 client.
  * - `netjack2.save`: if jack port connections should be save automatically. Can also be
  *                   placed per stream.
@@ -123,6 +126,8 @@ PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
 #define DEFAULT_NET_LOOP	false
 #define DEFAULT_NET_DSCP	34 /* Default to AES-67 AF41 (34) */
 #define MAX_MTU			9000
+#define DEFAULT_SOURCE_IP	"0.0.0.0"
+#define DEFAULT_SOURCE_PORT	0
 
 #define DEFAULT_NETWORK_LATENCY	2
 #define NETWORK_MAX_LATENCY	30
@@ -143,6 +148,8 @@ PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
 			"( net.mtu=<MTU to use, default 1500> ) "		\
 			"( net.ttl=<TTL to use, default 1> ) "			\
 			"( net.loop=<loopback, default false> ) "		\
+			"( source.ip=<ip address to bind, default 0.0.0.0> ) "	\
+			"( source.port=<port to bind, default 0> ) "		\
 			"( netjack2.client-name=<name of the NETJACK2 client> ) "	\
 			"( netjack2.save=<bool, save ports> ) "			\
 			"( netjack2.latency=<latency in cycles, default 2> ) "	\
@@ -395,7 +402,7 @@ static void make_stream_ports(struct stream *s)
 	struct impl *impl = s->impl;
 	uint32_t i;
 	struct pw_properties *props;
-	const char *str, *prefix;
+	const char *str;
 	char name[256];
 	bool is_midi;
 	uint8_t buffer[512];
@@ -403,14 +410,6 @@ static void make_stream_ports(struct stream *s)
 	struct spa_latency_info latency;
 	const struct spa_pod *params[1];
 
-	if (s->direction == PW_DIRECTION_INPUT) {
-		/* sink */
-		prefix = "playback";
-	} else {
-		/* source */
-		prefix = "capture";
-	}
-
 	for (i = 0; i < s->n_ports; i++) {
 		struct port *port = s->ports[i];
 
@@ -422,24 +421,18 @@ static void make_stream_ports(struct stream *s)
 		if (i < s->info.channels) {
 			str = spa_debug_type_find_short_name(spa_type_audio_channel,
 					s->info.position[i % SPA_AUDIO_MAX_CHANNELS]);
-			if (str)
-				snprintf(name, sizeof(name), "%s_%s", prefix, str);
-			else
-				snprintf(name, sizeof(name), "%s_%d", prefix, i);
-
 			props = pw_properties_new(
 					PW_KEY_FORMAT_DSP, "32 bit float mono audio",
 					PW_KEY_AUDIO_CHANNEL, str ? str : "UNK",
 					PW_KEY_PORT_PHYSICAL, "true",
-					PW_KEY_PORT_NAME, name,
 					NULL);
 
 			is_midi = false;
 		} else {
-			snprintf(name, sizeof(name), "%s_%d", prefix, i - s->info.channels);
+			snprintf(name, sizeof(name), "midi%d", i - s->info.channels);
 			props = pw_properties_new(
-					PW_KEY_FORMAT_DSP, "8 bit raw midi",
-					PW_KEY_PORT_NAME, name,
+					PW_KEY_FORMAT_DSP, "32 bit raw UMP",
+					PW_KEY_AUDIO_CHANNEL, name,
 					PW_KEY_PORT_PHYSICAL, "true",
 					NULL);
 
@@ -973,11 +966,15 @@ static int create_netjack2_socket(struct impl *impl)
 	if ((str = pw_properties_get(impl->props, "net.ip")) == NULL)
 		str = DEFAULT_NET_IP;
 	if ((res = pw_net_parse_address(str, port, &impl->dst_addr, &impl->dst_len)) < 0) {
-		pw_log_error("invalid net.ip %s: %s", str, spa_strerror(res));
+		pw_log_error("invalid net.ip:%s port:%d: %s", str, port, spa_strerror(res));
 		goto out;
 	}
-	if ((res = pw_net_parse_address("0.0.0.0", 0, &impl->src_addr, &impl->src_len)) < 0) {
-		pw_log_error("invalid source.ip: %s", spa_strerror(res));
+
+	port = pw_properties_get_uint32(impl->props, "source.port", DEFAULT_SOURCE_PORT);
+	if ((str = pw_properties_get(impl->props, "source.ip")) == NULL)
+		str = DEFAULT_SOURCE_IP;
+	if ((res = pw_net_parse_address(str, port, &impl->src_addr, &impl->src_len)) < 0) {
+		pw_log_error("invalid source.ip:%s port:%d: %s", str, port, spa_strerror(res));
 		goto out;
 	}
 
@@ -1150,45 +1147,15 @@ static const struct pw_impl_module_events module_events = {
 	.destroy = module_destroy,
 };
 
-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);
-	info->format = SPA_AUDIO_FORMAT_F32P;
-	info->rate = 0;
-	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));
+	spa_audio_info_raw_init_dict_keys(info,
+			&SPA_DICT_ITEMS(
+				 SPA_DICT_ITEM(SPA_KEY_AUDIO_FORMAT, "F32P"),
+				 SPA_DICT_ITEM(SPA_KEY_AUDIO_POSITION, DEFAULT_POSITION)),
+			&props->dict,
+			SPA_KEY_AUDIO_CHANNELS,
+			SPA_KEY_AUDIO_POSITION, NULL);
 }
 
 static void copy_props(struct impl *impl, struct pw_properties *props, const char *key)
diff --git a/src/modules/module-netjack2-manager.c b/src/modules/module-netjack2-manager.c
index 4217fbcb..8229b5d5 100644
--- a/src/modules/module-netjack2-manager.c
+++ b/src/modules/module-netjack2-manager.c
@@ -29,6 +29,7 @@
 #include <spa/param/audio/format-utils.h>
 #include <spa/param/latency-utils.h>
 #include <spa/param/audio/raw.h>
+#include <spa/param/audio/raw-json.h>
 
 #include <pipewire/impl.h>
 #include <pipewire/i18n.h>
@@ -61,7 +62,7 @@
  * - `net.ttl = <int>`: TTL to use, default 1
  * - `net.loop = <bool>`: loopback multicast, default false
  * - `netjack2.connect`: if jack ports should be connected automatically. Can also be
- *                   placed per stream.
+ *                   placed per stream, default false.
  * - `netjack2.sample-rate`: the sample rate to use, default 48000
  * - `netjack2.period-size`: the buffer size to use, default 1024
  * - `netjack2.encoding`: the encoding, float|opus|int, default float
@@ -131,6 +132,7 @@ PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
 
 #define NETWORK_MAX_LATENCY	30
 
+#define DEFAULT_CONNECT		false
 #define DEFAULT_SAMPLE_RATE	48000
 #define DEFAULT_PERIOD_SIZE	1024
 #define DEFAULT_ENCODING	"float"
@@ -146,7 +148,7 @@ PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
 			"( net.mtu=<MTU to use, default 1500> ) "		\
 			"( net.ttl=<TTL to use, default 1> ) "			\
 			"( net.loop=<loopback, default false> ) "		\
-			"( netjack2.connect=<bool, autoconnect ports> ) "	\
+			"( netjack2.connect=<autoconnect ports, default false> ) "	\
 			"( netjack2.sample-rate=<sampl erate, default 48000> ) "\
 			"( netjack2.period-size=<period size, default 1024> ) "	\
 			"( midi.ports=<number of midi ports> ) "		\
@@ -390,11 +392,8 @@ static void follower_free(struct follower *follower)
 	free(follower);
 }
 
-static int
-do_stop_follower(struct spa_loop *loop,
-	bool async, uint32_t seq, const void *data, size_t size, void *user_data)
+static int stop_follower(struct follower *follower)
 {
-	struct follower *follower = user_data;
 	follower->started = false;
 	if (follower->source.filter)
 		pw_filter_set_active(follower->source.filter, false);
@@ -421,12 +420,10 @@ static void
 on_setup_io(void *data, int fd, uint32_t mask)
 {
 	struct follower *follower = data;
-	struct impl *impl = follower->impl;
 
 	if (mask & (SPA_IO_ERR | SPA_IO_HUP)) {
 		pw_log_warn("error:%08x", mask);
-		pw_loop_destroy_source(impl->main_loop, follower->setup_socket);
-		follower->setup_socket = NULL;
+		stop_follower(follower);
 		return;
 	}
 	if (mask & SPA_IO_IN) {
@@ -471,7 +468,6 @@ on_data_io(void *data, int fd, uint32_t mask)
 		pw_log_warn("error:%08x", mask);
 		pw_loop_destroy_source(impl->data_loop, follower->socket);
 		follower->socket = NULL;
-		pw_loop_invoke(impl->main_loop, do_stop_follower, 1, NULL, 0, false, follower);
 		return;
 	}
 	if (mask & SPA_IO_IN) {
@@ -517,7 +513,7 @@ static void make_stream_ports(struct stream *s)
 	struct follower *follower = s->follower;
 	uint32_t i;
 	struct pw_properties *props;
-	const char *str, *prefix;
+	const char *str;
 	char name[256];
 	bool is_midi;
 	uint8_t buffer[512];
@@ -525,14 +521,6 @@ static void make_stream_ports(struct stream *s)
 	struct spa_latency_info latency;
 	const struct spa_pod *params[1];
 
-	if (s->direction == PW_DIRECTION_INPUT) {
-		/* sink */
-		prefix = "playback";
-	} else {
-		/* source */
-		prefix = "capture";
-	}
-
 	for (i = 0; i < s->n_ports; i++) {
 		struct port *port = s->ports[i];
 		if (port != NULL) {
@@ -543,25 +531,20 @@ static void make_stream_ports(struct stream *s)
 		if (i < s->info.channels) {
 			str = spa_debug_type_find_short_name(spa_type_audio_channel,
 					s->info.position[i]);
-			if (str)
-				snprintf(name, sizeof(name), "%s_%s", prefix, str);
-			else
-				snprintf(name, sizeof(name), "%s_%d", prefix, i);
 
 			props = pw_properties_new(
 					PW_KEY_FORMAT_DSP, "32 bit float mono audio",
 					PW_KEY_AUDIO_CHANNEL, str ? str : "UNK",
 					PW_KEY_PORT_PHYSICAL, "true",
-					PW_KEY_PORT_NAME, name,
 					NULL);
 
 			is_midi = false;
 		} else {
-			snprintf(name, sizeof(name), "%s_%d", prefix, i - s->info.channels);
+			snprintf(name, sizeof(name), "midi%d", i - s->info.channels);
 			props = pw_properties_new(
-					PW_KEY_FORMAT_DSP, "8 bit raw midi",
-					PW_KEY_PORT_NAME, name,
+					PW_KEY_FORMAT_DSP, "32 bit raw UMP",
 					PW_KEY_PORT_PHYSICAL, "true",
+					PW_KEY_AUDIO_CHANNEL, name,
 					NULL);
 
 			is_midi = true;
@@ -753,7 +736,7 @@ static bool is_multicast(struct sockaddr *sa, socklen_t salen)
 }
 
 static int make_data_socket(struct sockaddr_storage *sa, socklen_t salen,
-		bool loop, int ttl, int dscp, char *ifname)
+		bool loop, int ttl, int dscp, const char *ifname)
 {
 	int af, fd, val, res;
 	struct timeval timeout;
@@ -763,6 +746,13 @@ static int make_data_socket(struct sockaddr_storage *sa, socklen_t salen,
 		pw_log_error("socket failed: %m");
 		return -errno;
 	}
+#ifdef SO_BINDTODEVICE
+	if (ifname && setsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE, ifname, strlen(ifname)) < 0) {
+		res = -errno;
+		pw_log_error("setsockopt(SO_BINDTODEVICE) failed: %m");
+		goto error;
+	}
+#endif
 	if (connect(fd, (struct sockaddr*)sa, salen) < 0) {
 		res = -errno;
 		pw_log_error("connect() failed: %m");
@@ -795,7 +785,7 @@ error:
 }
 
 static int make_announce_socket(struct sockaddr_storage *sa, socklen_t salen,
-		char *ifname)
+		const char *ifname)
 {
 	int af, fd, val, res;
 	struct ifreq req;
@@ -963,7 +953,8 @@ static int handle_follower_available(struct impl *impl, struct nj2_session_param
 		goto create_failed;
 
 	fd = make_data_socket(addr, addr_len, impl->loop,
-			impl->ttl, impl->dscp, NULL);
+			impl->ttl, impl->dscp,
+			pw_properties_get(impl->props, "local.ifname"));
 	if (fd < 0)
 		goto socket_failed;
 
@@ -1089,8 +1080,10 @@ static int create_netjack2_socket(struct impl *impl)
 	impl->ttl = pw_properties_get_uint32(impl->props, "net.ttl", DEFAULT_NET_TTL);
 	impl->loop = pw_properties_get_bool(impl->props, "net.loop", DEFAULT_NET_LOOP);
 	impl->dscp = pw_properties_get_uint32(impl->props, "net.dscp", DEFAULT_NET_DSCP);
+	str = pw_properties_get(impl->props, "local.ifname");
 
-	fd = make_announce_socket(&impl->src_addr, impl->src_len, NULL);
+	fd = make_announce_socket(&impl->src_addr, impl->src_len,
+			pw_properties_get(impl->props, "local.ifname"));
 	if (fd < 0) {
 		res = fd;
 		pw_log_error("can't create socket: %s", spa_strerror(res));
@@ -1176,45 +1169,15 @@ static const struct pw_impl_module_events module_events = {
 	.destroy = module_destroy,
 };
 
-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);
-	info->format = SPA_AUDIO_FORMAT_F32P;
-	info->rate = 0;
-	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));
+	spa_audio_info_raw_init_dict_keys(info,
+			&SPA_DICT_ITEMS(
+				 SPA_DICT_ITEM(SPA_KEY_AUDIO_FORMAT, "F32P"),
+				 SPA_DICT_ITEM(SPA_KEY_AUDIO_POSITION, DEFAULT_POSITION)),
+			&props->dict,
+			SPA_KEY_AUDIO_CHANNELS,
+			SPA_KEY_AUDIO_POSITION, NULL);
 }
 
 static void copy_props(struct impl *impl, struct pw_properties *props, const char *key)
@@ -1329,10 +1292,7 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
 	if (pw_properties_get(props, PW_KEY_NODE_LOCK_RATE) == NULL)
 		pw_properties_set(props, PW_KEY_NODE_LOCK_RATE, "true");
 
-	pw_properties_set(impl->sink_props, PW_KEY_MEDIA_CLASS, "Audio/Sink");
 	pw_properties_set(impl->sink_props, PW_KEY_NODE_NAME, "netjack2_manager_send");
-
-	pw_properties_set(impl->source_props, PW_KEY_MEDIA_CLASS, "Audio/Source");
 	pw_properties_set(impl->source_props, PW_KEY_NODE_NAME, "netjack2_manager_recv");
 
 	if ((str = pw_properties_get(props, "sink.props")) != NULL)
@@ -1349,6 +1309,26 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
 	copy_props(impl, props, PW_KEY_NODE_LOCK_RATE);
 	copy_props(impl, props, PW_KEY_AUDIO_CHANNELS);
 	copy_props(impl, props, SPA_KEY_AUDIO_POSITION);
+	copy_props(impl, props, "netjack2.connect");
+
+	if (pw_properties_get_bool(impl->sink_props, "netjack2.connect", DEFAULT_CONNECT)) {
+		if (pw_properties_get(impl->sink_props, PW_KEY_NODE_AUTOCONNECT) == NULL)
+			pw_properties_set(impl->sink_props, PW_KEY_NODE_AUTOCONNECT, "true");
+		if (pw_properties_get(impl->sink_props, PW_KEY_MEDIA_CLASS) == NULL)
+			pw_properties_set(impl->sink_props, PW_KEY_MEDIA_CLASS, "Stream/Input/Audio");
+	} else {
+		if (pw_properties_get(impl->sink_props, PW_KEY_MEDIA_CLASS) == NULL)
+			pw_properties_set(impl->sink_props, PW_KEY_MEDIA_CLASS, "Audio/Sink");
+	}
+	if (pw_properties_get_bool(impl->source_props, "netjack2.connect", DEFAULT_CONNECT)) {
+		if (pw_properties_get(impl->source_props, PW_KEY_NODE_AUTOCONNECT) == NULL)
+			pw_properties_set(impl->source_props, PW_KEY_NODE_AUTOCONNECT, "true");
+		if (pw_properties_get(impl->source_props, PW_KEY_MEDIA_CLASS) == NULL)
+			pw_properties_set(impl->source_props, PW_KEY_MEDIA_CLASS, "Stream/Output/Audio");
+	} else {
+		if (pw_properties_get(impl->source_props, PW_KEY_MEDIA_CLASS) == NULL)
+			pw_properties_set(impl->source_props, PW_KEY_MEDIA_CLASS, "Audio/Source");
+	}
 
 	impl->core = pw_context_get_object(impl->context, PW_TYPE_INTERFACE_Core);
 	if (impl->core == NULL) {
diff --git a/src/modules/module-netjack2/peer.c b/src/modules/module-netjack2/peer.c
index 9417323a..bac6f132 100644
--- a/src/modules/module-netjack2/peer.c
+++ b/src/modules/module-netjack2/peer.c
@@ -1,5 +1,6 @@
 
-#include <byteswap.h>
+#include <spa/utils/endian.h>
+#include <spa/control/ump-utils.h>
 
 #ifdef HAVE_OPUS_CUSTOM
 #include <opus/opus.h>
@@ -248,7 +249,7 @@ static void midi_to_netjack2(struct netjack2_peer *peer,
 	struct spa_pod_sequence *seq;
 	struct spa_pod_control *c;
 	struct nj2_midi_event *ev;
-	uint32_t free_size;
+	int free_size;
 
 	buf->magic = MIDI_BUFFER_MAGIC;
 	buf->buffer_size = peer->quantum_limit * sizeof(float);
@@ -271,40 +272,41 @@ static void midi_to_netjack2(struct netjack2_peer *peer,
 	free_size = buf->buffer_size - sizeof(*buf);
 
 	SPA_POD_SEQUENCE_FOREACH(seq, c) {
-		switch(c->type) {
-		case SPA_CONTROL_Midi:
-		{
-			uint8_t *data = SPA_POD_BODY(&c->value);
-			size_t size = SPA_POD_BODY_SIZE(&c->value);
-			void *ptr;
-
-			if (c->offset >= n_samples ||
-			    size >= free_size) {
-				buf->lost_events++;
-				continue;
-			}
-			if (peer->fix_midi)
-				fix_midi_event(data, size);
-
-			ev = &buf->event[buf->event_count];
-			ev->time = c->offset;
-			ev->size = size;
-			if (size <= MIDI_INLINE_MAX) {
-				ptr = ev->buffer;
-			} else {
-				buf->write_pos += size;
-				ev->offset = buf->buffer_size - 1 - buf->write_pos;
-				free_size -= size;
-				ptr = SPA_PTROFF(buf, ev->offset, void);
-			}
-			memcpy(ptr, data, size);
-			buf->event_count++;
-			free_size -= sizeof(*ev);
-			break;
+		int size;
+		uint8_t data[16];
+		void *ptr;
+
+		if (c->type != SPA_CONTROL_UMP)
+			continue;
+
+		size = spa_ump_to_midi(SPA_POD_BODY(&c->value),
+				SPA_POD_BODY_SIZE(&c->value), data, sizeof(data));
+		if (size <= 0)
+			continue;
+
+		if (c->offset >= n_samples ||
+		    size >= free_size) {
+			buf->lost_events++;
+			continue;
 		}
-		default:
-			break;
+
+		if (peer->fix_midi)
+			fix_midi_event(data, size);
+
+		ev = &buf->event[buf->event_count];
+		ev->time = c->offset;
+		ev->size = size;
+		if (size <= MIDI_INLINE_MAX) {
+			ptr = ev->buffer;
+		} else {
+			buf->write_pos += size;
+			ev->offset = buf->buffer_size - 1 - buf->write_pos;
+			free_size -= size;
+			ptr = SPA_PTROFF(buf, ev->offset, void);
 		}
+		memcpy(ptr, data, size);
+		buf->event_count++;
+		free_size -= sizeof(*ev);
 	}
 	if (buf->write_pos > 0)
 		memmove(SPA_PTROFF(buf, sizeof(*buf) + buf->event_count * sizeof(struct nj2_midi_event), void),
@@ -322,7 +324,9 @@ static inline void netjack2_to_midi(float *dst, uint32_t size, struct nj2_midi_b
 	spa_pod_builder_push_sequence(&b, &f, 0);
 	for (i = 0; buf != NULL && i < buf->event_count; i++) {
 		struct nj2_midi_event *ev = &buf->event[i];
-		void *data;
+		uint8_t *data;
+		size_t s;
+		uint64_t state = 0;
 
 		if (ev->size <= MIDI_INLINE_MAX)
 			data = ev->buffer;
@@ -331,8 +335,16 @@ static inline void netjack2_to_midi(float *dst, uint32_t size, struct nj2_midi_b
 		else
 			continue;
 
-		spa_pod_builder_control(&b, ev->time, SPA_CONTROL_Midi);
-		spa_pod_builder_bytes(&b, data, ev->size);
+		s = ev->size;
+		while (s > 0) {
+			uint32_t ump[4];
+			int ump_size = spa_ump_from_midi(&data, &s, ump, sizeof(ump), 0, &state);
+			if (ump_size <= 0)
+				break;
+
+			spa_pod_builder_control(&b, ev->time, SPA_CONTROL_UMP);
+	                spa_pod_builder_bytes(&b, ump, ump_size);
+		}
 	}
 	spa_pod_builder_pop(&b, &f);
 }
diff --git a/src/modules/module-parametric-equalizer.c b/src/modules/module-parametric-equalizer.c
index 6d29dcf0..315662f8 100644
--- a/src/modules/module-parametric-equalizer.c
+++ b/src/modules/module-parametric-equalizer.c
@@ -4,11 +4,12 @@
 /* SPDX-License-Identifier: MIT */
 
 #include <errno.h>
+#include <limits.h>
 
 #include "config.h"
 
 #include <spa/utils/result.h>
-#include <spa/utils/json.h>
+#include <spa/utils/json-core.h>
 #include <spa/param/audio/raw.h>
 
 #include <pipewire/impl.h>
@@ -30,6 +31,7 @@
  * Parametric equalizer configuration generated from AutoEQ or Squiglink looks
  * like below.
  *
+ * \code{.unparsed}
  * Preamp: -6.8 dB
  * Filter 1: ON PK Fc 21 Hz Gain 6.7 dB Q 1.100
  * Filter 2: ON PK Fc 85 Hz Gain 6.9 dB Q 3.000
@@ -37,6 +39,7 @@
  * Filter 4: ON PK Fc 210 Hz Gain 5.9 dB Q 2.100
  * Filter 5: ON PK Fc 710 Hz Gain -1.0 dB Q 0.600
  * Filter 6: ON PK Fc 1600 Hz Gain 2.3 dB Q 2.700
+ * \endcode
  *
  * Fc, Gain and Q specify the frequency, gain and Q factor respectively.
  * The fourth column can be one of PK, LSC or HSC specifying peaking, low
@@ -58,7 +61,9 @@
  * - `equalizer.description = <str>`: Name which will show up in
  * - `audio.channels = <int>`: Number of audio channels, default 2
  * - `audio.position = <str>`: Channel map, default "[FL, FR]"
- * - `remote.name =<str>`: environment with remote name, default "pipewire-0"
+ * - `remote.name = <str>`: environment with remote name, default "pipewire-0"
+ * - `capture.props = {}`: properties passed to the input stream, default `{ media.class = "Audio/Sink", node.name = "effect_input.eq<number of nodes>" }`
+ * - `playback.props = {}`: properties passed to the output stream, default `{ node.passive = true, node.name = "effect_output.eq<number of nodes>" }`
  *
  * ## General options
  *
@@ -80,6 +85,12 @@
  *         #equalizer.description = "Parametric EQ Sink"
  *         #audio.channels = 2
  *         #audio.position = [FL, FR]
+ *         #capture.props = {
+ *         #  node.name = "Parametric EQ input"
+ *         #}
+ *         #playback.props = {
+ *         #  node.name = "Parametric EQ output"
+ *         #}
  *     }
  * }
  * ]
@@ -100,8 +111,10 @@ PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
 #define MODULE_USAGE	"( remote.name=<remote> ) "			\
 			"( equalizer.filepath=<filepath> )"		\
 			"( equalizer.description=<description> )"	\
-			"( audio.channels=<number of channels>)"	\
-			"( audio.position=<channel map>)"
+			"( audio.channels=<number of channels> )"	\
+			"( audio.position=<channel map> )"		\
+			"( capture.props=<properties> )"		\
+			"( playback.props=<properties> )"
 
 static const struct spa_dict_item module_props[] = {
 	{ PW_KEY_MODULE_AUTHOR, "Sanchayan Maity <sanchayan@asymptotic.io>" },
@@ -123,8 +136,6 @@ struct impl {
 	struct spa_hook module_listener;
 	struct spa_hook eq_module_listener;
 
-	char position[64];
-	uint32_t channels;
 	unsigned int do_disconnect:1;
 };
 
@@ -148,154 +159,82 @@ static const struct pw_impl_module_events filter_chain_module_events = {
 	.destroy = filter_chain_module_destroy,
 };
 
-void init_eq_node(FILE *f, const char *node_desc)
+static int enhance_properties(struct pw_properties *props, const char *key, ...)
 {
-	fprintf(f, "{\n");
-	fprintf(f, "node.description = \"%s\"\n", node_desc);
-	fprintf(f, "media.name = \"%s\"\n", node_desc);
-	fprintf(f, "filter.graph = {\n");
-	fprintf(f, "nodes = [\n");
-}
-
-void add_eq_node(FILE *f, struct eq_node_param *param, uint32_t eq_band_idx)
-{
-	char str1[64], str2[64];
-
-	fprintf(f, "{\n");
-	fprintf(f, "type = builtin\n");
-	fprintf(f, "name = eq_band_%d\n", eq_band_idx);
-
-	if (strcmp(param->filter_type, "PK") == 0) {
-		fprintf(f, "label = bq_peaking\n");
-	} else if (strcmp(param->filter_type, "LSC") == 0) {
-		fprintf(f, "label = bq_lowshelf\n");
-	} else if (strcmp(param->filter_type, "HSC") == 0) {
-		fprintf(f, "label = bq_highshelf\n");
-	} else {
-		fprintf(f, "label = bq_peaking\n");
-	}
-
-	fprintf(f, "control = { \"Freq\" = %d \"Q\" = %s \"Gain\" = %s }\n", param->freq,
-			spa_json_format_float(str1, sizeof(str1), param->q_fact),
-			spa_json_format_float(str2, sizeof(str2), param->gain));
-
-	fprintf(f, "}\n");
-}
+	FILE *f;
+	spa_autoptr(pw_properties) p = NULL;
+	char *args = NULL;
+	const char *str;
+	size_t size;
+        va_list varargs;
+	int res;
 
-void end_eq_node(struct impl *impl, FILE *f, uint32_t number_of_nodes)
-{
-	fprintf(f, "]\n");
+	if ((str = pw_properties_get(props, key)) == NULL)
+		str = "{}";
+	if ((p = pw_properties_new_string(str)) == NULL)
+		return -errno;
 
-	fprintf(f, "links = [\n");
-	for (uint32_t i = 1; i < number_of_nodes; i++) {
-		fprintf(f, "{ output = \"eq_band_%d:Out\" input = \"eq_band_%d:In\" }\n", i, i + 1);
+	va_start(varargs, key);
+        while (true) {
+		char *k, *v;
+                k = va_arg(varargs, char *);
+		if (k == NULL)
+			break;
+                v = va_arg(varargs, char *);
+		if (v == NULL || pw_properties_get(p, k) == NULL)
+			pw_properties_set(p, k, v);
+        }
+        va_end(varargs);
+
+	if ((f = open_memstream(&args, &size)) == NULL) {
+		res = -errno;
+		pw_log_error("Can't open memstream: %m");
+		return res;
 	}
-	fprintf(f, "]\n");
-
-	fprintf(f, "}\n");
-	fprintf(f, "audio.channels = %d\n", impl->channels);
-	fprintf(f, "audio.position = %s\n", impl->position);
-
-	fprintf(f, "capture.props = {\n");
-	fprintf(f, "node.name = \"effect_input.eq%d\"\n", number_of_nodes);
-	fprintf(f, "media.class = Audio/Sink\n");
-	fprintf(f, "}\n");
-
-	fprintf(f, "playback.props = {\n");
-	fprintf(f, "node.name = \"effect_output.eq%d\"\n", number_of_nodes);
-	fprintf(f, "node.passive = true\n");
-	fprintf(f, "}\n");
+	pw_properties_serialize_dict(f, &p->dict, PW_PROPERTIES_FLAG_ENCLOSE);
+	fclose(f);
 
-	fprintf(f, "}\n");
+	pw_properties_set(props, key, args);
+	free(args);
+	return 0;
 }
 
-int32_t parse_eq_filter_file(struct impl *impl, FILE *f)
+static int create_eq_filter(struct impl *impl, const char *filename)
 {
-	struct eq_node_param eq_param;
-	FILE *memstream = NULL;
+	FILE *f = NULL;
 	const char* str;
-
 	char *args = NULL;
-	char *line = NULL;
-	ssize_t nread;
-	size_t len, size;
-	uint32_t eq_band_idx = 1;
-	uint32_t eq_bands = 0;
+	size_t size;
 	int32_t res = 0;
+	char path[PATH_MAX];
 
-	if ((memstream = open_memstream(&args, &size)) == NULL) {
-		res = -errno;
-		pw_log_error("Can't open memstream: %m");
-		goto done;
+	if ((str = pw_properties_get(impl->props, "equalizer.description")) != NULL) {
+		if (pw_properties_get(impl->props, PW_KEY_NODE_DESCRIPTION) == NULL)
+			pw_properties_set(impl->props, PW_KEY_NODE_DESCRIPTION, str);
+		if (pw_properties_get(impl->props, PW_KEY_MEDIA_NAME) == NULL)
+			pw_properties_set(impl->props, PW_KEY_MEDIA_NAME, str);
 	}
 
-	if ((str = pw_properties_get(impl->props, "equalizer.description")) == NULL)
-		str = DEFAULT_DESCRIPTION;
-	init_eq_node(memstream, str);
-
-	/*
-	 * Read the Preamp gain line.
-	 * Example: Preamp: -6.8 dB
-	 *
-	 * When a pre-amp gain is required, which is usually the case when
-	 * applying EQ, we need to modify the first EQ band to apply a
-	 * bq_highshelf filter at frequency 0 Hz with the provided negative
-	 * gain.
-	 *
-	 * Pre-amp gain is always negative to offset the effect of possible
-	 * clipping introduced by the amplification resulting from EQ.
-	 */
-	spa_zero(eq_param);
-	nread = getline(&line, &len, f);
-	if (nread != -1 && sscanf(line, "%*s %6f %*s", &eq_param.gain) == 1) {
-		memcpy(eq_param.filter, "ON", 2);
-		memcpy(eq_param.filter_type, "HSC", 3);
-		eq_param.freq = 0;
-		eq_param.q_fact = 1.0;
-
-		add_eq_node(memstream, &eq_param, eq_band_idx);
-
-		eq_band_idx++;
-		eq_bands++;
-	}
+	spa_json_encode_string(path, sizeof(path), filename);
+	pw_properties_setf(impl->props, "filter.graph",
+			"{"
+			"  nodes = [ "
+			"    { type = builtin name = eq label = param_eq "
+			"      config = { filename = %s } "
+			"    } "
+			"  ] "
+			"}", path);
 
-	/* Read the filter bands */
-	while ((nread = getline(&line, &len, f)) != -1) {
-		spa_zero(eq_param);
-
-		/*
-		 * On field widths:
-		 * - filter can be ON or OFF
-		 * - filter type can be PK, LSC, HSC
-		 * - freq can be at most 5 decimal digits
-		 * - gain can be -xy.z
-		 * - Q can be x.y00
-		 *
-		 * Use a field width of 6 for gain and Q to account for any
-		 * possible zeros.
-		 */
-		if (sscanf(line, "%*s %*d: %3s %3s %*s %5d %*s %*s %6f %*s %*c %6f",
-					eq_param.filter, eq_param.filter_type, &eq_param.freq,
-					&eq_param.gain, &eq_param.q_fact) == 5) {
-			if (strcmp(eq_param.filter, "ON") == 0) {
-				add_eq_node(memstream, &eq_param, eq_band_idx);
-
-				eq_band_idx++;
-				eq_bands++;
-			}
-		}
-	}
+	enhance_properties(impl->props, "capture.props", PW_KEY_MEDIA_CLASS, "Audio/Sink", NULL);
+	enhance_properties(impl->props, "playback.props", PW_KEY_NODE_PASSIVE, "true", NULL);
 
-	if (eq_bands > 0) {
-		end_eq_node(impl, memstream, eq_bands);
-	} else {
-		pw_log_error("failed to parse equalizer configuration");
+	if ((f = open_memstream(&args, &size)) == NULL) {
 		res = -errno;
+		pw_log_error("Can't open memstream: %m");
 		goto done;
 	}
-
-	fclose(memstream);
-	memstream = NULL;
+	pw_properties_serialize_dict(f, &impl->props->dict, PW_PROPERTIES_FLAG_ENCLOSE);
+	fclose(f);
 
 	pw_log_info("loading new module-filter-chain with args: %s", args);
 	impl->eq_module = pw_context_load_module(impl->context,
@@ -315,10 +254,7 @@ int32_t parse_eq_filter_file(struct impl *impl, FILE *f)
 	res = 0;
 
 done:
-	if (memstream != NULL)
-		fclose(memstream);
 	free(args);
-
 	return res;
 }
 
@@ -377,7 +313,6 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
 	struct pw_properties *props = NULL;
 	struct impl *impl;
 	const char *str;
-	FILE *f = NULL;
 	int res;
 
 	PW_LOG_TOPIC_INIT(mod_topic);
@@ -426,39 +361,17 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
 			&impl->core_listener,
 			&core_events, impl);
 
-	impl->channels = pw_properties_get_uint32(impl->props, PW_KEY_AUDIO_CHANNELS, DEFAULT_CHANNELS);
-	if (impl->channels == 0) {
-		res = -EINVAL;
-		pw_log_error("invalid channels '%d'", impl->channels);
-		goto error;
-	}
-
-	if ((str = pw_properties_get(impl->props, SPA_KEY_AUDIO_POSITION)) == NULL)
-		str = DEFAULT_POSITION;
-	strncpy(impl->position, str, strlen(str));
-
 	if ((str = pw_properties_get(props, "equalizer.filepath")) == NULL) {
-		res = -errno;
-		pw_log_error( "missing property equalizer.filepath: %m");
+		res = -ENOENT;
+		pw_log_error( "missing property equalizer.filepath: %s", spa_strerror(res));
 		goto error;
 	}
 
-	pw_log_info("Loading equalizer file %s for parsing", str);
-
-	if ((f = fopen(str, "r")) == NULL) {
-		res = -errno;
-		pw_log_error("failed to open equalizer file: %m");
+	if ((res = create_eq_filter(impl, str)) < 0) {
+		pw_log_error("failed to parse equalizer file: %s", spa_strerror(res));
 		goto error;
 	}
 
-	if (parse_eq_filter_file(impl, f) == -1) {
-		res = -EINVAL;
-		pw_log_error("failed to parse equalizer file: %m");
-		goto error;
-	}
-
-	fclose(f);
-
 	pw_impl_module_add_listener(module, &impl->module_listener, &module_events, impl);
 
 	pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props));
@@ -466,10 +379,6 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
 	return 0;
 
 error:
-	if (f != NULL)
-		fclose(f);
-
 	impl_destroy(impl);
-
 	return res;
 }
diff --git a/src/modules/module-pipe-tunnel.c b/src/modules/module-pipe-tunnel.c
index 22c809f2..afc8e0f1 100644
--- a/src/modules/module-pipe-tunnel.c
+++ b/src/modules/module-pipe-tunnel.c
@@ -28,6 +28,7 @@
 #include <spa/param/audio/format-utils.h>
 #include <spa/param/latency-utils.h>
 #include <spa/param/audio/raw.h>
+#include <spa/param/audio/raw-json.h>
 
 #include <pipewire/impl.h>
 #include <pipewire/i18n.h>
@@ -123,7 +124,6 @@
 
 #define DEFAULT_FORMAT "S16"
 #define DEFAULT_RATE 48000
-#define DEFAULT_CHANNELS 2
 #define DEFAULT_POSITION "[ FL FR ]"
 
 #define RINGBUFFER_SIZE		(1u << 22)
@@ -196,7 +196,6 @@ struct impl {
 	void *buffer;
 	uint32_t target_buffer;
 
-	struct spa_io_rate_match *rate_match;
 	struct spa_io_position *position;
 
 	struct spa_dll dll;
@@ -364,22 +363,17 @@ static void playback_stream_process(void *data)
 
 static void update_rate(struct impl *impl, uint32_t filled)
 {
-	float error;
+	double error;
 
-	if (impl->rate_match == NULL)
-		return;
-
-	error = (float)impl->target_buffer - (float)(filled);
-	error = SPA_CLAMP(error, -impl->max_error, impl->max_error);
+	error = (double)impl->target_buffer - (double)(filled);
+	error = SPA_CLAMPD(error, -impl->max_error, impl->max_error);
 
 	impl->corr = spa_dll_update(&impl->dll, error);
 	pw_log_debug("error:%f corr:%f current:%u target:%u",
 			error, impl->corr, filled, impl->target_buffer);
 
-	if (!impl->driving) {
-		SPA_FLAG_SET(impl->rate_match->flags, SPA_IO_RATE_MATCH_FLAG_ACTIVE);
-		impl->rate_match->rate = 1.0 / impl->corr;
-	}
+	if (!impl->driving)
+		pw_stream_set_rate(impl->stream, 1.0 / impl->corr);
 }
 
 static void capture_stream_process(void *data)
@@ -452,9 +446,6 @@ static void stream_io_changed(void *data, uint32_t id, void *area, uint32_t size
 {
 	struct impl *impl = data;
 	switch (id) {
-	case SPA_IO_RateMatch:
-		impl->rate_match = area;
-		break;
 	case SPA_IO_Position:
 		impl->position = area;
 		break;
@@ -754,61 +745,18 @@ static const struct pw_impl_module_events module_events = {
 	.destroy = module_destroy,
 };
 
-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 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 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));
+	spa_audio_info_raw_init_dict_keys(info,
+			&SPA_DICT_ITEMS(
+				 SPA_DICT_ITEM(SPA_KEY_AUDIO_FORMAT, DEFAULT_FORMAT),
+				 SPA_DICT_ITEM(SPA_KEY_AUDIO_RATE, SPA_STRINGIFY(DEFAULT_RATE)),
+				 SPA_DICT_ITEM(SPA_KEY_AUDIO_POSITION, DEFAULT_POSITION)),
+			&props->dict,
+			SPA_KEY_AUDIO_FORMAT,
+			SPA_KEY_AUDIO_RATE,
+			SPA_KEY_AUDIO_CHANNELS,
+			SPA_KEY_AUDIO_POSITION, NULL);
 }
 
 static int calc_frame_size(const struct spa_audio_info_raw *info)
diff --git a/src/modules/module-profiler.c b/src/modules/module-profiler.c
index 0b764d57..ec8c5c7f 100644
--- a/src/modules/module-profiler.c
+++ b/src/modules/module-profiler.c
@@ -18,6 +18,7 @@
 #include <spa/utils/ringbuffer.h>
 #include <spa/param/profiler.h>
 
+#define PW_API_PROFILER		SPA_EXPORT
 #include <pipewire/private.h>
 #include <pipewire/impl.h>
 #include <pipewire/extensions/profiler.h>
@@ -34,14 +35,36 @@
  *
  * `libpipewire-module-profiler`
  *
+ * ## Module Options
+ *
+ * - `profile.interval.ms`: Can be used to avoid gathering profiling information
+ *			    on every processing cycle. This allows trading off
+ *			    CPU usage for profiling accuracy. Default 0
+ *
+ * ## Config override
+ *
+ * A `module.profiler.args` config section can be added
+ * to override the module arguments.
+ *
+ *\code{.unparsed}
+ * # ~/.config/pipewire/pipewire.conf.d/my-profiler-args.conf
+ *
+ * module.profiler.args = {
+ *     #profile.interval.ms = 10
+ * }
+ *\endcode
+ *
  * ## Example configuration
  *
- * The module has no arguments and is usually added to the config file of
- * the main pipewire daemon.
+ * The module is usually added to the config file of the main pipewire daemon.
  *
  *\code{.unparsed}
  * context.modules = [
- * { name = libpipewire-module-profiler }
+ * { name = libpipewire-module-profiler
+ *   args = {
+ *       #profile.interval.ms = 0
+ *   }
+ * }
  * ]
  *\endcode
  *
@@ -68,9 +91,14 @@ int pw_protocol_native_ext_profiler_init(struct pw_context *context);
 #define pw_profiler_resource_profile(r,...)        \
         pw_profiler_resource(r,profile,0,__VA_ARGS__)
 
+#define DEFAULT_INTERVAL	0
+
+#define MODULE_USAGE	"( profile.interval.ms=<minimum interval for sampling data (in ms) ) "
+
 static const struct spa_dict_item module_props[] = {
 	{ PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
 	{ PW_KEY_MODULE_DESCRIPTION, "Generate Profiling data" },
+	{ PW_KEY_MODULE_USAGE, MODULE_USAGE },
 	{ PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
 };
 
@@ -109,6 +137,9 @@ struct impl {
 
 	uint8_t *flush;
 	size_t flush_size;
+
+	uint32_t interval;
+	uint64_t last_signal_time;
 };
 
 struct resource_data {
@@ -165,6 +196,13 @@ static void do_flush_event(void *data, uint64_t count)
 		pw_profiler_resource_profile(resource, &p->pod);
 }
 
+static void update_denom(struct spa_fraction *frac, uint32_t denom)
+{
+	if (frac->denom != 0)
+		frac->num = frac->num * denom / frac->denom;
+	frac->denom = denom;
+}
+
 static void context_do_profile(void *data)
 {
 	struct node *n = data;
@@ -182,6 +220,11 @@ static void context_do_profile(void *data)
 	if (SPA_FLAG_IS_SET(pos->clock.flags, SPA_IO_CLOCK_FLAG_FREEWHEEL))
 		return;
 
+	if (a->signal_time - impl->last_signal_time < impl->interval)
+		goto done;
+
+	impl->last_signal_time = a->signal_time;
+
 	spa_pod_builder_init(&b, n->tmp, sizeof(n->tmp));
 	spa_pod_builder_push_object(&b, &f[0],
 			SPA_TYPE_OBJECT_Profiler, 0);
@@ -206,7 +249,9 @@ static void context_do_profile(void *data)
 			SPA_POD_Long(pos->clock.delay),
 			SPA_POD_Double(pos->clock.rate_diff),
 			SPA_POD_Long(pos->clock.next_nsec),
-			SPA_POD_Int(pos->state));
+			SPA_POD_Int(pos->state),
+			SPA_POD_Int(pos->clock.cycle),
+			SPA_POD_Long(pos->clock.xrun));
 
 	spa_pod_builder_prop(&b, SPA_PROFILER_driverBlock, 0);
 	spa_pod_builder_add_struct(&b,
@@ -224,6 +269,8 @@ static void context_do_profile(void *data)
 		struct pw_impl_node *n = t->node;
 		struct pw_node_activation *na;
 		struct spa_fraction latency;
+		struct pw_node_activation *a = n->rt.target.activation;
+		struct spa_io_position *pos = &a->position;
 
 		if (t->id == id)
 			continue;
@@ -233,9 +280,9 @@ static void context_do_profile(void *data)
 			if (n->force_quantum != 0)
 				latency.num = n->force_quantum;
 			if (n->force_rate != 0)
-				latency.denom = n->force_rate;
+				update_denom(&latency, n->force_rate);
 			else if (n->rate.denom != 0)
-				latency.denom = n->rate.denom;
+				update_denom(&latency, n->rate.denom);
 		} else {
 			spa_zero(latency);
 		}
@@ -252,6 +299,21 @@ static void context_do_profile(void *data)
 			SPA_POD_Int(na->status),
 			SPA_POD_Fraction(&latency),
 			SPA_POD_Int(na->xrun_count));
+
+		if (n->driver) {
+			spa_pod_builder_prop(&b, SPA_PROFILER_followerClock, 0);
+			spa_pod_builder_add_struct(&b,
+				SPA_POD_Int(pos->clock.id),
+				SPA_POD_String(pos->clock.name),
+				SPA_POD_Long(pos->clock.nsec),
+				SPA_POD_Fraction(&pos->clock.rate),
+				SPA_POD_Long(pos->clock.position),
+				SPA_POD_Long(pos->clock.duration),
+				SPA_POD_Long(pos->clock.delay),
+				SPA_POD_Double(pos->clock.rate_diff),
+				SPA_POD_Long(pos->clock.next_nsec),
+				SPA_POD_Long(pos->clock.xrun));
+		}
 	}
 	spa_pod_builder_pop(&b, &f[0]);
 
@@ -475,6 +537,12 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
 	impl->properties = props;
 	impl->main_loop = pw_context_get_main_loop(impl->context);
 
+	pw_context_conf_update_props(context, "module."NAME".args", props);
+
+	impl->interval = SPA_NSEC_PER_MSEC *
+		pw_properties_get_uint32(props, "profile.interval.ms", DEFAULT_INTERVAL);
+	impl->last_signal_time = 0;
+
 	impl->global = pw_global_new(context,
 			PW_TYPE_INTERFACE_Profiler,
 			PW_VERSION_PROFILER,
diff --git a/src/modules/module-protocol-native.c b/src/modules/module-protocol-native.c
index c6efc373..c7536bd0 100644
--- a/src/modules/module-protocol-native.c
+++ b/src/modules/module-protocol-native.c
@@ -70,7 +70,7 @@ PW_LOG_TOPIC(mod_topic_connection, "conn." NAME);
  * a client and a server using unix local sockets.
  *
  * Normally this module is loaded in both client and server config files
- * so that they cam communicate.
+ * so that they can communicate.
  *
  * ## Module Name
  *
@@ -130,6 +130,22 @@ PW_LOG_TOPIC(mod_topic_connection, "conn." NAME);
  * local context. This can be done even when the server is not a daemon. It can
  * be used to treat a local context as if it was a server.
  *
+ * ## Config override
+ *
+ * A `module.protocol-native.args` config section can be added
+ * to override the module arguments.
+ *
+ *\code{.unparsed}
+ * # ~/.config/pipewire/pipewire.conf.d/my-protocol-native-args.conf
+ *
+ * module.protocol-native.args = {
+ *        sockets = [
+ *            { name = "pipewire-0" }
+ *            { name = "pipewire-0-manager" }
+ *        ]
+ * }
+ *\endcode
+ *
  * ## Example configuration
  *
  *\code{.unparsed}
@@ -730,7 +746,7 @@ static int init_socket_name(struct server *s, const char *name)
 	const char *runtime_dir;
 	bool path_is_absolute;
 
-	path_is_absolute = name[0] == '/';
+	path_is_absolute = name[0] == '/' || name[0] == '@';
 
 	runtime_dir = get_runtime_dir();
 
@@ -768,6 +784,9 @@ static int lock_socket(struct server *s)
 {
 	int res;
 
+	if (s->addr.sun_path[0] == '\0')
+		return 0;
+
 	snprintf(s->lock_addr, sizeof(s->lock_addr), "%s%s", s->addr.sun_path, LOCK_SUFFIX);
 
 	s->fd_lock = open(s->lock_addr, O_CREAT | O_CLOEXEC,
@@ -923,18 +942,24 @@ static int add_socket(struct pw_protocol *protocol, struct server *s, struct soc
 			res = -errno;
 			goto error;
 		}
-		if (stat(s->addr.sun_path, &socket_stat) < 0) {
-			if (errno != ENOENT) {
-				res = -errno;
-				pw_log_error("server %p: stat %s failed with error: %m",
-						s, s->addr.sun_path);
-				goto error_close;
+		if (s->addr.sun_path[0] == '@') {
+			s->addr.sun_path[0] = 0;
+			size = (socklen_t) (strlen(&s->addr.sun_path[1]) + 1);
+		} else {
+			if (stat(s->addr.sun_path, &socket_stat) < 0) {
+				if (errno != ENOENT) {
+					res = -errno;
+					pw_log_error("server %p: stat %s failed with error: %m",
+							s, s->addr.sun_path);
+					goto error_close;
+				}
+			} else if (socket_stat.st_mode & S_IWUSR || socket_stat.st_mode & S_IWGRP) {
+				unlink(s->addr.sun_path);
 			}
-		} else if (socket_stat.st_mode & S_IWUSR || socket_stat.st_mode & S_IWGRP) {
-			unlink(s->addr.sun_path);
+			size = (socklen_t) (strlen(s->addr.sun_path) + 1);
 		}
 
-		size = offsetof(struct sockaddr_un, sun_path) + strlen(s->addr.sun_path);
+		size += offsetof(struct sockaddr_un, sun_path);
 		if (bind(fd, (struct sockaddr *) &s->addr, size) < 0) {
 			res = -errno;
 			pw_log_error("server %p: bind() failed with error: %m", s);
@@ -959,12 +984,6 @@ static int add_socket(struct pw_protocol *protocol, struct server *s, struct soc
 					s, info->name);
 	}
 
-	res = write_socket_address(s);
-	if (res < 0) {
-		pw_log_error("server %p: failed to write socket address: %s", s,
-				spa_strerror(res));
-		goto error_close;
-	}
 	s->activated = activated;
 	s->loop = pw_context_get_main_loop(protocol->context);
 	if (s->loop == NULL) {
@@ -976,6 +995,11 @@ static int add_socket(struct pw_protocol *protocol, struct server *s, struct soc
 		res = -errno;
 		goto error_close;
 	}
+	res = write_socket_address(s);
+	if (res < 0) {
+		pw_log_warn("server %p: failed to write socket address: %s", s,
+				spa_strerror(res));
+	}
 	return 0;
 
 error_close:
@@ -1311,9 +1335,8 @@ impl_new_client(struct pw_protocol *protocol,
 
 	if (props) {
 		str = spa_dict_lookup(props, PW_KEY_REMOTE_INTENTION);
-		if (str == NULL &&
-		   (str = spa_dict_lookup(props, PW_KEY_REMOTE_NAME)) != NULL &&
-		    spa_streq(str, "internal"))
+		if ((str == NULL || spa_streq(str, "generic")) &&
+		   spa_streq(spa_dict_lookup(props, PW_KEY_REMOTE_NAME), "internal"))
 			str = "internal";
 	}
 	if (str == NULL)
@@ -1659,7 +1682,7 @@ static int create_servers(struct pw_protocol *this, struct pw_impl_core *core,
 		const struct pw_properties *props, const struct pw_properties *args)
 {
 	const char *sockets = args ? pw_properties_get(args, "sockets") : NULL;
-	struct spa_json it[3];
+	struct spa_json it[2];
 	spa_autoptr(pw_properties) p = pw_properties_copy(props);
 
 	if (sockets == NULL) {
@@ -1681,16 +1704,16 @@ static int create_servers(struct pw_protocol *this, struct pw_impl_core *core,
 		return 0;
 	}
 
-	spa_json_init(&it[0], sockets, strlen(sockets));
-
-	if (spa_json_enter_array(&it[0], &it[1]) <= 0)
+	if (spa_json_begin_array(&it[0], sockets, strlen(sockets)) <= 0)
 		goto error_invalid;
 
-	while (spa_json_enter_object(&it[1], &it[2]) > 0) {
+	while (spa_json_enter_object(&it[0], &it[1]) > 0) {
 		struct socket_info info = {0};
 		char key[256];
 		char name[PATH_MAX];
 		char selinux_context[PATH_MAX];
+		const char *value;
+		int len;
 
 		info.uid = getuid();
 		info.gid = getgid();
@@ -1698,13 +1721,7 @@ static int create_servers(struct pw_protocol *this, struct pw_impl_core *core,
 		pw_properties_clear(p);
 		pw_properties_update(p, &props->dict);
 
-		while (spa_json_get_string(&it[2], key, sizeof(key)) > 0) {
-			const char *value;
-			int len;
-
-			if ((len = spa_json_next(&it[2], &value)) <= 0)
-				goto error_invalid;
-
+		while ((len = spa_json_object_next(&it[1], key, sizeof(key), &value)) > 0) {
 			if (spa_streq(key, "name")) {
 				if (spa_json_parse_stringn(value, len, name, sizeof(name)) < 0)
 					goto error_invalid;
@@ -1762,7 +1779,7 @@ static int create_servers(struct pw_protocol *this, struct pw_impl_core *core,
 				info.has_mode = true;
 			} else if (spa_streq(key, "props")) {
 				if (spa_json_is_container(value, len))
-	                                len = spa_json_container_len(&it[2], value, len);
+	                                len = spa_json_container_len(&it[1], value, len);
 
 				pw_properties_update_string(p, value, len);
 			}
@@ -1805,7 +1822,11 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args_str)
 		return -EEXIST;
 	}
 
-	args = args_str ? pw_properties_new_string(args_str) : NULL;
+	args = args_str ? pw_properties_new_string(args_str) : pw_properties_new(NULL, NULL);
+	if (!args)
+		return -errno;
+
+	pw_context_conf_update_props(context, "module."NAME".args", args);
 
 	this = pw_protocol_new(context, PW_TYPE_INFO_PROTOCOL_Native, sizeof(struct protocol_data));
 	if (this == NULL)
diff --git a/src/modules/module-protocol-native/local-socket.c b/src/modules/module-protocol-native/local-socket.c
index f514881a..1e6addef 100644
--- a/src/modules/module-protocol-native/local-socket.c
+++ b/src/modules/module-protocol-native/local-socket.c
@@ -83,6 +83,11 @@ static int try_connect(struct pw_protocol_client *client,
 	else
 		name_size = snprintf(addr.sun_path, sizeof(addr.sun_path), "%s/%s", runtime_dir, name) + 1;
 
+	if (addr.sun_path[0] == '@') {
+		addr.sun_path[0] = '\0';
+		name_size--;
+	}
+
 	if (name_size > (int) sizeof addr.sun_path) {
 		if (runtime_dir == NULL)
 			pw_log_error("client %p: socket path \"%s\" plus null terminator exceeds %i bytes",
@@ -122,14 +127,21 @@ error:
 }
 
 static int try_connect_name(struct pw_protocol_client *client,
-		const char *name,
+		const char *name, bool manager,
 		void (*done_callback) (void *data, int res),
 		void *data)
 {
 	const char *runtime_dir;
+	char path[PATH_MAX];
 	int res;
 
-	if (name[0] == '/') {
+	if (manager && !spa_strendswith(name, "-manager")) {
+		snprintf(path, sizeof(path), "%s-manager", name);
+		res = try_connect_name(client, path, false, done_callback, data);
+		if (res >= 0)
+			return res;
+	}
+	if (name[0] == '/' || name[0] == '@') {
 		return try_connect(client, NULL, name, done_callback, data);
 	} else {
 		runtime_dir = get_runtime_dir();
@@ -152,21 +164,22 @@ int pw_protocol_native_connect_local_socket(struct pw_protocol_client *client,
 					    void *data)
 {
 	const char *name;
-	struct spa_json it[2];
+	struct spa_json it[1];
 	char path[PATH_MAX];
 	int res = -EINVAL;
+	bool manager;
+
+	manager = props && spa_streq(spa_dict_lookup(props, PW_KEY_REMOTE_INTENTION), "manager");
 
 	name = get_remote(props);
 	if (name == NULL)
 		return -EINVAL;
 
-	spa_json_init(&it[0], name, strlen(name));
-
-	if (spa_json_enter_array(&it[0], &it[1]) < 0)
-		return try_connect_name(client, name, done_callback, data);
+	if (spa_json_begin_array(&it[0], name, strlen(name)) <= 0)
+		return try_connect_name(client, name, manager, done_callback, data);
 
-	while (spa_json_get_string(&it[1], path, sizeof(path)) > 0) {
-		res = try_connect_name(client, path, done_callback, data);
+	while (spa_json_get_string(&it[0], path, sizeof(path)) > 0) {
+		res = try_connect_name(client, path, manager, done_callback, data);
 		if (res < 0)
 			continue;
 		break;
diff --git a/src/modules/module-protocol-native/security-context.c b/src/modules/module-protocol-native/security-context.c
index 22da8b55..df0541a9 100644
--- a/src/modules/module-protocol-native/security-context.c
+++ b/src/modules/module-protocol-native/security-context.c
@@ -6,6 +6,8 @@
 #include <pipewire/pipewire.h>
 #include <pipewire/impl.h>
 #include <pipewire/private.h>
+
+#define PW_API_SECURITY_CONTEXT	SPA_EXPORT
 #include <pipewire/extensions/security-context.h>
 
 PW_LOG_TOPIC_EXTERN(mod_topic);
diff --git a/src/modules/module-protocol-pulse.c b/src/modules/module-protocol-pulse.c
index 556fe257..ea5eb6cb 100644
--- a/src/modules/module-protocol-pulse.c
+++ b/src/modules/module-protocol-pulse.c
@@ -256,10 +256,12 @@
  * # Extra commands can be executed here.
  * #   load-module : loads a module with args and flags
  * #      args = "<module-name> <module-args>"
- * #      flags = [ "no-fail" ]
+ * #      ( flags = [ "no-fail" ] )
+ * #      ( condition = [ { <key1> = <value1>, ... } ... ] )
+ * # conditions will check the pulse.properties key/values.
  * pulse.cmd = [
  *     { cmd = "load-module" args = "module-always-sink" flags = [ ] }
- *     #{ cmd = "load-module" args = "module-switch-on-connect" }
+ *     #{ cmd = "load-module" args = "module-switch-on-connect" condition = [ { pulse.cmd.switch-on-connect = true } ]
  *     #{ cmd = "load-module" args = "module-gsettings" flags = [ "nofail" ] }
  * ]
  *\endcode
@@ -296,7 +298,8 @@
  * ## Application settings (Rules)
  *
  * The pulse protocol module supports generic config rules. It supports a pulse.rules
- * section with a `quirks` and an `update-props` action.
+ * section with a `quirks` and an `update-props` action. These rules operate on the client
+ * properties (not the stream properties, see above).
  *
  *\code{.unparsed}
  * # ~/.config/pipewire/pipewire-pulse.conf.d/custom.conf
@@ -338,11 +341,20 @@
  * * `block-source-volume` blocks the client from updating any source volumes. This can be used
  *                    to disable things like automatic gain control.
  * * `block-sink-volume` blocks the client from updating any sink volumes.
+ * * `block-record-stream` blocks the client from creating any record stream.
+ * * `block-playback-stream` blocks the client from creating any playback stream.
  *
  * ### update-props
  *
  * Takes an object with the properties to update on the client. Common actions are to
- * tweak the quantum values.
+ * tweak the quantum values. You can use the stream specific keys in pulse.properties.
+ *
+ * ### startup notification
+ *
+ * A newline will be written into the notification file descriptor when the server has
+ * started if the following environment variable is set:
+ *
+ * - PIPEWIRE_PULSE_NOTIFICATION_FD
  *
  * ## Example configuration
  *
diff --git a/src/modules/module-protocol-pulse/client.c b/src/modules/module-protocol-pulse/client.c
index c6c037f1..454ffe7b 100644
--- a/src/modules/module-protocol-pulse/client.c
+++ b/src/modules/module-protocol-pulse/client.c
@@ -301,23 +301,23 @@ static bool drop_from_out_queue(struct client *client, struct message *m)
 }
 
 /* returns true if an event with the (mask, event, index) triplet should be dropped because it is redundant */
-static bool client_prune_subscribe_events(struct client *client, uint32_t event, uint32_t index)
+static bool client_prune_subscribe_events(struct client *client, uint32_t facility, uint32_t type, uint32_t index)
 {
 	struct message *m, *t;
 
-	if ((event & SUBSCRIPTION_EVENT_TYPE_MASK) == SUBSCRIPTION_EVENT_NEW)
+	if (type == SUBSCRIPTION_EVENT_NEW)
 		return false;
 
 	/* NOTE: reverse iteration */
 	spa_list_for_each_safe_reverse(m, t, &client->out_messages, link) {
 		if (m->type != MESSAGE_TYPE_SUBSCRIPTION_EVENT)
 			continue;
-		if ((m->u.subscription_event.event ^ event) & SUBSCRIPTION_EVENT_FACILITY_MASK)
+		if ((m->u.subscription_event.event & SUBSCRIPTION_EVENT_FACILITY_MASK) != facility)
 			continue;
 		if (m->u.subscription_event.index != index)
 			continue;
 
-		if ((event & SUBSCRIPTION_EVENT_TYPE_MASK) == SUBSCRIPTION_EVENT_REMOVE) {
+		if (type == SUBSCRIPTION_EVENT_REMOVE) {
 			/* This object is being removed, hence there is
 			 * point in keeping the old events regarding
 			 * entry in the queue. */
@@ -338,8 +338,7 @@ static bool client_prune_subscribe_events(struct client *client, uint32_t event,
 			if (is_new)
 				break;
 		}
-
-		if ((event & SUBSCRIPTION_EVENT_TYPE_MASK) == SUBSCRIPTION_EVENT_CHANGE) {
+		else if (type == SUBSCRIPTION_EVENT_CHANGE) {
 			/* This object has changed. If a "new" or "change" event for
 			 * this object is still in the queue we can exit. */
 			goto drop;
@@ -349,28 +348,46 @@ static bool client_prune_subscribe_events(struct client *client, uint32_t event,
 	return false;
 
 drop:
-	pw_log_debug("client %p: dropped redundant event for object %u", client, index);
+	pw_log_debug("client %p: dropped redundant event '%s' on %s #%u",
+			client,
+			subscription_event_type_to_string(type), subscription_event_facility_to_string(facility),
+			index);
 
 	return true;
 }
 
-int client_queue_subscribe_event(struct client *client, uint32_t mask, uint32_t event, uint32_t index)
+int client_queue_subscribe_event(struct client *client, uint32_t facility, uint32_t type, uint32_t index)
 {
+	spa_assert(
+		type == SUBSCRIPTION_EVENT_NEW ||
+		type == SUBSCRIPTION_EVENT_CHANGE ||
+		type == SUBSCRIPTION_EVENT_REMOVE
+	);
+
+	const uint32_t mask = 1u << facility;
+	spa_assert(SUBSCRIPTION_MASK_ALL & mask);
+
 	if (client->disconnect)
 		return -ENOTCONN;
 
 	if (!(client->subscribed & mask))
 		return 0;
 
-	pw_log_debug("client %p: SUBSCRIBE event:%08x index:%u", client, event, index);
+	pw_log_debug("client %p: SUBSCRIBE facility:%s (%u) type:%s (0x%02x) index:%u",
+			client,
+			subscription_event_facility_to_string(facility), facility,
+			subscription_event_type_to_string(type), type,
+			index);
 
-	if (client_prune_subscribe_events(client, event, index))
+	if (client_prune_subscribe_events(client, facility, type, index))
 		return 0;
 
 	struct message *reply = message_alloc(client->impl, -1, 0);
 	if (!reply)
 		return -errno;
 
+	const uint32_t event = facility | type;
+
 	reply->type = MESSAGE_TYPE_SUBSCRIPTION_EVENT;
 	reply->u.subscription_event.event = event;
 	reply->u.subscription_event.index = index;
diff --git a/src/modules/module-protocol-pulse/client.h b/src/modules/module-protocol-pulse/client.h
index e67c1afc..18c3efb7 100644
--- a/src/modules/module-protocol-pulse/client.h
+++ b/src/modules/module-protocol-pulse/client.h
@@ -99,7 +99,7 @@ void client_disconnect(struct client *client);
 void client_free(struct client *client);
 int client_queue_message(struct client *client, struct message *msg);
 int client_flush_messages(struct client *client);
-int client_queue_subscribe_event(struct client *client, uint32_t mask, uint32_t event, uint32_t id);
+int client_queue_subscribe_event(struct client *client, uint32_t facility, uint32_t type, uint32_t index);
 
 void client_update_routes(struct client *client, const char *key, const char *value);
 
diff --git a/src/modules/module-protocol-pulse/cmd.c b/src/modules/module-protocol-pulse/cmd.c
index 0fcd83a9..58f0ce4b 100644
--- a/src/modules/module-protocol-pulse/cmd.c
+++ b/src/modules/module-protocol-pulse/cmd.c
@@ -73,42 +73,50 @@ static int parse_cmd(void *user_data, const char *location,
 	int res = 0;
 
 	spa_autofree char *s = strndup(str, len);
-	spa_json_init(&it[0], s, len);
-	if (spa_json_enter_array(&it[0], &it[1]) < 0) {
+	if (spa_json_begin_array(&it[0], s, len) < 0) {
 		pw_log_error("config file error: pulse.cmd is not an array");
 		return -EINVAL;
 	}
 
-	while (spa_json_enter_object(&it[1], &it[2]) > 0) {
+	while (spa_json_enter_object(&it[0], &it[1]) > 0) {
 		char *cmd = NULL, *args = NULL, *flags = NULL;
+		const char *val;
+		bool have_match = true;
+		int l;
 
-		while (spa_json_get_string(&it[2], key, sizeof(key)) > 0) {
-			const char *val;
-			int len;
-
-			if ((len = spa_json_next(&it[2], &val)) <= 0)
-				break;
-
+		while ((l = spa_json_object_next(&it[1], key, sizeof(key), &val)) > 0) {
 			if (spa_streq(key, "cmd")) {
 				cmd = (char*)val;
-				spa_json_parse_stringn(val, len, cmd, len+1);
+				spa_json_parse_stringn(val, l, cmd, l+1);
 			} else if (spa_streq(key, "args")) {
 				args = (char*)val;
-				spa_json_parse_stringn(val, len, args, len+1);
+				spa_json_parse_stringn(val, l, args, l+1);
 			} else if (spa_streq(key, "flags")) {
-				if (spa_json_is_container(val, len))
-					len = spa_json_container_len(&it[2], val, len);
+				if (spa_json_is_container(val, l))
+					l = spa_json_container_len(&it[1], val, l);
 				flags = (char*)val;
-				spa_json_parse_stringn(val, len, flags, len+1);
+				spa_json_parse_stringn(val, l, flags, l+1);
+			} else if (spa_streq(key, "condition")) {
+				if (!spa_json_is_array(val, l)) {
+					pw_log_warn("expected array for condition in '%.*s'",
+							(int)l, str);
+					break;
+				}
+				spa_json_enter(&it[1], &it[2]);
+				have_match = pw_conf_find_match(&it[2], &impl->props->dict, true);
+			} else {
+				pw_log_warn("unknown pulse.cmd key %s", key);
 			}
 		}
+		if (!have_match)
+			continue;
+
 		if (cmd != NULL)
 			res = do_cmd(impl, cmd, args, flags);
 		if (res < 0)
 			break;
 	}
-
-	return res;
+	return res < 0 ? res : 0;
 }
 
 int cmd_run(struct impl *impl)
diff --git a/src/modules/module-protocol-pulse/defs.h b/src/modules/module-protocol-pulse/defs.h
index 8832f0aa..fa47c3d8 100644
--- a/src/modules/module-protocol-pulse/defs.h
+++ b/src/modules/module-protocol-pulse/defs.h
@@ -153,21 +153,6 @@ static inline int err_to_res(int err)
 	return -EIO;
 }
 
-enum {
-	SUBSCRIPTION_MASK_NULL = 0x0000U,
-	SUBSCRIPTION_MASK_SINK = 0x0001U,
-	SUBSCRIPTION_MASK_SOURCE = 0x0002U,
-	SUBSCRIPTION_MASK_SINK_INPUT = 0x0004U,
-	SUBSCRIPTION_MASK_SOURCE_OUTPUT = 0x0008U,
-	SUBSCRIPTION_MASK_MODULE = 0x0010U,
-	SUBSCRIPTION_MASK_CLIENT = 0x0020U,
-	SUBSCRIPTION_MASK_SAMPLE_CACHE = 0x0040U,
-	SUBSCRIPTION_MASK_SERVER = 0x0080U,
-	SUBSCRIPTION_MASK_AUTOLOAD = 0x0100U,
-	SUBSCRIPTION_MASK_CARD = 0x0200U,
-	SUBSCRIPTION_MASK_ALL = 0x02ffU
-};
-
 enum {
 	SUBSCRIPTION_EVENT_SINK = 0x0000U,
 	SUBSCRIPTION_EVENT_SOURCE = 0x0001U,
@@ -177,7 +162,7 @@ enum {
 	SUBSCRIPTION_EVENT_CLIENT = 0x0005U,
 	SUBSCRIPTION_EVENT_SAMPLE_CACHE = 0x0006U,
 	SUBSCRIPTION_EVENT_SERVER = 0x0007U,
-	SUBSCRIPTION_EVENT_AUTOLOAD = 0x0008U,
+	// SUBSCRIPTION_EVENT_AUTOLOAD = 0x0008U,
 	SUBSCRIPTION_EVENT_CARD = 0x0009U,
 	SUBSCRIPTION_EVENT_FACILITY_MASK = 0x000FU,
 
@@ -187,6 +172,31 @@ enum {
 	SUBSCRIPTION_EVENT_TYPE_MASK = 0x0030U
 };
 
+enum {
+	SUBSCRIPTION_MASK_NULL = 0,
+	SUBSCRIPTION_MASK_SINK = 1u << SUBSCRIPTION_EVENT_SINK,
+	SUBSCRIPTION_MASK_SOURCE = 1u << SUBSCRIPTION_EVENT_SOURCE,
+	SUBSCRIPTION_MASK_SINK_INPUT = 1u << SUBSCRIPTION_EVENT_SINK_INPUT,
+	SUBSCRIPTION_MASK_SOURCE_OUTPUT = 1u << SUBSCRIPTION_EVENT_SOURCE_OUTPUT,
+	SUBSCRIPTION_MASK_MODULE = 1u << SUBSCRIPTION_EVENT_MODULE,
+	SUBSCRIPTION_MASK_CLIENT = 1u << SUBSCRIPTION_EVENT_CLIENT,
+	SUBSCRIPTION_MASK_SAMPLE_CACHE = 1u << SUBSCRIPTION_EVENT_SAMPLE_CACHE,
+	SUBSCRIPTION_MASK_SERVER = 1u << SUBSCRIPTION_EVENT_SERVER,
+	// SUBSCRIPTION_MASK_AUTOLOAD = 1u << SUBSCRIPTION_EVENT_AUTOLOAD,
+	SUBSCRIPTION_MASK_CARD = 1u << SUBSCRIPTION_EVENT_CARD,
+	SUBSCRIPTION_MASK_ALL =
+		  SUBSCRIPTION_MASK_SINK
+		| SUBSCRIPTION_MASK_SOURCE
+		| SUBSCRIPTION_MASK_SINK_INPUT
+		| SUBSCRIPTION_MASK_SOURCE_OUTPUT
+		| SUBSCRIPTION_MASK_MODULE
+		| SUBSCRIPTION_MASK_CLIENT
+		| SUBSCRIPTION_MASK_SAMPLE_CACHE
+		| SUBSCRIPTION_MASK_SERVER
+		// | SUBSCRIPTION_MASK_AUTOLOAD
+		| SUBSCRIPTION_MASK_CARD
+};
+
 enum {
 	STATE_INVALID = -1,
 	STATE_RUNNING = 0,
@@ -236,6 +246,41 @@ enum {
 	SOURCE_FLAT_VOLUME = 0x0080U,
 };
 
+static inline const char *subscription_event_type_to_string(uint32_t type)
+{
+	switch (type) {
+	case SUBSCRIPTION_EVENT_NEW:
+		return "new";
+	case SUBSCRIPTION_EVENT_CHANGE:
+		return "change";
+	case SUBSCRIPTION_EVENT_REMOVE:
+		return "remove";
+	}
+
+	return NULL;
+}
+
+static inline const char *subscription_event_facility_to_string(uint32_t facility)
+{
+	static const char * const strings[] = {
+		[SUBSCRIPTION_EVENT_SINK] = "sink",
+		[SUBSCRIPTION_EVENT_SOURCE] = "source",
+		[SUBSCRIPTION_EVENT_SINK_INPUT] = "sink-input",
+		[SUBSCRIPTION_EVENT_SOURCE_OUTPUT] = "source-output",
+		[SUBSCRIPTION_EVENT_MODULE] = "module",
+		[SUBSCRIPTION_EVENT_CLIENT] = "client",
+		[SUBSCRIPTION_EVENT_SAMPLE_CACHE] = "sample-cache",
+		[SUBSCRIPTION_EVENT_SERVER] = "server",
+		// [SUBSCRIPTION_EVENT_AUTOLOAD] = "autoload",
+		[SUBSCRIPTION_EVENT_CARD] = "card",
+	};
+
+	if (facility >= SPA_N_ELEMENTS(strings))
+		return NULL;
+
+	return strings[facility];
+}
+
 static const char * const port_types[] = {
 	"unknown",
 	"aux",
diff --git a/src/modules/module-protocol-pulse/format.c b/src/modules/module-protocol-pulse/format.c
index 5626fe27..7ce9757c 100644
--- a/src/modules/module-protocol-pulse/format.c
+++ b/src/modules/module-protocol-pulse/format.c
@@ -7,6 +7,7 @@
 #include <spa/param/audio/format.h>
 #include <spa/param/audio/format-utils.h>
 #include <spa/param/audio/raw.h>
+#include <spa/param/audio/raw-json.h>
 #include <spa/utils/json.h>
 
 #include "format.h"
@@ -130,22 +131,12 @@ uint32_t format_pa2id(enum sample_format format)
 
 const char *format_id2name(uint32_t format)
 {
-	int i;
-	for (i = 0; spa_type_audio_format[i].name; i++) {
-		if (spa_type_audio_format[i].type == format)
-			return spa_debug_type_short_name(spa_type_audio_format[i].name);
-	}
-	return "UNKNOWN";
+	return spa_type_audio_format_to_short_name(format);
 }
 
 uint32_t format_name2id(const char *name)
 {
-	int i;
-	for (i = 0; spa_type_audio_format[i].name; i++) {
-		if (spa_streq(name, spa_debug_type_short_name(spa_type_audio_format[i].name)))
-			return spa_type_audio_format[i].type;
-	}
-	return SPA_AUDIO_FORMAT_UNKNOWN;
+	return spa_type_audio_format_from_short_name(name);
 }
 
 uint32_t format_paname2id(const char *name, size_t size)
@@ -230,6 +221,24 @@ uint32_t sample_spec_frame_size(const struct sample_spec *ss)
 	}
 }
 
+void sample_spec_silence(const struct sample_spec *ss, void *data, size_t size)
+{
+	switch (ss->format) {
+	case SPA_AUDIO_FORMAT_U8:
+		memset(data, 0x80, size);
+		break;
+	case SPA_AUDIO_FORMAT_ALAW:
+		memset(data, 0x80 ^ 0x55, size);
+		break;
+	case SPA_AUDIO_FORMAT_ULAW:
+		memset(data, 0x00 ^ 0xff, size);
+		break;
+	default:
+		memset(data, 0, size);
+		break;
+	}
+}
+
 bool sample_spec_valid(const struct sample_spec *ss)
 {
 	return (sample_spec_frame_size(ss) > 0 &&
@@ -289,22 +298,12 @@ uint32_t channel_pa2id(enum channel_position channel)
 
 const char *channel_id2name(uint32_t channel)
 {
-	int i;
-	for (i = 0; spa_type_audio_channel[i].name; i++) {
-		if (spa_type_audio_channel[i].type == channel)
-			return spa_debug_type_short_name(spa_type_audio_channel[i].name);
-	}
-	return "UNK";
+	return spa_type_audio_channel_to_short_name(channel);
 }
 
 uint32_t channel_name2id(const char *name)
 {
-	int i;
-	for (i = 0; spa_type_audio_channel[i].name; i++) {
-		if (strcmp(name, spa_debug_type_short_name(spa_type_audio_channel[i].name)) == 0)
-			return spa_type_audio_channel[i].type;
-	}
-	return SPA_AUDIO_CHANNEL_UNKNOWN;
+	return spa_type_audio_channel_from_short_name(name);
 }
 
 enum channel_position channel_id2pa(uint32_t id, uint32_t *aux)
@@ -354,6 +353,14 @@ void channel_map_to_positions(const struct channel_map *map, uint32_t *pos)
 		pos[i] = map->map[i];
 }
 
+void positions_to_channel_map(const uint32_t *pos, uint32_t channels, struct channel_map *map)
+{
+	uint32_t i;
+	for (i = 0; i < channels; i++)
+		map->map[i] = pos[i];
+	map->channels = channels;
+}
+
 void channel_map_parse(const char *str, struct channel_map *map)
 {
 	const char *p = str;
@@ -440,18 +447,9 @@ void channel_map_parse(const char *str, struct channel_map *map)
 
 void channel_map_parse_position(const char *str, struct channel_map *map)
 {
-	struct spa_json it[2];
-	char v[256];
-
-	spa_json_init(&it[0], str, strlen(str));
-	if (spa_json_enter_array(&it[0], &it[1]) <= 0)
-		spa_json_init(&it[1], str, strlen(str));
-
-	map->channels = 0;
-	while (spa_json_get_string(&it[1], v, sizeof(v)) > 0 &&
-	    map->channels < SPA_AUDIO_MAX_CHANNELS) {
-		map->map[map->channels++] = channel_name2id(v);
-	}
+	uint32_t channels = 0, position[SPA_AUDIO_MAX_CHANNELS];
+	spa_audio_parse_position(str, strlen(str), position, &channels);
+	positions_to_channel_map(position, channels, map);
 }
 
 bool channel_map_valid(const struct channel_map *map)
@@ -806,8 +804,7 @@ static uint32_t format_info_get_format(const struct format_info *info)
 	if ((str = pw_properties_get(info->props, "format.sample_format")) == NULL)
 		return SPA_AUDIO_FORMAT_UNKNOWN;
 
-	spa_json_init(&it[0], str, strlen(str));
-	if ((len = spa_json_next(&it[0], &val)) <= 0)
+	if ((len = spa_json_begin(&it[0], str, strlen(str), &val)) <= 0)
 		return SPA_AUDIO_FORMAT_UNKNOWN;
 
 	if (spa_json_is_string(val, len))
@@ -825,8 +822,7 @@ static int format_info_get_rate(const struct format_info *info)
 	if ((str = pw_properties_get(info->props, "format.rate")) == NULL)
 		return -ENOENT;
 
-	spa_json_init(&it[0], str, strlen(str));
-	if ((len = spa_json_next(&it[0], &val)) <= 0)
+	if ((len = spa_json_begin(&it[0], str, strlen(str), &val)) <= 0)
 		return -EINVAL;
 	if (spa_json_is_int(val, len)) {
 		if (spa_json_parse_int(val, len, &v) <= 0)
@@ -862,8 +858,7 @@ int format_info_to_spec(const struct format_info *info, struct sample_spec *ss,
 	if ((str = pw_properties_get(info->props, "format.channels")) == NULL)
 		return -ENOENT;
 
-	spa_json_init(&it[0], str, strlen(str));
-	if ((len = spa_json_next(&it[0], &val)) <= 0)
+	if ((len = spa_json_begin(&it[0], str, strlen(str), &val)) <= 0)
 		return -EINVAL;
 	if (spa_json_is_float(val, len)) {
 		if (spa_json_parse_float(val, len, &f) <= 0)
@@ -877,8 +872,7 @@ int format_info_to_spec(const struct format_info *info, struct sample_spec *ss,
 		return -ENOTSUP;
 
 	if ((str = pw_properties_get(info->props, "format.channel_map")) != NULL) {
-		spa_json_init(&it[0], str, strlen(str));
-		if ((len = spa_json_next(&it[0], &val)) <= 0)
+		if ((len = spa_json_begin(&it[0], str, strlen(str), &val)) <= 0)
 			return -EINVAL;
 		if (!spa_json_is_string(val, len))
 			return -EINVAL;
diff --git a/src/modules/module-protocol-pulse/format.h b/src/modules/module-protocol-pulse/format.h
index 8f0133f4..d564b182 100644
--- a/src/modules/module-protocol-pulse/format.h
+++ b/src/modules/module-protocol-pulse/format.h
@@ -11,7 +11,7 @@
 struct spa_pod;
 struct spa_pod_builder;
 
-#define RATE_MAX	(48000u*8u)
+#define RATE_MAX	(48000u*16u)
 #define CHANNELS_MAX	(64u)
 
 enum sample_format {
@@ -182,6 +182,7 @@ const char *format_encoding2name(enum encoding enc);
 uint32_t format_encoding2id(enum encoding enc);
 
 uint32_t sample_spec_frame_size(const struct sample_spec *ss);
+void sample_spec_silence(const struct sample_spec *ss, void *data, size_t size);
 bool sample_spec_valid(const struct sample_spec *ss);
 
 void sample_spec_fix(struct sample_spec *ss, struct channel_map *map,
diff --git a/src/modules/module-protocol-pulse/internal.h b/src/modules/module-protocol-pulse/internal.h
index a24296ae..6e7eba7b 100644
--- a/src/modules/module-protocol-pulse/internal.h
+++ b/src/modules/module-protocol-pulse/internal.h
@@ -83,6 +83,6 @@ void impl_add_listener(struct impl *impl,
 		struct spa_hook *listener,
 		const struct impl_events *events, void *data);
 
-void broadcast_subscribe_event(struct impl *impl, uint32_t mask, uint32_t event, uint32_t id);
+void broadcast_subscribe_event(struct impl *impl, uint32_t facility, uint32_t type, uint32_t id);
 
 #endif
diff --git a/src/modules/module-protocol-pulse/module.c b/src/modules/module-protocol-pulse/module.c
index 4b9217a0..0d45399c 100644
--- a/src/modules/module-protocol-pulse/module.c
+++ b/src/modules/module-protocol-pulse/module.c
@@ -106,8 +106,8 @@ int module_unload(struct module *module)
 
 	if (module->loaded)
 		broadcast_subscribe_event(impl,
-			SUBSCRIPTION_MASK_MODULE,
-			SUBSCRIPTION_EVENT_REMOVE | SUBSCRIPTION_EVENT_MODULE,
+			SUBSCRIPTION_EVENT_MODULE,
+			SUBSCRIPTION_EVENT_REMOVE,
 			module->index);
 
 	module_free(module);
diff --git a/src/modules/module-protocol-pulse/modules/module-stream-restore.c b/src/modules/module-protocol-pulse/modules/module-stream-restore.c
index 79481390..bdb9cfd7 100644
--- a/src/modules/module-protocol-pulse/modules/module-stream-restore.c
+++ b/src/modules/module-protocol-pulse/modules/module-stream-restore.c
@@ -177,7 +177,7 @@ static int do_extension_stream_restore_read(struct module *module, struct client
 	reply = reply_new(client, tag);
 
 	spa_dict_for_each(item, &client->routes->dict) {
-		struct spa_json it[3];
+		struct spa_json it[2];
 		const char *value;
 		char name[1024], key[128];
 		char device_name[1024] = "\0";
@@ -185,52 +185,52 @@ static int do_extension_stream_restore_read(struct module *module, struct client
 		struct volume vol = VOLUME_INIT;
 		struct channel_map map = CHANNEL_MAP_INIT;
 		float volume = 0.0f;
+		int len;
 
 		if (key_to_name(item->key, name, sizeof(name)) < 0)
 			continue;
 
 		pw_log_debug("%s -> %s: %s", item->key, name, item->value);
 
-		spa_json_init(&it[0], item->value, strlen(item->value));
-		if (spa_json_enter_object(&it[0], &it[1]) <= 0)
+		if (spa_json_begin_object(&it[0], item->value, strlen(item->value)) <= 0)
 			continue;
 
-		while (spa_json_get_string(&it[1], key, sizeof(key)) > 0) {
+		while ((len = spa_json_object_next(&it[0], key, sizeof(key), &value)) > 0) {
 			if (spa_streq(key, "volume")) {
-				if (spa_json_get_float(&it[1], &volume) <= 0)
+				if (spa_json_parse_float(value, len, &volume) <= 0)
 					continue;
 			}
 			else if (spa_streq(key, "mute")) {
-				if (spa_json_get_bool(&it[1], &mute) <= 0)
+				if (spa_json_parse_bool(value, len, &mute) <= 0)
 					continue;
 			}
 			else if (spa_streq(key, "volumes")) {
 				vol = VOLUME_INIT;
-				if (spa_json_enter_array(&it[1], &it[2]) <= 0)
+				if (!spa_json_is_array(value, len))
 					continue;
 
+				spa_json_enter(&it[0], &it[1]);
 				for (vol.channels = 0; vol.channels < CHANNELS_MAX; vol.channels++) {
-					if (spa_json_get_float(&it[2], &vol.values[vol.channels]) <= 0)
+					if (spa_json_get_float(&it[1], &vol.values[vol.channels]) <= 0)
 						break;
 				}
 			}
 			else if (spa_streq(key, "channels")) {
-				if (spa_json_enter_array(&it[1], &it[2]) <= 0)
+				if (!spa_json_is_array(value, len))
 					continue;
 
+				spa_json_enter(&it[0], &it[1]);
 				for (map.channels = 0; map.channels < CHANNELS_MAX; map.channels++) {
 					char chname[16];
-					if (spa_json_get_string(&it[2], chname, sizeof(chname)) <= 0)
+					if (spa_json_get_string(&it[1], chname, sizeof(chname)) <= 0)
 						break;
 					map.map[map.channels] = channel_name2id(chname);
 				}
 			}
 			else if (spa_streq(key, "target-node")) {
-				if (spa_json_get_string(&it[1], device_name, sizeof(device_name)) <= 0)
+				if (spa_json_parse_stringn(value, len, device_name, sizeof(device_name)) <= 0)
 					continue;
 			}
-			else if (spa_json_next(&it[1], &value) <= 0)
-				break;
 		}
 		message_put(reply,
 			TAG_STRING, name,
diff --git a/src/modules/module-protocol-pulse/pulse-server.c b/src/modules/module-protocol-pulse/pulse-server.c
index 313054b5..cb66f75e 100644
--- a/src/modules/module-protocol-pulse/pulse-server.c
+++ b/src/modules/module-protocol-pulse/pulse-server.c
@@ -100,13 +100,13 @@ static struct sample *find_sample(struct impl *impl, uint32_t index, const char
 	return NULL;
 }
 
-void broadcast_subscribe_event(struct impl *impl, uint32_t mask, uint32_t event, uint32_t index)
+void broadcast_subscribe_event(struct impl *impl, uint32_t facility, uint32_t type, uint32_t index)
 {
 	struct server *s;
 	spa_list_for_each(s, &impl->servers, link) {
 		struct client *c;
 		spa_list_for_each(c, &s->clients, link)
-			client_queue_subscribe_event(c, mask, event, index);
+			client_queue_subscribe_event(c, facility, type, index);
 	}
 }
 
@@ -203,46 +203,36 @@ static struct stream *find_stream(struct client *client, uint32_t index)
 static int send_object_event(struct client *client, struct pw_manager_object *o,
 		uint32_t type)
 {
-	uint32_t event = 0, mask = 0, res_index = o->index;
+	uint32_t event = 0, res_index = o->index;
 
 	pw_log_debug("index:%d id:%d %08" PRIx64 " type:%u", o->index, o->id, o->change_mask, type);
 
 	if (pw_manager_object_is_sink(o) && o->change_mask & PW_MANAGER_OBJECT_FLAG_SINK) {
 		client_queue_subscribe_event(client,
-				SUBSCRIPTION_MASK_SINK,
-				SUBSCRIPTION_EVENT_SINK | type,
+				SUBSCRIPTION_EVENT_SINK,
+				type,
 				res_index);
 	}
-	if (pw_manager_object_is_source_or_monitor(o) && o->change_mask & PW_MANAGER_OBJECT_FLAG_SOURCE) {
-		mask = SUBSCRIPTION_MASK_SOURCE;
+
+	if (pw_manager_object_is_source_or_monitor(o) && o->change_mask & PW_MANAGER_OBJECT_FLAG_SOURCE)
 		event = SUBSCRIPTION_EVENT_SOURCE;
-	}
-	else if (pw_manager_object_is_sink_input(o)) {
-		mask = SUBSCRIPTION_MASK_SINK_INPUT;
+	else if (pw_manager_object_is_sink_input(o))
 		event = SUBSCRIPTION_EVENT_SINK_INPUT;
-	}
-	else if (pw_manager_object_is_source_output(o)) {
-		mask = SUBSCRIPTION_MASK_SOURCE_OUTPUT;
+	else if (pw_manager_object_is_source_output(o))
 		event = SUBSCRIPTION_EVENT_SOURCE_OUTPUT;
-	}
-	else if (pw_manager_object_is_module(o)) {
-		mask = SUBSCRIPTION_MASK_MODULE;
+	else if (pw_manager_object_is_module(o))
 		event = SUBSCRIPTION_EVENT_MODULE;
-	}
-	else if (pw_manager_object_is_client(o)) {
-		mask = SUBSCRIPTION_MASK_CLIENT;
+	else if (pw_manager_object_is_client(o))
 		event = SUBSCRIPTION_EVENT_CLIENT;
-	}
-	else if (pw_manager_object_is_card(o)) {
-		mask = SUBSCRIPTION_MASK_CARD;
+	else if (pw_manager_object_is_card(o))
 		event = SUBSCRIPTION_EVENT_CARD;
-	} else
+	else
 		event = SPA_ID_INVALID;
 
 	if (event != SPA_ID_INVALID)
 		client_queue_subscribe_event(client,
-				mask,
-				event | type,
+				event,
+				type,
 				res_index);
 	return 0;
 }
@@ -370,8 +360,8 @@ static void send_latency_offset_subscribe_event(struct client *client, struct pw
 
 	if (changed)
 		client_queue_subscribe_event(client,
-				SUBSCRIPTION_MASK_CARD,
-				SUBSCRIPTION_EVENT_CARD | SUBSCRIPTION_EVENT_CHANGE,
+				SUBSCRIPTION_EVENT_CARD,
+				SUBSCRIPTION_EVENT_CHANGE,
 				id_to_index(manager, card_id));
 }
 
@@ -398,9 +388,8 @@ static void send_default_change_subscribe_event(struct client *client, bool sink
 
 	if (changed)
 		client_queue_subscribe_event(client,
-				SUBSCRIPTION_MASK_SERVER,
-				SUBSCRIPTION_EVENT_CHANGE |
 				SUBSCRIPTION_EVENT_SERVER,
+				SUBSCRIPTION_EVENT_CHANGE,
 				-1);
 }
 
@@ -928,29 +917,6 @@ static void manager_object_data_timeout(void *data, struct pw_manager_object *o,
 		temporary_move_target_timeout(client, o);
 }
 
-static int json_object_find(const char *obj, const char *key, char *value, size_t len)
-{
-	struct spa_json it[2];
-	const char *v;
-	char k[128];
-
-	spa_json_init(&it[0], obj, strlen(obj));
-	if (spa_json_enter_object(&it[0], &it[1]) <= 0)
-		return -EINVAL;
-
-	while (spa_json_get_string(&it[1], k, sizeof(k)) > 0) {
-		if (spa_streq(k, key)) {
-			if (spa_json_get_string(&it[1], value, len) <= 0)
-				continue;
-			return 0;
-		} else {
-			if (spa_json_next(&it[1], &v) <= 0)
-				break;
-		}
-	}
-	return -ENOENT;
-}
-
 static void manager_metadata(void *data, struct pw_manager_object *o,
 		uint32_t subject, const char *key, const char *type, const char *value)
 {
@@ -965,7 +931,7 @@ static void manager_metadata(void *data, struct pw_manager_object *o,
 
 		if (key == NULL || spa_streq(key, "default.audio.sink")) {
 			if (value != NULL) {
-				if (json_object_find(value,
+				if (spa_json_str_object_find(value, strlen(value),
 						"name", name, sizeof(name)) < 0)
 					value = NULL;
 				else
@@ -980,7 +946,7 @@ static void manager_metadata(void *data, struct pw_manager_object *o,
 		}
 		if (key == NULL || spa_streq(key, "default.audio.source")) {
 			if (value != NULL) {
-				if (json_object_find(value,
+				if (spa_json_str_object_find(value, strlen(value),
 						"name", name, sizeof(name)) < 0)
 					value = NULL;
 				else
@@ -1143,7 +1109,7 @@ static void stream_state_changed(void *data, enum pw_stream_state old,
 
 	switch (state) {
 	case PW_STREAM_STATE_ERROR:
-		reply_error(client, -1, stream->create_tag, -EIO);
+		reply_error(client, -1, stream->create_tag, -errno);
 		destroy_stream = true;
 		break;
 	case PW_STREAM_STATE_UNCONNECTED:
@@ -1424,20 +1390,7 @@ static void stream_process(void *data)
 		if (avail < (int32_t)minreq || stream->corked) {
 			/* underrun, produce a silence buffer */
 			size = SPA_MIN(d->maxsize, minreq);
-			switch (stream->ss.format) {
-			case SPA_AUDIO_FORMAT_U8:
-				memset(p, 0x80, size);
-				break;
-			case SPA_AUDIO_FORMAT_ALAW:
-				memset(p, 0x80 ^ 0x55, size);
-				break;
-			case SPA_AUDIO_FORMAT_ULAW:
-				memset(p, 0x00 ^ 0xff, size);
-				break;
-			default:
-				memset(p, 0, size);
-				break;
-			}
+			sample_spec_silence(&stream->ss, p, size);
 
 			if (stream->draining && !stream->corked) {
 				stream->draining = false;
@@ -1760,6 +1713,9 @@ static int do_create_playback_stream(struct client *client, uint32_t command, ui
 	if (n_valid_formats == 0)
 		goto error_no_formats;
 
+	if (client->quirks & QUIRK_BLOCK_PLAYBACK_STREAM)
+		goto error_no_permission;
+
 	stream = stream_new(client, STREAM_TYPE_PLAYBACK, tag, &ss, &map, &attr);
 	if (stream == NULL)
 		goto error_errno;
@@ -1774,6 +1730,8 @@ static int do_create_playback_stream(struct client *client, uint32_t command, ui
 	stream->is_underrun = true;
 	stream->underrun_for = -1;
 
+	pw_properties_set(props, "pulse.corked", corked ? "true" : "false");
+
 	if (rate != 0) {
 		struct spa_fraction lat;
 		fix_playback_buffer_attr(stream, &attr, ss_rate, &lat);
@@ -1832,6 +1790,9 @@ error_protocol:
 error_no_formats:
 	res = -ENOTSUP;
 	goto error;
+error_no_permission:
+	res = -EPERM;
+	goto error;
 error_invalid:
 	res = -EINVAL;
 	goto error;
@@ -2026,6 +1987,9 @@ static int do_create_record_stream(struct client *client, uint32_t command, uint
 	if (n_valid_formats == 0)
 		goto error_no_formats;
 
+	if (client->quirks & QUIRK_BLOCK_RECORD_STREAM)
+		goto error_no_permission;
+
 	stream = stream_new(client, STREAM_TYPE_RECORD, tag, &ss, &map, &attr);
 	if (stream == NULL)
 		goto error_errno;
@@ -2041,6 +2005,8 @@ static int do_create_record_stream(struct client *client, uint32_t command, uint
 	if (client->quirks & QUIRK_REMOVE_CAPTURE_DONT_MOVE)
 		no_move = false;
 
+	pw_properties_set(props, "pulse.corked", corked ? "true" : "false");
+
 	if (rate != 0) {
 		struct spa_fraction lat;
 		fix_record_buffer_attr(stream, &attr, ss_rate, &lat);
@@ -2112,6 +2078,9 @@ error_protocol:
 error_no_formats:
 	res = -ENOTSUP;
 	goto error;
+error_no_permission:
+	res = -EPERM;
+	goto error;
 error_invalid:
 	res = -EINVAL;
 	goto error;
@@ -2411,8 +2380,8 @@ static int do_finish_upload_stream(struct client *client, uint32_t command, uint
 	stream_free(stream);
 
 	broadcast_subscribe_event(impl,
-			SUBSCRIPTION_MASK_SAMPLE_CACHE,
-			event | SUBSCRIPTION_EVENT_SAMPLE_CACHE,
+			SUBSCRIPTION_EVENT_SAMPLE_CACHE,
+			event,
 			sample->index);
 
 	return reply_simple_ack(client, tag);
@@ -2621,9 +2590,8 @@ static int do_remove_sample(struct client *client, uint32_t command, uint32_t ta
 		return -ENOENT;
 
 	broadcast_subscribe_event(impl,
-			SUBSCRIPTION_MASK_SAMPLE_CACHE,
-			SUBSCRIPTION_EVENT_REMOVE |
 			SUBSCRIPTION_EVENT_SAMPLE_CACHE,
+			SUBSCRIPTION_EVENT_REMOVE,
 			sample->index);
 
 	pw_map_remove(&impl->samples, sample->index);
@@ -2655,8 +2623,7 @@ static int do_cork_stream(struct client *client, uint32_t command, uint32_t tag,
 	if (stream == NULL || stream->type == STREAM_TYPE_UPLOAD)
 		return -ENOENT;
 
-	stream->corked = cork;
-	stream_set_paused(stream, cork, "cork request");
+	stream_set_corked(stream, cork);
 	if (cork) {
 		stream->is_underrun = true;
 	} else {
@@ -4042,6 +4009,7 @@ static int fill_sink_input_info(struct client *client, struct message *m,
 	uint32_t module_id = SPA_ID_INVALID, client_id = SPA_ID_INVALID;
 	uint32_t peer_index;
 	struct device_info dev_info;
+	bool corked;
 
 	if (!pw_manager_object_is_sink_input(o) || info == NULL || info->props == NULL)
 		return -ENOENT;
@@ -4069,6 +4037,10 @@ static int fill_sink_input_info(struct client *client, struct message *m,
 		else
 			peer_index = SPA_ID_INVALID;
 	}
+	if ((str = spa_dict_lookup(info->props, "pulse.corked")) != NULL)
+		corked = spa_atob(str);
+	else
+		corked = dev_info.state != STATE_RUNNING;
 
 	message_put(m,
 		TAG_U32, o->index,				/* sink_input index */
@@ -4094,7 +4066,7 @@ static int fill_sink_input_info(struct client *client, struct message *m,
 			TAG_INVALID);
 	if (client->version >= 19)
 		message_put(m,
-			TAG_BOOLEAN, dev_info.state != STATE_RUNNING,		/* corked */
+			TAG_BOOLEAN, corked,		/* corked */
 			TAG_INVALID);
 	if (client->version >= 20)
 		message_put(m,
@@ -4121,6 +4093,7 @@ static int fill_source_output_info(struct client *client, struct message *m,
 	uint32_t module_id = SPA_ID_INVALID, client_id = SPA_ID_INVALID;
 	uint32_t peer_index;
 	struct device_info dev_info;
+	bool corked;
 
 	if (!pw_manager_object_is_source_output(o) || info == NULL || info->props == NULL)
 		return -ENOENT;
@@ -4148,6 +4121,10 @@ static int fill_source_output_info(struct client *client, struct message *m,
 		else
 			peer_index = SPA_ID_INVALID;
 	}
+	if ((str = spa_dict_lookup(info->props, "pulse.corked")) != NULL)
+		corked = spa_atob(str);
+	else
+		corked = dev_info.state != STATE_RUNNING;
 
 	message_put(m,
 		TAG_U32, o->index,				/* source_output index */
@@ -4168,7 +4145,7 @@ static int fill_source_output_info(struct client *client, struct message *m,
 			TAG_INVALID);
 	if (client->version >= 19)
 		message_put(m,
-			TAG_BOOLEAN, dev_info.state != STATE_RUNNING,		/* corked */
+			TAG_BOOLEAN, corked,		/* corked */
 			TAG_INVALID);
 	if (client->version >= 22) {
 		struct format_info fi;
@@ -4878,8 +4855,8 @@ static void handle_module_loaded(struct module *module, struct client *client, u
 		module->loaded = true;
 
 		broadcast_subscribe_event(impl,
-			SUBSCRIPTION_MASK_MODULE,
-			SUBSCRIPTION_EVENT_NEW | SUBSCRIPTION_EVENT_MODULE,
+			SUBSCRIPTION_EVENT_MODULE,
+			SUBSCRIPTION_EVENT_NEW,
 			module->index);
 
 		if (client != NULL) {
@@ -5576,6 +5553,8 @@ struct pw_protocol_pulse *pw_protocol_pulse_new(struct pw_context *context,
 
 	cmd_run(impl);
 
+	notify_startup();
+
 	return (struct pw_protocol_pulse *) impl;
 
 error_free:
diff --git a/src/modules/module-protocol-pulse/quirks.c b/src/modules/module-protocol-pulse/quirks.c
index e2e972e9..34cd4484 100644
--- a/src/modules/module-protocol-pulse/quirks.c
+++ b/src/modules/module-protocol-pulse/quirks.c
@@ -19,6 +19,8 @@ static uint64_t parse_quirks(const char *str)
 		{ "remove-capture-dont-move", QUIRK_REMOVE_CAPTURE_DONT_MOVE },
 		{ "block-source-volume", QUIRK_BLOCK_SOURCE_VOLUME },
 		{ "block-sink-volume", QUIRK_BLOCK_SINK_VOLUME },
+		{ "block-record-stream", QUIRK_BLOCK_RECORD_STREAM },
+		{ "block-playback-stream", QUIRK_BLOCK_PLAYBACK_STREAM },
 	};
 	SPA_FOR_EACH_ELEMENT_VAR(quirk_keys, i) {
 		if (spa_streq(str, i->key))
diff --git a/src/modules/module-protocol-pulse/quirks.h b/src/modules/module-protocol-pulse/quirks.h
index e817d0b8..56ca9fa0 100644
--- a/src/modules/module-protocol-pulse/quirks.h
+++ b/src/modules/module-protocol-pulse/quirks.h
@@ -12,6 +12,8 @@
 #define QUIRK_REMOVE_CAPTURE_DONT_MOVE		(1ull<<1)	/** removes the capture stream DONT_MOVE flag */
 #define QUIRK_BLOCK_SOURCE_VOLUME		(1ull<<2)	/** block volume changes to sources */
 #define QUIRK_BLOCK_SINK_VOLUME			(1ull<<3)	/** block volume changes to sinks */
+#define QUIRK_BLOCK_RECORD_STREAM		(1ull<<4)	/** block creating a record stream */
+#define QUIRK_BLOCK_PLAYBACK_STREAM		(1ull<<5)	/** block creating a playback stream */
 
 int client_update_quirks(struct client *client);
 
diff --git a/src/modules/module-protocol-pulse/server.c b/src/modules/module-protocol-pulse/server.c
index 3e25eaad..fe7d47ab 100644
--- a/src/modules/module-protocol-pulse/server.c
+++ b/src/modules/module-protocol-pulse/server.c
@@ -103,6 +103,15 @@ finish:
 	return 0;
 }
 
+static void stream_clear_data(struct stream *stream,
+		uint32_t offset, uint32_t len)
+{
+	uint32_t l0 = SPA_MIN(len, MAXLENGTH - offset), l1 = len - l0;
+	sample_spec_silence(&stream->ss, SPA_PTROFF(stream->buffer, offset, void), l0);
+	if (SPA_UNLIKELY(l1 > 0))
+		sample_spec_silence(&stream->ss, stream->buffer, l1);
+}
+
 static int handle_memblock(struct client *client, struct message *msg)
 {
 	struct stream *stream;
@@ -149,6 +158,15 @@ static int handle_memblock(struct client *client, struct message *msg)
 		goto finish;
 	}
 
+	if (diff > 0) {
+		pw_log_debug("clear gap of %"PRIu64, diff);
+		/* if we jump forwards, clear the data we skipped because we might otherwise
+		 * play back old data. FIXME, if the write pointer goes backwards and
+		 * forwards, this might clear valid data. We should probably keep track of
+		 * the highest write pointer and only clear when we go past that one. */
+		stream_clear_data(stream, index % MAXLENGTH, SPA_MIN(diff, MAXLENGTH));
+	}
+
 	index += diff;
 	filled += diff;
 	stream->write_index += diff;
@@ -981,31 +999,27 @@ int servers_create_and_start(struct impl *impl, const char *addresses, struct pw
 {
 	int len, res, count = 0, err = 0; /* store the first error to return when no servers could be created */
 	const char *v;
-	struct spa_json it[3];
+	struct spa_json it[2];
 
 	/* update `err` if it hasn't been set to an errno */
 #define UPDATE_ERR(e) do { if (err == 0) err = (e); } while (false)
 
 	/* collect addresses into an array of `struct sockaddr_storage` */
-	spa_json_init(&it[0], addresses, strlen(addresses));
 
 	/* [ <server-spec> ... ] */
-	if (spa_json_enter_array(&it[0], &it[1]) < 0)
+	if (spa_json_begin_array(&it[0], addresses, strlen(addresses)) < 0)
 		return -EINVAL;
 
 	/* a server-spec is either an address or an object */
-	while ((len = spa_json_next(&it[1], &v)) > 0) {
+	while ((len = spa_json_next(&it[0], &v)) > 0) {
 		char addr_str[FORMATTED_SOCKET_ADDR_STRLEN] = { 0 };
 		char key[128], client_access[64] = { 0 };
 		struct sockaddr_storage addrs[2];
 		int i, max_clients = MAX_CLIENTS, listen_backlog = LISTEN_BACKLOG, n_addr;
 
 		if (spa_json_is_object(v, len)) {
-			spa_json_enter(&it[1], &it[2]);
-			while (spa_json_get_string(&it[2], key, sizeof(key)) > 0) {
-				if ((len = spa_json_next(&it[2], &v)) <= 0)
-					break;
-
+			spa_json_enter(&it[0], &it[1]);
+			while ((len = spa_json_object_next(&it[1], key, sizeof(key), &v)) > 0) {
 				if (spa_streq(key, "address")) {
 					spa_json_parse_stringn(v, len, addr_str, sizeof(addr_str));
 				} else if (spa_streq(key, "max-clients")) {
diff --git a/src/modules/module-protocol-pulse/stream.c b/src/modules/module-protocol-pulse/stream.c
index 94d1fdd9..e3b8b7b0 100644
--- a/src/modules/module-protocol-pulse/stream.c
+++ b/src/modules/module-protocol-pulse/stream.c
@@ -211,6 +211,16 @@ void stream_set_paused(struct stream *stream, bool paused, const char *reason)
 	pw_stream_set_active(stream->stream, !paused);
 }
 
+void stream_set_corked(struct stream *stream, bool cork)
+{
+	stream->corked = cork;
+	pw_log_info("cork %d", cork);
+	pw_stream_update_properties(stream->stream,
+			&SPA_DICT_ITEMS(
+				SPA_DICT_ITEM("pulse.corked", cork ? "true" : "false")));
+	stream_set_paused(stream, cork, "cork request");
+}
+
 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 b0522ea4..64ecea68 100644
--- a/src/modules/module-protocol-pulse/stream.h
+++ b/src/modules/module-protocol-pulse/stream.h
@@ -107,6 +107,7 @@ void stream_free(struct stream *stream);
 void stream_flush(struct stream *stream);
 uint32_t stream_pop_missing(struct stream *stream);
 
+void stream_set_corked(struct stream *stream, bool corked);
 void stream_set_paused(struct stream *stream, bool paused, const char *reason);
 
 int stream_send_underflow(struct stream *stream, int64_t offset);
diff --git a/src/modules/module-protocol-pulse/utils.c b/src/modules/module-protocol-pulse/utils.c
index 5ad8184a..e6b24e70 100644
--- a/src/modules/module-protocol-pulse/utils.c
+++ b/src/modules/module-protocol-pulse/utils.c
@@ -148,18 +148,21 @@ pid_t get_client_pid(struct client *client, int client_fd)
 
 const char *get_server_name(struct pw_context *context)
 {
-	const char *name = NULL;
+	const char *name = NULL, *sep;
 	const struct pw_properties *props = pw_context_get_properties(context);
 
 	name = getenv("PIPEWIRE_REMOTE");
 	if ((name == NULL || name[0] == '\0') && props != NULL)
 		name = pw_properties_get(props, PW_KEY_REMOTE_NAME);
+	if (name != NULL && (sep = strrchr(name, '/')) != NULL)
+		name = sep+1;
 	if (name == NULL || name[0] == '\0')
 		name = PW_DEFAULT_REMOTE;
 	return name;
 }
 
-int create_pid_file(void) {
+int create_pid_file(void)
+{
 	char pid_file[PATH_MAX];
 	FILE *f;
 	int res;
@@ -185,3 +188,41 @@ int create_pid_file(void) {
 
 	return 0;
 }
+
+int notify_startup(void)
+{
+	long v;
+	int fd, res = 0;
+	char *endptr;
+	const char *env = getenv("PIPEWIRE_PULSE_NOTIFICATION_FD");
+
+	if (env == NULL || env[0] == '\0')
+		return 0;
+
+	errno = 0;
+	v = strtol(env, &endptr, 10);
+	if (endptr[0] != '\0')
+		errno = EINVAL;
+	if (errno != 0) {
+		res = -errno;
+		pw_log_error("can't parse PIPEWIRE_PULSE_NOTIFICATION_FD env: %m");
+		goto error;
+	}
+	fd = (int)v;
+	if (v != fd) {
+		res = -ERANGE;
+		pw_log_error("invalid PIPEWIRE_PULSE_NOTIFICATION_FD %ld: %s", v, spa_strerror(res));
+		goto error;
+	}
+	if (dprintf(fd, "\n") < 0) {
+		res = -errno;
+		pw_log_error("can't signal PIPEWIRE_PULSE_NOTIFICATION_FD: %m");
+		goto error;
+	}
+	close(fd);
+	unsetenv("PIPEWIRE_PULSE_NOTIFICATION_FD");
+	return 0;
+
+error:
+	return res;
+}
diff --git a/src/modules/module-protocol-pulse/utils.h b/src/modules/module-protocol-pulse/utils.h
index 4f867202..2b34bb83 100644
--- a/src/modules/module-protocol-pulse/utils.h
+++ b/src/modules/module-protocol-pulse/utils.h
@@ -16,5 +16,6 @@ int check_flatpak(struct client *client, pid_t pid);
 pid_t get_client_pid(struct client *client, int client_fd);
 const char *get_server_name(struct pw_context *context);
 int create_pid_file(void);
+int notify_startup(void);
 
 #endif /* PULSE_SERVER_UTILS_H */
diff --git a/src/modules/module-protocol-simple.c b/src/modules/module-protocol-simple.c
index 5e327b68..bbed3b5f 100644
--- a/src/modules/module-protocol-simple.c
+++ b/src/modules/module-protocol-simple.c
@@ -27,6 +27,7 @@
 #include <spa/debug/types.h>
 #include <spa/param/audio/type-info.h>
 #include <spa/param/audio/format-utils.h>
+#include <spa/param/audio/raw-json.h>
 
 #include <pipewire/impl.h>
 
@@ -785,42 +786,6 @@ static void impl_free(struct impl *impl)
 	free(impl);
 }
 
-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 inline 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 int calc_frame_size(struct spa_audio_info_raw *info)
 {
 	int res = info->channels;
@@ -857,23 +822,16 @@ static int calc_frame_size(struct spa_audio_info_raw *info)
 
 static int 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));
+	spa_audio_info_raw_init_dict_keys(info,
+			&SPA_DICT_ITEMS(
+				 SPA_DICT_ITEM(SPA_KEY_AUDIO_FORMAT, DEFAULT_FORMAT),
+				 SPA_DICT_ITEM(SPA_KEY_AUDIO_RATE, SPA_STRINGIFY(DEFAULT_RATE)),
+				 SPA_DICT_ITEM(SPA_KEY_AUDIO_POSITION, DEFAULT_POSITION)),
+			&props->dict,
+			SPA_KEY_AUDIO_FORMAT,
+			SPA_KEY_AUDIO_RATE,
+			SPA_KEY_AUDIO_CHANNELS,
+			SPA_KEY_AUDIO_POSITION, NULL);
 
 	return calc_frame_size(info);
 }
@@ -892,7 +850,7 @@ static void copy_props(struct impl *impl, const char *key)
 static int parse_params(struct impl *impl)
 {
 	const char *str;
-	struct spa_json it[2];
+	struct spa_json it[1];
 	char value[512];
 
 	pw_properties_fetch_bool(impl->props, "capture", &impl->capture);
@@ -967,9 +925,8 @@ static int parse_params(struct impl *impl)
 	if ((str = pw_properties_get(impl->props, "server.address")) == NULL)
 		str = DEFAULT_SERVER;
 
-        spa_json_init(&it[0], str, strlen(str));
-        if (spa_json_enter_array(&it[0], &it[1]) > 0) {
-                while (spa_json_get_string(&it[1], value, sizeof(value)) > 0) {
+        if (spa_json_begin_array_relax(&it[0], str, strlen(str)) > 0) {
+                while (spa_json_get_string(&it[0], value, sizeof(value)) > 0) {
                         if (create_server(impl, value) == NULL) {
 				pw_log_warn("%p: can't create server for %s: %m",
 					impl, value);
diff --git a/src/modules/module-pulse-tunnel.c b/src/modules/module-pulse-tunnel.c
index ccc24376..32d01122 100644
--- a/src/modules/module-pulse-tunnel.c
+++ b/src/modules/module-pulse-tunnel.c
@@ -27,6 +27,7 @@
 #include <spa/param/audio/format-utils.h>
 #include <spa/param/latency-utils.h>
 #include <spa/param/audio/raw.h>
+#include <spa/param/audio/raw-json.h>
 
 #include <pipewire/impl.h>
 #include <pipewire/i18n.h>
@@ -186,9 +187,8 @@ struct impl {
 	uint32_t target_latency;
 	uint32_t current_latency;
 	uint32_t target_buffer;
-	struct spa_io_rate_match *rate_match;
 	struct spa_dll dll;
-	float max_error;
+	double max_error;
 	unsigned resync:1;
 
 	bool do_disconnect:1;
@@ -331,23 +331,19 @@ static void stream_param_changed(void *d, uint32_t id, const struct spa_pod *par
 
 static void update_rate(struct impl *impl, uint32_t filled)
 {
-	float error, corr;
+	double error, corr;
 	uint32_t current_latency;
 
-	if (impl->rate_match == NULL)
-		return;
-
 	current_latency = impl->current_latency + filled;
-	error = (float)impl->target_latency - (float)(current_latency);
-	error = SPA_CLAMP(error, -impl->max_error, impl->max_error);
+	error = (double)impl->target_latency - (double)(current_latency);
+	error = SPA_CLAMPD(error, -impl->max_error, impl->max_error);
 
-	corr = (float)spa_dll_update(&impl->dll, error);
+	corr = spa_dll_update(&impl->dll, error);
 	pw_log_debug("error:%f corr:%f current:%u target:%u",
 			error, corr,
 			current_latency, impl->target_latency);
 
-	SPA_FLAG_SET(impl->rate_match->flags, SPA_IO_RATE_MATCH_FLAG_ACTIVE);
-	impl->rate_match->rate = 1.0f / corr;
+	pw_stream_set_rate(impl->stream, 1.0 / corr);
 }
 
 static void playback_stream_process(void *d)
@@ -440,21 +436,10 @@ static void capture_stream_process(void *d)
 	pw_stream_queue_buffer(impl->stream, buf);
 }
 
-static void stream_io_changed(void *data, uint32_t id, void *area, uint32_t size)
-{
-	struct impl *impl = data;
-	switch (id) {
-	case SPA_IO_RateMatch:
-		impl->rate_match = area;
-		break;
-	}
-}
-
 static const struct pw_stream_events playback_stream_events = {
 	PW_VERSION_STREAM_EVENTS,
 	.destroy = stream_destroy,
 	.state_changed = stream_state_changed,
-	.io_changed = stream_io_changed,
 	.param_changed = stream_param_changed,
 	.process = playback_stream_process
 };
@@ -463,7 +448,6 @@ static const struct pw_stream_events capture_stream_events = {
 	PW_VERSION_STREAM_EVENTS,
 	.destroy = stream_destroy,
 	.state_changed = stream_state_changed,
-	.io_changed = stream_io_changed,
 	.param_changed = stream_param_changed,
 	.process = capture_stream_process
 };
@@ -752,7 +736,7 @@ static void stream_latency_update_cb(pa_stream *s, void *userdata)
 	pa_usec_t usec;
 	int negative;
 	pa_stream_get_latency(s, &usec, &negative);
-	pw_log_debug("latency %ld negative %d", usec, negative);
+	pw_log_debug("latency %" PRIu64 " negative %d", usec, negative);
 }
 
 static int create_pulse_stream(struct impl *impl)
@@ -1064,61 +1048,18 @@ static const struct pw_impl_module_events module_events = {
 	.destroy = module_destroy,
 };
 
-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 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 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));
+	spa_audio_info_raw_init_dict_keys(info,
+			&SPA_DICT_ITEMS(
+				 SPA_DICT_ITEM(SPA_KEY_AUDIO_FORMAT, DEFAULT_FORMAT),
+				 SPA_DICT_ITEM(SPA_KEY_AUDIO_RATE, SPA_STRINGIFY(DEFAULT_RATE)),
+				 SPA_DICT_ITEM(SPA_KEY_AUDIO_POSITION, DEFAULT_POSITION)),
+			&props->dict,
+			SPA_KEY_AUDIO_FORMAT,
+			SPA_KEY_AUDIO_RATE,
+			SPA_KEY_AUDIO_CHANNELS,
+			SPA_KEY_AUDIO_POSITION, NULL);
 }
 
 static int calc_frame_size(struct spa_audio_info_raw *info)
@@ -1267,7 +1208,7 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
 		goto error;
 	}
 	spa_dll_set_bw(&impl->dll, SPA_DLL_BW_MIN, 128, impl->info.rate);
-	impl->max_error = 256.0f;
+	impl->max_error = 256.0;
 
 	impl->core = pw_context_get_object(impl->context, PW_TYPE_INTERFACE_Core);
 	if (impl->core == NULL) {
diff --git a/src/modules/module-raop-sink.c b/src/modules/module-raop-sink.c
index da6fc74b..9d940681 100644
--- a/src/modules/module-raop-sink.c
+++ b/src/modules/module-raop-sink.c
@@ -271,6 +271,9 @@ struct impl {
 	bool mute;
 	float volume;
 
+	struct spa_latency_info latency_info;
+	struct spa_process_latency_info process_latency;
+
 	struct spa_ringbuffer ring;
 	uint8_t buffer[BUFFER_SIZE];
 
@@ -509,21 +512,11 @@ static void stream_send_packet(void *data, struct iovec *iov, size_t iovlen)
 	out_vec[msg.msg_iovlen++] = (struct iovec) { header, 12 };
 	out_vec[msg.msg_iovlen++] = (struct iovec) { out, len };
 
-	pw_log_debug("raop sending %ld", out_vec[0].iov_len + out_vec[1].iov_len + out_vec[2].iov_len);
+	pw_log_debug("raop sending %zu", out_vec[0].iov_len + out_vec[1].iov_len + out_vec[2].iov_len);
 
 	send_packet(impl->server_fd, &msg);
 }
 
-static inline void
-set_iovec(struct spa_ringbuffer *rbuf, void *buffer, uint32_t size,
-		uint32_t offset, struct iovec *iov, uint32_t len)
-{
-	iov[0].iov_len = SPA_MIN(len, size - offset);
-	iov[0].iov_base = SPA_PTROFF(buffer, offset, void);
-	iov[1].iov_len = len - iov[0].iov_len;
-	iov[1].iov_base = buffer;
-}
-
 static int create_udp_socket(struct impl *impl, uint16_t *port)
 {
 	int res, ip_version, fd, val, i, af;
@@ -858,15 +851,29 @@ static uint32_t msec_to_samples(struct impl *impl, uint32_t msec)
 	return (uint64_t) msec * impl->rate / 1000;
 }
 
-static int rtsp_record_reply(void *data, int status, const struct spa_dict *headers, const struct pw_array *content)
+static void update_latency(struct impl *impl)
 {
-	struct impl *impl = data;
-	const char *str;
-	uint32_t n_params;
-	const struct spa_pod *params[2];
+	uint32_t n_params = 0;
+	const struct spa_pod *params[3];
 	uint8_t buffer[1024];
 	struct spa_pod_builder b;
 	struct spa_latency_info latency;
+
+	spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+	latency = SPA_LATENCY_INFO(PW_DIRECTION_INPUT);
+
+	spa_process_latency_info_add(&impl->process_latency, &latency);
+	params[n_params++] = spa_latency_build(&b, SPA_PARAM_Latency, &latency);
+	params[n_params++] = spa_latency_build(&b, SPA_PARAM_Latency, &impl->latency_info);
+	params[n_params++] = spa_process_latency_build(&b, SPA_PARAM_ProcessLatency, &impl->process_latency);
+	rtp_stream_update_params(impl->stream, params, n_params);
+}
+
+static int rtsp_record_reply(void *data, int status, const struct spa_dict *headers, const struct pw_array *content)
+{
+	struct impl *impl = data;
+	const char *str;
 	char progress[128];
 	struct timespec timeout, interval;
 
@@ -884,25 +891,21 @@ static int rtsp_record_reply(void *data, int status, const struct spa_dict *head
 	interval.tv_sec = 2;
 	interval.tv_nsec = 0;
 
-	if (!impl->feedback_timer)
+	// feedback timer is only needed for auth_setup	encryption
+	if (impl->encryption == CRYPTO_AUTH_SETUP && !impl->feedback_timer) {
+
 		impl->feedback_timer = pw_loop_add_timer(impl->loop, rtsp_do_post_feedback, impl);
-	pw_loop_update_timer(impl->loop, impl->feedback_timer, &timeout, &interval, false);
+		pw_loop_update_timer(impl->loop, impl->feedback_timer, &timeout, &interval, false);
+	}
 
 	if ((str = spa_dict_lookup(headers, "Audio-Latency")) != NULL) {
 		uint32_t l;
 		if (spa_atou32(str, &l, 0))
 			impl->latency = SPA_MAX(l, impl->latency);
 	}
+	impl->process_latency.rate = impl->latency + msec_to_samples(impl, RAOP_LATENCY_MS);
 
-	spa_zero(latency);
-	latency.direction = PW_DIRECTION_INPUT;
-	latency.min_rate = latency.max_rate = impl->latency + msec_to_samples(impl, RAOP_LATENCY_MS);
-
-	n_params = 0;
-	spa_pod_builder_init(&b, buffer, sizeof(buffer));
-	params[n_params++] = spa_latency_build(&b, SPA_PARAM_Latency, &latency);
-
-	rtp_stream_update_params(impl->stream, params, n_params);
+	update_latency(impl);
 
 	rtp_stream_set_first(impl->stream);
 
@@ -1664,6 +1667,32 @@ static void stream_props_changed(struct impl *impl, uint32_t id, const struct sp
 	rtp_stream_set_param(impl->stream, id, param);
 }
 
+static void param_latency_changed(struct impl *impl, const struct spa_pod *param)
+{
+	struct spa_latency_info latency;
+
+	if (param == NULL || spa_latency_parse(param, &latency) < 0)
+		return;
+	if (latency.direction == SPA_DIRECTION_OUTPUT)
+		impl->latency_info = latency;
+
+	update_latency(impl);
+}
+
+static void param_process_latency_changed(struct impl *impl, const struct spa_pod *param)
+{
+	struct spa_process_latency_info info;
+
+	if (param == NULL)
+		spa_zero(info);
+	else if (spa_process_latency_parse(param, &info) < 0)
+		return;
+	if (spa_process_latency_info_compare(&impl->process_latency, &info) == 0)
+		return;
+	impl->process_latency = info;
+	update_latency(impl);
+}
+
 static void stream_param_changed(void *data, uint32_t id, const struct spa_pod *param)
 {
 	struct impl *impl = data;
@@ -1679,6 +1708,12 @@ static void stream_param_changed(void *data, uint32_t id, const struct spa_pod *
 		if (param != NULL)
 			stream_props_changed(impl, id, param);
 		break;
+	case SPA_PARAM_Latency:
+		param_latency_changed(impl, param);
+		break;
+	case SPA_PARAM_ProcessLatency:
+		param_process_latency_changed(impl, param);
+		break;
 	default:
 		break;
 	}
@@ -1808,6 +1843,8 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
 	impl->context = context;
 	impl->loop = pw_context_get_main_loop(context);
 
+	impl->latency_info = SPA_LATENCY_INFO(SPA_DIRECTION_OUTPUT);
+
 	ip = pw_properties_get(props, "raop.ip");
 	port = pw_properties_get(props, "raop.port");
 	if (ip == NULL || port == NULL) {
diff --git a/src/modules/module-raop/rtsp-client.c b/src/modules/module-raop/rtsp-client.c
index 952891d9..fae71977 100644
--- a/src/modules/module-raop/rtsp-client.c
+++ b/src/modules/module-raop/rtsp-client.c
@@ -576,7 +576,7 @@ int pw_rtsp_client_url_send(struct pw_rtsp_client *client, const char *url,
 	if ((f = open_memstream((char**)&msg, &len)) == NULL)
 		return -errno;
 
-	fseek(f, sizeof(*msg), SEEK_SET);
+	fseek(f, offsetof(struct message, data), SEEK_SET);
 
 	cseq = ++client->cseq;
 
@@ -598,7 +598,7 @@ int pw_rtsp_client_url_send(struct pw_rtsp_client *client, const char *url,
 
 	fclose(f);
 
-	msg->len = len - sizeof(*msg);
+	msg->len = len - offsetof(struct message, data);
 	msg->offset = 0;
 	msg->reply = reply;
 	msg->user_data = user_data;
diff --git a/src/modules/module-roc-source.c b/src/modules/module-roc-source.c
index dc37756d..b5a5ea99 100644
--- a/src/modules/module-roc-source.c
+++ b/src/modules/module-roc-source.c
@@ -45,9 +45,16 @@
  * - `local.repair.port = <str>`: local receiver TCP/UDP port for receiver packets
  * - `local.control.port = <str>`: local receiver TCP/UDP port for control packets
  * - `sess.latency.msec = <str>`: target network latency in milliseconds
- * - `resampler.profile = <str>`: Possible values: `disable`, `high`,
- *   `medium`, `low`.
- * - `fec.code = <str>`: Possible values: `disable`, `rs8m`, `ldpc`
+ * - `roc.resampler.backend = <str>`: Possible values: `default`, `builtin`,
+ *       `speex`, `speexdec`.
+ * - `roc.resampler.profile = <str>`: Possible values: `default`, `high`,
+ *       `medium`, `low`.
+ * - `roc.latency-tuner.backend = <str>`: Possible values: `default`, `niq`
+ * - `roc.latency-tuner.profile = <str>`: Possible values: `default`, `intact`,
+ *       `responsive`, `gradual`
+ * - `fec.code = <str>`: Possible values: `default`, `disable`, `rs8m`, `ldpc`
+ *
+ * - `resampler.profile = <str>`: Deprecated, use roc.resampler.profile
  *
  * ## General options
  *
@@ -65,7 +72,10 @@
  *  {   name = libpipewire-module-roc-source
  *      args = {
  *          local.ip = 0.0.0.0
- *          resampler.profile = medium
+ *          #roc.resampler.backend = default
+ *          roc.resampler.profile = medium
+ *          #roc.latency-tuner.backend = default
+ *          #roc.latency-tuner.profile = default
  *          fec.code = disable
  *          sess.latency.msec = 5000
  *          local.source.port = 10001
@@ -109,6 +119,9 @@ struct module_roc_source_data {
 	roc_receiver *receiver;
 
 	roc_resampler_profile resampler_profile;
+	roc_resampler_backend resampler_backend;
+	roc_latency_tuner_backend latency_tuner_backend;
+	roc_latency_tuner_profile latency_tuner_profile;
 	roc_fec_encoding fec_code;
 	uint32_t rate;
 	char *local_ip;
@@ -275,6 +288,9 @@ static int roc_source_setup(struct module_roc_source_data *data)
 	receiver_config.frame_encoding.channels = ROC_CHANNEL_LAYOUT_STEREO;
 	receiver_config.frame_encoding.format = ROC_FORMAT_PCM_FLOAT32;
 	receiver_config.resampler_profile = data->resampler_profile;
+	receiver_config.resampler_backend = data->resampler_backend;
+	receiver_config.latency_tuner_backend = data->latency_tuner_backend;
+	receiver_config.latency_tuner_profile = data->latency_tuner_profile;
 
 	info.rate = data->rate;
 
@@ -377,7 +393,10 @@ static const struct spa_dict_item module_roc_source_info[] = {
 	{ PW_KEY_MODULE_AUTHOR, "Sanchayan Maity <sanchayan@asymptotic.io>" },
 	{ PW_KEY_MODULE_DESCRIPTION, "roc source" },
 	{ PW_KEY_MODULE_USAGE,	"( source.name=<name for the source> ) "
-				"( resampler.profile=<empty>|disable|high|medium|low ) "
+				"( roc.resampler.backend=<empty>|default|builtin|speex|speexdec ) "
+				"( roc.resampler.profile=<empty>|default|high|medium|low ) "
+				"( roc.latency-tuner.backend=<empty>|default|niq ) "
+				"( roc.latency-tuner.profile=<empty>|default|intact|responsive|gradual ) "
 				"( fec.code=<empty>|disable|rs8m|ldpc ) "
 				"( sess.latency.msec=<target network latency in milliseconds> ) "
 				"( local.ip=<local receiver ip> ) "
@@ -473,8 +492,16 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
 	} else {
 		data->sess_latency_msec = PW_ROC_DEFAULT_SESS_LATENCY;
 	}
-
-	if ((str = pw_properties_get(props, "resampler.profile")) != NULL) {
+	if ((str = pw_properties_get(props, "roc.resampler.backend")) != NULL) {
+		if (pw_roc_parse_resampler_backend(&data->resampler_backend, str)) {
+			pw_log_warn("Invalid resampler backend %s, using default", str);
+			data->resampler_backend = ROC_RESAMPLER_BACKEND_DEFAULT;
+		}
+	} else {
+		data->resampler_backend = ROC_RESAMPLER_BACKEND_DEFAULT;
+	}
+	if ((str = pw_properties_get(props, "roc.resampler.profile")) != NULL ||
+	    (str = pw_properties_get(props, "resampler.profile")) != NULL) {
 		if (pw_roc_parse_resampler_profile(&data->resampler_profile, str)) {
 			pw_log_warn("Invalid resampler profile %s, using default", str);
 			data->resampler_profile = ROC_RESAMPLER_PROFILE_DEFAULT;
@@ -482,6 +509,22 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
 	} else {
 		data->resampler_profile = ROC_RESAMPLER_PROFILE_DEFAULT;
 	}
+	if ((str = pw_properties_get(props, "roc.latency-tuner.backend")) != NULL) {
+		if (pw_roc_parse_latency_tuner_backend(&data->latency_tuner_backend, str)) {
+			pw_log_warn("Invalid latency-tuner backend %s, using default", str);
+			data->latency_tuner_backend = ROC_LATENCY_TUNER_BACKEND_DEFAULT;
+		}
+	} else {
+		data->latency_tuner_backend = ROC_LATENCY_TUNER_BACKEND_DEFAULT;
+	}
+	if ((str = pw_properties_get(props, "roc.latency-tuner.profile")) != NULL) {
+		if (pw_roc_parse_latency_tuner_profile(&data->latency_tuner_profile, str)) {
+			pw_log_warn("Invalid latency-tuner profile %s, using default", str);
+			data->latency_tuner_profile = ROC_LATENCY_TUNER_PROFILE_DEFAULT;
+		}
+	} else {
+		data->latency_tuner_profile = ROC_LATENCY_TUNER_PROFILE_DEFAULT;
+	}
 	if ((str = pw_properties_get(props, "fec.code")) != NULL) {
 		if (pw_roc_parse_fec_encoding(&data->fec_code, str)) {
 			pw_log_error("Invalid fec code %s, using default", str);
diff --git a/src/modules/module-roc/common.h b/src/modules/module-roc/common.h
index 2164a342..4b30f41d 100644
--- a/src/modules/module-roc/common.h
+++ b/src/modules/module-roc/common.h
@@ -16,7 +16,7 @@
 
 static inline int pw_roc_parse_fec_encoding(roc_fec_encoding *out, const char *str)
 {
-	if (!str || !*str)
+	if (!str || !*str || spa_streq(str, "default"))
 		*out = ROC_FEC_ENCODING_DEFAULT;
 	else if (spa_streq(str, "disable"))
 		*out = ROC_FEC_ENCODING_DISABLE;
@@ -31,7 +31,7 @@ static inline int pw_roc_parse_fec_encoding(roc_fec_encoding *out, const char *s
 
 static inline int pw_roc_parse_resampler_profile(roc_resampler_profile *out, const char *str)
 {
-	if (!str || !*str)
+	if (!str || !*str || spa_streq(str, "default"))
 		*out = ROC_RESAMPLER_PROFILE_DEFAULT;
 	else if (spa_streq(str, "high"))
 		*out = ROC_RESAMPLER_PROFILE_HIGH;
@@ -44,6 +44,47 @@ static inline int pw_roc_parse_resampler_profile(roc_resampler_profile *out, con
 	return 0;
 }
 
+static inline int pw_roc_parse_resampler_backend(roc_resampler_backend *out, const char *str)
+{
+	if (!str || !*str || spa_streq(str, "default"))
+		*out = ROC_RESAMPLER_BACKEND_DEFAULT;
+	else if (spa_streq(str, "builtin"))
+		*out = ROC_RESAMPLER_BACKEND_BUILTIN;
+	else if (spa_streq(str, "speex"))
+		*out = ROC_RESAMPLER_BACKEND_SPEEX;
+	else if (spa_streq(str, "speexdec"))
+		*out = ROC_RESAMPLER_BACKEND_SPEEXDEC;
+	else
+		return -EINVAL;
+	return 0;
+}
+
+static inline int pw_roc_parse_latency_tuner_backend(roc_latency_tuner_backend *out, const char *str)
+{
+	if (!str || !*str || spa_streq(str, "default"))
+		*out = ROC_LATENCY_TUNER_BACKEND_DEFAULT;
+	else if (spa_streq(str, "niq"))
+		*out = ROC_LATENCY_TUNER_BACKEND_NIQ;
+	else
+		return -EINVAL;
+	return 0;
+}
+
+static inline int pw_roc_parse_latency_tuner_profile(roc_latency_tuner_profile *out, const char *str)
+{
+	if (!str || !*str || spa_streq(str, "default"))
+		*out = ROC_LATENCY_TUNER_PROFILE_DEFAULT;
+	else if (spa_streq(str, "intact"))
+		*out = ROC_LATENCY_TUNER_PROFILE_INTACT;
+	else if (spa_streq(str, "responsive"))
+		*out = ROC_LATENCY_TUNER_PROFILE_RESPONSIVE;
+	else if (spa_streq(str, "gradual"))
+		*out = ROC_LATENCY_TUNER_PROFILE_GRADUAL;
+	else
+		return -EINVAL;
+	return 0;
+}
+
 static inline int pw_roc_create_endpoint(roc_endpoint **result, roc_protocol protocol, const char *ip, int port)
 {
 	roc_endpoint *endpoint;
diff --git a/src/modules/module-rt.c b/src/modules/module-rt.c
index af22e624..b2d0739c 100644
--- a/src/modules/module-rt.c
+++ b/src/modules/module-rt.c
@@ -95,6 +95,19 @@
  *
  * The PipeWire server processes are explicitly configured with a valid nice level.
  *
+ * ## Config override
+ *
+ * A `module.rt.args` config section can be added
+ * to override the module arguments.
+ *
+ *\code{.unparsed}
+ * # ~/.config/pipewire/pipewire.conf.d/my-rt-args.conf
+ *
+ * module.rt.args = {
+ *     #nice.level = 22
+ * }
+ *\endcode
+ *
  * ## Example configuration
  *
  *\code{.unparsed}
@@ -1076,6 +1089,7 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
 		res = -errno;
 		goto error;
 	}
+	pw_context_conf_update_props(context, "module."NAME".args", props);
 
 	impl->context = context;
 	impl->nice_level = pw_properties_get_int32(props, "nice.level", DEFAULT_NICE_LEVEL);
@@ -1114,7 +1128,7 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
 	if (!check_realtime_privileges(impl)) {
 		if (!can_use_rtkit) {
 			res = -ENOTSUP;
-			pw_log_warn("regular realtime scheduling not available"
+			pw_log_info("regular realtime scheduling not available"
 					" (Portal/RTKit fallback disabled)");
 			goto error;
 		}
diff --git a/src/modules/module-rtp-sap.c b/src/modules/module-rtp-sap.c
index 2ed466b7..9eeb8502 100644
--- a/src/modules/module-rtp-sap.c
+++ b/src/modules/module-rtp-sap.c
@@ -60,6 +60,7 @@
  * - `net.ttl = <int>`: TTL to use, default 1
  * - `net.loop = <bool>`: loopback multicast, default false
  * - `stream.rules` = <rules>: match rules, use create-stream and announce-stream actions
+ * - `sap.max-sessions = <int>`: maximum number of concurrent send/receive sessions to track
  * - `sap.preamble-extra = [strings]`: extra attributes to add to the atomic SDP preamble
  * - `sap.end-extra = [strings]`: extra attributes to add to the end of the SDP message
  *
@@ -135,7 +136,7 @@
 PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
 #define PW_LOG_TOPIC_DEFAULT mod_topic
 
-#define MAX_SESSIONS		64
+#define DEFAULT_MAX_SESSIONS		64
 
 #define DEFAULT_ANNOUNCE_RULES	\
 	"[ { matches = [ { sess.sap.announce = true } ] actions = { announce-stream = { } } } ]"
@@ -154,6 +155,8 @@ PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
 #define DEFAULT_TTL		1
 #define DEFAULT_LOOP		false
 
+#define MAX_SDP			2048
+
 #define USAGE	"( local.ifname=<local interface name to use> ) "					\
 		"( 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)"> ) "	\
@@ -180,7 +183,8 @@ static const struct spa_dict_item module_info[] = {
 
 struct sdp_info {
 	uint16_t hash;
-	uint32_t ntp;
+	uint32_t session_id;
+	uint32_t session_version;
 	uint32_t t_ntp;
 
 	char *origin;
@@ -203,6 +207,7 @@ struct sdp_info {
 	float ptime;
 	uint32_t framecount;
 
+	uint32_t ssrc;
 	uint32_t ts_offset;
 	char *ts_refclk;
 };
@@ -220,6 +225,8 @@ struct session {
 	struct sdp_info info;
 
 	unsigned has_sent_sap:1;
+	unsigned has_sdp:1;
+	char sdp[MAX_SDP];
 
 	struct pw_properties *props;
 
@@ -272,6 +279,7 @@ struct impl {
 	struct spa_source *sap_source;
 	uint32_t cleanup_interval;
 
+	uint32_t max_sessions;
 	uint32_t n_sessions;
 	struct spa_list sessions;
 
@@ -376,7 +384,7 @@ static bool is_multicast(struct sockaddr *sa, socklen_t salen)
 static int make_unix_socket(const char *path) {
 	struct sockaddr_un addr;
 
-	spa_autoclose int fd = socket(AF_UNIX, SOCK_DGRAM, 0);
+	spa_autoclose int fd = socket(AF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC, 0);
 	if (fd < 0) {
 		pw_log_warn("Failed to create PTP management socket");
 		return -1;
@@ -452,6 +460,8 @@ static int make_recv_socket(struct sockaddr_storage *sa, socklen_t salen,
 {
 	int af, fd, val, res;
 	struct ifreq req;
+	struct sockaddr_storage ba = *sa;
+	bool do_connect = false;
 	char addr[128];
 
 	af = sa->ss_family;
@@ -485,7 +495,11 @@ static int make_recv_socket(struct sockaddr_storage *sa, socklen_t salen,
 			pw_log_info("join IPv4 group: %s iface:%d", addr, req.ifr_ifindex);
 			res = setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mr4, sizeof(mr4));
 		} else {
-			sa4->sin_addr.s_addr = INADDR_ANY;
+			struct sockaddr_in *ba4 = (struct sockaddr_in*)&ba;
+			if (ba4->sin_addr.s_addr != INADDR_ANY) {
+				ba4->sin_addr.s_addr = INADDR_ANY;
+				do_connect = true;
+			}
 		}
 	} else if (af == AF_INET6) {
 		struct sockaddr_in6 *sa6 = (struct sockaddr_in6*)sa;
@@ -498,7 +512,8 @@ static int make_recv_socket(struct sockaddr_storage *sa, socklen_t salen,
 			pw_log_info("join IPv6 group: %s iface:%d", addr, req.ifr_ifindex);
 			res = setsockopt(fd, IPPROTO_IPV6, IPV6_JOIN_GROUP, &mr6, sizeof(mr6));
 		} else {
-		        sa6->sin6_addr = in6addr_any;
+			struct sockaddr_in6 *ba6 = (struct sockaddr_in6*)&ba;
+			ba6->sin6_addr = in6addr_any;
 		}
 	} else {
 		res = -EINVAL;
@@ -511,11 +526,18 @@ static int make_recv_socket(struct sockaddr_storage *sa, socklen_t salen,
 		goto error;
 	}
 
-	if (bind(fd, (struct sockaddr*)sa, salen) < 0) {
+	if (bind(fd, (struct sockaddr*)&ba, salen) < 0) {
 		res = -errno;
 		pw_log_error("bind() failed: %m");
 		goto error;
 	}
+	if (do_connect) {
+		if (connect(fd, (struct sockaddr*)sa, salen) < 0) {
+			res = -errno;
+			pw_log_error("connect() failed: %m");
+			goto error;
+		}
+	}
 	return fd;
 error:
 	close(fd);
@@ -529,9 +551,11 @@ static void update_ts_refclk(struct impl *impl)
 
 	// Read if something is left in the socket
 	int avail;
-	ioctl(impl->ptp_fd, FIONREAD, &avail);
 	uint8_t tmp;
-	while (avail--) read(impl->ptp_fd, &tmp, 1);
+
+	ioctl(impl->ptp_fd, FIONREAD, &avail);
+	pw_log_debug("Flushing stale data: %u bytes", avail);
+	while (avail-- && read(impl->ptp_fd, &tmp, 1));
 
 	struct ptp_management_msg req;
 	spa_zero(req);
@@ -634,68 +658,45 @@ static void update_ts_refclk(struct impl *impl)
 	memcpy(impl->gm_id, gmid, 8);
 }
 
-static int send_sap(struct impl *impl, struct session *sess, bool bye)
+static int make_sdp(struct impl *impl, struct session *sess, char *buffer, size_t buffer_size)
 {
-	char buffer[2048], src_addr[64], dst_addr[64], dst_ttl[8];
-	const char *user_name;
-	struct sockaddr *sa = (struct sockaddr*)&impl->src_addr;
-	struct sap_header header;
-	struct iovec iov[4];
-	struct msghdr msg;
-	struct spa_strbuf buf;
+	char src_addr[64], dst_addr[64], dst_ttl[8];
 	struct sdp_info *sdp = &sess->info;
 	bool src_ip4, dst_ip4;
+	bool multicast;
+	const char *user_name;
+	struct spa_strbuf buf;
 	int res;
 
-	if (!sess->has_sent_sap && bye)
-		return 0;
-
-	spa_zero(header);
-	header.v = 1;
-	header.t = bye;
-	header.msg_id_hash = sdp->hash;
-
-	iov[0].iov_base = &header;
-	iov[0].iov_len = sizeof(header);
-
 	if ((res = pw_net_get_ip(&impl->src_addr, src_addr, sizeof(src_addr), &src_ip4, NULL)) < 0)
 		return res;
 
-	if (src_ip4) {
-		iov[1].iov_base = &((struct sockaddr_in*) sa)->sin_addr;
-		iov[1].iov_len = 4U;
-	} else {
-		iov[1].iov_base = &((struct sockaddr_in6*) sa)->sin6_addr;
-		iov[1].iov_len = 16U;
-		header.a = 1;
-	}
-	iov[2].iov_base = SAP_MIME_TYPE;
-	iov[2].iov_len = sizeof(SAP_MIME_TYPE);
-
 	if ((res = pw_net_get_ip(&sdp->dst_addr, dst_addr, sizeof(dst_addr), &dst_ip4, NULL)) < 0)
 		return res;
 
 	if ((user_name = pw_get_user_name()) == NULL)
 		user_name = "-";
 
+	multicast = is_multicast((struct sockaddr*)&sdp->dst_addr, sdp->dst_len);
+
 	spa_zero(dst_ttl);
-	if (is_multicast((struct sockaddr*)&sdp->dst_addr, sdp->dst_len))
+	if (multicast)
 		snprintf(dst_ttl, sizeof(dst_ttl), "/%d", sdp->ttl);
 
-	spa_strbuf_init(&buf, buffer, sizeof(buffer));
+	spa_strbuf_init(&buf, buffer, buffer_size);
 	/* Don't add any sdp records in between this definition or change the order
 	   it will break compatibility with Dante/AES67 devices. Add new records to
 	   the end. */
 	spa_strbuf_append(&buf,
 			"v=0\n"
-			"o=%s %u 0 IN %s %s\n"
+			"o=%s %u %u IN %s %s\n"
 			"s=%s\n"
 			"c=IN %s %s%s\n"
 			"t=%u 0\n"
 			"m=%s %u RTP/AVP %i\n",
-			user_name, sdp->ntp, src_ip4 ? "IP4" : "IP6", src_addr,
+			user_name, sdp->session_id, sdp->session_version, src_ip4 ? "IP4" : "IP6", src_addr,
 			sdp->session_name,
-			dst_ip4 ? "IP4" : "IP6", dst_addr, dst_ttl,
+			(multicast ? dst_ip4 : src_ip4) ? "IP4" : "IP6", multicast ? dst_addr : src_addr, dst_ttl,
 			sdp->t_ntp,
 			sdp->media_type, sdp->dst_port, sdp->payload);
 
@@ -732,6 +733,9 @@ static int send_sap(struct impl *impl, struct session *sess, bool bye)
 			"a=source-filter: incl IN %s %s %s\n", dst_ip4 ? "IP4" : "IP6",
 				dst_addr, src_addr);
 
+	if (sdp->ssrc > 0)
+		spa_strbuf_append(&buf, "a=ssrc:%u\n", sdp->ssrc);
+
 	if (sdp->ptime > 0)
 		spa_strbuf_append(&buf,
 			"a=ptime:%.6g\n", sdp->ptime);
@@ -770,10 +774,95 @@ static int send_sap(struct impl *impl, struct session *sess, bool bye)
 	if (impl->extra_attrs_end)
 		spa_strbuf_append(&buf, "%s", impl->extra_attrs_end);
 
-	pw_log_debug("sending SAP for %u %s", sess->node->id, buffer);
+	return 0;
+}
+
+static int send_sap(struct impl *impl, struct session *sess, bool bye)
+{
+	struct sap_header header;
+	struct iovec iov[4];
+	struct msghdr msg;
+	struct sdp_info *sdp = &sess->info;
+	int res;
+
+	if (!sess->has_sent_sap && bye)
+		return 0;
+
+	if (impl->sap_fd == -1) {
+		int fd;
+		char addr[64];
+		const char *str;
+
+		if ((str = pw_properties_get(sess->props, "source.ip")) == NULL) {
+			if (impl->ifname) {
+				int fd = socket(impl->sap_addr.ss_family, SOCK_DGRAM, 0);
+				if (fd >= 0) {
+					struct ifreq req;
+					spa_zero(req);
+					req.ifr_addr.sa_family = impl->sap_addr.ss_family;
+					snprintf(req.ifr_name, sizeof(req.ifr_name), "%s", impl->ifname);
+					res = ioctl(fd, SIOCGIFADDR, &req);
+					if (res < 0)
+						pw_log_warn("SIOCGIFADDR %s failed: %m", impl->ifname);
+					str = inet_ntop(req.ifr_addr.sa_family,
+							&((struct sockaddr_in *)&req.ifr_addr)->sin_addr,
+							addr, sizeof(addr));
+					if (str == NULL) {
+						pw_log_warn("can't parse interface ip: %m");
+					} else {
+						pw_log_info("interface %s IP: %s", impl->ifname, str);
+					}
+					close(fd);
+				}
+			}
+			if (str == NULL)
+				str = impl->sap_addr.ss_family == AF_INET ?
+					DEFAULT_SOURCE_IP : DEFAULT_SOURCE_IP6;
+		}
+		if ((res = pw_net_parse_address(str, 0, &impl->src_addr, &impl->src_len)) < 0) {
+			pw_log_error("invalid source.ip %s: %s", str, spa_strerror(res));
+			return res;
+		}
+		if ((fd = make_send_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;
+	}
+
+        /* For the first session, we might not yet have an SDP because the
+         * socket needs to be open for us to get the interface address (which
+         * happens above. So let's create the SDP now, if needed. */
+        if (!sess->has_sdp) {
+		res = make_sdp(impl, sess, sess->sdp, sizeof(sess->sdp));
+		if (res != 0) {
+			pw_log_error("Failed to create SDP: %s", spa_strerror(res));
+			return res;
+		}
+		sess->has_sdp = true;
+	}
+
+	spa_zero(header);
+	header.v = 1;
+	header.t = bye;
+	header.msg_id_hash = sdp->hash;
+
+	iov[0].iov_base = &header;
+	iov[0].iov_len = sizeof(header);
 
-	iov[3].iov_base = buffer;
-	iov[3].iov_len = strlen(buffer);
+	if (impl->src_addr.ss_family == AF_INET) {
+		iov[1].iov_base = &((struct sockaddr_in*) &impl->src_addr)->sin_addr;
+		iov[1].iov_len = 4U;
+	} else {
+		iov[1].iov_base = &((struct sockaddr_in6*) &impl->src_addr)->sin6_addr;
+		iov[1].iov_len = 16U;
+		header.a = 1;
+	}
+	iov[2].iov_base = SAP_MIME_TYPE;
+	iov[2].iov_len = sizeof(SAP_MIME_TYPE);
+	iov[3].iov_base = sess->sdp;
+	iov[3].iov_len = strlen(sess->sdp);
 
 	msg.msg_name = NULL;
 	msg.msg_namelen = 0;
@@ -783,6 +872,8 @@ static int send_sap(struct impl *impl, struct session *sess, bool bye)
 	msg.msg_controllen = 0;
 	msg.msg_flags = 0;
 
+	pw_log_debug("sending SAP for %u %s", sess->node->id, sess->sdp);
+
 	res = sendmsg(impl->sap_fd, &msg, MSG_NOSIGNAL);
 	if (res < 0)
 		res = -errno;
@@ -827,37 +918,50 @@ static struct session *session_find(struct impl *impl, const struct sdp_info *in
 	return NULL;
 }
 
+static inline void replace_str(char **dst, const char *val)
+{
+	free(*dst);
+	*dst = val ? strdup(val) : NULL;
+}
+
 static struct session *session_new_announce(struct impl *impl, struct node *node,
 		struct pw_properties *props)
 {
+	char buffer[MAX_SDP];
 	struct session *sess = NULL;
 	struct sdp_info *sdp;
 	const char *str;
 	uint32_t port;
 	int res;
 
-	if (impl->n_sessions >= MAX_SESSIONS) {
-		pw_log_warn("too many sessions (%u >= %u)", impl->n_sessions, MAX_SESSIONS);
-		errno = EMFILE;
-		return NULL;
+	sess = node->session;
+	if (sess == NULL) {
+		if (impl->n_sessions >= impl->max_sessions) {
+			pw_log_warn("too many sessions (%u >= %u)", impl->n_sessions, impl->max_sessions);
+			errno = EMFILE;
+			return NULL;
+		}
+		sess = calloc(1, sizeof(struct session));
+		if (sess == NULL)
+			return NULL;
+
+		pw_log_info("created new session for node:%u", node->id);
+		node->session = sess;
+		sess->node = node;
+		sess->impl = impl;
+		sess->announce = true;
+		spa_list_append(&impl->sessions, &sess->link);
+		impl->n_sessions++;
 	}
 
-	sess = calloc(1, sizeof(struct session));
-	if (sess == NULL)
-		return NULL;
-
 	sdp = &sess->info;
 
-	sess->announce = true;
-
-	sdp->hash = pw_rand32();
-	sdp->ntp = (uint32_t) time(NULL) + 2208988800U + impl->n_sessions;
-	sdp->t_ntp = pw_properties_get_uint32(props, "rtp.ntp", sdp->ntp);
+	pw_properties_free(sess->props);
 	sess->props = props;
 
 	if ((str = pw_properties_get(props, "sess.name")) == NULL)
 		str = pw_get_host_name();
-	sdp->session_name = strdup(str);
+	replace_str(&sdp->session_name, str);
 
 	if ((str = pw_properties_get(props, "rtp.destination.port")) == NULL)
 		goto error_free;
@@ -882,43 +986,78 @@ static struct session *session_new_announce(struct impl *impl, struct node *node
 		if (!spa_atou32(str, &sdp->framecount, 0))
 			sdp->framecount = 0;
 
-	if ((str = pw_properties_get(props, "rtp.media")) != NULL)
-		sdp->media_type = strdup(str);
-	if ((str = pw_properties_get(props, "rtp.mime")) != NULL)
-		sdp->mime_type = strdup(str);
+	str = pw_properties_get(props, "rtp.media");
+	replace_str(&sdp->media_type, str);
+	str = pw_properties_get(props, "rtp.mime");
+	replace_str(&sdp->mime_type, str);
+
 	if ((str = pw_properties_get(props, "rtp.rate")) != NULL)
 		sdp->rate = atoi(str);
 	if ((str = pw_properties_get(props, "rtp.channels")) != NULL)
 		sdp->channels = atoi(str);
+	if ((str = pw_properties_get(props, "rtp.ssrc")) != NULL)
+		sdp->ssrc = atoi(str);
+	else
+		sdp->ssrc = 0;
 	if ((str = pw_properties_get(props, "rtp.ts-offset")) != NULL)
 		sdp->ts_offset = atoi(str);
-	if ((str = pw_properties_get(props, "rtp.ts-refclk")) != NULL)
-		sdp->ts_refclk = strdup(str);
+	str = pw_properties_get(props, "rtp.ts-refclk");
+	replace_str(&sdp->ts_refclk, str);
+
 	sess->ts_refclk_ptp = pw_properties_get_bool(props, "rtp.fetch-ts-refclk", false);
 	if ((str = pw_properties_get(props, PW_KEY_NODE_CHANNELNAMES)) != NULL) {
 		struct spa_strbuf buf;
-		spa_strbuf_init(&buf, sdp->channelmap, sizeof(sdp->channelmap));
-
-		struct spa_json it[2];
+		struct spa_json it[1];
 		char v[256];
+		int count = 0;
 
-		spa_json_init(&it[0], str, strlen(str));
-		if (spa_json_enter_array(&it[0], &it[1]) <= 0)
-			spa_json_init(&it[1], str, strlen(str));
+		spa_strbuf_init(&buf, sdp->channelmap, sizeof(sdp->channelmap));
 
-		if (spa_json_get_string(&it[1], v, sizeof(v)) > 0)
-			spa_strbuf_append(&buf, "%s", v);
-		while (spa_json_get_string(&it[1], v, sizeof(v)) > 0)
-			spa_strbuf_append(&buf, ", %s", v);
+		if (spa_json_begin_array_relax(&it[0], str, strlen(str)) > 0) {
+			while (spa_json_get_string(&it[0], v, sizeof(v)) > 0)
+				spa_strbuf_append(&buf, "%s%s", count++ > 0 ? ", " : "", v);
+		}
 	}
 
-	pw_log_info("created new session for node:%u", node->id);
-	node->session = sess;
-	sess->node = node;
+	/* see if we can make an SDP, will fail for the first session because we
+	 * haven't got the SAP socket open yet */
+	res = make_sdp(impl, sess, buffer, sizeof(buffer));
+
+	/* we had no sdp or something changed */
+	if (res == 0 && (!sess->has_sdp || strcmp(buffer, sess->sdp) != 0)) {
+		/*  send bye on the old session */
+		send_sap(impl, sess, 1);
+
+		/* update the version and hash */
+		sdp->hash = pw_rand32();
+		if ((str = pw_properties_get(props, "sess.id")) != NULL) {
+			if (!spa_atou32(str, &sdp->session_id, 10)) {
+				pw_log_error("Invalid session id: %s (must be a uint32)", str);
+				goto error_free;
+			}
+			sdp->t_ntp = pw_properties_get_uint32(props, "rtp.ntp",
+					(uint32_t) time(NULL) + 2208988800U + impl->n_sessions);
+		} else {
+			sdp->session_id = (uint32_t) time(NULL) + 2208988800U + impl->n_sessions;
+			sdp->t_ntp = pw_properties_get_uint32(props, "rtp.ntp", sdp->session_id);
+		}
+		if ((str = pw_properties_get(props, "sess.version")) != NULL) {
+			if (!spa_atou32(str, &sdp->session_version, 10)) {
+				pw_log_error("Invalid session version: %s (must be a uint32)", str);
+				goto error_free;
+			}
+		} else {
+			sdp->session_version = sdp->t_ntp;
+		}
 
-	sess->impl = impl;
-	spa_list_append(&impl->sessions, &sess->link);
-	impl->n_sessions++;
+		/* make an updated SDP for sending, this should not actually fail */
+		res = make_sdp(impl, sess, sess->sdp, sizeof(sess->sdp));
+
+		if (res == 0)
+			sess->has_sdp = true;
+		else
+			pw_log_error("Failed to create SDP: %s", spa_strerror(res));
+	}
 
 	send_sap(impl, sess, 0);
 
@@ -1002,6 +1141,8 @@ static int session_load_source(struct session *session, struct pw_properties *pr
 			if ((str = pw_properties_get(props, "rtp.channels")) != NULL)
 				pw_properties_set(props, "audio.channels", str);
 		}
+		if ((str = pw_properties_get(props, "rtp.ssrc")) != NULL)
+			fprintf(f, "\"rtp.receiver-ssrc\" = \"%s\", ", str);
 	} else {
 		pw_log_error("Unhandled media %s", media);
 		res = -EINVAL;
@@ -1080,8 +1221,8 @@ static struct session *session_new(struct impl *impl, struct sdp_info *info)
 	const char *str;
 	char dst_addr[64], tmp[64];
 
-	if (impl->n_sessions >= MAX_SESSIONS) {
-		pw_log_warn("too many sessions (%u >= %u)", impl->n_sessions, MAX_SESSIONS);
+	if (impl->n_sessions >= impl->max_sessions) {
+		pw_log_warn("too many sessions (%u >= %u)", impl->n_sessions, impl->max_sessions);
 		errno = EMFILE;
 		return NULL;
 	}
@@ -1128,6 +1269,9 @@ static struct session *session_new(struct impl *impl, struct sdp_info *info)
 	pw_properties_setf(props, "rtp.ts-offset", "%u", info->ts_offset);
 	pw_properties_set(props, "rtp.ts-refclk", info->ts_refclk);
 
+	if (info->ssrc > 0)
+		pw_properties_setf(props, "rtp.ssrc", "%u", info->ssrc);
+
 	if (info->channelmap[0])
 		pw_properties_set(props, PW_KEY_NODE_CHANNELNAMES, info->channelmap);
 
@@ -1277,13 +1421,25 @@ static int parse_sdp_a_rtpmap(struct impl *impl, char *c, struct sdp_info *info)
 	return 0;
 }
 
+static int parse_sdp_a_ssrc(struct impl *impl, char *c, struct sdp_info *info)
+{
+	if (!spa_strstartswith(c, "a=ssrc:"))
+		return 0;
+
+	c += strlen("a=ssrc:");
+	if (!spa_atou32(c, &info->ssrc, 10))
+		return -EINVAL;
+	return 0;
+}
+
 static int parse_sdp_a_ptime(struct impl *impl, char *c, struct sdp_info *info)
 {
 	if (!spa_strstartswith(c, "a=ptime:"))
 		return 0;
 
 	c += strlen("a=ptime:");
-	spa_atof(c, &info->ptime);
+	if (!spa_atof(c, &info->ptime))
+		return -EINVAL;
 	return 0;
 }
 
@@ -1342,6 +1498,8 @@ static int parse_sdp(struct impl *impl, char *sdp, struct sdp_info *info)
 			res = parse_sdp_m(impl, s, info);
 		else if (spa_strstartswith(s, "a=rtpmap:"))
 			res = parse_sdp_a_rtpmap(impl, s, info);
+		else if (spa_strstartswith(s, "a=ssrc:"))
+			res = parse_sdp_a_ssrc(impl, s, info);
 		else if (spa_strstartswith(s, "a=ptime:"))
 			res = parse_sdp_a_ptime(impl, s, info);
 		else if (spa_strstartswith(s, "a=mediaclk:"))
@@ -1439,7 +1597,7 @@ on_sap_io(void *data, int fd, uint32_t mask)
 	int res;
 
 	if (mask & SPA_IO_IN) {
-		uint8_t buffer[2048];
+		uint8_t buffer[MAX_SDP];
 		ssize_t len;
 
 		if ((len = recv(fd, buffer, sizeof(buffer), 0)) < 0) {
@@ -1457,17 +1615,10 @@ on_sap_io(void *data, int fd, uint32_t mask)
 
 static int start_sap(struct impl *impl)
 {
-	int fd, res;
+	int fd = -1, res;
 	struct timespec value, interval;
 	char addr[128] = "invalid";
 
-	if ((fd = make_send_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) {
@@ -1495,7 +1646,8 @@ static int start_sap(struct impl *impl)
 
 	return 0;
 error:
-	close(fd);
+	if (fd > 0)
+		close(fd);
 	return res;
 }
 
@@ -1505,7 +1657,12 @@ static void node_event_info(void *data, const struct pw_node_info *info)
 	struct impl *impl = n->impl;
 	const char *str;
 
-	if (n->session != NULL || info == NULL)
+	if (info == NULL)
+		return;
+
+	// We only really want to do anything here if properties are updated,
+	// or if we don't have a session for this node already
+	if (!(info->change_mask & PW_NODE_CHANGE_MASK_PROPS) && n->session != NULL)
 		return;
 
 	n->info = pw_node_info_merge(n->info, info, true);
@@ -1678,7 +1835,6 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
 	uint32_t port;
 	const char *str;
 	int res = 0;
-	char addr[64];
 
 	PW_LOG_TOPIC_INIT(mod_topic);
 
@@ -1723,70 +1879,34 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
 	impl->cleanup_interval = pw_properties_get_uint32(impl->props,
 			"sap.cleanup.sec", DEFAULT_CLEANUP_SEC);
 
-	if ((str = pw_properties_get(props, "source.ip")) == NULL) {
-		if (impl->ifname) {
-			int fd = socket(impl->sap_addr.ss_family, SOCK_DGRAM, 0);
-			if (fd >= 0) {
-				struct ifreq req;
-				spa_zero(req);
-				req.ifr_addr.sa_family = impl->sap_addr.ss_family;
-				snprintf(req.ifr_name, sizeof(req.ifr_name), "%s", impl->ifname);
-				res = ioctl(fd, SIOCGIFADDR, &req);
-				if (res < 0)
-					pw_log_warn("SIOCGIFADDR %s failed: %m", impl->ifname);
-				str = inet_ntop(req.ifr_addr.sa_family,
-						&((struct sockaddr_in *)&req.ifr_addr)->sin_addr,
-						addr, sizeof(addr));
-				if (str == NULL) {
-					pw_log_warn("can't parse interface ip: %m");
-				} else {
-					pw_log_info("interface %s IP: %s", impl->ifname, str);
-				}
-				close(fd);
-			}
-		}
-		if (str == NULL)
-			str = impl->sap_addr.ss_family == AF_INET ?
-				DEFAULT_SOURCE_IP : DEFAULT_SOURCE_IP6;
-	}
-	if ((res = pw_net_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->ttl = pw_properties_get_uint32(props, "net.ttl", DEFAULT_TTL);
 	impl->mcast_loop = pw_properties_get_bool(props, "net.loop", DEFAULT_LOOP);
+	impl->max_sessions = pw_properties_get_uint32(props, "sap.max-sessions", DEFAULT_MAX_SESSIONS);
 
 	impl->extra_attrs_preamble = NULL;
 	impl->extra_attrs_end = NULL;
-	char buffer[2048];
+	char buffer[MAX_SDP];
 	struct spa_strbuf buf;
 	if ((str = pw_properties_get(props, "sap.preamble-extra")) != NULL) {
 		spa_strbuf_init(&buf, buffer, sizeof(buffer));
-		struct spa_json it[2];
+		struct spa_json it[1];
 		char line[256];
 
-		spa_json_init(&it[0], str, strlen(str));
-		if (spa_json_enter_array(&it[0], &it[1]) <= 0)
-			spa_json_init(&it[1], str, strlen(str));
-
-		while (spa_json_get_string(&it[1], line, sizeof(line)) > 0)
-			spa_strbuf_append(&buf, "%s\n", line);
-
+		if (spa_json_begin_array_relax(&it[0], str, strlen(str)) > 0) {
+			while (spa_json_get_string(&it[0], line, sizeof(line)) > 0)
+				spa_strbuf_append(&buf, "%s\n", line);
+		}
 		impl->extra_attrs_preamble = strdup(buffer);
 	}
 	if ((str = pw_properties_get(props, "sap.end-extra")) != NULL) {
 		spa_strbuf_init(&buf, buffer, sizeof(buffer));
-		struct spa_json it[2];
+		struct spa_json it[1];
 		char line[256];
 
-		spa_json_init(&it[0], str, strlen(str));
-		if (spa_json_enter_array(&it[0], &it[1]) <= 0)
-			spa_json_init(&it[1], str, strlen(str));
-
-		while (spa_json_get_string(&it[1], line, sizeof(line)) > 0)
-			spa_strbuf_append(&buf, "%s\n", line);
-
+		if (spa_json_begin_array_relax(&it[0], str, strlen(str)) > 0) {
+			while (spa_json_get_string(&it[0], line, sizeof(line)) > 0)
+				spa_strbuf_append(&buf, "%s\n", line);
+		}
 		impl->extra_attrs_end = strdup(buffer);
 	}
 
diff --git a/src/modules/module-rtp-sink.c b/src/modules/module-rtp-sink.c
index 4d64b8b7..7abd4d53 100644
--- a/src/modules/module-rtp-sink.c
+++ b/src/modules/module-rtp-sink.c
@@ -65,6 +65,8 @@
  * - `sess.ts-refclk = <string>`: the name of a reference clock
  * - `sess.media = <string>`: the media type audio|midi|opus, default audio
  * - `stream.props = {}`: properties to be passed to the stream
+ * - `aes67.driver-group = <string>`: for AES67 streams, can be specified in order to allow
+ *       the sink to be driven by a different node than the PTP driver.
  *
  * ## General options
  *
@@ -147,6 +149,7 @@ PW_LOG_TOPIC(mod_topic, "mod." NAME);
 		"( 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"> ) "				\
+		"( aes67.driver-group=<driver driving the PTP send> ) "					\
 		"( stream.props= { key=value ... } ) "
 
 static const struct spa_dict_item module_info[] = {
@@ -334,7 +337,8 @@ static void stream_props_changed(struct impl *impl, uint32_t id, const struct sp
 			struct spa_pod_frame f;
 			const char *key;
 			struct spa_pod *pod;
-			const char *value;
+			struct spa_dict_item items[4];
+			unsigned int n_items = 0;
 
 			if (spa_pod_parse_object(param, SPA_TYPE_OBJECT_Props, NULL, SPA_PROP_params,
 					SPA_POD_OPT_Pod(&params)) < 0)
@@ -343,27 +347,44 @@ static void stream_props_changed(struct impl *impl, uint32_t id, const struct sp
 			if (spa_pod_parser_push_struct(&prs, &f) < 0)
 				return;
 
-			while (true) {
+			while (n_items < SPA_N_ELEMENTS(items)) {
+				const char *value_str = NULL;
+				int value_int = -1;
+
 				if (spa_pod_parser_get_string(&prs, &key) < 0)
 					break;
 				if (spa_pod_parser_get_pod(&prs, &pod) < 0)
 					break;
-				if (spa_pod_get_string(pod, &value) < 0)
-					continue;
-				pw_log_info("key '%s', value '%s'", key, value);
-				if (!spa_streq(key, "destination.ip"))
+				if (spa_pod_get_string(pod, &value_str) < 0 &&
+						spa_pod_get_int(pod, &value_int) < 0)
 					continue;
-				if (pw_net_parse_address(value, impl->dst_port, &impl->dst_addr,
-						&impl->dst_len) < 0) {
-					pw_log_error("invalid destination.ip: '%s'", value);
-					break;
+				pw_log_info("key '%s', value '%s'/%u", key, value_str, value_int);
+				if (spa_streq(key, "destination.ip")) {
+					if (!value_str || pw_net_parse_address(value_str, impl->dst_port, &impl->dst_addr,
+								&impl->dst_len) < 0) {
+						pw_log_error("invalid destination.ip: '%s'", value_str);
+						break;
+					}
+					pw_properties_set(impl->stream_props, "rtp.destination.ip", value_str);
+					items[n_items++] = SPA_DICT_ITEM_INIT("rtp.destination.ip", value_str);
+				} else if (spa_streq(key, "sess.name")) {
+					if (!value_str) {
+						pw_log_error("invalid sess.name");
+						break;
+					}
+					pw_properties_set(impl->stream_props, "sess.name", value_str);
+					items[n_items++] = SPA_DICT_ITEM_INIT("sess.name", value_str);
+				} else if (spa_streq(key, "sess.id") || spa_streq(key, "sess.version")) {
+					if (value_int < 0 || (unsigned int)value_int > UINT32_MAX) {
+						pw_log_error("invalid %s: '%d'", key, value_int);
+						break;
+					}
+					pw_properties_setf(impl->stream_props, key, "%d", value_int);
+					items[n_items++] = SPA_DICT_ITEM_INIT(key, pw_properties_get(impl->stream_props, key));
 				}
-				pw_properties_set(impl->stream_props, "rtp.destination.ip", value);
-				struct spa_dict_item item[1];
-				item[0] = SPA_DICT_ITEM_INIT("rtp.destination.ip", value);
-				rtp_stream_update_properties(impl->stream, &SPA_DICT_INIT(item, 1));
-				break;
 			}
+
+			rtp_stream_update_properties(impl->stream, &SPA_DICT_INIT(items, n_items));
 		}
 	}
 }
@@ -466,6 +487,7 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
 	const char *str, *sess_name;
 	int64_t ts_offset;
 	int res = 0;
+	uint32_t header_size;
 
 	PW_LOG_TOPIC_INIT(mod_topic);
 
@@ -527,10 +549,13 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
 	copy_props(impl, props, "net.mtu");
 	copy_props(impl, props, "sess.media");
 	copy_props(impl, props, "sess.name");
+	copy_props(impl, props, "sess.id");
+	copy_props(impl, props, "sess.version");
 	copy_props(impl, props, "sess.min-ptime");
 	copy_props(impl, props, "sess.max-ptime");
 	copy_props(impl, props, "sess.latency.msec");
 	copy_props(impl, props, "sess.ts-refclk");
+	copy_props(impl, props, "aes67.driver-group");
 
 	str = pw_properties_get(props, "local.ifname");
 	impl->ifname = str ? strdup(str) : NULL;
@@ -560,6 +585,10 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
 		ts_offset = pw_rand32();
 	pw_properties_setf(stream_props, "rtp.sender-ts-offset", "%u", (uint32_t)ts_offset);
 
+	header_size = impl->dst_addr.ss_family == AF_INET ?
+                        IP4_HEADER_SIZE : IP6_HEADER_SIZE;
+	header_size += UDP_HEADER_SIZE;
+	pw_properties_setf(stream_props, "net.header", "%u", header_size);
 	pw_net_get_ip(&impl->src_addr, addr, sizeof(addr), NULL, NULL);
 	pw_properties_set(stream_props, "rtp.source.ip", addr);
 	pw_net_get_ip(&impl->dst_addr, addr, sizeof(addr), NULL, NULL);
diff --git a/src/modules/module-rtp-source.c b/src/modules/module-rtp-source.c
index 79411f0d..98a26065 100644
--- a/src/modules/module-rtp-source.c
+++ b/src/modules/module-rtp-source.c
@@ -56,6 +56,7 @@
  * - `sess.latency.msec = <float>`: target network latency in milliseconds, default 100
  * - `sess.ignore-ssrc = <bool>`: ignore SSRC, default false
  * - `sess.media = <string>`: the media type audio|midi|opus, default audio
+ * - `stream.may-pause = <bool>`: pause the stream when no data is reveived, default false
  * - `stream.props = {}`: properties to be passed to the stream
  *
  * ## General options
@@ -165,10 +166,33 @@ struct impl {
 	uint8_t *buffer;
 	size_t buffer_size;
 
-	unsigned receiving:1;
-	unsigned last_receiving:1;
+	bool receiving;
+	bool may_pause;
+	bool standby;
+	bool waiting;
 };
 
+static int do_start(struct spa_loop *loop, bool async, uint32_t seq, const void *data,
+		size_t size, void *user_data)
+{
+	struct impl *impl = user_data;
+	if (impl->waiting) {
+		struct spa_dict_item item[1];
+
+		impl->waiting = false;
+		impl->standby = false;
+
+		pw_log_info("resume RTP source");
+
+		item[0] = SPA_DICT_ITEM_INIT("rtp.receiving", "true");
+		rtp_stream_update_properties(impl->stream, &SPA_DICT_INIT(item, 1));
+
+		if (impl->may_pause)
+			rtp_stream_set_active(impl->stream, true);
+	}
+	return 0;
+}
+
 static void
 on_rtp_io(void *data, int fd, uint32_t mask)
 {
@@ -187,7 +211,10 @@ on_rtp_io(void *data, int fd, uint32_t mask)
 				goto receive_error;
 		}
 
-		impl->receiving = true;
+		if (!impl->receiving) {
+			impl->receiving = true;
+			pw_loop_invoke(impl->loop, do_start, 1, NULL, 0, false, impl);
+		}
 	}
 	return;
 
@@ -353,7 +380,7 @@ static void stream_state_changed(void *data, bool started, const char *error)
 			rtp_stream_set_error(impl->stream, res, "Can't start RTP stream");
 		}
 	} else {
-		if (!impl->always_process)
+		if (!impl->always_process && !impl->standby)
 			stream_stop(impl);
 	}
 }
@@ -430,17 +457,22 @@ static void on_timer_event(void *data, uint64_t expirations)
 {
 	struct impl *impl = data;
 
-	if (impl->receiving != impl->last_receiving) {
-		struct spa_dict_item item[1];
+	pw_log_debug("timer %d", impl->receiving);
 
-		impl->last_receiving = impl->receiving;
+	if (!impl->receiving) {
+		if (!impl->standby) {
+			struct spa_dict_item item[1];
 
-		item[0] = SPA_DICT_ITEM_INIT("rtp.receiving", impl->receiving ? "true" : "false");
-		rtp_stream_update_properties(impl->stream, &SPA_DICT_INIT(item, 1));
-	}
+			pw_log_info("timeout, standby RTP source");
+			impl->standby = true;
+			impl->waiting = true;
 
-	if (!impl->receiving) {
-		pw_log_info("timeout, inactive RTP source");
+			item[0] = SPA_DICT_ITEM_INIT("rtp.receiving", "false");
+			rtp_stream_update_properties(impl->stream, &SPA_DICT_INIT(item, 1));
+
+			if (impl->may_pause)
+				rtp_stream_set_active(impl->stream, false);
+		}
 		//pw_impl_module_schedule_destroy(impl->module);
 	} else {
 		pw_log_debug("timeout, keeping active RTP source");
@@ -532,6 +564,7 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
 	int64_t ts_offset;
 	char addr[128];
 	int res = 0;
+	uint32_t header_size;
 
 	PW_LOG_TOPIC_INIT(mod_topic);
 
@@ -591,6 +624,7 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
 	copy_props(impl, props, "sess.latency.msec");
 	copy_props(impl, props, "sess.ts-direct");
 	copy_props(impl, props, "sess.ignore-ssrc");
+	copy_props(impl, props, "stream.may-pause");
 
 	str = pw_properties_get(props, "local.ifname");
 	impl->ifname = str ? strdup(str) : NULL;
@@ -611,6 +645,11 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
 	pw_properties_set(stream_props, "rtp.source.ip", addr);
 	pw_properties_setf(stream_props, "rtp.source.port", "%u", impl->src_port);
 
+	header_size = impl->src_addr.ss_family == AF_INET ?
+                        IP4_HEADER_SIZE : IP6_HEADER_SIZE;
+	header_size += UDP_HEADER_SIZE;
+	pw_properties_setf(stream_props, "net.header", "%u", header_size);
+
 	ts_offset = pw_properties_get_int64(props, "sess.ts-offset", DEFAULT_TS_OFFSET);
 	if (ts_offset == -1)
 		ts_offset = pw_rand32();
@@ -618,6 +657,13 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
 
 	impl->always_process = pw_properties_get_bool(stream_props,
 			PW_KEY_NODE_ALWAYS_PROCESS, true);
+	impl->may_pause = pw_properties_get_bool(stream_props,
+			"stream.may-pause", false);
+	impl->standby = false;
+	impl->waiting = true;
+	/* Because we don't know the stream receiving state at the start, we try to fake it
+	 * till we make it (or get timed out) */
+	pw_properties_set(stream_props, "rtp.receiving", "true");
 
 	impl->cleanup_interval = pw_properties_get_uint32(props,
 			"cleanup.sec", DEFAULT_CLEANUP_SEC);
diff --git a/src/modules/module-rtp/audio.c b/src/modules/module-rtp/audio.c
index 43b74e91..e89712a3 100644
--- a/src/modules/module-rtp/audio.c
+++ b/src/modules/module-rtp/audio.c
@@ -37,14 +37,14 @@ static void rtp_audio_process_playback(void *data)
 		memset(d[0].data, 0, wanted * stride);
 		if (impl->have_sync) {
 			impl->have_sync = false;
-			level = SPA_LOG_LEVEL_WARN;
+			level = SPA_LOG_LEVEL_INFO;
 		} else {
 			level = SPA_LOG_LEVEL_DEBUG;
 		}
 		pw_log(level, "underrun %d/%u < %u",
 					avail, target_buffer, wanted);
 	} else {
-		float error, corr;
+		double error, corr;
 		if (impl->first) {
 			if ((uint32_t)avail > target_buffer) {
 				uint32_t skip = avail - target_buffer;
@@ -63,19 +63,28 @@ static void rtp_audio_process_playback(void *data)
 			/* when not using direct timestamp and clocks are not
 			 * in sync, try to adjust our playback rate to keep the
 			 * requested target_buffer bytes in the ringbuffer */
-			error = (float)target_buffer - (float)avail;
-			error = SPA_CLAMP(error, -impl->max_error, impl->max_error);
+			double in_flight = 0;
+			struct spa_io_position *pos = impl->io_position;
+
+			if (SPA_LIKELY(pos && impl->last_recv_timestamp)) {
+				/* Account for samples that might be in flight but not yet received, and possibly
+				 * samples that were received _after_ the process() tick and therefore should not
+				 * yet be accounted for */
+				int64_t in_flight_ns = pos->clock.nsec - impl->last_recv_timestamp;
+				/* Use the best relative rate we know */
+				double relative_rate = impl->io_rate_match ? impl->io_rate_match->rate : pos->clock.rate_diff;
+				in_flight = (double)(in_flight_ns * impl->rate) * relative_rate / SPA_NSEC_PER_SEC;
+			}
+
+			error = (double)target_buffer - (double)avail - in_flight;
+			error = SPA_CLAMPD(error, -impl->max_error, impl->max_error);
 
-			corr = (float)spa_dll_update(&impl->dll, error);
+			corr = spa_dll_update(&impl->dll, error);
 
 			pw_log_trace("avail:%u target:%u error:%f corr:%f", avail,
 					target_buffer, error, corr);
 
-			if (impl->io_rate_match) {
-				SPA_FLAG_SET(impl->io_rate_match->flags,
-						SPA_IO_RATE_MATCH_FLAG_ACTIVE);
-				impl->io_rate_match->rate = 1.0f / corr;
-			}
+			pw_stream_set_rate(impl->stream, 1.0 / corr);
 		}
 		spa_ringbuffer_read_data(&impl->ring,
 				impl->buffer,
@@ -131,6 +140,7 @@ static int rtp_audio_receive(struct impl *impl, uint8_t *buffer, ssize_t len)
 	timestamp = ntohl(hdr->timestamp) - impl->ts_offset;
 
 	impl->receiving = true;
+	impl->last_recv_timestamp = pw_stream_get_nsec(impl->stream);
 
 	plen = len - hlen;
 	samples = plen / stride;
@@ -187,8 +197,12 @@ invalid_len:
 	pw_log_warn("invalid RTP length");
 	return -EINVAL;
 unexpected_ssrc:
-	pw_log_warn("unexpected SSRC (expected %u != %u)",
-		impl->ssrc, hdr->ssrc);
+	if (!impl->fixed_ssrc) {
+		/* We didn't have a configured SSRC, and there's more than one SSRC on
+		 * this address/port pair */
+		pw_log_warn("unexpected SSRC (expected %u != %u)", impl->ssrc,
+			hdr->ssrc);
+	}
 	return -EINVAL;
 }
 
@@ -214,7 +228,7 @@ set_iovec(struct spa_ringbuffer *rbuf, void *buffer, uint32_t size,
 	iov[1].iov_base = buffer;
 }
 
-static void rtp_audio_flush_packets(struct impl *impl, uint32_t num_packets)
+static void rtp_audio_flush_packets(struct impl *impl, uint32_t num_packets, uint64_t set_timestamp)
 {
 	int32_t avail, tosend;
 	uint32_t stride, timestamp;
@@ -250,7 +264,7 @@ static void rtp_audio_flush_packets(struct impl *impl, uint32_t num_packets)
 		else
 			header.m = 0;
 		header.sequence_number = htons(impl->seq);
-		header.timestamp = htonl(impl->ts_offset + timestamp);
+		header.timestamp = htonl(impl->ts_offset + (set_timestamp ? set_timestamp : timestamp));
 
 		set_iovec(&impl->ring,
 			impl->buffer, BUFFER_SIZE,
@@ -289,7 +303,7 @@ static void rtp_audio_flush_timeout(struct impl *impl, uint64_t expirations)
 {
 	if (expirations > 1)
 		pw_log_warn("missing timeout %"PRIu64, expirations);
-	rtp_audio_flush_packets(impl, expirations);
+	rtp_audio_flush_packets(impl, expirations, 0);
 }
 
 static void rtp_audio_process_capture(void *data)
@@ -303,6 +317,11 @@ static void rtp_audio_process_capture(void *data)
 	struct spa_io_position *pos;
 	uint64_t next_nsec, quantum;
 
+	if (impl->separate_sender) {
+		/* apply the DLL rate */
+		pw_stream_set_rate(impl->stream, impl->ptp_corr);
+	}
+
 	if ((buf = pw_stream_dequeue_buffer(impl->stream)) == NULL) {
 		pw_log_info("Out of stream buffers: %m");
 		return;
@@ -322,6 +341,14 @@ static void rtp_audio_process_capture(void *data)
 		timestamp = pos->clock.position * impl->rate / rate;
 		next_nsec = pos->clock.next_nsec;
 		quantum = (uint64_t)(pos->clock.duration * SPA_NSEC_PER_SEC / (rate * pos->clock.rate_diff));
+
+		if (impl->separate_sender) {
+			/* the sender process() function uses this for managing the DLL */
+			impl->sink_nsec = pos->clock.nsec;
+			impl->sink_next_nsec = pos->clock.next_nsec;
+			impl->sink_resamp_delay = impl->io_rate_match->delay;
+			impl->sink_quantum = (uint64_t)(pos->clock.duration * SPA_NSEC_PER_SEC / rate);
+		}
 	} else {
 		timestamp = expected_timestamp;
 		next_nsec = 0;
@@ -336,17 +363,26 @@ static void rtp_audio_process_capture(void *data)
 		impl->have_sync = true;
 		expected_timestamp = timestamp;
 		filled = 0;
+
+		if (impl->separate_sender) {
+			/* the sender should know that the sync state has changed, and that it should
+			 * refill the buffer */
+			impl->refilling = true;
+		}
 	} else {
-		if (SPA_ABS((int32_t)expected_timestamp - (int32_t)timestamp) > 32) {
+		if (SPA_ABS((int)expected_timestamp - (int)timestamp) > (int)quantum) {
 			pw_log_warn("expected %u != timestamp %u", expected_timestamp, timestamp);
 			impl->have_sync = false;
-		} else if (filled + wanted > (int32_t)(BUFFER_SIZE / stride)) {
-			pw_log_warn("overrun %u + %u > %u", filled, wanted, BUFFER_SIZE / stride);
+		} else if (filled + wanted > (int32_t)SPA_MIN(impl->target_buffer * 8, BUFFER_SIZE / stride)) {
+			pw_log_warn("overrun %u + %u > %u/%u", filled, wanted,
+					impl->target_buffer * 8, BUFFER_SIZE / stride);
 			impl->have_sync = false;
 			filled = 0;
 		}
 	}
 
+	pw_log_trace("writing %u samples at %u", wanted, expected_timestamp);
+
 	spa_ringbuffer_write_data(&impl->ring,
 			impl->buffer,
 			BUFFER_SIZE,
@@ -357,12 +393,17 @@ static void rtp_audio_process_capture(void *data)
 
 	pw_stream_queue_buffer(impl->stream, buf);
 
+	if (impl->separate_sender) {
+		/* sending will happen in a separate process() */
+		return;
+	}
+
 	pending = filled / impl->psamples;
 	num_queued = (filled + wanted) / impl->psamples;
 
 	if (num_queued > 0) {
 		/* flush all previous packets plus new one right away */
-		rtp_audio_flush_packets(impl, pending + 1);
+		rtp_audio_flush_packets(impl, pending + 1, 0);
 		num_queued -= SPA_MIN(num_queued, pending + 1);
 
 		if (num_queued > 0) {
@@ -375,13 +416,210 @@ static void rtp_audio_process_capture(void *data)
 	}
 }
 
-static int rtp_audio_init(struct impl *impl, enum spa_direction direction)
+static void ptp_sender_destroy(void *d)
+{
+	struct impl *impl = d;
+	spa_hook_remove(&impl->ptp_sender_listener);
+	impl->ptp_sender = NULL;
+}
+
+static void ptp_sender_process(void *d, struct spa_io_position *position)
+{
+	struct impl *impl = d;
+	uint64_t nsec, next_nsec, quantum, quantum_nsec;
+	uint32_t ptp_timestamp, rtp_timestamp, read_idx;
+	uint32_t rate;
+	uint32_t filled;
+	double error, in_flight, delay;
+
+	nsec = position->clock.nsec;
+	next_nsec = position->clock.next_nsec;
+
+	/* the ringbuffer indices are in sink timetamp domain */
+	filled = spa_ringbuffer_get_read_index(&impl->ring, &read_idx);
+
+	if (SPA_LIKELY(position)) {
+		rate = position->clock.rate.denom;
+		quantum = position->clock.duration;
+		quantum_nsec = (uint64_t)(quantum * SPA_NSEC_PER_SEC / rate);
+		/* PTP time tells us what time it is */
+		ptp_timestamp = position->clock.position * impl->rate / rate;
+		/* RTP time is based on when we sent the first packet after the last sync */
+		rtp_timestamp = impl->rtp_base_ts + read_idx;
+	} else {
+		pw_log_warn("No clock information, skipping");
+		return;
+	}
+
+	pw_log_trace("sink nsec:%"PRIu64", sink next_nsec:%"PRIu64", ptp nsec:%"PRIu64", ptp next_sec:%"PRIu64,
+			impl->sink_nsec, impl->sink_next_nsec, nsec, next_nsec);
+
+	/* If send is lagging by more than 2 or more quanta, reset */
+	if (!impl->refilling && impl->rtp_last_ts &&
+			SPA_ABS((int32_t)ptp_timestamp - (int32_t)impl->rtp_last_ts) >= (int32_t)(2 * quantum)) {
+		pw_log_warn("expected %u - timestamp %u = %d >= 2 * %"PRIu64" quantum", rtp_timestamp, impl->rtp_last_ts,
+				(int)ptp_timestamp - (int)impl->rtp_last_ts, quantum);
+		goto resync;
+	}
+
+	if (!impl->have_sync) {
+		pw_log_trace("Waiting for sync");
+		return;
+	}
+
+	in_flight = (double)impl->sink_quantum * impl->rate / SPA_NSEC_PER_SEC *
+		(double)(nsec - impl->sink_nsec) / (impl->sink_next_nsec - impl->sink_nsec);
+	delay = filled + in_flight + impl->sink_resamp_delay;
+
+	/* Make sure the PTP node wake up times are within the bounds of sink
+	 * node wake up times (with a little bit of tolerance). */
+	if (SPA_LIKELY(nsec > impl->sink_nsec - quantum_nsec &&
+				nsec < impl->sink_next_nsec + quantum_nsec)) {
+		/* Start adjusting if we're at/past the target delay. We requested ~1/2 the buffer
+		 * size as the sink latency, so doing so ensures that we have two sink quanta of
+		 * data, making the chance of and underrun low even for small buffer values */
+		if (impl->refilling && (double)impl->target_buffer - delay <= 0) {
+			impl->refilling = false;
+			/* Store the offset for the PTP time at which we start sending */
+			impl->rtp_base_ts = ptp_timestamp - read_idx;
+			rtp_timestamp = impl->rtp_base_ts + read_idx; /* = ptp_timestamp */
+			pw_log_debug("start sending. sink quantum:%"PRIu64", ptp quantum:%"PRIu64"", impl->sink_quantum, quantum_nsec);
+		}
+
+		if (!impl->refilling) {
+			/*
+			 * As per Controlling Adaptive Resampling paper[1], maintain
+			 * W(t) - R(t) - delta = 0. We keep delta as target_buffer.
+			 *
+			 * [1] http://kokkinizita.linuxaudio.org/papers/adapt-resamp.pdf
+			 */
+			error = delay - impl->target_buffer;
+			error = SPA_CLAMPD(error, -impl->max_error, impl->max_error);
+			impl->ptp_corr = spa_dll_update(&impl->ptp_dll, error);
+
+			pw_log_debug("filled:%u in_flight:%g delay:%g target:%u error:%f corr:%f",
+					filled, in_flight, delay, impl->target_buffer, error, impl->ptp_corr);
+
+			if (filled >= impl->psamples) {
+				rtp_audio_flush_packets(impl, 1, rtp_timestamp);
+				impl->rtp_last_ts = rtp_timestamp;
+			}
+		}
+	} else {
+		pw_log_warn("PTP node wake up time out of bounds !(%"PRIu64" < %"PRIu64" < %"PRIu64")",
+				impl->sink_nsec, nsec, impl->sink_next_nsec);
+		goto resync;
+	}
+
+	return;
+
+resync:
+	impl->have_sync = false;
+	impl->rtp_last_ts = 0;
+
+	return;
+}
+
+static const struct pw_filter_events ptp_sender_events = {
+	PW_VERSION_FILTER_EVENTS,
+	.destroy = ptp_sender_destroy,
+	.process = ptp_sender_process
+};
+
+static int setup_ptp_sender(struct impl *impl, struct pw_core *core, enum pw_direction direction, const char *driver_grp)
+{
+	const struct spa_pod *params[4];
+	struct pw_properties *filter_props = NULL;
+	struct spa_pod_builder b;
+	uint32_t n_params;
+	uint8_t buffer[1024];
+	int ret;
+
+	if (direction != PW_DIRECTION_INPUT)
+		return 0;
+
+	if (driver_grp == NULL) {
+		pw_log_info("AES67 driver group not specified, no separate sender configured");
+		return 0;
+	}
+
+	pw_log_info("AES67 driver group: %s, setting up separate sender", driver_grp);
+
+	spa_dll_init(&impl->ptp_dll);
+	/* BW selected empirically, as it converges most quickly and holds reasonably well in testing */
+	spa_dll_set_bw(&impl->ptp_dll, SPA_DLL_BW_MAX, impl->psamples, impl->rate);
+	impl->ptp_corr = 1.0;
+
+	n_params = 0;
+	spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+	filter_props = pw_properties_new(NULL, NULL);
+	if (filter_props == NULL) {
+		int res = -errno;
+		pw_log_error( "can't create properties: %m");
+		return res;
+	}
+
+	pw_properties_set(filter_props, PW_KEY_NODE_GROUP, driver_grp);
+	pw_properties_setf(filter_props, PW_KEY_NODE_NAME, "%s-ptp-sender", pw_stream_get_name(impl->stream));
+	pw_properties_set(filter_props, PW_KEY_NODE_ALWAYS_PROCESS, "true");
+
+	/*
+	 * sess.latency.msec defines how much data is buffered before it is
+	 * sent out on the network. This is done by setting the node.latency
+	 * to that value, and process function will get chunks of that size.
+	 * It is then split up into psamples chunks and send every ptime.
+	 *
+	 * With this separate sender mechanism we have some latency in stream
+	 * via node.latency, and some in ringbuffer between sink and sender.
+	 * Ideally we want to have a total latency that still corresponds to
+	 * sess.latency.msec. We do this by using the property setting and
+	 * splitting some of it as stream latency and some as ringbuffer
+	 * latency. The ringbuffer latency is actually determined by how
+	 * long we wait before setting `refilling` to false and start the
+	 * sending. Also, see `filter_process`.
+	 */
+	pw_properties_setf(filter_props, PW_KEY_NODE_FORCE_QUANTUM, "%u", impl->psamples);
+	pw_properties_setf(filter_props, PW_KEY_NODE_FORCE_RATE, "%u", impl->rate);
+
+	impl->ptp_sender = pw_filter_new(core, NULL, filter_props);
+	if (impl->ptp_sender == NULL)
+		return -errno;
+
+	pw_filter_add_listener(impl->ptp_sender, &impl->ptp_sender_listener,
+			&ptp_sender_events, impl);
+
+	n_params = 0;
+	params[n_params++] = spa_format_audio_raw_build(&b,
+			SPA_PARAM_EnumFormat, &impl->info.info.raw);
+	params[n_params++] = spa_format_audio_raw_build(&b,
+			SPA_PARAM_Format, &impl->info.info.raw);
+
+	ret = pw_filter_connect(impl->ptp_sender,
+			PW_FILTER_FLAG_RT_PROCESS,
+			params, n_params);
+	if (ret == 0) {
+		pw_log_info("created pw_filter for separate sender");
+		impl->separate_sender = true;
+	} else {
+		pw_log_error("failed to create pw_filter for separate sender");
+		impl->separate_sender = false;
+	}
+
+	return ret;
+}
+
+static int rtp_audio_init(struct impl *impl, struct pw_core *core, enum spa_direction direction, const char *ptp_driver)
 {
 	if (direction == SPA_DIRECTION_INPUT)
 		impl->stream_events.process = rtp_audio_process_capture;
 	else
 		impl->stream_events.process = rtp_audio_process_playback;
+
 	impl->receive_rtp = rtp_audio_receive;
 	impl->flush_timeout = rtp_audio_flush_timeout;
+
+	setup_ptp_sender(impl, core, direction, ptp_driver);
+
 	return 0;
 }
diff --git a/src/modules/module-rtp/midi.c b/src/modules/module-rtp/midi.c
index 663be0dc..1b7f9ad6 100644
--- a/src/modules/module-rtp/midi.c
+++ b/src/modules/module-rtp/midi.c
@@ -66,7 +66,7 @@ static void rtp_midi_process_playback(void *data)
 			} else {
 				timestamp = target;
 			}
-			spa_pod_builder_control(&b, target - timestamp, SPA_CONTROL_Midi);
+			spa_pod_builder_control(&b, target - timestamp, c->type);
 			spa_pod_builder_bytes(&b,
 					SPA_POD_BODY(&c->value),
 					SPA_POD_BODY_SIZE(&c->value));
@@ -242,25 +242,34 @@ static int rtp_midi_receive_midi(struct impl *impl, uint8_t *packet, uint32_t ti
 	while (offs < end) {
 		uint32_t delta;
 		int size;
+		uint64_t state = 0;
+		uint8_t *d;
+		size_t s;
 
 		if (first && !hdr->z)
 			delta = 0;
 		else
 			offs += parse_varlen(&packet[offs], end - offs, &delta);
-
 		timestamp += (uint32_t)(delta * impl->corr);
-		spa_pod_builder_control(&b, timestamp, SPA_CONTROL_Midi);
 
 		size = get_midi_size(&packet[offs], end - offs);
-
 		if (size <= 0 || offs + size > end) {
 			pw_log_warn("invalid size (%08x) %d (%u %u)",
 					packet[offs], size, offs, end);
 			break;
 		}
 
-		spa_pod_builder_bytes(&b, &packet[offs], size);
+		d = &packet[offs];
+		s = size;
+		while (s > 0) {
+			uint32_t ump[4];
+			int ump_size = spa_ump_from_midi(&d, &s, ump, sizeof(ump), 0, &state);
+			if (ump_size <= 0)
+				break;
 
+			spa_pod_builder_control(&b, timestamp, SPA_CONTROL_UMP);
+	                spa_pod_builder_bytes(&b, ump, ump_size);
+		}
 		offs += size;
 		first = false;
 	}
@@ -321,8 +330,12 @@ invalid_len:
 	pw_log_warn("invalid RTP length");
 	return -EINVAL;
 unexpected_ssrc:
-	pw_log_warn("unexpected SSRC (expected %u != %u)",
-		impl->ssrc, hdr->ssrc);
+	if (!impl->fixed_ssrc) {
+		/* We didn't have a configured SSRC, and there's more than one SSRC on
+		* this address/port pair */
+		pw_log_warn("unexpected SSRC (expected %u != %u)",
+			impl->ssrc, hdr->ssrc);
+	}
 	return -EINVAL;
 }
 
@@ -374,14 +387,17 @@ static void rtp_midi_flush_packets(struct impl *impl,
 	max_size = impl->payload_size - sizeof(midi_header);
 
 	SPA_POD_SEQUENCE_FOREACH(sequence, c) {
-		void *ev;
-		uint32_t size, delta, offset;
+		uint32_t delta, offset;
+		uint8_t event[16];
+		size_t size;
 
-		if (c->type != SPA_CONTROL_Midi)
+		if (c->type != SPA_CONTROL_UMP)
 			continue;
 
-		ev = SPA_POD_BODY(&c->value),
-                size = SPA_POD_BODY_SIZE(&c->value);
+		size = spa_ump_to_midi(SPA_POD_BODY(&c->value),
+				SPA_POD_BODY_SIZE(&c->value), event, sizeof(event));
+		if (size <= 0)
+			continue;
 
 		offset = c->offset * impl->rate / rate;
 
@@ -415,12 +431,12 @@ static void rtp_midi_flush_packets(struct impl *impl,
 			header.sequence_number = htons(impl->seq);
 			header.timestamp = htonl(impl->ts_offset + timestamp + base);
 
-			memcpy(&impl->buffer[len], ev, size);
+			memcpy(&impl->buffer[len], event, size);
 			len += size;
 		} else {
 			delta = offset - prev_offset;
 			prev_offset = offset;
-			len += write_event(&impl->buffer[len], delta, ev, size);
+			len += write_event(&impl->buffer[len], delta, event, size);
 		}
 	}
 	if (len > 0) {
diff --git a/src/modules/module-rtp/opus.c b/src/modules/module-rtp/opus.c
index 2a5eeaf3..83857c80 100644
--- a/src/modules/module-rtp/opus.c
+++ b/src/modules/module-rtp/opus.c
@@ -49,7 +49,7 @@ static void rtp_opus_process_playback(void *data)
 		pw_log(level, "underrun %d/%u < %u",
 					avail, target_buffer, wanted);
 	} else {
-		float error, corr;
+		double error, corr;
 		if (impl->first) {
 			if ((uint32_t)avail > target_buffer) {
 				uint32_t skip = avail - target_buffer;
@@ -68,19 +68,15 @@ static void rtp_opus_process_playback(void *data)
 			/* when not using direct timestamp and clocks are not
 			 * in sync, try to adjust our playback rate to keep the
 			 * requested target_buffer bytes in the ringbuffer */
-			error = (float)target_buffer - (float)avail;
-			error = SPA_CLAMP(error, -impl->max_error, impl->max_error);
+			error = (double)target_buffer - (double)avail;
+			error = SPA_CLAMPD(error, -impl->max_error, impl->max_error);
 
-			corr = (float)spa_dll_update(&impl->dll, error);
+			corr = spa_dll_update(&impl->dll, error);
 
 			pw_log_trace("avail:%u target:%u error:%f corr:%f", avail,
 					target_buffer, error, corr);
 
-			if (impl->io_rate_match) {
-				SPA_FLAG_SET(impl->io_rate_match->flags,
-						SPA_IO_RATE_MATCH_FLAG_ACTIVE);
-				impl->io_rate_match->rate = 1.0f / corr;
-			}
+			pw_stream_set_rate(impl->stream, 1.0 / corr);
 		}
 		spa_ringbuffer_read_data(&impl->ring,
 				impl->buffer,
@@ -202,8 +198,12 @@ invalid_len:
 	pw_log_warn("invalid RTP length");
 	return -EINVAL;
 unexpected_ssrc:
-	pw_log_warn("unexpected SSRC (expected %u != %u)",
-		impl->ssrc, hdr->ssrc);
+	if (!impl->fixed_ssrc) {
+		/* We didn't have a configured SSRC, and there's more than one SSRC on
+		* this address/port pair */
+		pw_log_warn("unexpected SSRC (expected %u != %u)",
+			impl->ssrc, hdr->ssrc);
+	}
 	return -EINVAL;
 }
 
diff --git a/src/modules/module-rtp/stream.c b/src/modules/module-rtp/stream.c
index 29e55ea9..f4d3fa3b 100644
--- a/src/modules/module-rtp/stream.c
+++ b/src/modules/module-rtp/stream.c
@@ -10,7 +10,9 @@
 #include <spa/utils/ringbuffer.h>
 #include <spa/utils/dll.h>
 #include <spa/param/audio/format-utils.h>
+#include <spa/param/audio/raw-json.h>
 #include <spa/control/control.h>
+#include <spa/control/ump-utils.h>
 #include <spa/debug/types.h>
 #include <spa/debug/mem.h>
 #include <spa/debug/log.h>
@@ -62,6 +64,7 @@ struct impl {
 	uint8_t payload;
 	uint32_t ssrc;
 	uint16_t seq;
+	unsigned fixed_ssrc:1;
 	unsigned have_ssrc:1;
 	unsigned ignore_ssrc:1;
 	unsigned have_seq:1;
@@ -69,17 +72,19 @@ struct impl {
 	uint32_t ts_offset;
 	uint32_t psamples;
 	uint32_t mtu;
+	uint32_t header_size;
 	uint32_t payload_size;
 
 	struct spa_ringbuffer ring;
 	uint8_t buffer[BUFFER_SIZE];
+	uint64_t last_recv_timestamp;
 
 	struct spa_io_rate_match *io_rate_match;
 	struct spa_io_position *io_position;
 	struct spa_dll dll;
 	double corr;
 	uint32_t target_buffer;
-	float max_error;
+	double max_error;
 
 	float last_timestamp;
 	float last_time;
@@ -98,6 +103,27 @@ struct impl {
 
 	int (*receive_rtp)(struct impl *impl, uint8_t *buffer, ssize_t len);
 	void (*flush_timeout)(struct impl *impl, uint64_t expirations);
+
+	/*
+	 * pw_filter where the filter would be driven at the PTP clock
+	 * rate with RTP sink being driven at the sink driver clock rate
+	 * or some ALSA clock rate.
+	 */
+	struct pw_filter *ptp_sender;
+	struct spa_hook ptp_sender_listener;
+	struct spa_dll ptp_dll;
+	double ptp_corr;
+	bool separate_sender;
+	bool refilling;
+
+	/* Track some variables we need from the sink driver */
+	uint64_t sink_next_nsec;
+	uint64_t sink_nsec;
+	uint64_t sink_resamp_delay;
+	uint64_t sink_quantum;
+	/* And some bookkeping for the sender processing */
+	uint64_t rtp_base_ts;
+	uint32_t rtp_last_ts;
 };
 
 static int do_emit_state_changed(struct spa_loop *loop, bool async, uint32_t seq, const void *data, size_t size, void *user_data)
@@ -161,7 +187,18 @@ static int stream_start(struct impl *impl)
 
 	rtp_stream_emit_state_changed(impl, true, NULL);
 
+	if (impl->separate_sender) {
+		struct spa_dict_item items[1];
+		items[0] = SPA_DICT_ITEM_INIT(PW_KEY_NODE_ALWAYS_PROCESS, "true");
+
+		pw_filter_set_active(impl->ptp_sender, true);
+		pw_filter_update_properties(impl->ptp_sender, NULL, &SPA_DICT_INIT(items, 1));
+
+		pw_log_info("activated pw_filter for separate sender");
+	}
+
 	impl->started = true;
+
 	return 0;
 }
 
@@ -174,6 +211,16 @@ static int stream_stop(struct impl *impl)
 	if (!impl->timer_running)
 		rtp_stream_emit_state_changed(impl, false, NULL);
 
+	if (impl->separate_sender) {
+		struct spa_dict_item items[1];
+		items[0] = SPA_DICT_ITEM_INIT(PW_KEY_NODE_ALWAYS_PROCESS, "false");
+
+		pw_filter_update_properties(impl->ptp_sender, NULL, &SPA_DICT_INIT(items, 1));
+
+		pw_log_info("deactivating pw_filter for separate sender");
+		pw_filter_set_active(impl->ptp_sender, false);
+	}
+
 	impl->started = false;
 	return 0;
 }
@@ -227,61 +274,18 @@ static const struct format_info *find_audio_format_info(const struct spa_audio_i
 	return NULL;
 }
 
-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));
+	spa_audio_info_raw_init_dict_keys(info,
+			&SPA_DICT_ITEMS(
+				 SPA_DICT_ITEM(SPA_KEY_AUDIO_FORMAT, DEFAULT_FORMAT),
+				 SPA_DICT_ITEM(SPA_KEY_AUDIO_RATE, SPA_STRINGIFY(DEFAULT_RATE)),
+				 SPA_DICT_ITEM(SPA_KEY_AUDIO_POSITION, DEFAULT_POSITION)),
+			&props->dict,
+			SPA_KEY_AUDIO_FORMAT,
+			SPA_KEY_AUDIO_RATE,
+			SPA_KEY_AUDIO_CHANNELS,
+			SPA_KEY_AUDIO_POSITION, NULL);
 }
 
 static uint32_t msec_to_samples(struct impl *impl, float msec)
@@ -304,7 +308,7 @@ struct rtp_stream *rtp_stream_new(struct pw_core *core,
 		const struct rtp_stream_events *events, void *data)
 {
 	struct impl *impl;
-	const char *str;
+	const char *str, *aes67_driver;
 	char tmp[64];
 	uint8_t buffer[1024];
 	struct spa_pod_builder b;
@@ -386,7 +390,7 @@ struct rtp_stream *rtp_stream_new(struct pw_core *core,
 			res = -EINVAL;
 			goto out;
 		}
-		pw_properties_set(props, PW_KEY_FORMAT_DSP, "8 bit raw midi");
+		pw_properties_set(props, PW_KEY_FORMAT_DSP, "32 bit raw UMP");
 		impl->stride = impl->format_info->size;
 		impl->rate = pw_properties_get_uint32(props, "midi.rate", 10000);
 		if (impl->rate == 0)
@@ -433,18 +437,21 @@ struct rtp_stream *rtp_stream_new(struct pw_core *core,
 		impl->ssrc = pw_properties_get_uint32(props, "rtp.sender-ssrc", pw_rand32());
 		impl->ts_offset = pw_properties_get_uint32(props, "rtp.sender-ts-offset", pw_rand32());
 	} else {
-		impl->have_ssrc = pw_properties_fetch_uint32(props, "rtp.receiver-ssrc", &impl->ssrc);
+		impl->have_ssrc = impl->fixed_ssrc = pw_properties_fetch_uint32(props, "rtp.receiver-ssrc", &impl->ssrc);
 		if (pw_properties_fetch_uint32(props, "rtp.receiver-ts-offset", &impl->ts_offset) < 0)
 			impl->direct_timestamp = false;
 	}
 
 	impl->payload = pw_properties_get_uint32(props, "rtp.payload", impl->payload);
 	impl->mtu = pw_properties_get_uint32(props, "net.mtu", DEFAULT_MTU);
-	if (impl->mtu <= PACKET_HEADER_SIZE) {
+	impl->header_size = pw_properties_get_uint32(props, "net.header", IP4_HEADER_SIZE + UDP_HEADER_SIZE);
+	impl->header_size += RTP_HEADER_SIZE;
+
+	if (impl->mtu <= impl->header_size) {
 		pw_log_error("invalid MTU %d, using %d", impl->mtu, DEFAULT_MTU);
 		impl->mtu = DEFAULT_MTU;
 	}
-	impl->payload_size = impl->mtu - PACKET_HEADER_SIZE;
+	impl->payload_size = impl->mtu - impl->header_size;
 
 	impl->seq = pw_rand32();
 
@@ -518,14 +525,21 @@ struct rtp_stream *rtp_stream_new(struct pw_core *core,
 	if (fmodf(impl->target_buffer, impl->psamples) != 0) {
 		pw_log_warn("sess.latency.msec %f should be an integer multiple of rtp.ptime %f",
 				latency_msec, ptime);
-		impl->target_buffer = (uint32_t)((impl->target_buffer / ptime) * impl->psamples);
+		impl->target_buffer = SPA_ROUND_DOWN(impl->target_buffer, impl->psamples);
 	}
 
+	aes67_driver = pw_properties_get(props, "aes67.driver-group");
+
 	pw_properties_setf(props, PW_KEY_NODE_RATE, "1/%d", impl->rate);
-	if (direction == PW_DIRECTION_INPUT) {
+	if (direction == PW_DIRECTION_INPUT && !aes67_driver) {
+		/* While sending, we accept latency-sized buffers, and break it
+		 * up and send in ptime intervals using a timer */
 		pw_properties_setf(props, PW_KEY_NODE_LATENCY, "%d/%d",
 				impl->target_buffer, impl->rate);
 	} else {
+		/* For receive, and with split sending, we break up the latency
+		 * as half being in stream latency, and the rest in our own
+		 * ringbuffer latency */
 		pw_properties_setf(props, PW_KEY_NODE_LATENCY, "%d/%d",
 				impl->target_buffer / 2, impl->rate);
 	}
@@ -534,6 +548,7 @@ struct rtp_stream *rtp_stream_new(struct pw_core *core,
 	pw_properties_setf(props, "rtp.media", "%s", impl->format_info->media_type);
 	pw_properties_setf(props, "rtp.mime", "%s", impl->format_info->mime);
 	pw_properties_setf(props, "rtp.payload", "%u", impl->payload);
+	pw_properties_setf(props, "rtp.ssrc", "%u", impl->ssrc);
 	pw_properties_setf(props, "rtp.rate", "%u", impl->rate);
 	if (impl->info.info.raw.channels > 0)
 		pw_properties_setf(props, "rtp.channels", "%u", impl->info.info.raw.channels);
@@ -564,7 +579,7 @@ struct rtp_stream *rtp_stream_new(struct pw_core *core,
 		params[n_params++] = spa_format_audio_build(&b,
 				SPA_PARAM_EnumFormat, &impl->stream_info);
 		flags |= PW_STREAM_FLAG_AUTOCONNECT;
-		rtp_audio_init(impl, direction);
+		rtp_audio_init(impl, core, direction, aes67_driver);
 		break;
 	case SPA_MEDIA_SUBTYPE_control:
 		params[n_params++] = spa_pod_builder_add_object(&b,
@@ -685,6 +700,12 @@ enum pw_stream_state rtp_stream_get_state(struct rtp_stream *s, const char **err
 
 	return pw_stream_get_state(impl->stream, error);
 }
+int rtp_stream_set_active(struct rtp_stream *s, bool active)
+{
+	struct impl *impl = (struct impl*)s;
+
+	return pw_stream_set_active(impl->stream, active);
+}
 
 int rtp_stream_set_param(struct rtp_stream *s, uint32_t id, const struct spa_pod *param)
 {
diff --git a/src/modules/module-rtp/stream.h b/src/modules/module-rtp/stream.h
index c14afcf5..83cf0da3 100644
--- a/src/modules/module-rtp/stream.h
+++ b/src/modules/module-rtp/stream.h
@@ -19,8 +19,11 @@ struct rtp_stream;
 #define ERROR_MSEC		2.0f
 #define DEFAULT_SESS_LATENCY	100.0f
 
-/* 28 bytes IP/UDP, 12 bytes RTP header */
-#define PACKET_HEADER_SIZE	(12+28)
+#define IP4_HEADER_SIZE		20
+#define IP6_HEADER_SIZE		40
+#define UDP_HEADER_SIZE		8
+/* 12 bytes RTP header */
+#define RTP_HEADER_SIZE		12
 
 #define DEFAULT_MTU		1280
 #define DEFAULT_MIN_PTIME	2.0f
@@ -59,6 +62,7 @@ size_t rtp_stream_get_mtu(struct rtp_stream *s);
 
 void rtp_stream_set_first(struct rtp_stream *s);
 
+int rtp_stream_set_active(struct rtp_stream *s, bool active);
 void rtp_stream_set_error(struct rtp_stream *s, int res, const char *error);
 enum pw_stream_state rtp_stream_get_state(struct rtp_stream *s, const char **error);
 
diff --git a/src/modules/module-snapcast-discover.c b/src/modules/module-snapcast-discover.c
index 5bb2f863..a1140916 100644
--- a/src/modules/module-snapcast-discover.c
+++ b/src/modules/module-snapcast-discover.c
@@ -23,6 +23,7 @@
 #include <spa/utils/string.h>
 #include <spa/utils/json.h>
 #include <spa/param/audio/format.h>
+#include <spa/param/audio/raw-json.h>
 #include <spa/debug/types.h>
 
 #include <pipewire/impl.h>
@@ -65,6 +66,26 @@
  * - `stream.rules` = <rules>: match rules, use create-stream actions. See
  *   \ref page_module_protocol_simple for module properties.
  *
+ * ### stream.rules matches
+ *
+ *  - `snapcast.ip`: the IP address of the snapcast server
+ *  - `snapcast.port`: the port of the snapcast server
+ *  - `snapcast.ifindex`: the interface index where the snapcast announcement
+ *                        was received.
+ *  - `snapcast.ifname`: the interface name where the snapcast announcement
+ *                        was received.
+ *  - `snapcast.name`: the name of the snapcast server
+ *  - `snapcast.hostname`: the hostname of the snapcast server
+ *  - `snapcast.domain`: the domain of the snapcast server
+ *
+ * ### stream.rules create-stream
+ *
+ * In addition to all the properties that can be passed to
+ * \ref page_module_protocol_simple, you can also set:
+ *
+ * - `snapcast.stream-name`: The name of the stream on a snapcast server.
+ * - `node.name`: The name of the sink that is created on the sender.
+ *
  * ## Example configuration
  *
  *\code{.unparsed}
@@ -76,9 +97,9 @@
  *         stream.rules = [
  *             {   matches = [
  *                     {    snapcast.ip = "~.*"
+ *                          #snapcast.port = 1000
  *                          #snapcast.ifindex = 1
  *                          #snapcast.ifname = eth0
- *                          #snapcast.port = 1000
  *                          #snapcast.name = ""
  *                          #snapcast.hostname = ""
  *                          #snapcast.domain = ""
@@ -91,11 +112,18 @@
  *                         #audio.channels = 2
  *                         #audio.position = [ FL FR ]
  *                         #
+ *                         # The stream name as is appears on the snapcast
+ *                         # server:
  *                         #snapcast.stream-name = "PipeWire"
  *                         #
+ *                         # The name of the sink on the sender:
+ *                         #node.name = "Snapcast Sink"
+ *                         #
  *                         #capture = true
+ *                         #server.address = [ "tcp:4711" ]
  *                         #capture.props = {
  *                             #target.object = ""
+ *                             #node.latency = 2048/48000
  *                             #media.class = "Audio/Sink"
  *                         #}
  *                     }
@@ -461,14 +489,13 @@ static int snapcast_connect(struct tunnel *t)
 static int add_snapcast_stream(struct impl *impl, struct tunnel *t,
 		struct pw_properties *props, const char *servers)
 {
-	struct spa_json it[2];
+	struct spa_json it[1];
 	char v[256];
 
-	spa_json_init(&it[0], servers, strlen(servers));
-        if (spa_json_enter_array(&it[0], &it[1]) <= 0)
-                spa_json_init(&it[1], servers, strlen(servers));
+        if (spa_json_begin_array_relax(&it[0], servers, strlen(servers)) <= 0)
+		return -EINVAL;
 
-	while (spa_json_get_string(&it[1], v, sizeof(v)) > 0) {
+	while (spa_json_get_string(&it[0], v, sizeof(v)) > 0) {
 		t->server_address = strdup(v);
 		snapcast_connect(t);
 		return 0;
@@ -476,67 +503,22 @@ static int add_snapcast_stream(struct impl *impl, struct tunnel *t,
 	return -ENOENT;
 }
 
-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 inline 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(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));
-	if (info->format == 0) {
-		str = DEFAULT_FORMAT;
-		info->format = format_from_name(str, strlen(str));
-	}
-	pw_properties_set(props, PW_KEY_AUDIO_FORMAT, str);
-
-	info->rate = pw_properties_get_uint32(props, PW_KEY_AUDIO_RATE, info->rate);
-	if (info->rate == 0)
-		info->rate = DEFAULT_RATE;
+	spa_audio_info_raw_init_dict_keys(info,
+			&SPA_DICT_ITEMS(
+				 SPA_DICT_ITEM(SPA_KEY_AUDIO_FORMAT, DEFAULT_FORMAT),
+				 SPA_DICT_ITEM(SPA_KEY_AUDIO_RATE, SPA_STRINGIFY(DEFAULT_RATE)),
+				 SPA_DICT_ITEM(SPA_KEY_AUDIO_POSITION, DEFAULT_POSITION)),
+			&props->dict,
+			SPA_KEY_AUDIO_FORMAT,
+			SPA_KEY_AUDIO_RATE,
+			SPA_KEY_AUDIO_CHANNELS,
+			SPA_KEY_AUDIO_POSITION, NULL);
+
+	pw_properties_set(props, PW_KEY_AUDIO_FORMAT,
+			spa_type_audio_format_to_short_name(info->format));
 	pw_properties_setf(props, PW_KEY_AUDIO_RATE, "%d", info->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));
 	pw_properties_setf(props, PW_KEY_AUDIO_CHANNELS, "%d", info->channels);
 }
 
diff --git a/src/modules/spa/module-device-factory.c b/src/modules/module-spa-device-factory.c
similarity index 97%
rename from src/modules/spa/module-device-factory.c
rename to src/modules/module-spa-device-factory.c
index 98acc4d6..eb8d2436 100644
--- a/src/modules/spa/module-device-factory.c
+++ b/src/modules/module-spa-device-factory.c
@@ -13,8 +13,12 @@
 
 #include "pipewire/impl.h"
 
-#include "spa-device.h"
+#include "spa/spa-device.h"
 
+/** \page page_module_spa_device_factory SPA Device factory
+ *
+ * Provide a factory to create SPA devices.
+ */
 #define NAME "spa-device-factory"
 
 PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
diff --git a/src/modules/spa/module-device.c b/src/modules/module-spa-device.c
similarity index 95%
rename from src/modules/spa/module-device.c
rename to src/modules/module-spa-device.c
index a5e09ecf..2d62ffb5 100644
--- a/src/modules/spa/module-device.c
+++ b/src/modules/module-spa-device.c
@@ -10,8 +10,12 @@
 
 #include <pipewire/impl.h>
 
-#include "spa-device.h"
+#include "spa/spa-device.h"
 
+/** \page page_module_spa_device SPA Device
+ *
+ * Load and manage an SPA device.
+ */
 #define NAME "spa-device"
 
 PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
diff --git a/src/modules/spa/module-node-factory.c b/src/modules/module-spa-node-factory.c
similarity index 98%
rename from src/modules/spa/module-node-factory.c
rename to src/modules/module-spa-node-factory.c
index dbbc6d0b..0f9a9702 100644
--- a/src/modules/spa/module-node-factory.c
+++ b/src/modules/module-spa-node-factory.c
@@ -13,8 +13,12 @@
 
 #include "pipewire/impl.h"
 
-#include "spa-node.h"
+#include "spa/spa-node.h"
 
+/** \page page_module_spa_node_factory SPA Node factory
+ *
+ * Provide a factory to create SPA nodes.
+ */
 #define NAME "spa-node-factory"
 
 PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
diff --git a/src/modules/spa/module-node.c b/src/modules/module-spa-node.c
similarity index 95%
rename from src/modules/spa/module-node.c
rename to src/modules/module-spa-node.c
index 9844607b..75009bc1 100644
--- a/src/modules/spa/module-node.c
+++ b/src/modules/module-spa-node.c
@@ -12,8 +12,12 @@
 
 #include <pipewire/impl.h>
 
-#include "spa-node.h"
+#include "spa/spa-node.h"
 
+/** \page page_module_spa_node SPA Node
+ *
+ * Load and manage an SPA node.
+ */
 #define NAME "spa-node"
 
 PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
diff --git a/src/modules/module-vban-recv.c b/src/modules/module-vban-recv.c
index 52cf363a..90205796 100644
--- a/src/modules/module-vban-recv.c
+++ b/src/modules/module-vban-recv.c
@@ -30,6 +30,7 @@
 #include <pipewire/impl.h>
 
 #include <module-vban/stream.h>
+#include <module-vban/vban.h>
 #include "network-utils.h"
 
 /** \page page_module_vban_recv VBAN receiver
@@ -37,6 +38,9 @@
  * The `vban-recv` module creates a PipeWire source that receives audio
  * and midi [VBAN](https://vb-audio.com) packets.
  *
+ * The receive will listen on a specific port (6980) and create a stream for each
+ * VBAN stream received on the port.
+ *
  * ## Module Name
  *
  * `libpipewire-module-vban-recv`
@@ -46,22 +50,31 @@
  * Options specific to the behavior of this module
  *
  * - `local.ifname = <str>`: interface name to use
- * - `source.ip = <str>`: the source ip address, default 127.0.0.1
- * - `source.port = <int>`: the source port
+ * - `source.ip = <str>`: the source ip address to listen on, default 127.0.0.1
+ * - `source.port = <int>`: the source port to listen on, default 6980
  * - `node.always-process = <bool>`: true to receive even when not running
  * - `sess.latency.msec = <str>`: target network latency in milliseconds, default 100
- * - `sess.ignore-ssrc = <bool>`: ignore SSRC, default false
- * - `sess.media = <string>`: the media type audio|midi|opus, default audio
- * - `stream.props = {}`: properties to be passed to the stream
+ * - `stream.props = {}`: properties to be passed to all the stream
+ * - `stream.rules` = <rules>: match rules, use create-stream actions.
+ *
+ * ### stream.rules matches
+ *
+ *  - `vban.ip`: the IP address of the VBAN sender
+ *  - `vban.port`: the port of the VBAN sender
+ *  - `sess.name`: the name of the VBAN stream
+ *
+ * ### stream.rules create-stream
+ *
+ * In addition to all the properties that can be passed to a stream,
+ * you can also set:
+ *
+ * - `sess.latency.msec = <str>`: target network latency in milliseconds, default 100
  *
  * ## 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_MEDIA_NAME
  * - \ref PW_KEY_MEDIA_CLASS
@@ -82,17 +95,36 @@
  *         #source.ip = 127.0.0.1
  *         #source.port = 6980
  *         sess.latency.msec = 100
- *         #sess.ignore-ssrc = false
  *         #node.always-process = false
- *         #sess.media = "audio"
- *         #audio.format = "S16LE"
- *         #audio.rate = 44100
- *         #audio.channels = 2
  *         #audio.position = [ FL FR ]
  *         stream.props = {
  *            #media.class = "Audio/Source"
- *            node.name = "vban-receiver"
+ *            #node.name = "vban-receiver"
  *         }
+ *         stream.rules = [
+ *             {   matches = [
+ *                     {    sess.name = "~.*"
+ *                          #sess.media = "audio" | "midi"
+ *                          #vban.ip = ""
+ *                          #vban.port = 1000
+ *                          #audio.channels = 2
+ *                          #audio.format = "U8" | "S16LE" | "S24LE" | "S32LE" | "F32LE" | "F64LE"
+ *                          #audio.rate = 44100
+ *                     }
+ *                 ]
+ *                 actions = {
+ *                     create-stream = {
+ *                         stream.props = {
+ *                             #sess.latency.msec = 100
+ *                             #target.object = ""
+ *                             #audio.position = [ FL FR ]
+ *                             #media.class = "Audio/Source"
+ *                             #node.name = "vban-receiver"
+ *                         }
+ *                     }
+ *                 }
+ *             }
+ *         ]
  *     }
  * }
  * ]
@@ -110,16 +142,16 @@ PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
 #define DEFAULT_SOURCE_IP		"127.0.0.1"
 #define DEFAULT_SOURCE_PORT		6980
 
+#define DEFAULT_CREATE_RULES	\
+        "[ { matches = [ { sess.name = \"~.*\" } ] actions = { create-stream = { } } } ] "
+
 #define USAGE   "( local.ifname=<local interface name to use> ) "						\
 		"( source.ip=<source IP address, default:"DEFAULT_SOURCE_IP"> ) "				\
  		"( source.port=<int, source port, default:"SPA_STRINGIFY(DEFAULT_SOURCE_PORT)"> "		\
 		"( sess.latency.msec=<target network latency, default "SPA_STRINGIFY(DEFAULT_SESS_LATENCY)"> ) "\
- 		"( sess.media=<string, the media type audio|midi, default audio> ) "				\
-		"( 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 ... } ) "
+		"( stream.props= { key=value ... } ) "								\
+		"( stream.rules=<rules>, use create-stream actions )"
 
 static const struct spa_dict_item module_info[] = {
 	{ PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
@@ -146,47 +178,31 @@ struct impl {
 	bool always_process;
 	uint32_t cleanup_interval;
 
-	struct spa_source *timer;
-
 	struct pw_properties *stream_props;
-	struct vban_stream *stream;
+
+	struct spa_source *timer;
 
 	uint16_t src_port;
 	struct sockaddr_storage src_addr;
 	socklen_t src_len;
 	struct spa_source *source;
 
-	unsigned receiving:1;
+	struct spa_list streams;
 };
 
-static void
-on_vban_io(void *data, int fd, uint32_t mask)
-{
-	struct impl *impl = data;
-	ssize_t len;
-	uint8_t buffer[2048];
-
-	if (mask & SPA_IO_IN) {
-		if ((len = recv(fd, buffer, sizeof(buffer), 0)) < 0)
-			goto receive_error;
-
-		if (len < 12)
-			goto short_packet;
+struct stream {
+	struct spa_list link;
+	struct impl *impl;
 
-		if (SPA_LIKELY(impl->stream))
-			vban_stream_receive_packet(impl->stream, buffer, len);
+	struct vban_header header;
+	struct sockaddr_storage sa;
+	socklen_t salen;
 
-		impl->receiving = true;
-	}
-	return;
+	struct vban_stream *stream;
 
-receive_error:
-	pw_log_warn("recv error: %m");
-	return;
-short_packet:
-	pw_log_warn("short packet received");
-	return;
-}
+	bool active;
+	bool receiving;
+};
 
 static int make_socket(const struct sockaddr* sa, socklen_t salen, char *ifname)
 {
@@ -266,7 +282,242 @@ error:
 	return res;
 }
 
-static int stream_start(struct impl *impl)
+static void stream_destroy(void *d)
+{
+	struct stream *s = d;
+	s->stream = NULL;
+}
+
+static void stream_state_changed(void *data, bool started, const char *error)
+{
+	struct stream *s = data;
+	struct impl *impl = s->impl;
+
+	if (error) {
+		pw_log_error("stream error: %s", error);
+		pw_impl_module_schedule_destroy(impl->module);
+	} else if (started) {
+		s->active = true;
+	} else {
+		s->active = false;
+	}
+}
+
+static const struct vban_stream_events stream_events = {
+	VBAN_VERSION_STREAM_EVENTS,
+	.destroy = stream_destroy,
+	.state_changed = stream_state_changed,
+};
+
+static int create_stream(struct stream *s, struct pw_properties *props)
+{
+	struct impl *impl = s->impl;
+	const char *sess_name, *ip, *port;
+
+	ip = pw_properties_get(props, "vban.ip");
+	port = pw_properties_get(props, "vban.port");
+	sess_name = pw_properties_get(props, "sess.name");
+
+	if (pw_properties_get(props, PW_KEY_NODE_NAME) == NULL)
+		pw_properties_setf(props, PW_KEY_NODE_NAME, "vban_session.%s.%s.%s", sess_name, ip, port);
+	if (pw_properties_get(props, PW_KEY_NODE_DESCRIPTION) == NULL)
+		pw_properties_setf(props, PW_KEY_NODE_DESCRIPTION, "%s from %s", sess_name, ip);
+	if (pw_properties_get(props, PW_KEY_MEDIA_NAME) == NULL)
+		pw_properties_setf(props, PW_KEY_MEDIA_NAME, "VBAN %s from %s",
+				sess_name, ip);
+
+	s->stream = vban_stream_new(impl->core,
+			PW_DIRECTION_OUTPUT, spa_steal_ptr(props),
+			&stream_events, s);
+	if (s->stream == NULL) {
+		pw_log_error("can't create stream: %m");
+		return -errno;
+	}
+	return 0;
+}
+
+struct match_info {
+	struct stream *stream;
+	const struct pw_properties *props;
+	bool matched;
+};
+
+static int rule_matched(void *data, const char *location, const char *action,
+                        const char *str, size_t len)
+{
+	struct match_info *i = data;
+	int res = 0;
+
+	i->matched = true;
+	if (spa_streq(action, "create-stream")) {
+		struct pw_properties *p = pw_properties_copy(i->props);
+		pw_properties_update_string(p, str, len);
+		create_stream(i->stream, p);
+	}
+	return res;
+}
+
+
+static int
+do_setup_stream(struct spa_loop *loop,
+	bool async, uint32_t seq, const void *data, size_t size, void *user_data)
+{
+        struct stream *s = user_data;
+	struct impl *impl = s->impl;
+	struct pw_properties *props;
+	int res;
+	const char *str;
+	char addr[128];
+	uint16_t port = 0;
+
+	props = pw_properties_copy(impl->stream_props);
+
+	pw_net_get_ip(&s->sa, addr, sizeof(addr), NULL, &port);
+
+	pw_properties_setf(props, "sess.name", "%s", s->header.stream_name);
+	pw_properties_setf(props, "vban.ip", "%s", addr);
+	pw_properties_setf(props, "vban.port", "%u", port);
+
+	if ((s->header.format_SR & 0xE0) == VBAN_PROTOCOL_AUDIO &&
+	    (s->header.format_bit & 0xF0) == VBAN_CODEC_PCM) {
+		const char *fmt;
+		pw_properties_set(props, "sess.media", "audio");
+		pw_properties_setf(props, PW_KEY_AUDIO_CHANNELS, "%u",s->header.format_nbc + 1);
+		pw_properties_setf(props, PW_KEY_AUDIO_RATE, "%u", vban_SR[s->header.format_SR & 0x1f]);
+		switch(s->header.format_bit & 0x07) {
+		case VBAN_DATATYPE_BYTE8:
+			fmt = "U8";
+			break;
+		case VBAN_DATATYPE_INT16:
+			fmt = "S16LE";
+			break;
+		case VBAN_DATATYPE_INT24:
+			fmt = "S24LE";
+			break;
+		case VBAN_DATATYPE_INT32:
+			fmt = "S32LE";
+			break;
+		case VBAN_DATATYPE_FLOAT32:
+			fmt = "F32LE";
+			break;
+		case VBAN_DATATYPE_FLOAT64:
+			fmt = "F64LE";
+			break;
+			break;
+		case VBAN_DATATYPE_12BITS:
+		case VBAN_DATATYPE_10BITS:
+		default:
+			pw_log_error("stream format %08x:%08x not supported",
+					s->header.format_SR, s->header.format_bit);
+			res = -ENOTSUP;
+			goto error;
+		}
+		pw_properties_set(props, PW_KEY_AUDIO_FORMAT, fmt);
+	} else if ((s->header.format_SR & 0xE0) == VBAN_PROTOCOL_SERIAL &&
+	    (s->header.format_bit & 0xF0) == VBAN_SERIAL_MIDI) {
+		pw_properties_set(props, "sess.media", "midi");
+	} else {
+		pw_log_error("stream format %08x:%08x not supported",
+				s->header.format_SR, s->header.format_bit);
+		res = -ENOTSUP;
+		goto error;
+	}
+
+	if ((str = pw_properties_get(impl->props, "stream.rules")) == NULL)
+		str = DEFAULT_CREATE_RULES;
+	if (str != NULL) {
+		struct match_info minfo = {
+			.stream = s,
+			.props = props,
+		};
+		pw_conf_match_rules(str, strlen(str), NAME, &props->dict,
+				rule_matched, &minfo);
+
+		if (!minfo.matched)
+			pw_log_info("unmatched stream found %s", str);
+	}
+	res = 0;
+error:
+	pw_properties_free(props);
+	return res;
+}
+
+static struct stream *make_stream(struct impl *impl, const struct vban_header *hdr,
+		struct sockaddr_storage *sa, socklen_t salen)
+{
+	struct stream *stream;
+
+	stream = calloc(1, sizeof(*stream));
+	if (stream == NULL)
+		return NULL;
+
+	stream->impl = impl;
+	stream->header = *hdr;
+	stream->sa = *sa;
+	stream->salen = salen;
+	spa_list_append(&impl->streams, &stream->link);
+
+	pw_loop_invoke(impl->loop, do_setup_stream, 1, NULL, 0, false, stream);
+
+	return stream;
+}
+
+static struct stream *find_stream(struct impl *impl, const char *name)
+{
+	struct stream *s;
+	spa_list_for_each(s, &impl->streams, link) {
+		if (strncmp(s->header.stream_name, name, VBAN_STREAM_NAME_SIZE) == 0)
+			return s;
+	}
+	return NULL;
+}
+
+static void
+on_vban_io(void *data, int fd, uint32_t mask)
+{
+	struct impl *impl = data;
+	ssize_t len;
+	uint8_t buffer[2048];
+
+	if (mask & SPA_IO_IN) {
+		struct vban_header *hdr;
+		struct stream *s;
+		struct sockaddr_storage sa;
+		socklen_t salen = sizeof(sa);
+
+		if ((len = recvfrom(fd, buffer, sizeof(buffer), 0,
+						(struct sockaddr*)&sa, &salen)) < 0)
+			goto receive_error;
+
+		if (len < VBAN_HEADER_SIZE)
+			goto short_packet;
+
+		hdr = (struct vban_header *)buffer;
+		if (strncmp(hdr->vban, "VBAN", 4))
+			goto invalid_version;
+
+		s = find_stream(impl, hdr->stream_name);
+		if (SPA_UNLIKELY(s == NULL))
+			s = make_stream(impl, hdr, &sa, salen);
+		if (SPA_LIKELY(s != NULL && s->active)) {
+			s->receiving = true;
+			vban_stream_receive_packet(s->stream, buffer, len);
+		}
+	}
+	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 VBAN version");
+	return;
+}
+
+static int listen_start(struct impl *impl)
 {
 	int fd;
 
@@ -291,7 +542,7 @@ static int stream_start(struct impl *impl)
 	return 0;
 }
 
-static void stream_stop(struct impl *impl)
+static void listen_stop(struct impl *impl)
 {
 	if (!impl->source)
 		return;
@@ -302,45 +553,29 @@ static void stream_stop(struct impl *impl)
 	impl->source = NULL;
 }
 
-static void stream_destroy(void *d)
-{
-	struct impl *impl = d;
-	impl->stream = NULL;
-}
 
-static void stream_state_changed(void *data, bool started, const char *error)
+static void destroy_stream(struct stream *s)
 {
-	struct impl *impl = data;
-
-	if (error) {
-		pw_log_error("stream error: %s", error);
-		pw_impl_module_schedule_destroy(impl->module);
-	} else if (started) {
-		if ((errno = -stream_start(impl)) < 0)
-			pw_log_error("failed to start VBAN stream: %m");
-	} else {
-		if (!impl->always_process)
-			stream_stop(impl);
-	}
+	spa_list_remove(&s->link);
+	if (s->stream)
+		vban_stream_destroy(s->stream);
+	free(s);
 }
 
-static const struct vban_stream_events stream_events = {
-	VBAN_VERSION_STREAM_EVENTS,
-	.destroy = stream_destroy,
-	.state_changed = stream_state_changed,
-};
-
 static void on_timer_event(void *data, uint64_t expirations)
 {
 	struct impl *impl = data;
+	struct stream *s;
 
-	if (!impl->receiving) {
-		pw_log_info("timeout, inactive VBAN source");
-		//pw_impl_module_schedule_destroy(impl->module);
-	} else {
-		pw_log_debug("timeout, keeping active VBAN source");
+	spa_list_for_each(s, &impl->streams, link) {
+		if (!s->receiving) {
+			pw_log_info("timeout, inactive VBAN source");
+			//pw_impl_module_schedule_destroy(impl->module);
+		} else {
+			pw_log_debug("timeout, keeping active VBAN source");
+		}
+		s->receiving = false;
 	}
-	impl->receiving = false;
 }
 
 static void core_destroy(void *d)
@@ -357,10 +592,12 @@ static const struct pw_proxy_events core_proxy_events = {
 
 static void impl_destroy(struct impl *impl)
 {
-	if (impl->stream)
-		vban_stream_destroy(impl->stream);
-	if (impl->source)
-		pw_loop_destroy_source(impl->data_loop, impl->source);
+	struct stream *s;
+
+	listen_stop(impl);
+
+	spa_list_consume(s, &impl->streams, link)
+		destroy_stream(s);
 
 	if (impl->core && impl->do_disconnect)
 		pw_core_disconnect(impl->core);
@@ -420,7 +657,7 @@ 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;
-	const char *str, *sess_name;
+	const char *str;
 	struct timespec value, interval;
 	struct pw_properties *props, *stream_props;
 	int res = 0;
@@ -446,26 +683,14 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
 	impl->context = context;
 	impl->loop = pw_context_get_main_loop(context);
 	impl->data_loop = pw_context_acquire_loop(context, &props->dict);
-
-	if ((sess_name = pw_properties_get(props, "sess.name")) == NULL)
-		sess_name = pw_get_host_name();
+	spa_list_init(&impl->streams);
 
 	pw_properties_set(props, PW_KEY_NODE_LOOP_NAME, impl->data_loop->name);
-	if (pw_properties_get(props, PW_KEY_NODE_NAME) == NULL)
-		pw_properties_setf(props, PW_KEY_NODE_NAME, "vban_session.%s", sess_name);
-	if (pw_properties_get(props, PW_KEY_NODE_DESCRIPTION) == NULL)
-		pw_properties_setf(props, PW_KEY_NODE_DESCRIPTION, "%s", sess_name);
-	if (pw_properties_get(props, PW_KEY_MEDIA_NAME) == NULL)
-		pw_properties_setf(props, PW_KEY_MEDIA_NAME, "VBAN Session with %s",
-				sess_name);
 
 	if ((str = pw_properties_get(props, "stream.props")) != NULL)
 		pw_properties_update_string(stream_props, str, strlen(str));
 
 	copy_props(impl, props, PW_KEY_NODE_LOOP_NAME);
-	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);
@@ -476,10 +701,6 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
 	copy_props(impl, props, PW_KEY_MEDIA_NAME);
 	copy_props(impl, props, PW_KEY_MEDIA_CLASS);
 	copy_props(impl, props, "net.mtu");
-	copy_props(impl, props, "sess.media");
-	copy_props(impl, props, "sess.name");
-	copy_props(impl, props, "sess.min-ptime");
-	copy_props(impl, props, "sess.max-ptime");
 	copy_props(impl, props, "sess.latency.msec");
 
 	str = pw_properties_get(props, "local.ifname");
@@ -538,12 +759,8 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
 	interval.tv_nsec = 0;
 	pw_loop_update_timer(impl->loop, impl->timer, &value, &interval, false);
 
-	impl->stream = vban_stream_new(impl->core,
-			PW_DIRECTION_OUTPUT, pw_properties_copy(stream_props),
-			&stream_events, impl);
-	if (impl->stream == NULL) {
-		res = -errno;
-		pw_log_error("can't create stream: %m");
+	if ((res = listen_start(impl)) < 0) {
+		pw_log_error("failed to start VBAN stream: %s", spa_strerror(res));
 		goto out;
 	}
 
diff --git a/src/modules/module-vban-send.c b/src/modules/module-vban-send.c
index b0861e9c..5fc6793a 100644
--- a/src/modules/module-vban-send.c
+++ b/src/modules/module-vban-send.c
@@ -26,6 +26,7 @@
 #include <pipewire/impl.h>
 
 #include <module-vban/stream.h>
+#include <module-vban/vban.h>
 #include "network-utils.h"
 
 #ifndef IPTOS_DSCP
@@ -354,6 +355,7 @@ SPA_EXPORT
 int pipewire__module_init(struct pw_impl_module *module, const char *args)
 {
 	struct pw_context *context = pw_impl_module_get_context(module);
+	uint32_t id = pw_global_get_id(pw_impl_module_get_global(module));
 	struct impl *impl;
 	struct pw_properties *props = NULL, *stream_props = NULL;
 	char addr[64];
@@ -382,7 +384,7 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
 	stream_props = pw_properties_new(NULL, NULL);
 	if (stream_props == NULL) {
 		res = -errno;
-		pw_log_error( "can't create properties: %m");
+		pw_log_error("can't create properties: %m");
 		goto out;
 	}
 	impl->stream_props = stream_props;
@@ -391,15 +393,22 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
 	impl->context = context;
 	impl->loop = pw_context_get_main_loop(context);
 
+	if ((sess_name = pw_properties_get(props, "sess.name")) == NULL)
+		pw_properties_setf(props, "sess.name", "%s-%d",
+				pw_get_host_name(), id);
 	if ((sess_name = pw_properties_get(props, "sess.name")) == NULL)
 		sess_name = pw_get_host_name();
 
+	if (strlen(sess_name) > VBAN_STREAM_NAME_SIZE)
+		pw_log_warn("session name '%s' will be truncated to %d characters",
+				sess_name, VBAN_STREAM_NAME_SIZE);
+
 	if (pw_properties_get(props, PW_KEY_NODE_NAME) == NULL)
 		pw_properties_setf(props, PW_KEY_NODE_NAME, "vban_session.%s", sess_name);
 	if (pw_properties_get(props, PW_KEY_NODE_DESCRIPTION) == NULL)
 		pw_properties_setf(props, PW_KEY_NODE_DESCRIPTION, "%s", sess_name);
 	if (pw_properties_get(props, PW_KEY_MEDIA_NAME) == NULL)
-		pw_properties_setf(props, PW_KEY_MEDIA_NAME, "VBAN Session with %s",
+		pw_properties_setf(props, PW_KEY_MEDIA_NAME, "VBAN Session %s",
 				sess_name);
 
 	if ((str = pw_properties_get(props, "stream.props")) != NULL)
diff --git a/src/modules/module-vban/audio.c b/src/modules/module-vban/audio.c
index 4aa0fae9..09924689 100644
--- a/src/modules/module-vban/audio.c
+++ b/src/modules/module-vban/audio.c
@@ -30,14 +30,14 @@ static void vban_audio_process_playback(void *data)
 		memset(d[0].data, 0, wanted * stride);
 		if (impl->have_sync) {
 			impl->have_sync = false;
-			level = SPA_LOG_LEVEL_WARN;
+			level = SPA_LOG_LEVEL_INFO;
 		} else {
 			level = SPA_LOG_LEVEL_DEBUG;
 		}
 		pw_log(level, "underrun %d/%u < %u",
 					avail, target_buffer, wanted);
 	} else {
-		float error, corr;
+		double error, corr;
 		if (impl->first) {
 			if ((uint32_t)avail > target_buffer) {
 				uint32_t skip = avail - target_buffer;
@@ -54,19 +54,16 @@ static void vban_audio_process_playback(void *data)
 		}
 		/* try to adjust our playback rate to keep the
 		 * requested target_buffer bytes in the ringbuffer */
-		error = (float)target_buffer - (float)avail;
-		error = SPA_CLAMP(error, -impl->max_error, impl->max_error);
+		error = (double)target_buffer - (double)avail;
+		error = SPA_CLAMPD(error, -impl->max_error, impl->max_error);
 
-		corr = (float)spa_dll_update(&impl->dll, error);
+		corr = spa_dll_update(&impl->dll, error);
 
 		pw_log_debug("avail:%u target:%u error:%f corr:%f", avail,
 				target_buffer, error, corr);
 
-		if (impl->io_rate_match) {
-			SPA_FLAG_SET(impl->io_rate_match->flags,
-					SPA_IO_RATE_MATCH_FLAG_ACTIVE);
-			impl->io_rate_match->rate = 1.0f / corr;
-		}
+		pw_stream_set_rate(impl->stream, 1.0 / corr);
+
 		spa_ringbuffer_read_data(&impl->ring,
 				impl->buffer,
 				BUFFER_SIZE,
@@ -92,12 +89,7 @@ static int vban_audio_receive(struct impl *impl, uint8_t *buffer, ssize_t len)
 	uint32_t stride = impl->stride;
 	int32_t filled;
 
-	if (len < VBAN_HEADER_SIZE)
-		goto short_packet;
-
 	hdr = (struct vban_header*)buffer;
-	if (strncmp(hdr->vban, "VBAN", 3))
-		goto invalid_version;
 
 	impl->receiving = true;
 
@@ -155,14 +147,6 @@ static int vban_audio_receive(struct impl *impl, uint8_t *buffer, ssize_t len)
 		spa_ringbuffer_write_update(&impl->ring, write);
 	}
 	return 0;
-
-short_packet:
-	pw_log_warn("short packet received");
-	return -EINVAL;
-invalid_version:
-	pw_log_warn("invalid VBAN version");
-	spa_debug_log_mem(pw_log_get(), SPA_LOG_LEVEL_INFO, 0, buffer, len);
-	return -EPROTO;
 }
 
 static inline void
diff --git a/src/modules/module-vban/midi.c b/src/modules/module-vban/midi.c
index 7a688299..f820b683 100644
--- a/src/modules/module-vban/midi.c
+++ b/src/modules/module-vban/midi.c
@@ -67,7 +67,7 @@ static void vban_midi_process_playback(void *data)
 			} else {
 				timestamp = target;
 			}
-			spa_pod_builder_control(&b, target - timestamp, SPA_CONTROL_Midi);
+			spa_pod_builder_control(&b, target - timestamp, c->type);
 			spa_pod_builder_bytes(&b,
 					SPA_POD_BODY(&c->value),
 					SPA_POD_BODY_SIZE(&c->value));
@@ -162,19 +162,29 @@ static int vban_midi_receive_midi(struct impl *impl, uint8_t *packet,
 
 	while (offs < plen) {
 		int size;
-
-		spa_pod_builder_control(&b, timestamp, SPA_CONTROL_Midi);
+		uint8_t *midi_data;
+		size_t midi_size;
+		uint64_t midi_state = 0;
 
 		size = get_midi_size(&packet[offs], plen - offs);
-
 		if (size <= 0 || offs + size > plen) {
 			pw_log_warn("invalid size (%08x) %d (%u %u)",
 					packet[offs], size, offs, plen);
 			break;
 		}
 
-		spa_pod_builder_bytes(&b, &packet[offs], size);
-
+		midi_data = &packet[offs];
+		midi_size = size;
+		while (midi_size > 0) {
+			uint32_t ump[4];
+			int ump_size = spa_ump_from_midi(&midi_data, &midi_size,
+					ump, sizeof(ump), 0, &midi_state);
+			if (ump_size <= 0)
+				break;
+
+			spa_pod_builder_control(&b, timestamp, SPA_CONTROL_UMP);
+	                spa_pod_builder_bytes(&b, ump, ump_size);
+		}
 		offs += size;
 	}
 	spa_pod_builder_pop(&b, &f[0]);
@@ -191,13 +201,7 @@ static int vban_midi_receive(struct impl *impl, uint8_t *buffer, ssize_t len)
 	ssize_t hlen;
 	uint32_t n_frames;
 
-	if (len < VBAN_HEADER_SIZE)
-		goto short_packet;
-
 	hdr = (struct vban_header*)buffer;
-	if (strncmp(hdr->vban, "VBAN", 3))
-		goto invalid_version;
-
 	hlen = VBAN_HEADER_SIZE;
 
 	n_frames = hdr->n_frames;
@@ -211,14 +215,6 @@ static int vban_midi_receive(struct impl *impl, uint8_t *buffer, ssize_t len)
 	impl->receiving = true;
 
 	return vban_midi_receive_midi(impl, buffer, hlen, len);
-
-short_packet:
-	pw_log_warn("short packet received");
-	return -EINVAL;
-invalid_version:
-	pw_log_warn("invalid RTP version");
-	spa_debug_log_mem(pw_log_get(), SPA_LOG_LEVEL_INFO, 0, buffer, len);
-	return -EPROTO;
 }
 
 static void vban_midi_flush_packets(struct impl *impl,
@@ -239,14 +235,17 @@ static void vban_midi_flush_packets(struct impl *impl,
 	len = 0;
 
 	SPA_POD_SEQUENCE_FOREACH(sequence, c) {
-		void *ev;
-		uint32_t size;
+		int size;
+		uint8_t event[16];
+
+		if (c->type != SPA_CONTROL_UMP)
+			continue;
 
-		if (c->type != SPA_CONTROL_Midi)
+		size = spa_ump_to_midi(SPA_POD_BODY(&c->value),
+				SPA_POD_BODY_SIZE(&c->value), event, sizeof(event));
+		if (size <= 0)
 			continue;
 
-		ev = SPA_POD_BODY(&c->value),
-                size = SPA_POD_BODY_SIZE(&c->value);
 		if (len == 0) {
 			/* start new packet */
 			header.n_frames++;
@@ -258,7 +257,7 @@ static void vban_midi_flush_packets(struct impl *impl,
 			vban_stream_emit_send_packet(impl, iov, 2);
 			len = 0;
 		}
-		memcpy(&impl->buffer[len], ev, size);
+		memcpy(&impl->buffer[len], event, size);
 		len += size;
 	}
 	if (len > 0) {
diff --git a/src/modules/module-vban/stream.c b/src/modules/module-vban/stream.c
index ed32f3bd..efa5af37 100644
--- a/src/modules/module-vban/stream.c
+++ b/src/modules/module-vban/stream.c
@@ -10,7 +10,10 @@
 #include <spa/utils/ringbuffer.h>
 #include <spa/utils/dll.h>
 #include <spa/param/audio/format-utils.h>
+#include <spa/param/audio/layout.h>
+#include <spa/param/audio/raw-json.h>
 #include <spa/control/control.h>
+#include <spa/control/ump-utils.h>
 #include <spa/debug/types.h>
 #include <spa/debug/mem.h>
 #include <spa/debug/log.h>
@@ -62,12 +65,11 @@ struct impl {
 	struct spa_ringbuffer ring;
 	uint8_t buffer[BUFFER_SIZE];
 
-	struct spa_io_rate_match *io_rate_match;
 	struct spa_io_position *io_position;
 	struct spa_dll dll;
 	double corr;
 	uint32_t target_buffer;
-	float max_error;
+	double max_error;
 
 	float last_timestamp;
 	float last_time;
@@ -92,22 +94,19 @@ struct format_info {
 };
 
 static const struct format_info audio_format_info[] = {
-	{ SPA_MEDIA_SUBTYPE_raw, SPA_AUDIO_FORMAT_U8, 1, VBAN_DATATYPE_U8, },
+	{ SPA_MEDIA_SUBTYPE_raw, SPA_AUDIO_FORMAT_U8, 1, VBAN_DATATYPE_BYTE8, },
 	{ SPA_MEDIA_SUBTYPE_raw, SPA_AUDIO_FORMAT_S16_LE, 2, VBAN_DATATYPE_INT16, },
 	{ SPA_MEDIA_SUBTYPE_raw, SPA_AUDIO_FORMAT_S24_LE, 3, VBAN_DATATYPE_INT24, },
 	{ SPA_MEDIA_SUBTYPE_raw, SPA_AUDIO_FORMAT_S32_LE, 4, VBAN_DATATYPE_INT32, },
 	{ SPA_MEDIA_SUBTYPE_raw, SPA_AUDIO_FORMAT_F32_LE, 4, VBAN_DATATYPE_FLOAT32, },
 	{ SPA_MEDIA_SUBTYPE_raw, SPA_AUDIO_FORMAT_F64_LE, 8, VBAN_DATATYPE_FLOAT64, },
-	{ SPA_MEDIA_SUBTYPE_control, 0, 1, VBAN_SERIAL_MIDI | VBAN_DATATYPE_U8, },
+	{ SPA_MEDIA_SUBTYPE_control, 0, 1, VBAN_SERIAL_MIDI | VBAN_DATATYPE_BYTE8, },
 };
 
 static void stream_io_changed(void *data, uint32_t id, void *area, uint32_t size)
 {
 	struct impl *impl = data;
 	switch (id) {
-	case SPA_IO_RateMatch:
-		impl->io_rate_match = area;
-		break;
 	case SPA_IO_Position:
 		impl->io_position = area;
 		break;
@@ -186,66 +185,47 @@ static const struct format_info *find_audio_format_info(const struct spa_audio_i
 	return NULL;
 }
 
-static inline uint32_t format_from_name(const char *name, size_t len)
+static void parse_audio_info(const struct pw_properties *props, struct spa_audio_info_raw *info)
 {
-	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;
+	spa_audio_info_raw_init_dict_keys(info,
+			&SPA_DICT_ITEMS(
+				 SPA_DICT_ITEM(SPA_KEY_AUDIO_FORMAT, DEFAULT_FORMAT),
+				 SPA_DICT_ITEM(SPA_KEY_AUDIO_RATE, SPA_STRINGIFY(DEFAULT_RATE)),
+				 SPA_DICT_ITEM(SPA_KEY_AUDIO_POSITION, DEFAULT_POSITION)),
+			&props->dict,
+			SPA_KEY_AUDIO_FORMAT,
+			SPA_KEY_AUDIO_RATE,
+			SPA_KEY_AUDIO_CHANNELS,
+			SPA_KEY_AUDIO_POSITION, NULL);
 }
 
-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)
+static uint32_t msec_to_samples(struct impl *impl, uint32_t msec)
 {
-	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);
-	}
+	return msec * impl->rate / 1000;
 }
 
-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 const struct spa_audio_layout_info layouts[] = {
+	{ SPA_AUDIO_LAYOUT_Mono },
+	{ SPA_AUDIO_LAYOUT_Stereo },
+	{ SPA_AUDIO_LAYOUT_2_1 },
+	{ SPA_AUDIO_LAYOUT_3_1 },
+	{ SPA_AUDIO_LAYOUT_5_0 },
+	{ SPA_AUDIO_LAYOUT_5_1 },
+	{ SPA_AUDIO_LAYOUT_7_0 },
+	{ SPA_AUDIO_LAYOUT_7_1 },
+};
 
-static uint32_t msec_to_samples(struct impl *impl, uint32_t msec)
+static void default_layout(uint32_t channels, uint32_t *position)
 {
-	return msec * impl->rate / 1000;
+	SPA_FOR_EACH_ELEMENT_VAR(layouts, l) {
+		if (l->n_channels == channels) {
+			for (uint32_t i = 0; i < l->n_channels; i++)
+				position[i] = l->position[i];
+			return;
+		}
+	}
+	for (uint32_t i = 0; i < channels; i++)
+		position[i] = SPA_AUDIO_CHANNEL_AUX0 + i;
 }
 
 struct vban_stream *vban_stream_new(struct pw_core *core,
@@ -295,6 +275,8 @@ struct vban_stream *vban_stream_new(struct pw_core *core,
 	switch (impl->info.media_subtype) {
 	case SPA_MEDIA_SUBTYPE_raw:
 		parse_audio_info(props, &impl->info.info.raw);
+		if (SPA_FLAG_IS_SET(impl->info.info.raw.flags, SPA_AUDIO_FLAG_UNPOSITIONED))
+			default_layout(impl->info.info.raw.channels, impl->info.info.raw.position);
 		impl->stream_info = impl->info;
 		impl->format_info = find_audio_format_info(&impl->info);
 		if (impl->format_info == NULL) {
@@ -325,13 +307,13 @@ struct vban_stream *vban_stream_new(struct pw_core *core,
 			res = -EINVAL;
 			goto out;
 		}
-		pw_properties_set(props, PW_KEY_FORMAT_DSP, "8 bit raw midi");
+		pw_properties_set(props, PW_KEY_FORMAT_DSP, "32 bit raw UMP");
 		impl->stride = impl->format_info->size;
 		impl->rate = pw_properties_get_uint32(props, "midi.rate", 10000);
 		if (impl->rate == 0)
 			impl->rate = 10000;
 
-		impl->header.format_SR = (0x1 << 5) | 14; /* 115200 */
+		impl->header.format_SR = VBAN_PROTOCOL_SERIAL | VBAN_BPS_115200;
 		impl->header.format_nbs = 0;
 		impl->header.format_nbc = 0;
 		impl->header.format_bit = impl->format_info->format_bit;
diff --git a/src/modules/module-vban/vban.h b/src/modules/module-vban/vban.h
index bf129920..efcfaf40 100644
--- a/src/modules/module-vban/vban.h
+++ b/src/modules/module-vban/vban.h
@@ -5,6 +5,10 @@
 #ifndef PIPEWIRE_VBAN_H
 #define PIPEWIRE_VBAN_H
 
+#include <stdint.h>
+
+#include <spa/utils/defs.h>
+
 #ifdef __cplusplus
 extern "C" {
 #endif
@@ -17,21 +21,30 @@ extern "C" {
 #define VBAN_SAMPLES_MAX_NB	256
 
 struct vban_header {
-	char vban[4];			/* contains 'V' 'B', 'A', 'N' */
-	uint8_t format_SR;			/* SR index */
-	uint8_t format_nbs;			/* nb sample per frame (1 to 256) */
-	uint8_t format_nbc;			/* nb channel (1 to 256) */
-	uint8_t format_bit;			/* bit format */
+	char vban[4];					/* contains 'V' 'B', 'A', 'N' */
+	uint8_t format_SR;				/* SR index */
+	uint8_t format_nbs;				/* nb sample per frame (1 to 256) */
+	uint8_t format_nbc;				/* nb channel (1 to 256) */
+	uint8_t format_bit;				/* bit format */
 	char stream_name[VBAN_STREAM_NAME_SIZE];	/* stream name */
-	uint32_t n_frames;			/* growing frame number. */
+	uint32_t n_frames;				/* growing frame number. */
 } __attribute__ ((packed));
 
+#define VBAN_PROTOCOL_AUDIO		0x00
+#define VBAN_PROTOCOL_SERIAL		0x20
+#define VBAN_PROTOCOL_TXT		0x40
+#define VBAN_PROTOCOL_SERVICE		0x60
+#define VBAN_PROTOCOL_UNDEFINED_1	0x80
+#define VBAN_PROTOCOL_UNDEFINED_2	0xA0
+#define VBAN_PROTOCOL_UNDEFINED_3	0xC0
+#define VBAN_PROTOCOL_USER		0xE0
+
 #define VBAN_SR_MAXNUMBER	21
 
-static uint32_t const vban_SR[VBAN_SR_MAXNUMBER] = {
-    6000, 12000, 24000, 48000, 96000, 192000, 384000,
-    8000, 16000, 32000, 64000, 128000, 256000, 512000,
-    11025, 22050, 44100, 88200, 176400, 352800, 705600
+static uint32_t const vban_SR[32] = {
+	6000, 12000, 24000, 48000, 96000, 192000, 384000,
+	8000, 16000, 32000, 64000, 128000, 256000, 512000,
+	11025, 22050, 44100, 88200, 176400, 352800, 705600
 };
 
 static inline uint8_t vban_sr_index(uint32_t rate)
@@ -44,7 +57,84 @@ static inline uint8_t vban_sr_index(uint32_t rate)
 	return VBAN_SR_MAXNUMBER;
 }
 
-#define VBAN_DATATYPE_U8	0x00
+#define VBAN_CODEC_PCM		0x00
+#define VBAN_CODEC_VBCA		0x10 //VB-AUDIO AOIP CODEC
+#define VBAN_CODEC_VBCV		0x20 //VB-AUDIO VOIP CODEC
+#define VBAN_CODEC_UNDEFINED_1	0x30
+#define VBAN_CODEC_UNDEFINED_2	0x40
+#define VBAN_CODEC_UNDEFINED_3	0x50
+#define VBAN_CODEC_UNDEFINED_4	0x60
+#define VBAN_CODEC_UNDEFINED_5	0x70
+#define VBAN_CODEC_UNDEFINED_6	0x80
+#define VBAN_CODEC_UNDEFINED_7	0x90
+#define VBAN_CODEC_UNDEFINED_8	0xA0
+#define VBAN_CODEC_UNDEFINED_9	0xB0
+#define VBAN_CODEC_UNDEFINED_10	0xC0
+#define VBAN_CODEC_UNDEFINED_11	0xD0
+#define VBAN_CODEC_UNDEFINED_12	0xE0
+#define VBAN_CODEC_USER		0xF0
+
+#define VBAN_BPS_0		0
+#define VBAN_BPS_110		1
+#define VBAN_BPS_150		2
+#define VBAN_BPS_300		3
+#define VBAN_BPS_600		4
+#define VBAN_BPS_1200		5
+#define VBAN_BPS_2400		6
+#define VBAN_BPS_4800		7
+#define VBAN_BPS_9600		8
+#define VBAN_BPS_14400		9
+#define VBAN_BPS_19200		10
+#define VBAN_BPS_31250		11
+#define VBAN_BPS_38400		12
+#define VBAN_BPS_57600		13
+#define VBAN_BPS_115200		14
+#define VBAN_BPS_128000		15
+#define VBAN_BPS_230400		16
+#define VBAN_BPS_250000		17
+#define VBAN_BPS_256000		18
+#define VBAN_BPS_460800		19
+#define VBAN_BPS_921600		20
+#define VBAN_BPS_1000000	21
+#define VBAN_BPS_1500000	22
+#define VBAN_BPS_2000000	23
+#define VBAN_BPS_3000000	24
+#define VBAN_BPS_MAXNUMBER	25
+
+static const int vban_BPSList[] = {
+	[VBAN_BPS_0] = 0,
+	[VBAN_BPS_110] = 110,
+	[VBAN_BPS_150] = 150,
+	[VBAN_BPS_300] = 300,
+	[VBAN_BPS_600] = 600,
+	[VBAN_BPS_1200] = 1200,
+	[VBAN_BPS_2400] = 2400,
+	[VBAN_BPS_4800] = 4800,
+	[VBAN_BPS_9600] = 9600,
+	[VBAN_BPS_14400] = 14400,
+	[VBAN_BPS_19200] = 19200,
+	[VBAN_BPS_31250] = 31250,
+	[VBAN_BPS_38400] = 38400,
+	[VBAN_BPS_57600] = 57600,
+	[VBAN_BPS_115200] = 115200,
+	[VBAN_BPS_128000] = 128000,
+	[VBAN_BPS_230400] = 230400,
+	[VBAN_BPS_250000] = 250000,
+	[VBAN_BPS_256000] = 256000,
+	[VBAN_BPS_460800] = 460800,
+	[VBAN_BPS_921600] = 921600,
+	[VBAN_BPS_1000000] = 1000000,
+	[VBAN_BPS_1500000] = 1500000,
+	[VBAN_BPS_2000000] = 2000000,
+	[VBAN_BPS_3000000] = 3000000,
+};
+SPA_STATIC_ASSERT(SPA_N_ELEMENTS(vban_BPSList) == VBAN_BPS_MAXNUMBER);
+
+#define VBAN_SERIAL_GENERIC	0x00
+#define VBAN_SERIAL_MIDI	0x10
+#define VBAN_SERIAL_USER	0xf0
+
+#define VBAN_DATATYPE_BYTE8	0x00
 #define VBAN_DATATYPE_INT16	0x01
 #define VBAN_DATATYPE_INT24	0x02
 #define VBAN_DATATYPE_INT32	0x03
@@ -53,10 +143,6 @@ static inline uint8_t vban_sr_index(uint32_t rate)
 #define VBAN_DATATYPE_12BITS	0x06
 #define VBAN_DATATYPE_10BITS	0x07
 
-#define VBAN_SERIAL_GENERIC	0x00
-#define VBAN_SERIAL_MIDI	0x10
-#define VBAN_SERIAL_USER	0xf0
-
 #ifdef __cplusplus
 }
 #endif
diff --git a/src/modules/network-utils.h b/src/modules/network-utils.h
index 25354f4c..16f9d927 100644
--- a/src/modules/network-utils.h
+++ b/src/modules/network-utils.h
@@ -82,31 +82,33 @@ static inline int pw_net_parse_address_port(const char *address,
 
 static inline int pw_net_get_ip(const struct sockaddr_storage *sa, char *ip, size_t len, bool *ip4, uint16_t *port)
 {
+	if (ip4)
+		*ip4 = sa->ss_family == AF_INET;
+
 	if (sa->ss_family == AF_INET) {
 		struct sockaddr_in *in = (struct sockaddr_in*)sa;
-		inet_ntop(sa->ss_family, &in->sin_addr, ip, len);
+		if (inet_ntop(sa->ss_family, &in->sin_addr, ip, len) == NULL)
+			return -errno;
 		if (port)
 			*port = ntohs(in->sin_port);
 	} 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);
+		if (inet_ntop(sa->ss_family, &in->sin6_addr, ip, len) == NULL)
+			return -errno;
 		if (port)
 			*port = ntohs(in->sin6_port);
-		if (in->sin6_scope_id == 0 || len <= 1)
-			goto finish;
-
-		size_t curlen = strlen(ip);
-		if (len-(curlen+1) >= IFNAMSIZ) {
-			ip += curlen+1;
-			ip[-1] = '%';
-			if (if_indextoname(in->sin6_scope_id, ip) == NULL)
-				ip[-1] = 0;
+
+		if (in->sin6_scope_id != 0 && len > 1) {
+			size_t curlen = strlen(ip);
+			if (len-(curlen+1) >= IFNAMSIZ) {
+				ip += curlen+1;
+				ip[-1] = '%';
+				if (if_indextoname(in->sin6_scope_id, ip) == NULL)
+					ip[-1] = 0;
+			}
 		}
 	} else
 		return -EINVAL;
-finish:
-	if (ip4)
-		*ip4 = sa->ss_family == AF_INET;
 	return 0;
 }
 
diff --git a/src/modules/spa/meson.build b/src/modules/spa/meson.build
deleted file mode 100644
index 8332910b..00000000
--- a/src/modules/spa/meson.build
+++ /dev/null
@@ -1,31 +0,0 @@
-pipewire_module_spa_node = shared_library('pipewire-module-spa-node',
-  [ 'module-node.c', 'spa-node.c' ],
-  include_directories : [configinc],
-  install : true,
-  install_dir : modules_install_dir,
-  dependencies : [spa_dep, mathlib, dl_lib, pipewire_dep],
-)
-
-pipewire_module_spa_device = shared_library('pipewire-module-spa-device',
-  [ 'module-device.c', 'spa-device.c' ],
-  include_directories : [configinc],
-  install : true,
-  install_dir : modules_install_dir,
-  dependencies : [spa_dep, mathlib, dl_lib, pipewire_dep],
-)
-
-pipewire_module_spa_node_factory = shared_library('pipewire-module-spa-node-factory',
-  [ 'module-node-factory.c', 'spa-node.c' ],
-  include_directories : [configinc],
-  install : true,
-  install_dir : modules_install_dir,
-  dependencies : [spa_dep, mathlib, dl_lib, pipewire_dep],
-)
-
-pipewire_module_spa_device_factory = shared_library('pipewire-module-spa-device-factory',
-  [ 'module-device-factory.c', 'spa-device.c' ],
-  include_directories : [configinc],
-  install : true,
-  install_dir : modules_install_dir,
-  dependencies : [spa_dep, mathlib, dl_lib, pipewire_dep],
-)
diff --git a/src/modules/spa/spa-node.c b/src/modules/spa/spa-node.c
index a7205895..c9681290 100644
--- a/src/modules/spa/spa-node.c
+++ b/src/modules/spa/spa-node.c
@@ -133,84 +133,6 @@ void *pw_spa_node_get_user_data(struct pw_impl_node *node)
 	return impl->user_data;
 }
 
-static int
-setup_props(struct pw_context *context, struct spa_node *spa_node, struct pw_properties *pw_props)
-{
-	int res;
-	struct spa_pod *props;
-	void *state = NULL;
-	const char *key;
-	uint32_t index = 0;
-	uint8_t buf[4096];
-	struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buf, sizeof(buf));
-	const struct spa_pod_prop *prop = NULL;
-
-	res = spa_node_enum_params_sync(spa_node,
-					SPA_PARAM_Props, &index, NULL, &props,
-					&b);
-	if (res != 1) {
-		if (res < 0)
-			pw_log_debug("spa_node_get_props result: %s", spa_strerror(res));
-		if (res == -ENOTSUP || res == -ENOENT)
-			res = 0;
-		return res;
-	}
-
-	while ((key = pw_properties_iterate(pw_props, &state))) {
-		uint32_t type = 0;
-
-		type = spa_debug_type_find_type(spa_type_props, key);
-		if (type == SPA_TYPE_None)
-			continue;
-
-		if ((prop = spa_pod_find_prop(props, prop, type))) {
-			const char *value = pw_properties_get(pw_props, key);
-
-			if (value == NULL)
-				continue;
-
-			pw_log_debug("configure prop %s to %s", key, value);
-
-			switch(prop->value.type) {
-			case SPA_TYPE_Bool:
-				SPA_POD_VALUE(struct spa_pod_bool, &prop->value) =
-					pw_properties_parse_bool(value);
-				break;
-			case SPA_TYPE_Id:
-				SPA_POD_VALUE(struct spa_pod_id, &prop->value) =
-					pw_properties_parse_int(value);
-				break;
-			case SPA_TYPE_Int:
-				SPA_POD_VALUE(struct spa_pod_int, &prop->value) =
-					pw_properties_parse_int(value);
-				break;
-			case SPA_TYPE_Long:
-				SPA_POD_VALUE(struct spa_pod_long, &prop->value) =
-					pw_properties_parse_int64(value);
-				break;
-			case SPA_TYPE_Float:
-				SPA_POD_VALUE(struct spa_pod_float, &prop->value) =
-					pw_properties_parse_float(value);
-				break;
-			case SPA_TYPE_Double:
-				SPA_POD_VALUE(struct spa_pod_double, &prop->value) =
-					pw_properties_parse_double(value);
-				break;
-			case SPA_TYPE_String:
-				break;
-			default:
-				break;
-			}
-		}
-	}
-
-	if ((res = spa_node_set_param(spa_node, SPA_PARAM_Props, 0, props)) < 0) {
-		pw_log_debug("spa_node_set_props failed: %s", spa_strerror(res));
-		return res;
-	}
-	return 0;
-}
-
 struct match {
 	struct pw_properties *props;
 	int count;
@@ -280,9 +202,6 @@ struct pw_impl_node *pw_spa_node_load(struct pw_context *context,
 
 	spa_node = iface;
 
-	if ((res = setup_props(context, spa_node, properties)) < 0)
-		pw_log_warn("can't setup properties: %s", spa_strerror(res));
-
 	this = pw_spa_node_new(context, flags,
 			       spa_node, handle, spa_steal_ptr(properties), user_data_size);
 	if (this == NULL) {
diff --git a/src/pipewire/array.h b/src/pipewire/array.h
index cbded821..ca00e7b9 100644
--- a/src/pipewire/array.h
+++ b/src/pipewire/array.h
@@ -13,6 +13,11 @@ extern "C" {
 
 #include <spa/utils/defs.h>
 
+#ifndef PW_API_ARRAY
+#define PW_API_ARRAY static inline
+#endif
+
+
 /** \defgroup pw_array Array
  *
  * \brief An array object
@@ -71,7 +76,7 @@ struct pw_array {
 
 /** Initialize the array with given extend. Extend needs to be > 0 or else
  * the array will not be able to expand. */
-static inline void pw_array_init(struct pw_array *arr, size_t extend)
+PW_API_ARRAY void pw_array_init(struct pw_array *arr, size_t extend)
 {
 	arr->data = NULL;
 	arr->size = arr->alloc = 0;
@@ -79,7 +84,7 @@ static inline void pw_array_init(struct pw_array *arr, size_t extend)
 }
 
 /** Clear the array. This should be called when pw_array_init() was called.  */
-static inline void pw_array_clear(struct pw_array *arr)
+PW_API_ARRAY void pw_array_clear(struct pw_array *arr)
 {
 	if (arr->extend > 0)
 		free(arr->data);
@@ -87,7 +92,7 @@ static inline void pw_array_clear(struct pw_array *arr)
 }
 
 /** Initialize a static array. */
-static inline void pw_array_init_static(struct pw_array *arr, void *data, size_t size)
+PW_API_ARRAY void pw_array_init_static(struct pw_array *arr, void *data, size_t size)
 {
 	arr->data = data;
 	arr->alloc = size;
@@ -95,13 +100,13 @@ static inline void pw_array_init_static(struct pw_array *arr, void *data, size_t
 }
 
 /** Reset the array */
-static inline void pw_array_reset(struct pw_array *arr)
+PW_API_ARRAY void pw_array_reset(struct pw_array *arr)
 {
 	arr->size = 0;
 }
 
 /** Make sure \a size bytes can be added to the array */
-static inline int pw_array_ensure_size(struct pw_array *arr, size_t size)
+PW_API_ARRAY int pw_array_ensure_size(struct pw_array *arr, size_t size)
 {
 	size_t alloc, need;
 
@@ -124,7 +129,7 @@ static inline int pw_array_ensure_size(struct pw_array *arr, size_t size)
 /** Add \a ref size bytes to \a arr. A pointer to memory that can
  * hold at least \a size bytes is returned or NULL when an error occurred
  * and errno will be set.*/
-static inline void *pw_array_add(struct pw_array *arr, size_t size)
+PW_API_ARRAY void *pw_array_add(struct pw_array *arr, size_t size)
 {
 	void *p;
 
@@ -139,7 +144,7 @@ static inline void *pw_array_add(struct pw_array *arr, size_t size)
 
 /** Add a pointer to array. Returns 0 on success and a negative errno style
  * error on failure. */
-static inline int pw_array_add_ptr(struct pw_array *arr, void *ptr)
+PW_API_ARRAY int pw_array_add_ptr(struct pw_array *arr, void *ptr)
 {
 	void **p = (void **)pw_array_add(arr, sizeof(void*));
 	if (p == NULL)
diff --git a/src/pipewire/client.h b/src/pipewire/client.h
index cd119643..7798e212 100644
--- a/src/pipewire/client.h
+++ b/src/pipewire/client.h
@@ -12,6 +12,7 @@ extern "C" {
 #include <spa/utils/defs.h>
 #include <spa/param/param.h>
 
+#include <pipewire/type.h>
 #include <pipewire/proxy.h>
 #include <pipewire/permission.h>
 
@@ -30,6 +31,10 @@ extern "C" {
 #define PW_VERSION_CLIENT		3
 struct pw_client;
 
+#ifndef PW_API_CLIENT_IMPL
+#define PW_API_CLIENT_IMPL static inline
+#endif
+
 /* default ID of the current client after connect */
 #define PW_ID_CLIENT			1
 
@@ -150,20 +155,45 @@ struct pw_client_methods {
 			const struct pw_permission *permissions);
 };
 
-#define pw_client_method(o,method,version,...)				\
-({									\
-	int _res = -ENOTSUP;						\
-	spa_interface_call_res((struct spa_interface*)o,		\
-			struct pw_client_methods, _res,			\
-			method, version, ##__VA_ARGS__);		\
-	_res;								\
-})
-
-#define pw_client_add_listener(c,...)		pw_client_method(c,add_listener,0,__VA_ARGS__)
-#define pw_client_error(c,...)			pw_client_method(c,error,0,__VA_ARGS__)
-#define pw_client_update_properties(c,...)	pw_client_method(c,update_properties,0,__VA_ARGS__)
-#define pw_client_get_permissions(c,...)	pw_client_method(c,get_permissions,0,__VA_ARGS__)
-#define pw_client_update_permissions(c,...)	pw_client_method(c,update_permissions,0,__VA_ARGS__)
+/** \copydoc pw_client_methods.add_listener
+ * \sa pw_client_methods.add_listener */
+PW_API_CLIENT_IMPL int pw_client_add_listener(struct pw_client *object,
+			struct spa_hook *listener,
+			const struct pw_client_events *events,
+			void *data)
+{
+	return spa_api_method_r(int, -ENOTSUP, pw_client, (struct spa_interface*)object, add_listener, 0,
+			listener, events, data);
+}
+/** \copydoc pw_client_methods.error
+ * \sa pw_client_methods.error */
+PW_API_CLIENT_IMPL int pw_client_error(struct pw_client *object, uint32_t id, int res, const char *message)
+{
+	return spa_api_method_r(int, -ENOTSUP, pw_client, (struct spa_interface*)object, error, 0,
+			id, res, message);
+}
+/** \copydoc pw_client_methods.update_properties
+ * \sa pw_client_methods.update_properties */
+PW_API_CLIENT_IMPL int pw_client_update_properties(struct pw_client *object, const struct spa_dict *props)
+{
+	return spa_api_method_r(int, -ENOTSUP, pw_client, (struct spa_interface*)object, update_properties, 0,
+			props);
+}
+/** \copydoc pw_client_methods.get_permissions
+ * \sa pw_client_methods.get_permissions */
+PW_API_CLIENT_IMPL int pw_client_get_permissions(struct pw_client *object, uint32_t index, uint32_t num)
+{
+	return spa_api_method_r(int, -ENOTSUP, pw_client, (struct spa_interface*)object, get_permissions, 0,
+			index, num);
+}
+/** \copydoc pw_client_methods.update_permissions
+ * \sa pw_client_methods.update_permissions */
+PW_API_CLIENT_IMPL int pw_client_update_permissions(struct pw_client *object, uint32_t n_permissions,
+			const struct pw_permission *permissions)
+{
+	return spa_api_method_r(int, -ENOTSUP, pw_client, (struct spa_interface*)object, update_permissions, 0,
+			n_permissions, permissions);
+}
 
 /**
  * \}
diff --git a/src/pipewire/conf.c b/src/pipewire/conf.c
index ee8e24a1..387a3ab9 100644
--- a/src/pipewire/conf.c
+++ b/src/pipewire/conf.c
@@ -565,19 +565,18 @@ static int parse_spa_libs(void *user_data, const char *location,
 {
 	struct data *d = user_data;
 	struct pw_context *context = d->context;
-	struct spa_json it[2];
+	struct spa_json it[1];
 	char key[512], value[512];
 	int res;
 
-	spa_json_init(&it[0], str, len);
-	if (spa_json_enter_object(&it[0], &it[1]) < 0) {
+	if (spa_json_begin_object(&it[0], str, len) < 0) {
 		pw_log_error("config file error: context.spa-libs is not an "
 				"object in '%.*s'", (int)len, str);
 		return -EINVAL;
 	}
 
-	while (spa_json_get_string(&it[1], key, sizeof(key)) > 0) {
-		if (spa_json_get_string(&it[1], value, sizeof(value)) > 0) {
+	while (spa_json_get_string(&it[0], key, sizeof(key)) > 0) {
+		if (spa_json_get_string(&it[0], value, sizeof(value)) > 0) {
 			if ((res = pw_context_add_spa_lib(context, key, value)) < 0) {
 				pw_log_error("error adding spa-libs for '%s' in '%.*s': %s",
 					key, (int)len, str, spa_strerror(res));
@@ -629,7 +628,8 @@ static int load_module(struct pw_context *context, const char *key, const char *
  *  "!null" -> same as !null
  *  !"null" and "!\"null\"" matches anything that is not the string "null"
  */
-static bool find_match(struct spa_json *arr, const struct spa_dict *props, bool condition)
+SPA_EXPORT
+bool pw_conf_find_match(struct spa_json *arr, const struct spa_dict *props, bool condition)
 {
 	struct spa_json it[1];
 	const char *as = arr->cur;
@@ -641,16 +641,10 @@ static bool find_match(struct spa_json *arr, const struct spa_dict *props, bool
 		int match = 0, fail = 0;
 		int len;
 
-		while (spa_json_get_string(&it[0], key, sizeof(key)) > 0) {
+		while ((len = spa_json_object_next(&it[0], key, sizeof(key), &value)) > 0) {
 			bool success = false, is_null, reg = false, parse_string = true;
 			int skip = 0;
 
-			if ((len = spa_json_next(&it[0], &value)) <= 0) {
-				pw_log_warn("malformed match rule: key '%s' has "
-						"no value in '%.*s'", key, az, as);
-				break;
-			}
-
 			/* first decode a string, when there was a string, we assume it
 			 * can not be null but the "null" string, unless there is a modifier,
 			 * see below. */
@@ -752,44 +746,36 @@ static int parse_modules(void *user_data, const char *location,
 {
 	struct data *d = user_data;
 	struct pw_context *context = d->context;
-	struct spa_json it[4];
+	struct spa_json it[3];
 	char key[512];
 	int res = 0, r;
 
 	spa_autofree char *s = strndup(str, len);
-	spa_json_init(&it[0], s, len);
-	if (spa_json_enter_array(&it[0], &it[1]) < 0) {
+	if (spa_json_begin_array(&it[0], s, len) < 0) {
 		pw_log_error("context.modules is not an array in '%.*s'",
 				(int)len, str);
 		return -EINVAL;
 	}
 
-	while ((r = spa_json_enter_object(&it[1], &it[2])) > 0) {
+	while ((r = spa_json_enter_object(&it[0], &it[1])) > 0) {
 		char *name = NULL, *args = NULL, *flags = NULL;
 		bool have_match = true;
+		const char *val;
+		int l;
 
-		while (spa_json_get_string(&it[2], key, sizeof(key)) > 0) {
-			const char *val;
-			int l;
-
-			if ((l = spa_json_next(&it[2], &val)) <= 0) {
-				pw_log_warn("malformed module: key '%s' has no "
-						"value in '%.*s'", key, (int)len, str);
-				break;
-			}
-
+		while ((l = spa_json_object_next(&it[1], key, sizeof(key), &val)) > 0) {
 			if (spa_streq(key, "name")) {
 				name = (char*)val;
 				spa_json_parse_stringn(val, l, name, l+1);
 			} else if (spa_streq(key, "args")) {
 				if (spa_json_is_container(val, l))
-					l = spa_json_container_len(&it[2], val, l);
+					l = spa_json_container_len(&it[1], val, l);
 
 				args = (char*)val;
 				spa_json_parse_stringn(val, l, args, l+1);
 			} else if (spa_streq(key, "flags")) {
 				if (spa_json_is_container(val, l))
-					l = spa_json_container_len(&it[2], val, l);
+					l = spa_json_container_len(&it[1], val, l);
 				flags = (char*)val;
 				spa_json_parse_stringn(val, l, flags, l+1);
 			} else if (spa_streq(key, "condition")) {
@@ -798,8 +784,8 @@ static int parse_modules(void *user_data, const char *location,
 							(int)len, str);
 					break;
 				}
-				spa_json_enter(&it[2], &it[3]);
-				have_match = find_match(&it[3], &context->properties->dict, true);
+				spa_json_enter(&it[1], &it[2]);
+				have_match = pw_conf_find_match(&it[2], &context->properties->dict, true);
 			} else {
 				pw_log_warn("unknown module key '%s' in '%.*s'", key,
 						(int)len, str);
@@ -862,43 +848,35 @@ static int parse_objects(void *user_data, const char *location,
 {
 	struct data *d = user_data;
 	struct pw_context *context = d->context;
-	struct spa_json it[4];
+	struct spa_json it[3];
 	char key[512];
 	int res = 0, r;
 
 	spa_autofree char *s = strndup(str, len);
-	spa_json_init(&it[0], s, len);
-	if (spa_json_enter_array(&it[0], &it[1]) < 0) {
+	if (spa_json_begin_array(&it[0], s, len) < 0) {
 		pw_log_error("config file error: context.objects is not an array");
 		return -EINVAL;
 	}
 
-	while ((r = spa_json_enter_object(&it[1], &it[2])) > 0) {
+	while ((r = spa_json_enter_object(&it[0], &it[1])) > 0) {
 		char *factory = NULL, *args = NULL, *flags = NULL;
 		bool have_match = true;
+		const char *val;
+		int l;
 
-		while (spa_json_get_string(&it[2], key, sizeof(key)) > 0) {
-			const char *val;
-			int l;
-
-			if ((l = spa_json_next(&it[2], &val)) <= 0) {
-				pw_log_warn("malformed object: key '%s' has no "
-						"value in '%.*s'", key, (int)len, str);
-				break;
-			}
-
+		while ((l = spa_json_object_next(&it[1], key, sizeof(key), &val)) > 0) {
 			if (spa_streq(key, "factory")) {
 				factory = (char*)val;
 				spa_json_parse_stringn(val, l, factory, l+1);
 			} else if (spa_streq(key, "args")) {
 				if (spa_json_is_container(val, l))
-					l = spa_json_container_len(&it[2], val, l);
+					l = spa_json_container_len(&it[1], val, l);
 
 				args = (char*)val;
 				spa_json_parse_stringn(val, l, args, l+1);
 			} else if (spa_streq(key, "flags")) {
 				if (spa_json_is_container(val, l))
-					l = spa_json_container_len(&it[2], val, l);
+					l = spa_json_container_len(&it[1], val, l);
 
 				flags = (char*)val;
 				spa_json_parse_stringn(val, l, flags, l+1);
@@ -908,8 +886,8 @@ static int parse_objects(void *user_data, const char *location,
 							(int)len, str);
 					break;
 				}
-				spa_json_enter(&it[2], &it[3]);
-				have_match = find_match(&it[3], &context->properties->dict, true);
+				spa_json_enter(&it[1], &it[2]);
+				have_match = pw_conf_find_match(&it[2], &context->properties->dict, true);
 			} else {
 				pw_log_warn("unknown object key '%s' in '%.*s'", key,
 						(int)len, str);
@@ -1046,40 +1024,30 @@ static int parse_exec(void *user_data, const char *location,
 {
 	struct data *d = user_data;
 	struct pw_context *context = d->context;
-	struct spa_json it[4];
+	struct spa_json it[3];
 	char key[512];
 	int r, res = 0;
 
 	spa_autofree char *s = strndup(str, len);
-	spa_json_init(&it[0], s, len);
-	if (spa_json_enter_array(&it[0], &it[1]) < 0) {
+	if (spa_json_begin_array(&it[0], s, len) < 0) {
 		pw_log_error("config file error: context.exec is not an array in '%.*s'",
 				(int)len, str);
 		return -EINVAL;
 	}
 
-	while ((r = spa_json_enter_object(&it[1], &it[2])) > 0) {
+	while ((r = spa_json_enter_object(&it[0], &it[1])) > 0) {
 		char *path = NULL;
-		const char *args_val = "[]";
-		int args_len = 2;
+		const char *args_val = "[]", *val;
+		int args_len = 2, l;
 		bool have_match = true;
 
-		while (spa_json_get_string(&it[2], key, sizeof(key)) > 0) {
-			const char *val;
-			int l;
-
-			if ((l = spa_json_next(&it[2], &val)) <= 0) {
-				pw_log_warn("malformed exec: key '%s' has no "
-						"value in '%.*s'", key, (int)len, str);
-				break;
-			}
-
+		while ((l = spa_json_object_next(&it[1], key, sizeof(key), &val)) > 0) {
 			if (spa_streq(key, "path")) {
 				path = (char*)val;
 				spa_json_parse_stringn(val, l, path, l+1);
 			} else if (spa_streq(key, "args")) {
 				if (spa_json_is_container(val, l))
-					l = spa_json_container_len(&it[2], val, l);
+					l = spa_json_container_len(&it[1], val, l);
 				args_val = val;
 				args_len = l;
 			} else if (spa_streq(key, "condition")) {
@@ -1088,8 +1056,8 @@ static int parse_exec(void *user_data, const char *location,
 							(int)len, str);
 					goto next;
 				}
-				spa_json_enter(&it[2], &it[3]);
-				have_match = find_match(&it[3], &context->properties->dict, true);
+				spa_json_enter(&it[1], &it[2]);
+				have_match = pw_conf_find_match(&it[2], &context->properties->dict, true);
 			} else {
 				pw_log_warn("unknown exec key '%s' in '%.*s'", key,
 						(int)len, str);
@@ -1232,6 +1200,10 @@ int pw_conf_load_conf_for_context(struct pw_properties *props, struct pw_propert
 	conf_name = getenv("PIPEWIRE_CONFIG_NAME");
 	if ((res = try_load_conf(conf_prefix, conf_name, conf)) < 0) {
 		conf_name = pw_properties_get(props, PW_KEY_CONFIG_NAME);
+		if (spa_streq(conf_name, "client-rt.conf")) {
+			pw_log_warn("setting config.name to client-rt.conf is deprecated, using client.conf");
+			conf_name = NULL;
+		}
 		if (conf_name == NULL)
 			conf_name = "client.conf";
 		else if (!valid_conf_name(conf_name)) {
@@ -1307,43 +1279,41 @@ int pw_conf_match_rules(const char *str, size_t len, const char *location,
 		void *data)
 {
 	const char *val;
-	struct spa_json it[4], actions;
+	struct spa_json it[3], actions;
 	int r;
 
-	spa_json_init(&it[0], str, len);
-	if (spa_json_enter_array(&it[0], &it[1]) < 0) {
+	if (spa_json_begin_array(&it[0], str, len) < 0) {
 		pw_log_warn("expect array of match rules in: '%.*s'", (int)len, str);
 		return 0;
 	}
 
-	while ((r = spa_json_enter_object(&it[1], &it[2])) > 0) {
+	while ((r = spa_json_enter_object(&it[0], &it[1])) > 0) {
 		char key[64];
 		bool have_match = false, have_actions = false;
+		int res, l;
 
-		while (spa_json_get_string(&it[2], key, sizeof(key)) > 0) {
+		while ((l = spa_json_object_next(&it[1], key, sizeof(key), &val)) > 0) {
 			if (spa_streq(key, "matches")) {
-				if (spa_json_enter_array(&it[2], &it[3]) < 0) {
+				if (!spa_json_is_array(val, l)) {
 					pw_log_warn("expected array as matches in '%.*s'",
 							(int)len, str);
 					break;
 				}
 
-				have_match = find_match(&it[3], props, false);
+				spa_json_enter(&it[1], &it[2]);
+				have_match = pw_conf_find_match(&it[2], props, false);
 			}
 			else if (spa_streq(key, "actions")) {
-				if (spa_json_enter_object(&it[2], &actions) > 0)
-					have_actions = true;
-				else
+				if (!spa_json_is_object(val, l)) {
 					pw_log_warn("expected object as match actions in '%.*s'",
 							(int)len, str);
+				} else {
+					have_actions = true;
+					spa_json_enter(&it[1], &actions);
+				}
 			}
 			else {
 				pw_log_warn("unknown match key '%s'", key);
-				if (spa_json_next(&it[2], &val) <= 0) {
-					pw_log_warn("malformed match rule: key '%s' has "
-							"no value in '%.*s'", key, (int)len, str);
-					break;
-				}
 			}
 		}
 		if (!have_match)
@@ -1353,16 +1323,9 @@ int pw_conf_match_rules(const char *str, size_t len, const char *location,
 			continue;
 		}
 
-		while (spa_json_get_string(&actions, key, sizeof(key)) > 0) {
-			int res, len;
+		while ((len = spa_json_object_next(&actions, key, sizeof(key), &val)) > 0) {
 			pw_log_debug("action %s", key);
 
-			if ((len = spa_json_next(&actions, &val)) <= 0) {
-				pw_log_warn("malformed action: key '%s' has no value in '%.*s'",
-						key, (int)len, str);
-				break;
-			}
-
 			if (spa_json_is_container(val, len))
 				len = spa_json_container_len(&actions, val, len);
 
diff --git a/src/pipewire/conf.h b/src/pipewire/conf.h
index 66898b1f..783c1356 100644
--- a/src/pipewire/conf.h
+++ b/src/pipewire/conf.h
@@ -5,6 +5,8 @@
 #ifndef PIPEWIRE_CONF_H
 #define PIPEWIRE_CONF_H
 
+#include <spa/utils/json-core.h>
+
 #include <pipewire/context.h>
 
 /** \defgroup pw_conf Configuration
@@ -21,6 +23,8 @@ int pw_conf_load_conf(const char *prefix, const char *name, struct pw_properties
 int pw_conf_load_state(const char *prefix, const char *name, struct pw_properties *conf);
 int pw_conf_save_state(const char *prefix, const char *name, const struct pw_properties *conf);
 
+bool pw_conf_find_match(struct spa_json *arr, const struct spa_dict *props, bool condition);
+
 int pw_conf_section_update_props(const struct spa_dict *conf,
 		const char *section, struct pw_properties *props);
 
diff --git a/src/pipewire/context.c b/src/pipewire/context.c
index e30a2ce7..e81de912 100644
--- a/src/pipewire/context.c
+++ b/src/pipewire/context.c
@@ -49,7 +49,7 @@ struct data_loop {
 	struct pw_data_loop *impl;
 	bool autostart;
 	bool started;
-	int ref;
+	uint64_t last_used;
 };
 
 /** \cond */
@@ -204,20 +204,21 @@ static int setup_data_loops(struct impl *impl)
 	lib_name = pw_properties_get(this->properties, "context.data-loop." PW_KEY_LIBRARY_NAME_SYSTEM);
 
 	if ((str = pw_properties_get(this->properties, "context.data-loops")) != NULL) {
-		struct spa_json it[4];
+		struct spa_json it[2];
 		char key[512];
 		int r, len = strlen(str);
 		spa_autofree char *s = strndup(str, len);
 
 		i = 0;
-		spa_json_init(&it[0], s, len);
-		if (spa_json_enter_array(&it[0], &it[1]) < 0) {
+		if (spa_json_begin_array(&it[0], s, len) < 0) {
 			pw_log_error("context.data-loops is not an array in '%s'", str);
 			res = -EINVAL;
 			goto exit;
 		}
-		while ((r = spa_json_enter_object(&it[1], &it[2])) > 0) {
+		while ((r = spa_json_enter_object(&it[0], &it[1])) > 0) {
 			char *props = NULL;
+			const char *val;
+			int l;
 
 			if (i >= MAX_LOOPS) {
 				pw_log_warn("too many context.data-loops, using first %d",
@@ -229,17 +230,9 @@ static int setup_data_loops(struct impl *impl)
 			pw_properties_update(pr, &this->properties->dict);
 			pw_properties_set(pr, PW_KEY_LIBRARY_NAME_SYSTEM, lib_name);
 
-			while (spa_json_get_string(&it[2], key, sizeof(key)) > 0) {
-				const char *val;
-				int l;
-
-				if ((l = spa_json_next(&it[2], &val)) <= 0) {
-					pw_log_warn("malformed data-loop: key '%s' has no "
-							"value in '%.*s'", key, (int)len, str);
-					break;
-				}
+			while ((l = spa_json_object_next(&it[1], key, sizeof(key), &val)) > 0) {
 				if (spa_json_is_container(val, l))
-					l = spa_json_container_len(&it[2], val, l);
+					l = spa_json_container_len(&it[1], val, l);
 
 				props = (char*)val;
 				spa_json_parse_stringn(val, l, props, l+1);
@@ -679,12 +672,12 @@ static struct pw_data_loop *acquire_data_loop(struct impl *impl, const char *nam
 			}
 		}
 
-		pw_log_debug("%d: name:'%s' class:'%s' score:%d ref:%d", i,
-				ln, l->impl->class, score, l->ref);
+		pw_log_debug("%d: name:'%s' class:'%s' score:%d last_used:%"PRIu64, i,
+				ln, l->impl->class, score, l->last_used);
 
 		if ((best_loop == NULL) ||
 		    (score > best_score) ||
-		    (score == best_score && l->ref < best_loop->ref)) {
+		    (score == best_score && l->last_used < best_loop->last_used)) {
 			best_loop = l;
 			best_score = score;
 		}
@@ -692,15 +685,15 @@ static struct pw_data_loop *acquire_data_loop(struct impl *impl, const char *nam
 	if (best_loop == NULL)
 		return NULL;
 
-	best_loop->ref++;
+	best_loop->last_used = get_time_ns(impl->this.main_loop->system);
 	if ((res = data_loop_start(impl, best_loop)) < 0) {
 		errno = -res;
 		return NULL;
 	}
 
-	pw_log_info("%p: using name:'%s' class:'%s' ref:%d", impl,
+	pw_log_info("%p: using name:'%s' class:'%s' last_used:%"PRIu64, impl,
 			best_loop->impl->loop->name,
-			best_loop->impl->class, best_loop->ref);
+			best_loop->impl->class, best_loop->last_used);
 
 	return best_loop->impl;
 }
@@ -744,9 +737,8 @@ void pw_context_release_loop(struct pw_context *context, struct pw_loop *loop)
 	for (i = 0; i < impl->n_data_loops; i++) {
 		struct data_loop *l = &impl->data_loops[i];
 		if (l->impl->loop == loop) {
-			l->ref--;
-			pw_log_info("release name:'%s' class:'%s' ref:%d", l->impl->loop->name,
-					l->impl->class, l->ref);
+			pw_log_info("release name:'%s' class:'%s' last_used:%"PRIu64,
+					l->impl->loop->name, l->impl->class, l->last_used);
 			return;
 		}
 	}
@@ -982,7 +974,15 @@ int pw_context_find_format(struct pw_context *context,
 			if (res == -ENOENT || res == 0) {
 				pw_log_debug("%p: no input format filter, using output format: %s",
 						context, spa_strerror(res));
-				*format = filter;
+
+				uint32_t offset = builder->state.offset;
+				res = spa_pod_builder_raw_padded(builder, filter, SPA_POD_SIZE(filter));
+				if (res < 0) {
+					*error = spa_aprintf("failed to add pod");
+					goto error;
+				}
+
+				*format = spa_pod_builder_deref(builder, offset);
 			} else {
 				*error = spa_aprintf("error input enum formats: %s", spa_strerror(res));
 				goto error;
@@ -1011,7 +1011,15 @@ int pw_context_find_format(struct pw_context *context,
 			if (res == -ENOENT || res == 0) {
 				pw_log_debug("%p: no output format filter, using input format: %s",
 						context, spa_strerror(res));
-				*format = filter;
+
+				uint32_t offset = builder->state.offset;
+				res = spa_pod_builder_raw_padded(builder, filter, SPA_POD_SIZE(filter));
+				if (res < 0) {
+					*error = spa_aprintf("failed to add pod");
+					goto error;
+				}
+
+				*format = spa_pod_builder_deref(builder, offset);
 			} else {
 				*error = spa_aprintf("error output enum formats: %s", spa_strerror(res));
 				goto error;
@@ -1511,8 +1519,8 @@ int pw_context_recalc_graph(struct pw_context *context, const char *reason)
 	struct pw_impl_node *n, *s, *target, *fallback;
 	const uint32_t *rates;
 	uint32_t max_quantum, min_quantum, def_quantum, rate_quantum, floor_quantum, ceil_quantum;
-	uint32_t n_rates, def_rate;
-	bool freewheel, global_force_rate, global_force_quantum, transport_start;
+	uint32_t n_rates, def_rate, transport;
+	bool freewheel, global_force_rate, global_force_quantum;
 	struct spa_list collect;
 
 	pw_log_info("%p: busy:%d reason:%s", context, impl->recalc, reason);
@@ -1525,7 +1533,6 @@ int pw_context_recalc_graph(struct pw_context *context, const char *reason)
 again:
 	impl->recalc = true;
 	freewheel = false;
-	transport_start = false;
 
 	/* clean up the flags first */
 	spa_list_for_each(n, &context->node_list, link) {
@@ -1758,10 +1765,16 @@ again:
 			/* Here we are allowed to change the rate of the driver.
 			 * Start with the default rate. If the desired rate is
 			 * allowed, switch to it */
-			target_rate = node_def_rate;
 			if (rate.denom != 0 && rate.num == 1)
-				target_rate = find_best_rate(node_rates, node_n_rates,
-						rate.denom, target_rate);
+				target_rate = rate.denom;
+			else
+				target_rate = node_def_rate;
+
+			target_rate = find_best_rate(node_rates, node_n_rates,
+						target_rate, node_def_rate);
+
+			pw_log_debug("%p: def_rate:%d target_rate:%d rate:%d/%d", context,
+					node_def_rate, target_rate, rate.num, rate.denom);
 		}
 
 		was_target_pending = n->target_pending;
@@ -1791,15 +1804,15 @@ again:
 
 		if (node_rate_quantum != 0 && current_rate != node_rate_quantum) {
 			/* the quantum values are scaled with the current rate */
-			node_def_quantum = node_def_quantum * current_rate / node_rate_quantum;
-			node_min_quantum = node_min_quantum * current_rate / node_rate_quantum;
-			node_max_quantum = node_max_quantum * current_rate / node_rate_quantum;
+			node_def_quantum = SPA_SCALE32(node_def_quantum, current_rate, node_rate_quantum);
+			node_min_quantum = SPA_SCALE32(node_min_quantum, current_rate, node_rate_quantum);
+			node_max_quantum = SPA_SCALE32(node_max_quantum, current_rate, node_rate_quantum);
 		}
 
 		/* calculate desired quantum. Don't limit to the max_latency when we are
 		 * going to force a quantum or rate and reconfigure the nodes. */
 		if (max_latency.denom != 0 && !force_quantum && !force_rate) {
-			uint32_t tmp = (max_latency.num * current_rate / max_latency.denom);
+			uint32_t tmp = SPA_SCALE32(max_latency.num, current_rate, max_latency.denom);
 			if (tmp < node_max_quantum)
 				node_max_quantum = tmp;
 		}
@@ -1816,7 +1829,7 @@ again:
 		else {
 			target_quantum = node_def_quantum;
 			if (latency.denom != 0)
-				target_quantum = (latency.num * current_rate / latency.denom);
+				target_quantum = SPA_SCALE32(latency.num, current_rate, latency.denom);
 			target_quantum = SPA_CLAMP(target_quantum, node_min_quantum, node_max_quantum);
 			target_quantum = SPA_CLAMP(target_quantum, floor_quantum, ceil_quantum);
 
@@ -1873,10 +1886,14 @@ again:
 				n->rt.position->clock.target_duration,
 				n->rt.position->clock.target_rate.denom, n->name);
 
+		transport = PW_NODE_ACTIVATION_COMMAND_NONE;
+
 		/* first change the node states of the followers to the new target */
 		spa_list_for_each(s, &n->follower_list, follower_link) {
-			if (s->transport)
-				transport_start = true;
+			if (s->transport != PW_NODE_ACTIVATION_COMMAND_NONE) {
+				transport = s->transport;
+				s->transport = PW_NODE_ACTIVATION_COMMAND_NONE;
+			}
 			if (s == n)
 				continue;
 			pw_log_debug("%p: follower %p: active:%d '%s'",
@@ -1884,10 +1901,10 @@ again:
 			ensure_state(s, running);
 		}
 
-		SPA_ATOMIC_STORE(n->rt.target.activation->command,
-				transport_start ?
-					PW_NODE_ACTIVATION_COMMAND_START :
-					PW_NODE_ACTIVATION_COMMAND_STOP);
+		if (transport != PW_NODE_ACTIVATION_COMMAND_NONE) {
+			pw_log_info("%s: transport %d", n->name, transport);
+			SPA_ATOMIC_STORE(n->rt.target.activation->command, transport);
+		}
 
 		/* now that all the followers are ready, start the driver */
 		ensure_state(n, running);
diff --git a/src/pipewire/core.c b/src/pipewire/core.c
index a627452f..3eb6d7bb 100644
--- a/src/pipewire/core.c
+++ b/src/pipewire/core.c
@@ -12,6 +12,9 @@
 #include <spa/pod/parser.h>
 #include <spa/debug/types.h>
 
+#define PW_API_CORE_IMPL	SPA_EXPORT
+#define PW_API_REGISTRY_IMPL	SPA_EXPORT
+
 #include "pipewire/pipewire.h"
 #include "pipewire/private.h"
 
@@ -20,6 +23,18 @@
 PW_LOG_TOPIC_EXTERN(log_core);
 #define PW_LOG_TOPIC_DEFAULT log_core
 
+static void core_event_info(void *data, const struct pw_core_info *info)
+{
+	struct pw_core *this = data;
+	if (info && info->props) {
+		static const char * const keys[] = {
+			"default.clock.quantum-limit",
+			NULL
+		};
+		pw_properties_update_keys(this->context->properties, info->props, keys);
+	}
+}
+
 static void core_event_ping(void *data, uint32_t id, int seq)
 {
 	struct pw_core *this = data;
@@ -111,6 +126,7 @@ static void core_event_remove_mem(void *data, uint32_t id)
 
 static const struct pw_core_events core_events = {
 	PW_VERSION_CORE_EVENTS,
+	.info = core_event_info,
 	.error = core_event_error,
 	.ping = core_event_ping,
 	.done = core_event_done,
@@ -476,6 +492,15 @@ struct pw_mempool * pw_core_get_mempool(struct pw_core *core)
 	return core->pool;
 }
 
+SPA_EXPORT
+void pw_core_add_proxy_listener(struct pw_core *object,
+			struct spa_hook *listener,
+			const struct pw_proxy_events *events,
+			void *data)
+{
+	pw_proxy_add_listener((struct pw_proxy *)object, listener, events, data);
+}
+
 SPA_EXPORT
 int pw_core_disconnect(struct pw_core *core)
 {
diff --git a/src/pipewire/core.h b/src/pipewire/core.h
index 171d35d6..be7beb41 100644
--- a/src/pipewire/core.h
+++ b/src/pipewire/core.h
@@ -14,6 +14,8 @@ extern "C" {
 
 #include <spa/utils/hook.h>
 
+#include <pipewire/type.h>
+
 /** \defgroup pw_core Core
  *
  * \brief The core global object.
@@ -41,6 +43,13 @@ struct pw_core;
 #define PW_VERSION_REGISTRY	3
 struct pw_registry;
 
+#ifndef PW_API_CORE_IMPL
+#define PW_API_CORE_IMPL static inline
+#endif
+#ifndef PW_API_REGISTRY_IMPL
+#define PW_API_REGISTRY_IMPL static inline
+#endif
+
 /** The default remote name to connect to */
 #define PW_DEFAULT_REMOTE	"pipewire-0"
 
@@ -334,23 +343,51 @@ struct pw_core_methods {
 	int (*destroy) (void *object, void *proxy);
 };
 
-#define pw_core_method(o,method,version,...)			\
-({									\
-	int _res = -ENOTSUP;						\
-	spa_interface_call_res((struct spa_interface*)o,		\
-			struct pw_core_methods, _res,		\
-			method, version, ##__VA_ARGS__);		\
-	_res;								\
-})
 
-#define pw_core_add_listener(c,...)	pw_core_method(c,add_listener,0,__VA_ARGS__)
-#define pw_core_hello(c,...)		pw_core_method(c,hello,0,__VA_ARGS__)
-#define pw_core_sync(c,...)		pw_core_method(c,sync,0,__VA_ARGS__)
-#define pw_core_pong(c,...)		pw_core_method(c,pong,0,__VA_ARGS__)
-#define pw_core_error(c,...)		pw_core_method(c,error,0,__VA_ARGS__)
-
-
-static inline
+/** \copydoc pw_core_methods.add_listener
+ * \sa pw_core_methods.add_listener */
+PW_API_CORE_IMPL int pw_core_add_listener(struct pw_core *object,
+			struct spa_hook *listener,
+			const struct pw_core_events *events,
+			void *data)
+{
+	return spa_api_method_r(int, -ENOTSUP,
+			pw_core, (struct spa_interface*)object, add_listener, 0,
+			listener, events, data);
+}
+/** \copydoc pw_core_methods.hello
+ * \sa pw_core_methods.hello */
+PW_API_CORE_IMPL int pw_core_hello(struct pw_core *object, uint32_t version)
+{
+	return spa_api_method_r(int, -ENOTSUP,
+			pw_core, (struct spa_interface*)object, hello, 0,
+			version);
+}
+/** \copydoc pw_core_methods.sync
+ * \sa pw_core_methods.sync */
+PW_API_CORE_IMPL int pw_core_sync(struct pw_core *object, uint32_t id, int seq)
+{
+	return spa_api_method_r(int, -ENOTSUP,
+			pw_core, (struct spa_interface*)object, sync, 0,
+			id, seq);
+}
+/** \copydoc pw_core_methods.pong
+ * \sa pw_core_methods.pong */
+PW_API_CORE_IMPL int pw_core_pong(struct pw_core *object, uint32_t id, int seq)
+{
+	return spa_api_method_r(int, -ENOTSUP,
+			pw_core, (struct spa_interface*)object, pong, 0,
+			id, seq);
+}
+/** \copydoc pw_core_methods.error
+ * \sa pw_core_methods.error */
+PW_API_CORE_IMPL int pw_core_error(struct pw_core *object, uint32_t id, int seq, int res, const char *message)
+{
+	return spa_api_method_r(int, -ENOTSUP,
+			pw_core, (struct spa_interface*)object, error, 0,
+			id, seq, res, message);
+}
+PW_API_CORE_IMPL
 SPA_PRINTF_FUNC(5, 0) int
 pw_core_errorv(struct pw_core *core, uint32_t id, int seq,
 		int res, const char *message, va_list args)
@@ -361,7 +398,7 @@ pw_core_errorv(struct pw_core *core, uint32_t id, int seq,
 	return pw_core_error(core, id, seq, res, buffer);
 }
 
-static inline
+PW_API_CORE_IMPL
 SPA_PRINTF_FUNC(5, 6) int
 pw_core_errorf(struct pw_core *core, uint32_t id, int seq,
 		int res, const char *message, ...)
@@ -374,17 +411,18 @@ pw_core_errorf(struct pw_core *core, uint32_t id, int seq,
 	return r;
 }
 
-static inline struct pw_registry *
+/** \copydoc pw_core_methods.get_registry
+ * \sa pw_core_methods.get_registry */
+PW_API_CORE_IMPL struct pw_registry *
 pw_core_get_registry(struct pw_core *core, uint32_t version, size_t user_data_size)
 {
-	struct pw_registry *res = NULL;
-	spa_interface_call_res((struct spa_interface*)core,
-			struct pw_core_methods, res,
-			get_registry, 0, version, user_data_size);
-	return res;
+	return spa_api_method_r(struct pw_registry*, NULL,
+			pw_core, (struct spa_interface*)core, get_registry, 0,
+			version, user_data_size);
 }
-
-static inline void *
+/** \copydoc pw_core_methods.create_object
+ * \sa pw_core_methods.create_object */
+PW_API_CORE_IMPL void *
 pw_core_create_object(struct pw_core *core,
 			    const char *factory_name,
 			    const char *type,
@@ -392,15 +430,18 @@ pw_core_create_object(struct pw_core *core,
 			    const struct spa_dict *props,
 			    size_t user_data_size)
 {
-	void *res = NULL;
-	spa_interface_call_res((struct spa_interface*)core,
-			struct pw_core_methods, res,
-			create_object, 0, factory_name,
-			type, version, props, user_data_size);
-	return res;
+	return spa_api_method_r(void*, NULL,
+			pw_core, (struct spa_interface*)core, create_object, 0,
+			factory_name, type, version, props, user_data_size);
+}
+/** \copydoc pw_core_methods.destroy
+ * \sa pw_core_methods.destroy */
+PW_API_CORE_IMPL void
+pw_core_destroy(struct pw_core *core, void *proxy)
+{
+	spa_api_method_v(pw_core, (struct spa_interface*)core, destroy, 0,
+			proxy);
 }
-
-#define pw_core_destroy(c,...)		pw_core_method(c,destroy,0,__VA_ARGS__)
 
 /**
  * \}
@@ -516,31 +557,38 @@ struct pw_registry_methods {
 	int (*destroy) (void *object, uint32_t id);
 };
 
-#define pw_registry_method(o,method,version,...)			\
-({									\
-	int _res = -ENOTSUP;						\
-	spa_interface_call_res((struct spa_interface*)o,		\
-			struct pw_registry_methods, _res,		\
-			method, version, ##__VA_ARGS__);		\
-	_res;								\
-})
 
 /** Registry */
-#define pw_registry_add_listener(p,...)	pw_registry_method(p,add_listener,0,__VA_ARGS__)
-
-static inline void *
+/** \copydoc pw_registry_methods.add_listener
+ * \sa pw_registry_methods.add_listener */
+PW_API_REGISTRY_IMPL int pw_registry_add_listener(struct pw_registry *registry,
+			struct spa_hook *listener,
+			const struct pw_registry_events *events,
+			void *data)
+{
+	return spa_api_method_r(int, -ENOTSUP,
+			pw_registry, (struct spa_interface*)registry, add_listener, 0,
+			listener, events, data);
+}
+/** \copydoc pw_registry_methods.bind
+ * \sa pw_registry_methods.bind */
+PW_API_REGISTRY_IMPL void *
 pw_registry_bind(struct pw_registry *registry,
 		       uint32_t id, const char *type, uint32_t version,
 		       size_t user_data_size)
 {
-	void *res = NULL;
-	spa_interface_call_res((struct spa_interface*)registry,
-			struct pw_registry_methods, res,
-			bind, 0, id, type, version, user_data_size);
-	return res;
+	return spa_api_method_r(void*, NULL,
+			pw_registry, (struct spa_interface*)registry, bind, 0,
+			id, type, version, user_data_size);
+}
+/** \copydoc pw_registry_methods.destroy
+ * \sa pw_registry_methods.destroy */
+PW_API_REGISTRY_IMPL int
+pw_registry_destroy(struct pw_registry *registry, uint32_t id)
+{
+	return spa_api_method_r(int, -ENOTSUP,
+			pw_registry, (struct spa_interface*)registry, destroy, 0, id);
 }
-
-#define pw_registry_destroy(p,...)	pw_registry_method(p,destroy,0,__VA_ARGS__)
 
 /**
  * \}
diff --git a/src/pipewire/device.h b/src/pipewire/device.h
index 4b546b99..9154daee 100644
--- a/src/pipewire/device.h
+++ b/src/pipewire/device.h
@@ -30,6 +30,10 @@ extern "C" {
 #define PW_VERSION_DEVICE		3
 struct pw_device;
 
+#ifndef PW_API_DEVICE_IMPL
+#define PW_API_DEVICE_IMPL static inline
+#endif
+
 /** The device information. Extra information can be added in later versions */
 struct pw_device_info {
 	uint32_t id;			/**< id of the global */
@@ -141,19 +145,44 @@ struct pw_device_methods {
 			  const struct spa_pod *param);
 };
 
-#define pw_device_method(o,method,version,...)				\
-({									\
-	int _res = -ENOTSUP;						\
-	spa_interface_call_res((struct spa_interface*)o,		\
-			struct pw_device_methods, _res,			\
-			method, version, ##__VA_ARGS__);		\
-	_res;								\
-})
-
-#define pw_device_add_listener(c,...)		pw_device_method(c,add_listener,0,__VA_ARGS__)
-#define pw_device_subscribe_params(c,...)	pw_device_method(c,subscribe_params,0,__VA_ARGS__)
-#define pw_device_enum_params(c,...)		pw_device_method(c,enum_params,0,__VA_ARGS__)
-#define pw_device_set_param(c,...)		pw_device_method(c,set_param,0,__VA_ARGS__)
+/** \copydoc pw_device_methods.add_listener
+ * \sa pw_device_methods.add_listener */
+PW_API_DEVICE_IMPL int pw_device_add_listener(struct pw_device *object,
+			struct spa_hook *listener,
+			const struct pw_device_events *events,
+			void *data)
+{
+	return spa_api_method_r(int, -ENOTSUP,
+			pw_device, (struct spa_interface*)object, add_listener, 0,
+			listener, events, data);
+}
+/** \copydoc pw_device_methods.subscribe_params
+ * \sa pw_device_methods.subscribe_params */
+PW_API_DEVICE_IMPL int pw_device_subscribe_params(struct pw_device *object, uint32_t *ids, uint32_t n_ids)
+{
+	return spa_api_method_r(int, -ENOTSUP,
+			pw_device, (struct spa_interface*)object, subscribe_params, 0,
+			ids, n_ids);
+}
+/** \copydoc pw_device_methods.enum_params
+ * \sa pw_device_methods.enum_params */
+PW_API_DEVICE_IMPL int pw_device_enum_params(struct pw_device *object,
+		int seq, uint32_t id, uint32_t start, uint32_t num,
+			    const struct spa_pod *filter)
+{
+	return spa_api_method_r(int, -ENOTSUP,
+			pw_device, (struct spa_interface*)object, enum_params, 0,
+			seq, id, start, num, filter);
+}
+/** \copydoc pw_device_methods.set_param
+ * \sa pw_device_methods.set_param */
+PW_API_DEVICE_IMPL int pw_device_set_param(struct pw_device *object, uint32_t id, uint32_t flags,
+			  const struct spa_pod *param)
+{
+	return spa_api_method_r(int, -ENOTSUP,
+			pw_device, (struct spa_interface*)object, set_param, 0,
+			id, flags, param);
+}
 
 /**
  * \}
diff --git a/src/pipewire/extensions/client-node.h b/src/pipewire/extensions/client-node.h
index d1c6e733..69f278e0 100644
--- a/src/pipewire/extensions/client-node.h
+++ b/src/pipewire/extensions/client-node.h
@@ -30,6 +30,10 @@ extern "C" {
 #define PW_VERSION_CLIENT_NODE			6
 struct pw_client_node;
 
+#ifndef PW_API_CLIENT_NODE_IMPL
+#define PW_API_CLIENT_NODE_IMPL static inline
+#endif
+
 #define PW_EXTENSION_MODULE_CLIENT_NODE		PIPEWIRE_MODULE_PREFIX "module-client-node"
 
 /** information about a buffer */
@@ -303,33 +307,54 @@ struct pw_client_node_methods {
 			  struct spa_buffer **buffers);
 };
 
-
-#define pw_client_node_method(o,method,version,...)			\
-({									\
-	int _res = -ENOTSUP;						\
-	spa_interface_call_res((struct spa_interface*)o,		\
-			struct pw_client_node_methods, _res,		\
-			method, version, ##__VA_ARGS__);		\
-	_res;								\
-})
-
-#define pw_client_node_add_listener(c,...)	pw_client_node_method(c,add_listener,0,__VA_ARGS__)
-
-static inline struct pw_node *
+PW_API_CLIENT_NODE_IMPL int pw_client_node_add_listener(struct pw_client_node *object,
+			struct spa_hook *listener,
+			const struct pw_client_node_events *events,
+			void *data)
+{
+	return spa_api_method_r(int, -ENOTSUP, pw_client_node, (struct spa_interface*)object,
+			add_listener, 0, listener, events, data);
+}
+PW_API_CLIENT_NODE_IMPL struct pw_node *
 pw_client_node_get_node(struct pw_client_node *p, uint32_t version, size_t user_data_size)
 {
-	struct pw_node *res = NULL;
-	spa_interface_call_res((struct spa_interface*)p,
-			struct pw_client_node_methods, res,
+	return spa_api_method_r(struct pw_node*, NULL, pw_client_node, (struct spa_interface*)p,
 			get_node, 0, version, user_data_size);
-	return res;
 }
-
-#define pw_client_node_update(c,...)		pw_client_node_method(c,update,0,__VA_ARGS__)
-#define pw_client_node_port_update(c,...)	pw_client_node_method(c,port_update,0,__VA_ARGS__)
-#define pw_client_node_set_active(c,...)	pw_client_node_method(c,set_active,0,__VA_ARGS__)
-#define pw_client_node_event(c,...)		pw_client_node_method(c,event,0,__VA_ARGS__)
-#define pw_client_node_port_buffers(c,...)	pw_client_node_method(c,port_buffers,0,__VA_ARGS__)
+PW_API_CLIENT_NODE_IMPL int pw_client_node_update(struct pw_client_node *object,
+			uint32_t change_mask,
+			uint32_t n_params, const struct spa_pod **params,
+			const struct spa_node_info *info)
+{
+	return spa_api_method_r(int, -ENOTSUP, pw_client_node, (struct spa_interface*)object,
+			update, 0, change_mask, n_params, params, info);
+}
+PW_API_CLIENT_NODE_IMPL int pw_client_node_port_update(struct pw_client_node *object,
+			     enum spa_direction direction, uint32_t port_id,
+			     uint32_t change_mask,
+			     uint32_t n_params, const struct spa_pod **params,
+			     const struct spa_port_info *info)
+{
+	return spa_api_method_r(int, -ENOTSUP, pw_client_node, (struct spa_interface*)object,
+			port_update, 0, direction, port_id, change_mask, n_params, params, info);
+}
+PW_API_CLIENT_NODE_IMPL int pw_client_node_set_active(struct pw_client_node *object, bool active)
+{
+	return spa_api_method_r(int, -ENOTSUP, pw_client_node, (struct spa_interface*)object,
+			set_active, 0, active);
+}
+PW_API_CLIENT_NODE_IMPL int pw_client_node_event(struct pw_client_node *object, const struct spa_event *event)
+{
+	return spa_api_method_r(int, -ENOTSUP, pw_client_node, (struct spa_interface*)object,
+			event, 0, event);
+}
+PW_API_CLIENT_NODE_IMPL int pw_client_node_port_buffers(struct pw_client_node *object,
+			  enum spa_direction direction, uint32_t port_id,
+			  uint32_t mix_id, uint32_t n_buffers, struct spa_buffer **buffers)
+{
+	return spa_api_method_r(int, -ENOTSUP, pw_client_node, (struct spa_interface*)object,
+			port_buffers, 0, direction, port_id, mix_id, n_buffers, buffers);
+}
 
 /**
  * \}
diff --git a/src/pipewire/extensions/metadata.h b/src/pipewire/extensions/metadata.h
index 8c0641fb..58057c60 100644
--- a/src/pipewire/extensions/metadata.h
+++ b/src/pipewire/extensions/metadata.h
@@ -10,6 +10,9 @@ extern "C" {
 #endif
 
 #include <spa/utils/defs.h>
+#include <spa/utils/hook.h>
+
+#include <errno.h>
 
 /** \defgroup pw_metadata Metadata
  * Metadata interface
@@ -26,6 +29,10 @@ extern "C" {
 #define PW_VERSION_METADATA			3
 struct pw_metadata;
 
+#ifndef PW_API_METADATA_IMPL
+#define PW_API_METADATA_IMPL static inline
+#endif
+
 #define PW_EXTENSION_MODULE_METADATA		PIPEWIRE_MODULE_PREFIX "module-metadata"
 
 #define PW_METADATA_EVENT_PROPERTY		0
@@ -89,19 +96,36 @@ struct pw_metadata_methods {
 	int (*clear) (void *object);
 };
 
-
-#define pw_metadata_method(o,method,version,...)			\
-({									\
-	int _res = -ENOTSUP;						\
-	spa_interface_call_res((struct spa_interface*)o,		\
-			struct pw_metadata_methods, _res,		\
-			method, version, ##__VA_ARGS__);		\
-	_res;								\
-})
-
-#define pw_metadata_add_listener(c,...)		pw_metadata_method(c,add_listener,0,__VA_ARGS__)
-#define pw_metadata_set_property(c,...)		pw_metadata_method(c,set_property,0,__VA_ARGS__)
-#define pw_metadata_clear(c)			pw_metadata_method(c,clear,0)
+/** \copydoc pw_metadata_methods.add_listener
+ * \sa pw_metadata_methods.add_listener */
+PW_API_METADATA_IMPL int pw_metadata_add_listener(struct pw_metadata *object,
+			struct spa_hook *listener,
+			const struct pw_metadata_events *events,
+			void *data)
+{
+	return spa_api_method_r(int, -ENOTSUP,
+			pw_metadata, (struct spa_interface*)object, add_listener, 0,
+			listener, events, data);
+}
+/** \copydoc pw_metadata_methods.set_property
+ * \sa pw_metadata_methods.set_property */
+PW_API_METADATA_IMPL int pw_metadata_set_property(struct pw_metadata *object,
+			uint32_t subject,
+			const char *key,
+			const char *type,
+			const char *value)
+{
+	return spa_api_method_r(int, -ENOTSUP,
+			pw_metadata, (struct spa_interface*)object, set_property, 0,
+			subject, key, type, value);
+}
+/** \copydoc pw_metadata_methods.clear
+ * \sa pw_metadata_methods.clear */
+PW_API_METADATA_IMPL int pw_metadata_clear(struct pw_metadata *object)
+{
+	return spa_api_method_r(int, -ENOTSUP,
+			pw_metadata, (struct spa_interface*)object, clear, 0);
+}
 
 #define PW_KEY_METADATA_NAME		"metadata.name"
 #define PW_KEY_METADATA_VALUES		"metadata.values"
diff --git a/src/pipewire/extensions/profiler.h b/src/pipewire/extensions/profiler.h
index 81e997b4..d0e8908e 100644
--- a/src/pipewire/extensions/profiler.h
+++ b/src/pipewire/extensions/profiler.h
@@ -24,6 +24,10 @@ extern "C" {
 #define PW_VERSION_PROFILER			3
 struct pw_profiler;
 
+#ifndef PW_API_PROFILER
+#define PW_API_PROFILER static inline
+#endif
+
 #define PW_EXTENSION_MODULE_PROFILER		PIPEWIRE_MODULE_PREFIX "module-profiler"
 
 #define PW_PROFILER_PERM_MASK			PW_PERM_R
@@ -53,16 +57,17 @@ struct pw_profiler_methods {
 			void *data);
 };
 
-#define pw_profiler_method(o,method,version,...)			\
-({									\
-	int _res = -ENOTSUP;						\
-	spa_interface_call_res((struct spa_interface*)o,		\
-			struct pw_profiler_methods, _res,		\
-			method, version, ##__VA_ARGS__);		\
-	_res;								\
-})
-
-#define pw_profiler_add_listener(c,...)		pw_profiler_method(c,add_listener,0,__VA_ARGS__)
+/** \copydoc pw_profiler_methods.add_listener
+ * \sa pw_profiler_methods.add_listener */
+PW_API_PROFILER int pw_profiler_add_listener(struct pw_profiler *object,
+			struct spa_hook *listener,
+			const struct pw_profiler_events *events,
+			void *data)
+{
+	return spa_api_method_r(int, -ENOTSUP,
+			pw_profiler, (struct spa_interface*)object, add_listener, 0,
+			listener, events, data);
+}
 
 #define PW_KEY_PROFILER_NAME		"profiler.name"
 
diff --git a/src/pipewire/extensions/security-context.h b/src/pipewire/extensions/security-context.h
index e21b5a3d..5a3cd2c1 100644
--- a/src/pipewire/extensions/security-context.h
+++ b/src/pipewire/extensions/security-context.h
@@ -26,6 +26,10 @@ extern "C" {
 #define PW_VERSION_SECURITY_CONTEXT			3
 struct pw_security_context;
 
+#ifndef PW_API_SECURITY_CONTEXT
+#define PW_API_SECURITY_CONTEXT static inline
+#endif
+
 #define PW_EXTENSION_MODULE_SECURITY_CONTEXT		PIPEWIRE_MODULE_PREFIX "module-security-context"
 
 #define PW_SECURITY_CONTEXT_EVENT_NUM			0
@@ -94,18 +98,27 @@ struct pw_security_context_methods {
 			const struct spa_dict *props);
 };
 
-
-#define pw_security_context_method(o,method,version,...)		\
-({									\
-	int _res = -ENOTSUP;						\
-	spa_interface_call_res((struct spa_interface*)o,		\
-			struct pw_security_context_methods, _res,	\
-			method, version, ##__VA_ARGS__);		\
-	_res;								\
-})
-
-#define pw_security_context_add_listener(c,...)	pw_security_context_method(c,add_listener,0,__VA_ARGS__)
-#define pw_security_context_create(c,...)	pw_security_context_method(c,create,0,__VA_ARGS__)
+/** \copydoc pw_security_context_methods.add_listener
+ * \sa pw_security_context_methods.add_listener */
+PW_API_SECURITY_CONTEXT int pw_security_context_add_listener(struct pw_security_context *object,
+			struct spa_hook *listener,
+			const struct pw_security_context_events *events,
+			void *data)
+{
+	return spa_api_method_r(int, -ENOTSUP,
+			pw_security_context, (struct spa_interface*)object, add_listener, 0,
+			listener, events, data);
+}
+
+/** \copydoc pw_security_context_methods.create
+ * \sa pw_security_context_methods.create */
+PW_API_SECURITY_CONTEXT int pw_security_context_create(struct pw_security_context *object,
+			int listen_fd, int close_fd, const struct spa_dict *props)
+{
+	return spa_api_method_r(int, -ENOTSUP,
+			pw_security_context, (struct spa_interface*)object, create, 0,
+			listen_fd, close_fd, props);
+}
 
 /**
  * \}
diff --git a/src/pipewire/factory.h b/src/pipewire/factory.h
index 6eda0420..9c6ab74f 100644
--- a/src/pipewire/factory.h
+++ b/src/pipewire/factory.h
@@ -32,6 +32,10 @@ extern "C" {
 #define PW_VERSION_FACTORY		3
 struct pw_factory;
 
+#ifndef PW_API_FACTORY_IMPL
+#define PW_API_FACTORY_IMPL static inline
+#endif
+
 /** The factory information. Extra information can be added in later versions */
 struct pw_factory_info {
 	uint32_t id;			/**< id of the global */
@@ -83,16 +87,17 @@ struct pw_factory_methods {
 			void *data);
 };
 
-#define pw_factory_method(o,method,version,...)				\
-({									\
-	int _res = -ENOTSUP;						\
-	spa_interface_call_res((struct spa_interface*)o,		\
-			struct pw_factory_methods, _res,		\
-			method, version, ##__VA_ARGS__);		\
-	_res;								\
-})
-
-#define pw_factory_add_listener(c,...)	pw_factory_method(c,add_listener,0,__VA_ARGS__)
+/** \copydoc pw_factory_methods.add_listener
+ * \sa pw_factory_methods.add_listener */
+PW_API_FACTORY_IMPL int pw_factory_add_listener(struct pw_factory *object,
+			struct spa_hook *listener,
+			const struct pw_factory_events *events,
+			void *data)
+{
+	return spa_api_method_r(int, -ENOTSUP,
+			pw_factory, (struct spa_interface*)object, add_listener, 0,
+			listener, events, data);
+}
 
 /**
  * \}
diff --git a/src/pipewire/filter.c b/src/pipewire/filter.c
index 4c774a9d..519c1e2f 100644
--- a/src/pipewire/filter.c
+++ b/src/pipewire/filter.c
@@ -146,6 +146,7 @@ struct filter {
 	unsigned int warn_mlock:1;
 	unsigned int trigger:1;
 	int in_emit_param_changed;
+	int pending_drain;
 };
 
 static int get_param_index(uint32_t id)
@@ -406,8 +407,10 @@ static bool filter_set_state(struct pw_filter *filter, enum pw_filter_state stat
 			     pw_filter_state_as_string(old),
 			     pw_filter_state_as_string(state), res, error);
 
-		if (state == PW_FILTER_STATE_ERROR)
+		if (state == PW_FILTER_STATE_ERROR) {
 			pw_log_error("%p: error (%d) %s", filter, res, error);
+			errno = -res;
+		}
 
 		filter->state = state;
 		pw_filter_emit_state_changed(filter, old, state, error);
@@ -989,13 +992,19 @@ do_call_drained(struct spa_loop *loop,
 	struct pw_filter *filter = &impl->this;
 	pw_log_trace("%p: drained", filter);
 	pw_filter_emit_drained(filter);
+	SPA_ATOMIC_DEC(impl->pending_drain);
 	return 0;
 }
 
 static void call_drained(struct filter *impl)
 {
-	pw_loop_invoke(impl->main_loop,
-		do_call_drained, 1, NULL, 0, false, impl);
+	pw_log_info("%p: drained", impl);
+	if (SPA_ATOMIC_INC(impl->pending_drain) == 1) {
+		pw_loop_invoke(impl->main_loop,
+			do_call_drained, 1, NULL, 0, false, impl);
+	} else {
+		SPA_ATOMIC_DEC(impl->pending_drain);
+	}
 }
 
 static int impl_node_process(void *object)
@@ -1119,11 +1128,14 @@ static void proxy_destroy(void *_data)
 static void proxy_error(void *_data, int seq, int res, const char *message)
 {
 	struct pw_filter *filter = _data;
+	int old_errno = errno;
 	/* we just emit the state change here to inform the application.
 	 * If this is supposed to be a permanent error, the app should
 	 * do a pw_filter_set_error() */
+	errno = -res;
 	pw_filter_emit_state_changed(filter, filter->state,
 			PW_FILTER_STATE_ERROR, message);
+	errno = old_errno;
 }
 
 static void proxy_bound_props(void *_data, uint32_t global_id, const struct spa_dict *props)
@@ -1467,6 +1479,8 @@ enum pw_filter_state pw_filter_get_state(struct pw_filter *filter, const char **
 {
 	if (error)
 		*error = filter->error;
+	if (filter->state == PW_FILTER_STATE_ERROR)
+		errno = -filter->error_res;
 	return filter->state;
 }
 
@@ -1762,17 +1776,26 @@ static void add_video_dsp_port_params(struct filter *impl, struct port *port)
 			SPA_FORMAT_VIDEO_format,   SPA_POD_Id(SPA_VIDEO_FORMAT_DSP_F32)));
 }
 
-static void add_control_dsp_port_params(struct filter *impl, struct port *port)
+static void add_control_dsp_port_params(struct filter *impl, struct port *port, uint32_t types)
 {
 	uint8_t buffer[4096];
 	struct spa_pod_builder b;
+	struct spa_pod_frame f[1];
 
 	spa_pod_builder_init(&b, buffer, sizeof(buffer));
+	spa_pod_builder_push_object(&b, &f[0],
+		SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat);
+	spa_pod_builder_add(&b,
+		SPA_FORMAT_mediaType,      SPA_POD_Id(SPA_MEDIA_TYPE_application),
+		SPA_FORMAT_mediaSubtype,   SPA_POD_Id(SPA_MEDIA_SUBTYPE_control),
+		0);
+	if (types != 0) {
+		spa_pod_builder_add(&b,
+			SPA_FORMAT_CONTROL_types, SPA_POD_CHOICE_FLAGS_Int(types),
+			0);
+	}
 	add_param(impl, port, SPA_PARAM_EnumFormat, PARAM_FLAG_LOCKED,
-		spa_pod_builder_add_object(&b,
-			SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat,
-			SPA_FORMAT_mediaType,      SPA_POD_Id(SPA_MEDIA_TYPE_application),
-			SPA_FORMAT_mediaSubtype,   SPA_POD_Id(SPA_MEDIA_SUBTYPE_control)));
+			spa_pod_builder_pop(&b, &f[0]));
 }
 
 SPA_EXPORT
@@ -1828,9 +1851,12 @@ void *pw_filter_add_port(struct pw_filter *filter,
 			add_audio_dsp_port_params(impl, p);
 		else if (spa_streq(str, "32 bit float RGBA video"))
 			add_video_dsp_port_params(impl, p);
-		else if (spa_streq(str, "8 bit raw midi") ||
-		    spa_streq(str, "8 bit raw control"))
-			add_control_dsp_port_params(impl, p);
+		else if (spa_streq(str, "8 bit raw midi"))
+			add_control_dsp_port_params(impl, p, 1u << SPA_CONTROL_Midi);
+		else if (spa_streq(str, "8 bit raw control"))
+			add_control_dsp_port_params(impl, p, 0);
+		else if (spa_streq(str, "32 bit raw UMP"))
+			add_control_dsp_port_params(impl, p, 1u << SPA_CONTROL_UMP);
 	}
 	/* then override with user provided if any */
 	if (update_params(impl, p, SPA_ID_INVALID, params, n_params) < 0)
@@ -2079,20 +2105,23 @@ do_trigger_process(struct spa_loop *loop,
 	return spa_node_call_ready(&impl->callbacks, res);
 }
 
-static int do_trigger_request_process(struct spa_loop *loop,
+static int do_emit_event(struct spa_loop *loop,
                  bool async, uint32_t seq, const void *data, size_t size, void *user_data)
 {
 	struct filter *impl = user_data;
-	uint8_t buffer[1024];
-	struct spa_pod_builder b = { 0 };
-
-	spa_pod_builder_init(&b, buffer, sizeof(buffer));
-	spa_node_emit_event(&impl->hooks,
-			spa_pod_builder_add_object(&b,
-				SPA_TYPE_EVENT_Node, SPA_NODE_EVENT_RequestProcess));
+	const struct spa_event *event = data;
+	spa_node_emit_event(&impl->hooks, event);
 	return 0;
 }
 
+SPA_EXPORT
+int pw_filter_emit_event(struct pw_filter *filter, const struct spa_event *event)
+{
+	struct filter *impl = SPA_CONTAINER_OF(filter, struct filter, this);
+	return pw_loop_invoke(impl->main_loop,
+		do_emit_event, 1, event, SPA_POD_SIZE(&event->pod), false, impl);
+}
+
 SPA_EXPORT
 int pw_filter_trigger_process(struct pw_filter *filter)
 {
@@ -2102,13 +2131,13 @@ int pw_filter_trigger_process(struct pw_filter *filter)
 	pw_log_trace_fp("%p: driving:%d", impl, filter->node->driving);
 
 	if (impl->trigger) {
-		pw_impl_node_trigger(filter->node);
+		res = pw_impl_node_trigger(filter->node);
 	} else if (filter->node->driving) {
 		res = pw_loop_invoke(impl->data_loop,
 			do_trigger_process, 1, NULL, 0, false, impl);
 	} else {
-		res = pw_loop_invoke(impl->main_loop,
-			do_trigger_request_process, 1, NULL, 0, false, impl);
+		pw_filter_emit_event(filter,
+				&SPA_NODE_EVENT_INIT(SPA_NODE_EVENT_RequestProcess));
 	}
 	return res;
 }
diff --git a/src/pipewire/filter.h b/src/pipewire/filter.h
index 701c4481..8298b654 100644
--- a/src/pipewire/filter.h
+++ b/src/pipewire/filter.h
@@ -29,6 +29,7 @@ struct pw_filter;
 #include <spa/node/io.h>
 #include <spa/param/param.h>
 #include <spa/pod/command.h>
+#include <spa/pod/event.h>
 
 #include <pipewire/core.h>
 #include <pipewire/stream.h>
@@ -61,7 +62,8 @@ struct pw_filter_events {
 	uint32_t version;
 
 	void (*destroy) (void *data);
-	/** when the filter state changes */
+	/** when the filter state changes. Since 1.4 this also sets errno when the
+	 * new state is PW_FILTER_STATE_ERROR */
 	void (*state_changed) (void *data, enum pw_filter_state old,
 				enum pw_filter_state state, const char *error);
 
@@ -79,7 +81,9 @@ struct pw_filter_events {
 
         /** do processing. This is normally called from the
 	 *  mainloop but can also be called directly from the realtime data
-	 *  thread if the user is prepared to deal with this. */
+	 *  thread if the user is prepared to deal with this with the
+	 *  PW_FILTER_FLAG_RT_PROCESS. Only call methods marked with RT safe
+	 *  from this event when called from the realtime thread. */
         void (*process) (void *data, struct spa_io_position *position);
 
 	/** The filter is drained */
@@ -100,7 +104,8 @@ enum pw_filter_flags {
 							  *  called explicitly */
 	PW_FILTER_FLAG_DRIVER		= (1 << 1),	/**< be a driver */
 	PW_FILTER_FLAG_RT_PROCESS	= (1 << 2),	/**< call process from the realtime
-							  *  thread */
+							  *  thread. Only call methods marked as
+							  *  RT safe. */
 	PW_FILTER_FLAG_CUSTOM_LATENCY	= (1 << 3),	/**< don't call the default latency algorithm
 							  *  but emit the param_changed event for the
 							  *  ports when Latency params are received. */
@@ -149,6 +154,8 @@ void pw_filter_add_listener(struct pw_filter *filter,
 			    const struct pw_filter_events *events,
 			    void *data);
 
+/** Get the current filter state. Since 1.4 this also sets errno when the
+ * state is PW_FILTER_STATE_ERROR */
 enum pw_filter_state pw_filter_get_state(struct pw_filter *filter, const char **error);
 
 const char *pw_filter_get_name(struct pw_filter *filter);
@@ -211,26 +218,26 @@ pw_filter_update_params(struct pw_filter *filter,	/**< a \ref pw_filter */
 
 
 /** Query the time on the filter, deprecated, use the spa_io_position in the
- * process() method for timing information. */
+ * process() method for timing information. RT safe. */
 SPA_DEPRECATED
 int pw_filter_get_time(struct pw_filter *filter, struct pw_time *time);
 
 /** Get the current time in nanoseconds. This value can be compared with
- * the nsec value in the spa_io_position. Since 1.1.0 */
+ * the nsec value in the spa_io_position. RT safe. Since 1.1.0 */
 uint64_t pw_filter_get_nsec(struct pw_filter *filter);
 
 /** Get the data loop that is doing the processing of this filter. This loop
- * is assigned after pw_filter_connect().  * Since 1.1.0 */
+ * is assigned after pw_filter_connect(). Since 1.1.0 */
 struct pw_loop *pw_filter_get_data_loop(struct pw_filter *filter);
 
 /** Get a buffer that can be filled for output ports or consumed
- * for input ports.  */
+ * for input ports. RT safe. */
 struct pw_buffer *pw_filter_dequeue_buffer(void *port_data);
 
-/** Submit a buffer for playback or recycle a buffer for capture. */
+/** Submit a buffer for playback or recycle a buffer for capture. RT safe. */
 int pw_filter_queue_buffer(void *port_data, struct pw_buffer *buffer);
 
-/** Get a data pointer to the buffer data */
+/** Get a data pointer to the buffer data. RT safe. */
 void *pw_filter_get_dsp_buffer(void *port_data, uint32_t n_samples);
 
 /** Activate or deactivate the filter  */
@@ -240,7 +247,8 @@ int pw_filter_set_active(struct pw_filter *filter, bool active);
  * be called when all data is played or recorded. The filter can be resumed
  * after the drain by setting it active again with
  * \ref pw_filter_set_active(). A flush without a drain is mostly useful afer
- * a state change to PAUSED, to flush any remaining data from the queues. */
+ * a state change to PAUSED, to flush any remaining data from the queues.
+ * RT safe. */
 int pw_filter_flush(struct pw_filter *filter, bool drain);
 
 /** Check if the filter is driving. The filter needs to have the
@@ -250,13 +258,17 @@ int pw_filter_flush(struct pw_filter *filter, bool drain);
 bool pw_filter_is_driving(struct pw_filter *filter);
 
 /** Check if the graph is using lazy scheduling.
- * Since 1.2.7 */
+ * Since 1.4.0 */
 bool pw_filter_is_lazy(struct pw_filter *filter);
 
 /** Trigger a push/pull on the filter. One iteration of the graph will
- * be scheduled and process() will be called. Since 0.3.66 */
+ * be scheduled and process() will be called. RT safe. Since 0.3.66 */
 int pw_filter_trigger_process(struct pw_filter *filter);
 
+/** Emit an event from this filter. RT safe.
+ * Since 1.2.6 */
+int pw_filter_emit_event(struct pw_filter *filter, const struct spa_event *event);
+
 /**
  * \}
  */
diff --git a/src/pipewire/impl-client.c b/src/pipewire/impl-client.c
index 06e3d53b..d46e0793 100644
--- a/src/pipewire/impl-client.c
+++ b/src/pipewire/impl-client.c
@@ -6,8 +6,12 @@
 #include <string.h>
 #include <assert.h>
 
+#include <spa/utils/defs.h>
 #include <spa/utils/string.h>
 
+#define PW_API_CLIENT_IMPL	SPA_EXPORT
+#include "pipewire/client.h"
+
 #include "pipewire/impl.h"
 #include "pipewire/private.h"
 
diff --git a/src/pipewire/impl-device.c b/src/pipewire/impl-device.c
index 0a96baaf..71e2e266 100644
--- a/src/pipewire/impl-device.c
+++ b/src/pipewire/impl-device.c
@@ -11,6 +11,7 @@
 #include <spa/utils/string.h>
 #include <spa/utils/json-pod.h>
 
+#define PW_API_DEVICE_IMPL SPA_EXPORT
 #include "pipewire/impl.h"
 #include "pipewire/private.h"
 
diff --git a/src/pipewire/impl-factory.c b/src/pipewire/impl-factory.c
index 8863ef2e..413fa339 100644
--- a/src/pipewire/impl-factory.c
+++ b/src/pipewire/impl-factory.c
@@ -7,6 +7,8 @@
 #include <spa/debug/types.h>
 #include <spa/utils/string.h>
 
+#define PW_API_FACTORY_IMPL	SPA_EXPORT
+
 #include "pipewire/impl.h"
 #include "pipewire/private.h"
 
diff --git a/src/pipewire/impl-link.c b/src/pipewire/impl-link.c
index 6824301e..3df092ad 100644
--- a/src/pipewire/impl-link.c
+++ b/src/pipewire/impl-link.c
@@ -13,6 +13,7 @@
 #include <spa/param/param.h>
 #include <spa/debug/types.h>
 
+#define PW_API_LINK_IMPL	SPA_EXPORT
 #include "pipewire/impl-link.h"
 #include "pipewire/private.h"
 
@@ -33,6 +34,10 @@ struct impl {
 
 	uint32_t output_busy_id;
 	uint32_t input_busy_id;
+	int output_pending_seq;
+	int input_pending_seq;
+	int output_result;
+	int input_result;
 
 	struct spa_pod *format_filter;
 	struct pw_properties *properties;
@@ -68,28 +73,36 @@ static void info_changed(struct pw_impl_link *link)
 	link->info.change_mask = 0;
 }
 
-static inline void input_set_busy_id(struct pw_impl_link *link, uint32_t id)
+static inline int input_set_busy_id(struct pw_impl_link *link, uint32_t id, int pending_seq)
 {
 	struct impl *impl = SPA_CONTAINER_OF(link, struct impl, this);
+	int res = impl->input_result;
 	if (impl->input_busy_id != SPA_ID_INVALID)
 		link->input->busy_count--;
 	if (id != SPA_ID_INVALID)
 		link->input->busy_count++;
 	impl->input_busy_id = id;
+	impl->input_pending_seq = SPA_RESULT_ASYNC_SEQ(pending_seq);
+	impl->input_result = 0;
 	if (link->input->busy_count < 0)
 		pw_log_error("%s: invalid busy count:%d", link->name, link->input->busy_count);
+	return res;
 }
 
-static inline void output_set_busy_id(struct pw_impl_link *link, uint32_t id)
+static inline int output_set_busy_id(struct pw_impl_link *link, uint32_t id, int pending_seq)
 {
 	struct impl *impl = SPA_CONTAINER_OF(link, struct impl, this);
+	int res = impl->output_result;
 	if (impl->output_busy_id != SPA_ID_INVALID)
 		link->output->busy_count--;
 	if (id != SPA_ID_INVALID)
 		link->output->busy_count++;
 	impl->output_busy_id = id;
+	impl->output_pending_seq = SPA_RESULT_ASYNC_SEQ(pending_seq);
+	impl->output_result = 0;
 	if (link->output->busy_count < 0)
 		pw_log_error("%s: invalid busy count:%d", link->name, link->output->busy_count);
+	return res;
 }
 
 static void link_update_state(struct pw_impl_link *link, enum pw_link_state state, int res, char *error)
@@ -147,10 +160,10 @@ static void link_update_state(struct pw_impl_link *link, enum pw_link_state stat
 		link->prepared = false;
 		link->preparing = false;
 
-		output_set_busy_id(link, SPA_ID_INVALID);
+		output_set_busy_id(link, SPA_ID_INVALID, SPA_ID_INVALID);
 		pw_work_queue_cancel(impl->work, &link->output_link, SPA_ID_INVALID);
 
-		input_set_busy_id(link, SPA_ID_INVALID);
+		input_set_busy_id(link, SPA_ID_INVALID, SPA_ID_INVALID);
 		pw_work_queue_cancel(impl->work, &link->input_link, SPA_ID_INVALID);
 	}
 }
@@ -167,13 +180,12 @@ static void complete_ready(void *obj, void *data, int res, uint32_t id)
 		port = this->output;
 
 	if (id != SPA_ID_INVALID) {
-		if (id == impl->input_busy_id) {
-			input_set_busy_id(this, SPA_ID_INVALID);
-		} else if (id == impl->output_busy_id) {
-			output_set_busy_id(this, SPA_ID_INVALID);
-		} else {
+		if (id == impl->input_busy_id)
+			res = input_set_busy_id(this, SPA_ID_INVALID, SPA_ID_INVALID);
+		else if (id == impl->output_busy_id)
+			res = output_set_busy_id(this, SPA_ID_INVALID, SPA_ID_INVALID);
+		else
 			return;
-		}
 	}
 
 	pw_log_debug("%p: obj:%p port %p complete state:%d: %s", this, obj, port,
@@ -183,13 +195,12 @@ static void complete_ready(void *obj, void *data, int res, uint32_t id)
 		if (port->state < PW_IMPL_PORT_STATE_READY)
 			pw_impl_port_update_state(port, PW_IMPL_PORT_STATE_READY,
 					0, NULL);
+		if (this->input->state >= PW_IMPL_PORT_STATE_READY &&
+		    this->output->state >= PW_IMPL_PORT_STATE_READY)
+			link_update_state(this, PW_LINK_STATE_ALLOCATING, 0, NULL);
 	} else {
-		pw_impl_port_update_state(port, PW_IMPL_PORT_STATE_ERROR,
-				res, spa_aprintf("port error going to READY: %s", spa_strerror(res)));
+		link_update_state(this, PW_LINK_STATE_ERROR, -EIO, strdup("Format negotiation failed"));
 	}
-	if (this->input->state >= PW_IMPL_PORT_STATE_READY &&
-	    this->output->state >= PW_IMPL_PORT_STATE_READY)
-		link_update_state(this, PW_LINK_STATE_ALLOCATING, 0, NULL);
 }
 
 static void complete_paused(void *obj, void *data, int res, uint32_t id)
@@ -208,13 +219,12 @@ static void complete_paused(void *obj, void *data, int res, uint32_t id)
 	}
 
 	if (id != SPA_ID_INVALID) {
-		if (id == impl->input_busy_id) {
-			input_set_busy_id(this, SPA_ID_INVALID);
-		} else if (id == impl->output_busy_id) {
-			output_set_busy_id(this, SPA_ID_INVALID);
-		} else {
+		if (id == impl->input_busy_id)
+			res = input_set_busy_id(this, SPA_ID_INVALID, SPA_ID_INVALID);
+		else if (id == impl->output_busy_id)
+			res = output_set_busy_id(this, SPA_ID_INVALID, SPA_ID_INVALID);
+		else
 			return;
-		}
 	}
 
 	pw_log_debug("%p: obj:%p port %p complete state:%d: %s", this, obj, port,
@@ -225,13 +235,13 @@ static void complete_paused(void *obj, void *data, int res, uint32_t id)
 			pw_impl_port_update_state(port, PW_IMPL_PORT_STATE_PAUSED,
 					0, NULL);
 		mix->have_buffers = true;
+
+		if (this->rt.in_mix.have_buffers && this->rt.out_mix.have_buffers)
+			link_update_state(this, PW_LINK_STATE_PAUSED, 0, NULL);
 	} else {
-		pw_impl_port_update_state(port, PW_IMPL_PORT_STATE_ERROR,
-				res, spa_aprintf("port error going to PAUSED: %s", spa_strerror(res)));
 		mix->have_buffers = false;
+		link_update_state(this, PW_LINK_STATE_ERROR, -EIO, strdup("Buffer allocation failed"));
 	}
-	if (this->rt.in_mix.have_buffers && this->rt.out_mix.have_buffers)
-		link_update_state(this, PW_LINK_STATE_PAUSED, 0, NULL);
 }
 
 static void complete_sync(void *obj, void *data, int res, uint32_t id)
@@ -395,10 +405,11 @@ static int do_negotiate(struct pw_impl_link *this)
 			goto error;
 		}
 		if (SPA_RESULT_IS_ASYNC(res)) {
-			res = spa_node_sync(output->node->node, res);
-			busy_id = pw_work_queue_add(impl->work, &this->output_link, res,
+			pw_log_info("output set format %d", res);
+			busy_id = pw_work_queue_add(impl->work, &this->output_link,
+					spa_node_sync(output->node->node, res),
 					complete_ready, this);
-			output_set_busy_id(this, busy_id);
+			output_set_busy_id(this, busy_id, res);
 		} else {
 			complete_ready(&this->output_link, this, res, SPA_ID_INVALID);
 		}
@@ -414,10 +425,11 @@ static int do_negotiate(struct pw_impl_link *this)
 			goto error;
 		}
 		if (SPA_RESULT_IS_ASYNC(res2)) {
-			res2 = spa_node_sync(input->node->node, res2);
-			busy_id = pw_work_queue_add(impl->work, &this->input_link, res2,
+			pw_log_info("input set format %d", res2);
+			busy_id = pw_work_queue_add(impl->work, &this->input_link,
+					spa_node_sync(input->node->node, res2),
 					complete_ready, this);
-			input_set_busy_id(this, busy_id);
+			input_set_busy_id(this, busy_id, res2);
 			if (res == 0)
 				res = res2;
 		} else {
@@ -579,10 +591,10 @@ static int do_allocation(struct pw_impl_link *this)
 			goto error_clear;
 		}
 		if (SPA_RESULT_IS_ASYNC(res)) {
-			res = spa_node_sync(output->node->node, res);
-			busy_id = pw_work_queue_add(impl->work, &this->output_link, res,
+			busy_id = pw_work_queue_add(impl->work, &this->output_link,
+					spa_node_sync(output->node->node, res),
 					complete_paused, this);
-			output_set_busy_id(this, busy_id);
+			output_set_busy_id(this, busy_id, res);
 			if (flags & SPA_NODE_BUFFERS_FLAG_ALLOC)
 				return 0;
 		} else {
@@ -602,10 +614,10 @@ static int do_allocation(struct pw_impl_link *this)
 	}
 
 	if (SPA_RESULT_IS_ASYNC(res)) {
-		res = spa_node_sync(input->node->node, res);
-		busy_id = pw_work_queue_add(impl->work, &this->input_link, res,
+		busy_id = pw_work_queue_add(impl->work, &this->input_link,
+				spa_node_sync(input->node->node, res),
 				complete_paused, this);
-		input_set_busy_id(this, busy_id);
+		input_set_busy_id(this, busy_id, res);
 	} else {
 		complete_paused(&this->input_link, this, res, SPA_ID_INVALID);
 	}
@@ -742,7 +754,7 @@ static void input_remove(struct pw_impl_link *this, struct pw_impl_port *port)
 
 	pw_log_debug("%p: remove input port %p", this, port);
 
-	input_set_busy_id(this, SPA_ID_INVALID);
+	input_set_busy_id(this, SPA_ID_INVALID, SPA_ID_INVALID);
 
 	spa_hook_remove(&impl->input_port_listener);
 	spa_hook_remove(&impl->input_node_listener);
@@ -772,7 +784,7 @@ static void output_remove(struct pw_impl_link *this, struct pw_impl_port *port)
 
 	pw_log_debug("%p: remove output port %p", this, port);
 
-	output_set_busy_id(this, SPA_ID_INVALID);
+	output_set_busy_id(this, SPA_ID_INVALID, SPA_ID_INVALID);
 
 	spa_hook_remove(&impl->output_port_listener);
 	spa_hook_remove(&impl->output_node_listener);
@@ -1007,8 +1019,12 @@ static void input_node_result(void *data, int seq, int res, uint32_t type, const
 {
 	struct impl *impl = data;
 	struct pw_impl_port *port = impl->this.input;
-	pw_log_trace("%p: input port %p result seq:%d res:%d type:%u",
-			impl, port, seq, res, type);
+	pw_log_trace("%p: input port %p result seq:%d %d res:%d type:%u",
+			impl, port, seq, SPA_RESULT_ASYNC_SEQ(seq), res, type);
+
+	if (type == SPA_RESULT_TYPE_NODE_ERROR && impl->input_pending_seq == seq)
+		impl->input_result = res;
+
 	node_result(impl, &impl->this.input_link, seq, res, type, result);
 }
 
@@ -1016,8 +1032,12 @@ static void output_node_result(void *data, int seq, int res, uint32_t type, cons
 {
 	struct impl *impl = data;
 	struct pw_impl_port *port = impl->this.output;
-	pw_log_trace("%p: output port %p result seq:%d res:%d type:%u",
-			impl, port, seq, res, type);
+	pw_log_trace("%p: output port %p result seq:%d %d res:%d type:%u",
+			impl, port, seq, SPA_RESULT_ASYNC_SEQ(seq), res, type);
+
+	if (type == SPA_RESULT_TYPE_NODE_ERROR && impl->output_pending_seq == seq)
+		impl->output_result = res;
+
 	node_result(impl, &impl->this.output_link, seq, res, type, result);
 }
 
@@ -1377,8 +1397,9 @@ struct pw_impl_link *pw_context_create_link(struct pw_context *context,
 	this->name = spa_aprintf("%d.%d.%d -> %d.%d.%d",
 			output_node->info.id, output->port_id, this->rt.out_mix.port.port_id,
 			input_node->info.id, input->port_id, this->rt.in_mix.port.port_id);
-	pw_log_info("(%s) (%s) -> (%s) async:%d:%04x:%04x:%d", this->name, output_node->name,
+	pw_log_info("(%s) (%s) -> (%s) async:%d:%d:%d:%04x:%04x:%d", this->name, output_node->name,
 			input_node->name, output_node->driving,
+			output_node->async, input_node->async,
 			output->flags, input->flags, impl->async);
 
 	pw_impl_port_emit_link_added(output, this);
diff --git a/src/pipewire/impl-module.c b/src/pipewire/impl-module.c
index 2388760d..10aa432f 100644
--- a/src/pipewire/impl-module.c
+++ b/src/pipewire/impl-module.c
@@ -16,6 +16,7 @@
 #include <spa/utils/cleanup.h>
 #include <spa/utils/string.h>
 
+#define PW_API_MODULE_IMPL	SPA_EXPORT
 #include "pipewire/impl.h"
 #include "pipewire/private.h"
 
diff --git a/src/pipewire/impl-node.c b/src/pipewire/impl-node.c
index 7329884b..351f98b0 100644
--- a/src/pipewire/impl-node.c
+++ b/src/pipewire/impl-node.c
@@ -22,6 +22,7 @@
 #include <spa/utils/string.h>
 #include <spa/utils/json-pod.h>
 
+#define PW_API_NODE_IMPL	SPA_EXPORT
 #include "pipewire/impl-node.h"
 #include "pipewire/private.h"
 
@@ -47,6 +48,8 @@ struct impl {
 	unsigned int cache_params:1;
 	unsigned int pending_play:1;
 
+	struct spa_command *pending_request_process;
+
 	char *group;
 	char *link_group;
 	char *sync_group;
@@ -334,7 +337,7 @@ static int start_node(struct pw_impl_node *this)
 	} else {
 		/* driver nodes will wait until all other nodes are started before
 		 * they are started */
-		this->pending_request_process = 0;
+		spa_clear_ptr(impl->pending_request_process, free);
 		res = EBUSY;
 	}
 
@@ -439,7 +442,7 @@ static void node_update_state(struct pw_impl_node *node, enum pw_node_state stat
 				state = PW_NODE_STATE_ERROR;
 				error = spa_aprintf("Start error: %s", spa_strerror(res));
 				remove_node_from_graph(node);
-			} else if (node->pending_request_process > 0) {
+			} else if (impl->pending_request_process != NULL) {
 				emit_pending_request_process = true;
 			}
 		}
@@ -448,7 +451,8 @@ static void node_update_state(struct pw_impl_node *node, enum pw_node_state stat
 	case PW_NODE_STATE_SUSPENDED:
 	case PW_NODE_STATE_ERROR:
 		if (state != PW_NODE_STATE_IDLE || node->pause_on_idle)
-			remove_node_from_graph(node);
+			if (old != PW_NODE_STATE_CREATING)
+				remove_node_from_graph(node);
 		break;
 	default:
 		break;
@@ -475,10 +479,9 @@ static void node_update_state(struct pw_impl_node *node, enum pw_node_state stat
 	pw_impl_node_emit_state_changed(node, old, state, error);
 
 	if (emit_pending_request_process) {
-		pw_log_debug("%p: request process:%d", node, node->pending_request_process);
-		node->pending_request_process = 0;
-		spa_node_send_command(node->node,
-			    &SPA_NODE_COMMAND_INIT(SPA_NODE_COMMAND_RequestProcess));
+		pw_log_debug("%p: request process:%p", node, impl->pending_request_process);
+		spa_node_send_command(node->node, impl->pending_request_process);
+		spa_clear_ptr(impl->pending_request_process, free);
 	}
 
 	node->info.change_mask |= PW_NODE_CHANGE_MASK_STATE;
@@ -834,7 +837,9 @@ int pw_impl_node_set_io(struct pw_impl_node *this, uint32_t id, void *data, size
 
 	res = spa_node_set_io(this->node, id, data, size);
 
-	if (res >= 0 && !SPA_RESULT_IS_ASYNC(res) && this->rt.position)
+	if (this->rt.position &&
+	    ((res >= 0 && !SPA_RESULT_IS_ASYNC(res)) ||
+	    this->rt.target.activation->client_version < 1))
 		this->rt.target.activation->active_driver_id = this->rt.position->clock.id;
 
 	pw_log_debug("%p: set io: %s", this, spa_strerror(res));
@@ -1114,23 +1119,21 @@ static void check_properties(struct pw_impl_node *node)
 	const char *str, *recalc_reason = NULL;
 	struct spa_fraction frac;
 	uint32_t value;
-	bool driver, trigger, transport, sync, async;
+	bool driver, trigger, sync, async;
 	struct match match;
 
 	match = MATCH_INIT(node);
 	pw_context_conf_section_match_rules(context, "node.rules",
 			&node->properties->dict, execute_match, &match);
 
-	if ((str = pw_properties_get(node->properties, PW_KEY_PRIORITY_DRIVER))) {
-		value = pw_properties_parse_int(str);
-		if (value != node->priority_driver) {
-			pw_log_debug("%p: priority driver %d -> %d", node, node->priority_driver, value);
-			node->priority_driver = value;
-			if (node->registered && node->driver) {
-				remove_driver(context, node);
-				insert_driver(context, node);
-				recalc_reason = "driver priority changed";
-			}
+	value = pw_properties_get_uint32(node->properties, PW_KEY_PRIORITY_DRIVER, 0);
+	if (value != node->priority_driver) {
+		pw_log_debug("%p: priority driver %d -> %d", node, node->priority_driver, value);
+		node->priority_driver = value;
+		if (node->registered && node->driver) {
+			remove_driver(context, node);
+			insert_driver(context, node);
+			recalc_reason = "driver priority changed";
 		}
 	}
 	node->supports_lazy = pw_properties_get_uint32(node->properties, PW_KEY_NODE_SUPPORTS_LAZY, 0);
@@ -1219,10 +1222,13 @@ static void check_properties(struct pw_impl_node *node)
 		recalc_reason = "sync changed";
 	}
 
-	transport = pw_properties_get_bool(node->properties, PW_KEY_NODE_TRANSPORT, false);
-	if (transport != node->transport) {
-		pw_log_info("%p: transport %d -> %d", node, node->transport, transport);
-		node->transport = transport;
+	str = pw_properties_get(node->properties, PW_KEY_NODE_TRANSPORT);
+	if (str != NULL) {
+		node->transport = spa_atob(str) ?
+			PW_NODE_ACTIVATION_COMMAND_START :
+			PW_NODE_ACTIVATION_COMMAND_STOP;
+		pw_log_info("%p: transport %d", node, node->transport);
+		pw_properties_set(node->properties, PW_KEY_NODE_TRANSPORT, NULL);
 		recalc_reason = "transport changed";
 	}
 	async = pw_properties_get_bool(node->properties, PW_KEY_NODE_ASYNC, false);
@@ -1230,6 +1236,7 @@ static void check_properties(struct pw_impl_node *node)
 	if (async != node->async) {
 		pw_log_info("%p: async %d -> %d", node, node->async, async);
 		node->async = async;
+		SPA_FLAG_UPDATE(node->rt.target.activation->flags, PW_NODE_ACTIVATION_FLAG_ASYNC, async);
 	}
 
 	if ((str = pw_properties_get(node->properties, PW_KEY_MEDIA_CLASS)) != NULL &&
@@ -1277,13 +1284,11 @@ static void check_properties(struct pw_impl_node *node)
 	}
 	node->lock_quantum = pw_properties_get_bool(node->properties, PW_KEY_NODE_LOCK_QUANTUM, false);
 
-	if ((str = pw_properties_get(node->properties, PW_KEY_NODE_FORCE_QUANTUM))) {
-		if (spa_atou32(str, &value, 0) &&
-		    node->force_quantum != value) {
-		        node->force_quantum = value;
-			node->stamp = ++context->stamp;
-			recalc_reason = "force quantum changed";
-		}
+	value = pw_properties_get_uint32(node->properties, PW_KEY_NODE_FORCE_QUANTUM, 0);
+	if (node->force_quantum != value) {
+	        node->force_quantum = value;
+		node->stamp = ++context->stamp;
+		recalc_reason = "force quantum changed";
 	}
 
 	if ((str = pw_properties_get(node->properties, PW_KEY_NODE_RATE))) {
@@ -1299,18 +1304,17 @@ static void check_properties(struct pw_impl_node *node)
 	}
 	node->lock_rate = pw_properties_get_bool(node->properties, PW_KEY_NODE_LOCK_RATE, false);
 
-	if ((str = pw_properties_get(node->properties, PW_KEY_NODE_FORCE_RATE))) {
-		if (spa_atou32(str, &value, 0)) {
-			if (value == 0)
-				value = node->rate.denom;
-			if (node->force_rate != value) {
-				pw_log_info("(%s-%u) force-rate:%u -> %u", node->name,
-							node->info.id, node->force_rate, value);
-				node->force_rate = value;
-				node->stamp = ++context->stamp;
-				recalc_reason = "force rate changed";
-			}
-		}
+	value = pw_properties_get_uint32(node->properties, PW_KEY_NODE_FORCE_RATE, SPA_ID_INVALID);
+	if (value == 0)
+		value = node->rate.denom;
+	if (value == SPA_ID_INVALID)
+		value = 0;
+	if (node->force_rate != value) {
+		pw_log_info("(%s-%u) force-rate:%u -> %u", node->name,
+					node->info.id, node->force_rate, value);
+		node->force_rate = value;
+		node->stamp = ++context->stamp;
+		recalc_reason = "force rate changed";
 	}
 
 	pw_log_debug("%p: driver:%d recalc:%s active:%d", node, node->driver,
@@ -1354,7 +1358,7 @@ static inline void debug_xrun_target(struct pw_impl_node *driver,
 	enum spa_log_level level = SPA_LOG_LEVEL_DEBUG;
 
 	if ((suppressed = spa_ratelimit_test(&driver->rt.rate_limit, nsec)) >= 0)
-		level = SPA_LOG_LEVEL_WARN;
+		level = SPA_LOG_LEVEL_INFO;
 
 	pw_log(level, "(%s-%u) xrun state:%p pending:%d/%d s:%"PRIu64" a:%"PRIu64" f:%"PRIu64
 		" waiting:%"PRIu64" process:%"PRIu64" status:%s (%d suppressed)",
@@ -1375,7 +1379,7 @@ static inline void debug_xrun_graph(struct pw_impl_node *driver, uint64_t nsec,
 	struct pw_node_target *t;
 
 	if ((suppressed = spa_ratelimit_test(&driver->rt.rate_limit, nsec)) >= 0)
-		level = SPA_LOG_LEVEL_WARN;
+		level = SPA_LOG_LEVEL_INFO;
 
 	pw_log(level, "(%s-%u) graph xrun %s (%d suppressed)",
 			driver->name, driver->info.id, str_status(old_status), suppressed);
@@ -1409,7 +1413,7 @@ static void debug_sync_timeout(struct pw_impl_node *driver, uint64_t nsec)
 	int suppressed;
 
 	if ((suppressed = spa_ratelimit_test(&driver->rt.rate_limit, nsec)) >= 0)
-		level = SPA_LOG_LEVEL_WARN;
+		level = SPA_LOG_LEVEL_INFO;
 
 	pw_log(level, "(%s-%u) sync timeout, going to RUNNING (%d suppressed)",
 				driver->name, driver->info.id, suppressed);
@@ -1537,8 +1541,7 @@ int pw_impl_node_trigger(struct pw_impl_node *node)
 {
 	uint64_t nsec = get_time_ns(node->rt.target.system);
 	struct pw_node_target *t = &node->rt.target;
-	t->trigger(t, nsec);
-	return 0;
+	return t->trigger(t, nsec);
 }
 
 static void node_on_fd_events(struct spa_source *source)
@@ -1901,16 +1904,16 @@ static void node_result(void *data, int seq, int res, uint32_t type, const void
 	pw_impl_node_emit_result(node, seq, res, type, result);
 }
 
-static void handle_request_process(struct pw_impl_node *node)
+static void handle_request_process_command(struct pw_impl_node *node, const struct spa_command *command)
 {
 	struct impl *impl = SPA_CONTAINER_OF(node, struct impl, this);
 	if (node->driving) {
 		pw_log_debug("request process %d %d", node->info.state, impl->pending_state);
 		if (node->info.state == PW_NODE_STATE_RUNNING) {
-			spa_node_send_command(node->driver_node->node,
-				    &SPA_NODE_COMMAND_INIT(SPA_NODE_COMMAND_RequestProcess));
+			spa_node_send_command(node->driver_node->node, command);
 		} else if (impl->pending_state == PW_NODE_STATE_RUNNING) {
-			node->pending_request_process++;
+			spa_clear_ptr(impl->pending_request_process, free);
+			impl->pending_request_process = (struct spa_command*)spa_pod_copy(&command->pod);
 		}
 	}
 }
@@ -1932,9 +1935,18 @@ static void node_event(void *data, const struct spa_event *event)
 		break;
 	case SPA_NODE_EVENT_RequestProcess:
 		if (!node->driving && !node->exported) {
+			struct spa_command *command;
+			size_t size = SPA_POD_SIZE(&event->pod);
+
+			/* turn the event and all the arguments into a command */
+			command = alloca(size);
+			memcpy(command, event, size);
+			command->body.body.type = SPA_TYPE_COMMAND_Node;
+			command->body.body.id = SPA_NODE_COMMAND_RequestProcess;
+
 			/* send the request process to the driver but only on the
 			 * server size */
-			handle_request_process(node->driver_node);
+			handle_request_process_command(node->driver_node, command);
 		}
 		break;
 	default:
@@ -2048,12 +2060,13 @@ static int node_ready(void *data, int status)
 	struct pw_impl_node *node = data;
 	struct pw_impl_node *driver = node->driver_node;
 	struct pw_node_activation *a = node->rt.target.activation;
+	struct pw_node_activation_state *state = &a->state[0];
 	struct spa_system *data_system = node->rt.target.system;
 	struct pw_node_target *t, *reposition_target = NULL;;
 	struct pw_impl_port *p;
 	struct spa_io_clock *cl = &node->rt.position->clock;
 	int sync_type, all_ready, update_sync, target_sync, old_status;
-	uint32_t owner[2], reposition_owner;
+	uint32_t owner[2], reposition_owner, pending;
 	uint64_t min_timeout = UINT64_MAX, nsec;
 
 	pw_log_trace_fp("%p: ready driver:%d exported:%d %p status:%d prepared:%d", node,
@@ -2096,20 +2109,6 @@ static int node_ready(void *data, int status)
 		}
 	}
 
-	/* This update is done too late, the driver should do this
-	 * before calling the ready callback so that it can use the new target
-	 * duration and rate to schedule the next update. We do this here to
-	 * help drivers that don't support this yet */
-	if (SPA_UNLIKELY(cl->duration != cl->target_duration ||
-	    cl->rate.denom != cl->target_rate.denom)) {
-		pw_log_warn("driver %s did not update duration/rate (%"PRIu64"/%"PRIu64" %u/%u)",
-				node->name,
-				cl->duration, cl->target_duration,
-				cl->rate.denom, cl->target_rate.denom);
-		cl->duration = cl->target_duration;
-		cl->rate = cl->target_rate;
-	}
-
 	sync_type = check_updates(node, &reposition_owner);
 	owner[0] = SPA_ATOMIC_LOAD(a->segment_owner[0]);
 	owner[1] = SPA_ATOMIC_LOAD(a->segment_owner[1]);
@@ -2117,6 +2116,7 @@ again:
 	all_ready = sync_type == SYNC_CHECK;
 	update_sync = !all_ready;
 	target_sync = sync_type == SYNC_START ? true : false;
+	pending = 0;
 
 	spa_list_for_each(t, &driver->rt.target_list, link) {
 		struct pw_node_activation *ta = t->activation;
@@ -2125,6 +2125,14 @@ again:
 		ta->driver_id = driver->info.id;
 retry_status:
 		pw_node_activation_state_reset(&ta->state[0]);
+
+		if (ta->active_driver_id != ta->driver_id) {
+			pw_log_trace_fp("%p: (%s-%u) %d waiting for driver %d<>%d", t->node,
+					t->name, t->id, ta->status,
+					ta->active_driver_id, ta->driver_id);
+			continue;
+		}
+
 		/* we don't change the state of inactive nodes and don't use them
 		 * for reposition. The pending will be at least 1 and they might
 		 * get decremented to 0 but since the status is inactive, we don't
@@ -2138,6 +2146,9 @@ retry_status:
 		if (SPA_UNLIKELY(!SPA_ATOMIC_CAS(ta->status, old_status, PW_NODE_ACTIVATION_NOT_TRIGGERED)))
 			goto retry_status;
 
+		if (!SPA_FLAG_IS_SET(ta->flags, PW_NODE_ACTIVATION_FLAG_ASYNC))
+			pending++;
+
 		if (old_status == PW_NODE_ACTIVATION_TRIGGERED ||
 		    old_status == PW_NODE_ACTIVATION_AWAKE) {
 			update_xrun_stats(ta, 1, nsec / 1000, 0);
@@ -2178,6 +2189,7 @@ retry_status:
 		reposition_target = NULL;
 		goto again;
 	}
+	state->pending = pending;
 
 	update_position(node, all_ready, nsec);
 
@@ -2450,6 +2462,7 @@ void pw_impl_node_destroy(struct pw_impl_node *node)
 	pw_work_queue_cancel(impl->work, node, SPA_ID_INVALID);
 
 	pw_properties_free(node->properties);
+	spa_clear_ptr(impl->pending_request_process, free);
 
 	clear_info(node);
 
@@ -2846,7 +2859,7 @@ int pw_impl_node_send_command(struct pw_impl_node *node, const struct spa_comman
 
 	switch (id) {
 	case SPA_NODE_COMMAND_RequestProcess:
-		handle_request_process(node);
+		handle_request_process_command(node, command);
 		break;
 	default:
 		res = spa_node_send_command(node->node, command);
diff --git a/src/pipewire/impl-port.c b/src/pipewire/impl-port.c
index bbfa9ede..4356604a 100644
--- a/src/pipewire/impl-port.c
+++ b/src/pipewire/impl-port.c
@@ -19,6 +19,7 @@
 #include <spa/pod/dynamic.h>
 #include <spa/debug/pod.h>
 
+#define PW_API_PORT_IMPL	SPA_EXPORT
 #include "pipewire/impl.h"
 #include "pipewire/private.h"
 
@@ -1282,20 +1283,18 @@ int pw_impl_port_add(struct pw_impl_port *port, struct pw_impl_node *node)
 
 	channel_names = pw_properties_get(nprops, PW_KEY_NODE_CHANNELNAMES);
 	if (channel_names != NULL) {
-		struct spa_json it[2];
+		struct spa_json it[1];
 		char v[256];
                 uint32_t i;
 
-		spa_json_init(&it[0], channel_names, strlen(channel_names));
-		if (spa_json_enter_array(&it[0], &it[1]) <= 0)
-			spa_json_init(&it[1], channel_names, strlen(channel_names));
+		if (spa_json_begin_array_relax(&it[0], channel_names, strlen(channel_names)) > 0) {
+			for (i = 0; i < port->port_id + 1; i++)
+				if (spa_json_get_string(&it[0], v, sizeof(v)) <= 0)
+					break;
 
-		for (i = 0; i < port->port_id + 1; i++)
-			if (spa_json_get_string(&it[1], v, sizeof(v)) <= 0)
-				break;
-
-		if (i == port->port_id + 1 && strlen(v) > 0)
-			snprintf(position, sizeof(position), "%s", v);
+			if (i == port->port_id + 1 && strlen(v) > 0)
+				snprintf(position, sizeof(position), "%s", v);
+		}
 	}
 
 	if (pw_properties_get(port->properties, PW_KEY_PORT_NAME) == NULL) {
diff --git a/src/pipewire/keys.h b/src/pipewire/keys.h
index 600b2fa6..de4a9d40 100644
--- a/src/pipewire/keys.h
+++ b/src/pipewire/keys.h
@@ -104,9 +104,11 @@ extern "C" {
 								  *  default pipewire-0, overwritten by
 								  *  env(PIPEWIRE_REMOTE). May also be
 								  *  a SPA-JSON array of sockets, to be tried
-								  *  in order. */
+								  *  in order. The "internal" remote name and
+								  *  "generic" intention connects to the local
+								  *  PipeWire instance. */
 #define PW_KEY_REMOTE_INTENTION		"remote.intention"	/**< The intention of the remote connection,
-								  *  "generic", "screencast" */
+								  *  "generic", "screencast", "manager" */
 
 /** application keys */
 #define PW_KEY_APP_NAME			"application.name"	/**< application name. Ex: "Totem Music Player" */
diff --git a/src/pipewire/link.h b/src/pipewire/link.h
index ef96dfe2..316fb044 100644
--- a/src/pipewire/link.h
+++ b/src/pipewire/link.h
@@ -36,6 +36,11 @@ extern "C" {
 #define PW_VERSION_LINK		3
 struct pw_link;
 
+#ifndef PW_API_LINK_IMPL
+#define PW_API_LINK_IMPL static inline
+#endif
+
+
 /** \enum pw_link_state The different link states */
 enum pw_link_state {
 	PW_LINK_STATE_ERROR = -2,	/**< the link is in error */
@@ -108,16 +113,17 @@ struct pw_link_methods {
 			void *data);
 };
 
-#define pw_link_method(o,method,version,...)				\
-({									\
-	int _res = -ENOTSUP;						\
-	spa_interface_call_res((struct spa_interface*)o,		\
-			struct pw_link_methods, _res,			\
-			method, version, ##__VA_ARGS__);		\
-	_res;								\
-})
-
-#define pw_link_add_listener(c,...)		pw_link_method(c,add_listener,0,__VA_ARGS__)
+/** \copydoc pw_link_methods.add_listener
+ * \sa pw_link_methods.add_listener */
+PW_API_LINK_IMPL int pw_link_add_listener(struct pw_link *object,
+			struct spa_hook *listener,
+			const struct pw_link_events *events,
+			void *data)
+{
+	return spa_api_method_r(int, -ENOTSUP,
+			pw_link, (struct spa_interface*)object, add_listener, 0,
+			listener, events, data);
+}
 
 /**
  * \}
diff --git a/src/pipewire/loop.c b/src/pipewire/loop.c
index 64baaa7f..2e7fb47d 100644
--- a/src/pipewire/loop.c
+++ b/src/pipewire/loop.c
@@ -8,6 +8,7 @@
 #include <spa/utils/names.h>
 #include <spa/utils/result.h>
 
+#define PW_API_LOOP_IMPL	SPA_EXPORT
 #include <pipewire/pipewire.h>
 #include <pipewire/private.h>
 #include <pipewire/loop.h>
diff --git a/src/pipewire/loop.h b/src/pipewire/loop.h
index 2ec26f5e..9de1cbf3 100644
--- a/src/pipewire/loop.h
+++ b/src/pipewire/loop.h
@@ -34,6 +34,10 @@ struct pw_loop {
 	const char *name;
 };
 
+#ifndef PW_API_LOOP_IMPL
+#define PW_API_LOOP_IMPL static inline
+#endif
+
 struct pw_loop *
 pw_loop_new(const struct spa_dict *props);
 
@@ -42,27 +46,103 @@ pw_loop_destroy(struct pw_loop *loop);
 
 int pw_loop_set_name(struct pw_loop *loop, const char *name);
 
-#define pw_loop_add_source(l,...)	spa_loop_add_source((l)->loop,__VA_ARGS__)
-#define pw_loop_update_source(l,...)	spa_loop_update_source((l)->loop,__VA_ARGS__)
-#define pw_loop_remove_source(l,...)	spa_loop_remove_source((l)->loop,__VA_ARGS__)
-#define pw_loop_invoke(l,...)		spa_loop_invoke((l)->loop,__VA_ARGS__)
-
-#define pw_loop_get_fd(l)		spa_loop_control_get_fd((l)->control)
-#define pw_loop_add_hook(l,...)		spa_loop_control_add_hook((l)->control,__VA_ARGS__)
-#define pw_loop_enter(l)		spa_loop_control_enter((l)->control)
-#define pw_loop_leave(l)		spa_loop_control_leave((l)->control)
-#define pw_loop_iterate(l,...)		spa_loop_control_iterate_fast((l)->control,__VA_ARGS__)
-
-#define pw_loop_add_io(l,...)		spa_loop_utils_add_io((l)->utils,__VA_ARGS__)
-#define pw_loop_update_io(l,...)	spa_loop_utils_update_io((l)->utils,__VA_ARGS__)
-#define pw_loop_add_idle(l,...)		spa_loop_utils_add_idle((l)->utils,__VA_ARGS__)
-#define pw_loop_enable_idle(l,...)	spa_loop_utils_enable_idle((l)->utils,__VA_ARGS__)
-#define pw_loop_add_event(l,...)	spa_loop_utils_add_event((l)->utils,__VA_ARGS__)
-#define pw_loop_signal_event(l,...)	spa_loop_utils_signal_event((l)->utils,__VA_ARGS__)
-#define pw_loop_add_timer(l,...)	spa_loop_utils_add_timer((l)->utils,__VA_ARGS__)
-#define pw_loop_update_timer(l,...)	spa_loop_utils_update_timer((l)->utils,__VA_ARGS__)
-#define pw_loop_add_signal(l,...)	spa_loop_utils_add_signal((l)->utils,__VA_ARGS__)
-#define pw_loop_destroy_source(l,...)	spa_loop_utils_destroy_source((l)->utils,__VA_ARGS__)
+PW_API_LOOP_IMPL int pw_loop_add_source(struct pw_loop *object, struct spa_source *source)
+{
+	return spa_loop_add_source(object->loop, source);
+}
+PW_API_LOOP_IMPL int pw_loop_update_source(struct pw_loop *object, struct spa_source *source)
+{
+	return spa_loop_update_source(object->loop, source);
+}
+PW_API_LOOP_IMPL int pw_loop_remove_source(struct pw_loop *object, struct spa_source *source)
+{
+	return spa_loop_remove_source(object->loop, source);
+}
+PW_API_LOOP_IMPL int pw_loop_invoke(struct pw_loop *object,
+                spa_invoke_func_t func, uint32_t seq, const void *data,
+                size_t size, bool block, void *user_data)
+{
+	return spa_loop_invoke(object->loop, func, seq, data, size, block, user_data);
+}
+
+PW_API_LOOP_IMPL int pw_loop_get_fd(struct pw_loop *object)
+{
+	return spa_loop_control_get_fd(object->control);
+}
+PW_API_LOOP_IMPL void pw_loop_add_hook(struct pw_loop *object,
+                struct spa_hook *hook, const struct spa_loop_control_hooks *hooks,
+                void *data)
+{
+	spa_loop_control_add_hook(object->control, hook, hooks, data);
+}
+PW_API_LOOP_IMPL void pw_loop_enter(struct pw_loop *object)
+{
+	spa_loop_control_enter(object->control);
+}
+PW_API_LOOP_IMPL void pw_loop_leave(struct pw_loop *object)
+{
+	spa_loop_control_leave(object->control);
+}
+PW_API_LOOP_IMPL int pw_loop_iterate(struct pw_loop *object,
+                int timeout)
+{
+	return spa_loop_control_iterate_fast(object->control, timeout);
+}
+
+PW_API_LOOP_IMPL struct spa_source *
+pw_loop_add_io(struct pw_loop *object, int fd, uint32_t mask,
+                bool close, spa_source_io_func_t func, void *data)
+{
+	return spa_loop_utils_add_io(object->utils, fd, mask, close, func, data);
+}
+PW_API_LOOP_IMPL int pw_loop_update_io(struct pw_loop *object,
+                struct spa_source *source, uint32_t mask)
+{
+	return spa_loop_utils_update_io(object->utils, source, mask);
+}
+PW_API_LOOP_IMPL struct spa_source *
+pw_loop_add_idle(struct pw_loop *object, bool enabled,
+                spa_source_idle_func_t func, void *data)
+{
+	return spa_loop_utils_add_idle(object->utils, enabled, func, data);
+}
+PW_API_LOOP_IMPL int pw_loop_enable_idle(struct pw_loop *object,
+                struct spa_source *source, bool enabled)
+{
+	return spa_loop_utils_enable_idle(object->utils, source, enabled);
+}
+PW_API_LOOP_IMPL struct spa_source *
+pw_loop_add_event(struct pw_loop *object, spa_source_event_func_t func, void *data)
+{
+	return spa_loop_utils_add_event(object->utils, func, data);
+}
+PW_API_LOOP_IMPL int pw_loop_signal_event(struct pw_loop *object,
+                struct spa_source *source)
+{
+	return spa_loop_utils_signal_event(object->utils, source);
+}
+PW_API_LOOP_IMPL struct spa_source *
+pw_loop_add_timer(struct pw_loop *object, spa_source_timer_func_t func, void *data)
+{
+	return spa_loop_utils_add_timer(object->utils, func, data);
+}
+PW_API_LOOP_IMPL int pw_loop_update_timer(struct pw_loop *object,
+                struct spa_source *source, struct timespec *value,
+                struct timespec *interval, bool absolute)
+{
+	return spa_loop_utils_update_timer(object->utils, source, value, interval, absolute);
+}
+PW_API_LOOP_IMPL struct spa_source *
+pw_loop_add_signal(struct pw_loop *object, int signal_number,
+                spa_source_signal_func_t func, void *data)
+{
+	return spa_loop_utils_add_signal(object->utils, signal_number, func, data);
+}
+PW_API_LOOP_IMPL void pw_loop_destroy_source(struct pw_loop *object,
+                struct spa_source *source)
+{
+	return spa_loop_utils_destroy_source(object->utils, source);
+}
 
 /**
  * \}
diff --git a/src/pipewire/map.h b/src/pipewire/map.h
index 39d6d084..fb00ddf6 100644
--- a/src/pipewire/map.h
+++ b/src/pipewire/map.h
@@ -15,6 +15,10 @@ extern "C" {
 #include <spa/utils/defs.h>
 #include <pipewire/array.h>
 
+#ifndef PW_API_MAP
+#define PW_API_MAP static inline
+#endif
+
 /** \defgroup pw_map Map
  *
  * \brief A map that holds pointers to objects indexed by id
@@ -93,7 +97,7 @@ struct pw_map {
  * \param size the initial size of the map
  * \param extend the amount to bytes to grow the map with when needed
  */
-static inline void pw_map_init(struct pw_map *map, size_t size, size_t extend)
+PW_API_MAP void pw_map_init(struct pw_map *map, size_t size, size_t extend)
 {
 	pw_array_init(&map->items, extend * sizeof(union pw_map_item));
 	pw_array_ensure_size(&map->items, size * sizeof(union pw_map_item));
@@ -103,7 +107,7 @@ static inline void pw_map_init(struct pw_map *map, size_t size, size_t extend)
 /** Clear a map and free the data storage. All previously returned ids
  * must be treated as invalid.
  */
-static inline void pw_map_clear(struct pw_map *map)
+PW_API_MAP void pw_map_clear(struct pw_map *map)
 {
 	pw_array_clear(&map->items);
 }
@@ -111,7 +115,7 @@ static inline void pw_map_clear(struct pw_map *map)
 /** Reset a map but keep previously allocated storage. All previously
  * returned ids must be treated as invalid.
  */
-static inline void pw_map_reset(struct pw_map *map)
+PW_API_MAP void pw_map_reset(struct pw_map *map)
 {
 	pw_array_reset(&map->items);
 	map->free_list = SPA_ID_INVALID;
@@ -123,7 +127,7 @@ static inline void pw_map_reset(struct pw_map *map)
  * \return the id where the item was inserted or SPA_ID_INVALID when the
  *	item can not be inserted.
  */
-static inline uint32_t pw_map_insert_new(struct pw_map *map, void *data)
+PW_API_MAP uint32_t pw_map_insert_new(struct pw_map *map, void *data)
 {
 	union pw_map_item *start, *item;
 	uint32_t id;
@@ -150,7 +154,7 @@ static inline uint32_t pw_map_insert_new(struct pw_map *map, void *data)
  * \param data the data to insert
  * \return 0 on success, -ENOSPC value when the index is invalid or a negative errno
  */
-static inline int pw_map_insert_at(struct pw_map *map, uint32_t id, void *data)
+PW_API_MAP int pw_map_insert_at(struct pw_map *map, uint32_t id, void *data)
 {
 	size_t size = pw_map_get_size(map);
 	union pw_map_item *item;
@@ -175,7 +179,7 @@ static inline int pw_map_insert_at(struct pw_map *map, uint32_t id, void *data)
  * \param map the map to remove from
  * \param id the index to remove
  */
-static inline void pw_map_remove(struct pw_map *map, uint32_t id)
+PW_API_MAP void pw_map_remove(struct pw_map *map, uint32_t id)
 {
 	if (pw_map_id_is_free(map, id))
 		return;
@@ -189,7 +193,7 @@ static inline void pw_map_remove(struct pw_map *map, uint32_t id)
  * \param id the index to look at
  * \return the item at \a id or NULL when no such item exists
  */
-static inline void *pw_map_lookup(const struct pw_map *map, uint32_t id)
+PW_API_MAP void *pw_map_lookup(const struct pw_map *map, uint32_t id)
 {
 	if (SPA_LIKELY(pw_map_check_id(map, id))) {
 		union pw_map_item *item = pw_map_get_item(map, id);
@@ -207,7 +211,7 @@ static inline void *pw_map_lookup(const struct pw_map *map, uint32_t id)
  * \param data data to pass to \a func
  * \return the result of the last call to \a func or 0 when all callbacks returned 0.
  */
-static inline int pw_map_for_each(const struct pw_map *map,
+PW_API_MAP int pw_map_for_each(const struct pw_map *map,
 				  int (*func) (void *item_data, void *data), void *data)
 {
 	union pw_map_item *item;
diff --git a/src/pipewire/mem.c b/src/pipewire/mem.c
index 5a2bcc8a..f5cc198f 100644
--- a/src/pipewire/mem.c
+++ b/src/pipewire/mem.c
@@ -18,6 +18,7 @@
 #include <spa/utils/list.h>
 #include <spa/buffer/buffer.h>
 
+#define PW_API_MEM SPA_EXPORT
 #include <pipewire/log.h>
 #include <pipewire/map.h>
 #include <pipewire/mem.h>
diff --git a/src/pipewire/mem.h b/src/pipewire/mem.h
index 8ce57bd9..525e5852 100644
--- a/src/pipewire/mem.h
+++ b/src/pipewire/mem.h
@@ -11,6 +11,10 @@
 extern "C" {
 #endif
 
+#ifndef PW_API_MEM
+#define PW_API_MEM static inline
+#endif
+
 /** \defgroup pw_memblock Memory Blocks
  * Memory allocation and pools.
  */
@@ -123,7 +127,7 @@ struct pw_memblock * pw_mempool_import(struct pw_mempool *pool,
 void pw_memblock_free(struct pw_memblock *mem);
 
 /** Unref a memblock */
-static inline void pw_memblock_unref(struct pw_memblock *mem)
+PW_API_MEM void pw_memblock_unref(struct pw_memblock *mem)
 {
 	if (--mem->ref == 0)
 		pw_memblock_free(mem);
@@ -173,7 +177,7 @@ struct pw_map_range {
 
 /** Calculate parameters to mmap() memory into \a range so that
  * \a size bytes at \a offset can be mapped with mmap().  */
-static inline void pw_map_range_init(struct pw_map_range *range,
+PW_API_MEM void pw_map_range_init(struct pw_map_range *range,
 				     uint32_t offset, uint32_t size,
 				     uint32_t page_size)
 {
diff --git a/src/pipewire/module.h b/src/pipewire/module.h
index bd4eca61..f5531af8 100644
--- a/src/pipewire/module.h
+++ b/src/pipewire/module.h
@@ -29,6 +29,10 @@ extern "C" {
 #define PW_VERSION_MODULE		3
 struct pw_module;
 
+#ifndef PW_API_MODULE_IMPL
+#define PW_API_MODULE_IMPL static inline
+#endif
+
 /** The module information. Extra information can be added in later versions */
 struct pw_module_info {
 	uint32_t id;		/**< id of the global */
@@ -81,16 +85,17 @@ struct pw_module_methods {
 			void *data);
 };
 
-#define pw_module_method(o,method,version,...)				\
-({									\
-	int _res = -ENOTSUP;						\
-	spa_interface_call_res((struct spa_interface*)o,		\
-			struct pw_module_methods, _res,			\
-			method, version, ##__VA_ARGS__);		\
-	_res;								\
-})
-
-#define pw_module_add_listener(c,...)	pw_module_method(c,add_listener,0,__VA_ARGS__)
+/** \copydoc pw_module_methods.add_listener
+ * \sa pw_module_methods.add_listener */
+PW_API_MODULE_IMPL int pw_module_add_listener(struct pw_module *object,
+			struct spa_hook *listener,
+			const struct pw_module_events *events,
+			void *data)
+{
+	return spa_api_method_r(int, -ENOTSUP,
+			pw_module, (struct spa_interface*)object, add_listener, 0,
+			listener, events, data);
+}
 
 /**
  * \}
diff --git a/src/pipewire/node.h b/src/pipewire/node.h
index 87ba1a06..1ee9d212 100644
--- a/src/pipewire/node.h
+++ b/src/pipewire/node.h
@@ -34,6 +34,10 @@ extern "C" {
 #define PW_VERSION_NODE		3
 struct pw_node;
 
+#ifndef PW_API_NODE_IMPL
+#define PW_API_NODE_IMPL static inline
+#endif
+
 /** \enum pw_node_state The different node states */
 enum pw_node_state {
 	PW_NODE_STATE_ERROR = -1,	/**< error state */
@@ -179,21 +183,52 @@ struct pw_node_methods {
 	int (*send_command) (void *object, const struct spa_command *command);
 };
 
-#define pw_node_method(o,method,version,...)				\
-({									\
-	int _res = -ENOTSUP;						\
-	spa_interface_call_res((struct spa_interface*)o,		\
-			struct pw_node_methods, _res,			\
-			method, version, ##__VA_ARGS__);		\
-	_res;								\
-})
-
-/** Node */
-#define pw_node_add_listener(c,...)	pw_node_method(c,add_listener,0,__VA_ARGS__)
-#define pw_node_subscribe_params(c,...)	pw_node_method(c,subscribe_params,0,__VA_ARGS__)
-#define pw_node_enum_params(c,...)	pw_node_method(c,enum_params,0,__VA_ARGS__)
-#define pw_node_set_param(c,...)	pw_node_method(c,set_param,0,__VA_ARGS__)
-#define pw_node_send_command(c,...)	pw_node_method(c,send_command,0,__VA_ARGS__)
+
+/** \copydoc pw_node_methods.add_listener
+ * \sa pw_node_methods.add_listener */
+PW_API_NODE_IMPL int pw_node_add_listener(struct pw_node *object,
+			struct spa_hook *listener,
+			const struct pw_node_events *events,
+			void *data)
+{
+	return spa_api_method_r(int, -ENOTSUP,
+			pw_node, (struct spa_interface*)object, add_listener, 0,
+			listener, events, data);
+}
+/** \copydoc pw_node_methods.subscribe_params
+ * \sa pw_node_methods.subscribe_params */
+PW_API_NODE_IMPL int pw_node_subscribe_params(struct pw_node *object, uint32_t *ids, uint32_t n_ids)
+{
+	return spa_api_method_r(int, -ENOTSUP,
+			pw_node, (struct spa_interface*)object, subscribe_params, 0,
+			ids, n_ids);
+}
+/** \copydoc pw_node_methods.enum_params
+ * \sa pw_node_methods.enum_params */
+PW_API_NODE_IMPL int pw_node_enum_params(struct pw_node *object,
+		int seq, uint32_t id, uint32_t start, uint32_t num,
+			    const struct spa_pod *filter)
+{
+	return spa_api_method_r(int, -ENOTSUP,
+			pw_node, (struct spa_interface*)object, enum_params, 0,
+			seq, id, start, num, filter);
+}
+/** \copydoc pw_node_methods.set_param
+ * \sa pw_node_methods.set_param */
+PW_API_NODE_IMPL int pw_node_set_param(struct pw_node *object, uint32_t id, uint32_t flags,
+			  const struct spa_pod *param)
+{
+	return spa_api_method_r(int, -ENOTSUP,
+			pw_node, (struct spa_interface*)object, set_param, 0,
+			id, flags, param);
+}
+/** \copydoc pw_node_methods.send_command
+ * \sa pw_node_methods.send_command */
+PW_API_NODE_IMPL int pw_node_send_command(struct pw_node *object, const struct spa_command *command)
+{
+	return spa_api_method_r(int, -ENOTSUP,
+			pw_node, (struct spa_interface*)object, send_command, 0, command);
+}
 
 /**
  * \}
diff --git a/src/pipewire/pipewire.c b/src/pipewire/pipewire.c
index 74545d61..4451fa52 100644
--- a/src/pipewire/pipewire.c
+++ b/src/pipewire/pipewire.c
@@ -533,7 +533,10 @@ void pw_init(int *argc, char **argv[])
 				str = "true";
 			items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_LOG_COLORS, str);
 		}
-		items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_LOG_TIMESTAMP, "true");
+		if ((str = getenv("PIPEWIRE_LOG_TIMESTAMP")) != NULL)
+			items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_LOG_TIMESTAMP, str);
+		else
+			items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_LOG_TIMESTAMP, "local");
 		if ((str = getenv("PIPEWIRE_LOG_LINE")) == NULL || spa_atob(str))
 			items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_LOG_LINE, "true");
 		snprintf(level, sizeof(level), "%d", pw_log_level);
diff --git a/src/pipewire/port.h b/src/pipewire/port.h
index d0ada8b7..ea4cb33b 100644
--- a/src/pipewire/port.h
+++ b/src/pipewire/port.h
@@ -34,6 +34,10 @@ extern "C" {
 #define PW_VERSION_PORT		3
 struct pw_port;
 
+#ifndef PW_API_PORT_IMPL
+#define PW_API_PORT_IMPL static inline
+#endif
+
 /** The direction of a port */
 #define pw_direction spa_direction
 #define PW_DIRECTION_INPUT SPA_DIRECTION_INPUT
@@ -141,18 +145,35 @@ struct pw_port_methods {
 			const struct spa_pod *filter);
 };
 
-#define pw_port_method(o,method,version,...)				\
-({									\
-	int _res = -ENOTSUP;						\
-	spa_interface_call_res((struct spa_interface*)o,		\
-			struct pw_port_methods, _res,			\
-			method, version, ##__VA_ARGS__);		\
-	_res;								\
-})
-
-#define pw_port_add_listener(c,...)	pw_port_method(c,add_listener,0,__VA_ARGS__)
-#define pw_port_subscribe_params(c,...)	pw_port_method(c,subscribe_params,0,__VA_ARGS__)
-#define pw_port_enum_params(c,...)	pw_port_method(c,enum_params,0,__VA_ARGS__)
+/** \copydoc pw_port_methods.add_listener
+ * \sa pw_port_methods.add_listener */
+PW_API_PORT_IMPL int pw_port_add_listener(struct pw_port *object,
+			struct spa_hook *listener,
+			const struct pw_port_events *events,
+			void *data)
+{
+	return spa_api_method_r(int, -ENOTSUP,
+			pw_port, (struct spa_interface*)object, add_listener, 0,
+			listener, events, data);
+}
+/** \copydoc pw_port_methods.subscribe_params
+ * \sa pw_port_methods.subscribe_params */
+PW_API_PORT_IMPL int pw_port_subscribe_params(struct pw_port *object, uint32_t *ids, uint32_t n_ids)
+{
+	return spa_api_method_r(int, -ENOTSUP,
+			pw_port, (struct spa_interface*)object, subscribe_params, 0,
+			ids, n_ids);
+}
+/** \copydoc pw_port_methods.enum_params
+ * \sa pw_port_methods.enum_params */
+PW_API_PORT_IMPL int pw_port_enum_params(struct pw_port *object,
+		int seq, uint32_t id, uint32_t start, uint32_t num,
+			    const struct spa_pod *filter)
+{
+	return spa_api_method_r(int, -ENOTSUP,
+			pw_port, (struct spa_interface*)object, enum_params, 0,
+			seq, id, start, num, filter);
+}
 
 /**
  * \}
diff --git a/src/pipewire/private.h b/src/pipewire/private.h
index 5a7bb42b..cddd3b82 100644
--- a/src/pipewire/private.h
+++ b/src/pipewire/private.h
@@ -539,7 +539,7 @@ struct pw_node_target {
 	struct pw_node_activation *activation;
 	struct spa_system *system;
 	int fd;
-	void (*trigger)(struct pw_node_target *t, uint64_t nsec);
+	int (*trigger)(struct pw_node_target *t, uint64_t nsec);
 	unsigned int active:1;
 	unsigned int added:1;
 };
@@ -593,10 +593,12 @@ struct pw_node_activation {
 
 	struct pw_node_activation_state state[2];	/* one current state and one next state,
 							 * as version flag */
-	uint64_t signal_time;
-	uint64_t awake_time;
-	uint64_t finish_time;
-	uint64_t prev_signal_time;
+	uint64_t signal_time;                           /* time at which the node was triggered (i.e. marked
+							 * as ready to start processing in the current loop
+							 * iteration) */
+	uint64_t awake_time;                            /* time at which processing actually started */
+	uint64_t finish_time;                           /* time at which processing was completed */
+	uint64_t prev_signal_time;                      /* previous time at which the node was triggered */
 
 	/* updates */
 	struct spa_io_segment reposition;		/* reposition info, used when driver reposition_owner
@@ -619,6 +621,7 @@ struct pw_node_activation {
 	uint32_t driver_id;				/* the current node driver id */
 #define PW_NODE_ACTIVATION_FLAG_NONE		0
 #define PW_NODE_ACTIVATION_FLAG_PROFILER	(1<<0)	/* the profiler is running */
+#define PW_NODE_ACTIVATION_FLAG_ASYNC		(1<<1)	/* the node is async */
 	uint32_t flags;					/* extra flags */
 	struct spa_io_position position;		/* contains current position and segment info.
 							 * extra info is updated by nodes that have set
@@ -653,44 +656,53 @@ static inline uint64_t get_time_ns(struct spa_system *system)
 
 /* called from data-loop decrement the dependency counter of the target and when
  * there are no more dependencies, trigger the node. */
-static inline void trigger_target_v1(struct pw_node_target *t, uint64_t nsec)
+static inline int trigger_target_v1(struct pw_node_target *t, uint64_t nsec)
 {
 	struct pw_node_activation *a = t->activation;
 	struct pw_node_activation_state *state = &a->state[0];
 	int32_t pending = SPA_ATOMIC_DEC(state->pending);
+	int res = pending == 0, r;
 
 	pw_log_trace_fp("%p: (%s-%u) state:%p pending:%d/%d", t->node,
 				t->name, t->id, state, pending, state->required);
 
-	if (pending == 0) {
-		if (SPA_ATOMIC_CAS(a->status,
+	if (res) {
+		if (SPA_LIKELY(SPA_ATOMIC_CAS(a->status,
 					PW_NODE_ACTIVATION_NOT_TRIGGERED,
-					PW_NODE_ACTIVATION_TRIGGERED)) {
+					PW_NODE_ACTIVATION_TRIGGERED))) {
 			a->signal_time = nsec;
-			if (SPA_UNLIKELY(spa_system_eventfd_write(t->system, t->fd, 1) < 0))
-				pw_log_warn("%p: write failed %m", t->node);
+			if (SPA_UNLIKELY((r = spa_system_eventfd_write(t->system, t->fd, 1)) < 0)) {
+				pw_log_warn("%p: write failed %s", t->node, spa_strerror(r));
+				res = r;
+			}
 		} else {
 			pw_log_trace_fp("%p: (%s-%u) not ready %d", t->node,
 					t->name, t->id, a->status);
+			res = -EIO;
 		}
 	}
+	return res;
 }
 
-static inline void trigger_target_v0(struct pw_node_target *t, uint64_t nsec)
+static inline int trigger_target_v0(struct pw_node_target *t, uint64_t nsec)
 {
 	struct pw_node_activation *a = t->activation;
 	struct pw_node_activation_state *state = &a->state[0];
 	int32_t pending = SPA_ATOMIC_DEC(state->pending);
+	int res = pending == 0, r;
 
 	pw_log_trace_fp("%p: (%s-%u) state:%p pending:%d/%d", t->node,
 			t->name, t->id, state, pending, state->required);
 
-	if (pending == 0) {
+	if (res) {
 		SPA_ATOMIC_STORE(a->status, PW_NODE_ACTIVATION_TRIGGERED);
 		a->signal_time = nsec;
-		if (SPA_UNLIKELY(spa_system_eventfd_write(t->system, t->fd, 1) < 0))
-			pw_log_warn("%p: write failed %m", t->node);
+		if (SPA_UNLIKELY((r = spa_system_eventfd_write(t->system, t->fd, 1)) < 0)) {
+			res = r;
+			pw_log_warn("%p: write failed %s", t->node, spa_strerror(r));
+		}
 	}
+	return res;
 }
 
 struct pw_node_peer {
@@ -783,10 +795,11 @@ struct pw_impl_node {
 	unsigned int can_suspend:1;
 	unsigned int checked;		/**< for sorting */
 	unsigned int sync:1;		/**< the sync-groups are active */
-	unsigned int transport:1;	/**< the transport is active */
 	unsigned int async:1;		/**< async processing, one cycle latency */
 	unsigned int lazy:1;		/**< the graph is lazy scheduling */
 
+	uint32_t transport;		/**< latest transport request */
+
 	uint32_t port_user_data_size;	/**< extra size for port user data */
 
 	struct spa_list driver_link;
@@ -844,8 +857,6 @@ struct pw_impl_node {
 	uint64_t driver_start;
 	uint64_t elapsed;		/* elapsed time in playing */
 
-	uint32_t pending_request_process;
-
 	void *user_data;                /**< extra user data */
 };
 
diff --git a/src/pipewire/properties.c b/src/pipewire/properties.c
index c7fa6b78..de81088b 100644
--- a/src/pipewire/properties.c
+++ b/src/pipewire/properties.c
@@ -12,6 +12,7 @@
 #include <spa/utils/result.h>
 #include <spa/debug/log.h>
 
+#define PW_API_PROPERTIES SPA_EXPORT
 #include "pipewire/array.h"
 #include "pipewire/log.h"
 #include "pipewire/utils.h"
@@ -210,39 +211,34 @@ exit_noupdate:
 static int update_string(struct pw_properties *props, const char *str, size_t size,
 		int *count, struct spa_error_location *loc)
 {
-	struct spa_json it[2];
+	struct spa_json it[1];
 	char key[1024];
 	struct spa_error_location el;
 	bool err;
-	int res, cnt = 0;
+	int len, res, cnt = 0;
 	struct properties changes;
+	const char *value;
+
+	if ((res = spa_json_begin_object_relax(&it[0], str, size)) <= 0)
+		return res;
 
 	if (props)
 		properties_init(&changes, 16);
 
-	spa_json_init(&it[0], str, size);
-	if (spa_json_enter_object(&it[0], &it[1]) <= 0)
-		spa_json_init(&it[1], str, size);
-
-	while (spa_json_get_string(&it[1], key, sizeof(key)) > 0) {
-		int len;
-		const char *value;
+	while ((len = spa_json_object_next(&it[0], key, sizeof(key), &value)) > 0) {
 		char *val = NULL;
 
-		if ((len = spa_json_next(&it[1], &value)) <= 0)
-			break;
-
 		if (spa_json_is_null(value, len))
 			val = NULL;
 		else {
 			if (spa_json_is_container(value, len))
-				len = spa_json_container_len(&it[1], value, len);
+				len = spa_json_container_len(&it[0], value, len);
 			if (len <= 0)
 				break;
 
 			if (props) {
 				if ((val = malloc(len+1)) == NULL) {
-					it[1].state = SPA_JSON_ERROR_FLAG;
+					it[0].state = SPA_JSON_ERROR_FLAG;
 					break;
 				}
 				spa_json_parse_stringn(value, len, val, len+1);
@@ -257,12 +253,12 @@ static int update_string(struct pw_properties *props, const char *str, size_t si
 			}
 			/* item changed or added, apply changes later */
 			if ((errno = -add_item(&changes, key, false, val, true) < 0)) {
-				it[1].state = SPA_JSON_ERROR_FLAG;
+				it[0].state = SPA_JSON_ERROR_FLAG;
 				break;
 			}
 		}
 	}
-	if ((err = spa_json_get_error(&it[1], str, &el))) {
+	if ((err = spa_json_get_error(&it[0], str, &el))) {
 		if (loc == NULL)
 			spa_debug_log_error_location(pw_log_get(), SPA_LOG_LEVEL_WARN,
 					&el, "error parsing more than %d properties: %s",
@@ -888,14 +884,12 @@ static int dump(struct dump_config *c, int indent, struct spa_json *it, const ch
 		fprintf(file, "{");
 		spa_json_enter(it, &sub);
 		indent += c->indent;
-		while (spa_json_get_string(&sub, key, sizeof(key)) > 0) {
+		while ((len = spa_json_object_next(&sub, key, sizeof(key), &value)) > 0) {
 			fprintf(file, "%s%s%*s",
 					count++ > 0 ? "," : "",
 					c->sep, indent, "");
 			encode_string(c, KEY(c), key, strlen(key), NORMAL(c));
 			fprintf(file, ": ");
-			if ((len = spa_json_next(&sub, &value)) <= 0)
-				break;
 			dump(c, indent, &sub, value, len);
 		}
 		indent -= c->indent;
diff --git a/src/pipewire/properties.h b/src/pipewire/properties.h
index dec816a8..769198dc 100644
--- a/src/pipewire/properties.h
+++ b/src/pipewire/properties.h
@@ -15,6 +15,10 @@ extern "C" {
 #include <spa/utils/dict.h>
 #include <spa/utils/string.h>
 
+#ifndef PW_API_PROPERTIES
+#define PW_API_PROPERTIES static inline
+#endif
+
 /** \defgroup pw_properties Properties
  *
  * Properties are used to pass around arbitrary key/value pairs.
@@ -101,7 +105,7 @@ pw_properties_fetch_int64(const struct pw_properties *properties, const char *ke
 int
 pw_properties_fetch_bool(const struct pw_properties *properties, const char *key, bool *value);
 
-static inline uint32_t
+PW_API_PROPERTIES uint32_t
 pw_properties_get_uint32(const struct pw_properties *properties, const char *key, uint32_t deflt)
 {
 	uint32_t val = deflt;
@@ -109,7 +113,7 @@ pw_properties_get_uint32(const struct pw_properties *properties, const char *key
 	return val;
 }
 
-static inline int32_t
+PW_API_PROPERTIES int32_t
 pw_properties_get_int32(const struct pw_properties *properties, const char *key, int32_t deflt)
 {
 	int32_t val = deflt;
@@ -117,7 +121,7 @@ pw_properties_get_int32(const struct pw_properties *properties, const char *key,
 	return val;
 }
 
-static inline uint64_t
+PW_API_PROPERTIES uint64_t
 pw_properties_get_uint64(const struct pw_properties *properties, const char *key, uint64_t deflt)
 {
 	uint64_t val = deflt;
@@ -125,7 +129,7 @@ pw_properties_get_uint64(const struct pw_properties *properties, const char *key
 	return val;
 }
 
-static inline int64_t
+PW_API_PROPERTIES int64_t
 pw_properties_get_int64(const struct pw_properties *properties, const char *key, int64_t deflt)
 {
 	int64_t val = deflt;
@@ -134,7 +138,7 @@ pw_properties_get_int64(const struct pw_properties *properties, const char *key,
 }
 
 
-static inline bool
+PW_API_PROPERTIES bool
 pw_properties_get_bool(const struct pw_properties *properties, const char *key, bool deflt)
 {
 	bool val = deflt;
@@ -152,31 +156,31 @@ pw_properties_iterate(const struct pw_properties *properties, void **state);
 #define PW_PROPERTIES_FLAG_COLORS	(1<<4)
 int pw_properties_serialize_dict(FILE *f, const struct spa_dict *dict, uint32_t flags);
 
-static inline bool pw_properties_parse_bool(const char *value) {
+PW_API_PROPERTIES bool pw_properties_parse_bool(const char *value) {
 	return spa_atob(value);
 }
 
-static inline int pw_properties_parse_int(const char *value) {
+PW_API_PROPERTIES int pw_properties_parse_int(const char *value) {
 	int v;
 	return spa_atoi32(value, &v, 0) ? v: 0;
 }
 
-static inline int64_t pw_properties_parse_int64(const char *value) {
+PW_API_PROPERTIES int64_t pw_properties_parse_int64(const char *value) {
 	int64_t v;
 	return spa_atoi64(value, &v, 0) ? v : 0;
 }
 
-static inline uint64_t pw_properties_parse_uint64(const char *value) {
+PW_API_PROPERTIES uint64_t pw_properties_parse_uint64(const char *value) {
 	uint64_t v;
 	return spa_atou64(value, &v, 0) ? v : 0;
 }
 
-static inline float pw_properties_parse_float(const char *value) {
+PW_API_PROPERTIES float pw_properties_parse_float(const char *value) {
 	float v;
 	return spa_atof(value, &v) ? v : 0.0f;
 }
 
-static inline double pw_properties_parse_double(const char *value) {
+PW_API_PROPERTIES double pw_properties_parse_double(const char *value) {
 	double v;
 	return spa_atod(value, &v) ? v : 0.0;
 }
diff --git a/src/pipewire/settings.c b/src/pipewire/settings.c
index 350efc69..597e686a 100644
--- a/src/pipewire/settings.c
+++ b/src/pipewire/settings.c
@@ -90,14 +90,13 @@ static bool uint32_array_contains(uint32_t *vals, uint32_t n_vals, uint32_t val)
 static uint32_t parse_uint32_array(const char *str, uint32_t *vals, uint32_t max, uint32_t def)
 {
 	uint32_t count = 0, r;
-	struct spa_json it[2];
+	struct spa_json it[1];
 	char v[256];
 
-	spa_json_init(&it[0], str, strlen(str));
-	if (spa_json_enter_array(&it[0], &it[1]) <= 0)
-		spa_json_init(&it[1], str, strlen(str));
+	if (spa_json_begin_array_relax(&it[0], str, strlen(str)) <= 0)
+		return 0;
 
-	while (spa_json_get_string(&it[1], v, sizeof(v)) > 0 &&
+	while (spa_json_get_string(&it[0], v, sizeof(v)) > 0 &&
 	    count < max) {
 		if (spa_atou32(v, &r, 0))
 	                vals[count++] = r;
diff --git a/src/pipewire/stream.c b/src/pipewire/stream.c
index a4f33ebc..5e5aed3b 100644
--- a/src/pipewire/stream.c
+++ b/src/pipewire/stream.c
@@ -115,11 +115,12 @@ struct stream {
 
 	uint64_t change_mask_all;
 	struct spa_node_info info;
-#define NODE_PropInfo	0
-#define NODE_Props	1
-#define NODE_EnumFormat	2
-#define NODE_Format	3
-#define N_NODE_PARAMS	4
+#define NODE_PropInfo		0
+#define NODE_Props		1
+#define NODE_EnumFormat		2
+#define NODE_Format		3
+#define NODE_ProcessLatency	4
+#define N_NODE_PARAMS		5
 	struct spa_param_info params[N_NODE_PARAMS];
 
 	uint32_t media_type;
@@ -153,6 +154,7 @@ struct stream {
 	unsigned int trigger_done_rt:1;
 	int in_set_param;
 	int in_emit_param_changed;
+	int pending_drain;
 };
 
 static int get_param_index(uint32_t id)
@@ -166,6 +168,8 @@ static int get_param_index(uint32_t id)
 		return NODE_EnumFormat;
 	case SPA_PARAM_Format:
 		return NODE_Format;
+	case SPA_PARAM_ProcessLatency:
+		return NODE_ProcessLatency;
 	default:
 		return -1;
 	}
@@ -245,10 +249,13 @@ static int add_param(struct stream *impl,
 	memcpy(p->param, param, SPA_POD_SIZE(param));
 	SPA_POD_OBJECT_ID(p->param) = id;
 
-	if (id == SPA_PARAM_Buffers &&
-	    SPA_FLAG_IS_SET(impl->flags, PW_STREAM_FLAG_MAP_BUFFERS) &&
-	    impl->direction == SPA_DIRECTION_INPUT)
-		fix_datatype(p->param);
+	switch (id) {
+	case SPA_PARAM_Buffers:
+		if (impl->direction == SPA_DIRECTION_INPUT &&
+		    SPA_FLAG_IS_SET(impl->flags, PW_STREAM_FLAG_MAP_BUFFERS))
+			fix_datatype(p->param);
+		break;
+	}
 
 	spa_list_append(&impl->param_list, &p->link);
 
@@ -306,7 +313,7 @@ static void clear_params(struct stream *impl, uint32_t id)
 	}
 }
 
-static int update_params(struct stream *impl, uint32_t id,
+static int update_params(struct stream *impl, uint32_t id, uint32_t flags,
 		const struct spa_pod **params, uint32_t n_params)
 {
 	uint32_t i;
@@ -322,7 +329,7 @@ static int update_params(struct stream *impl, uint32_t id,
 		}
 	}
 	for (i = 0; i < n_params; i++) {
-		if ((res = add_param(impl, id, 0, params[i])) < 0)
+		if ((res = add_param(impl, id, flags, params[i])) < 0)
 			break;
 	}
 	return res;
@@ -393,8 +400,10 @@ static bool stream_set_state(struct pw_stream *stream, enum pw_stream_state stat
 			     pw_stream_state_as_string(old),
 			     pw_stream_state_as_string(state), res, stream->error);
 
-		if (state == PW_STREAM_STATE_ERROR)
+		if (state == PW_STREAM_STATE_ERROR) {
 			pw_log_error("%p: error (%d) %s", stream, res, error);
+			errno = -res;
+		}
 
 		stream->state = state;
 		pw_stream_emit_state_changed(stream, old, state, error);
@@ -449,14 +458,19 @@ do_call_drained(struct spa_loop *loop,
 	struct pw_stream *stream = &impl->this;
 	pw_log_trace_fp("%p: drained", stream);
 	pw_stream_emit_drained(stream);
+	SPA_ATOMIC_DEC(impl->pending_drain);
 	return 0;
 }
 
 static void call_drained(struct stream *impl)
 {
 	pw_log_info("%p: drained", impl);
-	pw_loop_invoke(impl->main_loop,
-		do_call_drained, 1, NULL, 0, false, impl);
+	if (SPA_ATOMIC_INC(impl->pending_drain) == 1) {
+		pw_loop_invoke(impl->main_loop,
+			do_call_drained, 1, NULL, 0, false, impl);
+	} else {
+		SPA_ATOMIC_DEC(impl->pending_drain);
+	}
 }
 
 static int
@@ -601,8 +615,13 @@ static int impl_set_param(void *object, uint32_t id, uint32_t flags, const struc
 	struct stream *impl = object;
 	struct pw_stream *stream = &impl->this;
 
-	if (id != SPA_PARAM_Props)
+	switch (id) {
+	case SPA_PARAM_Props:
+	case SPA_PARAM_ProcessLatency:
+		break;
+	default:
 		return -ENOTSUP;
+	}
 
 	if (impl->in_set_param == 0)
 		emit_param_changed(impl, id, param);
@@ -627,7 +646,19 @@ static inline void copy_position(struct stream *impl, int64_t queued)
 			impl->base_pos = p->clock.position - impl->time.ticks;
 			impl->clock_id = p->clock.id;
 		}
-		impl->time.ticks = p->clock.position - impl->base_pos;
+		if (SPA_FLAG_IS_SET(p->clock.flags, SPA_IO_CLOCK_FLAG_NO_RATE)) {
+			if (p->clock.rate.num == 0 || p->clock.rate.denom == 0) {
+				impl->time.ticks = p->clock.nsec;
+				impl->time.rate.num = 1;
+				impl->time.rate.denom = SPA_NSEC_PER_SEC;
+			} else {
+				impl->time.ticks = (p->clock.nsec * p->clock.rate.denom) /
+					(SPA_NSEC_PER_SEC * p->clock.rate.num);
+			}
+		} else {
+			impl->time.ticks = p->clock.position - impl->base_pos;
+		}
+
 		impl->time.delay = 0;
 		impl->time.queued = queued;
 		impl->quantum = p->clock.duration;
@@ -819,8 +850,14 @@ static void clear_buffers(struct pw_stream *stream)
 			if (b->busy)
 				SPA_ATOMIC_DEC(b->busy->count);
 		}
-	} else
+	} else {
 		clear_queue(impl, &impl->dequeued);
+		struct spa_io_buffers *io = impl->io;
+		if (io && io->status == SPA_STATUS_HAVE_DATA) {
+			io->buffer_id = SPA_ID_INVALID;
+			io->status = SPA_STATUS_OK;
+		}
+	}
 	clear_queue(impl, &impl->queued);
 }
 
@@ -870,7 +907,7 @@ static int impl_port_set_param(void *object,
 	if (param)
 		pw_log_pod(SPA_LOG_LEVEL_DEBUG, param);
 
-	if ((res = update_params(impl, id, &param, param ? 1 : 0)) < 0)
+	if ((res = update_params(impl, id, 0, &param, param ? 1 : 0)) < 0)
 		return res;
 
 	switch (id) {
@@ -1134,11 +1171,14 @@ static void proxy_destroy(void *_data)
 static void proxy_error(void *_data, int seq, int res, const char *message)
 {
 	struct pw_stream *stream = _data;
+	int old_errno = errno;
 	/* we just emit the state change here to inform the application.
 	 * If this is supposed to be a permanent error, the app should
 	 * do a pw_stream_set_error() */
+	errno = -res;
 	pw_stream_emit_state_changed(stream, stream->state,
 			PW_STREAM_STATE_ERROR, message);
+	errno = old_errno;
 }
 
 static void proxy_bound_props(void *data, uint32_t global_id, const struct spa_dict *props)
@@ -1737,6 +1777,8 @@ enum pw_stream_state pw_stream_get_state(struct pw_stream *stream, const char **
 {
 	if (error)
 		*error = stream->error;
+	if (stream->state == PW_STREAM_STATE_ERROR)
+		errno = -stream->error_res;
 	return stream->state;
 }
 
@@ -1919,6 +1961,7 @@ pw_stream_connect(struct pw_stream *stream,
 	impl->params[NODE_Props] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_WRITE);
 	impl->params[NODE_EnumFormat] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, 0);
 	impl->params[NODE_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+	impl->params[NODE_ProcessLatency] = SPA_PARAM_INFO(SPA_PARAM_ProcessLatency, SPA_PARAM_INFO_READWRITE);
 	impl->info.params = impl->params;
 	impl->info.n_params = N_NODE_PARAMS;
 	impl->info.change_mask = impl->change_mask_all;
@@ -2010,7 +2053,7 @@ pw_stream_connect(struct pw_stream *stream,
 		pw_properties_set(impl->port_props, PW_KEY_FORMAT_DSP, str);
 	else if (impl->media_type == SPA_MEDIA_TYPE_application &&
 	    impl->media_subtype == SPA_MEDIA_SUBTYPE_control)
-		pw_properties_set(impl->port_props, PW_KEY_FORMAT_DSP, "8 bit raw midi");
+		pw_properties_set(impl->port_props, PW_KEY_FORMAT_DSP, "32 bit raw UMP");
 	if (pw_properties_get(impl->port_props, PW_KEY_PORT_GROUP) == NULL)
 		pw_properties_set(impl->port_props, PW_KEY_PORT_GROUP, "stream.0");
 
@@ -2077,9 +2120,8 @@ pw_stream_connect(struct pw_stream *stream,
 	if (pw_properties_get(props, PW_KEY_PORT_GROUP) == NULL)
 		pw_properties_set(props, PW_KEY_PORT_GROUP, "stream.0");
 
-	if (impl->media_type == SPA_MEDIA_TYPE_audio
-			|| (impl->media_type == SPA_MEDIA_TYPE_video
-				&& pw_properties_get(props, "video.adapt.converter"))) {
+	if (impl->media_type == SPA_MEDIA_TYPE_audio ||
+	    impl->media_type == SPA_MEDIA_TYPE_video) {
 		factory = pw_context_find_factory(impl->context, "adapter");
 		if (factory == NULL) {
 			pw_log_error("%p: no adapter factory found", stream);
@@ -2196,7 +2238,7 @@ int pw_stream_update_params(struct pw_stream *stream,
 	ensure_loop(impl->main_loop, return -EIO);
 
 	pw_log_debug("%p: update params", stream);
-	if ((res = update_params(impl, SPA_ID_INVALID, params, n_params)) < 0)
+	if ((res = update_params(impl, SPA_ID_INVALID, 0, params, n_params)) < 0)
 		return res;
 
 	if (impl->in_emit_param_changed == 0) {
@@ -2362,7 +2404,8 @@ int pw_stream_get_time_n(struct pw_stream *stream, struct pw_time *time, size_t
 
 	time->delay += (int64_t)(((impl->latency.min_quantum + impl->latency.max_quantum) / 2.0f) * quantum);
 	time->delay += (impl->latency.min_rate + impl->latency.max_rate) / 2;
-	time->delay += ((impl->latency.min_ns + impl->latency.max_ns) / 2) * time->rate.denom / SPA_NSEC_PER_SEC;
+	time->delay += ((impl->latency.min_ns + impl->latency.max_ns) / 2) *
+		(int64_t)time->rate.denom / (int64_t)SPA_NSEC_PER_SEC;
 
 	avail_buffers = spa_ringbuffer_get_read_index(&impl->dequeued.ring, &index);
 	avail_buffers = SPA_CLAMP(avail_buffers, 0, (int32_t)impl->n_buffers);
@@ -2462,6 +2505,41 @@ int pw_stream_queue_buffer(struct pw_stream *stream, struct pw_buffer *buffer)
 	return res;
 }
 
+static inline int queue_push_front(struct stream *stream, struct queue *queue, struct buffer *buffer)
+{
+	int ret = 0;
+	uint32_t index;
+
+	if ((ret = spa_ringbuffer_get_read_index(&queue->ring, &index)) < 0)
+		return ret;
+
+	/* undo the pop operation and place the buffer in front of the queue */
+	index -= 1;
+	queue->ids[index & MASK_BUFFERS] = buffer->id;
+	queue->outcount -= buffer->this.size;
+	SPA_FLAG_SET(buffer->flags, BUFFER_FLAG_QUEUED);
+	spa_ringbuffer_read_update(&queue->ring, index);
+
+	return ret;
+}
+
+SPA_EXPORT
+int pw_stream_return_buffer(struct pw_stream *stream, struct pw_buffer *buffer)
+{
+	struct stream *impl = SPA_CONTAINER_OF(stream, struct stream, this);
+	struct buffer *b = SPA_CONTAINER_OF(buffer, struct buffer, this);
+
+	pw_log_trace_fp("%p: %p id: %d", impl, buffer, b->id);
+
+	/* dequeue increments the busy count, so undo that */
+	if (b->busy) {
+		SPA_ATOMIC_DEC(b->busy->count);
+		pw_log_trace_fp("%p: %p: %p busy count %u", impl, b, b->busy, SPA_ATOMIC_LOAD(b->busy->count));
+	}
+
+	return queue_push_front(impl, &impl->dequeued, b);
+}
+
 static int
 do_flush(struct spa_loop *loop,
                  bool async, uint32_t seq, const void *data, size_t size, void *user_data)
@@ -2546,20 +2624,23 @@ do_trigger_driver(struct spa_loop *loop,
 	return spa_node_call_ready(&impl->callbacks, res);
 }
 
-static int do_trigger_request_process(struct spa_loop *loop,
+static int do_emit_event(struct spa_loop *loop,
                  bool async, uint32_t seq, const void *data, size_t size, void *user_data)
 {
 	struct stream *impl = user_data;
-	uint8_t buffer[1024];
-	struct spa_pod_builder b = { 0 };
-
-	spa_pod_builder_init(&b, buffer, sizeof(buffer));
-	spa_node_emit_event(&impl->hooks,
-			spa_pod_builder_add_object(&b,
-				SPA_TYPE_EVENT_Node, SPA_NODE_EVENT_RequestProcess));
+	const struct spa_event *event = data;
+	spa_node_emit_event(&impl->hooks, event);
 	return 0;
 }
 
+SPA_EXPORT
+int pw_stream_emit_event(struct pw_stream *stream, const struct spa_event *event)
+{
+	struct stream *impl = SPA_CONTAINER_OF(stream, struct stream, this);
+	return pw_loop_invoke(impl->main_loop,
+		do_emit_event, 1, event, SPA_POD_SIZE(&event->pod), false, impl);
+}
+
 SPA_EXPORT
 int pw_stream_trigger_process(struct pw_stream *stream)
 {
@@ -2572,13 +2653,33 @@ int pw_stream_trigger_process(struct pw_stream *stream)
 	impl->using_trigger = true;
 
 	if (impl->trigger) {
-		pw_impl_node_trigger(stream->node);
+		res = pw_impl_node_trigger(stream->node);
 	} else if (stream->node->driving) {
 		res = pw_loop_invoke(impl->data_loop,
 			do_trigger_driver, 1, NULL, 0, false, impl);
 	} else {
-		res = pw_loop_invoke(impl->main_loop,
-			do_trigger_request_process, 1, NULL, 0, false, impl);
+		pw_stream_emit_event(stream,
+				&SPA_NODE_EVENT_INIT(SPA_NODE_EVENT_RequestProcess));
 	}
 	return res;
 }
+
+SPA_EXPORT
+int pw_stream_set_rate(struct pw_stream *stream, double rate)
+{
+	struct stream *impl = SPA_CONTAINER_OF(stream, struct stream, this);
+	bool enable;
+
+	if (impl->rate_match == NULL)
+		return -ENOTSUP;
+
+	if (rate <= 0.0) {
+		rate = 1.0;
+		enable = false;
+	} else {
+		enable = true;
+	}
+	impl->rate_match->rate = rate;
+	SPA_FLAG_UPDATE(impl->rate_match->flags, SPA_IO_RATE_MATCH_FLAG_ACTIVE, enable);
+	return 0;
+}
diff --git a/src/pipewire/stream.h b/src/pipewire/stream.h
index b1c8c6cd..8e8ba095 100644
--- a/src/pipewire/stream.h
+++ b/src/pipewire/stream.h
@@ -133,8 +133,25 @@ extern "C" {
  * When the buffer has been processed, call \ref pw_stream_queue_buffer()
  * to let PipeWire reuse the buffer.
  *
+ * Although not strictly required, it is recommended to call \ref
+ * pw_stream_dequeue_buffer() and pw_stream_queue_buffer() from the
+ * process() callback to minimize the amount of buffering and
+ * maximize the amount of buffer reuse in the stream.
+ *
+ * It is also possible to dequeue the buffer from the process event,
+ * then process and queue the buffer from a helper thread. It is also
+ * possible to dequeue, process and queue a buffer from a helper thread
+ * after receiving the process event.
+ *
  * \subsection ssec_produce Produce data
  *
+ * The process event is emitted when a new buffer should be queued.
+ *
+ * When the PW_STREAM_FLAG_RT_PROCESS flag was given, this function will be
+ * called from a realtime thread and it is not safe to call non-reatime
+ * functions such as doing file operations, blocking operations or any of
+ * the PipeWire functions that are not explicitly marked as being RT safe.
+ *
  * \ref pw_stream_dequeue_buffer() gives an empty buffer that can be filled.
  *
  * The buffer is owned by the stream and stays alive until the
@@ -142,8 +159,19 @@ extern "C" {
  *
  * Filled buffers should be queued with \ref pw_stream_queue_buffer().
  *
- * The process event is emitted when PipeWire has emptied a buffer that
- * can now be refilled.
+ * Although not strictly required, it is recommended to call \ref
+ * pw_stream_dequeue_buffer() and pw_stream_queue_buffer() from the
+ * process() callback to minimize the amount of buffering and
+ * maximize the amount of buffer reuse in the stream.
+ *
+ * Buffers that are queued after the process event completes will be delayed
+ * to the next processing cycle.
+ *
+ * \section sec_stream_timing Obtaining timing information
+ *
+ * With \ref pw_stream_get_time_n() and pw_stream_get_nsec() on can get accurate
+ * timing information of the data and the graph in the \ref pw_time. See
+ * the documentation of these functions.
  *
  * \section sec_stream_driving Driving the graph
  *
@@ -157,6 +185,26 @@ extern "C" {
  * true. It must then use pw_stream_trigger_process() to start the graph
  * cycle.
  *
+ * \ref pw_stream_trigger_process() will result in a process event, where a buffer
+ * should be dequeued, and queued again. This is the recommended behaviour that
+ * minimizes buffering and maximized buffer reuse.
+ *
+ * Producers of data that drive the graph can also dequeue a buffer in a helper
+ * thread, fill it with data and then call \ref pw_stream_trigger_process() to
+ * start the graph cycle. In the process event they will then queue the filled
+ * buffer and dequeue a new empty buffer to fill again in the helper thread,
+ *
+ * Consumers of data that drive the graph (pull based scheduling) will use
+ * \ref pw_stream_trigger_process() to start the graph and will dequeue, process
+ * and queue the buffers in the process event.
+ *
+ * \section sec_stream_process_requests Request processing
+ *
+ * A stream that is not driving the graph can request a new graph cycle by doing
+ * \ref pw_stream_trigger_process(). This will result in a RequestProcess command
+ * in the driver stream. If the driver supports this, it can then perform
+ * \ref pw_stream_trigger_process() to start the actual graph cycle.
+ *
  * \section sec_stream_disconnect Disconnect
  *
  * Use \ref pw_stream_disconnect() to disconnect a stream after use.
@@ -192,6 +240,7 @@ struct pw_stream;
 #include <spa/buffer/buffer.h>
 #include <spa/param/param.h>
 #include <spa/pod/command.h>
+#include <spa/pod/event.h>
 
 /** \enum pw_stream_state The state of a stream */
 enum pw_stream_state {
@@ -265,6 +314,13 @@ struct pw_stream_control {
  * caused by the hardware. The delay is usually quite stable and should only change when
  * the topology, quantum or samplerate of the graph changes.
  *
+ * The delay requires the application to send the stream early relative to other synchronized
+ * streams in order to arrive at the edge of the graph in time. This is usually done by
+ * delaying the other streams with the given delay.
+ *
+ * Note that the delay can be negative. A negative delay means that this stream should be
+ * delayed with the (positive) delay relative to other streams.
+ *
  * pw_time.queued and pw_time.buffered is expressed in the time domain of the stream,
  * or the format that is used for the buffers of this stream.
  *
@@ -356,7 +412,8 @@ struct pw_stream_events {
 	uint32_t version;
 
 	void (*destroy) (void *data);
-	/** when the stream state changes */
+	/** when the stream state changes. Since 1.4 this also sets errno when the
+	 * new state is PW_STREAM_STATE_ERROR */
 	void (*state_changed) (void *data, enum pw_stream_state old,
 				enum pw_stream_state state, const char *error);
 
@@ -461,6 +518,8 @@ void pw_stream_add_listener(struct pw_stream *stream,
 			    const struct pw_stream_events *events,
 			    void *data);
 
+/** Get the current stream state. Since 1.4 this also sets errno when the
+ * state is PW_STREAM_STATE_ERROR */
 enum pw_stream_state pw_stream_get_state(struct pw_stream *stream, const char **error);
 
 const char *pw_stream_get_name(struct pw_stream *stream);
@@ -529,11 +588,11 @@ const struct pw_stream_control *pw_stream_get_control(struct pw_stream *stream,
 /** Set control values */
 int pw_stream_set_control(struct pw_stream *stream, uint32_t id, uint32_t n_values, float *values, ...);
 
-/** Query the time on the stream */
+/** Query the time on the stream, RT safe */
 int pw_stream_get_time_n(struct pw_stream *stream, struct pw_time *time, size_t size);
 
 /** Get the current time in nanoseconds. This value can be compared with
- * the \ref pw_time.now value. Since 1.1.0 */
+ * the \ref pw_time.now value. RT safe. Since 1.1.0 */
 uint64_t pw_stream_get_nsec(struct pw_stream *stream);
 
 /** Get the data loop that is doing the processing of this stream. This loop
@@ -541,17 +600,21 @@ uint64_t pw_stream_get_nsec(struct pw_stream *stream);
 struct pw_loop *pw_stream_get_data_loop(struct pw_stream *stream);
 
 /** Query the time on the stream, deprecated since 0.3.50,
- * use pw_stream_get_time_n() to get the fields added since 0.3.50. */
+ * use pw_stream_get_time_n() to get the fields added since 0.3.50. RT safe. */
 SPA_DEPRECATED
 int pw_stream_get_time(struct pw_stream *stream, struct pw_time *time);
 
 /** Get a buffer that can be filled for playback streams or consumed
- * for capture streams. */
+ * for capture streams. RT safe. */
 struct pw_buffer *pw_stream_dequeue_buffer(struct pw_stream *stream);
 
-/** Submit a buffer for playback or recycle a buffer for capture. */
+/** Submit a buffer for playback or recycle a buffer for capture. RT safe. */
 int pw_stream_queue_buffer(struct pw_stream *stream, struct pw_buffer *buffer);
 
+/** Return a buffer to the queue without using it. This makes the buffer
+ * immediately available to dequeue again. RT safe. */
+int pw_stream_return_buffer(struct pw_stream *stream, struct pw_buffer *buffer);
+
 /** Activate or deactivate the stream */
 int pw_stream_set_active(struct pw_stream *stream, bool active);
 
@@ -560,7 +623,7 @@ int pw_stream_set_active(struct pw_stream *stream, bool active);
  * after the drain by setting it active again with
  * \ref pw_stream_set_active(). A flush without a drain is mostly useful afer
  * a state change to PAUSED, to flush any remaining data from the queues and
- * the converters. */
+ * the converters. RT safe. */
 int pw_stream_flush(struct pw_stream *stream, bool drain);
 
 /** Check if the stream is driving. The stream needs to have the
@@ -581,7 +644,7 @@ bool pw_stream_is_driving(struct pw_stream *stream);
  *
  * It is not a requirement that all RequestProcess events/commands
  * need to start a graph cycle.
- * Since 1.2.7 */
+ * Since 1.4.0 */
 bool pw_stream_is_lazy(struct pw_stream *stream);
 
 /** Trigger a push/pull on the stream. One iteration of the graph will
@@ -610,9 +673,23 @@ bool pw_stream_is_lazy(struct pw_stream *stream);
  * driver. If the graph is not lazy scheduling and the stream is not a
  * driver, this method will have no effect.
  *
+ * RT safe.
+ *
  * Since 0.3.34 */
 int pw_stream_trigger_process(struct pw_stream *stream);
 
+/** Emit an event from this stream. RT safe.
+ * Since 1.2.6 */
+int pw_stream_emit_event(struct pw_stream *stream, const struct spa_event *event);
+
+/** Adjust the rate of the stream.
+ * When the stream is using an adaptive resampler, adjust the resampler rate.
+ * When there is no resampler, -ENOTSUP is returned. Activating the adaptive
+ * resampler will add a small amount of delay to the samples, you can deactivate
+ * it again by setting a value <= 0.0. RT safe.
+ * Since 1.4.0 */
+int pw_stream_set_rate(struct pw_stream *stream, double rate);
+
 /**
  * \}
  */
diff --git a/src/pipewire/thread-loop.h b/src/pipewire/thread-loop.h
index f1eb1910..2734799d 100644
--- a/src/pipewire/thread-loop.h
+++ b/src/pipewire/thread-loop.h
@@ -61,6 +61,11 @@ extern "C" {
  *
  * All events and callbacks are called with the thread lock held.
  *
+ * An exception to this is for the data processing callbacks, which are
+ * explcitly marked as being emitted from the data realtime threads. One
+ * such callback is the \ref pw_stream::process() callback when the
+ * \ref PW_STREAM_FLAG_RT_PROCESS is set.
+ *
  */
 /** \defgroup pw_thread_loop Thread Loop
  *
diff --git a/src/pipewire/thread.c b/src/pipewire/thread.c
index 125a16a7..defa6a68 100644
--- a/src/pipewire/thread.c
+++ b/src/pipewire/thread.c
@@ -15,6 +15,7 @@
 #include <spa/utils/list.h>
 #include <spa/utils/json.h>
 
+#define PW_API_THREAD_IMPL SPA_EXPORT
 #include <pipewire/log.h>
 #include <pipewire/private.h>
 #include <pipewire/thread.h>
@@ -30,15 +31,14 @@ do {									\
 
 static int parse_affinity(const char *affinity, cpu_set_t *set)
 {
-	struct spa_json it[2];
+	struct spa_json it[1];
 	int v;
 
 	CPU_ZERO(set);
-	spa_json_init(&it[0], affinity, strlen(affinity));
-	if (spa_json_enter_array(&it[0], &it[1]) <= 0)
-		spa_json_init(&it[1], affinity, strlen(affinity));
+	if (spa_json_begin_array_relax(&it[0], affinity, strlen(affinity)) <= 0)
+		return 0;
 
-	while (spa_json_get_int(&it[1], &v) > 0) {
+	while (spa_json_get_int(&it[0], &v) > 0) {
 		if (v >= 0 && v < CPU_SETSIZE)
 			CPU_SET(v, set);
         }
@@ -117,7 +117,7 @@ static struct spa_thread *impl_create(void *object,
 			pw_log_warn("pthread_setname error: %s", strerror(err));
 		if ((str = spa_dict_lookup(props, SPA_KEY_THREAD_AFFINITY)) != NULL &&
 		    (err = thread_setaffinity(pt, str)) != 0)
-			pw_log_warn("pthread_setaffinity error: %s", strerror(err));
+			pw_log_warn("pthread_setaffinity error: %s", strerror(-err));
 	}
 	return (struct spa_thread*)pt;
 }
diff --git a/src/pipewire/thread.h b/src/pipewire/thread.h
index 5ce948dc..f53e6295 100644
--- a/src/pipewire/thread.h
+++ b/src/pipewire/thread.h
@@ -29,11 +29,31 @@ void pw_thread_utils_set(struct spa_thread_utils *impl);
 struct spa_thread_utils *pw_thread_utils_get(void);
 void *pw_thread_fill_attr(const struct spa_dict *props, void *attr);
 
-#define pw_thread_utils_create(...)		spa_thread_utils_create(pw_thread_utils_get(), ##__VA_ARGS__)
-#define pw_thread_utils_join(...)		spa_thread_utils_join(pw_thread_utils_get(), ##__VA_ARGS__)
-#define pw_thread_utils_get_rt_range(...)	spa_thread_utils_get_rt_range(pw_thread_utils_get(), ##__VA_ARGS__)
-#define pw_thread_utils_acquire_rt(...)		spa_thread_utils_acquire_rt(pw_thread_utils_get(), ##__VA_ARGS__)
-#define pw_thread_utils_drop_rt(...)		spa_thread_utils_drop_rt(pw_thread_utils_get(), ##__VA_ARGS__)
+#ifndef PW_API_THREAD_IMPL
+#define PW_API_THREAD_IMPL static inline
+#endif
+
+PW_API_THREAD_IMPL struct spa_thread *pw_thread_utils_create(
+		const struct spa_dict *props, void *(*start_routine)(void*), void *arg)
+{
+	return spa_thread_utils_create(pw_thread_utils_get(), props, start_routine, arg);
+}
+PW_API_THREAD_IMPL int pw_thread_utils_join(struct spa_thread *thread, void **retval)
+{
+	return spa_thread_utils_join(pw_thread_utils_get(), thread, retval);
+}
+PW_API_THREAD_IMPL int pw_thread_utils_get_rt_range(const struct spa_dict *props, int *min, int *max)
+{
+	return spa_thread_utils_get_rt_range(pw_thread_utils_get(), props, min, max);
+}
+PW_API_THREAD_IMPL int pw_thread_utils_acquire_rt(struct spa_thread *thread, int priority)
+{
+	return spa_thread_utils_acquire_rt(pw_thread_utils_get(), thread, priority);
+}
+PW_API_THREAD_IMPL int pw_thread_utils_drop_rt(struct spa_thread *thread)
+{
+	return spa_thread_utils_drop_rt(pw_thread_utils_get(), thread);
+}
 
 /**
  * \}
diff --git a/src/pipewire/utils.c b/src/pipewire/utils.c
index 4db73a04..103058a6 100644
--- a/src/pipewire/utils.c
+++ b/src/pipewire/utils.c
@@ -130,7 +130,7 @@ SPA_EXPORT
 char **pw_strv_parse(const char *val, size_t len, int max_tokens, int *n_tokens)
 {
 	struct pw_array arr;
-	struct spa_json it[2];
+	struct spa_json it[1];
 	int n = 0, l, res;
 	const char *value;
 	struct spa_error_location el;
@@ -140,11 +140,10 @@ char **pw_strv_parse(const char *val, size_t len, int max_tokens, int *n_tokens)
 
 	pw_array_init(&arr, sizeof(char*) * 16);
 
-	spa_json_init(&it[0], val, len);
-        if (spa_json_enter_array(&it[0], &it[1]) <= 0)
-                spa_json_init(&it[1], val, len);
+        if (spa_json_begin_array_relax(&it[0], val, len) <= 0)
+		return NULL;
 
-	while ((l = spa_json_next(&it[1], &value)) > 0 && n + 1 < max_tokens) {
+	while ((l = spa_json_next(&it[0], &value)) > 0 && n + 1 < max_tokens) {
 		char *s;
 
 		if ((s = malloc(l+1)) == NULL)
@@ -161,7 +160,7 @@ char **pw_strv_parse(const char *val, size_t len, int max_tokens, int *n_tokens)
 	if ((res = pw_array_add_ptr(&arr, NULL)) < 0)
 		goto error;
 done:
-	if ((res = spa_json_get_error(&it[1], val, &el))) {
+	if ((res = spa_json_get_error(&it[0], val, &el))) {
 		spa_debug_log_error_location(pw_log_get(), SPA_LOG_LEVEL_WARN,
 				&el, "error parsing strv: %s", el.reason);
 
@@ -179,7 +178,7 @@ done:
 error_errno:
 	res = -errno;
 error:
-	it[1].state = SPA_JSON_ERROR_FLAG;
+	it[0].state = SPA_JSON_ERROR_FLAG;
 	errno = -res;
 	goto done;
 }
diff --git a/src/tests/test-security-context.c b/src/tests/test-security-context.c
index 2ee7a386..a81ed1d1 100644
--- a/src/tests/test-security-context.c
+++ b/src/tests/test-security-context.c
@@ -97,7 +97,7 @@ static void test_create(void)
 	struct registry_info info;
 	struct spa_hook listener;
 	int res, listen_fd, close_fd[2];
-	char temp[PATH_MAX] = "/tmp/pipewire-XXXXXX";
+	char temp[] = "/tmp/pipewire-XXXXXX";
 	struct sockaddr_un sockaddr = {0};
 
 	loop = pw_main_loop_new(NULL);
diff --git a/src/tools/dfffile.c b/src/tools/dfffile.c
index dd40be4b..c2e4db22 100644
--- a/src/tools/dfffile.c
+++ b/src/tools/dfffile.c
@@ -14,22 +14,25 @@
 
 #include "dfffile.h"
 
+#define BLOCKSIZE	8192
+
 struct dff_file {
-	uint8_t *data;
-	size_t size;
+	uint8_t *buffer;
+	size_t blocksize;
+	size_t offset;
 
 	int mode;
-	int fd;
+	bool close;
+	FILE *file;
+	size_t pos;
 
 	struct dff_file_info info;
-
-	uint8_t *p;
-	size_t offset;
 };
 
 struct dff_chunk {
 	uint32_t id;
 	uint64_t size;
+	uint64_t pos;
 	void *data;
 };
 
@@ -57,28 +60,44 @@ static inline uint64_t parse_be64(const uint8_t *in)
 	return res;
 }
 
-static inline int f_avail(struct dff_file *f)
+static inline int f_read(struct dff_file *f, void *data, size_t size)
 {
-	if (f->p < f->data + f->size)
-		return f->size + f->data - f->p;
+	size_t s = fread(data, 1, size, f->file);
+	f->pos += s;
+	if (s < size)
+		return -1;
 	return 0;
 }
 
 static int read_chunk(struct dff_file *f, struct dff_chunk *c)
 {
-	if (f_avail(f) < 12)
+	uint8_t data[12];
+
+	if (f_read(f, data, sizeof(data)) < 0)
 		return -ENOSPC;
 
-	c->id = parse_be32(f->p);	/* id of this chunk */
-	c->size = parse_be64(f->p + 4);	/* size of this chunk */
-	f->p += 12;
-	c->data = f->p;
+	c->id = parse_be32(data);	/* id of this chunk */
+	c->size = parse_be64(data + 4);	/* size of this chunk */
+	c->pos = f->pos;
 	return 0;
 }
 
 static int skip_chunk(struct dff_file *f, const struct dff_chunk *c)
 {
-	f->p = SPA_PTROFF(c->data, c->size, uint8_t);
+	size_t bytes;
+	uint8_t data[256];
+
+	if (c->pos + c->size <= f->pos)
+		return 0;
+
+	bytes = c->size + c->pos - f->pos;
+	while (bytes > 0) {
+		size_t s = fread(data, 1, SPA_MIN(bytes, sizeof(data)), f->file);
+		if (s == 0)
+			break;
+		f->pos += s;
+		bytes -= s;
+	}
 	return 0;
 }
 
@@ -86,22 +105,26 @@ static int read_PROP(struct dff_file *f, struct dff_chunk *prop)
 {
 	struct dff_chunk c[1];
 	int res;
+	uint8_t data[4];
 
-	if (f_avail(f) < 4 ||
-	    memcmp(prop->data, "SND ", 4) != 0)
+	if (f_read(f, data, sizeof(data)) < 0 ||
+	    memcmp(data, "SND ", 4) != 0)
 		return -EINVAL;
-	f->p += 4;
 
-	while (f->p < SPA_PTROFF(prop->data, prop->size, uint8_t)) {
+	while (f->pos < prop->pos + prop->size) {
 		if ((res = read_chunk(f, &c[0])) < 0)
 			return res;
 
 		switch (c[0].id) {
 		case FOURCC('F', 'S', ' ', ' '):
-			f->info.rate = parse_be32(f->p);
+			if (f_read(f, data, 4) < 0)
+				return -EINVAL;
+			f->info.rate = parse_be32(data);
 			break;
 		case FOURCC('C', 'H', 'N', 'L'):
-			f->info.channels = parse_be16(f->p);
+			if (f_read(f, data, 2) < 0)
+				return -EINVAL;
+			f->info.channels = parse_be16(data);
 			switch (f->info.channels) {
 			case 2:
 				f->info.channel_type = 2;
@@ -116,7 +139,9 @@ static int read_PROP(struct dff_file *f, struct dff_chunk *prop)
 			break;
 		case FOURCC('C', 'M', 'P', 'R'):
 		{
-			uint32_t cmpr = parse_be32(f->p);
+			if (f_read(f, data, 4) < 0)
+				return -EINVAL;
+			uint32_t cmpr = parse_be32(data);
 			if (cmpr != FOURCC('D', 'S', 'D', ' '))
 				return -ENOTSUP;
 			break;
@@ -138,15 +163,16 @@ static int read_FRM8(struct dff_file *f)
 	struct dff_chunk c[2];
 	int res;
 	bool found_dsd = false;
+	uint8_t data[4];
 
 	if ((res = read_chunk(f, &c[0])) < 0)
 		return res;
 	if (c[0].id != FOURCC('F','R','M','8'))
 		return -EINVAL;
-	if (f_avail(f) < 4 ||
-	    memcmp(c[0].data, "DSD ", 4) != 0)
+
+	if (f_read(f, data, sizeof(data)) < 0 ||
+	    memcmp(data, "DSD ", 4) != 0)
 		return -EINVAL;
-	f->p += 4;
 
 	while (true) {
 		if ((res = read_chunk(f, &c[1])) < 0)
@@ -181,37 +207,33 @@ static int read_FRM8(struct dff_file *f)
 static int open_read(struct dff_file *f, const char *filename, struct dff_file_info *info)
 {
 	int res;
-	struct stat st;
 
-	if ((f->fd = open(filename, O_RDONLY)) < 0) {
-		res = -errno;
-		goto exit;
+	if (strcmp(filename, "-") != 0) {
+		if ((f->file = fopen(filename, "r")) == NULL) {
+			res = -errno;
+			goto exit;
+		}
+		f->close = true;
+	} else {
+		f->close = false;
+		f->file = stdin;
 	}
-	if (fstat(f->fd, &st) < 0) {
-		res = -errno;
+	if ((res = read_FRM8(f)) < 0)
 		goto exit_close;
-	}
-	f->size = st.st_size;
 
-	f->data = mmap(NULL, f->size, PROT_READ, MAP_SHARED, f->fd, 0);
-	if (f->data == MAP_FAILED) {
+	f->blocksize = BLOCKSIZE * f->info.channels;
+	f->buffer = calloc(1, f->blocksize);
+	if (f->buffer == NULL) {
 		res = -errno;
 		goto exit_close;
 	}
-
-	f->p = f->data;
-
-	if ((res = read_FRM8(f)) < 0)
-		goto exit_unmap;
-
 	f->mode = 1;
 	*info = f->info;
 	return 0;
 
-exit_unmap:
-	munmap(f->data, f->size);
 exit_close:
-	close(f->fd);
+	if (f->close)
+		fclose(f->file);
 exit:
 	return res;
 }
@@ -267,18 +289,26 @@ dff_file_read(struct dff_file *f, void *data, size_t samples, const struct dff_l
 	int32_t step = SPA_ABS(layout->interleave);
 	uint32_t channels = f->info.channels;
 	bool rev = layout->lsb != f->info.lsb;
-	size_t total, offset, scale;
+	size_t total, offset, scale, pos;
 
 	offset = f->offset;
+	pos = offset % f->blocksize;
 	scale = SPA_CLAMP(f->info.rate / (44100u * 64u), 1u, 4u);
 
 	samples *= step;
 	samples *= scale;
 
-	for (total = 0; total < samples && offset < f->info.length; total++) {
+	for (total = 0; total < samples; total++) {
 		uint32_t i;
 		int32_t j;
-		const uint8_t *s = f->p + offset;
+		const uint8_t *s = f->buffer + pos;
+
+		if (pos == 0) {
+			if (fread(f->buffer, 1, f->blocksize, f->file) != f->blocksize)
+				break;
+		}
+		if (f->info.length > 0 && offset >= f->info.length)
+			break;
 
 		for (i = 0; i < layout->channels; i++) {
 			if (layout->interleave > 0) {
@@ -294,6 +324,9 @@ dff_file_read(struct dff_file *f, void *data, size_t samples, const struct dff_l
 			}
 		}
 		offset += step * channels;
+		pos += step * channels;
+		if (pos == f->blocksize)
+			pos = 0;
 	}
 	f->offset = offset;
 
@@ -302,12 +335,9 @@ dff_file_read(struct dff_file *f, void *data, size_t samples, const struct dff_l
 
 int dff_file_close(struct dff_file *f)
 {
-	if (f->mode == 1) {
-		munmap(f->data, f->size);
-	} else
-		return -EINVAL;
-
-	close(f->fd);
+	if (f->close)
+		fclose(f->file);
+	free(f->buffer);
 	free(f);
 	return 0;
 }
diff --git a/src/tools/dsffile.c b/src/tools/dsffile.c
index 213eefe2..4122944a 100644
--- a/src/tools/dsffile.c
+++ b/src/tools/dsffile.c
@@ -14,16 +14,14 @@
 #include "dsffile.h"
 
 struct dsf_file {
-	uint8_t *data;
-	size_t size;
+	uint8_t *buffer;
+	size_t offset;
 
 	int mode;
-	int fd;
+	bool close;
+	FILE *file;
 
 	struct dsf_file_info info;
-
-	uint8_t *p;
-	size_t offset;
 };
 
 static inline uint32_t parse_le32(const uint8_t *in)
@@ -44,104 +42,110 @@ static inline uint64_t parse_le64(const uint8_t *in)
 	return res;
 }
 
-static inline int f_avail(struct dsf_file *f)
+static inline int f_skip(struct dsf_file *f, size_t bytes)
 {
-	if (f->p < f->data + f->size)
-		return f->size + f->data - f->p;
+	uint8_t data[256];
+	while (bytes > 0) {
+		size_t s = fread(data, 1, SPA_MIN(bytes, sizeof(data)), f->file);
+		bytes -= s;
+	}
 	return 0;
 }
 
 static int read_DSD(struct dsf_file *f)
 {
+	size_t s;
 	uint64_t size;
+	uint8_t data[28];
 
-	if (f_avail(f) < 28 ||
-	    memcmp(f->p, "DSD ", 4) != 0)
+	s = fread(data, 1, 28, f->file);
+	if (s < 28 || memcmp(data, "DSD ", 4) != 0)
 		return -EINVAL;
 
-	size = parse_le64(f->p + 4);	/* size of this chunk */
-	parse_le64(f->p + 12);		/* total size */
-	parse_le64(f->p + 20);		/* metadata */
-	f->p += size;
+	size = parse_le64(data + 4);	/* size of this chunk */
+	parse_le64(data + 12);		/* total size */
+	parse_le64(data + 20);		/* metadata */
+	if (size > s)
+		f_skip(f, size - s);
 	return 0;
 }
 
 static int read_fmt(struct dsf_file *f)
 {
+	size_t s;
 	uint64_t size;
+	uint8_t data[52];
 
-	if (f_avail(f) < 52 ||
-	    memcmp(f->p, "fmt ", 4) != 0)
+	s = fread(data, 1, 52, f->file);
+	if (s < 52 || memcmp(data, "fmt ", 4) != 0)
 		return -EINVAL;
 
-	size = parse_le64(f->p + 4);	/* size of this chunk */
-	if (parse_le32(f->p + 12) != 1)	/* version */
+	size = parse_le64(data + 4);	/* size of this chunk */
+	if (parse_le32(data + 12) != 1)	/* version */
 		return -EINVAL;
-	if (parse_le32(f->p + 16) != 0)	/* format id */
+	if (parse_le32(data + 16) != 0)	/* format id */
 		return -EINVAL;
 
-	f->info.channel_type = parse_le32(f->p + 20);
-	f->info.channels = parse_le32(f->p + 24);
-	f->info.rate = parse_le32(f->p + 28);
-	f->info.lsb = parse_le32(f->p + 32) == 1;
-	f->info.samples = parse_le64(f->p + 36);
-	f->info.blocksize = parse_le32(f->p + 44);
-	f->p += size;
+	f->info.channel_type = parse_le32(data + 20);
+	f->info.channels = parse_le32(data + 24);
+	f->info.rate = parse_le32(data + 28);
+	f->info.lsb = parse_le32(data + 32) == 1;
+	f->info.samples = parse_le64(data + 36);
+	f->info.blocksize = parse_le32(data + 44);
+	if (size > s)
+		f_skip(f, size - s);
+
+	f->buffer = calloc(1, f->info.blocksize * f->info.channels);
+	if (f->buffer == NULL)
+		return -errno;
+
 	return 0;
 }
 
 static int read_data(struct dsf_file *f)
 {
+	size_t s;
 	uint64_t size;
+	uint8_t data[12];
 
-	if (f_avail(f) < 12 ||
-	    memcmp(f->p, "data", 4) != 0)
+	s = fread(data, 1, 12, f->file);
+	if (s < 12 || memcmp(data, "data", 4) != 0)
 		return -EINVAL;
 
-	size = parse_le64(f->p + 4);	/* size of this chunk */
+	size = parse_le64(data + 4);	/* size of this chunk */
 	f->info.length = size - 12;
-	f->p += 12;
 	return 0;
 }
 
 static int open_read(struct dsf_file *f, const char *filename, struct dsf_file_info *info)
 {
 	int res;
-	struct stat st;
-
-	if ((f->fd = open(filename, O_RDONLY)) < 0) {
-		res = -errno;
-		goto exit;
-	}
-	if (fstat(f->fd, &st) < 0) {
-		res = -errno;
-		goto exit_close;
-	}
-	f->size = st.st_size;
 
-	f->data = mmap(NULL, f->size, PROT_READ, MAP_SHARED, f->fd, 0);
-	if (f->data == MAP_FAILED) {
-		res = -errno;
-		goto exit_close;
+	if (strcmp(filename, "-") != 0) {
+		if ((f->file = fopen(filename, "r")) == NULL) {
+			res = -errno;
+			goto exit;
+		}
+		f->close = true;
+	} else {
+		f->close = false;
+		f->file = stdin;
 	}
 
-	f->p = f->data;
-
 	if ((res = read_DSD(f)) < 0)
-		goto exit_unmap;
+		goto exit_close;
 	if ((res = read_fmt(f)) < 0)
-		goto exit_unmap;
+		goto exit_close;
 	if ((res = read_data(f)) < 0)
-		goto exit_unmap;
+		goto exit_close;
 
 	f->mode = 1;
 	*info = f->info;
 	return 0;
 
-exit_unmap:
-	munmap(f->data, f->size);
 exit_close:
-	close(f->fd);
+	if (f->close)
+		fclose(f->file);
 exit:
 	return res;
 }
@@ -197,21 +201,28 @@ dsf_file_read(struct dsf_file *f, void *data, size_t samples, const struct dsf_l
 	int step = SPA_ABS(layout->interleave);
 	bool rev = layout->lsb != f->info.lsb;
 	size_t total, block, offset, pos, scale;
+	size_t blocksize = f->info.blocksize * f->info.channels;
 
 	block = f->offset / f->info.blocksize;
-	offset = block * f->info.blocksize * f->info.channels;
+	offset = block * blocksize;
 	pos = f->offset % f->info.blocksize;
 	scale = SPA_CLAMP(f->info.rate / (44100u * 64u), 1u, 4u);
 
 	samples *= step;
 	samples *= scale;
 
-	for (total = 0; total < samples && offset + pos < f->info.length; total++) {
-		const uint8_t *s = f->p + offset + pos;
+	for (total = 0; total < samples; total++) {
 		uint32_t i;
 
+		if (pos == 0) {
+			if (fread(f->buffer, 1, blocksize, f->file) != blocksize)
+				break;
+		}
+		if (f->info.length > 0 && offset + pos >= f->info.length) {
+			break;
+		}
 		for (i = 0; i < layout->channels; i++) {
-			const uint8_t *c = &s[f->info.blocksize * i];
+			const uint8_t *c = &f->buffer[f->info.blocksize * i + pos];
 			int j;
 
 			if (layout->interleave > 0) {
@@ -225,7 +236,7 @@ dsf_file_read(struct dsf_file *f, void *data, size_t samples, const struct dsf_l
 		pos += step;
 		if (pos == f->info.blocksize) {
 			pos = 0;
-			offset += f->info.blocksize * f->info.channels;
+			offset += blocksize;
 		}
 	}
 	f->offset += total * step;
@@ -235,12 +246,9 @@ dsf_file_read(struct dsf_file *f, void *data, size_t samples, const struct dsf_l
 
 int dsf_file_close(struct dsf_file *f)
 {
-	if (f->mode == 1) {
-		munmap(f->data, f->size);
-	} else
-		return -EINVAL;
-
-	close(f->fd);
+	if (f->close)
+		fclose(f->file);
+	free(f->buffer);
 	free(f);
 	return 0;
 }
diff --git a/src/tools/midifile.c b/src/tools/midifile.c
index 2ee2be62..80c5bbb1 100644
--- a/src/tools/midifile.c
+++ b/src/tools/midifile.c
@@ -10,6 +10,7 @@
 #include <math.h>
 
 #include <spa/utils/string.h>
+#include <spa/control/ump-utils.h>
 
 #include "midifile.h"
 
@@ -18,27 +19,28 @@
 struct midi_track {
 	uint16_t id;
 
-	uint8_t *data;
+	long start;
 	uint32_t size;
+	long pos;
 
-	uint8_t *p;
 	int64_t tick;
 	unsigned int eof:1;
 	uint8_t event[4];
 };
 
 struct midi_file {
-	uint8_t *data;
-	size_t size;
-
 	int mode;
-	int fd;
+	FILE *file;
+	bool close;
+	long pos;
+
+	uint8_t *buffer;
+	size_t buffer_size;
 
 	struct midi_file_info info;
 	uint32_t length;
 	uint32_t tempo;
 
-	uint8_t *p;
 	int64_t tick;
 	double tick_sec;
 	double tick_start;
@@ -56,138 +58,157 @@ static inline uint32_t parse_be32(const uint8_t *in)
 	return (in[0] << 24) | (in[1] << 16) | (in[2] << 8) | in[3];
 }
 
-static inline int mf_avail(struct midi_file *mf)
+static inline int mf_read(struct midi_file *mf, void *data, size_t size)
 {
-	if (mf->p < mf->data + mf->size)
-		return mf->size + mf->data - mf->p;
-	return 0;
+	if (fread(data, size, 1, mf->file) != 1)
+		return 0;
+	mf->pos += size;
+	return 1;
 }
 
 static inline int tr_avail(struct midi_track *tr)
 {
 	if (tr->eof)
 		return 0;
-	if (tr->p < tr->data + tr->size)
-		return tr->size + tr->data - tr->p;
+	if (tr->size == 0)
+		return 1;
+	if (tr->pos < tr->start + tr->size)
+		return tr->size + tr->start - tr->pos;
 	tr->eof = true;
 	return 0;
 }
 
 static int read_mthd(struct midi_file *mf)
 {
-	if (mf_avail(mf) < 14 ||
-	    memcmp(mf->p, "MThd", 4) != 0)
+	uint8_t data[14];
+
+	if (mf_read(mf, data, sizeof(data)) != 1 ||
+	    memcmp(data, "MThd", 4) != 0)
 		return -EINVAL;
 
-	mf->length = parse_be32(mf->p + 4);
-	mf->info.format = parse_be16(mf->p + 8);
-	mf->info.ntracks = parse_be16(mf->p + 10);
-	mf->info.division = parse_be16(mf->p + 12);
+	mf->length = parse_be32(data + 4);
+	mf->info.format = parse_be16(data + 8);
+	mf->info.ntracks = parse_be16(data + 10);
+	mf->info.division = parse_be16(data + 12);
+	return 0;
+}
+
+static int parse_varlen(struct midi_file *mf, struct midi_track *tr, uint32_t *result)
+{
+	uint32_t value = 0;
+	uint8_t data[1];
 
-	mf->p += 14;
+	while (mf_read(mf, data, 1) == 1) {
+		value = (value << 7) | (data[0] & 0x7f);
+		if ((data[0] & 0x80) == 0)
+			break;
+	}
+	*result = value;
 	return 0;
 }
 
-static int read_mtrk(struct midi_file *mf, struct midi_track *track)
+static int read_delta_time(struct midi_file *mf, struct midi_track *tr)
 {
-	if (mf_avail(mf) < 8 ||
-	    memcmp(mf->p, "MTrk", 4) != 0)
-		return -EINVAL;
+	int res;
+	uint32_t delta_time;
+
+	if ((res = parse_varlen(mf, tr, &delta_time)) < 0)
+		return res;
+
+	tr->tick += delta_time;
+	tr->pos = mf->pos;
+	return 0;
 
-	track->data = track->p = mf->p + 8;
-	track->size = parse_be32(mf->p + 4);
+}
 
-	mf->p = track->data + track->size;
-	if (mf->p > mf->data + mf->size)
+static int read_mtrk(struct midi_file *mf, struct midi_track *track)
+{
+	uint8_t data[8];
+
+	if (mf_read(mf, data, sizeof(data)) != 1 ||
+	    memcmp(data, "MTrk", 4) != 0)
 		return -EINVAL;
 
-	return 0;
+	track->start = track->pos = mf->pos;
+	track->size = parse_be32(data + 4);
+
+	return read_delta_time(mf, track);
 }
 
-static int parse_varlen(struct midi_file *mf, struct midi_track *tr, uint32_t *result)
+static uint8_t *ensure_buffer(struct midi_file *mf, struct midi_track *tr, size_t size)
 {
-	uint32_t value = 0;
+	if (size <= 4)
+		return tr->event;
 
-	while (tr_avail(tr) > 0) {
-		uint8_t b = *tr->p++;
-		value = (value << 7) | (b & 0x7f);
-		if ((b & 0x80) == 0)
-			break;
+	if (size > mf->buffer_size) {
+		mf->buffer = realloc(mf->buffer, size);
+		mf->buffer_size = size;
 	}
-	*result = value;
-	return 0;
+	return mf->buffer;
 }
 
 static int open_read(struct midi_file *mf, const char *filename, struct midi_file_info *info)
 {
 	int res;
 	uint16_t i;
-	struct stat st;
-
-	if ((mf->fd = open(filename, O_RDONLY)) < 0) {
-		res = -errno;
-		goto exit;
-	}
-	if (fstat(mf->fd, &st) < 0) {
-		res = -errno;
-		goto exit_close;
-	}
-	mf->size = st.st_size;
 
-	mf->data = mmap(NULL, mf->size, PROT_READ, MAP_SHARED, mf->fd, 0);
-	if (mf->data == MAP_FAILED) {
-		res = -errno;
-		goto exit_close;
+	if (strcmp(filename, "-") != 0) {
+		if ((mf->file = fopen(filename, "r")) == NULL) {
+			res = -errno;
+			goto exit;
+		}
+		mf->close = true;
+	} else {
+		mf->file = stdin;
+		mf->close = false;
 	}
 
-	mf->p = mf->data;
-
 	if ((res = read_mthd(mf)) < 0)
-		goto exit_unmap;
+		goto exit_close;
 
 	mf->tempo = DEFAULT_TEMPO;
 	mf->tick = 0;
 
 	for (i = 0; i < mf->info.ntracks; i++) {
 		struct midi_track *tr = &mf->tracks[i];
-		uint32_t delta_time;
 
 		if ((res = read_mtrk(mf, tr)) < 0)
-			goto exit_unmap;
+			goto exit_close;
 
-		if ((res = parse_varlen(mf, tr, &delta_time)) < 0)
-			goto exit_unmap;
-
-		tr->tick = delta_time;
 		tr->id = i;
+
+		if (i + 1 < mf->info.ntracks &&
+		    fseek(mf->file, tr->start + tr->size, SEEK_SET) != 0) {
+			res = -errno;
+			goto exit_close;
+		}
 	}
 	mf->mode = 1;
 	*info = mf->info;
 	return 0;
 
-exit_unmap:
-	munmap(mf->data, mf->size);
 exit_close:
-	close(mf->fd);
+	if (mf->close)
+		fclose(mf->file);
 exit:
 	return res;
 }
 
-static inline int write_n(int fd, const void *buf, int count)
+static inline int write_n(FILE *file, const void *buf, int count)
 {
-	return write(fd, buf, count) == (ssize_t)count ? count : -errno;
+	return fwrite(buf, 1, count, file) == (size_t)count ? count : -errno;
 }
 
-static inline int write_be16(int fd, uint16_t val)
+static inline int write_be16(FILE *file, uint16_t val)
 {
 	uint8_t buf[2] = { val >> 8, val };
-	return write_n(fd, buf, 2);
+	return write_n(file, buf, 2);
 }
 
-static inline int write_be32(int fd, uint32_t val)
+static inline int write_be32(FILE *file, uint32_t val)
 {
 	uint8_t buf[4] = { val >> 24, val >> 16, val >> 8, val };
-	return write_n(fd, buf, 4);
+	return write_n(file, buf, 4);
 }
 
 #define CHECK_RES(expr) if ((res = (expr)) < 0) return res
@@ -197,17 +218,17 @@ static int write_headers(struct midi_file *mf)
 	struct midi_track *tr = &mf->tracks[0];
 	int res;
 
-	lseek(mf->fd, 0, SEEK_SET);
+	fseek(mf->file, 0, SEEK_SET);
 
 	mf->length = 6;
-	CHECK_RES(write_n(mf->fd, "MThd", 4));
-	CHECK_RES(write_be32(mf->fd, mf->length));
-	CHECK_RES(write_be16(mf->fd, mf->info.format));
-	CHECK_RES(write_be16(mf->fd, mf->info.ntracks));
-	CHECK_RES(write_be16(mf->fd, mf->info.division));
+	CHECK_RES(write_n(mf->file, "MThd", 4));
+	CHECK_RES(write_be32(mf->file, mf->length));
+	CHECK_RES(write_be16(mf->file, mf->info.format));
+	CHECK_RES(write_be16(mf->file, mf->info.ntracks));
+	CHECK_RES(write_be16(mf->file, mf->info.division));
 
-	CHECK_RES(write_n(mf->fd, "MTrk", 4));
-	CHECK_RES(write_be32(mf->fd, tr->size));
+	CHECK_RES(write_n(mf->file, "MTrk", 4));
+	CHECK_RES(write_be32(mf->file, tr->size));
 
 	return 0;
 }
@@ -225,9 +246,15 @@ static int open_write(struct midi_file *mf, const char *filename, struct midi_fi
 	if (info->division == 0)
 		info->division = 96;
 
-	if ((mf->fd = open(filename, O_WRONLY | O_CREAT, 0660)) < 0) {
-		res = -errno;
-		goto exit;
+	if (strcmp(filename, "-") != 0) {
+		if ((mf->file = fopen(filename, "w")) == NULL) {
+			res = -errno;
+			goto exit;
+		}
+		mf->close = true;
+	} else {
+		mf->file = stdout;
+		mf->close = false;
 	}
 	mf->mode = 2;
 	mf->tempo = DEFAULT_TEMPO;
@@ -270,17 +297,17 @@ int midi_file_close(struct midi_file *mf)
 {
 	int res;
 
-	if (mf->mode == 1) {
-		munmap(mf->data, mf->size);
-	} else if (mf->mode == 2) {
+	if (mf->mode == 2) {
 		uint8_t buf[4] = { 0x00, 0xff, 0x2f, 0x00 };
-		CHECK_RES(write_n(mf->fd, buf, 4));
+		CHECK_RES(write_n(mf->file, buf, 4));
 		mf->tracks[0].size += 4;
 		CHECK_RES(write_headers(mf));
 	} else
 		return -EINVAL;
 
-	close(mf->fd);
+	if (mf->close)
+		fclose(mf->file);
+	free(mf->buffer);
 	free(mf);
 	return 0;
 }
@@ -302,6 +329,7 @@ static int peek_next(struct midi_file *mf, struct midi_event *ev)
 
 	ev->track = found->id;
 	ev->sec = mf->tick_sec + ((found->tick - mf->tick_start) * (double)mf->tempo) / (1000000.0 * mf->info.division);
+	ev->type = MIDI_EVENT_TYPE_MIDI1;
 	return 1;
 }
 
@@ -320,22 +348,31 @@ int midi_file_next_time(struct midi_file *mf, double *sec)
 int midi_file_read_event(struct midi_file *mf, struct midi_event *event)
 {
 	struct midi_track *tr;
-	uint32_t delta_time, size;
+	uint32_t size;
 	uint8_t status, meta;
 	int res, running;
+	long offs;
+
+	event->data = NULL;
 
 	if ((res = peek_next(mf, event)) <= 0)
 		return res;
 
 	tr = &mf->tracks[event->track];
-	status = *tr->p;
+
+	offs = tr->pos;
+	if (offs != mf->pos) {
+		if (fseek(mf->file, offs, SEEK_SET) != 0)
+			return -errno;
+	}
+
+	mf_read(mf, &status, 1);
 
 	running = (status & 0x80) == 0;
 	if (running) {
+		tr->event[1] = status;
 		status = tr->event[0];
-		event->data = tr->event;
 	} else {
-		event->data = tr->p++;
 		tr->event[0] = status;
 	}
 
@@ -350,52 +387,82 @@ int midi_file_read_event(struct midi_file *mf, struct midi_event *event)
 		break;
 
 	case 0xff:
-		meta = *tr->p++;
+		if (running)
+			return -EINVAL;
+
+		mf_read(mf, &meta, 1);
 
 		if ((res = parse_varlen(mf, tr, &size)) < 0)
 			return res;
 
-		event->meta.offset = tr->p - event->data;
+		event->meta.offset = 2;
 		event->meta.size = size;
 
+		if ((event->data = ensure_buffer(mf, tr, size + event->meta.offset)) == NULL)
+			return -ENOMEM;
+
+		event->data[0] = status;
+		event->data[1] = meta;
+		if (size > 0 && mf_read(mf, &event->data[2], size) != 1)
+			return -EINVAL;
+
 		switch (meta) {
 		case 0x2f:
 			tr->eof = true;
 			break;
 		case 0x51:
+		{
 			if (size < 3)
 				return -EINVAL;
 			mf->tick_sec = event->sec;
 			mf->tick_start = tr->tick;
-			event->meta.parsed.tempo.uspqn = mf->tempo = (tr->p[0]<<16) | (tr->p[1]<<8) | tr->p[2];
+			event->meta.parsed.tempo.uspqn = mf->tempo =
+				(event->data[2]<<16) | (event->data[3]<<8) | event->data[4];
 			break;
 		}
-		size += tr->p - event->data;
+		}
+		size += event->meta.offset;
 		break;
 
 	case 0xf0:
 	case 0xf7:
+		if (running)
+			return -EINVAL;
+
 		if ((res = parse_varlen(mf, tr, &size)) < 0)
 			return res;
-		size += tr->p - event->data;
+
+		if ((event->data = ensure_buffer(mf, tr, size + 1)) == NULL)
+			return -ENOMEM;
+
+		event->data[0] = status;
+		if (mf_read(mf, &event->data[1], size) != 1)
+			return -EINVAL;
+
+		size += 1;
 		break;
 	default:
 		return -EINVAL;
 	}
 
 	event->size = size;
-
-	if (running) {
-		memcpy(&event->data[1], tr->p, size - 1);
-		tr->p += size - 1;
-	} else {
-		tr->p = event->data + event->size;
+	if (event->data == NULL) {
+		if ((event->data = ensure_buffer(mf, tr, size)) == NULL)
+			return -ENOMEM;
+		event->data[0] = tr->event[0];
+		if (running) {
+			event->data[1] = tr->event[1];
+			if (size > 2 && mf_read(mf, &event->data[2], size - 2) != 1)
+				return -EINVAL;
+		} else {
+			if (size > 1 && mf_read(mf, &event->data[1], size - 1) != 1)
+				return -EINVAL;
+		}
 	}
 
-	if ((res = parse_varlen(mf, tr, &delta_time)) < 0)
+	if ((res = read_delta_time(mf, tr)) < 0)
 		return res;
 
-	tr->tick += delta_time;
 	return 1;
 }
 
@@ -412,7 +479,7 @@ static int write_varlen(struct midi_file *mf, struct midi_track *tr, uint32_t va
 	}
         do  {
 		b = buffer & 0xff;
-		CHECK_RES(write_n(mf->fd, &b, 1));
+		CHECK_RES(write_n(mf->file, &b, 1));
 		tr->size++;
 		buffer >>= 8;
 	} while (b & 0x80);
@@ -424,13 +491,31 @@ int midi_file_write_event(struct midi_file *mf, const struct midi_event *event)
 {
 	struct midi_track *tr;
 	uint32_t tick;
+	void *data;
+	size_t size;
 	int res;
+	uint8_t ev[32];
 
 	spa_return_val_if_fail(event != NULL, -EINVAL);
 	spa_return_val_if_fail(mf != NULL, -EINVAL);
 	spa_return_val_if_fail(event->track == 0, -EINVAL);
 	spa_return_val_if_fail(event->size > 1, -EINVAL);
 
+	switch (event->type) {
+	case MIDI_EVENT_TYPE_MIDI1:
+		data = event->data;
+		size = event->size;
+		break;
+	case MIDI_EVENT_TYPE_UMP:
+		data = ev;
+		size = spa_ump_to_midi((uint32_t*)event->data, event->size, ev, sizeof(ev));
+		if (size == 0)
+			return 0;
+		break;
+	default:
+		return -EINVAL;
+	}
+
 	tr = &mf->tracks[event->track];
 
 	tick = (uint32_t)(event->sec * (1000000.0 * mf->info.division) / (double)mf->tempo);
@@ -438,8 +523,8 @@ int midi_file_write_event(struct midi_file *mf, const struct midi_event *event)
 	CHECK_RES(write_varlen(mf, tr, tick - tr->tick));
 	tr->tick = tick;
 
-	CHECK_RES(write_n(mf->fd, event->data, event->size));
-	tr->size += event->size;
+	CHECK_RES(write_n(mf->file, data, size));
+	tr->size += size;
 
 	return 0;
 }
@@ -587,7 +672,7 @@ static void dump_mem(FILE *out, const char *label, uint8_t *data, uint32_t size)
 		fprintf(out, "%02x ", *data++);
 }
 
-int midi_file_dump_event(FILE *out, const struct midi_event *ev)
+static int dump_event_midi1(FILE *out, const struct midi_event *ev)
 {
 	fprintf(out, "track:%2d sec:%f ", ev->track, ev->sec);
 
@@ -662,19 +747,21 @@ int midi_file_dump_event(FILE *out, const struct midi_event *ev)
 		fprintf(out, "Active Sensing");
 		break;
 	case 0xff:
+	{
+		uint8_t *meta = &ev->data[ev->meta.offset];
 		fprintf(out, "Meta: ");
 		switch (ev->data[1]) {
 		case 0x00:
-			fprintf(out, "Sequence Number %3d %3d", ev->data[3], ev->data[4]);
+			fprintf(out, "Sequence Number %3d %3d", meta[0], meta[1]);
 			break;
 		case 0x01 ... 0x09:
-			fprintf(out, "%s: %s", event_names[ev->data[1] - 1], &ev->data[ev->meta.offset]);
+			fprintf(out, "%s: %s", event_names[ev->data[1] - 1], meta);
 			break;
 		case 0x20:
-			fprintf(out, "Channel Prefix: %03d", ev->data[3]);
+			fprintf(out, "Channel Prefix: %03d", meta[0]);
 			break;
 		case 0x21:
-			fprintf(out, "Midi Port: %03d", ev->data[3]);
+			fprintf(out, "Midi Port: %03d", meta[0]);
 			break;
 		case 0x2f:
 			fprintf(out, "End Of Track");
@@ -686,20 +773,20 @@ int midi_file_dump_event(FILE *out, const struct midi_event *ev)
 			break;
 		case 0x54:
 			fprintf(out, "SMPTE Offset: %s %02d:%02d:%02d:%02d.%03d",
-					smpte_rates[(ev->data[3] & 0x60) >> 5],
-					ev->data[3] & 0x1f, ev->data[4], ev->data[5],
-					ev->data[6], ev->data[7]);
+					smpte_rates[(meta[0] & 0x60) >> 5],
+					meta[0] & 0x1f, meta[1], meta[2],
+					meta[3], meta[4]);
 			break;
 		case 0x58:
 			fprintf(out, "Time Signature: %d/%d, %d clocks per click, %d notated 32nd notes per quarter note",
-				ev->data[3], (int)pow(2, ev->data[4]), ev->data[5], ev->data[6]);
+				meta[0], (int)pow(2, meta[1]), meta[2], meta[3]);
 			break;
 		case 0x59:
 		{
-			int sf = ev->data[3];
+			int sf = meta[0];
 			fprintf(out, "Key Signature: %d %s: %s", abs(sf),
 					sf > 0 ? "sharps" : "flats",
-					ev->data[4] == 0 ?
+					meta[1] == 0 ?
 						major_keys[SPA_CLAMP(sf + 9, 0, 18)] :
 						minor_keys[SPA_CLAMP(sf + 9, 0, 18)]);
 			break;
@@ -711,10 +798,217 @@ int midi_file_dump_event(FILE *out, const struct midi_event *ev)
 			dump_mem(out, "Invalid", ev->data, ev->size);
 		}
 		break;
+	}
 	default:
 		dump_mem(out, "Unknown", ev->data, ev->size);
 		break;
 	}
-	fprintf(out, "\n");
 	return 0;
 }
+
+static int dump_event_midi2_channel(FILE *out, const struct midi_event *ev)
+{
+	uint32_t *d = (uint32_t*)ev->data;
+	uint8_t status = d[0] >> 16;
+
+	fprintf(out, "track:%2d sec:%f ", ev->track, ev->sec);
+
+	switch (status) {
+	case 0x00 ... 0x0f:
+	case 0x10 ... 0x1f:
+	{
+		uint8_t note = (d[0] >> 8) & 0x7f;
+		uint8_t index = d[0] & 0xff;
+		fprintf(out, "%s Per-Note controller (channel %2d): note %3s%d, index %u, value %u",
+				(status & 0xf0) == 0x00 ? "Registered" : "Assignable",
+				(status & 0x0f) + 1,
+				note_names[note % 12], note / 12 -1, index, d[1]);
+		break;
+	}
+	case 0x20 ... 0x2f:
+	case 0x30 ... 0x3f:
+	{
+		uint16_t index = (d[0] & 0x7f) | ((d[0] & 0x7f00) >> 1);
+		fprintf(out, "%s controller (channel %2d): index %u, value %u",
+				(status & 0xf0) == 0x20 ? "Registered" : "Assignable",
+				(status & 0x0f) + 1, index, d[1]);
+		break;
+	}
+	case 0x40 ... 0x4f:
+	case 0x50 ... 0x5f:
+	{
+		uint16_t index = (d[0] & 0x7f) | ((d[0] & 0x7f00) >> 1);
+		fprintf(out, "Relative %s controller (channel %2d): index %u, value %u",
+				(status & 0xf0) == 0x20 ? "Registered" : "Assignable",
+				(status & 0x0f) + 1, index, d[1]);
+		break;
+	}
+	case 0x60 ... 0x6f:
+	{
+		uint8_t note = (d[0] >> 8) & 0x7f;
+		fprintf(out, "Per-Note Pitch Bend  (channel %2d): note %3s%d, pitch %u",
+				(status & 0x0f) + 1,
+				note_names[note % 12], note / 12 -1, d[1]);
+		break;
+	}
+	case 0x80 ... 0x8f:
+	{
+		uint8_t note = (d[0] >> 8) & 0x7f;
+		uint8_t attr_type = d[0] & 0xff;
+		uint16_t velocity = (d[1] >> 16) & 0xffff;
+		uint16_t attr_data = (d[1]) & 0xffff;
+		fprintf(out, "Note Off   (channel %2d): note %3s%d, velocity %5d, attr (%u)%u",
+				(status & 0x0f) + 1,
+				note_names[note % 12], note / 12 -1,
+				velocity, attr_type, attr_data);
+		break;
+	}
+	case 0x90 ... 0x9f:
+	{
+		uint8_t note = (d[0] >> 8) & 0x7f;
+		uint8_t attr_type = d[0] & 0xff;
+		uint16_t velocity = (d[1] >> 16) & 0xffff;
+		uint16_t attr_data = (d[1]) & 0xffff;
+		fprintf(out, "Note On    (channel %2d): note %3s%d, velocity %5d, attr (%u)%u",
+				(status & 0x0f) + 1,
+				note_names[note % 12], note / 12 -1,
+				velocity, attr_type, attr_data);
+		break;
+	}
+	case 0xa0 ... 0xaf:
+	{
+		uint8_t note = (d[0] >> 8) & 0x7f;
+		fprintf(out, "Aftertouch (channel %2d): note %3s%d, pressure %u",
+				(status & 0x0f) + 1,
+				note_names[note % 12], note / 12 -1, d[1]);
+		break;
+	}
+	case 0xb0 ... 0xbf:
+	{
+		uint8_t index = (d[0] >> 8) & 0x7f;
+		fprintf(out, "Controller (channel %2d): controller %3d (%s), value %u",
+				(status & 0x0f) + 1, index,
+				controller_name(index), d[1]);
+		break;
+	}
+	case 0xc0 ... 0xcf:
+	{
+		uint8_t flags = (d[0] & 0xff);
+		uint8_t program = (d[1] >> 24) & 0x7f;
+		uint16_t bank = (d[1] & 0x7f) | ((d[1] & 0x7f00) >> 1);
+		fprintf(out, "Program    (channel %2d): flags %u program %3d (%s), bank %u",
+				(status & 0x0f) + 1, flags, program,
+				program_names[program], bank);
+		break;
+	}
+	case 0xd0 ... 0xdf:
+		fprintf(out, "Channel Pressure (channel %2d): pressure %u",
+				(status & 0x0f) + 1, d[1]);
+		break;
+	case 0xe0 ... 0xef:
+		fprintf(out, "Pitch Bend (channel %2d): value %u",
+				(status & 0x0f) + 1, d[1]);
+		break;
+	case 0xf0 ... 0xff:
+	{
+		uint8_t note = (d[0] >> 8) & 0x7f;
+		uint8_t flags = d[0] & 0xff;
+		fprintf(out, "Per-Note management (channel %2d): note %3s%d, flags %u",
+				(status & 0x0f) + 1,
+				note_names[note % 12], note / 12 -1, flags);
+		break;
+	}
+	default:
+		dump_mem(out, "Unknown", ev->data, ev->size);
+		break;
+	}
+
+	return 0;
+}
+
+static int dump_event_ump(FILE *out, const struct midi_event *ev)
+{
+	uint32_t *d = (uint32_t*)ev->data;
+	uint8_t group = (d[0] >> 24) & 0xf;
+	uint8_t mt = (d[0] >> 28) & 0xf;
+	int res = 0;
+
+	fprintf(out, "group:%2d ", group);
+
+	switch (mt) {
+	case 0x0:
+		dump_mem(out, "Utility", ev->data, ev->size);
+		break;
+	case 0x1:
+		dump_mem(out, "SysRT", ev->data, ev->size);
+		break;
+	case 0x2:
+	{
+		struct midi_event ev1;
+		uint8_t msg[4];
+
+		ev1 = *ev;
+		msg[0] = (d[0] >> 16);
+		msg[1] = (d[0] >> 8);
+		msg[2] = (d[0]);
+		if (msg[0] >= 0xc0 && msg[0] <= 0xdf)
+			ev1.size = 2;
+                else
+			ev1.size = 3;
+		ev1.data = msg;
+		dump_event_midi1(out, &ev1);
+		break;
+	}
+	case 0x3:
+	{
+		uint8_t status = (d[0] >> 20) & 0xf;
+		uint8_t bytes = SPA_CLAMP((d[0] >> 16) & 0xf, 0u, 6u);
+		uint8_t b[6] = { d[0] >> 8, d[0], d[1] >> 24, d[1] >> 16, d[1] >> 8, d[1] };
+		switch (status) {
+		case 0x0:
+			dump_mem(out, "SysEx7 (Complete) ", b, bytes);
+			break;
+		case 0x1:
+			dump_mem(out, "SysEx7 (Start)    ", b, bytes);
+			break;
+		case 0x2:
+			dump_mem(out, "SysEx7 (Continue) ", b, bytes);
+			break;
+		case 0x3:
+			dump_mem(out, "SysEx7 (End)      ", b, bytes);
+			break;
+		default:
+			dump_mem(out, "SysEx7 (invalid)", ev->data, ev->size);
+			break;
+		}
+		break;
+	}
+	case 0x4:
+		res = dump_event_midi2_channel(out, ev);
+		break;
+	case 0x5:
+		dump_mem(out, "Data128", ev->data, ev->size);
+		break;
+	default:
+		dump_mem(out, "Reserved", ev->data, ev->size);
+		break;
+	}
+	return res;
+}
+
+int midi_file_dump_event(FILE *out, const struct midi_event *ev)
+{
+	int res;
+	switch (ev->type) {
+	case MIDI_EVENT_TYPE_MIDI1:
+		res = dump_event_midi1(out, ev);
+		break;
+	case MIDI_EVENT_TYPE_UMP:
+		res = dump_event_ump(out, ev);
+		break;
+	default:
+		return -EINVAL;
+	}
+	fprintf(out, "\n");
+	return res;
+}
diff --git a/src/tools/midifile.h b/src/tools/midifile.h
index 767be7d7..6b3c23b2 100644
--- a/src/tools/midifile.h
+++ b/src/tools/midifile.h
@@ -9,6 +9,9 @@
 struct midi_file;
 
 struct midi_event {
+#define MIDI_EVENT_TYPE_MIDI1		0
+#define MIDI_EVENT_TYPE_UMP		1
+	uint32_t type;
 	uint32_t track;
 	double sec;
 	uint8_t *data;
diff --git a/src/tools/pw-cat.c b/src/tools/pw-cat.c
index 377be270..f485a08f 100644
--- a/src/tools/pw-cat.c
+++ b/src/tools/pw-cat.c
@@ -19,7 +19,8 @@
 
 #include <spa/param/audio/layout.h>
 #include <spa/param/audio/format-utils.h>
-#include <spa/param/audio/type-info.h>
+#include <spa/param/audio/raw-json.h>
+#include <spa/utils/type-info.h>
 #include <spa/param/tag-utils.h>
 #include <spa/param/props.h>
 #include <spa/utils/result.h>
@@ -27,6 +28,7 @@
 #include <spa/utils/json.h>
 #include <spa/debug/types.h>
 #include <spa/debug/file.h>
+#include <spa/control/ump-utils.h>
 
 #include <pipewire/pipewire.h>
 #include <pipewire/i18n.h>
@@ -78,8 +80,8 @@ struct data;
 typedef int (*fill_fn)(struct data *d, void *dest, unsigned int n_frames, bool *null_frame);
 
 struct channelmap {
-	int n_channels;
-	int channels[SPA_AUDIO_MAX_CHANNELS];
+	uint32_t n_channels;
+	uint32_t channels[SPA_AUDIO_MAX_CHANNELS];
 };
 
 struct data {
@@ -102,6 +104,7 @@ struct data {
 #define TYPE_ENCODED    3
 #endif
 	int data_type;
+	bool raw;
 	const char *remote_name;
 	const char *media_type;
 	const char *media_category;
@@ -117,7 +120,7 @@ struct data {
 
 	unsigned int bitrate;
 	unsigned int rate;
-	int channels;
+	uint32_t channels;
 	struct channelmap channelmap;
 	unsigned int stride;
 	enum unit latency_unit;
@@ -564,10 +567,10 @@ static int channelmap_from_sf(struct channelmap *map)
 		[SF_CHANNEL_MAP_TOP_REAR_RIGHT] =        SPA_AUDIO_CHANNEL_TRR,
 		[SF_CHANNEL_MAP_TOP_REAR_CENTER] =       SPA_AUDIO_CHANNEL_TRC
 	};
-	int i;
+	uint32_t i;
 
 	for (i = 0; i < map->n_channels; i++) {
-		if (map->channels[i] >= 0 && map->channels[i] < (int) SPA_N_ELEMENTS(table))
+		if (map->channels[i] < SPA_N_ELEMENTS(table))
 			map->channels[i] = table[map->channels[i]];
 		else
 			map->channels[i] = SPA_AUDIO_CHANNEL_UNKNOWN;
@@ -576,8 +579,8 @@ static int channelmap_from_sf(struct channelmap *map)
 }
 struct mapping {
 	const char *name;
-	unsigned int channels;
-	unsigned int values[32];
+	uint32_t channels;
+	uint32_t values[32];
 };
 
 static const struct mapping maps[] =
@@ -597,21 +600,8 @@ static const struct mapping maps[] =
 	{ "surround-71",  SPA_AUDIO_LAYOUT_7_1 },
 };
 
-static unsigned int find_channel(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 int parse_channelmap(const char *channel_map, struct channelmap *map)
 {
-	int i, nch;
-
 	SPA_FOR_EACH_ELEMENT_VAR(maps, m) {
 		if (spa_streq(m->name, channel_map)) {
 			map->n_channels = m->channels;
@@ -621,16 +611,7 @@ static int parse_channelmap(const char *channel_map, struct channelmap *map)
 		}
 	}
 
-	spa_auto(pw_strv) ch = pw_split_strv(channel_map, ",", SPA_AUDIO_MAX_CHANNELS, &nch);
-	if (ch == NULL)
-		return -1;
-
-	map->n_channels = nch;
-	for (i = 0; i < map->n_channels; i++) {
-		int c = find_channel(ch[i]);
-		map->channels[i] = c;
-	}
-
+	spa_audio_parse_position(channel_map, strlen(channel_map), map->channels, &map->n_channels);
 	return 0;
 }
 
@@ -671,13 +652,11 @@ static int channelmap_default(struct channelmap *map, int n_channels)
 
 static void channelmap_print(struct channelmap *map)
 {
-	int i;
+	uint32_t i;
 
 	for (i = 0; i < map->n_channels; i++) {
-		const char *name = spa_debug_type_find_name(spa_type_audio_channel, map->channels[i]);
-		if (name == NULL)
-			name = ":UNK";
-		printf("%s%s", spa_debug_type_short_name(name), i + 1 < map->n_channels ? "," : "");
+		const char *name = spa_type_audio_channel_to_short_name(map->channels[i]);
+		fprintf(stderr, "%s%s", name, i + 1 < map->n_channels ? "," : "");
 	}
 }
 
@@ -686,7 +665,7 @@ static void on_core_info(void *userdata, const struct pw_core_info *info)
 	struct data *data = userdata;
 
 	if (data->verbose)
-		printf("remote %"PRIu32" is named \"%s\"\n",
+		fprintf(stderr, "remote %"PRIu32" is named \"%s\"\n",
 				info->id, info->name);
 }
 
@@ -715,7 +694,7 @@ on_state_changed(void *userdata, enum pw_stream_state old,
 	int ret;
 
 	if (data->verbose)
-		printf("stream state changed %s -> %s\n",
+		fprintf(stderr, "stream state changed %s -> %s\n",
 				pw_stream_state_as_string(old),
 				pw_stream_state_as_string(state));
 
@@ -726,7 +705,7 @@ on_state_changed(void *userdata, enum pw_stream_state old,
 					SPA_PROP_volume, 1, &data->volume,
 					0);
 			if (data->verbose)
-				printf("stream set volume to %.3f - %s\n", data->volume,
+				fprintf(stderr, "stream set volume to %.3f - %s\n", data->volume,
 						ret == 0 ? "success" : "FAILED");
 
 			data->volume_is_set = true;
@@ -735,7 +714,7 @@ on_state_changed(void *userdata, enum pw_stream_state old,
 			struct timespec timeout = {0, 1}, interval = {1, 0};
 			struct pw_loop *l = pw_main_loop_get_loop(data->loop);
 			pw_loop_update_timer(l, data->timer, &timeout, &interval, false);
-			printf("stream node %"PRIu32"\n",
+			fprintf(stderr, "stream node %"PRIu32"\n",
 				pw_stream_get_node_id(data->stream));
 		}
 		break;
@@ -747,13 +726,13 @@ on_state_changed(void *userdata, enum pw_stream_state old,
 		}
 		break;
 	case PW_STREAM_STATE_ERROR:
-		printf("stream node %"PRIu32" error: %s\n",
+		fprintf(stderr, "stream node %"PRIu32" error: %s\n",
 				pw_stream_get_node_id(data->stream),
 				error);
 		pw_main_loop_quit(data->loop);
 		break;
 	case PW_STREAM_STATE_UNCONNECTED:
-		printf("stream node %"PRIu32" unconnected\n",
+		fprintf(stderr, "stream node %"PRIu32" unconnected\n",
 				pw_stream_get_node_id(data->stream));
 		pw_main_loop_quit(data->loop);
 		break;
@@ -784,7 +763,7 @@ on_param_changed(void *userdata, uint32_t id, const struct spa_pod *param)
 	int err;
 
 	if (data->verbose)
-		printf("stream param change: %s\n",
+		fprintf(stderr, "stream param change: %s\n",
 			spa_debug_type_find_name(spa_type_param, id));
 
 	if (id != SPA_PARAM_Format || param == NULL)
@@ -811,7 +790,7 @@ on_param_changed(void *userdata, uint32_t id, const struct spa_pod *param)
 	data->stride = data->dsf.layout.channels * SPA_ABS(data->dsf.layout.interleave);
 
 	if (data->verbose) {
-		printf("DSD: channels:%d bitorder:%s interleave:%d stride:%d\n",
+		fprintf(stderr, "DSD: channels:%d bitorder:%s interleave:%d stride:%d\n",
 				data->dsf.layout.channels,
 				data->dsf.layout.lsb ? "lsb" : "msb",
 				data->dsf.layout.interleave,
@@ -873,7 +852,7 @@ static void on_process(void *userdata)
 			fprintf(stderr, "fill error %d\n", n_fill_frames);
 		} else {
 			if (data->verbose)
-				printf("drain start\n");
+				fprintf(stderr, "drain start\n");
 		}
 	} else {
 		bool null_frame = false;
@@ -904,7 +883,7 @@ static void on_drained(void *userdata)
 	struct data *data = userdata;
 
 	if (data->verbose)
-		printf("stream drained\n");
+		fprintf(stderr, "stream drained\n");
 
 	data->drained = true;
 	pw_main_loop_quit(data->loop);
@@ -930,7 +909,7 @@ static void do_print_delay(void *userdata, uint64_t expirations)
 	struct data *data = userdata;
 	struct pw_time time;
 	pw_stream_get_time_n(data->stream, &time, sizeof(time));
-	printf("stream time: now:%"PRIi64" rate:%u/%u ticks:%"PRIu64
+	fprintf(stderr, "stream time: now:%"PRIi64" rate:%u/%u ticks:%"PRIu64
 			" delay:%"PRIi64" queued:%"PRIu64
 			" buffered:%"PRIi64" buffers:%u avail:%u size:%"PRIu64"\n",
 		time.now,
@@ -961,6 +940,8 @@ static const struct option long_options[] = {
 	{ "record",		no_argument,	   NULL, 'r' },
 	{ "playback",		no_argument,	   NULL, 'p' },
 	{ "midi",		no_argument,	   NULL, 'm' },
+	{ "dsd",		no_argument,	   NULL, 'd' },
+	{ "encoded",		no_argument,	   NULL, 'o' },
 
 	{ "remote",		required_argument, NULL, 'R' },
 
@@ -977,6 +958,7 @@ static const struct option long_options[] = {
 	{ "format",		required_argument, NULL, OPT_FORMAT },
 	{ "volume",		required_argument, NULL, OPT_VOLUME },
 	{ "quality",		required_argument, NULL, 'q' },
+	{ "raw",		no_argument, NULL, 'a' },
 
 	{ NULL, 0, NULL, 0 }
 };
@@ -1021,6 +1003,7 @@ static void show_usage(const char *name, bool is_error)
 	     "      --format                          Sample format %s (req. for rec) (default %s)\n"
 	     "      --volume                          Stream volume 0-1.0 (default %.3f)\n"
 	     "  -q  --quality                         Resampler quality (0 - 15) (default %d)\n"
+	     "  -a, --raw                             RAW mode\n"
 	     "\n"),
 	     DEFAULT_RATE,
 	     DEFAULT_CHANNELS,
@@ -1080,13 +1063,36 @@ static int midi_play(struct data *d, void *src, unsigned int n_frames, bool *nul
 		midi_file_read_event(d->midi.file, &ev);
 
 		if (d->verbose)
-			midi_file_dump_event(stdout, &ev);
+			midi_file_dump_event(stderr, &ev);
+
+		if (ev.type == MIDI_EVENT_TYPE_MIDI1) {
+			size_t size;
+			uint8_t *data;
+			uint64_t state = 0;
 
-		if (ev.data[0] == 0xff)
+			if (ev.data[0] == 0xff)
+				continue;
+
+			data = ev.data;
+			size = ev.size;
+
+			while (size > 0) {
+				uint32_t ump[4];
+				int ump_size = spa_ump_from_midi(&data, &size,
+						ump, sizeof(ump), 0, &state);
+				if (ump_size <= 0)
+					break;
+
+				spa_pod_builder_control(&b, frame, SPA_CONTROL_UMP);
+				spa_pod_builder_bytes(&b, ump, ump_size);
+			}
+		} else if (ev.type == MIDI_EVENT_TYPE_UMP) {
+			spa_pod_builder_control(&b, frame, SPA_CONTROL_UMP);
+			spa_pod_builder_bytes(&b, ev.data, ev.size);
+		}
+		else
 			continue;
 
-		spa_pod_builder_control(&b, frame, SPA_CONTROL_Midi);
-		spa_pod_builder_bytes(&b, ev.data, ev.size);
 		have_data = true;
 	}
 	spa_pod_builder_pop(&b, &f);
@@ -1111,16 +1117,17 @@ static int midi_record(struct data *d, void *src, unsigned int n_frames, bool *n
 	SPA_POD_SEQUENCE_FOREACH((struct spa_pod_sequence*)pod, c) {
 		struct midi_event ev;
 
-		if (c->type != SPA_CONTROL_Midi)
+		if (c->type != SPA_CONTROL_UMP)
 			continue;
 
 		ev.track = 0;
 		ev.sec = (frame + c->offset) / (float) d->position->clock.rate.denom;
 		ev.data = SPA_POD_BODY(&c->value),
 		ev.size = SPA_POD_BODY_SIZE(&c->value);
+		ev.type = MIDI_EVENT_TYPE_UMP;
 
 		if (d->verbose)
-			midi_file_dump_event(stdout, &ev);
+			midi_file_dump_event(stderr, &ev);
 
 		midi_file_write_event(d->midi.file, &ev);
 	}
@@ -1145,7 +1152,7 @@ static int setup_midifile(struct data *data)
 	}
 
 	if (data->verbose)
-		printf("midifile: opened file \"%s\" format %08x ntracks:%d div:%d\n",
+		fprintf(stderr, "midifile: opened file \"%s\" format %08x ntracks:%d div:%d\n",
 				data->filename,
 				data->midi.info.format, data->midi.info.ntracks,
 				data->midi.info.division);
@@ -1196,7 +1203,7 @@ static int setup_dsdfile(struct data *data)
 
 	if (data->dsf.file != NULL) {
 		if (data->verbose)
-			printf("dsffile: opened file \"%s\" channels:%d rate:%d "
+			fprintf(stderr, "dsffile: opened file \"%s\" channels:%d rate:%d "
 					"samples:%"PRIu64" bitorder:%s\n",
 				data->filename,
 				data->dsf.info.channels, data->dsf.info.rate,
@@ -1206,7 +1213,7 @@ static int setup_dsdfile(struct data *data)
 		data->fill = dsf_play;
 	} else {
 		if (data->verbose)
-			printf("dfffile: opened file \"%s\" channels:%d rate:%d "
+			fprintf(stderr, "dfffile: opened file \"%s\" channels:%d rate:%d "
 					"samples:%"PRIu64" bitorder:%s\n",
 				data->filename,
 				data->dff.info.channels, data->dff.info.rate,
@@ -1250,7 +1257,7 @@ static int setup_pipe(struct data *data)
 	data->fill = data->mode == mode_playback ?  stdin_play : stdout_record;
 
 	if (data->verbose)
-		printf("PIPE: rate=%u channels=%u fmt=%s samplesize=%u stride=%u\n",
+		fprintf(stderr, "PIPE: rate=%u channels=%u fmt=%s samplesize=%u stride=%u\n",
 				data->rate, data->channels,
 				info->name, info->width, data->stride);
 
@@ -1420,7 +1427,7 @@ static int setup_encodedfile(struct data *data)
 	data->fill = encoded_playback_fill;
 
 	if (data->verbose) {
-		printf("Opened file \"%s\" with encoded audio; channels:%d rate:%d bitrate: %d time units %d/%d\n",
+		fprintf(stderr, "Opened file \"%s\" with encoded audio; channels:%d rate:%d bitrate: %d time units %d/%d\n",
 		       data->filename, data->channels, data->rate, data->bitrate,
 		       data->encoded.audio_stream->time_base.num, data->encoded.audio_stream->time_base.den);
 	}
@@ -1467,9 +1474,9 @@ static int setup_sndfile(struct data *data)
 	}
 
 	if (data->verbose)
-		printf("sndfile: opened file \"%s\" format %08x channels:%d rate:%d\n",
+		fprintf(stderr, "sndfile: opened file \"%s\" format %08x channels:%d rate:%d\n",
 				data->filename, info.format, info.channels, info.samplerate);
-	if (data->channels > 0 && info.channels != data->channels) {
+	if (data->channels > 0 && info.channels != (int)data->channels) {
 		fprintf(stderr, "sndfile: given channels (%u) don't match file channels (%d)\n",
 				data->channels, info.channels);
 		return -EINVAL;
@@ -1494,9 +1501,9 @@ static int setup_sndfile(struct data *data)
 				def = true;
 			}
 			if (data->verbose) {
-				printf("sndfile: using %s channel map: ", def ? "default" : "file");
+				fprintf(stderr, "sndfile: using %s channel map: ", def ? "default" : "file");
 				channelmap_print(&data->channelmap);
-				printf("\n");
+				fprintf(stderr, "\n");
 			}
 		}
 		fill_properties(data);
@@ -1510,7 +1517,7 @@ static int setup_sndfile(struct data *data)
 		return -EIO;
 
 	if (data->verbose)
-		printf("PCM: fmt:%s rate:%u channels:%u width:%u\n",
+		fprintf(stderr, "PCM: fmt:%s rate:%u channels:%u width:%u\n",
 				fi->name, data->rate, data->channels, fi->width);
 
 	/* we read and write S24 as S32 with sndfile */
@@ -1590,7 +1597,7 @@ static int setup_properties(struct data *data)
 	}
 
 	if (data->verbose)
-		printf("rate:%d latency:%u (%.3fs)\n",
+		fprintf(stderr, "rate:%d latency:%u (%.3fs)\n",
 				data->rate, nom, data->rate ? (double)nom/data->rate : 0.0f);
 	if (nom && pw_properties_get(data->props, PW_KEY_NODE_LATENCY) == NULL)
 		pw_properties_setf(data->props, PW_KEY_NODE_LATENCY, "%u/%u", nom, data->rate);
@@ -1664,9 +1671,9 @@ int main(int argc, char *argv[])
 	}
 
 #ifdef HAVE_PW_CAT_FFMPEG_INTEGRATION
-	while ((c = getopt_long(argc, argv, "hvprmdoR:q:P:", long_options, NULL)) != -1) {
+	while ((c = getopt_long(argc, argv, "hvprmdoR:q:P:a", long_options, NULL)) != -1) {
 #else
-	while ((c = getopt_long(argc, argv, "hvprmdR:q:P:", long_options, NULL)) != -1) {
+	while ((c = getopt_long(argc, argv, "hvprmdR:q:P:a", long_options, NULL)) != -1) {
 #endif
 
 		switch (c) {
@@ -1718,6 +1725,10 @@ int main(int argc, char *argv[])
 			data.quality = atoi(optarg);
 			break;
 
+		case 'a':
+			data.raw = true;
+			break;
+
 		case OPT_MEDIA_TYPE:
 			data.media_type = optarg;
 			break;
@@ -1846,11 +1857,7 @@ int main(int argc, char *argv[])
 	pw_loop_add_signal(l, SIGINT, do_quit, &data);
 	pw_loop_add_signal(l, SIGTERM, do_quit, &data);
 
-	data.context = pw_context_new(l,
-			pw_properties_new(
-				PW_KEY_CONFIG_NAME, "client-rt.conf",
-				NULL),
-			0);
+	data.context = pw_context_new(l, NULL, 0);
 	if (!data.context) {
 		fprintf(stderr, "error: pw_context_new() failed: %m\n");
 		goto error_no_context;
@@ -1867,7 +1874,7 @@ int main(int argc, char *argv[])
 	}
 	pw_core_add_listener(data.core, &data.core_listener, &core_events, &data);
 
-	if (spa_streq(data.filename, "-")) {
+	if (data.raw) {
 		ret = setup_pipe(&data);
 	} else {
 		switch (data.data_type) {
@@ -1952,7 +1959,7 @@ int main(int argc, char *argv[])
 				SPA_FORMAT_mediaType,		SPA_POD_Id(SPA_MEDIA_TYPE_application),
 				SPA_FORMAT_mediaSubtype,	SPA_POD_Id(SPA_MEDIA_SUBTYPE_control));
 
-		pw_properties_set(data.props, PW_KEY_FORMAT_DSP, "8 bit raw midi");
+		pw_properties_set(data.props, PW_KEY_FORMAT_DSP, "32 bit raw UMP");
 		break;
 	case TYPE_DSD:
 	{
@@ -2008,7 +2015,7 @@ int main(int argc, char *argv[])
 	pw_stream_add_listener(data.stream, &data.stream_listener, &stream_events, &data);
 
 	if (data.verbose)
-		printf("connecting %s stream; target=%s\n",
+		fprintf(stderr, "connecting %s stream; target=%s\n",
 				data.mode == mode_playback ? "playback" : "record",
 				data.target);
 
@@ -2032,11 +2039,11 @@ int main(int argc, char *argv[])
 		const char *key, *val;
 
 		if ((props = pw_stream_get_properties(data.stream)) != NULL) {
-			printf("stream properties:\n");
+			fprintf(stderr, "stream properties:\n");
 			pstate = NULL;
 			while ((key = pw_properties_iterate(props, &pstate)) != NULL &&
 				(val = pw_properties_get(props, key)) != NULL) {
-				printf("\t%s = \"%s\"\n", key, val);
+				fprintf(stderr, "\t%s = \"%s\"\n", key, val);
 			}
 		}
 	}
diff --git a/src/tools/pw-cli.c b/src/tools/pw-cli.c
index 4b0e5a9d..467a4be6 100644
--- a/src/tools/pw-cli.c
+++ b/src/tools/pw-cli.c
@@ -10,9 +10,6 @@
 #include <signal.h>
 #include <string.h>
 #include <ctype.h>
-#if !defined(__FreeBSD__) && !defined(__MidnightBSD__)
-#include <alloca.h>
-#endif
 #include <getopt.h>
 #include <fnmatch.h>
 #ifdef HAVE_READLINE
@@ -30,6 +27,7 @@
 #include <spa/utils/result.h>
 #include <spa/utils/string.h>
 #include <spa/debug/pod.h>
+#include <spa/debug/file.h>
 #include <spa/utils/keys.h>
 #include <spa/utils/json-pod.h>
 #include <spa/pod/dynamic.h>
@@ -163,14 +161,17 @@ static void print_params(struct spa_param_info *params, uint32_t n_params, char
 	}
 }
 
+#if 0
 static bool do_not_implemented(struct data *data, const char *cmd, char *args, char **error)
 {
 	*error = spa_aprintf("Command \"%s\" not yet implemented", cmd);
 	return false;
 }
+#endif
 
 static bool do_help(struct data *data, const char *cmd, char *args, char **error);
 static bool do_load_module(struct data *data, const char *cmd, char *args, char **error);
+static bool do_unload_module(struct data *data, const char *cmd, char *args, char **error);
 static bool do_list_objects(struct data *data, const char *cmd, char *args, char **error);
 static bool do_connect(struct data *data, const char *cmd, char *args, char **error);
 static bool do_disconnect(struct data *data, const char *cmd, char *args, char **error);
@@ -194,7 +195,7 @@ static bool do_quit(struct data *data, const char *cmd, char *args, char **error
 static const struct command command_list[] = {
 	{ "help", "h", "Show this help", do_help },
 	{ "load-module", "lm", "Load a module. <module-name> [<module-arguments>]", do_load_module },
-	{ "unload-module", "um", "Unload a module. <module-var>", do_not_implemented },
+	{ "unload-module", "um", "Unload a module. <module-var>", do_unload_module },
 	{ "connect", "con", "Connect to a remote. [<remote-name>]", do_connect },
 	{ "disconnect", "dis", "Disconnect from a remote. [<remote-var>]", do_disconnect },
 	{ "list-remotes", "lr", "List connected remotes.", do_list_remotes },
@@ -263,6 +264,29 @@ static bool do_load_module(struct data *data, const char *cmd, char *args, char
 	return true;
 }
 
+static bool do_unload_module(struct data *data, const char *cmd, char *args, char **error)
+{
+	char *a[1];
+	int n;
+	struct pw_impl_module *module;
+	uint32_t idx;
+
+	n = pw_split_ip(args, WHITESPACE, 1, a);
+	if (n < 1) {
+		*error = spa_aprintf("%s <module-var>", cmd);
+		return false;
+	}
+	idx = atoi(a[0]);
+	module = pw_map_lookup(&data->vars, idx);
+	if (module == NULL) {
+		*error = spa_aprintf("%s: unknown module '%s'", cmd, a[0]);
+		return false;
+	}
+	pw_map_remove(&data->vars, idx);
+	pw_impl_module_destroy(module);
+	return true;
+}
+
 static void on_core_info(void *_data, const struct pw_core_info *info)
 {
 	struct remote_data *rd = _data;
@@ -1780,6 +1804,7 @@ static bool do_set_param(struct data *data, const char *cmd, char *args, char **
 	spa_auto(spa_pod_dynamic_builder) b = { 0 };
 	const struct spa_type_info *ti;
 	struct spa_pod *pod;
+	struct spa_error_location loc;
 
 	spa_pod_dynamic_builder_init(&b, buffer, sizeof(buffer), 4096);
 
@@ -1804,8 +1829,13 @@ static bool do_set_param(struct data *data, const char *cmd, char *args, char **
 		*error = spa_aprintf("%s: unknown param type: %s", cmd, a[1]);
 		return false;
 	}
-	if ((res = spa_json_to_pod(&b.b, 0, ti, a[2], strlen(a[2]))) < 0) {
-		*error = spa_aprintf("%s: can't make pod: %s", cmd, spa_strerror(res));
+	if ((res = spa_json_to_pod_checked(&b.b, 0, ti, a[2], strlen(a[2]), &loc)) < 0) {
+		if (loc.line != 0) {
+			spa_debug_file_error_location(stderr, &loc,
+					"syntax error in json '%s': %s",
+					a[2], loc.reason);
+		}
+		*error = spa_aprintf("%s: invalid pod: %s", cmd, loc.reason);
 		return false;
 	}
 	if ((pod = spa_pod_builder_deref(&b.b, 0)) == NULL) {
@@ -2129,15 +2159,6 @@ children_of(struct remote_data *rd, uint32_t parent_id,
 	return count;
 }
 
-#define INDENT(_level) \
-	({ \
-		int __level = (_level); \
-		char *_indent = alloca(__level + 1); \
-		memset(_indent, '\t', __level); \
-		_indent[__level] = '\0'; \
-		(const char *)_indent; \
-	})
-
 static bool parse(struct data *data, char *buf, char **error)
 {
 	char *a[2];
diff --git a/src/tools/pw-config.c b/src/tools/pw-config.c
index de38095f..bfc64e42 100644
--- a/src/tools/pw-config.c
+++ b/src/tools/pw-config.c
@@ -66,8 +66,7 @@ static int do_merge_section(void *data, const char *location, const char *sectio
 	int l;
 	const char *value;
 
-	spa_json_init(&it[0], str, len);
-	if ((l = spa_json_next(&it[0], &value)) <= 0)
+	if ((l = spa_json_begin(&it[0], str, len, &value)) <= 0)
 		return 0;
 
 	if (spa_json_is_array(value, l)) {
diff --git a/src/tools/pw-container.c b/src/tools/pw-container.c
index daa2c7b6..02b7c881 100644
--- a/src/tools/pw-container.c
+++ b/src/tools/pw-container.c
@@ -96,8 +96,8 @@ static void core_event_done(void *object, uint32_t id, int seq)
 static int roundtrip(struct data *data)
 {
 	struct spa_hook core_listener;
-	const struct pw_core_events core_events = {
-	PW_VERSION_CORE_EVENTS,
+	static const struct pw_core_events core_events = {
+		PW_VERSION_CORE_EVENTS,
 		.done = core_event_done,
 	};
 	spa_zero(core_listener);
@@ -150,7 +150,7 @@ int main(int argc, char *argv[])
 	};
 	struct spa_error_location loc;
 	int c, res, listen_fd, close_fd[2];
-	char temp[PATH_MAX] = "/tmp/pipewire-XXXXXX";
+	char temp[] = "/tmp/pipewire-XXXXXX";
 	struct sockaddr_un sockaddr = {0};
 
 	data.props = pw_properties_new(
@@ -209,8 +209,8 @@ int main(int argc, char *argv[])
 
 	data.core = pw_context_connect(data.context,
 			pw_properties_new(
-				PW_KEY_REMOTE_NAME, opt_remote ? opt_remote :
-					("[" PW_DEFAULT_REMOTE "-manager," PW_DEFAULT_REMOTE "]"),
+				PW_KEY_REMOTE_INTENTION, "manager",
+				PW_KEY_REMOTE_NAME, opt_remote,
 				NULL),
 			0);
 	if (data.core == NULL) {
diff --git a/src/tools/pw-dot.c b/src/tools/pw-dot.c
index 089f0565..8536e130 100644
--- a/src/tools/pw-dot.c
+++ b/src/tools/pw-dot.c
@@ -1276,7 +1276,7 @@ static int get_data_from_json(struct data *data, const char *json_path)
 	int fd, len;
 	void *json;
 	struct stat sbuf;
-	struct spa_json it[2];
+	struct spa_json it[1];
 	const char *value;
 	struct spa_error_location loc;
 
@@ -1296,18 +1296,16 @@ static int get_data_from_json(struct data *data, const char *json_path)
 	}
 
 	close(fd);
-	spa_json_init(&it[0], json, sbuf.st_size);
-
-	if (spa_json_enter_array(&it[0], &it[1]) <= 0) {
+	if (spa_json_begin_array(&it[0], json, sbuf.st_size) <= 0) {
 		fprintf(stderr, "expected top-level array in JSON file '%s'\n", json_path);
 		munmap(json, sbuf.st_size);
 		return -1;
 	}
 
-	while ((len = spa_json_next(&it[1], &value)) > 0 && spa_json_is_object(value, len)) {
+	while ((len = spa_json_next(&it[0], &value)) > 0 && spa_json_is_object(value, len)) {
 		struct pw_properties *obj;
 		obj = pw_properties_new(NULL, NULL);
-		len = spa_json_container_len(&it[1], value, len);
+		len = spa_json_container_len(&it[0], value, len);
 		pw_properties_update_string(obj, value, len);
 		handle_json_obj(data, obj);
 		pw_properties_free(obj);
diff --git a/src/tools/pw-dump.c b/src/tools/pw-dump.c
index e55e2e60..179e0c67 100644
--- a/src/tools/pw-dump.c
+++ b/src/tools/pw-dump.c
@@ -1126,11 +1126,8 @@ static void json_dump_val(struct data *d, const char *key, struct spa_json *it,
 		char val[1024];
 		put_begin(d, key, "{", STATE_SIMPLE);
 		spa_json_enter(it, &sub);
-		while (spa_json_get_string(&sub, val, sizeof(val)) > 0) {
-			if ((len = spa_json_next(&sub, &value)) <= 0)
-				break;
+		while ((len = spa_json_object_next(&sub, val, sizeof(val), &value)) > 0)
 			json_dump_val(d, val, &sub, value, len);
-		}
 		put_end(d, "}", STATE_SIMPLE);
 	} else if (spa_json_is_string(value, len)) {
 		put_encoded_string(d, key, strndupa(value, len));
@@ -1144,8 +1141,7 @@ static void json_dump(struct data *d, const char *key, const char *value)
 	struct spa_json it[1];
 	int len;
 	const char *val;
-	spa_json_init(&it[0], value, strlen(value));
-	if ((len = spa_json_next(&it[0], &val)) >= 0)
+	if ((len = spa_json_begin(&it[0], value, strlen(value), &val)) >= 0)
 		json_dump_val(d, key, &it[0], val, len);
 }
 
@@ -1608,8 +1604,8 @@ int main(int argc, char *argv[])
 
 	data.core = pw_context_connect(data.context,
 			pw_properties_new(
-				PW_KEY_REMOTE_NAME, opt_remote ? opt_remote :
-					("[" PW_DEFAULT_REMOTE "-manager," PW_DEFAULT_REMOTE "]"),
+				PW_KEY_REMOTE_INTENTION, "manager",
+				PW_KEY_REMOTE_NAME, opt_remote,
 				NULL),
 			0);
 	if (data.core == NULL) {
diff --git a/src/tools/pw-loopback.c b/src/tools/pw-loopback.c
index 1f77f6ae..65a613c1 100644
--- a/src/tools/pw-loopback.c
+++ b/src/tools/pw-loopback.c
@@ -77,9 +77,9 @@ static void show_help(struct data *data, const char *name, bool error)
 		"  -l, --latency                         Desired latency in ms\n"
 		"  -d, --delay                           Desired delay in float s\n"
 		"  -C  --capture                         Capture source to connect to (name or serial)\n"
-		"      --capture-props                   Capture stream properties\n"
+		"  -i  --capture-props                   Capture stream properties\n"
 		"  -P  --playback                        Playback sink to connect to (name or serial)\n"
-		"      --playback-props                  Playback stream properties\n",
+		"  -o  --playback-props                  Playback stream properties\n",
 		name,
 		data->opt_node_name,
 		data->opt_group_name,
diff --git a/src/tools/pw-mididump.c b/src/tools/pw-mididump.c
index 7fa672f9..a29bb725 100644
--- a/src/tools/pw-mididump.c
+++ b/src/tools/pw-mididump.c
@@ -13,6 +13,7 @@
 #include <spa/control/control.h>
 #include <spa/param/audio/format-utils.h>
 #include <spa/param/props.h>
+#include <spa/debug/mem.h>
 
 #include <pipewire/pipewire.h>
 #include <pipewire/filter.h>
@@ -45,7 +46,7 @@ static int dump_file(const char *filename)
 		return -1;
 	}
 
-	printf("opened %s\n", filename);
+	printf("opened %s format:%u ntracks:%u division:%u\n", filename, info.format, info.ntracks, info.division);
 
 	while (midi_file_read_event(file, &ev) == 1) {
 		midi_file_dump_event(stdout, &ev);
@@ -86,15 +87,16 @@ static void on_process(void *_data, struct spa_io_position *position)
 	SPA_POD_SEQUENCE_FOREACH((struct spa_pod_sequence*)pod, c) {
 		struct midi_event ev;
 
-		if (c->type != SPA_CONTROL_Midi)
+		if (c->type != SPA_CONTROL_UMP)
 			continue;
 
 		ev.track = 0;
 		ev.sec = (frame + c->offset) / (float) position->clock.rate.denom;
 		ev.data = SPA_POD_BODY(&c->value),
 		ev.size = SPA_POD_BODY_SIZE(&c->value);
+		ev.type = MIDI_EVENT_TYPE_UMP;
 
-		printf("%4d: ", c->offset);
+		fprintf(stdout, "%4d: ", c->offset);
 		midi_file_dump_event(stdout, &ev);
 	}
 
@@ -139,7 +141,7 @@ static int dump_filter(struct data *data)
 			PW_FILTER_PORT_FLAG_MAP_BUFFERS,
 			sizeof(struct port),
 			pw_properties_new(
-				PW_KEY_FORMAT_DSP, "8 bit raw midi",
+				PW_KEY_FORMAT_DSP, "32 bit raw UMP",
 				PW_KEY_PORT_NAME, "input",
 				NULL),
 			NULL, 0);
diff --git a/src/tools/pw-mon.c b/src/tools/pw-mon.c
index 381778b3..e66de979 100644
--- a/src/tools/pw-mon.c
+++ b/src/tools/pw-mon.c
@@ -886,8 +886,8 @@ int main(int argc, char *argv[])
 
 	data.core = pw_context_connect(data.context,
 			pw_properties_new(
-				PW_KEY_REMOTE_NAME, opt_remote ? opt_remote :
-					("[" PW_DEFAULT_REMOTE "-manager," PW_DEFAULT_REMOTE "]"),
+				PW_KEY_REMOTE_INTENTION, "manager",
+				PW_KEY_REMOTE_NAME, opt_remote,
 				NULL),
 			0);
 	if (data.core == NULL) {
diff --git a/src/tools/pw-profiler.c b/src/tools/pw-profiler.c
index 277c9f9b..a73cc7ec 100644
--- a/src/tools/pw-profiler.c
+++ b/src/tools/pw-profiler.c
@@ -9,6 +9,7 @@
 
 #include <spa/utils/result.h>
 #include <spa/utils/string.h>
+#include <spa/utils/json.h>
 #include <spa/pod/parser.h>
 #include <spa/debug/types.h>
 
@@ -36,6 +37,8 @@ struct data {
 
 	const char *filename;
 	FILE *output;
+	bool json_dump;
+	uint32_t iterations;
 
 	int64_t count;
 	int64_t start_status;
@@ -58,28 +61,74 @@ struct measurement {
 	int64_t awake;
 	int64_t finish;
 	int32_t status;
+	struct spa_fraction latency;
+	int32_t xrun_count;
 };
 
 struct point {
 	int64_t count;
 	float cpu_load[3];
 	struct spa_io_clock clock;
+	int transport_state;
 	struct measurement driver;
 	struct measurement follower[MAX_FOLLOWERS];
 };
 
+static const char *status_to_string(int status)
+{
+	switch (status) {
+	case 0:
+		return "not-triggered";
+	case 1:
+		return "triggered";
+	case 2:
+		return "awake";
+	case 3:
+		return "finished";
+	case 4:
+		return "inactive";
+	}
+	return "unknown";
+}
+static const char *transport_to_string(int state)
+{
+	switch(state) {
+	case SPA_IO_POSITION_STATE_STOPPED:
+		return "stopped";
+	case SPA_IO_POSITION_STATE_STARTING:
+		return "starting";
+	case SPA_IO_POSITION_STATE_RUNNING:
+		return "running";
+	}
+	return "unknown";
+}
+
 static int process_info(struct data *d, const struct spa_pod *pod, struct point *point)
 {
-	return spa_pod_parse_struct(pod,
+	int res;
+	char cpu_load0[128], cpu_load1[128], cpu_load2[128];
+
+	res = spa_pod_parse_struct(pod,
 			SPA_POD_Long(&point->count),
 			SPA_POD_Float(&point->cpu_load[0]),
 			SPA_POD_Float(&point->cpu_load[1]),
 			SPA_POD_Float(&point->cpu_load[2]));
+	if (d->json_dump) {
+		fprintf(stdout, "{ \"type\": \"info\", \"count\": %"PRIu64", "
+				"\"cpu_load0\": %s, \"cpu_load1\": %s, \"cpu_load2\": %s },\n",
+				point->count,
+				spa_json_format_float(cpu_load0, sizeof(cpu_load0), point->cpu_load[0]),
+				spa_json_format_float(cpu_load1, sizeof(cpu_load1), point->cpu_load[1]),
+				spa_json_format_float(cpu_load2, sizeof(cpu_load2), point->cpu_load[2]));
+	}
+	return res;
 }
 
 static int process_clock(struct data *d, const struct spa_pod *pod, struct point *point)
 {
-	return spa_pod_parse_struct(pod,
+	int res;
+	char val[128];
+	res = spa_pod_parse_struct(pod,
 			SPA_POD_Int(&point->clock.flags),
 			SPA_POD_Int(&point->clock.id),
 			SPA_POD_Stringn(point->clock.name, sizeof(point->clock.name)),
@@ -89,7 +138,25 @@ static int process_clock(struct data *d, const struct spa_pod *pod, struct point
 			SPA_POD_Long(&point->clock.duration),
 			SPA_POD_Long(&point->clock.delay),
 			SPA_POD_Double(&point->clock.rate_diff),
-			SPA_POD_Long(&point->clock.next_nsec));
+			SPA_POD_Long(&point->clock.next_nsec),
+			SPA_POD_Int(&point->transport_state),
+			SPA_POD_OPT_Int(&point->clock.cycle),
+			SPA_POD_OPT_Long(&point->clock.xrun));
+	if (d->json_dump) {
+		fprintf(stdout, "{ \"type\": \"clock\", \"flags\": %u, \"id\": %u, "
+				"\"name\": \"%s\", \"nsec\": %"PRIu64", \"rate\": \"%u/%u\", "
+				"\"position\": %"PRIu64", \"duration\": %"PRIu64", "
+				"\"delay\": %"PRIu64", \"diff\": %s, \"next_nsec\": %"PRIu64", "
+				"\"transport\": \"%s\", \"cycle\": %u, \"xrun\": %"PRIu64" },\n",
+				point->clock.flags, point->clock.id, point->clock.name,
+				point->clock.nsec, point->clock.rate.num, point->clock.rate.denom,
+				point->clock.position, point->clock.duration,
+				point->clock.delay,
+				spa_json_format_float(val, sizeof(val), (float)point->clock.rate_diff),
+				point->clock.next_nsec, transport_to_string(point->transport_state),
+				point->clock.cycle, point->clock.xrun);
+	}
+	return res;
 }
 
 static int process_driver_block(struct data *d, const struct spa_pod *pod, struct point *point)
@@ -107,14 +174,27 @@ static int process_driver_block(struct data *d, const struct spa_pod *pod, struc
 			SPA_POD_Long(&driver.signal),
 			SPA_POD_Long(&driver.awake),
 			SPA_POD_Long(&driver.finish),
-			SPA_POD_Int(&driver.status))) < 0)
+			SPA_POD_Int(&driver.status),
+			SPA_POD_Fraction(&driver.latency),
+			SPA_POD_Int(&driver.xrun_count))) < 0)
 		return res;
 
+	if (d->json_dump) {
+		fprintf(stdout, "{ \"type\": \"driver\", \"id\": %u, \"name\": \"%s\", \"prev\": %"PRIu64", "
+				"\"signal\": %"PRIu64", \"awake\": %"PRIu64", "
+				"\"finish\": %"PRIu64", \"status\": \"%s\", \"latency\": \"%u/%u\", "
+				"\"xrun_count\": %u },\n",
+				driver_id, name, driver.prev_signal, driver.signal,
+				driver.awake, driver.finish, status_to_string(driver.status),
+				driver.latency.num, driver.latency.denom,
+				driver.xrun_count);
+	}
+
 	if (d->driver_id == 0) {
 		d->driver_id = driver_id;
-		printf("logging driver %u\n", driver_id);
+		pw_log_info("logging driver %u", driver_id);
 	}
-	else if (d->driver_id != driver_id)
+	else if (d->driver_id != driver_id && !d->json_dump)
 		return -1;
 
 	point->driver = driver;
@@ -143,7 +223,8 @@ static int add_follower(struct data *d, uint32_t id, const char *name)
 	strncpy(d->followers[idx].name, name, MAX_NAME);
 	d->followers[idx].name[MAX_NAME-1] = '\0';
 	d->followers[idx].id = id;
-	printf("logging follower %u (\"%s\")\n", id, name);
+
+	pw_log_info("logging follower %u (\"%s\")", id, name);
 
 	return idx;
 }
@@ -163,9 +244,23 @@ static int process_follower_block(struct data *d, const struct spa_pod *pod, str
 			SPA_POD_Long(&m.signal),
 			SPA_POD_Long(&m.awake),
 			SPA_POD_Long(&m.finish),
-			SPA_POD_Int(&m.status))) < 0)
+			SPA_POD_Int(&m.status),
+			SPA_POD_Fraction(&m.latency),
+			SPA_POD_Int(&m.xrun_count))) < 0)
 		return res;
 
+	if (d->json_dump) {
+		fprintf(stdout, "{ \"type\": \"follower\", \"id\": %u, \"name\": \"%s\", \"prev\": %"PRIu64", "
+				"\"signal\": %"PRIu64", \"awake\": %"PRIu64", "
+				"\"finish\": %"PRIu64", \"status\": \"%s\", \"latency\": \"%u/%u\", "
+				"\"xrun_count\": %u },\n",
+				id, name, m.prev_signal, m.signal,
+				m.awake, m.finish, status_to_string(m.status),
+				m.latency.num, m.latency.denom,
+				m.xrun_count);
+	}
+
+
 	if ((idx = find_follower(d, id, name)) < 0) {
 		if ((idx = add_follower(d, id, name)) < 0) {
 			pw_log_warn("too many followers");
@@ -176,6 +271,39 @@ static int process_follower_block(struct data *d, const struct spa_pod *pod, str
 	return 0;
 }
 
+static int process_follower_clock(struct data *d, const struct spa_pod *pod, struct point *point)
+{
+	int res;
+	char val[128];
+	struct spa_io_clock clock;
+
+	res = spa_pod_parse_struct(pod,
+			SPA_POD_Int(&clock.id),
+			SPA_POD_Stringn(clock.name, sizeof(clock.name)),
+			SPA_POD_Long(&clock.nsec),
+			SPA_POD_Fraction(&clock.rate),
+			SPA_POD_Long(&clock.position),
+			SPA_POD_Long(&clock.duration),
+			SPA_POD_Long(&clock.delay),
+			SPA_POD_Double(&clock.rate_diff),
+			SPA_POD_Long(&clock.next_nsec),
+			SPA_POD_Long(&clock.xrun));
+	if (d->json_dump) {
+		fprintf(stdout, "{ \"type\": \"followerClock\", \"id\": %u, "
+				"\"name\": \"%s\", \"nsec\": %"PRIu64", \"rate\": \"%u/%u\", "
+				"\"position\": %"PRIu64", \"duration\": %"PRIu64", "
+				"\"delay\": %"PRIu64", \"diff\": %s, \"next_nsec\": %"PRIu64", "
+				"\"xrun\": %"PRIu64" },\n",
+				clock.id, clock.name,
+				clock.nsec, clock.rate.num, clock.rate.denom,
+				clock.position, clock.duration,
+				clock.delay,
+				spa_json_format_float(val, sizeof(val), (float)clock.rate_diff),
+				clock.next_nsec, clock.xrun);
+	}
+	return res;
+}
+
 static void dump_point(struct data *d, struct point *point)
 {
 	int i;
@@ -224,7 +352,7 @@ static void dump_point(struct data *d, struct point *point)
 		d->last_status = point->clock.nsec;
 	}
 	else if (point->clock.nsec - d->last_status > SPA_NSEC_PER_SEC) {
-		printf("logging %"PRIi64" samples  %"PRIi64" seconds [CPU %f %f %f]\r",
+		fprintf(stderr, "logging %"PRIi64" samples  %"PRIi64" seconds [CPU %f %f %f]\r",
 				d->count, (int64_t) ((d->last_status - d->start_status) / SPA_NSEC_PER_SEC),
 				point->cpu_load[0], point->cpu_load[1], point->cpu_load[2]);
 		d->last_status = point->clock.nsec;
@@ -240,7 +368,7 @@ static void dump_scripts(struct data *d)
 	if (d->driver_id == 0)
 		return;
 
-	printf("\ndumping scripts for %d followers\n", d->n_followers);
+	fprintf(stderr, "\ndumping scripts for %d followers\n", d->n_followers);
 
 	out = fopen("Timing1.plot", "we");
 	if (out == NULL) {
@@ -254,9 +382,9 @@ static void dump_scripts(struct data *d)
 			"set title \"Audio driver timing\"\n"
 			"set xlabel \"audio cycles\"\n"
 			"set ylabel \"usec\"\n"
-			"plot \"%1$s\" using 3 title \"Audio driver delay\" with lines, "
-			"\"%1$s\" using 1 title \"Audio period\" with lines,"
-			"\"%1$s\" using 4 title \"Audio estimated\" with lines\n"
+			"plot \"%1$s\" using 3 title \"Audio driver delay (h/w ptr - wakeup time)\" with lines, "
+			"\"%1$s\" using 1 title \"Audio period (current wakeup - prev wakeup)\" with lines,"
+			"\"%1$s\" using 4 title \"Audio estimated (cycle period or quantum)\" with lines\n"
 			"unset multiplot\n"
 			"unset output\n", d->filename);
 		fclose(out);
@@ -270,7 +398,7 @@ static void dump_scripts(struct data *d)
 			"set output 'Timing2.svg\n"
 			"set terminal svg\n"
 			"set grid\n"
-			"set title \"Driver end date\"\n"
+			"set title \"Driver end date (total cycle processing time)\"\n"
 			"set xlabel \"audio cycles\"\n"
 			"set ylabel \"usec\"\n"
 			"plot  \"%s\" using 2 title \"Driver end date\" with lines \n"
@@ -287,7 +415,8 @@ static void dump_scripts(struct data *d)
 			"set terminal svg\n"
 			"set multiplot\n"
 			"set grid\n"
-			"set title \"Clients end date\"\n"
+			"set key tmargin\n"
+			"set title \"Clients end date (scheduled -> finished)\"\n"
 			"set xlabel \"audio cycles\"\n"
 			"set ylabel \"usec\"\n"
 			"plot "
@@ -317,7 +446,8 @@ static void dump_scripts(struct data *d)
 			"set terminal svg\n"
 			"set multiplot\n"
 			"set grid\n"
-			"set title \"Clients scheduling latency\"\n"
+			"set key tmargin\n"
+			"set title \"Clients scheduling latency (scheduled -> active)\"\n"
 			"set xlabel \"audio cycles\"\n"
 			"set ylabel \"usec\"\n"
 			"plot ");
@@ -344,7 +474,8 @@ static void dump_scripts(struct data *d)
 			"set terminal svg\n"
 			"set multiplot\n"
 			"set grid\n"
-			"set title \"Clients duration\"\n"
+			"set key tmargin\n"
+			"set title \"Clients duration (active -> finished)\"\n"
 			"set xlabel \"audio cycles\"\n"
 			"set ylabel \"usec\"\n"
 			"plot ");
@@ -431,6 +562,9 @@ static void profiler_profile(void *data, const struct spa_pod *pod)
 			case SPA_PROFILER_followerBlock:
 				process_follower_block(d, &p->value, &point);
 				break;
+			case SPA_PROFILER_followerClock:
+				process_follower_clock(d, &p->value, &point);
+				break;
 			default:
 				break;
 			}
@@ -440,7 +574,13 @@ static void profiler_profile(void *data, const struct spa_pod *pod)
 		if (res < 0)
 			continue;
 
-		dump_point(d, &point);
+		if (!d->json_dump)
+			dump_point(d, &point);
+
+		if (d->iterations > 0 && --d->iterations == 0) {
+			pw_main_loop_quit(d->loop);
+			break;
+		}
 	}
 }
 
@@ -468,7 +608,7 @@ static void registry_event_global(void *data, uint32_t id,
 	if (proxy == NULL)
 		goto error_proxy;
 
-	printf("Attaching to Profiler id:%d\n", id);
+	pw_log_info("Attaching to Profiler id:%d", id);
 	d->profiler = proxy;
 	pw_proxy_add_object_listener(proxy, &d->profiler_listener, &profiler_events, d);
 
@@ -526,7 +666,9 @@ static void show_help(const char *name, bool error)
 		"  -h, --help                            Show this help\n"
 		"      --version                         Show version\n"
 		"  -r, --remote                          Remote daemon name\n"
-		"  -o, --output                          Profiler output name (default \"%s\")\n",
+		"  -o, --output                          Profiler output name (default \"%s\")\n"
+		"  -J, --json                            Dump raw data as JSON\n"
+		"  -n, --iterations                      Collect this many samples\n",
 		name,
 		DEFAULT_FILENAME);
 }
@@ -542,6 +684,8 @@ int main(int argc, char *argv[])
 		{ "version",	no_argument,		NULL, 'V' },
 		{ "remote",	required_argument,	NULL, 'r' },
 		{ "output",	required_argument,	NULL, 'o' },
+		{ "json",	no_argument,		NULL, 'J' },
+		{ "iterations",	required_argument,	NULL, 'n' },
 		{ NULL, 0, NULL, 0}
 	};
 	int c;
@@ -549,7 +693,7 @@ int main(int argc, char *argv[])
 	setlocale(LC_ALL, "");
 	pw_init(&argc, &argv);
 
-	while ((c = getopt_long(argc, argv, "hVr:o:", long_options, NULL)) != -1) {
+	while ((c = getopt_long(argc, argv, "hVr:o:Jn:", long_options, NULL)) != -1) {
 		switch (c) {
 		case 'h':
 			show_help(argv[0], false);
@@ -568,6 +712,12 @@ int main(int argc, char *argv[])
 		case 'r':
 			opt_remote = optarg;
 			break;
+		case 'J':
+			data.json_dump = true;
+			break;
+		case 'n':
+			spa_atou32(optarg, &data.iterations, 10);
+			break;
 		default:
 			show_help(argv[0], true);
 			return -1;
@@ -604,14 +754,17 @@ int main(int argc, char *argv[])
 
 	data.filename = opt_output;
 
-	data.output = fopen(data.filename, "we");
-	if (data.output == NULL) {
-		fprintf(stderr, "Can't open file %s: %m\n", data.filename);
-		return -1;
+	if (!data.json_dump) {
+		data.output = fopen(data.filename, "we");
+		if (data.output == NULL) {
+			fprintf(stderr, "Can't open file %s: %m\n", data.filename);
+			return -1;
+		}
+		fprintf(stderr, "Logging to %s\n", data.filename);
+	} else {
+		printf("[");
 	}
 
-	printf("Logging to %s\n", data.filename);
-
 	pw_core_add_listener(data.core,
 				   &data.core_listener,
 				   &core_events, &data);
@@ -635,9 +788,12 @@ int main(int argc, char *argv[])
 	pw_context_destroy(data.context);
 	pw_main_loop_destroy(data.loop);
 
-	fclose(data.output);
-
-	dump_scripts(&data);
+	if (!data.json_dump) {
+		fclose(data.output);
+		dump_scripts(&data);
+	} else {
+		printf("{ } ]\n");
+	}
 
 	pw_deinit();
 
diff --git a/src/tools/pw-top.c b/src/tools/pw-top.c
index dc99ef0a..fc4da923 100644
--- a/src/tools/pw-top.c
+++ b/src/tools/pw-top.c
@@ -8,6 +8,8 @@
 #include <locale.h>
 #include <ncurses.h>
 
+#undef clear
+
 #include <spa/utils/result.h>
 #include <spa/utils/string.h>
 #include <spa/pod/parser.h>
@@ -233,8 +235,7 @@ static void node_param(void *data, int seq,
 				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_type_audio_format_to_short_name(info.format),
 						info.channels, info.rate);
 				}
 				break;
@@ -272,7 +273,7 @@ static void node_param(void *data, int seq,
 				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),
+						spa_type_video_format_to_short_name(info.format),
 						info.size.width, info.size.height);
 				}
 				break;
@@ -869,8 +870,8 @@ int main(int argc, char *argv[])
 
 	data.core = pw_context_connect(data.context,
 			pw_properties_new(
-				PW_KEY_REMOTE_NAME, opt_remote ? opt_remote :
-					("[" PW_DEFAULT_REMOTE "-manager," PW_DEFAULT_REMOTE "]"),
+				PW_KEY_REMOTE_INTENTION, "manager",
+				PW_KEY_REMOTE_NAME, opt_remote,
 				NULL),
 			0);
 	if (data.core == NULL) {
diff --git a/subprojects/webrtc-audio-processing.wrap b/subprojects/webrtc-audio-processing.wrap
index 1382212d..593acd83 100644
--- a/subprojects/webrtc-audio-processing.wrap
+++ b/subprojects/webrtc-audio-processing.wrap
@@ -2,7 +2,7 @@
 directory = webrtc-audio-processing
 url = https://gitlab.freedesktop.org/pulseaudio/webrtc-audio-processing.git
 push-url = git@gitlab.freedesktop.org:pulseaudio/webrtc-audio-processing.git
-revision = v1.3
+revision = v2.1
 
 [provide]
-dependency_names = webrtc-audio-coding-1, webrtc-audio-processing-1
+dependency_names = webrtc-audio-processing-2
diff --git a/subprojects/wireplumber.wrap b/subprojects/wireplumber.wrap
index 0153b2ac..6527259f 100644
--- a/subprojects/wireplumber.wrap
+++ b/subprojects/wireplumber.wrap
@@ -1,3 +1,3 @@
 [wrap-git]
 url = https://gitlab.freedesktop.org/pipewire/wireplumber.git
-revision = head
+revision = master
diff --git a/test/meson.build b/test/meson.build
index 75ae9602..43a59a25 100644
--- a/test/meson.build
+++ b/test/meson.build
@@ -111,6 +111,7 @@ endif
 test('test-spa',
     executable('test-spa',
                'test-spa-buffer.c',
+               'test-spa-control.c',
                'test-spa-json.c',
                'test-spa-utils.c',
                'test-spa-log.c',
diff --git a/test/test-example.c b/test/test-example.c
index 60d3392d..c080168f 100644
--- a/test/test-example.c
+++ b/test/test-example.c
@@ -182,7 +182,9 @@ PWTEST(daemon_test)
         core = pw_context_connect(ctx, NULL, 0);
 	pwtest_ptr_notnull(core);
 
+	pw_loop_enter(loop);
 	pw_loop_iterate(loop, -1);
+	pw_loop_leave(loop);
 	pw_core_disconnect(core);
         pw_context_destroy(ctx);
 	pw_loop_destroy(loop);
@@ -205,7 +207,9 @@ PWTEST(daemon_test_without_daemon)
 
 	pwtest_ptr_notnull(core); /* Expect this to fail because we don't have a daemon */
 
+	pw_loop_enter(loop);
 	pw_loop_iterate(loop, -1);
+	pw_loop_leave(loop);
 	pw_core_disconnect(core);
         pw_context_destroy(ctx);
 	pw_loop_destroy(loop);
diff --git a/test/test-spa-control.c b/test/test-spa-control.c
new file mode 100644
index 00000000..d493b293
--- /dev/null
+++ b/test/test-spa-control.c
@@ -0,0 +1,173 @@
+/* Simple Plugin API */
+/* SPDX-FileCopyrightText: Copyright © 2024 Wim Taymans. */
+/* SPDX-License-Identifier: MIT */
+
+#include "pwtest.h"
+
+#include <spa/control/control.h>
+#include <spa/control/ump-utils.h>
+
+PWTEST(control_abi_types)
+{
+	/* contol */
+	pwtest_int_eq(SPA_CONTROL_Invalid, 0);
+	pwtest_int_eq(SPA_CONTROL_Properties, 1);
+	pwtest_int_eq(SPA_CONTROL_Midi, 2);
+	pwtest_int_eq(SPA_CONTROL_OSC, 3);
+	pwtest_int_eq(SPA_CONTROL_UMP, 4);
+	pwtest_int_eq(_SPA_CONTROL_LAST, 5);
+
+	return PWTEST_PASS;
+}
+
+static inline uint32_t tohex(char v)
+{
+	if (v >= '0' && v <= '9')
+		return v - '0';
+	if (v >= 'a' && v <= 'f')
+		return v - 'a' + 10;
+	return 0;
+}
+
+static size_t parse_midi(const char *midi, uint8_t *data, size_t max_size)
+{
+	size_t size = 0;
+	while (*midi) {
+		while (*midi == ' ')
+			midi++;
+		data[size++] = tohex(*(midi+0)) << 4 |
+				tohex(*(midi+1));
+		midi+=2;
+	}
+	return size;
+}
+
+static size_t parse_ump(const char *ump, uint32_t *data, size_t max_size)
+{
+	size_t size = 0;
+	while (*ump) {
+		while (*ump == ' ')
+			ump++;
+		data[size++] = tohex(*(ump+0)) << 28 |
+				tohex(*(ump+1)) << 24 |
+				tohex(*(ump+2)) << 20 |
+				tohex(*(ump+3)) << 16 |
+				tohex(*(ump+4)) << 12 |
+				tohex(*(ump+5)) << 8 |
+				tohex(*(ump+6)) << 4 |
+				tohex(*(ump+7));
+		ump+=8;
+	}
+	return size * 4;
+}
+
+static int do_midi_to_ump_test(char *midi, char *ump)
+{
+	int i;
+	size_t m_size, u_size, u_offs = 0;
+	uint8_t *m_data = alloca(strlen(midi) / 2);
+	uint32_t *u_data = alloca(strlen(ump) / 2);
+	uint64_t state = 0;
+
+	m_size = parse_midi(midi, m_data, sizeof(m_data));
+	u_size = parse_ump(ump, u_data, sizeof(u_data));
+
+	while (m_size > 0) {
+		uint32_t ump[4];
+		fprintf(stdout, "%zd %08x\n", m_size, *m_data);
+		int ump_size = spa_ump_from_midi(&m_data, &m_size,
+				ump, sizeof(ump), 0, &state);
+		if (ump_size <= 0)
+			return -1;
+
+		if (u_size <= u_offs)
+			return -1;
+
+		for (i = 0; i < ump_size / 4; i++) {
+			fprintf(stdout, "%08x %08x\n", u_data[u_offs], ump[i]);
+			spa_assert(u_data[u_offs++] == ump[i]);
+		}
+	}
+	return 0;
+}
+
+PWTEST(control_midi_to_ump)
+{
+	/* sysex */
+	do_midi_to_ump_test("f0 f7",
+			"30000000 00000000");
+
+	do_midi_to_ump_test("f0 01 02 03 04 05 f7",
+			"30050102 03040500");
+
+	do_midi_to_ump_test("f0 01 02 03 04 05 06 f7",
+			"30060102 03040506");
+	do_midi_to_ump_test("f0 01 02 03 04 05 06 07 f7",
+			"30160102 03040506 30310700 00000000");
+	do_midi_to_ump_test("f0 01 02 03 04 05 06 07 08 09 10 11 12 13 f7",
+			"30160102 03040506 30260708 09101112 30311300 00000000");
+
+	do_midi_to_ump_test("f0 01 02 03 04 05 06 f0",
+			"30160102 03040506");
+	do_midi_to_ump_test("f7 01 02 03 04 05 06 07 08 f0",
+			"30260102 03040506 30220708 00000000");
+	do_midi_to_ump_test("f7 01 02 03 04 05 06 07 08 09 f7",
+			"30260102 03040506 30330708 09000000");
+
+	return PWTEST_PASS;
+}
+
+static int do_ump_to_midi_test(char *ump, char *midi)
+{
+	int i;
+	size_t m_size, u_size, m_offs = 0;
+	uint8_t *m_data = alloca(strlen(midi) / 2);
+	uint32_t *u_data = alloca(strlen(ump) / 2);
+
+	u_size = parse_ump(ump, u_data, sizeof(u_data));
+	m_size = parse_midi(midi, m_data, sizeof(m_data));
+
+	spa_assert(u_size > 0);
+	spa_assert(m_size > 0);
+
+	while (u_size > 0) {
+		uint8_t midi[32];
+		fprintf(stdout, "%zd %08x\n", u_size, *u_data);
+		int midi_size = spa_ump_to_midi(u_data, u_size,
+				midi, sizeof(midi));
+		if (midi_size <= 0)
+			return midi_size;
+
+		if (m_size <= m_offs)
+			return -1;
+
+		for (i = 0; i < midi_size; i++) {
+			fprintf(stdout, "%08x %08x\n", m_data[m_offs], midi[i]);
+			spa_assert(m_data[m_offs++] == midi[i]);
+		}
+		u_size -= spa_ump_message_size(*u_data >> 28) * 4;
+		u_data += spa_ump_message_size(*u_data >> 28);
+	}
+	return 0;
+}
+
+PWTEST(control_ump_to_midi)
+{
+	spa_assert(do_ump_to_midi_test("30000000 00000000",
+			"f0 f7") >= 0);
+	spa_assert(do_ump_to_midi_test("30050102 03040500",
+			"f0 01 02 03 04 05 f7") >= 0);
+
+	spa_assert(do_ump_to_midi_test("30160102 03040506 30260708 09101112 30311300 00000000",
+				"f0 01 02 03 04 05 06 07 08 09 10 11 12 13 f7") >= 0);
+	return PWTEST_PASS;
+}
+
+PWTEST_SUITE(spa_buffer)
+{
+	pwtest_add(control_abi_types, PWTEST_NOARG);
+	pwtest_add(control_midi_to_ump, PWTEST_NOARG);
+	pwtest_add(control_ump_to_midi, PWTEST_NOARG);
+
+	return PWTEST_PASS;
+}
diff --git a/test/test-spa-json.c b/test/test-spa-json.c
index 869a2040..b07acc00 100644
--- a/test/test-spa-json.c
+++ b/test/test-spa-json.c
@@ -1065,6 +1065,38 @@ PWTEST(json_data)
 	return PWTEST_PASS;
 }
 
+PWTEST(json_object_find)
+{
+	const char *json = " { "
+		"\"foo\": \"bar\","
+		"\"int-key\": 42,"
+		"\"list-key\": [],"
+		"\"obj-key\": {},"
+		"\"bool-key\": true,"
+		"\"float-key\": 66.6"
+		" } ";
+	char value[128];
+
+	pwtest_int_eq(spa_json_str_object_find(json, strlen(json), "unknown-key", value, 128), -2);
+	pwtest_int_eq(spa_json_str_object_find("{", 1, "key", value, 128), -2);
+	pwtest_int_eq(spa_json_str_object_find("this is no json", 15, "key", value, 128), -22);
+	pwtest_int_eq(spa_json_str_object_find(json, strlen(json), "foo", value, 128), 1);
+	pwtest_str_eq(value, "bar");
+	pwtest_int_eq(spa_json_str_object_find(json, strlen(json), "int-key", value, 128), 1);
+	pwtest_str_eq(value, "42");
+	pwtest_int_eq(spa_json_str_object_find(json, strlen(json), "list-key", value, 128), 1);
+	pwtest_str_eq(value, "[");
+	pwtest_int_eq(spa_json_str_object_find(json, strlen(json), "obj-key", value, 128), 1);
+	pwtest_str_eq(value, "{");
+	pwtest_int_eq(spa_json_str_object_find(json, strlen(json), "bool-key", value, 128), 1);
+	pwtest_str_eq(value, "true");
+	pwtest_int_eq(spa_json_str_object_find(json, strlen(json), "float-key", value, 128), 1);
+	pwtest_str_eq(value, "66.6");
+
+	return PWTEST_PASS;
+}
+
+
 PWTEST_SUITE(spa_json)
 {
 	pwtest_add(json_abi, PWTEST_NOARG);
@@ -1077,6 +1109,7 @@ PWTEST_SUITE(spa_json)
 	pwtest_add(json_float_check, PWTEST_NOARG);
 	pwtest_add(json_int, PWTEST_NOARG);
 	pwtest_add(json_data, PWTEST_NOARG);
+	pwtest_add(json_object_find, PWTEST_NOARG);
 
 	return PWTEST_PASS;
 }
-- 
GitLab


From f2154c1e6bd6b596b14bd079d9b02e45cf32ddbf Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Dylan=20A=C3=AFssi?= <dylan.aissi@collabora.com>
Date: Wed, 12 Mar 2025 16:46:26 +0100
Subject: [PATCH 2/5] Inject -Wno-error=format-overflow
 -Wno-error=format-truncation in debian/rules
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

In contrary to Debian, we use by default -Wformat-overflow=2
and -Wformat-truncation=2 in Apertis, but due to the use of -Werror this
package FTBFS with: "cc1: all warnings being treated as errors".
In order to avoid this error, we don't treat these warnings as errors.

../src/pipewire/conf.c:283:44: error: ‘/’ directive output may be truncated writing 1 byte into a region of size between 0 and 1 [-Werror=format-truncation=]

Signed-off-by: Dylan Aïssi <dylan.aissi@collabora.com>
---
 debian/rules | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/debian/rules b/debian/rules
index 776eefd0..b6f5f200 100755
--- a/debian/rules
+++ b/debian/rules
@@ -3,6 +3,13 @@
 export DEB_BUILD_MAINT_OPTIONS = hardening=+all
 export DEB_LDFLAGS_MAINT_APPEND = -Wl,-z,defs
 
+# In contrary to Debian, we use by default -Wformat-overflow=2 -Wformat-truncation=2
+# in Apertis, but due to the use of -Werror this package FTBFS with:
+# "cc1: all warnings being treated as errors". In order to avoid this error, we
+# don't treat these warnings as errors.
+export DEB_CFLAGS_MAINT_APPEND = -Wno-error=format-overflow -Wno-error=format-truncation
+export DEB_CXXFLAGS_MAINT_APPEND = -Wno-error=format-overflow -Wno-error=format-truncation
+
 %:
 	dh $@ -Nlibspa-0.2-jack
 
-- 
GitLab


From 8cc3595aa03487ff424835f88c92029addece993 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Dylan=20A=C3=AFssi?= <dylan.aissi@collabora.com>
Date: Wed, 12 Mar 2025 18:30:45 +0100
Subject: [PATCH 3/5] dpkg-shlibdeps: exclude ump-source
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

It depends on libjack.so.0 which is not in a standard folder.
The same issue is probably hidden in Debian because in contrary
to Apertis, the libjack-jackd2-dev is still in the Build-deps,
thus libjack.so.0 is available from the package libjack-jackd2-0.

Some work are required in Debian to make pipewire-jack the new
jackd3...

Signed-off-by: Dylan Aïssi <dylan.aissi@collabora.com>
---
 debian/rules | 1 +
 1 file changed, 1 insertion(+)

diff --git a/debian/rules b/debian/rules
index b6f5f200..1c4462ae 100755
--- a/debian/rules
+++ b/debian/rules
@@ -138,4 +138,5 @@ override_dh_shlibdeps-arch:
 	dh_shlibdeps \
 		--remaining-packages \
 		-l/usr/lib/$(DEB_HOST_MULTIARCH)/pipewire-0.3 \
+		-Xexamples/jack/ump-source \
 		$(NULL)
-- 
GitLab


From 5461006b0fca2aded58e8be5fe9e53a02fe3e8d5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Dylan=20A=C3=AFssi?= <dylan.aissi@collabora.com>
Date: Mon, 10 Mar 2025 14:45:10 +0100
Subject: [PATCH 4/5] Release pipewire version 1.4.0-1+apertis1
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Dylan Aïssi <dylan.aissi@collabora.com>
---
 debian/changelog | 24 ++++++++++++++++++++++++
 1 file changed, 24 insertions(+)

diff --git a/debian/changelog b/debian/changelog
index 3fc5769d..c7618230 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,27 @@
+pipewire (1.4.0-1+apertis1) apertis; urgency=medium
+
+  * Sync from debian/trixie.
+  * Remaining Apertis specific changes:
+    - Continue to disable build-depends:
+      - libffado-dev (not needed)
+      - libfreeaptx-dev (proprietary codec, unknown legal status)
+      - libjack-jackd2-dev (not needed)
+      - liblilv-dev (not needed)
+      - libmysofa-dev (not needed)
+      - libsdl2 (not needed)
+      - libsnapd-glib-dev (not needed)
+      - libxfixes-dev (not needed)
+    - Replace libreadline-dev by libeditreadline-dev in Build-Deps
+    - Install AppArmor rules
+    - Inject -Wno-error=format-overflow -Wno-error=format-truncation in
+      debian/rules. In contrary to Debian, we use by default -Wformat-overflow=2
+      and -Wformat-truncation=2 in Apertis, but due to the use of -Werror this
+      package FTBFS with: "cc1: all warnings being treated as errors".
+      In order to avoid this error, we don't treat these warnings as errors.
+    - dpkg-shlibdeps: exclude the binary example ump-source
+
+ -- Dylan Aïssi <dylan.aissi@collabora.com>  Mon, 10 Mar 2025 14:44:42 +0100
+
 pipewire (1.4.0-1) unstable; urgency=medium
 
   * New upstream release
-- 
GitLab


From caa3aaa6dabfc91705706e224ef0f3ddad1800da Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Dylan=20A=C3=AFssi?= <dylan.aissi@collabora.com>
Date: Mon, 10 Mar 2025 13:53:00 +0000
Subject: [PATCH 5/5] Refresh the automatically detected licensing information
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Dylan Aïssi <dylan.aissi@collabora.com>
---
 debian/apertis/copyright | 97 ++++++++++++++++++++++++++++++----------
 1 file changed, 73 insertions(+), 24 deletions(-)

diff --git a/debian/apertis/copyright b/debian/apertis/copyright
index ffd19efc..0119075f 100644
--- a/debian/apertis/copyright
+++ b/debian/apertis/copyright
@@ -26,6 +26,22 @@ Copyright: 2021, jothepro
  2000-2002, Richard W.E. Furse, Paul Barton-Davis
 License: Expat
 
+Files: spa/plugins/bluez5/g722/*
+Copyright: 2004-2010 Marcel Holtmann
+ 2006-2010 Nokia Corporation
+ 2016-2017 Arkadiusz Bokowy
+ 2018-2022 Wim Taymans
+ 2018-2022 Collabora Ltd.
+ 2018 Pali Rohár
+ 2021-2022 Pauli Virtanen
+ 2013 Julien Pommier
+License: Expat and LGPL-2.1+
+
+Files: debian/tests/gstreamer1.0-pipewire
+ debian/tests/libpipewire-0.3-dev
+Copyright: 2018-2021, 2024, Collabora Ltd.
+License: Expat
+
 Files: doc/doxygen-awesome.css
 Copyright: 2021, jothepro
 License: Expat
@@ -95,9 +111,18 @@ Files: pipewire-jack/src/control.c
 Copyright: 2021, Florian Hülsmann <fh@cbix.de>
 License: Expat
 
-Files: spa/examples/adapter-control.c
- spa/examples/local-libcamera.c
-Copyright: 2018-2021, Collabora Ltd.
+Files: pipewire-jack/src/pipewire-jack.c
+Copyright: 2024, Nedko Arnaudov
+ 2018, Wim Taymans <wim.taymans@gmail.com>
+License: Expat
+
+Files: spa/examples/*
+Copyright: 2018-2021, 2024, Collabora Ltd.
+License: Expat
+
+Files: spa/examples/example-control.c
+ spa/examples/local-v4l2.c
+Copyright: 2015-2024, Wim Taymans <wim.taymans@gmail.com>
 License: Expat
 
 Files: spa/include-private/*
@@ -105,7 +130,7 @@ Copyright: 2023, PipeWire authors
 License: Expat
 
 Files: spa/include/spa/monitor/type-info.h
-Copyright: 2018-2021, Collabora Ltd.
+Copyright: 2018-2021, 2024, Collabora Ltd.
 License: Expat
 
 Files: spa/include/spa/param/audio/compressed.h
@@ -186,6 +211,14 @@ Files: spa/plugins/alsa/mixer/profile-sets/kinect-audio.conf
 Copyright: 2011, Antonio Ospite <ospite@studenti.unina.it>
 License: LGPL-2.1+
 
+Files: spa/plugins/audioconvert/fmt-ops-rvv.c
+Copyright: 2023, Institue of Software Chinese Academy of Sciences (ISCAS).
+License: Expat
+
+Files: spa/plugins/audioconvert/spa-resample-dump-coeffs.c
+Copyright: 2024, Arun Raghavan <arun@asymptotic.io>
+License: Expat
+
 Files: spa/plugins/bluez5/*
 Copyright: 2021-2024, Pauli Virtanen <pav@iki.fi>
 License: Expat
@@ -194,6 +227,7 @@ Files: spa/plugins/bluez5/a2dp-codec-aac.c
  spa/plugins/bluez5/a2dp-codec-aptx.c
  spa/plugins/bluez5/a2dp-codec-ldac.c
  spa/plugins/bluez5/a2dp-codec-sbc.c
+ spa/plugins/bluez5/asha-codec-g722.c
  spa/plugins/bluez5/bluez5-dbus.c
  spa/plugins/bluez5/bluez5-device.c
  spa/plugins/bluez5/codec-loader.c
@@ -228,7 +262,9 @@ Files: spa/plugins/bluez5/backend-hsphfpd.c
  spa/plugins/bluez5/sco-io.c
  spa/plugins/bluez5/sco-sink.c
  spa/plugins/bluez5/sco-source.c
-Copyright: 2018-2021, Collabora Ltd.
+ spa/plugins/bluez5/telephony.c
+ spa/plugins/bluez5/telephony.h
+Copyright: 2018-2021, 2024, Collabora Ltd.
 License: Expat
 
 Files: spa/plugins/bluez5/backend-native.c
@@ -267,6 +303,15 @@ Files: spa/plugins/bluez5/rtp.h
 Copyright: 2004-2010, Marcel Holtmann <marcel@holtmann.org>
 License: LGPL-2.1+
 
+Files: spa/plugins/filter-graph/convolver.c
+Copyright: 2021, Wim Taymans <wim.taymans@gmail.com>
+ 2017, HiFi-LoFi
+License: Expat
+
+Files: spa/plugins/filter-graph/ladspa.h
+Copyright: 2000-2002, Richard W.E. Furse, Paul Barton-Davis
+License: LGPL-2.1+
+
 Files: spa/plugins/libcamera/libcamera-device.cpp
  spa/plugins/libcamera/libcamera-utils.cpp
 Copyright: 2019, 2020, 2024, Collabora Ltd.
@@ -274,7 +319,7 @@ Copyright: 2019, 2020, 2024, Collabora Ltd.
 License: Expat
 
 Files: spa/plugins/libcamera/libcamera-source.cpp
-Copyright: 2018-2021, Collabora Ltd.
+Copyright: 2018-2021, 2024, Collabora Ltd.
 License: Expat
 
 Files: spa/plugins/libcamera/libcamera.c
@@ -282,10 +327,19 @@ Files: spa/plugins/libcamera/libcamera.c
 Copyright: 2020, collabora
 License: Expat
 
+Files: spa/plugins/support/cpu-riscv.c
+Copyright: 2023, Institue of Software Chinese Academy of Sciences (ISCAS).
+License: Expat
+
 Files: spa/plugins/support/journal.c
 Copyright: 2020, Sergey Bugaev
 License: Expat
 
+Files: spa/plugins/videoconvert/videoconvert-dummy.c
+Copyright: 2023, columbarius
+ 2019, Wim Taymans <wim.taymans@gmail.com>
+License: Expat
+
 Files: spa/plugins/vulkan/dmabuf.h
  spa/plugins/vulkan/dmabuf_fallback.c
  spa/plugins/vulkan/dmabuf_linux.c
@@ -331,15 +385,6 @@ Copyright: 2021, Wim Taymans <wim.taymans@gmail.com>
  2021, Arun Raghavan <arun@asymptotic.io>
 License: Expat
 
-Files: src/modules/module-filter-chain/convolver.c
-Copyright: 2021, Wim Taymans <wim.taymans@gmail.com>
- 2017, HiFi-LoFi
-License: Expat
-
-Files: src/modules/module-filter-chain/ladspa.h
-Copyright: 2000-2002, Richard W.E. Furse, Paul Barton-Davis
-License: LGPL-2.1+
-
 Files: src/modules/module-jackdbus-detect.c
  src/modules/module-portal.c
 Copyright: 2019, Red Hat Inc.
@@ -422,20 +467,20 @@ Copyright: 2024, Dmitry Sharshakov <d3dx12.xx@gmail.com>
 License: Expat
 
 Files: src/modules/module-session-manager.c
-Copyright: 2018-2021, Collabora Ltd.
+Copyright: 2018-2021, 2024, Collabora Ltd.
 License: Expat
 
 Files: src/modules/module-session-manager/*
-Copyright: 2018-2021, Collabora Ltd.
+Copyright: 2018-2021, 2024, Collabora Ltd.
 License: Expat
 
-Files: src/modules/spa/module-node.c
+Files: src/modules/module-spa-node.c
 Copyright: 2018, Wim Taymans <wim.taymans@gmail.com>
  2016, Axis Communications <dev-gstreamer@axis.com>
 License: Expat
 
 Files: src/pipewire/extensions/*
-Copyright: 2018-2021, Collabora Ltd.
+Copyright: 2018-2021, 2024, Collabora Ltd.
 License: Expat
 
 Files: src/pipewire/extensions/client-node.h
@@ -453,7 +498,7 @@ Copyright: 2018, Wim Taymans <wim.taymans@gmail.com>
 License: Expat
 
 Files: src/tests/test-endpoint.c
-Copyright: 2018-2021, Collabora Ltd.
+Copyright: 2018-2021, 2024, Collabora Ltd.
 License: Expat
 
 Files: src/tools/pw-cat.c
@@ -461,7 +506,7 @@ Copyright: 2020, Konsulko Group
 License: Expat
 
 Files: src/tools/pw-dot.c
-Copyright: 2018-2021, Collabora Ltd.
+Copyright: 2018-2021, 2024, Collabora Ltd.
 License: Expat
 
 Files: test/*
@@ -491,10 +536,14 @@ License: Expat
 
 Files: test/test-spa-buffer.c
  test/test-spa-utils.c
-Copyright: 2018-2021, Collabora Ltd.
+Copyright: 2018-2021, 2024, Collabora Ltd.
+License: Expat
+
+Files: test/test-spa-control.c
+Copyright: 2024, Wim Taymans.
 License: Expat
 
-Files: doc/dox/config/pipewire-props.7.md spa/plugins/audioconvert/biquad.c spa/plugins/audioconvert/biquad.h spa/plugins/audioconvert/crossover.c spa/plugins/audioconvert/crossover.h src/modules/module-filter-chain/biquad.c src/modules/module-filter-chain/biquad.h
+Files: doc/dox/config/pipewire-props.7.md spa/plugins/audioconvert/biquad.c spa/plugins/audioconvert/biquad.h spa/plugins/audioconvert/crossover.c spa/plugins/audioconvert/crossover.h spa/plugins/filter-graph/biquad.h
 Copyright: 2009 Lennart Poettering
  2010 David Henningsson
  2013 Inigo Quilez
@@ -516,7 +565,7 @@ Copyright: 2009 Lennart Poettering
  2021 Florian Hülsmann
 License: Expat
 
-Files: src/modules/module-filter-chain/pffft.c src/modules/module-filter-chain/pffft.h
+Files: spa/plugins/filter-graph/pffft.c spa/plugins/filter-graph/pffft.h
 Copyright: 2013 Julien Pommier
  2004 The University Corporation for Atmospheric Research
 License: FFTPACK
-- 
GitLab